diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6be4fefe..b7ce1bff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Cache Homebrew packages uses: actions/cache@v4 @@ -79,6 +81,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: recursive # pnpm must be set up before setup-node so the built-in cache can locate the store. - uses: pnpm/action-setup@v4 diff --git a/.gitignore b/.gitignore index ec7eb483..92ed4021 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ _deps CMakeUserPresets.json Build/ build/ +build-tsan/ .cache/ .vs/ .idea/ diff --git a/.gitmodules b/.gitmodules index e69de29b..406d4676 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ThirdParty/uWebSockets"] + path = ThirdParty/uWebSockets + url = https://github.com/uNetworking/uWebSockets.git diff --git a/Axiom/Assets/AssetCookManifest.cpp b/Axiom/Assets/AssetCookManifest.cpp index e46611d8..6a25d8f7 100644 --- a/Axiom/Assets/AssetCookManifest.cpp +++ b/Axiom/Assets/AssetCookManifest.cpp @@ -2,34 +2,15 @@ #include "Core/Log.h" -#include -#include +#include +#include +#include + #include -#include -#include namespace Axiom::Assets { namespace { -std::string EscapeJson(std::string_view Value) { - std::string Out; - Out.reserve(Value.size() + 2); - Out += '"'; - for (char Character : Value) { - if (Character == '"') { - Out += "\\\""; - } else if (Character == '\\') { - Out += "\\\\"; - } else if (Character == '\n') { - Out += "\\n"; - } else { - Out += Character; - } - } - Out += '"'; - return Out; -} - const char *AssetKindToString(AssetKind Kind) { switch (Kind) { case AssetKind::Mesh: @@ -44,246 +25,87 @@ const char *AssetKindToString(AssetKind Kind) { } AssetKind AssetKindFromString(std::string_view Value) { - if (Value == "mesh") + if (Value == "mesh") { return AssetKind::Mesh; - if (Value == "texture") + } + if (Value == "texture") { return AssetKind::Texture; - if (Value == "material") + } + if (Value == "material") { return AssetKind::Material; + } return AssetKind::Unknown; } -struct Parser { - std::string_view Src; - size_t Pos{0}; - - char Peek() const { return Pos < Src.size() ? Src[Pos] : '\0'; } +} // namespace - void SkipWs() { - while (Pos < Src.size() && - std::isspace(static_cast(Src[Pos])) != 0) { - ++Pos; - } +std::optional +LoadAssetCookManifest(const std::filesystem::path &Path) { + std::ifstream File(Path); + if (!File.is_open()) { + return std::nullopt; } - bool Expect(char Character) { - SkipWs(); - if (Peek() != Character) - return false; - ++Pos; - return true; + std::string Text((std::istreambuf_iterator(File)), + std::istreambuf_iterator()); + rapidjson::Document Document; + Document.ParseInsitu(Text.data()); + if (Document.HasParseError() || !Document.IsObject()) { + A_CORE_WARN("AssetCookManifest: failed to parse '{}'", Path.string()); + return std::nullopt; } - std::optional ParseString() { - SkipWs(); - if (Peek() != '"') - return std::nullopt; - ++Pos; - - std::string Out; - while (Pos < Src.size()) { - const char Character = Src[Pos++]; - if (Character == '"') - return Out; - if (Character == '\\') { - if (Pos >= Src.size()) - return std::nullopt; - const char Escaped = Src[Pos++]; - if (Escaped == 'n') { - Out += '\n'; - } else { - Out += Escaped; - } - } else { - Out += Character; - } - } - + AssetCookManifest Manifest; + const auto EntriesIt = Document.FindMember("entries"); + if (EntriesIt == Document.MemberEnd()) { + return Manifest; + } + if (!EntriesIt->value.IsArray()) { + A_CORE_WARN("AssetCookManifest: failed to parse '{}'", Path.string()); return std::nullopt; } - std::optional ParseUint64() { - SkipWs(); - const size_t Start = Pos; - while (Pos < Src.size() && - std::isdigit(static_cast(Src[Pos])) != 0) { - ++Pos; - } - if (Start == Pos) + for (const auto &EntryValue : EntriesIt->value.GetArray()) { + if (!EntryValue.IsObject()) { + A_CORE_WARN("AssetCookManifest: failed to parse '{}'", Path.string()); return std::nullopt; - - uint64_t Value = 0; - for (size_t Index = Start; Index < Pos; ++Index) { - Value = Value * 10u + static_cast(Src[Index] - '0'); } - return Value; - } - void SkipValue() { - SkipWs(); - const char Character = Peek(); - if (Character == '"') { - ParseString(); - return; - } - if (Character == '{') { - SkipObject(); - return; - } - if (Character == '[') { - SkipArray(); - return; - } - if (Src.substr(Pos, 4) == "null") { - Pos += 4; - return; + AssetCookManifestEntry Entry; + if (const auto AssetIdIt = EntryValue.FindMember("assetId"); + AssetIdIt != EntryValue.MemberEnd() && AssetIdIt->value.IsUint64()) { + Entry.Id = AssetId{AssetIdIt->value.GetUint64()}; } - ParseUint64(); - } - - void SkipObject() { - if (!Expect('{')) - return; - SkipWs(); - if (Peek() == '}') { - ++Pos; - return; + if (const auto KindIt = EntryValue.FindMember("kind"); + KindIt != EntryValue.MemberEnd() && KindIt->value.IsString()) { + Entry.Kind = AssetKindFromString( + std::string_view(KindIt->value.GetString(), + KindIt->value.GetStringLength())); } - do { - ParseString(); - Expect(':'); - SkipValue(); - SkipWs(); - } while (Expect(',')); - Expect('}'); - } - - void SkipArray() { - if (!Expect('[')) - return; - SkipWs(); - if (Peek() == ']') { - ++Pos; - return; + if (const auto RelativePathIt = EntryValue.FindMember("relativePath"); + RelativePathIt != EntryValue.MemberEnd() && + RelativePathIt->value.IsString()) { + Entry.RelativePath.assign(RelativePathIt->value.GetString(), + RelativePathIt->value.GetStringLength()); } - do { - SkipValue(); - SkipWs(); - } while (Expect(',')); - Expect(']'); - } - - template bool ParseObject(HandlerFn Handler) { - if (!Expect('{')) - return false; - SkipWs(); - if (Peek() == '}') { - ++Pos; - return true; + if (const auto CookedPathIt = EntryValue.FindMember("cookedPath"); + CookedPathIt != EntryValue.MemberEnd() && + CookedPathIt->value.IsString()) { + Entry.CookedPath.assign(CookedPathIt->value.GetString(), + CookedPathIt->value.GetStringLength()); } - - do { - auto Key = ParseString(); - if (!Key.has_value()) - return false; - if (!Expect(':')) - return false; - if (!Handler(*Key)) - SkipValue(); - SkipWs(); - } while (Expect(',')); - - return Expect('}'); - } - - template bool ParseArray(HandlerFn Handler) { - if (!Expect('[')) - return false; - SkipWs(); - if (Peek() == ']') { - ++Pos; - return true; + if (const auto FormatVersionIt = EntryValue.FindMember("formatVersion"); + FormatVersionIt != EntryValue.MemberEnd() && + FormatVersionIt->value.IsUint()) { + Entry.FormatVersion = FormatVersionIt->value.GetUint(); } - - do { - if (!Handler()) - return false; - SkipWs(); - } while (Expect(',')); - - return Expect(']'); - } -}; - -} // namespace - -std::optional -LoadAssetCookManifest(const std::filesystem::path &Path) { - std::ifstream File(Path); - if (!File.is_open()) { - return std::nullopt; - } - - const std::string Text((std::istreambuf_iterator(File)), - std::istreambuf_iterator()); - Parser P{Text}; - AssetCookManifest Manifest; - - const bool Parsed = P.ParseObject([&](const std::string &Key) -> bool { - if (Key == "entries") { - return P.ParseArray([&]() -> bool { - AssetCookManifestEntry Entry; - const bool EntryParsed = P.ParseObject([&](const std::string &EntryKey) -> bool { - if (EntryKey == "assetId") { - auto Value = P.ParseUint64(); - if (Value.has_value()) - Entry.Id = AssetId{*Value}; - return true; - } - if (EntryKey == "kind") { - auto Value = P.ParseString(); - if (Value.has_value()) - Entry.Kind = AssetKindFromString(*Value); - return true; - } - if (EntryKey == "relativePath") { - auto Value = P.ParseString(); - if (Value.has_value()) - Entry.RelativePath = *Value; - return true; - } - if (EntryKey == "cookedPath") { - auto Value = P.ParseString(); - if (Value.has_value()) - Entry.CookedPath = *Value; - return true; - } - if (EntryKey == "formatVersion") { - auto Value = P.ParseUint64(); - if (Value.has_value()) - Entry.FormatVersion = static_cast(*Value); - return true; - } - if (EntryKey == "sourceHash") { - auto Value = P.ParseUint64(); - if (Value.has_value()) - Entry.SourceHash = *Value; - return true; - } - return false; - }); - if (!EntryParsed) - return false; - Manifest.Entries.push_back(std::move(Entry)); - return true; - }); + if (const auto SourceHashIt = EntryValue.FindMember("sourceHash"); + SourceHashIt != EntryValue.MemberEnd() && + SourceHashIt->value.IsUint64()) { + Entry.SourceHash = SourceHashIt->value.GetUint64(); } - return false; - }); - if (!Parsed) { - A_CORE_WARN("AssetCookManifest: failed to parse '{}'", Path.string()); - return std::nullopt; + Manifest.Entries.push_back(std::move(Entry)); } return Manifest; @@ -298,25 +120,48 @@ bool SaveAssetCookManifest(const std::filesystem::path &Path, return false; } - std::ostringstream Out; - Out << "{\n"; - Out << " \"entries\": [\n"; - for (size_t Index = 0; Index < Manifest.Entries.size(); ++Index) { - const auto &Entry = Manifest.Entries[Index]; - if (Index > 0) { - Out << ",\n"; - } - Out << " {\"assetId\":" << Entry.Id.Value - << ",\"kind\":" << EscapeJson(AssetKindToString(Entry.Kind)) - << ",\"relativePath\":" << EscapeJson(Entry.RelativePath) - << ",\"cookedPath\":" << EscapeJson(Entry.CookedPath) - << ",\"formatVersion\":" << Entry.FormatVersion - << ",\"sourceHash\":" << Entry.SourceHash << "}"; + rapidjson::Document Document; + Document.SetObject(); + auto &Allocator = Document.GetAllocator(); + + rapidjson::Value Entries(rapidjson::kArrayType); + Entries.Reserve(static_cast(Manifest.Entries.size()), + Allocator); + for (const auto &Entry : Manifest.Entries) { + rapidjson::Value EntryValue(rapidjson::kObjectType); + EntryValue.AddMember("assetId", Entry.Id.Value, Allocator); + EntryValue.AddMember( + "kind", + rapidjson::Value(AssetKindToString(Entry.Kind), Allocator).Move(), + Allocator); + EntryValue.AddMember( + "relativePath", + rapidjson::Value(Entry.RelativePath.c_str(), + static_cast( + Entry.RelativePath.size()), + Allocator) + .Move(), + Allocator); + EntryValue.AddMember( + "cookedPath", + rapidjson::Value(Entry.CookedPath.c_str(), + static_cast(Entry.CookedPath.size()), + Allocator) + .Move(), + Allocator); + EntryValue.AddMember("formatVersion", Entry.FormatVersion, Allocator); + EntryValue.AddMember("sourceHash", Entry.SourceHash, Allocator); + Entries.PushBack(EntryValue, Allocator); } - Out << "\n ]\n"; - Out << "}\n"; - File << Out.str(); + Document.AddMember("entries", Entries, Allocator); + + rapidjson::StringBuffer Buffer; + rapidjson::PrettyWriter Writer(Buffer); + Writer.SetIndent(' ', 2); + Document.Accept(Writer); + + File << Buffer.GetString() << '\n'; return File.good(); } diff --git a/Axiom/Assets/AssetCooker.cpp b/Axiom/Assets/AssetCooker.cpp index 400381d2..a9ab4ace 100644 --- a/Axiom/Assets/AssetCooker.cpp +++ b/Axiom/Assets/AssetCooker.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -101,7 +102,11 @@ CookMeshAsset(const std::filesystem::path &ContentRoot, MeshSceneData CookedScene = *Scene; std::vector MaterialAssetPaths(CookedScene.Instances.size()); + std::unordered_map SharedMaterialAssetPaths; + std::unordered_map SharedTextureAssetPaths; const std::string AssetStem = RelativeAssetPath.stem().generic_string(); + size_t NextSharedMaterialIndex = 0; + size_t NextSharedTextureIndex = 0; for (size_t InstanceIndex = 0; InstanceIndex < CookedScene.Instances.size(); ++InstanceIndex) { auto &Instance = CookedScene.Instances[InstanceIndex]; @@ -109,16 +114,31 @@ CookMeshAsset(const std::filesystem::path &ContentRoot, continue; } + if (const auto ExistingMaterial = + SharedMaterialAssetPaths.find(Instance.Material.get()); + ExistingMaterial != SharedMaterialAssetPaths.end()) { + MaterialAssetPaths[InstanceIndex] = ExistingMaterial->second; + continue; + } + std::string TextureAssetPath; if (Instance.Material->BaseColorTexture && Instance.Material->BaseColorTexture->IsValid()) { - const std::filesystem::path RelativeTexturePath = - std::filesystem::path("Generated/MeshTextures") / - (AssetStem + "__" + std::to_string(InstanceIndex)); - const auto TextureEntry = CookTextureAsset( - ContentRoot, RelativeTexturePath, *Instance.Material->BaseColorTexture); - if (TextureEntry.has_value()) { - TextureAssetPath = TextureEntry->RelativePath; + if (const auto ExistingTexture = + SharedTextureAssetPaths.find(Instance.Material->BaseColorTexture.get()); + ExistingTexture != SharedTextureAssetPaths.end()) { + TextureAssetPath = ExistingTexture->second; + } else { + const std::filesystem::path RelativeTexturePath = + std::filesystem::path("Generated/MeshTextures") / + (AssetStem + "__shared_" + std::to_string(NextSharedTextureIndex++)); + const auto TextureEntry = CookTextureAsset( + ContentRoot, RelativeTexturePath, *Instance.Material->BaseColorTexture); + if (TextureEntry.has_value()) { + TextureAssetPath = TextureEntry->RelativePath; + SharedTextureAssetPaths.emplace(Instance.Material->BaseColorTexture.get(), + TextureAssetPath); + } } } else if (!Instance.Material->TextureAssetPath.empty()) { const auto TextureEntry = @@ -130,7 +150,7 @@ CookMeshAsset(const std::filesystem::path &ContentRoot, const std::filesystem::path RelativeMaterialPath = std::filesystem::path("Generated/MeshMaterials") / - (AssetStem + "__" + std::to_string(InstanceIndex)); + (AssetStem + "__shared_" + std::to_string(NextSharedMaterialIndex++)); const auto MaterialEntry = CookMaterialAsset( ContentRoot, RelativeMaterialPath, {.BaseColorFactor = Instance.Material->BaseColorFactor, @@ -139,6 +159,8 @@ CookMeshAsset(const std::filesystem::path &ContentRoot, .TextureAssetPath = TextureAssetPath}); if (MaterialEntry.has_value()) { MaterialAssetPaths[InstanceIndex] = MaterialEntry->RelativePath; + SharedMaterialAssetPaths.emplace(Instance.Material.get(), + MaterialEntry->RelativePath); } } diff --git a/Axiom/Assets/AssimpImporter.cpp b/Axiom/Assets/AssimpImporter.cpp index 25db0dcf..05801c55 100644 --- a/Axiom/Assets/AssimpImporter.cpp +++ b/Axiom/Assets/AssimpImporter.cpp @@ -26,12 +26,12 @@ MeshData ConvertMesh(const aiMesh *AiMesh) { for (unsigned i = 0; i < AiMesh->mNumVertices; ++i) { MeshVertex V{}; V.Position = {AiMesh->mVertices[i].x, AiMesh->mVertices[i].y, - AiMesh->mVertices[i].z, 1.0f}; + AiMesh->mVertices[i].z}; if (AiMesh->HasNormals()) { V.Normal = {AiMesh->mNormals[i].x, AiMesh->mNormals[i].y, - AiMesh->mNormals[i].z, 0.0f}; + AiMesh->mNormals[i].z}; } else { - V.Normal = {0.0f, 1.0f, 0.0f, 0.0f}; + V.Normal = {0.0f, 1.0f, 0.0f}; } if (AiMesh->HasTextureCoords(0)) { V.TexCoord = {AiMesh->mTextureCoords[0][i].x, @@ -54,8 +54,9 @@ MeshData ConvertMesh(const aiMesh *AiMesh) { return Data; } -MaterialInstanceRef ConvertMaterial(const aiScene *Scene, unsigned MatIndex, - const std::filesystem::path &AssetDir) { +std::shared_ptr +ConvertMaterial(const aiScene *Scene, unsigned MatIndex, + const std::filesystem::path &AssetDir) { auto Mat = std::make_shared(); if (MatIndex >= Scene->mNumMaterials) return Mat; diff --git a/Axiom/Assets/CookedAssetRuntime.cpp b/Axiom/Assets/CookedAssetRuntime.cpp index 7caf4580..c1f0d216 100644 --- a/Axiom/Assets/CookedAssetRuntime.cpp +++ b/Axiom/Assets/CookedAssetRuntime.cpp @@ -6,12 +6,26 @@ #include "Assets/IAssetSource.h" #include "Assets/SceneFile.h" +#include + +#include +#include #include -#include #include +#include +#include namespace Axiom::Assets { namespace { +constexpr char kCookedSceneMagic[] = {'W', 'S', 'C', 'N'}; +constexpr std::uint32_t kCookedSceneVersion = 1; + +struct CookedSceneAssetReferences { + std::vector MeshAssetPaths; + std::vector MaterialAssetPaths; + std::vector TextureAssetPaths; +}; + bool ReadPackageManifestFields( const std::filesystem::path &ManifestPath, std::unordered_map &Fields) { @@ -20,81 +34,124 @@ bool ReadPackageManifestFields( return false; } - const std::string Text((std::istreambuf_iterator(File)), - std::istreambuf_iterator()); - std::size_t Position = 0; - auto SkipWs = [&]() { - while (Position < Text.size() && - (Text[Position] == ' ' || Text[Position] == '\n' || - Text[Position] == '\r' || Text[Position] == '\t')) { - ++Position; + std::string Text((std::istreambuf_iterator(File)), + std::istreambuf_iterator()); + rapidjson::Document Document; + Document.ParseInsitu(Text.data()); + if (Document.HasParseError() || !Document.IsObject()) { + return false; + } + + for (const auto &Member : Document.GetObject()) { + if (Member.value.IsString()) { + Fields.emplace( + Member.name.GetString(), + std::string(Member.value.GetString(), Member.value.GetStringLength())); + continue; } - }; - auto ParseString = [&]() -> std::optional { - SkipWs(); - if (Position >= Text.size() || Text[Position] != '"') { - return std::nullopt; + + if (Member.value.IsBool()) { + Fields.emplace(Member.name.GetString(), + Member.value.GetBool() ? "true" : "false"); + continue; } - ++Position; - std::string Result; - while (Position < Text.size()) { - const char Character = Text[Position++]; - if (Character == '"') { - return Result; - } - if (Character == '\\' && Position < Text.size()) { - Result.push_back(Text[Position++]); - } else { - Result.push_back(Character); - } + + if (Member.value.IsInt64()) { + Fields.emplace(Member.name.GetString(), + std::to_string(Member.value.GetInt64())); + continue; } - return std::nullopt; - }; - SkipWs(); - if (Position >= Text.size() || Text[Position] != '{') { - return false; - } - ++Position; - while (true) { - SkipWs(); - if (Position >= Text.size()) { - return false; + if (Member.value.IsUint64()) { + Fields.emplace(Member.name.GetString(), + std::to_string(Member.value.GetUint64())); + continue; } - if (Text[Position] == '}') { - return true; + + if (Member.value.IsDouble()) { + Fields.emplace(Member.name.GetString(), + std::to_string(Member.value.GetDouble())); + continue; } - const auto Key = ParseString(); - if (!Key.has_value()) { - return false; + if (Member.value.IsNull()) { + Fields.emplace(Member.name.GetString(), "null"); } - SkipWs(); - if (Position >= Text.size() || Text[Position] != ':') { - return false; + } + + return true; +} + +bool ReadCookedSceneAssetReferences(const std::filesystem::path &ScenePath, + CookedSceneAssetReferences &References) { + std::ifstream File(ScenePath, std::ios::binary); + if (!File.is_open()) { + return false; + } + + char Magic[sizeof(kCookedSceneMagic)]; + File.read(Magic, sizeof(Magic)); + if (!File.good() || std::memcmp(Magic, kCookedSceneMagic, sizeof(Magic)) != 0) { + return false; + } + + std::uint32_t Version = 0; + std::uint64_t PayloadSize = 0; + File.read(reinterpret_cast(&Version), sizeof(Version)); + File.read(reinterpret_cast(&PayloadSize), sizeof(PayloadSize)); + if (!File.good() || Version != kCookedSceneVersion) { + return false; + } + + std::string Payload(PayloadSize, '\0'); + File.read(Payload.data(), static_cast(PayloadSize)); + if (!File.good()) { + return false; + } + + rapidjson::Document Document; + Document.ParseInsitu(Payload.data()); + if (Document.HasParseError() || !Document.IsObject()) { + return false; + } + + const auto ObjectsIt = Document.FindMember("objects"); + if (ObjectsIt == Document.MemberEnd() || !ObjectsIt->value.IsArray()) { + return true; + } + + for (const auto &ObjectValue : ObjectsIt->value.GetArray()) { + if (!ObjectValue.IsObject()) { + continue; } - ++Position; - const auto Value = ParseString(); - if (Value.has_value()) { - Fields[*Key] = *Value; - } else { - SkipWs(); - const std::size_t ValueStart = Position; - while (Position < Text.size() && Text[Position] != ',' && Text[Position] != '}') { - ++Position; - } - Fields[*Key] = Text.substr(ValueStart, Position - ValueStart); + + const auto AssetRelativePathIt = ObjectValue.FindMember("assetRelativePath"); + if (AssetRelativePathIt != ObjectValue.MemberEnd() && + AssetRelativePathIt->value.IsString()) { + References.MeshAssetPaths.emplace_back( + AssetRelativePathIt->value.GetString(), + AssetRelativePathIt->value.GetStringLength()); } - SkipWs(); - if (Position < Text.size() && Text[Position] == ',') { - ++Position; - continue; + const auto MaterialAssetPathIt = + ObjectValue.FindMember("materialAssetPath"); + if (MaterialAssetPathIt != ObjectValue.MemberEnd() && + MaterialAssetPathIt->value.IsString()) { + References.MaterialAssetPaths.emplace_back( + MaterialAssetPathIt->value.GetString(), + MaterialAssetPathIt->value.GetStringLength()); } - if (Position < Text.size() && Text[Position] == '}') { - return true; + + const auto TextureAssetPathIt = ObjectValue.FindMember("textureAssetPath"); + if (TextureAssetPathIt != ObjectValue.MemberEnd() && + TextureAssetPathIt->value.IsString()) { + References.TextureAssetPaths.emplace_back( + TextureAssetPathIt->value.GetString(), + TextureAssetPathIt->value.GetStringLength()); } } + + return true; } } // namespace @@ -104,13 +161,14 @@ bool IsCookedOnlyContentPath(const std::filesystem::path &Path) { return false; } - const auto PackageManifestPath = ContentRoot->parent_path() / "package.wraith.json"; + const auto PackageManifestPath = + ContentRoot->parent_path() / "package.wraith.json"; return std::filesystem::exists(PackageManifestPath); } std::optional ResolvePackagedContentDescriptor(const std::filesystem::path &Path, - std::string *FailureReason) { + std::string *FailureReason) { const auto ContentRoot = FindContentRootForPath(Path); if (!ContentRoot.has_value()) { if (FailureReason != nullptr) { @@ -143,7 +201,8 @@ ResolvePackagedContentDescriptor(const std::filesystem::path &Path, if (ContentModeIt == Fields.end() || SceneAssetIt == Fields.end() || CookManifestIt == Fields.end() || EngineContentIt == Fields.end()) { if (FailureReason != nullptr) { - *FailureReason = "package.wraith.json is missing required packaged runtime fields."; + *FailureReason = + "package.wraith.json is missing required packaged runtime fields."; } return std::nullopt; } @@ -196,8 +255,9 @@ bool ValidatePackagedContentDescriptor(const PackagedContentDescriptor &Descript return false; } - const auto LoadedScene = LoadCookedSceneFromFile(Descriptor.SceneAssetPath); - if (!LoadedScene.has_value()) { + CookedSceneAssetReferences SceneReferences; + if (!ReadCookedSceneAssetReferences(Descriptor.SceneAssetPath, + SceneReferences)) { if (FailureReason != nullptr) { *FailureReason = "Failed to load packaged scene asset '" + Descriptor.SceneAssetPath.string() + "'."; @@ -259,47 +319,30 @@ bool ValidatePackagedContentDescriptor(const PackagedContentDescriptor &Descript if (FailureReason != nullptr) { *FailureReason = std::string(Usage) + " '" + RelativeAssetPath.generic_string() + - "' maps to missing cooked asset '" + + "' resolves to missing cooked asset '" + CookedPath->string() + "'."; } return false; } + return true; }; - std::unordered_set MeshPaths; - std::unordered_set TexturePaths; - for (const auto &Instance : LoadedScene->MeshInstances) { - if (!Instance.AssetRelativePath.empty()) { - MeshPaths.insert(Instance.AssetRelativePath); - } - if (Instance.Material != nullptr && - !Instance.Material->TextureAssetPath.empty()) { - TexturePaths.insert(Instance.Material->TextureAssetPath); - } - } - for (const auto &[ObjectId, Details] : LoadedScene->ObjectDetailsById) { - static_cast(ObjectId); - if (!Details.AssetRelativePath.empty()) { - MeshPaths.insert(Details.AssetRelativePath); - } - if (Details.Material.has_value() && - Details.Material->TextureAssetPath.has_value() && - !Details.Material->TextureAssetPath->empty()) { - TexturePaths.insert(*Details.Material->TextureAssetPath); + for (const std::string &MeshAssetPath : SceneReferences.MeshAssetPaths) { + if (!ValidateResolvedAssetPath(MeshAssetPath, "Mesh asset")) { + return false; } } - if (!LoadedScene->WorldSettings.SkyboxHDRPath.empty()) { - TexturePaths.insert(LoadedScene->WorldSettings.SkyboxHDRPath); - } - for (const std::string &MeshPath : MeshPaths) { - if (!ValidateResolvedAssetPath(MeshPath, "Mesh asset reference")) { + for (const std::string &MaterialAssetPath : + SceneReferences.MaterialAssetPaths) { + if (!ValidateResolvedAssetPath(MaterialAssetPath, "Material asset")) { return false; } } - for (const std::string &TexturePath : TexturePaths) { - if (!ValidateResolvedAssetPath(TexturePath, "Texture asset reference")) { + + for (const std::string &TextureAssetPath : SceneReferences.TextureAssetPaths) { + if (!ValidateResolvedAssetPath(TextureAssetPath, "Texture asset")) { return false; } } diff --git a/Axiom/Assets/CookedMeshAsset.cpp b/Axiom/Assets/CookedMeshAsset.cpp index c44c8246..3887c526 100644 --- a/Axiom/Assets/CookedMeshAsset.cpp +++ b/Axiom/Assets/CookedMeshAsset.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace Axiom::Assets { namespace { @@ -46,6 +47,14 @@ static_assert(std::is_trivially_copyable_v); static_assert(std::is_trivially_copyable_v); static_assert(std::is_trivially_copyable_v); +struct LegacyMeshVertexV2 { + glm::vec4 Position{0.0f, 0.0f, 0.0f, 1.0f}; + glm::vec4 Normal{0.0f, 0.0f, 1.0f, 0.0f}; + glm::vec2 TexCoord{0.0f, 0.0f}; +}; + +static_assert(std::is_trivially_copyable_v); + template bool WriteValue(std::ofstream &Stream, const T &Value) { Stream.write(reinterpret_cast(&Value), sizeof(T)); @@ -169,7 +178,8 @@ LoadCookedMeshAsset(const std::filesystem::path &Path) { return std::nullopt; } - if (Header.Version != 1 && Header.Version != kCookedMeshFormatVersion) { + if (Header.Version != 1 && Header.Version != 2 && + Header.Version != kCookedMeshFormatVersion) { A_CORE_WARN("CookedMeshAsset: unsupported version {} in '{}'", Header.Version, Path.string()); return std::nullopt; @@ -222,11 +232,30 @@ LoadCookedMeshAsset(const std::filesystem::path &Path) { Instance.Mesh.Vertices.resize(InstanceMeta.VertexCount); if (InstanceMeta.VertexCount > 0) { - Stream.read(reinterpret_cast(Instance.Mesh.Vertices.data()), - static_cast(Instance.Mesh.Vertices.size() * - sizeof(MeshVertex))); - if (!Stream.good()) - return std::nullopt; + if (Header.Version >= 3) { + Stream.read(reinterpret_cast(Instance.Mesh.Vertices.data()), + static_cast(Instance.Mesh.Vertices.size() * + sizeof(MeshVertex))); + if (!Stream.good()) { + return std::nullopt; + } + } else { + std::vector LegacyVertices(InstanceMeta.VertexCount); + Stream.read(reinterpret_cast(LegacyVertices.data()), + static_cast(LegacyVertices.size() * + sizeof(LegacyMeshVertexV2))); + if (!Stream.good()) { + return std::nullopt; + } + for (size_t VertexIndex = 0; VertexIndex < LegacyVertices.size(); + ++VertexIndex) { + Instance.Mesh.Vertices[VertexIndex] = { + .Position = glm::vec3(LegacyVertices[VertexIndex].Position), + .Normal = glm::vec3(LegacyVertices[VertexIndex].Normal), + .TexCoord = LegacyVertices[VertexIndex].TexCoord, + }; + } + } } Instance.Mesh.Indices.resize(InstanceMeta.IndexCount); @@ -269,9 +298,18 @@ MeshSceneData ToRuntimeMeshSceneData(const CookedMeshSceneData &Scene, const std::filesystem::path &ContentRoot) { MeshSceneData Out; Out.Instances.reserve(Scene.Instances.size()); + std::unordered_map> + SharedMaterials; for (const auto &Instance : Scene.Instances) { - auto Material = std::make_shared(); - if (!Instance.MaterialAssetPath.empty()) { + std::shared_ptr Material; + if (Instance.MaterialAssetPath.empty()) { + Material = std::make_shared(); + } else if (const auto SharedMaterial = + SharedMaterials.find(Instance.MaterialAssetPath); + SharedMaterial != SharedMaterials.end()) { + Material = SharedMaterial->second; + } else { + Material = std::make_shared(); const auto CookedMaterial = LoadCookedMaterialAssetIfAvailable(ContentRoot / Instance.MaterialAssetPath); @@ -285,6 +323,7 @@ MeshSceneData ToRuntimeMeshSceneData(const CookedMeshSceneData &Scene, LoadTextureFromFile(ContentRoot / CookedMaterial->TextureAssetPath); } } + SharedMaterials.emplace(Instance.MaterialAssetPath, Material); } Out.Instances.push_back({ .Name = Instance.Name, diff --git a/Axiom/Assets/CookedMeshAsset.h b/Axiom/Assets/CookedMeshAsset.h index 8297393f..6dca48a1 100644 --- a/Axiom/Assets/CookedMeshAsset.h +++ b/Axiom/Assets/CookedMeshAsset.h @@ -10,7 +10,7 @@ namespace Axiom::Assets { -constexpr uint32_t kCookedMeshFormatVersion = 2; +constexpr uint32_t kCookedMeshFormatVersion = 3; struct CookedMeshSceneData { struct InstanceData { diff --git a/Axiom/Assets/MeshAsset.cpp b/Axiom/Assets/MeshAsset.cpp index f6d466bd..553f8856 100644 --- a/Axiom/Assets/MeshAsset.cpp +++ b/Axiom/Assets/MeshAsset.cpp @@ -164,7 +164,7 @@ BuildMeshData(const fastgltf::Asset &Asset, const fastgltf::Primitive &Primitive fastgltf::iterateAccessorWithIndex( Asset, PositionAccessor, [&](const glm::vec3 &Position, size_t Index) { - Result.Mesh.Vertices[Index].Position = glm::vec4(Position, 1.0f); + Result.Mesh.Vertices[Index].Position = Position; Result.Mesh.BoundsMin = glm::min(Result.Mesh.BoundsMin, Position); Result.Mesh.BoundsMax = glm::max(Result.Mesh.BoundsMax, Position); }); @@ -173,12 +173,11 @@ BuildMeshData(const fastgltf::Asset &Asset, const fastgltf::Primitive &Primitive const auto &NormalAccessor = Asset.accessors[*NormalAccessorIndex]; fastgltf::iterateAccessorWithIndex( Asset, NormalAccessor, [&](const glm::vec3 &Normal, size_t Index) { - Result.Mesh.Vertices[Index].Normal = - glm::vec4(glm::normalize(Normal), 0.0f); + Result.Mesh.Vertices[Index].Normal = glm::normalize(Normal); }); } else { for (auto &Vertex : Result.Mesh.Vertices) { - Vertex.Normal = glm::vec4(0.0f, 0.0f, 1.0f, 0.0f); + Vertex.Normal = glm::vec3(0.0f, 0.0f, 1.0f); } } @@ -428,12 +427,14 @@ LoadBasicMeshAssetFromSource(const std::filesystem::path &Path) { } std::vector ImageCache(ParsedAsset.images.size()); - std::vector MaterialCache(ParsedAsset.materials.size()); - MaterialInstanceRef FallbackMaterial = std::make_shared(); + std::vector> MaterialCache( + ParsedAsset.materials.size()); + std::shared_ptr FallbackMaterial = + std::make_shared(); auto MakeMaterialWithFactors = [](const fastgltf::Material &Mat, - TextureSourceDataRef Texture) -> MaterialInstanceRef { + TextureSourceDataRef Texture) -> std::shared_ptr { auto Ref = std::make_shared(); Ref->BaseColorTexture = std::move(Texture); const auto &Pbr = Mat.pbrData; @@ -448,7 +449,7 @@ LoadBasicMeshAssetFromSource(const std::filesystem::path &Path) { }; auto ResolveMaterial = [&](const fastgltf::Primitive &Primitive, - bool HasTexCoord0) -> MaterialInstanceRef { + bool HasTexCoord0) -> std::shared_ptr { if (!Primitive.materialIndex.has_value() || *Primitive.materialIndex >= ParsedAsset.materials.size()) { return FallbackMaterial; diff --git a/Axiom/Assets/SceneFile.cpp b/Axiom/Assets/SceneFile.cpp index db0a1e4f..67215703 100644 --- a/Axiom/Assets/SceneFile.cpp +++ b/Axiom/Assets/SceneFile.cpp @@ -9,13 +9,15 @@ #include #include -#include +#include +#include +#include + #include #include +#include #include #include -#include -#include #include #include @@ -34,24 +36,30 @@ namespace { constexpr char kCookedSceneMagic[] = {'W', 'S', 'C', 'N'}; constexpr std::uint32_t kCookedSceneVersion = 1; -std::string EscStr(std::string_view S) { - std::string Out; - Out.reserve(S.size() + 2); - Out += '"'; - for (char C : S) { - if (C == '"') { Out += "\\\""; } - else if (C == '\\') { Out += "\\\\"; } - else if (C == '\n') { Out += "\\n"; } - else { Out += C; } - } - Out += '"'; - return Out; +rapidjson::Value CopyString(std::string_view Value, + rapidjson::Document::AllocatorType &Allocator) { + rapidjson::Value StringValue; + StringValue.SetString(Value.data(), + static_cast(Value.size()), + Allocator); + return StringValue; +} + +rapidjson::Value SerializeVec3(const glm::vec3 &Value, + rapidjson::Document::AllocatorType &Allocator) { + rapidjson::Value ArrayValue(rapidjson::kArrayType); + ArrayValue.PushBack(Value.x, Allocator); + ArrayValue.PushBack(Value.y, Allocator); + ArrayValue.PushBack(Value.z, Allocator); + return ArrayValue; } -std::string SerializeVec3(const glm::vec3 &V) { - std::ostringstream S; - S << "[" << V.x << "," << V.y << "," << V.z << "]"; - return S.str(); +std::string SerializePrettyJson(const rapidjson::Document &Document) { + rapidjson::StringBuffer Buffer; + rapidjson::PrettyWriter Writer(Buffer); + Writer.SetIndent(' ', 2); + Document.Accept(Writer); + return std::string(Buffer.GetString(), Buffer.GetSize()) + '\n'; } std::string SanitizeGeneratedAssetToken(std::string_view Value) { @@ -114,22 +122,31 @@ const char *KindStr(EditorSceneItemKind K) { } void SerializeSceneItemsFlat( - std::ostringstream &Out, const std::vector &Items, + rapidjson::Value &Out, const std::vector &Items, const std::unordered_map &DetailsById, - const std::string &ParentId, bool &First) { + std::string_view ParentId, + rapidjson::Document::AllocatorType &Allocator) { for (const auto &Item : Items) { const auto DetailsIt = DetailsById.find(Item.Id); if (DetailsIt != DetailsById.end() && DetailsIt->second.IsGeneratedAssetChild) { continue; } - if (!First) Out << ",\n"; - First = false; - Out << " {\"id\":" << EscStr(Item.Id) - << ",\"parentId\":" << (ParentId.empty() ? "null" : EscStr(ParentId)) - << ",\"displayName\":" << EscStr(Item.DisplayName) - << ",\"kind\":\"" << KindStr(Item.Kind) << "\"" - << ",\"visible\":" << (Item.Visible ? "true" : "false") << "}"; - SerializeSceneItemsFlat(Out, Item.Children, DetailsById, Item.Id, First); + rapidjson::Value NodeValue(rapidjson::kObjectType); + NodeValue.AddMember("handle", Item.Handle.Value, Allocator); + NodeValue.AddMember("id", CopyString(Item.Id, Allocator), Allocator); + if (ParentId.empty()) { + NodeValue.AddMember("parentId", rapidjson::Value().SetNull(), Allocator); + } else { + NodeValue.AddMember("parentId", CopyString(ParentId, Allocator), + Allocator); + } + NodeValue.AddMember("displayName", CopyString(Item.DisplayName, Allocator), + Allocator); + NodeValue.AddMember("kind", CopyString(KindStr(Item.Kind), Allocator), + Allocator); + NodeValue.AddMember("visible", Item.Visible, Allocator); + Out.PushBack(NodeValue, Allocator); + SerializeSceneItemsFlat(Out, Item.Children, DetailsById, Item.Id, Allocator); } } @@ -318,6 +335,7 @@ void ExpandMeshAssetIntoScene(EditorSceneState &State, std::string_view RootObje }) : std::nullopt; State.MeshInstances.push_back({ + .ObjectHandle = RootDetails.Handle, .ObjectId = std::string(RootObjectId), .Mesh = First.Mesh, .Material = First.Material, @@ -365,6 +383,7 @@ void ExpandMeshAssetIntoScene(EditorSceneState &State, std::string_view RootObje .GeneratedFromAssetRootId = std::string(RootObjectId), }; RootItem->Children.push_back({ + .Handle = State.ObjectDetailsById[ChildId].Handle, .Id = ChildId, .DisplayName = ResolveGeneratedAssetChildDisplayName( SourceInstance.Name, InstanceIndex), @@ -372,6 +391,7 @@ void ExpandMeshAssetIntoScene(EditorSceneState &State, std::string_view RootObje .Visible = RootDetails.Visible, }); State.MeshInstances.push_back({ + .ObjectHandle = State.ObjectDetailsById[ChildId].Handle, .ObjectId = ChildId, .Mesh = SourceInstance.Mesh, .Material = SourceInstance.Material, @@ -414,51 +434,68 @@ std::string SerializeSceneToJsonString(const std::filesystem::path &Path, } } - std::ostringstream Out; - Out << "{\n"; - Out << " \"version\": 1,\n"; - Out << " \"meshAsset\": " - << EscStr(HasImplicitGlobalMeshAsset ? "basicmesh.glb" : "") << ",\n"; - - // Flat node list (scene tree + parent links) - Out << " \"nodes\": [\n"; - bool FirstNode = true; - SerializeSceneItemsFlat(Out, Scene.Items, Scene.ObjectDetailsById, "", FirstNode); - Out << "\n ],\n"; - - // Object details (transforms, visibility, mesh name mapping) - Out << " \"objects\": [\n"; - bool FirstObj = true; + rapidjson::Document Document; + Document.SetObject(); + auto &Allocator = Document.GetAllocator(); + + Document.AddMember("version", 1u, Allocator); + Document.AddMember("meshAsset", + CopyString(HasImplicitGlobalMeshAsset ? "basicmesh.glb" : "", + Allocator), + Allocator); + + rapidjson::Value Nodes(rapidjson::kArrayType); + SerializeSceneItemsFlat(Nodes, Scene.Items, Scene.ObjectDetailsById, "", + Allocator); + Document.AddMember("nodes", Nodes, Allocator); + + rapidjson::Value Objects(rapidjson::kArrayType); for (const auto &[Id, Details] : Scene.ObjectDetailsById) { if (Details.IsGeneratedAssetChild) { continue; } - if (!FirstObj) Out << ",\n"; - FirstObj = false; - Out << " {\"id\":" << EscStr(Id) - << ",\"displayName\":" << EscStr(Details.DisplayName) - << ",\"kind\":\"" << KindStr(Details.Kind) << "\"" - << ",\"visible\":" << (Details.Visible ? "true" : "false") - << ",\"isGeneratedAssetChild\":" - << (Details.IsGeneratedAssetChild ? "true" : "false") - << ",\"supportsTransform\":" << (Details.SupportsTransform ? "true" : "false") - << ",\"transformReadOnly\":" << (Details.TransformReadOnly ? "true" : "false"); + rapidjson::Value ObjectValue(rapidjson::kObjectType); + ObjectValue.AddMember("handle", Details.Handle.Value, Allocator); + ObjectValue.AddMember("id", CopyString(Id, Allocator), Allocator); + ObjectValue.AddMember("displayName", CopyString(Details.DisplayName, Allocator), + Allocator); + ObjectValue.AddMember("kind", CopyString(KindStr(Details.Kind), Allocator), + Allocator); + ObjectValue.AddMember("visible", Details.Visible, Allocator); + ObjectValue.AddMember("isGeneratedAssetChild", Details.IsGeneratedAssetChild, + Allocator); + ObjectValue.AddMember("supportsTransform", Details.SupportsTransform, + Allocator); + ObjectValue.AddMember("transformReadOnly", Details.TransformReadOnly, + Allocator); if (Details.Transform.has_value()) { - Out << ",\"location\":" << SerializeVec3(Details.Transform->Location) - << ",\"rotationDegrees\":" << SerializeVec3(Details.Transform->RotationDegrees) - << ",\"scale\":" << SerializeVec3(Details.Transform->Scale); + ObjectValue.AddMember("location", + SerializeVec3(Details.Transform->Location, Allocator), + Allocator); + ObjectValue.AddMember( + "rotationDegrees", + SerializeVec3(Details.Transform->RotationDegrees, Allocator), + Allocator); + ObjectValue.AddMember("scale", + SerializeVec3(Details.Transform->Scale, Allocator), + Allocator); } if (Details.ScriptClass.has_value()) { - Out << ",\"scriptClass\":" << EscStr(*Details.ScriptClass); + ObjectValue.AddMember("scriptClass", + CopyString(*Details.ScriptClass, Allocator), + Allocator); } if (Details.GeneratedFromAssetRootId.has_value()) { - Out << ",\"generatedFromAssetRootId\":" - << EscStr(*Details.GeneratedFromAssetRootId); + ObjectValue.AddMember("generatedFromAssetRootId", + CopyString(*Details.GeneratedFromAssetRootId, + Allocator), + Allocator); } if (Details.Kind == EditorSceneItemKind::Mesh) { const auto AssetIt = AssetPathByObjectId.find(Id); if (AssetIt != AssetPathByObjectId.end()) { - Out << ",\"assetRelativePath\":" << EscStr(AssetIt->second); + ObjectValue.AddMember("assetRelativePath", + CopyString(AssetIt->second, Allocator), Allocator); } if (Details.Material.has_value()) { const std::filesystem::path MaterialPath = @@ -471,70 +508,94 @@ std::string SerializeSceneToJsonString(const std::filesystem::path &Path, .TextureAssetPath = Details.Material->TextureAssetPath.value_or("")}); if (MaterialCooked.has_value()) { - Out << ",\"materialAssetPath\":" - << EscStr(MaterialCooked->RelativePath); + ObjectValue.AddMember("materialAssetPath", + CopyString(MaterialCooked->RelativePath, + Allocator), + Allocator); } } if (Details.Material.has_value() && Details.Material->TextureAssetPath.has_value()) { - Out << ",\"textureAssetPath\":" << EscStr(*Details.Material->TextureAssetPath); + ObjectValue.AddMember("textureAssetPath", + CopyString(*Details.Material->TextureAssetPath, + Allocator), + Allocator); } } if (Details.Light.has_value()) { - Out << ",\"lightColor\":" << SerializeVec3(Details.Light->Color) - << ",\"lightIntensity\":" << Details.Light->Intensity - << ",\"lightDirection\":" << SerializeVec3(Details.Light->Direction); + ObjectValue.AddMember("lightColor", + SerializeVec3(Details.Light->Color, Allocator), + Allocator); + ObjectValue.AddMember("lightIntensity", Details.Light->Intensity, + Allocator); + ObjectValue.AddMember("lightDirection", + SerializeVec3(Details.Light->Direction, Allocator), + Allocator); } if (Details.Physics.has_value()) { - Out << ",\"physicsBodyType\":" - << EscStr(Details.Physics->BodyType == EditorPhysicsBodyType::Dynamic - ? "dynamic" - : (Details.Physics->BodyType == EditorPhysicsBodyType::Static - ? "static" - : "none")) - << ",\"physicsColliderType\":" - << EscStr(Details.Physics->ColliderType == EditorPhysicsColliderType::Sphere - ? "sphere" - : (Details.Physics->ColliderType == EditorPhysicsColliderType::Box - ? "box" - : "none")) - << ",\"physicsBoxHalfExtents\":" - << SerializeVec3(Details.Physics->BoxHalfExtents) - << ",\"physicsSphereRadius\":" << Details.Physics->SphereRadius - << ",\"physicsMass\":" << Details.Physics->Mass - << ",\"physicsFriction\":" << Details.Physics->Friction - << ",\"physicsRestitution\":" << Details.Physics->Restitution; + ObjectValue.AddMember( + "physicsBodyType", + CopyString(Details.Physics->BodyType == EditorPhysicsBodyType::Dynamic + ? "dynamic" + : (Details.Physics->BodyType == + EditorPhysicsBodyType::Static + ? "static" + : "none"), + Allocator), + Allocator); + ObjectValue.AddMember( + "physicsColliderType", + CopyString( + Details.Physics->ColliderType == EditorPhysicsColliderType::Sphere + ? "sphere" + : (Details.Physics->ColliderType == + EditorPhysicsColliderType::Box + ? "box" + : "none"), + Allocator), + Allocator); + ObjectValue.AddMember( + "physicsBoxHalfExtents", + SerializeVec3(Details.Physics->BoxHalfExtents, Allocator), Allocator); + ObjectValue.AddMember("physicsSphereRadius", + Details.Physics->SphereRadius, Allocator); + ObjectValue.AddMember("physicsMass", Details.Physics->Mass, Allocator); + ObjectValue.AddMember("physicsFriction", Details.Physics->Friction, + Allocator); + ObjectValue.AddMember("physicsRestitution", + Details.Physics->Restitution, Allocator); } - Out << "}"; + Objects.PushBack(ObjectValue, Allocator); } - Out << "\n ],\n"; + Document.AddMember("objects", Objects, Allocator); - // Mesh name → object ID mapping (needed to re-hydrate MeshInstances) - Out << " \"meshNameToObjectId\": {\n"; - bool FirstMesh = true; + rapidjson::Value MeshNameToObjectId(rapidjson::kObjectType); for (const auto &Instance : Scene.MeshInstances) { const auto DetailsIt = Scene.ObjectDetailsById.find(Instance.ObjectId); if (DetailsIt == Scene.ObjectDetailsById.end() || DetailsIt->second.IsGeneratedAssetChild) { continue; } - // We stored the display name as the mesh source name via ResolveStartupObjectId - // Look up the display name from ObjectDetailsById - const auto It = DetailsIt; - if (!FirstMesh) Out << ",\n"; - FirstMesh = false; - Out << " " << EscStr(It->second.DisplayName) << ": " << EscStr(Instance.ObjectId); + MeshNameToObjectId.AddMember( + CopyString(DetailsIt->second.DisplayName, Allocator), + CopyString(Instance.ObjectId, Allocator), Allocator); } - Out << "\n },\n"; - Out << " \"worldSettings\": {\n" - << " \"skyboxColorTop\": " - << SerializeVec3(Scene.WorldSettings.SkyboxColorTop) << ",\n" - << " \"skyboxColorBottom\": " - << SerializeVec3(Scene.WorldSettings.SkyboxColorBottom) << ",\n" - << " \"skyboxHDRPath\": " - << EscStr(Scene.WorldSettings.SkyboxHDRPath) << "\n" - << " }\n"; - Out << "}\n"; - return Out.str(); + Document.AddMember("meshNameToObjectId", MeshNameToObjectId, Allocator); + + rapidjson::Value WorldSettings(rapidjson::kObjectType); + WorldSettings.AddMember( + "skyboxColorTop", + SerializeVec3(Scene.WorldSettings.SkyboxColorTop, Allocator), Allocator); + WorldSettings.AddMember( + "skyboxColorBottom", + SerializeVec3(Scene.WorldSettings.SkyboxColorBottom, Allocator), + Allocator); + WorldSettings.AddMember("skyboxHDRPath", + CopyString(Scene.WorldSettings.SkyboxHDRPath, + Allocator), + Allocator); + Document.AddMember("worldSettings", WorldSettings, Allocator); + + return SerializePrettyJson(Document); } bool SaveSceneToFile(const std::filesystem::path &Path, @@ -567,165 +628,38 @@ bool SaveCookedSceneToFile(const std::filesystem::path &Path, return File.good(); } -// --------------------------------------------------------------------------- -// Minimal JSON parser (purpose-built for the known scene file schema) -// --------------------------------------------------------------------------- - namespace { -struct Parser { - std::string_view Src; - size_t Pos{0}; - - char Peek() const { return Pos < Src.size() ? Src[Pos] : '\0'; } - char Eat() { return Pos < Src.size() ? Src[Pos++] : '\0'; } - - void SkipWs() { - while (Pos < Src.size() && (Src[Pos] == ' ' || Src[Pos] == '\t' || - Src[Pos] == '\r' || Src[Pos] == '\n')) - ++Pos; - } - - bool Expect(char C) { - SkipWs(); - if (Peek() == C) { ++Pos; return true; } - return false; - } +EditorSceneItemKind KindFromStr(std::string_view S) { + if (S == "Mesh") return EditorSceneItemKind::Mesh; + if (S == "Light") return EditorSceneItemKind::Light; + if (S == "Camera") return EditorSceneItemKind::Camera; + if (S == "Actor") return EditorSceneItemKind::Actor; + return EditorSceneItemKind::Folder; +} - std::optional ParseString() { - SkipWs(); - if (Peek() != '"') return std::nullopt; - ++Pos; - std::string Out; - while (Pos < Src.size()) { - char C = Eat(); - if (C == '"') return Out; - if (C == '\\') { - char E = Eat(); - if (E == 'n') Out += '\n'; - else if (E == '\\') Out += '\\'; - else if (E == '"') Out += '"'; - else Out += E; - } else { - Out += C; - } - } +std::optional GetOptionalString( + const rapidjson::Value &Object, const char *Name) { + const auto It = Object.FindMember(Name); + if (It == Object.MemberEnd()) { return std::nullopt; } - - std::optional ParseNumber() { - SkipWs(); - const size_t Start = Pos; - if (Peek() == '-') ++Pos; - while (Pos < Src.size() && - (std::isdigit(static_cast(Src[Pos])) || - Src[Pos] == '.' || Src[Pos] == 'e' || Src[Pos] == 'E' || - Src[Pos] == '+' || Src[Pos] == '-')) - ++Pos; - // std::from_chars for floating-point requires macOS 13.3+; use strtod instead. - char *End = nullptr; - double V = std::strtod(Src.data() + Start, &End); - if (End == Src.data() + Start) return std::nullopt; - return V; + if (It->value.IsNull()) { + return std::string(); } - - std::optional ParseBool() { - SkipWs(); - if (Src.substr(Pos, 4) == "true") { Pos += 4; return true; } - if (Src.substr(Pos, 5) == "false") { Pos += 5; return false; } + if (!It->value.IsString()) { return std::nullopt; } + return std::string(It->value.GetString(), It->value.GetStringLength()); +} - bool ParseNull() { - SkipWs(); - if (Src.substr(Pos, 4) == "null") { Pos += 4; return true; } - return false; - } - - std::optional ParseVec3() { - if (!Expect('[')) return std::nullopt; - auto X = ParseNumber(); if (!X) return std::nullopt; - if (!Expect(',')) return std::nullopt; - auto Y = ParseNumber(); if (!Y) return std::nullopt; - if (!Expect(',')) return std::nullopt; - auto Z = ParseNumber(); if (!Z) return std::nullopt; - if (!Expect(']')) return std::nullopt; - return glm::vec3{static_cast(*X), static_cast(*Y), - static_cast(*Z)}; - } - - // Skip any JSON value (string, number, bool, null, array, object) - void SkipValue() { - SkipWs(); - char C = Peek(); - if (C == '"') { ParseString(); return; } - if (C == '{') { SkipObject(); return; } - if (C == '[') { SkipArray(); return; } - if (C == 't' || C == 'f') { ParseBool(); return; } - if (C == 'n') { ParseNull(); return; } - ParseNumber(); - } - - void SkipObject() { - Expect('{'); - SkipWs(); - if (Peek() == '}') { ++Pos; return; } - do { - ParseString(); Expect(':'); SkipValue(); SkipWs(); - } while (Expect(',')); - Expect('}'); - } - - void SkipArray() { - Expect('['); - SkipWs(); - if (Peek() == ']') { ++Pos; return; } - do { - SkipValue(); SkipWs(); - } while (Expect(',')); - Expect(']'); - } - - // Parse {"key": value, ...} calling Handler(key) for each known field. - // Handler should read the value via this Parser before returning. - template - bool ParseObject(Fn Handler) { - if (!Expect('{')) return false; - SkipWs(); - if (Peek() == '}') { ++Pos; return true; } - do { - SkipWs(); - auto Key = ParseString(); - if (!Key) return false; - if (!Expect(':')) return false; - SkipWs(); - if (!Handler(*Key)) SkipValue(); - SkipWs(); - } while (Expect(',')); - return Expect('}'); - } - - // Parse [element, ...] calling Handler() for each element. - template - bool ParseArray(Fn Handler) { - if (!Expect('[')) return false; - SkipWs(); - if (Peek() == ']') { ++Pos; return true; } - do { - SkipWs(); - Handler(); - SkipWs(); - } while (Expect(',')); - return Expect(']'); +std::optional ParseVec3(const rapidjson::Value &Value) { + if (!Value.IsArray() || Value.Size() != 3 || !Value[0].IsNumber() || + !Value[1].IsNumber() || !Value[2].IsNumber()) { + return std::nullopt; } -}; - -EditorSceneItemKind KindFromStr(std::string_view S) { - if (S == "Mesh") return EditorSceneItemKind::Mesh; - if (S == "Light") return EditorSceneItemKind::Light; - if (S == "Camera") return EditorSceneItemKind::Camera; - if (S == "Actor") return EditorSceneItemKind::Actor; - return EditorSceneItemKind::Folder; + return glm::vec3(Value[0].GetFloat(), Value[1].GetFloat(), + Value[2].GetFloat()); } } // namespace @@ -739,16 +673,23 @@ DeserializeSceneFromJsonString(const std::filesystem::path &Path, std::string_view Text) { const std::filesystem::path ContentRoot = ResolveContentRootForScenePath(Path); const bool CookedOnlyContent = IsCookedOnlyContentPath(ContentRoot); - - Parser P{Text}; + std::string MutableText(Text); + rapidjson::Document Document; + Document.ParseInsitu(MutableText.data()); + if (Document.HasParseError() || !Document.IsObject()) { + A_CORE_ERROR("SceneFile: failed to parse {0}", Path.string()); + return std::nullopt; + } // --- Stage 1: parse flat data --- struct FlatNode { + SceneObjectHandle Handle{}; std::string Id, ParentId, DisplayName; EditorSceneItemKind Kind{EditorSceneItemKind::Folder}; bool Visible{true}; }; struct ObjectData { + SceneObjectHandle Handle{}; std::string DisplayName; EditorSceneItemKind Kind{EditorSceneItemKind::Folder}; bool Visible{true}; @@ -770,208 +711,303 @@ DeserializeSceneFromJsonString(const std::filesystem::path &Path, std::unordered_map Objects; std::unordered_map MeshNameToObjectId; EditorWorldSettings WorldSettings; + if (const auto MeshAssetIt = Document.FindMember("meshAsset"); + MeshAssetIt != Document.MemberEnd() && MeshAssetIt->value.IsString()) { + MeshAsset.assign(MeshAssetIt->value.GetString(), + MeshAssetIt->value.GetStringLength()); + } - bool Ok = P.ParseObject([&](const std::string &Key) -> bool { - if (Key == "version") { P.ParseNumber(); return true; } - if (Key == "meshAsset") { - auto V = P.ParseString(); if (V) MeshAsset = *V; return true; - } - if (Key == "nodes") { - P.ParseArray([&] { - FlatNode Node; - P.ParseObject([&](const std::string &K) -> bool { - if (K == "id") { auto V = P.ParseString(); if (V) Node.Id = *V; return true; } - if (K == "parentId") { P.SkipWs(); if (P.Peek() == 'n') { P.ParseNull(); } else { auto V = P.ParseString(); if (V) Node.ParentId = *V; } return true; } - if (K == "displayName") { auto V = P.ParseString(); if (V) Node.DisplayName = *V; return true; } - if (K == "kind") { auto V = P.ParseString(); if (V) Node.Kind = KindFromStr(*V); return true; } - if (K == "visible") { auto V = P.ParseBool(); if (V) Node.Visible = *V; return true; } - return false; - }); - Nodes.push_back(std::move(Node)); - }); - return true; + if (const auto NodesIt = Document.FindMember("nodes"); + NodesIt != Document.MemberEnd() && NodesIt->value.IsArray()) { + for (const auto &NodeValue : NodesIt->value.GetArray()) { + if (!NodeValue.IsObject()) { + continue; + } + FlatNode Node; + if (const auto HandleIt = NodeValue.FindMember("handle"); + HandleIt != NodeValue.MemberEnd() && HandleIt->value.IsUint64()) { + Node.Handle = SceneObjectHandle{HandleIt->value.GetUint64()}; + } + if (const auto Id = GetOptionalString(NodeValue, "id"); Id.has_value()) { + Node.Id = *Id; + } + if (const auto ParentId = GetOptionalString(NodeValue, "parentId"); + ParentId.has_value()) { + Node.ParentId = *ParentId; + } + if (const auto DisplayName = GetOptionalString(NodeValue, "displayName"); + DisplayName.has_value()) { + Node.DisplayName = *DisplayName; + } + if (const auto KindIt = NodeValue.FindMember("kind"); + KindIt != NodeValue.MemberEnd() && KindIt->value.IsString()) { + Node.Kind = KindFromStr( + std::string_view(KindIt->value.GetString(), + KindIt->value.GetStringLength())); + } + if (const auto VisibleIt = NodeValue.FindMember("visible"); + VisibleIt != NodeValue.MemberEnd() && VisibleIt->value.IsBool()) { + Node.Visible = VisibleIt->value.GetBool(); + } + Nodes.push_back(std::move(Node)); } - if (Key == "objects") { - P.ParseArray([&] { - std::string ObjId; - ObjectData Data; - P.ParseObject([&](const std::string &K) -> bool { - if (K == "id") { auto V = P.ParseString(); if (V) ObjId = *V; return true; } - if (K == "displayName") { auto V = P.ParseString(); if (V) Data.DisplayName = *V; return true; } - if (K == "kind") { auto V = P.ParseString(); if (V) Data.Kind = KindFromStr(*V); return true; } - if (K == "visible") { auto V = P.ParseBool(); if (V) Data.Visible = *V; return true; } - if (K == "isGeneratedAssetChild") { auto V = P.ParseBool(); if (V) Data.IsGeneratedAssetChild = *V; return true; } - if (K == "supportsTransform"){ auto V = P.ParseBool(); if (V) Data.SupportsTransform = *V; return true; } - if (K == "transformReadOnly"){ auto V = P.ParseBool(); if (V) Data.TransformReadOnly = *V; return true; } - if (K == "location") { - auto V = P.ParseVec3(); - if (V) { - if (!Data.Transform) Data.Transform = EditorTransformDetails{}; - Data.Transform->Location = *V; - } - return true; - } - if (K == "rotationDegrees") { - auto V = P.ParseVec3(); - if (V) { - if (!Data.Transform) Data.Transform = EditorTransformDetails{}; - Data.Transform->RotationDegrees = *V; - } - return true; - } - if (K == "scale") { - auto V = P.ParseVec3(); - if (V) { - if (!Data.Transform) Data.Transform = EditorTransformDetails{}; - Data.Transform->Scale = *V; - } - return true; - } - if (K == "scriptClass") { - P.SkipWs(); - if (P.Peek() == 'n') { P.ParseNull(); } else { auto V = P.ParseString(); if (V) Data.ScriptClass = *V; } - return true; - } - if (K == "generatedFromAssetRootId") { - P.SkipWs(); - if (P.Peek() == 'n') { P.ParseNull(); } else { auto V = P.ParseString(); if (V) Data.GeneratedFromAssetRootId = *V; } - return true; - } - if (K == "assetRelativePath") { - auto V = P.ParseString(); if (V) Data.AssetRelativePath = *V; return true; - } - if (K == "materialAssetPath") { - auto V = P.ParseString(); if (V) Data.MaterialAssetPath = *V; return true; - } - if (K == "textureAssetPath") { - P.SkipWs(); - if (P.Peek() == 'n') { P.ParseNull(); } else { auto V = P.ParseString(); if (V) Data.TextureAssetPath = *V; } - return true; - } - if (K == "lightColor") { - auto V = P.ParseVec3(); - if (V) { if (!Data.Light) Data.Light = EditorLightProperties{}; Data.Light->Color = *V; } - return true; - } - if (K == "lightIntensity") { - auto V = P.ParseNumber(); - if (V) { if (!Data.Light) Data.Light = EditorLightProperties{}; Data.Light->Intensity = static_cast(*V); } - return true; - } - if (K == "lightDirection") { - auto V = P.ParseVec3(); - if (V) { if (!Data.Light) Data.Light = EditorLightProperties{}; Data.Light->Direction = *V; } - return true; - } - if (K == "physicsBodyType") { - auto V = P.ParseString(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - if (*V == "static") { - Data.Physics->BodyType = EditorPhysicsBodyType::Static; - } else if (*V == "dynamic") { - Data.Physics->BodyType = EditorPhysicsBodyType::Dynamic; - } else { - Data.Physics->BodyType = EditorPhysicsBodyType::None; - } - } - return true; - } - if (K == "physicsColliderType") { - auto V = P.ParseString(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - if (*V == "box") { - Data.Physics->ColliderType = EditorPhysicsColliderType::Box; - } else if (*V == "sphere") { - Data.Physics->ColliderType = EditorPhysicsColliderType::Sphere; - } else { - Data.Physics->ColliderType = EditorPhysicsColliderType::None; - } - } - return true; + } + + if (const auto ObjectsIt = Document.FindMember("objects"); + ObjectsIt != Document.MemberEnd() && ObjectsIt->value.IsArray()) { + for (const auto &ObjectValue : ObjectsIt->value.GetArray()) { + if (!ObjectValue.IsObject()) { + continue; + } + + std::string ObjId; + ObjectData Data; + if (const auto HandleIt = ObjectValue.FindMember("handle"); + HandleIt != ObjectValue.MemberEnd() && HandleIt->value.IsUint64()) { + Data.Handle = SceneObjectHandle{HandleIt->value.GetUint64()}; + } + if (const auto Id = GetOptionalString(ObjectValue, "id"); Id.has_value()) { + ObjId = *Id; + } + if (const auto DisplayName = GetOptionalString(ObjectValue, "displayName"); + DisplayName.has_value()) { + Data.DisplayName = *DisplayName; + } + if (const auto KindIt = ObjectValue.FindMember("kind"); + KindIt != ObjectValue.MemberEnd() && KindIt->value.IsString()) { + Data.Kind = KindFromStr( + std::string_view(KindIt->value.GetString(), + KindIt->value.GetStringLength())); + } + if (const auto VisibleIt = ObjectValue.FindMember("visible"); + VisibleIt != ObjectValue.MemberEnd() && VisibleIt->value.IsBool()) { + Data.Visible = VisibleIt->value.GetBool(); + } + if (const auto GeneratedIt = + ObjectValue.FindMember("isGeneratedAssetChild"); + GeneratedIt != ObjectValue.MemberEnd() && GeneratedIt->value.IsBool()) { + Data.IsGeneratedAssetChild = GeneratedIt->value.GetBool(); + } + if (const auto SupportsTransformIt = + ObjectValue.FindMember("supportsTransform"); + SupportsTransformIt != ObjectValue.MemberEnd() && + SupportsTransformIt->value.IsBool()) { + Data.SupportsTransform = SupportsTransformIt->value.GetBool(); + } + if (const auto TransformReadOnlyIt = + ObjectValue.FindMember("transformReadOnly"); + TransformReadOnlyIt != ObjectValue.MemberEnd() && + TransformReadOnlyIt->value.IsBool()) { + Data.TransformReadOnly = TransformReadOnlyIt->value.GetBool(); + } + if (const auto LocationIt = ObjectValue.FindMember("location"); + LocationIt != ObjectValue.MemberEnd()) { + if (const auto Value = ParseVec3(LocationIt->value); Value.has_value()) { + if (!Data.Transform.has_value()) { + Data.Transform = EditorTransformDetails{}; } - if (K == "physicsBoxHalfExtents") { - auto V = P.ParseVec3(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - Data.Physics->BoxHalfExtents = *V; - } - return true; + Data.Transform->Location = *Value; + } + } + if (const auto RotationIt = ObjectValue.FindMember("rotationDegrees"); + RotationIt != ObjectValue.MemberEnd()) { + if (const auto Value = ParseVec3(RotationIt->value); Value.has_value()) { + if (!Data.Transform.has_value()) { + Data.Transform = EditorTransformDetails{}; } - if (K == "physicsSphereRadius") { - auto V = P.ParseNumber(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - Data.Physics->SphereRadius = static_cast(*V); - } - return true; + Data.Transform->RotationDegrees = *Value; + } + } + if (const auto ScaleIt = ObjectValue.FindMember("scale"); + ScaleIt != ObjectValue.MemberEnd()) { + if (const auto Value = ParseVec3(ScaleIt->value); Value.has_value()) { + if (!Data.Transform.has_value()) { + Data.Transform = EditorTransformDetails{}; } - if (K == "physicsMass") { - auto V = P.ParseNumber(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - Data.Physics->Mass = static_cast(*V); - } - return true; + Data.Transform->Scale = *Value; + } + } + if (const auto ScriptClass = GetOptionalString(ObjectValue, "scriptClass"); + ScriptClass.has_value() && !ScriptClass->empty()) { + Data.ScriptClass = *ScriptClass; + } + if (const auto GeneratedRootId = + GetOptionalString(ObjectValue, "generatedFromAssetRootId"); + GeneratedRootId.has_value() && !GeneratedRootId->empty()) { + Data.GeneratedFromAssetRootId = *GeneratedRootId; + } + if (const auto AssetRelativePath = + GetOptionalString(ObjectValue, "assetRelativePath"); + AssetRelativePath.has_value()) { + Data.AssetRelativePath = *AssetRelativePath; + } + if (const auto MaterialAssetPath = + GetOptionalString(ObjectValue, "materialAssetPath"); + MaterialAssetPath.has_value()) { + Data.MaterialAssetPath = *MaterialAssetPath; + } + if (const auto TextureAssetPath = + GetOptionalString(ObjectValue, "textureAssetPath"); + TextureAssetPath.has_value()) { + Data.TextureAssetPath = *TextureAssetPath; + } + if (const auto LightColorIt = ObjectValue.FindMember("lightColor"); + LightColorIt != ObjectValue.MemberEnd()) { + if (const auto Value = ParseVec3(LightColorIt->value); + Value.has_value()) { + if (!Data.Light.has_value()) { + Data.Light = EditorLightProperties{}; } - if (K == "physicsFriction") { - auto V = P.ParseNumber(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - Data.Physics->Friction = static_cast(*V); - } - return true; + Data.Light->Color = *Value; + } + } + if (const auto LightIntensityIt = + ObjectValue.FindMember("lightIntensity"); + LightIntensityIt != ObjectValue.MemberEnd() && + LightIntensityIt->value.IsNumber()) { + if (!Data.Light.has_value()) { + Data.Light = EditorLightProperties{}; + } + Data.Light->Intensity = LightIntensityIt->value.GetFloat(); + } + if (const auto LightDirectionIt = + ObjectValue.FindMember("lightDirection"); + LightDirectionIt != ObjectValue.MemberEnd()) { + if (const auto Value = ParseVec3(LightDirectionIt->value); + Value.has_value()) { + if (!Data.Light.has_value()) { + Data.Light = EditorLightProperties{}; } - if (K == "physicsRestitution") { - auto V = P.ParseNumber(); - if (V) { - if (!Data.Physics) Data.Physics = EditorPhysicsProperties{}; - Data.Physics->Restitution = static_cast(*V); - } - return true; + Data.Light->Direction = *Value; + } + } + if (const auto PhysicsBodyTypeIt = + ObjectValue.FindMember("physicsBodyType"); + PhysicsBodyTypeIt != ObjectValue.MemberEnd() && + PhysicsBodyTypeIt->value.IsString()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; + } + const std::string_view PhysicsBodyType( + PhysicsBodyTypeIt->value.GetString(), + PhysicsBodyTypeIt->value.GetStringLength()); + if (PhysicsBodyType == "static") { + Data.Physics->BodyType = EditorPhysicsBodyType::Static; + } else if (PhysicsBodyType == "dynamic") { + Data.Physics->BodyType = EditorPhysicsBodyType::Dynamic; + } else { + Data.Physics->BodyType = EditorPhysicsBodyType::None; + } + } + if (const auto PhysicsColliderTypeIt = + ObjectValue.FindMember("physicsColliderType"); + PhysicsColliderTypeIt != ObjectValue.MemberEnd() && + PhysicsColliderTypeIt->value.IsString()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; + } + const std::string_view PhysicsColliderType( + PhysicsColliderTypeIt->value.GetString(), + PhysicsColliderTypeIt->value.GetStringLength()); + if (PhysicsColliderType == "box") { + Data.Physics->ColliderType = EditorPhysicsColliderType::Box; + } else if (PhysicsColliderType == "sphere") { + Data.Physics->ColliderType = EditorPhysicsColliderType::Sphere; + } else { + Data.Physics->ColliderType = EditorPhysicsColliderType::None; + } + } + if (const auto PhysicsBoxHalfExtentsIt = + ObjectValue.FindMember("physicsBoxHalfExtents"); + PhysicsBoxHalfExtentsIt != ObjectValue.MemberEnd()) { + if (const auto Value = ParseVec3(PhysicsBoxHalfExtentsIt->value); + Value.has_value()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; } - return false; - }); - if (!ObjId.empty()) Objects[ObjId] = std::move(Data); - }); - return true; - } - if (Key == "meshNameToObjectId") { - P.ParseObject([&](const std::string &MeshName) -> bool { - auto ObjId = P.ParseString(); - if (ObjId) MeshNameToObjectId[MeshName] = *ObjId; - return true; - }); - return true; - } - if (Key == "worldSettings") { - P.ParseObject([&](const std::string &K) -> bool { - if (K == "skyboxColorTop") { - auto V = P.ParseVec3(); - if (V) WorldSettings.SkyboxColorTop = *V; - return true; + Data.Physics->BoxHalfExtents = *Value; } - if (K == "skyboxColorBottom") { - auto V = P.ParseVec3(); - if (V) WorldSettings.SkyboxColorBottom = *V; - return true; + } + if (const auto PhysicsSphereRadiusIt = + ObjectValue.FindMember("physicsSphereRadius"); + PhysicsSphereRadiusIt != ObjectValue.MemberEnd() && + PhysicsSphereRadiusIt->value.IsNumber()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; + } + Data.Physics->SphereRadius = PhysicsSphereRadiusIt->value.GetFloat(); + } + if (const auto PhysicsMassIt = ObjectValue.FindMember("physicsMass"); + PhysicsMassIt != ObjectValue.MemberEnd() && + PhysicsMassIt->value.IsNumber()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; } - if (K == "skyboxHDRPath") { - auto V = P.ParseString(); - if (V) WorldSettings.SkyboxHDRPath = *V; - return true; + Data.Physics->Mass = PhysicsMassIt->value.GetFloat(); + } + if (const auto PhysicsFrictionIt = + ObjectValue.FindMember("physicsFriction"); + PhysicsFrictionIt != ObjectValue.MemberEnd() && + PhysicsFrictionIt->value.IsNumber()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; } - return false; - }); - return true; + Data.Physics->Friction = PhysicsFrictionIt->value.GetFloat(); + } + if (const auto PhysicsRestitutionIt = + ObjectValue.FindMember("physicsRestitution"); + PhysicsRestitutionIt != ObjectValue.MemberEnd() && + PhysicsRestitutionIt->value.IsNumber()) { + if (!Data.Physics.has_value()) { + Data.Physics = EditorPhysicsProperties{}; + } + Data.Physics->Restitution = PhysicsRestitutionIt->value.GetFloat(); + } + + if (!ObjId.empty()) { + Objects[ObjId] = std::move(Data); + } } - return false; - }); + } - if (!Ok) { - A_CORE_ERROR("SceneFile: failed to parse {0}", Path.string()); - return std::nullopt; + if (const auto MeshNameToObjectIdIt = + Document.FindMember("meshNameToObjectId"); + MeshNameToObjectIdIt != Document.MemberEnd() && + MeshNameToObjectIdIt->value.IsObject()) { + for (const auto &Member : MeshNameToObjectIdIt->value.GetObject()) { + if (Member.value.IsString()) { + MeshNameToObjectId.emplace( + Member.name.GetString(), + std::string(Member.value.GetString(), + Member.value.GetStringLength())); + } + } + } + + if (const auto WorldSettingsIt = Document.FindMember("worldSettings"); + WorldSettingsIt != Document.MemberEnd() && + WorldSettingsIt->value.IsObject()) { + if (const auto SkyboxColorTopIt = + WorldSettingsIt->value.FindMember("skyboxColorTop"); + SkyboxColorTopIt != WorldSettingsIt->value.MemberEnd()) { + if (const auto Value = ParseVec3(SkyboxColorTopIt->value); + Value.has_value()) { + WorldSettings.SkyboxColorTop = *Value; + } + } + if (const auto SkyboxColorBottomIt = + WorldSettingsIt->value.FindMember("skyboxColorBottom"); + SkyboxColorBottomIt != WorldSettingsIt->value.MemberEnd()) { + if (const auto Value = ParseVec3(SkyboxColorBottomIt->value); + Value.has_value()) { + WorldSettings.SkyboxColorBottom = *Value; + } + } + if (const auto SkyboxHDRPath = GetOptionalString(WorldSettingsIt->value, + "skyboxHDRPath"); + SkyboxHDRPath.has_value()) { + WorldSettings.SkyboxHDRPath = *SkyboxHDRPath; + } } // --- Stage 2: reconstruct scene tree from flat nodes --- @@ -982,6 +1018,7 @@ DeserializeSceneFromJsonString(const std::filesystem::path &Path, std::unordered_map AllItems; for (const auto &Node : Nodes) { EditorSceneItem Item; + Item.Handle = Node.Handle; Item.Id = Node.Id; Item.DisplayName = Node.DisplayName; Item.Kind = Node.Kind; @@ -1015,6 +1052,7 @@ DeserializeSceneFromJsonString(const std::filesystem::path &Path, State.WorldSettings = WorldSettings; for (auto &[Id, Data] : Objects) { EditorObjectDetails Details; + Details.Handle = Data.Handle; Details.ObjectId = Id; Details.DisplayName = Data.DisplayName; Details.Kind = Data.Kind; @@ -1147,6 +1185,10 @@ DeserializeSceneFromJsonString(const std::filesystem::path &Path, } State.MeshInstances.push_back({ + .ObjectHandle = + DetailsIt != State.ObjectDetailsById.end() + ? DetailsIt->second.Handle + : SceneObjectHandle{}, .ObjectId = ObjId, .Mesh = Instance.Mesh, .Material = Instance.Material, diff --git a/Axiom/Assets/SvgTexture.cpp b/Axiom/Assets/SvgTexture.cpp index ec842310..dbbdf28a 100644 --- a/Axiom/Assets/SvgTexture.cpp +++ b/Axiom/Assets/SvgTexture.cpp @@ -1,30 +1,12 @@ #include "Assets/SvgTexture.h" -#include -#include -#include -#include +#include "HAL/SvgRasterizer.h" + #include #include -#include #include -#include -#include - -#if defined(__APPLE__) -#include -#endif namespace Axiom::Assets { -namespace { - -struct SvgViewBox { - float MinX{0.0f}; - float MinY{0.0f}; - float Width{0.0f}; - float Height{0.0f}; -}; - std::string ReadTextFile(const std::filesystem::path &Path) { std::ifstream Input(Path, std::ios::binary); if (!Input.is_open()) { @@ -34,287 +16,6 @@ std::string ReadTextFile(const std::filesystem::path &Path) { std::istreambuf_iterator()); } -std::optional FindAttributeValue(std::string_view Text, - std::string_view Name) { - const std::string Key = std::string(Name) + "=\""; - const size_t Start = Text.find(Key); - if (Start == std::string_view::npos) { - return std::nullopt; - } - const size_t ValueStart = Start + Key.size(); - const size_t ValueEnd = Text.find('"', ValueStart); - if (ValueEnd == std::string_view::npos) { - return std::nullopt; - } - return Text.substr(ValueStart, ValueEnd - ValueStart); -} - -bool ParseFloatToken(const char *&Cursor, const char *End, float &OutValue) { - while (Cursor < End && - (std::isspace(static_cast(*Cursor)) || *Cursor == ',')) { - ++Cursor; - } - if (Cursor >= End) { - return false; - } - - char *ParseEnd = nullptr; - OutValue = std::strtof(Cursor, &ParseEnd); - if (ParseEnd == Cursor) { - return false; - } - Cursor = ParseEnd; - return true; -} - -std::optional ParseViewBox(std::string_view Value) { - SvgViewBox Result{}; - const char *Cursor = Value.data(); - const char *End = Value.data() + Value.size(); - if (!ParseFloatToken(Cursor, End, Result.MinX) || - !ParseFloatToken(Cursor, End, Result.MinY) || - !ParseFloatToken(Cursor, End, Result.Width) || - !ParseFloatToken(Cursor, End, Result.Height)) { - return std::nullopt; - } - if (Result.Width <= 0.0f || Result.Height <= 0.0f) { - return std::nullopt; - } - return Result; -} - -#if defined(__APPLE__) -class SvgPathParser { -public: - explicit SvgPathParser(std::string_view PathData) - : m_Cursor(PathData.data()), m_End(PathData.data() + PathData.size()) {} - - bool Parse(CGMutablePathRef Path) { - char Command = 0; - while (SkipSeparators()) { - if (std::isalpha(static_cast(*m_Cursor)) != 0) { - Command = *m_Cursor++; - } else if (Command == 0) { - return false; - } - - switch (Command) { - case 'M': - case 'm': - if (!ParseMoveTo(Path, Command == 'm')) { - return false; - } - break; - case 'L': - case 'l': - if (!ParseLineTo(Path, Command == 'l')) { - return false; - } - break; - case 'H': - case 'h': - if (!ParseHorizontalTo(Path, Command == 'h')) { - return false; - } - break; - case 'V': - case 'v': - if (!ParseVerticalTo(Path, Command == 'v')) { - return false; - } - break; - case 'C': - case 'c': - if (!ParseCurveTo(Path, Command == 'c')) { - return false; - } - break; - case 'Z': - case 'z': - CGPathCloseSubpath(Path); - m_Current = m_SubpathStart; - break; - default: - return false; - } - } - return true; - } - -private: - bool SkipSeparators() { - while (m_Cursor < m_End && - (std::isspace(static_cast(*m_Cursor)) || - *m_Cursor == ',')) { - ++m_Cursor; - } - return m_Cursor < m_End; - } - - bool HasNumberAhead() const { - const char *Probe = m_Cursor; - while (Probe < m_End && - (std::isspace(static_cast(*Probe)) || *Probe == ',')) { - ++Probe; - } - return Probe < m_End && - (std::isdigit(static_cast(*Probe)) != 0 || - *Probe == '-' || *Probe == '+' || *Probe == '.'); - } - - bool ReadNumber(float &OutValue) { - return ParseFloatToken(m_Cursor, m_End, OutValue); - } - - bool ReadPoint(CGPoint &OutPoint, bool Relative) { - float X = 0.0f; - float Y = 0.0f; - if (!ReadNumber(X) || !ReadNumber(Y)) { - return false; - } - OutPoint = CGPointMake(Relative ? m_Current.x + X : X, - Relative ? m_Current.y + Y : Y); - return true; - } - - bool ParseMoveTo(CGMutablePathRef Path, bool Relative) { - CGPoint Point{}; - if (!ReadPoint(Point, Relative)) { - return false; - } - CGPathMoveToPoint(Path, nullptr, Point.x, Point.y); - m_Current = Point; - m_SubpathStart = Point; - - while (HasNumberAhead()) { - if (!ReadPoint(Point, Relative)) { - return false; - } - CGPathAddLineToPoint(Path, nullptr, Point.x, Point.y); - m_Current = Point; - } - return true; - } - - bool ParseLineTo(CGMutablePathRef Path, bool Relative) { - while (HasNumberAhead()) { - CGPoint Point{}; - if (!ReadPoint(Point, Relative)) { - return false; - } - CGPathAddLineToPoint(Path, nullptr, Point.x, Point.y); - m_Current = Point; - } - return true; - } - - bool ParseHorizontalTo(CGMutablePathRef Path, bool Relative) { - while (HasNumberAhead()) { - float X = 0.0f; - if (!ReadNumber(X)) { - return false; - } - m_Current.x = Relative ? (m_Current.x + X) : X; - CGPathAddLineToPoint(Path, nullptr, m_Current.x, m_Current.y); - } - return true; - } - - bool ParseVerticalTo(CGMutablePathRef Path, bool Relative) { - while (HasNumberAhead()) { - float Y = 0.0f; - if (!ReadNumber(Y)) { - return false; - } - m_Current.y = Relative ? (m_Current.y + Y) : Y; - CGPathAddLineToPoint(Path, nullptr, m_Current.x, m_Current.y); - } - return true; - } - - bool ParseCurveTo(CGMutablePathRef Path, bool Relative) { - while (HasNumberAhead()) { - CGPoint C1{}; - CGPoint C2{}; - CGPoint End{}; - if (!ReadPoint(C1, Relative) || !ReadPoint(C2, Relative) || - !ReadPoint(End, Relative)) { - return false; - } - CGPathAddCurveToPoint(Path, nullptr, C1.x, C1.y, C2.x, C2.y, End.x, End.y); - m_Current = End; - } - return true; - } - - const char *m_Cursor{nullptr}; - const char *m_End{nullptr}; - CGPoint m_Current{0.0, 0.0}; - CGPoint m_SubpathStart{0.0, 0.0}; -}; - -TextureSourceDataRef RasterizeSvg(std::string_view SvgText, - uint32_t TargetSize) { - const auto ViewBoxText = FindAttributeValue(SvgText, "viewBox"); - const auto PathText = FindAttributeValue(SvgText, "d"); - if (!ViewBoxText.has_value() || !PathText.has_value()) { - return nullptr; - } - - const auto ViewBox = ParseViewBox(*ViewBoxText); - if (!ViewBox.has_value()) { - return nullptr; - } - - CGMutablePathRef Path = CGPathCreateMutable(); - SvgPathParser Parser(*PathText); - const bool Parsed = Parser.Parse(Path); - if (!Parsed) { - CGPathRelease(Path); - return nullptr; - } - - const float LongestSide = std::max(ViewBox->Width, ViewBox->Height); - const float Scale = static_cast(TargetSize) / LongestSide; - const uint32_t Width = - std::max(1u, static_cast(std::ceil(ViewBox->Width * Scale))); - const uint32_t Height = - std::max(1u, static_cast(std::ceil(ViewBox->Height * Scale))); - - auto Texture = std::make_shared(); - Texture->Width = Width; - Texture->Height = Height; - Texture->Pixels.resize(static_cast(Width) * static_cast(Height) * 4u, 0u); - - CGColorSpaceRef ColorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef Context = CGBitmapContextCreate( - Texture->Pixels.data(), Width, Height, 8, Width * 4, ColorSpace, - static_cast(kCGImageAlphaPremultipliedLast | - kCGBitmapByteOrder32Big)); - CGColorSpaceRelease(ColorSpace); - - if (Context == nullptr) { - CGPathRelease(Path); - return nullptr; - } - - CGContextSetAllowsAntialiasing(Context, true); - CGContextSetShouldAntialias(Context, true); - CGContextTranslateCTM(Context, 0.0f, static_cast(Height)); - CGContextScaleCTM(Context, Scale, -Scale); - CGContextTranslateCTM(Context, -ViewBox->MinX, -ViewBox->MinY); - CGContextAddPath(Context, Path); - CGContextSetRGBFillColor(Context, 1.0, 1.0, 1.0, 1.0); - CGContextFillPath(Context); - - CGContextRelease(Context); - CGPathRelease(Path); - return Texture->IsValid() ? Texture : nullptr; -} -#endif - -} // namespace - TextureSourceDataRef LoadSvgTextureFromFile(const std::filesystem::path &Path, uint32_t TargetSize) { const std::string SvgText = ReadTextFile(Path); @@ -322,11 +23,6 @@ TextureSourceDataRef LoadSvgTextureFromFile(const std::filesystem::path &Path, return nullptr; } -#if defined(__APPLE__) - return RasterizeSvg(SvgText, TargetSize); -#else - (void)TargetSize; - return nullptr; -#endif + return HAL::RasterizeSvg(SvgText, TargetSize); } } // namespace Axiom::Assets diff --git a/Axiom/CMakeLists.txt b/Axiom/CMakeLists.txt index 05b97cfa..d6413812 100644 --- a/Axiom/CMakeLists.txt +++ b/Axiom/CMakeLists.txt @@ -1,7 +1,37 @@ -set(ENGINE_SOURCES - Project/ProjectSystem.cpp - Scripting/ScriptHost.cpp - Scripting/InternalCalls.cpp +set(AXIOM_CORE_SOURCES + Core/Application.cpp + Core/ApplicationModules.cpp + Core/GlfwEditorInputSource.cpp + Core/GlfwWindow.cpp + Core/HeadlessRuntimeInstrumentation.cpp + Core/HeadlessWindow.cpp + Jobs/JobSystem.cpp + Jobs/TaskScheduler.cpp + Core/Log.cpp + Core/ModuleManager.cpp + Core/Threading.cpp + Core/VulkanLoader.cpp + Core/Window.cpp + Core/WindowInputPlatform.cpp +) + +set(AXIOM_SCENE_SOURCES + Scene/CoreInstance/Instance.cpp + Scene/CoreInstance/InstancePool.cpp + Scene/Project/ProjectSystem.cpp + Scene/Session/BufferedEditorInputSource.cpp + Scene/Session/EditorCommandDispatcher.cpp + Scene/Session/EditorCommandDispatcherRuntime.cpp + Scene/Session/EditorMessageBus.cpp + Scene/Session/EditorSceneRendererAdapter.cpp + Scene/Session/EditorSceneStateManager.cpp + Scene/Session/EditorSceneStateManagerAssets.cpp + Scene/Session/EditorSession.cpp + Scene/Session/EditorSessionValidationModule.cpp + Scene/Session/StartupScene.cpp +) + +set(AXIOM_ASSET_SOURCES Assets/AssetCookManifest.cpp Assets/AssetCooker.cpp Assets/AssimpImporter.cpp @@ -13,71 +43,88 @@ set(ENGINE_SOURCES Assets/MeshAsset.cpp Assets/SceneFile.cpp Assets/SvgTexture.cpp - Core/Application.cpp - Core/GlfwEditorInputSource.cpp - Core/GlfwWindow.cpp - Core/HeadlessWindow.cpp - Core/Layer.cpp - Core/LayerStack.cpp - Core/Window.cpp - Core/WindowInputPlatform.cpp - Core/Log.cpp - Core/VulkanLoader.cpp - Remote/AxiomSessionEndpoint.cpp - Physics/PhysicsWorld.cpp - Session/BufferedEditorInputSource.cpp - Session/EditorMessageBus.cpp - Session/EditorSceneRendererAdapter.cpp - Session/EditorSession.cpp - Session/StartupScene.cpp +) + +set(AXIOM_RENDERER_SOURCES Renderer/Camera.cpp - Renderer/ForwardRenderer.cpp + Renderer/Mesh.cpp Renderer/RenderCommand.cpp Renderer/Renderer.cpp + Renderer/RendererFrameModule.cpp Renderer/RenderScene.cpp + Renderer/SceneRenderer.cpp Renderer/VideoEncoderFactory.cpp - Renderer/Vulkan/VulkanBuffer.cpp - Renderer/Vulkan/VulkanCommandContext.cpp - Renderer/Vulkan/VulkanContext.cpp - Renderer/Vulkan/VulkanDescriptors.cpp - Renderer/Vulkan/VulkanDevice.cpp - Renderer/Vulkan/VulkanImage.cpp - Renderer/Vulkan/VulkanGizmoRenderer.cpp - Renderer/Vulkan/VulkanImGuiRenderer.cpp - Renderer/Vulkan/VulkanInitializers.cpp - Renderer/Vulkan/VulkanLightBillboardRenderer.cpp - Renderer/Vulkan/VulkanMaterialResources.cpp - Renderer/Vulkan/VulkanMesh.cpp - Renderer/Vulkan/VulkanOcclusionCulling.cpp - Renderer/Vulkan/VulkanPipeline.cpp - Renderer/Vulkan/VulkanRendererBackend.cpp - Renderer/Vulkan/VulkanSceneRenderer.cpp - Renderer/Vulkan/VulkanSwapchain.cpp + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui.cpp" + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_demo.cpp" + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_draw.cpp" + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_tables.cpp" + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_widgets.cpp" + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_impl_glfw.cpp" ) -if(APPLE) - list(APPEND ENGINE_SOURCES - Renderer/MacOSVideoToolboxH264Encoder.mm - ) +set(AXIOM_PHYSICS_SOURCES + Physics/Session/EditorPhysicsController.cpp +) +if(AXIOM_ENABLE_PHYSICS) + list(APPEND AXIOM_PHYSICS_SOURCES Physics/PhysicsWorldEnabled.cpp) endif() -set(THIRDPARTY_SOURCES - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui.h" - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui.cpp" +set(AXIOM_NET_SOURCES + Net/Remote/AxiomSessionEndpoint.cpp +) - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_demo.cpp" - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_draw.cpp" - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_widgets.cpp" - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_tables.cpp" +if(AXIOM_ENABLE_SCRIPTING) + set(AXIOM_SCRIPTING_SOURCES + Scripting/InternalCallsEnabled.cpp + Scripting/ScriptHostEnabled.cpp + ) +endif() + +set(AXIOM_VULKAN_IMPLEMENTATION_SOURCES + ../AxiomInternal/AxiomRHI/RHIFactory.cpp + ../AxiomInternal/AxiomRHI/SceneRendererBackendFactory.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanContext.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanImage.cpp + ../AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanInitializers.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.cpp + ../AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.cpp "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_impl_vulkan.cpp" - "${CMAKE_SOURCE_DIR}/ThirdParty/imgui/imgui_impl_glfw.cpp" - "${CMAKE_SOURCE_DIR}/ThirdParty/volk/volk.c" "${CMAKE_SOURCE_DIR}/ThirdParty/vkbootstrap/VkBootstrap.cpp" + "${CMAKE_SOURCE_DIR}/ThirdParty/volk/volk.c" ) -add_library(AxiomCore STATIC ${ENGINE_SOURCES} ${THIRDPARTY_SOURCES}) +add_library(AxiomCore STATIC ${AXIOM_CORE_SOURCES}) +add_library(AxiomScene STATIC ${AXIOM_SCENE_SOURCES}) +add_library(AxiomAssets STATIC ${AXIOM_ASSET_SOURCES}) +add_library(AxiomRenderer STATIC ${AXIOM_RENDERER_SOURCES}) +add_library(AxiomNet STATIC ${AXIOM_NET_SOURCES}) +if(AXIOM_ENABLE_PHYSICS) + add_library(AxiomPhysics STATIC ${AXIOM_PHYSICS_SOURCES}) +endif() +if(AXIOM_ENABLE_SCRIPTING) + add_library(AxiomScripting STATIC ${AXIOM_SCRIPTING_SOURCES}) +endif() +add_library(AxiomRendererVulkanObjects OBJECT + ${AXIOM_VULKAN_IMPLEMENTATION_SOURCES}) +target_sources(AxiomRenderer PRIVATE $) -add_dependencies(AxiomCore Shaders) +add_dependencies(AxiomRenderer Shaders) set(AXIOM_WEBRTC_LINKED 0) set(AXIOM_WEBRTC_COMPILE_OPTIONS) @@ -270,10 +317,6 @@ if(AXIOM_ENABLE_WEBRTC) endif() endif() -if(AXIOM_WEBRTC_REPACKED_TARGETS) - add_dependencies(AxiomCore ${AXIOM_WEBRTC_REPACKED_TARGETS}) -endif() - set(AXIOM_VULKAN_LOADER_PATH "" CACHE STRING "Optional explicit Vulkan loader path used when platform-default discovery fails") @@ -283,62 +326,235 @@ message(STATUS "Vulkan_INCLUDE_DIRS: ${Vulkan_INCLUDE_DIRS}") message(STATUS "Vulkan_LIBRARIES: ${Vulkan_LIBRARIES}") message(STATUS "Vulkan_VERSION: ${Vulkan_VERSION}") -target_include_directories(AxiomCore PUBLIC +add_subdirectory("${CMAKE_SOURCE_DIR}/HAL" "${CMAKE_BINARY_DIR}/HAL") + +if(AXIOM_WEBRTC_REPACKED_TARGETS) + add_dependencies(AxiomHAL ${AXIOM_WEBRTC_REPACKED_TARGETS}) +endif() + +function(axiom_configure_module_target target_name) + target_include_directories(${target_name} PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/Scene" + "${CMAKE_SOURCE_DIR}/AxiomInternal" + "${CMAKE_SOURCE_DIR}/ThirdParty/glfw/include" + "${CMAKE_SOURCE_DIR}/ThirdParty/imgui" + "${CMAKE_SOURCE_DIR}/ThirdParty/glm" + "${CMAKE_SOURCE_DIR}/ThirdParty/spdlog/include" + "${rapidjson_SOURCE_DIR}/include" + ) + + target_compile_definitions(${target_name} PUBLIC + VK_NO_PROTOTYPES + AXIOM_CONTENT_DIR="${CMAKE_SOURCE_DIR}/Content" + AXIOM_PROJECTS_DIR="${CMAKE_SOURCE_DIR}/Projects" + AXIOM_SOURCE_DIR="${CMAKE_SOURCE_DIR}" + AXIOM_ENABLE_WEBRTC=$,1,0> + AXIOM_THREADED_RENDER=$,1,0> + AXIOM_WEBRTC_LINKED=${AXIOM_WEBRTC_LINKED} + ) + + if(AXIOM_VULKAN_LOADER_PATH) + string(REPLACE "\\" "\\\\" AXIOM_VULKAN_LOADER_PATH_ESCAPED + "${AXIOM_VULKAN_LOADER_PATH}") + target_compile_definitions(${target_name} PUBLIC + AXIOM_VULKAN_LOADER_PATH="${AXIOM_VULKAN_LOADER_PATH_ESCAPED}" + ) + endif() + + if(AXIOM_WEBRTC_COMPILE_OPTIONS) + target_compile_options(${target_name} PUBLIC ${AXIOM_WEBRTC_COMPILE_OPTIONS}) + endif() + + if(AXIOM_WEBRTC_LINK_OPTIONS) + target_link_options(${target_name} PUBLIC ${AXIOM_WEBRTC_LINK_OPTIONS}) + endif() + + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + target_compile_options(${target_name} PRIVATE + $<$:-O3 -march=native> + $<$:-O2 -g -march=native> + $<$:-Os> + ) + endif() + + if(AXIOM_ENABLE_TSAN) + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + message(FATAL_ERROR "AXIOM_ENABLE_TSAN requires Clang or GCC") + endif() + target_compile_options(${target_name} PUBLIC + -fsanitize=thread + -fno-omit-frame-pointer + ) + target_link_options(${target_name} PUBLIC -fsanitize=thread) + target_compile_definitions(${target_name} PUBLIC AXIOM_THREAD_SANITIZER=1) + else() + target_compile_definitions(${target_name} PUBLIC AXIOM_THREAD_SANITIZER=0) + endif() +endfunction() + +set(AXIOM_MODULE_TARGETS + AxiomCore + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet +) +if(TARGET AxiomPhysics) + list(APPEND AXIOM_MODULE_TARGETS AxiomPhysics) +endif() +if(TARGET AxiomScripting) + list(APPEND AXIOM_MODULE_TARGETS AxiomScripting) +endif() + +foreach(AXIOM_TARGET IN LISTS AXIOM_MODULE_TARGETS) + axiom_configure_module_target(${AXIOM_TARGET}) +endforeach() + +target_include_directories(AxiomScene PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/Scene" +) + +target_include_directories(AxiomNet PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/Net" +) + +target_include_directories(AxiomCore PRIVATE + "${CMAKE_SOURCE_DIR}/ThirdParty/vkbootstrap" + "${CMAKE_SOURCE_DIR}/ThirdParty/vma" + "${CMAKE_SOURCE_DIR}/ThirdParty/volk" + "${Vulkan_INCLUDE_DIRS}" +) +target_include_directories(AxiomAssets PRIVATE + "${CMAKE_SOURCE_DIR}/ThirdParty/fastgltf/include" + "${CMAKE_SOURCE_DIR}/ThirdParty/stb_image" +) +if(TARGET AxiomPhysics) + target_include_directories(AxiomPhysics PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/Physics" + ) + target_include_directories(AxiomPhysics PRIVATE + "${AXIOM_JOLT_SOURCE_DIR}" + ) +endif() +target_include_directories(AxiomRenderer PRIVATE + "${CMAKE_SOURCE_DIR}/ThirdParty/fastgltf/include" + "${CMAKE_SOURCE_DIR}/ThirdParty/stb_image" + "${CMAKE_SOURCE_DIR}/ThirdParty/vkbootstrap" + "${CMAKE_SOURCE_DIR}/ThirdParty/vma" + "${CMAKE_SOURCE_DIR}/ThirdParty/volk" + "${Vulkan_INCLUDE_DIRS}" +) +target_include_directories(AxiomRendererVulkanObjects PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/Scene" + "${CMAKE_SOURCE_DIR}/AxiomInternal" "${CMAKE_SOURCE_DIR}/ThirdParty/glfw/include" "${CMAKE_SOURCE_DIR}/ThirdParty/imgui" "${CMAKE_SOURCE_DIR}/ThirdParty/fastgltf/include" "${CMAKE_SOURCE_DIR}/ThirdParty/glm" + "${CMAKE_SOURCE_DIR}/ThirdParty/spdlog/include" "${CMAKE_SOURCE_DIR}/ThirdParty/stb_image" + "${rapidjson_SOURCE_DIR}/include" "${CMAKE_SOURCE_DIR}/ThirdParty/vkbootstrap" "${CMAKE_SOURCE_DIR}/ThirdParty/vma" "${CMAKE_SOURCE_DIR}/ThirdParty/volk" - "${CMAKE_SOURCE_DIR}/ThirdParty/spdlog/include" - "$<$:${AXIOM_JOLT_SOURCE_DIR}>" "${Vulkan_INCLUDE_DIRS}" ) +if(AXIOM_ENABLE_PHYSICS) + target_include_directories(AxiomRendererVulkanObjects PRIVATE + "${AXIOM_JOLT_SOURCE_DIR}" + ) +endif() + +target_link_libraries(AxiomCore + PUBLIC + AxiomHAL + PRIVATE + glfw + spdlog + Vulkan::Vulkan +) + +target_link_libraries(AxiomScene + PUBLIC + AxiomCore +) + +target_link_libraries(AxiomAssets + PRIVATE + AxiomHAL + fastgltf + spdlog + assimp +) -target_link_libraries(AxiomCore PUBLIC - glfw Vulkan::Vulkan fastgltf glm - spdlog assimp +target_link_libraries(AxiomRenderer + PUBLIC + AxiomCore + PRIVATE + AxiomHAL + glfw + spdlog + Vulkan::Vulkan +) + +if(TARGET AxiomPhysics) + target_link_libraries(AxiomPhysics + PUBLIC + AxiomCore + AxiomScene + ) + target_link_libraries(AxiomPhysics PRIVATE Jolt) +endif() +target_link_libraries(AxiomNet + PUBLIC + AxiomCore + AxiomScene + AxiomRenderer ) -target_compile_definitions(AxiomCore PUBLIC +if(TARGET AxiomScripting) + target_link_libraries(AxiomScripting + PUBLIC + AxiomCore + AxiomScene + AxiomHAL + ) + target_link_libraries(AxiomScripting PRIVATE CoralNative) +endif() + +target_link_libraries(AxiomRendererVulkanObjects PRIVATE + AxiomHAL + glfw + fastgltf + spdlog + assimp + Vulkan::Vulkan +) +target_compile_definitions(AxiomRendererVulkanObjects PRIVATE VK_NO_PROTOTYPES AXIOM_CONTENT_DIR="${CMAKE_SOURCE_DIR}/Content" AXIOM_PROJECTS_DIR="${CMAKE_SOURCE_DIR}/Projects" AXIOM_SOURCE_DIR="${CMAKE_SOURCE_DIR}" + AXIOM_ENABLE_WEBRTC=$,1,0> + AXIOM_THREADED_RENDER=$,1,0> + AXIOM_WEBRTC_LINKED=${AXIOM_WEBRTC_LINKED} ) - -if(AXIOM_ENABLE_WEBRTC) - target_compile_definitions(AxiomCore PUBLIC AXIOM_ENABLE_WEBRTC=1) -else() - target_compile_definitions(AxiomCore PUBLIC AXIOM_ENABLE_WEBRTC=0) -endif() - -if(AXIOM_ENABLE_PHYSICS) - target_link_libraries(AxiomCore PUBLIC Jolt) - target_compile_definitions(AxiomCore PUBLIC AXIOM_ENABLE_PHYSICS=1) -else() - target_compile_definitions(AxiomCore PUBLIC AXIOM_ENABLE_PHYSICS=0) +if(AXIOM_ENABLE_TSAN) + target_compile_options(AxiomRendererVulkanObjects PRIVATE + -fsanitize=thread + -fno-omit-frame-pointer + ) endif() if(AXIOM_ENABLE_SCRIPTING) - target_link_libraries(AxiomCore PUBLIC CoralNative) - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_ENABLED=1) - # Default managed dir: Coral.Managed output (needed by HostInstance::Initialize) set(AXIOM_CORAL_MANAGED_DIR "${CMAKE_SOURCE_DIR}/ThirdParty/Coral/Coral.Managed/bin/Debug/net9.0" CACHE PATH "Directory containing Coral.Managed.dll and its runtimeconfig") - target_compile_definitions(AxiomCore PUBLIC - AXIOM_CORAL_MANAGED_DIR="${AXIOM_CORAL_MANAGED_DIR}") - - # Engine API assembly (WraithEngine.Managed.dll) set(AXIOM_MANAGED_DIR "${CMAKE_SOURCE_DIR}/Scripting/WraithEngine.Managed/bin/Debug" CACHE PATH "Directory containing WraithEngine.Managed.dll") - target_compile_definitions(AxiomCore PUBLIC - AXIOM_MANAGED_DIR="${AXIOM_MANAGED_DIR}") set(WRAITH_MANAGED_CSPROJ "${CMAKE_SOURCE_DIR}/Scripting/WraithEngine.Managed/WraithEngine.Managed.csproj") @@ -349,49 +565,25 @@ if(AXIOM_ENABLE_SCRIPTING) COMMENT "Building WraithEngine.Managed" VERBATIM ) - add_dependencies(AxiomCore WraithEngineManaged) + add_dependencies(AxiomScripting WraithEngineManaged) if(AXIOM_SCRIPTING_WATCH AND APPLE) - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_WATCH=1) + target_compile_definitions(AxiomScripting PUBLIC AXIOM_SCRIPTING_WATCH=1) else() - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_WATCH=0) + target_compile_definitions(AxiomScripting PUBLIC AXIOM_SCRIPTING_WATCH=0) endif() if(AXIOM_SCRIPTING_TRUST_DEFAULT STREQUAL "Trusted") - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_TRUST_RESTRICTED=0) + target_compile_definitions(AxiomScripting PUBLIC AXIOM_SCRIPTING_TRUST_RESTRICTED=0) message(STATUS "ScriptHost trust profile: Trusted") else() - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_TRUST_RESTRICTED=1) + target_compile_definitions(AxiomScripting PUBLIC AXIOM_SCRIPTING_TRUST_RESTRICTED=1) message(STATUS "ScriptHost trust profile: Restricted (default)") endif() -else() - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_ENABLED=0) - target_compile_definitions(AxiomCore PUBLIC AXIOM_CORAL_MANAGED_DIR="") - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_WATCH=0) - target_compile_definitions(AxiomCore PUBLIC AXIOM_SCRIPTING_TRUST_RESTRICTED=1) -endif() - -if(AXIOM_WEBRTC_COMPILE_OPTIONS) - target_compile_options(AxiomCore PUBLIC ${AXIOM_WEBRTC_COMPILE_OPTIONS}) -endif() - -if(AXIOM_WEBRTC_LINK_OPTIONS) - target_link_options(AxiomCore PUBLIC ${AXIOM_WEBRTC_LINK_OPTIONS}) -endif() - -if(AXIOM_WEBRTC_LIBRARIES) - target_link_libraries(AxiomCore PUBLIC ${AXIOM_WEBRTC_LIBRARIES}) -endif() -target_compile_definitions(AxiomCore PUBLIC - AXIOM_WEBRTC_LINKED=${AXIOM_WEBRTC_LINKED} -) - -if(AXIOM_VULKAN_LOADER_PATH) - string(REPLACE "\\" "\\\\" AXIOM_VULKAN_LOADER_PATH_ESCAPED - "${AXIOM_VULKAN_LOADER_PATH}") - target_compile_definitions(AxiomCore PUBLIC - AXIOM_VULKAN_LOADER_PATH="${AXIOM_VULKAN_LOADER_PATH_ESCAPED}" + target_compile_definitions(AxiomScripting PUBLIC + AXIOM_CORAL_MANAGED_DIR="${AXIOM_CORAL_MANAGED_DIR}" + AXIOM_MANAGED_DIR="${AXIOM_MANAGED_DIR}" ) endif() @@ -421,15 +613,3 @@ if(APPLE) "-framework VideoToolbox" ) endif() - -if(AXIOM_ENABLE_WEBRTC AND TARGET AxiomWebRTC) - target_link_libraries(AxiomCore PUBLIC AxiomWebRTC) -endif() - -if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") - target_compile_options(AxiomCore PRIVATE - $<$:-O3 -march=native> - $<$:-O2 -g -march=native> - $<$:-Os> - ) -endif() diff --git a/Axiom/Core/Application.cpp b/Axiom/Core/Application.cpp index d9710cd7..e07bc508 100644 --- a/Axiom/Core/Application.cpp +++ b/Axiom/Core/Application.cpp @@ -1,52 +1,85 @@ #include "Application.h" +#include "Core/ApplicationModules.h" #include "Core/GlfwWindow.h" #include "Core/HeadlessWindow.h" #include "Core/Log.h" +#include "Core/Threading.h" +#include "Jobs/JobSystem.h" +#include "Renderer/Renderer.h" #include +#include #include +#include #include namespace Axiom { +namespace { +constexpr auto kMinimizedFrameDelay = std::chrono::milliseconds(16); +} + Application *Application::s_Instance = nullptr; +void RendererDeleter::operator()(Renderer *Value) const { delete Value; } + Application::Application(const ApplicationConfig &Config, const ApplicationArgs &Args) - : m_Config(Config) { + : Application(Config, Args, {}) {} + +Application::Application(const ApplicationConfig &Config, + const ApplicationArgs &Args, + RuntimeDependencies Dependencies) + : m_Config(Config), + m_Window(std::move(Dependencies.Window)), + m_RenderSurface(std::move(Dependencies.RenderSurface)), + m_Renderer(std::move(Dependencies.Renderer)) { (void)Args; s_Instance = this; Log::Init(); - - switch (m_Config.Mode) { - case RuntimeMode::LocalWindowedEditor: - case RuntimeMode::LocalPackagedGame: - m_Window = std::make_unique(m_Config.Title, m_Config.Width, - m_Config.Height); - m_RenderSurface = std::make_shared(*m_Window); - break; - case RuntimeMode::HeadlessEditorSession: - m_Window = std::make_unique(m_Config.Title, m_Config.Width, - m_Config.Height); - m_RenderSurface = - std::make_shared(m_Config.Width, m_Config.Height); - break; + Threading::SetCurrentThreadName("Axiom Game Thread"); + + if (m_Window == nullptr || m_RenderSurface == nullptr) { + switch (m_Config.Mode) { + case RuntimeMode::LocalWindowedEditor: + case RuntimeMode::LocalPackagedGame: + m_Window = std::make_unique(m_Config.Title, m_Config.Width, + m_Config.Height); + m_RenderSurface = std::make_shared(*m_Window); + break; + case RuntimeMode::HeadlessEditorSession: + m_Window = std::make_unique(m_Config.Title, m_Config.Width, + m_Config.Height); + m_RenderSurface = std::make_shared( + m_Config.Width, m_Config.Height); + break; + } } - m_Renderer = std::make_unique(); - m_Renderer->Init({ - .TargetWindow = m_Window.get(), - .TargetSurface = m_RenderSurface, - .FrameOutput = m_Config.FrameOutput, - .Width = m_Window->GetWidth(), - .Height = m_Window->GetHeight(), - }); + if (m_Renderer == nullptr && Dependencies.InitializeRenderer) { + m_Renderer.reset(new Renderer()); + } + if (m_Renderer != nullptr && Dependencies.InitializeRenderer) { + m_Renderer->Init({ + .TargetSurface = m_RenderSurface, + .FrameOutput = m_Config.FrameOutput, + .Width = m_Window->GetWidth(), + .Height = m_Window->GetHeight(), + .EnableThreadedRendering = m_Config.EnableThreadedRendering, + }); + } + Jobs::Startup(); + if (Dependencies.RegisterDefaultModules) { + RegisterDefaultModules(); + } + m_ModuleManager.InitializeModules(*this); m_LastFrameTime = std::chrono::steady_clock::now(); } Application::~Application() { - m_LayerStack.Clear(); + m_ModuleManager.ShutdownModules(*this); m_Renderer.reset(); + Jobs::Shutdown(); m_Window.reset(); if (s_Instance == this) { s_Instance = nullptr; @@ -55,9 +88,13 @@ Application::~Application() { Application &Application::Get() { return *s_Instance; } +Application *Application::TryGet() { return s_Instance; } + Window *Application::GetWindow() const { return m_Window.get(); } -void Application::PushLayer(Layer *Layer) { m_LayerStack.PushLayer(Layer); } +Renderer &Application::GetRenderer() const { return *m_Renderer; } + +Renderer *Application::TryGetRenderer() const { return m_Renderer.get(); } void Application::RequestClose() { if (m_Window) { @@ -89,6 +126,10 @@ void Application::Run() { } } +void Application::RegisterDefaultModules() { + m_ModuleManager.RegisterModule(std::make_unique()); +} + size_t Application::BeginRenderPasses() { return 1u; } void Application::PrepareRenderPass(size_t PassIndex) { (void)PassIndex; } @@ -103,36 +144,62 @@ bool Application::Step() { return false; } + if (m_Config.Mode != RuntimeMode::HeadlessEditorSession && + m_Window->IsMinimized()) { + std::this_thread::sleep_for(kMinimizedFrameDelay); + } + const auto Now = std::chrono::steady_clock::now(); m_DeltaTime = std::chrono::duration(Now - m_LastFrameTime).count(); m_LastFrameTime = Now; ++m_FrameIndex; - m_Renderer->SetCpuFrameTime(m_DeltaTime * 1000.0f); - - m_Window->PollEvents(); - - for (Layer *Layer : m_LayerStack) { - Layer->OnUpdate(); - } + m_ModuleManager.UpdateActiveModules({ + .App = *this, + .DeltaTimeSeconds = m_DeltaTime, + .FrameIndex = m_FrameIndex, + .Phase = ModuleUpdatePhase::FrameStart, + }); const size_t RenderPassCount = std::max(1u, BeginRenderPasses()); for (size_t PassIndex = 0; PassIndex < RenderPassCount; ++PassIndex) { PrepareRenderPass(PassIndex); - m_Renderer->BeginFrame(); - - for (Layer *Layer : m_LayerStack) { - Layer->OnRender(); - } - - m_Renderer->Render(); + const ModuleUpdateContext RenderContext{ + .App = *this, + .DeltaTimeSeconds = m_DeltaTime, + .FrameIndex = m_FrameIndex, + .Phase = ModuleUpdatePhase::RenderBegin, + .RenderPassIndex = PassIndex, + .RenderPassCount = RenderPassCount, + }; + m_ModuleManager.UpdateActiveModules(RenderContext); + m_ModuleManager.UpdateActiveModules({ + .App = *this, + .DeltaTimeSeconds = m_DeltaTime, + .FrameIndex = m_FrameIndex, + .Phase = ModuleUpdatePhase::Render, + .RenderPassIndex = PassIndex, + .RenderPassCount = RenderPassCount, + }); if (ShouldRenderImGuiForPass(PassIndex, RenderPassCount)) { - for (Layer *Layer : m_LayerStack) { - Layer->OnImGuiRender(); - } + m_ModuleManager.UpdateActiveModules({ + .App = *this, + .DeltaTimeSeconds = m_DeltaTime, + .FrameIndex = m_FrameIndex, + .Phase = ModuleUpdatePhase::ImGuiRender, + .RenderPassIndex = PassIndex, + .RenderPassCount = RenderPassCount, + }); } - m_Renderer->EndFrame(); + m_ModuleManager.UpdateActiveModules({ + .App = *this, + .DeltaTimeSeconds = m_DeltaTime, + .FrameIndex = m_FrameIndex, + .Phase = ModuleUpdatePhase::RenderEnd, + .RenderPassIndex = PassIndex, + .RenderPassCount = RenderPassCount, + }); } return !m_Window->ShouldClose(); } diff --git a/Axiom/Core/Application.h b/Axiom/Core/Application.h index 0f3c3c6d..c6510fea 100644 --- a/Axiom/Core/Application.h +++ b/Axiom/Core/Application.h @@ -1,20 +1,27 @@ #pragma once -#include "Renderer/RenderSurface.h" -#include "Renderer/ViewportFrameOutput.h" +#include "Core/ModuleManager.h" +#include "Core/RenderRuntime.h" #include +#include #include #include -#include #include -#include "Core/LayerStack.h" #include "Core/Window.h" -#include "Renderer/Renderer.h" -#include "Renderer/RenderSurface.h" namespace Axiom { +#ifndef AXIOM_THREADED_RENDER +#define AXIOM_THREADED_RENDER 0 +#endif + +class Renderer; +struct RendererDeleter { + void operator()(Renderer *Value) const; +}; +using RendererPtr = std::unique_ptr; + struct ApplicationArgs { char **Arguments; int ArgumentCount; @@ -32,36 +39,52 @@ struct ApplicationConfig { uint32_t Height{900}; RuntimeMode Mode{RuntimeMode::LocalWindowedEditor}; IViewportFrameOutput *FrameOutput{nullptr}; + bool EnableThreadedRendering{AXIOM_THREADED_RENDER != 0}; }; class Application { public: + struct RuntimeDependencies { + std::unique_ptr Window; + RenderSurfacePtr RenderSurface; + RendererPtr Renderer; + bool InitializeRenderer{true}; + bool RegisterDefaultModules{true}; + }; + Application(const ApplicationConfig &Config, const ApplicationArgs &Args); virtual ~Application(); static Application &Get(); + static Application *TryGet(); void Run(); bool Step(); - void PushLayer(Layer *Layer); - [[nodiscard]] IRenderSurface &GetRenderSurface() const { - return *m_RenderSurface; - } + [[nodiscard]] IRenderSurface &GetRenderSurface() const { return *m_RenderSurface; } [[nodiscard]] Window *GetWindow() const; [[nodiscard]] float GetDeltaTime() const { return m_DeltaTime; } [[nodiscard]] uint64_t GetFrameIndex() const { return m_FrameIndex; } [[nodiscard]] RuntimeMode GetRuntimeMode() const { return m_Config.Mode; } - [[nodiscard]] Renderer &GetRenderer() const { return *m_Renderer; } + [[nodiscard]] Renderer &GetRenderer() const; + [[nodiscard]] Renderer *TryGetRenderer() const; + [[nodiscard]] ModuleManager &GetModuleManager() { return m_ModuleManager; } + [[nodiscard]] const ModuleManager &GetModuleManager() const { + return m_ModuleManager; + } void RequestClose(); void SetRendererViewMode(RendererViewMode ViewMode); void SetViewportFrameUser(SessionUserId User); void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput); protected: + Application(const ApplicationConfig &Config, const ApplicationArgs &Args, + RuntimeDependencies Dependencies); + virtual size_t BeginRenderPasses(); virtual void PrepareRenderPass(size_t PassIndex); virtual bool ShouldRenderImGuiForPass(size_t PassIndex, size_t PassCount) const; + void RegisterDefaultModules(); private: static Application *s_Instance; @@ -69,8 +92,8 @@ class Application { ApplicationConfig m_Config; std::unique_ptr m_Window; RenderSurfacePtr m_RenderSurface; - std::unique_ptr m_Renderer; - LayerStack m_LayerStack; + RendererPtr m_Renderer; + ModuleManager m_ModuleManager; std::chrono::steady_clock::time_point m_LastFrameTime; float m_DeltaTime{0.0f}; uint64_t m_FrameIndex{0}; diff --git a/Axiom/Core/ApplicationModules.cpp b/Axiom/Core/ApplicationModules.cpp new file mode 100644 index 00000000..92a2f965 --- /dev/null +++ b/Axiom/Core/ApplicationModules.cpp @@ -0,0 +1,26 @@ +#include "Core/ApplicationModules.h" + +#include "Core/Application.h" + +namespace Axiom { +std::string_view WindowEventsModule::GetName() const { + return "Core.WindowEvents"; +} + +bool WindowEventsModule::Initialize(Application &App) { + (void)App; + return true; +} + +void WindowEventsModule::Update(const ModuleUpdateContext &Context) { + if (Context.Phase != ModuleUpdatePhase::FrameStart) { + return; + } + + if (Window *Window = Context.App.GetWindow(); Window != nullptr) { + Window->PollEvents(); + } +} + +void WindowEventsModule::Shutdown(Application &App) { (void)App; } +} // namespace Axiom diff --git a/Axiom/Core/ApplicationModules.h b/Axiom/Core/ApplicationModules.h new file mode 100644 index 00000000..9c3c1827 --- /dev/null +++ b/Axiom/Core/ApplicationModules.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Core/IModule.h" + +namespace Axiom { +class WindowEventsModule final : public IModule { +public: + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; +}; +} // namespace Axiom diff --git a/Axiom/Core/GlfwEditorInputSource.cpp b/Axiom/Core/GlfwEditorInputSource.cpp index f49bb354..e1519f25 100644 --- a/Axiom/Core/GlfwEditorInputSource.cpp +++ b/Axiom/Core/GlfwEditorInputSource.cpp @@ -50,7 +50,7 @@ void GlfwEditorInputSource::Tick(const EditorInputFrame &Frame) { } glm::vec3 HorizontalRight = glm::normalize( - glm::cross(glm::vec3(0.0f, 1.0f, 0.0f), HorizontalForward)); + glm::cross(HorizontalForward, glm::vec3(0.0f, 1.0f, 0.0f))); glm::vec3 Movement{0.0f}; if (m_Platform.IsKeyPressed(GLFW_KEY_W)) { @@ -60,10 +60,10 @@ void GlfwEditorInputSource::Tick(const EditorInputFrame &Frame) { Movement -= HorizontalForward; } if (m_Platform.IsKeyPressed(GLFW_KEY_D)) { - Movement -= HorizontalRight; + Movement += HorizontalRight; } if (m_Platform.IsKeyPressed(GLFW_KEY_A)) { - Movement += HorizontalRight; + Movement -= HorizontalRight; } if (m_Platform.IsKeyPressed(GLFW_KEY_SPACE)) { Movement += glm::vec3(0.0f, 1.0f, 0.0f); diff --git a/Axiom/Core/GlfwWindow.cpp b/Axiom/Core/GlfwWindow.cpp index 4ccb0e96..dd84ad7e 100644 --- a/Axiom/Core/GlfwWindow.cpp +++ b/Axiom/Core/GlfwWindow.cpp @@ -78,7 +78,31 @@ bool GlfwWindow::ShouldClose() const { return glfwWindowShouldClose(m_NativeHandle); } +bool GlfwWindow::IsMinimized() const { + return glfwGetWindowAttrib(m_NativeHandle, GLFW_ICONIFIED) != 0; +} + void GlfwWindow::RequestClose() { glfwSetWindowShouldClose(m_NativeHandle, 1); } void *GlfwWindow::GetNativeHandle() const { return m_NativeHandle; } + +bool GlfwWindow::SupportsPresentationBackend( + PresentationBackendType Backend) const { + return Backend == PresentationBackendType::Vulkan && glfwVulkanSupported(); +} + +PresentationSurfaceResult +GlfwWindow::CreatePresentationSurface(PresentationBackendType Backend, + void *Instance, void *Surface) const { + if (Backend != PresentationBackendType::Vulkan || Surface == nullptr) { + return PresentationSurfaceResult::Unsupported; + } + + const VkResult Result = + glfwCreateWindowSurface(reinterpret_cast(Instance), + m_NativeHandle, nullptr, + static_cast(Surface)); + return Result == VK_SUCCESS ? PresentationSurfaceResult::Success + : PresentationSurfaceResult::InitializationFailed; +} } // namespace Axiom diff --git a/Axiom/Core/GlfwWindow.h b/Axiom/Core/GlfwWindow.h index c8429f5e..9279aeb8 100644 --- a/Axiom/Core/GlfwWindow.h +++ b/Axiom/Core/GlfwWindow.h @@ -17,8 +17,14 @@ class GlfwWindow final : public Window { void SetCursorMode(CursorMode Mode) override; [[nodiscard]] CursorMode GetCursorMode() const override; [[nodiscard]] bool ShouldClose() const override; + [[nodiscard]] bool IsMinimized() const override; void RequestClose() override; [[nodiscard]] void *GetNativeHandle() const override; + [[nodiscard]] bool + SupportsPresentationBackend(PresentationBackendType Backend) const override; + PresentationSurfaceResult + CreatePresentationSurface(PresentationBackendType Backend, void *Instance, + void *Surface) const override; private: GLFWwindow *m_NativeHandle{nullptr}; diff --git a/Axiom/Core/HeadlessRuntimeInstrumentation.cpp b/Axiom/Core/HeadlessRuntimeInstrumentation.cpp new file mode 100644 index 00000000..4620beb7 --- /dev/null +++ b/Axiom/Core/HeadlessRuntimeInstrumentation.cpp @@ -0,0 +1,214 @@ +#include "Core/HeadlessRuntimeInstrumentation.h" + +#include "Core/Log.h" + +#include +#include +#include + +namespace Axiom { +namespace { +#if !defined(NDEBUG) +struct HeadlessRuntimeInstrumentationState { + uint64_t EngineTickCount{0}; + uint64_t TotalRenderPasses{0}; + uint64_t LastTickRenderPassCount{0}; + size_t ActiveRemoteClientCount{0}; + size_t PendingOffscreenReadbacks{0}; + uint64_t TotalOffscreenReadbacksSubmitted{0}; + uint64_t TotalOffscreenReadbacksCompleted{0}; + uint64_t LastLoggedRenderPassCount{0}; + size_t LastLoggedRemoteClientCount{0}; + size_t LastLoggedPendingReadbacks{0}; + std::unordered_map ClientCadence; +}; + +HeadlessRuntimeInstrumentationState &GetState() { + static HeadlessRuntimeInstrumentationState State; + return State; +} + +std::mutex &GetStateMutex() { + static std::mutex Mutex; + return Mutex; +} + +std::string MakeCadenceKey(const std::string &ClientId, SessionUserId User, + bool IsLocal) { + if (IsLocal || ClientId.empty()) { + return "__local__:" + std::to_string(User.Value); + } + return ClientId; +} + +bool ShouldLogSummary(const HeadlessRuntimeInstrumentationState &State) { + return State.EngineTickCount <= 1u || + State.LastTickRenderPassCount != State.LastLoggedRenderPassCount || + State.ActiveRemoteClientCount != State.LastLoggedRemoteClientCount || + State.PendingOffscreenReadbacks != State.LastLoggedPendingReadbacks || + (State.EngineTickCount % 120u) == 0u; +} + +void EnsureLoggingInitialized() { + static std::once_flag Flag; + std::call_once(Flag, []() { Log::Init(); }); +} +#endif +} // namespace + +void HeadlessRuntimeInstrumentation::Reset() { +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + GetState() = {}; +#endif +} + +void HeadlessRuntimeInstrumentation::RecordHeadlessTick( + uint64_t EngineTick, size_t RenderPassCount, size_t ActiveRemoteClientCount) { +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + auto &State = GetState(); + State.EngineTickCount = EngineTick; + State.LastTickRenderPassCount = RenderPassCount; + State.TotalRenderPasses += RenderPassCount; + State.ActiveRemoteClientCount = ActiveRemoteClientCount; + if (ShouldLogSummary(State)) { + EnsureLoggingInitialized(); + A_CORE_INFO( + "HeadlessRuntime: tick={} render_passes={} active_remote_clients={} " + "pending_readbacks={} total_render_passes={}", + State.EngineTickCount, State.LastTickRenderPassCount, + State.ActiveRemoteClientCount, State.PendingOffscreenReadbacks, + State.TotalRenderPasses); + State.LastLoggedRenderPassCount = State.LastTickRenderPassCount; + State.LastLoggedRemoteClientCount = State.ActiveRemoteClientCount; + State.LastLoggedPendingReadbacks = State.PendingOffscreenReadbacks; + } +#else + (void)EngineTick; + (void)RenderPassCount; + (void)ActiveRemoteClientCount; +#endif +} + +void HeadlessRuntimeInstrumentation::RecordHeadlessRenderPass( + uint64_t EngineTick, size_t PassIndex, const std::string &ClientId, + SessionUserId User, bool IsLocal) { +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + auto &State = GetState(); + HeadlessClientCadenceSnapshot &Client = + State.ClientCadence[MakeCadenceKey(ClientId, User, IsLocal)]; + if (Client.RenderPassCount == 0) { + Client.ClientId = ClientId; + Client.User = User; + Client.IsLocal = IsLocal; + } + + const uint64_t PreviousTick = Client.LastEngineTick; + ++Client.RenderPassCount; + Client.LastEngineTick = EngineTick; + if (PreviousTick > 0u && EngineTick > PreviousTick) { + Client.LastTicksSincePreviousRender = EngineTick - PreviousTick; + Client.MaxTicksBetweenRenders = std::max(Client.MaxTicksBetweenRenders, + Client.LastTicksSincePreviousRender); + } + + EnsureLoggingInitialized(); + A_CORE_TRACE( + "HeadlessRuntime: tick={} pass={} client='{}' user={} local={} " + "client_render_passes={}", + EngineTick, PassIndex, ClientId.empty() ? "" : ClientId, User.Value, + IsLocal ? "true" : "false", Client.RenderPassCount); +#else + (void)EngineTick; + (void)PassIndex; + (void)ClientId; + (void)User; + (void)IsLocal; +#endif +} + +void HeadlessRuntimeInstrumentation::RecordPendingOffscreenReadbacks( + size_t PendingReadbacks) { +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + GetState().PendingOffscreenReadbacks = PendingReadbacks; +#else + (void)PendingReadbacks; +#endif +} + +void HeadlessRuntimeInstrumentation::RecordOffscreenReadbackSubmitted( + uint64_t FrameNumber, SessionUserId User, size_t PendingReadbacks) { +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + auto &State = GetState(); + ++State.TotalOffscreenReadbacksSubmitted; + State.PendingOffscreenReadbacks = PendingReadbacks; + EnsureLoggingInitialized(); + A_CORE_TRACE( + "HeadlessRuntime: submitted offscreen readback frame={} user={} " + "pending_readbacks={}", + FrameNumber, User.Value, State.PendingOffscreenReadbacks); +#else + (void)FrameNumber; + (void)User; + (void)PendingReadbacks; +#endif +} + +void HeadlessRuntimeInstrumentation::RecordOffscreenReadbackCompleted( + uint64_t FrameNumber, SessionUserId User, size_t PendingReadbacks) { +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + auto &State = GetState(); + ++State.TotalOffscreenReadbacksCompleted; + State.PendingOffscreenReadbacks = PendingReadbacks; + EnsureLoggingInitialized(); + A_CORE_TRACE( + "HeadlessRuntime: completed offscreen readback frame={} user={} " + "pending_readbacks={}", + FrameNumber, User.Value, State.PendingOffscreenReadbacks); +#else + (void)FrameNumber; + (void)User; + (void)PendingReadbacks; +#endif +} + +HeadlessRuntimeInstrumentationSnapshot +HeadlessRuntimeInstrumentation::GetSnapshot() { + HeadlessRuntimeInstrumentationSnapshot Snapshot{}; +#if !defined(NDEBUG) + std::scoped_lock Lock(GetStateMutex()); + const auto &State = GetState(); + Snapshot.EngineTickCount = State.EngineTickCount; + Snapshot.TotalRenderPasses = State.TotalRenderPasses; + Snapshot.LastTickRenderPassCount = State.LastTickRenderPassCount; + Snapshot.ActiveRemoteClientCount = State.ActiveRemoteClientCount; + Snapshot.PendingOffscreenReadbacks = State.PendingOffscreenReadbacks; + Snapshot.TotalOffscreenReadbacksSubmitted = + State.TotalOffscreenReadbacksSubmitted; + Snapshot.TotalOffscreenReadbacksCompleted = + State.TotalOffscreenReadbacksCompleted; + Snapshot.ClientCadence.reserve(State.ClientCadence.size()); + for (const auto &[Key, Client] : State.ClientCadence) { + (void)Key; + Snapshot.ClientCadence.push_back(Client); + } + std::sort(Snapshot.ClientCadence.begin(), Snapshot.ClientCadence.end(), + [](const HeadlessClientCadenceSnapshot &Left, + const HeadlessClientCadenceSnapshot &Right) { + if (Left.IsLocal != Right.IsLocal) { + return Left.IsLocal < Right.IsLocal; + } + if (Left.ClientId != Right.ClientId) { + return Left.ClientId < Right.ClientId; + } + return Left.User.Value < Right.User.Value; + }); +#endif + return Snapshot; +} +} // namespace Axiom diff --git a/Axiom/Core/HeadlessRuntimeInstrumentation.h b/Axiom/Core/HeadlessRuntimeInstrumentation.h new file mode 100644 index 00000000..69fb44e9 --- /dev/null +++ b/Axiom/Core/HeadlessRuntimeInstrumentation.h @@ -0,0 +1,60 @@ +#pragma once + +#include "Session/SessionTypes.h" + +#include +#include +#include +#include + +namespace Axiom { +#if defined(NDEBUG) +inline constexpr bool kHeadlessRuntimeInstrumentationEnabled = false; +#else +inline constexpr bool kHeadlessRuntimeInstrumentationEnabled = true; +#endif + +struct HeadlessClientCadenceSnapshot { + std::string ClientId; + SessionUserId User{}; + bool IsLocal{false}; + uint64_t RenderPassCount{0}; + uint64_t LastEngineTick{0}; + uint64_t LastTicksSincePreviousRender{0}; + uint64_t MaxTicksBetweenRenders{0}; +}; + +struct HeadlessRuntimeInstrumentationSnapshot { + bool Enabled{kHeadlessRuntimeInstrumentationEnabled}; + uint64_t EngineTickCount{0}; + uint64_t TotalRenderPasses{0}; + uint64_t LastTickRenderPassCount{0}; + size_t ActiveRemoteClientCount{0}; + size_t PendingOffscreenReadbacks{0}; + uint64_t TotalOffscreenReadbacksSubmitted{0}; + uint64_t TotalOffscreenReadbacksCompleted{0}; + std::vector ClientCadence; +}; + +class HeadlessRuntimeInstrumentation { +public: + static constexpr bool IsEnabled() { + return kHeadlessRuntimeInstrumentationEnabled; + } + + static void Reset(); + static void RecordHeadlessTick(uint64_t EngineTick, size_t RenderPassCount, + size_t ActiveRemoteClientCount); + static void RecordHeadlessRenderPass(uint64_t EngineTick, size_t PassIndex, + const std::string &ClientId, + SessionUserId User, bool IsLocal); + static void RecordPendingOffscreenReadbacks(size_t PendingReadbacks); + static void RecordOffscreenReadbackSubmitted(uint64_t FrameNumber, + SessionUserId User, + size_t PendingReadbacks); + static void RecordOffscreenReadbackCompleted(uint64_t FrameNumber, + SessionUserId User, + size_t PendingReadbacks); + static HeadlessRuntimeInstrumentationSnapshot GetSnapshot(); +}; +} // namespace Axiom diff --git a/Axiom/Core/HeadlessWindow.cpp b/Axiom/Core/HeadlessWindow.cpp index 29630568..0cbb3104 100644 --- a/Axiom/Core/HeadlessWindow.cpp +++ b/Axiom/Core/HeadlessWindow.cpp @@ -28,7 +28,24 @@ CursorMode HeadlessWindow::GetCursorMode() const { return m_CursorMode; } bool HeadlessWindow::ShouldClose() const { return m_ShouldClose; } +bool HeadlessWindow::IsMinimized() const { return false; } + void HeadlessWindow::RequestClose() { m_ShouldClose = true; } void *HeadlessWindow::GetNativeHandle() const { return nullptr; } + +bool HeadlessWindow::SupportsPresentationBackend( + PresentationBackendType Backend) const { + (void)Backend; + return false; +} + +PresentationSurfaceResult +HeadlessWindow::CreatePresentationSurface(PresentationBackendType Backend, + void *Instance, void *Surface) const { + (void)Backend; + (void)Instance; + (void)Surface; + return PresentationSurfaceResult::InitializationFailed; +} } // namespace Axiom diff --git a/Axiom/Core/HeadlessWindow.h b/Axiom/Core/HeadlessWindow.h index 0eedb41e..0781ceeb 100644 --- a/Axiom/Core/HeadlessWindow.h +++ b/Axiom/Core/HeadlessWindow.h @@ -14,8 +14,14 @@ class HeadlessWindow final : public Window { void SetCursorMode(CursorMode Mode) override; [[nodiscard]] CursorMode GetCursorMode() const override; [[nodiscard]] bool ShouldClose() const override; + [[nodiscard]] bool IsMinimized() const override; void RequestClose() override; [[nodiscard]] void *GetNativeHandle() const override; + [[nodiscard]] bool + SupportsPresentationBackend(PresentationBackendType Backend) const override; + PresentationSurfaceResult + CreatePresentationSurface(PresentationBackendType Backend, void *Instance, + void *Surface) const override; private: CursorMode m_CursorMode{CursorMode::Normal}; diff --git a/Axiom/Core/IModule.h b/Axiom/Core/IModule.h new file mode 100644 index 00000000..6a9b1bd2 --- /dev/null +++ b/Axiom/Core/IModule.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace Axiom { +class Application; + +enum class ModuleUpdatePhase { + FrameStart, + RenderBegin, + Render, + ImGuiRender, + RenderEnd, +}; + +struct ModuleUpdateContext { + Application &App; + float DeltaTimeSeconds{0.0f}; + uint64_t FrameIndex{0}; + ModuleUpdatePhase Phase{ModuleUpdatePhase::FrameStart}; + size_t RenderPassIndex{0}; + size_t RenderPassCount{1}; +}; + +class IModule { +public: + virtual ~IModule() = default; + + [[nodiscard]] virtual std::string_view GetName() const = 0; + virtual bool Initialize(Application &App) = 0; + virtual void Update(const ModuleUpdateContext &Context) = 0; + virtual void Shutdown(Application &App) = 0; +}; +} // namespace Axiom diff --git a/Axiom/Core/Layer.cpp b/Axiom/Core/Layer.cpp deleted file mode 100644 index df8ebc5a..00000000 --- a/Axiom/Core/Layer.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "Core/Layer.h" - -#include - -namespace Axiom { -Layer::Layer(std::string Name) : m_Name(std::move(Name)) {} -} // namespace Axiom diff --git a/Axiom/Core/Layer.h b/Axiom/Core/Layer.h deleted file mode 100644 index 5aa26d13..00000000 --- a/Axiom/Core/Layer.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -namespace Axiom { -class Layer { -public: - explicit Layer(std::string Name = "Layer"); - virtual ~Layer() = default; - - virtual void OnAttach() {} - virtual void OnDetach() {} - virtual void OnUpdate() {} - virtual void OnRender() {} - virtual void OnImGuiRender() {} - - const std::string &GetName() const { return m_Name; } - -private: - std::string m_Name; -}; -} // namespace Axiom diff --git a/Axiom/Core/LayerStack.cpp b/Axiom/Core/LayerStack.cpp deleted file mode 100644 index 3e827319..00000000 --- a/Axiom/Core/LayerStack.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "Core/LayerStack.h" - -namespace Axiom { -LayerStack::~LayerStack() { Clear(); } - -void LayerStack::Clear() { - for (Layer *Layer : m_Layers) { - Layer->OnDetach(); - delete Layer; - } - m_Layers.clear(); -} - -void LayerStack::PushLayer(Layer *Layer) { - m_Layers.emplace_back(Layer); - Layer->OnAttach(); -} -} // namespace Axiom diff --git a/Axiom/Core/LayerStack.h b/Axiom/Core/LayerStack.h deleted file mode 100644 index 3818eb75..00000000 --- a/Axiom/Core/LayerStack.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "Core/Layer.h" - -#include - -namespace Axiom { -class LayerStack { -public: - LayerStack() = default; - ~LayerStack(); - - LayerStack(const LayerStack &) = delete; - LayerStack &operator=(const LayerStack &) = delete; - - void PushLayer(Layer *Layer); - void Clear(); - - std::vector::iterator begin() { return m_Layers.begin(); } - std::vector::iterator end() { return m_Layers.end(); } - -private: - std::vector m_Layers; -}; -} // namespace Axiom diff --git a/Axiom/Core/Log.cpp b/Axiom/Core/Log.cpp index 2cbd605b..8eb1cfb7 100644 --- a/Axiom/Core/Log.cpp +++ b/Axiom/Core/Log.cpp @@ -8,6 +8,20 @@ std::shared_ptr Log::s_CoreLogger; std::shared_ptr Log::s_ClientLogger; void Log::Init() { + if (s_CoreLogger != nullptr && s_ClientLogger != nullptr) { + return; + } + + if (s_CoreLogger == nullptr) { + s_CoreLogger = spdlog::get("AXIOM"); + } + if (s_ClientLogger == nullptr) { + s_ClientLogger = spdlog::get("APP"); + } + if (s_CoreLogger != nullptr && s_ClientLogger != nullptr) { + return; + } + std::vector LogSinks; LogSinks.emplace_back( std::make_shared()); @@ -17,17 +31,21 @@ void Log::Init() { LogSinks[0]->set_pattern("%^[%T] %n: %v%$"); LogSinks[1]->set_pattern("[%T] [%l] %n: %v"); - s_CoreLogger = - std::make_shared("AXIOM", begin(LogSinks), end(LogSinks)); - spdlog::register_logger(s_CoreLogger); - s_CoreLogger->set_level(spdlog::level::trace); - s_CoreLogger->flush_on(spdlog::level::trace); - - s_ClientLogger = - std::make_shared("APP", begin(LogSinks), end(LogSinks)); - spdlog::register_logger(s_ClientLogger); - s_ClientLogger->set_level(spdlog::level::trace); - s_ClientLogger->flush_on(spdlog::level::trace); + if (s_CoreLogger == nullptr) { + s_CoreLogger = std::make_shared("AXIOM", begin(LogSinks), + end(LogSinks)); + spdlog::register_logger(s_CoreLogger); + s_CoreLogger->set_level(spdlog::level::trace); + s_CoreLogger->flush_on(spdlog::level::trace); + } + + if (s_ClientLogger == nullptr) { + s_ClientLogger = std::make_shared("APP", begin(LogSinks), + end(LogSinks)); + spdlog::register_logger(s_ClientLogger); + s_ClientLogger->set_level(spdlog::level::trace); + s_ClientLogger->flush_on(spdlog::level::trace); + } } void Log::Flush() { @@ -35,4 +53,3 @@ void Log::Flush() { s_ClientLogger->flush(); } } // namespace Axiom - diff --git a/Axiom/Core/ModuleManager.cpp b/Axiom/Core/ModuleManager.cpp new file mode 100644 index 00000000..7785b7b8 --- /dev/null +++ b/Axiom/Core/ModuleManager.cpp @@ -0,0 +1,148 @@ +#include "Core/ModuleManager.h" + +#include "Core/Application.h" + +namespace Axiom { +bool ModuleManager::RegisterModule(std::unique_ptr Module, + bool ActiveByDefault) { + if (Module == nullptr) { + return false; + } + + if (HasModule(Module->GetName())) { + return false; + } + + ModuleRecord &Record = + m_Modules.emplace_back(ModuleRecord{.Module = std::move(Module), + .Lifecycle = ModuleLifecycleState::Registered, + .IsLoaded = false, + .IsActive = ActiveByDefault}); + if (m_RuntimeApplication != nullptr) { + InitializeRecord(Record, *m_RuntimeApplication); + } + return true; +} + +void ModuleManager::InitializeModules(Application &App) { + m_RuntimeApplication = &App; + for (ModuleRecord &Record : m_Modules) { + InitializeRecord(Record, App); + } +} + +void ModuleManager::UpdateActiveModules(const ModuleUpdateContext &Context) { + for (ModuleRecord &Record : m_Modules) { + if (!Record.IsLoaded || !Record.IsActive) { + continue; + } + Record.Module->Update(Context); + } +} + +void ModuleManager::ShutdownModules(Application &App) { + for (auto It = m_Modules.rbegin(); It != m_Modules.rend(); ++It) { + if (!It->IsLoaded) { + continue; + } + + It->Module->Shutdown(App); + It->IsLoaded = false; + It->Lifecycle = ModuleLifecycleState::Registered; + } + m_RuntimeApplication = nullptr; +} + +bool ModuleManager::HasModule(std::string_view Name) const { + return FindRecord(Name) != nullptr; +} + +bool ModuleManager::SetModuleActive(std::string_view Name, bool Active) { + ModuleRecord *Record = FindRecord(Name); + if (Record == nullptr) { + return false; + } + + Record->IsActive = Active; + return true; +} + +bool ModuleManager::IsModuleActive(std::string_view Name) const { + const ModuleRecord *Record = FindRecord(Name); + return Record != nullptr && Record->IsActive; +} + +std::optional ModuleManager::GetModuleState( + std::string_view Name) const { + const ModuleRecord *Record = FindRecord(Name); + if (Record == nullptr) { + return std::nullopt; + } + + return ModuleState{ + .Name = std::string(Record->Module->GetName()), + .Lifecycle = Record->Lifecycle, + .IsLoaded = Record->IsLoaded, + .IsActive = Record->IsActive, + }; +} + +std::vector ModuleManager::GetModuleStates() const { + std::vector States; + States.reserve(m_Modules.size()); + for (const ModuleRecord &Record : m_Modules) { + States.push_back({ + .Name = std::string(Record.Module->GetName()), + .Lifecycle = Record.Lifecycle, + .IsLoaded = Record.IsLoaded, + .IsActive = Record.IsActive, + }); + } + return States; +} + +IModule *ModuleManager::FindModule(std::string_view Name) { + ModuleRecord *Record = FindRecord(Name); + return Record != nullptr ? Record->Module.get() : nullptr; +} + +const IModule *ModuleManager::FindModule(std::string_view Name) const { + const ModuleRecord *Record = FindRecord(Name); + return Record != nullptr ? Record->Module.get() : nullptr; +} + +ModuleManager::ModuleRecord *ModuleManager::FindRecord(std::string_view Name) { + for (ModuleRecord &Record : m_Modules) { + if (Record.Module->GetName() == Name) { + return &Record; + } + } + return nullptr; +} + +const ModuleManager::ModuleRecord * +ModuleManager::FindRecord(std::string_view Name) const { + for (const ModuleRecord &Record : m_Modules) { + if (Record.Module->GetName() == Name) { + return &Record; + } + } + return nullptr; +} + +void ModuleManager::InitializeRecord(ModuleRecord &Record, Application &App) { + if (Record.IsLoaded || Record.Lifecycle == ModuleLifecycleState::Failed) { + return; + } + + if (!Record.Module->Initialize(App)) { + Record.Lifecycle = ModuleLifecycleState::Failed; + Record.IsLoaded = false; + Record.IsActive = false; + return; + } + + Record.Lifecycle = ModuleLifecycleState::Initialized; + Record.IsLoaded = true; +} +} // namespace Axiom diff --git a/Axiom/Core/ModuleManager.h b/Axiom/Core/ModuleManager.h new file mode 100644 index 00000000..b989cb10 --- /dev/null +++ b/Axiom/Core/ModuleManager.h @@ -0,0 +1,63 @@ +#pragma once + +#include "Core/IModule.h" + +#include +#include +#include +#include +#include + +namespace Axiom { +enum class ModuleLifecycleState { + Registered, + Initialized, + Failed, +}; + +struct ModuleState { + std::string Name; + ModuleLifecycleState Lifecycle{ModuleLifecycleState::Registered}; + bool IsLoaded{false}; + bool IsActive{false}; +}; + +class ModuleManager { +public: + ModuleManager() = default; + ~ModuleManager() = default; + + ModuleManager(const ModuleManager &) = delete; + ModuleManager &operator=(const ModuleManager &) = delete; + + bool RegisterModule(std::unique_ptr Module, + bool ActiveByDefault = true); + void InitializeModules(Application &App); + void UpdateActiveModules(const ModuleUpdateContext &Context); + void ShutdownModules(Application &App); + + bool HasModule(std::string_view Name) const; + bool SetModuleActive(std::string_view Name, bool Active); + bool IsModuleActive(std::string_view Name) const; + std::optional GetModuleState(std::string_view Name) const; + std::vector GetModuleStates() const; + + IModule *FindModule(std::string_view Name); + const IModule *FindModule(std::string_view Name) const; + +private: + struct ModuleRecord { + std::unique_ptr Module; + ModuleLifecycleState Lifecycle{ModuleLifecycleState::Registered}; + bool IsLoaded{false}; + bool IsActive{false}; + }; + + ModuleRecord *FindRecord(std::string_view Name); + const ModuleRecord *FindRecord(std::string_view Name) const; + void InitializeRecord(ModuleRecord &Record, Application &App); + + std::vector m_Modules; + Application *m_RuntimeApplication{nullptr}; +}; +} // namespace Axiom diff --git a/Axiom/Core/Platform.h b/Axiom/Core/Platform.h index 0c8d76b0..15019195 100644 --- a/Axiom/Core/Platform.h +++ b/Axiom/Core/Platform.h @@ -1,27 +1,3 @@ #pragma once -#if defined(_WIN32) -#define AXIOM_PLATFORM_WINDOWS 1 -#elif defined(__APPLE__) && defined(__MACH__) -#define AXIOM_PLATFORM_MACOS 1 -#elif defined(__linux__) -#define AXIOM_PLATFORM_LINUX 1 -#else -#error "Axiom does not support this platform yet." -#endif - -#ifndef AXIOM_PLATFORM_WINDOWS -#define AXIOM_PLATFORM_WINDOWS 0 -#endif - -#ifndef AXIOM_PLATFORM_MACOS -#define AXIOM_PLATFORM_MACOS 0 -#endif - -#ifndef AXIOM_PLATFORM_LINUX -#define AXIOM_PLATFORM_LINUX 0 -#endif - -#if (AXIOM_PLATFORM_WINDOWS + AXIOM_PLATFORM_MACOS + AXIOM_PLATFORM_LINUX) != 1 -#error "Axiom platform detection must resolve to exactly one target platform." -#endif +#include "HAL/Platform.h" diff --git a/Axiom/Core/RenderRuntime.h b/Axiom/Core/RenderRuntime.h new file mode 100644 index 00000000..a4c76568 --- /dev/null +++ b/Axiom/Core/RenderRuntime.h @@ -0,0 +1,124 @@ +#pragma once + +#include "Core/Window.h" +#include "Session/SessionTypes.h" + +#include +#include +#include +#include + +namespace Axiom { +enum class RendererViewMode : uint32_t { + Lit = 0, + Unlit = 1, + Wireframe = 2, +}; + +enum class RenderSurfaceKind { Window, Offscreen }; + +class IRenderSurface { +public: + virtual ~IRenderSurface() = default; + + [[nodiscard]] virtual RenderSurfaceKind GetKind() const = 0; + [[nodiscard]] virtual bool SupportsPresentation() const = 0; + [[nodiscard]] virtual uint32_t GetWidth() const = 0; + [[nodiscard]] virtual uint32_t GetHeight() const = 0; + [[nodiscard]] virtual bool IsMinimized() const = 0; + [[nodiscard]] virtual void *GetNativeWindowHandle() const = 0; + [[nodiscard]] virtual bool + SupportsPresentationBackend(PresentationBackendType Backend) const = 0; + virtual PresentationSurfaceResult + CreatePresentationSurface(PresentationBackendType Backend, void *Instance, + void *Surface) const = 0; +}; + +class WindowRenderSurface final : public IRenderSurface { +public: + explicit WindowRenderSurface(Window &TargetWindow) : m_TargetWindow(TargetWindow) {} + + [[nodiscard]] RenderSurfaceKind GetKind() const override { + return RenderSurfaceKind::Window; + } + [[nodiscard]] bool SupportsPresentation() const override { return true; } + [[nodiscard]] uint32_t GetWidth() const override { + return m_TargetWindow.GetWidth(); + } + [[nodiscard]] uint32_t GetHeight() const override { + return m_TargetWindow.GetHeight(); + } + [[nodiscard]] bool IsMinimized() const override { + return m_TargetWindow.IsMinimized(); + } + [[nodiscard]] void *GetNativeWindowHandle() const override { + return m_TargetWindow.GetNativeHandle(); + } + [[nodiscard]] bool + SupportsPresentationBackend(PresentationBackendType Backend) const override { + return m_TargetWindow.SupportsPresentationBackend(Backend); + } + PresentationSurfaceResult + CreatePresentationSurface(PresentationBackendType Backend, void *Instance, + void *Surface) const override { + return m_TargetWindow.CreatePresentationSurface(Backend, Instance, Surface); + } + +private: + Window &m_TargetWindow; +}; + +class OffscreenRenderSurface final : public IRenderSurface { +public: + OffscreenRenderSurface(uint32_t Width, uint32_t Height) + : m_Width(Width), m_Height(Height) {} + + [[nodiscard]] RenderSurfaceKind GetKind() const override { + return RenderSurfaceKind::Offscreen; + } + [[nodiscard]] bool SupportsPresentation() const override { return false; } + [[nodiscard]] uint32_t GetWidth() const override { return m_Width; } + [[nodiscard]] uint32_t GetHeight() const override { return m_Height; } + [[nodiscard]] bool IsMinimized() const override { return false; } + [[nodiscard]] void *GetNativeWindowHandle() const override { return nullptr; } + [[nodiscard]] bool + SupportsPresentationBackend(PresentationBackendType Backend) const override { + (void)Backend; + return false; + } + PresentationSurfaceResult + CreatePresentationSurface(PresentationBackendType Backend, void *Instance, + void *Surface) const override { + (void)Backend; + (void)Instance; + (void)Surface; + return PresentationSurfaceResult::InitializationFailed; + } + +private: + uint32_t m_Width{0}; + uint32_t m_Height{0}; +}; + +using RenderSurfacePtr = std::shared_ptr; + +enum class ViewportFrameFormat : uint8_t { + R16G16B16A16Float, + R8G8B8A8Unorm, +}; + +struct ViewportFrame { + uint64_t FrameIndex{0}; + uint32_t Width{0}; + uint32_t Height{0}; + ViewportFrameFormat Format{ViewportFrameFormat::R16G16B16A16Float}; + std::span Pixels; + SessionUserId User{}; +}; + +class IViewportFrameOutput { +public: + virtual ~IViewportFrameOutput() = default; + virtual void OnViewportFrame(const ViewportFrame &Frame) = 0; +}; +} // namespace Axiom diff --git a/Axiom/Core/Threading.cpp b/Axiom/Core/Threading.cpp new file mode 100644 index 00000000..ed6e39e0 --- /dev/null +++ b/Axiom/Core/Threading.cpp @@ -0,0 +1,40 @@ +#include "Core/Threading.h" + +#include +#include + +#if defined(__APPLE__) || defined(__linux__) +#include +#endif + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + +namespace Axiom::Threading { +void SetCurrentThreadName(std::string_view Name) { + constexpr size_t MaxThreadNameLength = 63; + std::array Buffer{}; + const size_t CopyLength = std::min(Name.size(), MaxThreadNameLength); + std::copy_n(Name.data(), CopyLength, Buffer.data()); + Buffer[CopyLength] = '\0'; + +#if defined(__APPLE__) + pthread_setname_np(Buffer.data()); +#elif defined(__linux__) + pthread_setname_np(pthread_self(), Buffer.data()); +#elif defined(_WIN32) + std::array WideBuffer{}; + for (size_t Index = 0; Index < CopyLength; ++Index) { + WideBuffer[Index] = static_cast(Buffer[Index]); + } + WideBuffer[CopyLength] = L'\0'; + SetThreadDescription(GetCurrentThread(), WideBuffer.data()); +#else + (void)Buffer; +#endif +} +} // namespace Axiom::Threading diff --git a/Axiom/Core/Threading.h b/Axiom/Core/Threading.h new file mode 100644 index 00000000..4d6219ce --- /dev/null +++ b/Axiom/Core/Threading.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace Axiom::Threading { +void SetCurrentThreadName(std::string_view Name); +} // namespace Axiom::Threading diff --git a/Axiom/Core/VulkanLoader.cpp b/Axiom/Core/VulkanLoader.cpp index 5790ab10..86d1377e 100644 --- a/Axiom/Core/VulkanLoader.cpp +++ b/Axiom/Core/VulkanLoader.cpp @@ -1,137 +1,24 @@ #include "Core/VulkanLoader.h" +#include "HAL/DynamicLibrary.h" +#include "HAL/Platform.h" + #include "Core/Log.h" -#include "Core/Platform.h" +#define GLFW_INCLUDE_VULKAN #include -#include +#include +#include +#include #include -#include #include -#if AXIOM_PLATFORM_WINDOWS -#define WIN32_LEAN_AND_MEAN -#include -#else -#include -#endif - namespace { -constexpr std::string_view kLoaderEnvironmentVariable = - "AXIOM_VULKAN_LOADER_PATH"; - -#ifdef AXIOM_VULKAN_LOADER_PATH -constexpr const char *kConfiguredLoaderPath = AXIOM_VULKAN_LOADER_PATH; -#else -constexpr const char *kConfiguredLoaderPath = nullptr; -#endif - -struct VulkanLoaderModule { -#if AXIOM_PLATFORM_WINDOWS - HMODULE Handle{nullptr}; -#else - void *Handle{nullptr}; -#endif - - [[nodiscard]] bool IsValid() const { return Handle != nullptr; } -}; - -[[nodiscard]] std::string GetEnvironmentVariable( - std::string_view VariableName) { - if (const char *Value = std::getenv(std::string(VariableName).c_str())) { - return Value; - } - - return {}; -} - -[[nodiscard]] VulkanLoaderModule OpenDynamicLibrary(const char *Path) { - VulkanLoaderModule Module{}; - -#if AXIOM_PLATFORM_WINDOWS - Module.Handle = LoadLibraryA(Path); -#else - Module.Handle = dlopen(Path, RTLD_NOW | RTLD_LOCAL); -#endif - - return Module; -} - -[[nodiscard]] std::string GetLastDynamicLibraryError() { -#if AXIOM_PLATFORM_WINDOWS - const DWORD ErrorCode = GetLastError(); - if (ErrorCode == 0) { - return "unknown Windows loader error"; - } - - LPSTR MessageBuffer = nullptr; - const DWORD MessageLength = FormatMessageA( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, ErrorCode, 0, reinterpret_cast(&MessageBuffer), 0, - nullptr); - std::string Message = - MessageLength > 0 && MessageBuffer != nullptr ? MessageBuffer - : "unknown Windows loader error"; - if (MessageBuffer != nullptr) { - LocalFree(MessageBuffer); - } - return Message; -#else - if (const char *Error = dlerror()) { - return Error; - } - - return "unknown POSIX loader error"; -#endif -} - -[[nodiscard]] PFN_vkGetInstanceProcAddr LoadVkGetInstanceProcAddr( - const VulkanLoaderModule &Module) { -#if AXIOM_PLATFORM_WINDOWS - return reinterpret_cast( - GetProcAddress(Module.Handle, "vkGetInstanceProcAddr")); -#else - return reinterpret_cast( - dlsym(Module.Handle, "vkGetInstanceProcAddr")); -#endif -} - -[[nodiscard]] std::vector GetLoaderCandidatePaths() { - std::vector Candidates; - - if (kConfiguredLoaderPath != nullptr && kConfiguredLoaderPath[0] != '\0') { - Candidates.emplace_back(kConfiguredLoaderPath); - } - - std::string EnvironmentPath = GetEnvironmentVariable(kLoaderEnvironmentVariable); - if (!EnvironmentPath.empty() && - (Candidates.empty() || Candidates.front() != EnvironmentPath)) { - Candidates.emplace_back(std::move(EnvironmentPath)); - } - -#if AXIOM_PLATFORM_MACOS - const std::string VulkanSdk = GetEnvironmentVariable("VULKAN_SDK"); - if (!VulkanSdk.empty()) { - Candidates.emplace_back(VulkanSdk + "/macOS/lib/libvulkan.1.dylib"); - Candidates.emplace_back(VulkanSdk + "/macOS/lib/libvulkan.dylib"); - Candidates.emplace_back(VulkanSdk + "/macOS/lib/libMoltenVK.dylib"); - } - - Candidates.emplace_back("/usr/local/lib/libvulkan.1.dylib"); - Candidates.emplace_back("/usr/local/lib/libvulkan.dylib"); - Candidates.emplace_back("/usr/local/lib/libMoltenVK.dylib"); - Candidates.emplace_back("/opt/homebrew/lib/libvulkan.1.dylib"); - Candidates.emplace_back("/opt/homebrew/lib/libvulkan.dylib"); - Candidates.emplace_back("/opt/homebrew/lib/libMoltenVK.dylib"); -#endif - - return Candidates; -} +std::unique_ptr g_CustomVulkanLoader; [[nodiscard]] Axiom::VulkanLoaderInfo ResolveVulkanLoaderInfo() { Axiom::VulkanLoaderInfo Info{}; - A_CORE_INFO("Detected platform: {0}", Axiom::GetPlatformName()); + A_CORE_INFO("Detected platform: {0}", Axiom::HAL::GetPlatformName()); const VkResult DefaultLoaderResult = volkInitialize(); if (DefaultLoaderResult == VK_SUCCESS) { @@ -144,20 +31,21 @@ struct VulkanLoaderModule { A_CORE_WARN("Platform-default Vulkan loader discovery failed with VkResult {0}", static_cast(DefaultLoaderResult)); - const std::vector Candidates = GetLoaderCandidatePaths(); + const std::vector Candidates = + Axiom::HAL::GetDefaultVulkanLoaderCandidatePaths(); for (const std::string &Candidate : Candidates) { if (Candidate.empty()) { continue; } - VulkanLoaderModule Module = OpenDynamicLibrary(Candidate.c_str()); - if (!Module.IsValid()) { + auto Module = std::make_unique(); + if (!Module->Open(Candidate.c_str())) { A_CORE_WARN("Failed to open Vulkan loader candidate '{0}': {1}", - Candidate, GetLastDynamicLibraryError()); + Candidate, Axiom::HAL::DynamicLibrary::GetLastError()); continue; } - PFN_vkGetInstanceProcAddr ProcAddr = LoadVkGetInstanceProcAddr(Module); + void *ProcAddr = Module->FindSymbol("vkGetInstanceProcAddr"); if (ProcAddr == nullptr) { A_CORE_WARN( "Vulkan loader candidate '{0}' did not export vkGetInstanceProcAddr", @@ -165,23 +53,16 @@ struct VulkanLoaderModule { continue; } - volkInitializeCustom(ProcAddr); + volkInitializeCustom( + reinterpret_cast(ProcAddr)); Info.ProcAddr = ProcAddr; Info.Source = Candidate; Info.UsesCustomLoader = true; Info.IsAvailable = true; + g_CustomVulkanLoader = std::move(Module); -#if AXIOM_PLATFORM_MACOS - if (Candidate.find("MoltenVK") != std::string::npos || - Candidate.find("macOS/lib") != std::string::npos) { - A_CORE_INFO("Using macOS Vulkan loader fallback: {0}", Candidate); - } else { - A_CORE_INFO("Using custom Vulkan loader override: {0}", Candidate); - } -#else A_CORE_INFO("Using custom Vulkan loader override: {0}", Candidate); -#endif return Info; } @@ -196,15 +77,7 @@ struct VulkanLoaderModule { namespace Axiom { const char *GetPlatformName() { -#if AXIOM_PLATFORM_WINDOWS - return "Windows"; -#elif AXIOM_PLATFORM_MACOS - return "macOS"; -#elif AXIOM_PLATFORM_LINUX - return "Linux"; -#else - return "Unknown"; -#endif + return HAL::GetPlatformName(); } const VulkanLoaderInfo &GetVulkanLoaderInfo() { @@ -212,10 +85,38 @@ const VulkanLoaderInfo &GetVulkanLoaderInfo() { return LoaderInfo; } +bool CanInitializeHeadlessVulkan() { + const VulkanLoaderInfo &LoaderInfo = GetVulkanLoaderInfo(); + if (!LoaderInfo.IsAvailable) { + return false; + } + + vkb::InstanceBuilder Builder = [&LoaderInfo]() { + if (LoaderInfo.UsesCustomLoader) { + return vkb::InstanceBuilder{ + reinterpret_cast(LoaderInfo.ProcAddr)}; + } + return vkb::InstanceBuilder{}; + }(); + + auto InstanceReturn = Builder.set_app_name("Wraith Engine Vulkan Probe") + .set_headless() + .request_validation_layers(true) + .require_api_version(1, 3, 0) + .build(); + if (!InstanceReturn) { + return false; + } + + vkb::destroy_instance(InstanceReturn.value()); + return true; +} + void ConfigureGlfwVulkanLoader() { const VulkanLoaderInfo &LoaderInfo = GetVulkanLoaderInfo(); if (LoaderInfo.UsesCustomLoader && LoaderInfo.ProcAddr != nullptr) { - glfwInitVulkanLoader(LoaderInfo.ProcAddr); + glfwInitVulkanLoader( + reinterpret_cast(LoaderInfo.ProcAddr)); } } } // namespace Axiom diff --git a/Axiom/Core/VulkanLoader.h b/Axiom/Core/VulkanLoader.h index ed0ea7f4..189c6611 100644 --- a/Axiom/Core/VulkanLoader.h +++ b/Axiom/Core/VulkanLoader.h @@ -2,11 +2,9 @@ #include -#include - namespace Axiom { struct VulkanLoaderInfo { - PFN_vkGetInstanceProcAddr ProcAddr{nullptr}; + void *ProcAddr{nullptr}; std::string Source; bool UsesCustomLoader{false}; bool IsAvailable{false}; @@ -14,5 +12,6 @@ struct VulkanLoaderInfo { [[nodiscard]] const char *GetPlatformName(); [[nodiscard]] const VulkanLoaderInfo &GetVulkanLoaderInfo(); +[[nodiscard]] bool CanInitializeHeadlessVulkan(); void ConfigureGlfwVulkanLoader(); } // namespace Axiom diff --git a/Axiom/Core/Window.h b/Axiom/Core/Window.h index bcf5d502..04bf0ab1 100644 --- a/Axiom/Core/Window.h +++ b/Axiom/Core/Window.h @@ -6,6 +6,16 @@ #include namespace Axiom { +enum class PresentationBackendType : uint8_t { + Vulkan = 0, +}; + +enum class PresentationSurfaceResult : int32_t { + Success = 0, + Unsupported = 1, + InitializationFailed = 2, +}; + class Window { public: Window(std::string Title, uint32_t Width, uint32_t Height); @@ -21,8 +31,14 @@ class Window { virtual void SetCursorMode(CursorMode Mode) = 0; [[nodiscard]] virtual CursorMode GetCursorMode() const = 0; [[nodiscard]] virtual bool ShouldClose() const = 0; + [[nodiscard]] virtual bool IsMinimized() const = 0; virtual void RequestClose() = 0; [[nodiscard]] virtual void *GetNativeHandle() const = 0; + [[nodiscard]] virtual bool + SupportsPresentationBackend(PresentationBackendType Backend) const = 0; + virtual PresentationSurfaceResult + CreatePresentationSurface(PresentationBackendType Backend, void *Instance, + void *Surface) const = 0; [[nodiscard]] uint32_t GetWidth() const { return m_Width; } [[nodiscard]] uint32_t GetHeight() const { return m_Height; } diff --git a/Axiom/CoreInstance/Instance.h b/Axiom/CoreInstance/Instance.h deleted file mode 100644 index 7b3ba257..00000000 --- a/Axiom/CoreInstance/Instance.h +++ /dev/null @@ -1,163 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#define GENERATED_BODY(ClassName) \ - virtual const char *GetClassName() const override { return #ClassName; } - -namespace Axiom { -class Instance { -public: - Instance() = default; - Instance(const std::string &Name) : m_Name(Name) {} - - virtual ~Instance() { - if (!m_IsDestroyed) { - Destroy(); - } - } - - Instance(const Instance &) = delete; - void operator=(const Instance &) = delete; - - template - static inline T *Create(const std::string &Name = "Instance") { - static_assert(std::is_base_of::value, - "T must inherit from Instance"); - - T *NewObj = new T(Name); - NewObj->OnCreate(); - return NewObj; - } - -public: - void SetName(const std::string &Name) { m_Name = Name; } - const std::string &GetName() const { return m_Name; } - - void SetParent(Instance *Parent) { - if (m_Parent == Parent || IsDestroyed()) - return; - - Instance *Ancestor = Parent; - while (Ancestor) { - if (Ancestor == this) - return; // Would create a cycle - Ancestor = Ancestor->GetParent(); - } - - if (m_Parent) - m_Parent->RemoveChildInternal(this); - - m_Parent = Parent; - - if (m_Parent) - m_Parent->AddChildInternal(this); - } - - Instance *GetParent() const { return m_Parent; } - const std::vector &GetChildren() const { return m_Children; } - - virtual const char *GetClassName() const { return "Instance"; } - const std::string GetFullName() const { - std::stack NameStack; - const Instance *CurrentInstance = this; - while (CurrentInstance != nullptr) { - NameStack.push(CurrentInstance->GetName()); - CurrentInstance = CurrentInstance->GetParent(); - } - - std::stringstream StringStream; - while (!NameStack.empty()) { - std::string Name = NameStack.top(); - NameStack.pop(); - - StringStream << Name << "."; - } - - std::string Output = StringStream.str(); - Output.pop_back(); - return Output; - } - -public: - bool IsDestroyed() const { return m_IsDestroyed; } - - void Destroy() { - if (m_IsDestroyed) { - return; - } - - m_IsDestroyed = true; - - OnDestroy(); - - if (m_Parent) { - m_Parent->RemoveChildInternal(this); - m_Parent = nullptr; - } - - std::vector ChildrenCopy = m_Children; - m_Children.clear(); - - for (Instance *Child : ChildrenCopy) { - Child->m_Parent = nullptr; - delete Child; - } - } - - template bool IsA() const { - return dynamic_cast(this) != nullptr; - } - - Instance *FindFirstChild(const std::string &Name) const { - for (Instance *Child : m_Children) { - if (Child->GetName() == Name) { - return Child; - } - } - - return nullptr; - } - - template T *FindFirstChildOfClass() const { - for (Instance *Child : m_Children) { - if (T *CastChild = dynamic_cast(Child)) { - return CastChild; - } - } - - return nullptr; - } - -public: - virtual void OnCreate() {}; - virtual void OnDestroy() {}; - - virtual void OnChildAdded(Instance *Child) {} - virtual void OnChildRemoved(Instance *Child) {} - -private: - void AddChildInternal(Instance *Child) { - m_Children.push_back(Child); - OnChildAdded(Child); - } - void RemoveChildInternal(Instance *Child) { - auto It = std::find(m_Children.begin(), m_Children.end(), Child); - if (It != m_Children.end()) { - m_Children.erase(It); - OnChildRemoved(Child); - } - } - -protected: - std::string m_Name = "Instance"; - Instance *m_Parent = nullptr; - std::vector m_Children; - bool m_IsDestroyed = false; -}; -} // namespace Axiom diff --git a/Axiom/Jobs/JobSystem.cpp b/Axiom/Jobs/JobSystem.cpp new file mode 100644 index 00000000..0c393e1b --- /dev/null +++ b/Axiom/Jobs/JobSystem.cpp @@ -0,0 +1,165 @@ +#include "Jobs/JobSystem.h" + +#include "Core/Threading.h" +#include "Jobs/TaskScheduler.h" + +#include +#include +#include +#include +#include +#include + +namespace Axiom::Jobs { +struct JobState { + std::unique_ptr Task; + std::vector DependencyHandles; +}; + +namespace { +void OnWorkerThreadStart(uint32_t ThreadNum); + +class LambdaTaskSet final : public enki::ITaskSet { +public: + explicit LambdaTaskSet(JobFn Function) + : enki::ITaskSet(1), m_Function(std::move(Function)) {} + + void ExecuteRange(enki::TaskSetPartition, uint32_t) override { m_Function(); } + +private: + JobFn m_Function; +}; + +class ParallelForTaskSet final : public enki::ITaskSet { +public: + ParallelForTaskSet(size_t Count, ParallelForFn Function) + : enki::ITaskSet(static_cast(Count)), + m_Count(Count), + m_Function(std::move(Function)) { + m_MinRange = std::max(1u, static_cast(Count / 64u)); + } + + void ExecuteRange(enki::TaskSetPartition Partition, uint32_t) override { + const size_t End = std::min(Partition.end, m_Count); + for (size_t Index = Partition.start; Index < End; ++Index) { + m_Function(Index); + } + } + +private: + size_t m_Count{0}; + ParallelForFn m_Function; +}; + +class JobSystem { +public: + void Startup() { + std::scoped_lock Lock(m_Mutex); + ++m_StartupCount; + if (m_Scheduler == nullptr) { + m_Scheduler = std::make_unique(); + enki::TaskSchedulerConfig Config = m_Scheduler->GetConfig(); + Config.profilerCallbacks.threadStart = &OnWorkerThreadStart; + m_Scheduler->Initialize(Config); + } + } + + void Shutdown() { + std::scoped_lock Lock(m_Mutex); + if (m_StartupCount == 0) { + return; + } + + --m_StartupCount; + if (m_StartupCount == 0 && m_Scheduler != nullptr) { + m_Scheduler->WaitforAllAndShutdown(); + m_Scheduler.reset(); + } + } + + JobHandle ScheduleJob(JobFn Function) { + auto State = std::make_shared(); + State->Task = std::make_unique(std::move(Function)); + m_Scheduler->AddTaskSetToPipe(State->Task.get()); + return {.State = std::move(State)}; + } + + JobHandle ScheduleJobAfter(JobFn Function, std::span Deps) { + auto State = std::make_shared(); + State->DependencyHandles.reserve(Deps.size()); + for (const JobHandle &Dependency : Deps) { + if (!Dependency.IsValid() || Dependency.State->Task == nullptr) { + continue; + } + State->DependencyHandles.push_back(Dependency); + } + + State->Task = std::make_unique( + [this, State, Function = std::move(Function)]() mutable { + for (const JobHandle &Dependency : State->DependencyHandles) { + if (!Dependency.IsValid() || Dependency.State->Task == nullptr) { + continue; + } + + m_Scheduler->WaitforTask(Dependency.State->Task.get()); + } + + Function(); + }); + m_Scheduler->AddTaskSetToPipe(State->Task.get()); + return {.State = std::move(State)}; + } + + void Wait(JobHandle Handle) { + if (!Handle.IsValid() || Handle.State->Task == nullptr) { + return; + } + + m_Scheduler->WaitforTask(Handle.State->Task.get()); + } + + void ParallelFor(size_t Count, ParallelForFn Function) { + if (Count == 0) { + return; + } + + ParallelForTaskSet Task(Count, std::move(Function)); + m_Scheduler->AddTaskSetToPipe(&Task); + m_Scheduler->WaitforTask(&Task); + } + +private: + std::mutex m_Mutex; + std::unique_ptr m_Scheduler; + size_t m_StartupCount{0}; +}; + +JobSystem &GetJobSystem() { + static JobSystem Instance; + return Instance; +} + +void OnWorkerThreadStart(uint32_t ThreadNum) { + Threading::SetCurrentThreadName("Axiom Job Worker " + + std::to_string(ThreadNum)); +} +} // namespace + +void Startup() { GetJobSystem().Startup(); } + +void Shutdown() { GetJobSystem().Shutdown(); } + +JobHandle ScheduleJob(JobFn Function) { + return GetJobSystem().ScheduleJob(std::move(Function)); +} + +JobHandle ScheduleJobAfter(JobFn Function, std::span Deps) { + return GetJobSystem().ScheduleJobAfter(std::move(Function), Deps); +} + +void Wait(JobHandle Handle) { GetJobSystem().Wait(std::move(Handle)); } + +void ParallelFor(size_t Count, ParallelForFn Function) { + GetJobSystem().ParallelFor(Count, std::move(Function)); +} +} // namespace Axiom::Jobs diff --git a/Axiom/Jobs/JobSystem.h b/Axiom/Jobs/JobSystem.h new file mode 100644 index 00000000..50c92122 --- /dev/null +++ b/Axiom/Jobs/JobSystem.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +namespace Axiom::Jobs { +using JobFn = std::function; +using ParallelForFn = std::function; + +struct JobState; + +struct JobHandle { + [[nodiscard]] bool IsValid() const noexcept { return State != nullptr; } + + std::shared_ptr State; +}; + +void Startup(); +void Shutdown(); + +JobHandle ScheduleJob(JobFn Function); +JobHandle ScheduleJobAfter(JobFn Function, std::span Deps); +void Wait(JobHandle Handle); +void ParallelFor(size_t Count, ParallelForFn Function); +} // namespace Axiom::Jobs diff --git a/Axiom/Jobs/LockLessMultiReadPipe.h b/Axiom/Jobs/LockLessMultiReadPipe.h new file mode 100644 index 00000000..339c8bdd --- /dev/null +++ b/Axiom/Jobs/LockLessMultiReadPipe.h @@ -0,0 +1,283 @@ +// Copyright (c) 2013 Doug Binks +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. + +#pragma once + +#include +#include +#include + +#ifndef ENKI_ASSERT +#include +#define ENKI_ASSERT(x) assert(x) +#endif + +namespace enki +{ + // LockLessMultiReadPipe - Single writer, multiple reader thread safe pipe using (semi) lockless programming + // Readers can only read from the back of the pipe + // The single writer can write to the front of the pipe, and read from both ends (a writer can be a reader) + // for many of the principles used here, see http://msdn.microsoft.com/en-us/library/windows/desktop/ee418650(v=vs.85).aspx + // Note: using log2 sizes so we do not need to clamp (multi-operation) + // T is the contained type + // Note this is not true lockless as the use of flags as a form of lock state. + template class LockLessMultiReadPipe + { + public: + LockLessMultiReadPipe(); + ~LockLessMultiReadPipe() {} + + // ReaderTryReadBack returns false if we were unable to read + // This is thread safe for both multiple readers and the writer + bool ReaderTryReadBack( T* pOut ); + + // WriterTryReadFront returns false if we were unable to read + // This is thread safe for the single writer, but should not be called by readers + bool WriterTryReadFront( T* pOut ); + + // WriterTryWriteFront returns false if we were unable to write + // This is thread safe for the single writer, but should not be called by readers + bool WriterTryWriteFront( const T& in ); + + // IsPipeEmpty() is a utility function, not intended for general use + // Should only be used very prudently. + bool IsPipeEmpty() const + { + return 0 == m_WriteIndex.load( std::memory_order_relaxed ) - m_ReadCount.load( std::memory_order_relaxed ); + } + + void Clear() + { + m_WriteIndex = 0; + m_ReadIndex = 0; + m_ReadCount = 0; + memset( (void*)m_Flags, 0, sizeof( m_Flags ) ); + } + + private: + const static uint32_t ms_cSize = ( 1 << cSizeLog2 ); + const static uint32_t ms_cIndexMask = ms_cSize - 1; + const static uint32_t FLAG_INVALID = 0xFFFFFFFF; // 32bit for CAS + const static uint32_t FLAG_CAN_WRITE = 0x00000000; // 32bit for CAS + const static uint32_t FLAG_CAN_READ = 0x11111111; // 32bit for CAS + + T m_Buffer[ ms_cSize ]; + + // read and write indexes allow fast access to the pipe, but actual access + // controlled by the access flags. + std::atomic m_WriteIndex; + std::atomic m_ReadCount; + std::atomic m_Flags[ ms_cSize ]; + std::atomic m_ReadIndex; + }; + + template inline + LockLessMultiReadPipe::LockLessMultiReadPipe() + : m_WriteIndex(0) + , m_ReadCount(0) + , m_ReadIndex(0) + { + ENKI_ASSERT( cSizeLog2 < 32 ); + memset( (void*)m_Flags, 0, sizeof( m_Flags ) ); + } + + template inline + bool LockLessMultiReadPipe::ReaderTryReadBack( T* pOut ) + { + + uint32_t actualReadIndex; + uint32_t readCount = m_ReadCount.load( std::memory_order_relaxed ); + + // We get hold of read index for consistency + // and do first pass starting at read count + uint32_t readIndexToUse = readCount; + while(true) + { + + uint32_t writeIndex = m_WriteIndex.load( std::memory_order_relaxed ); + // power of two sizes ensures we can use a simple calc without modulus + uint32_t numInPipe = writeIndex - readCount; + if( 0 == numInPipe ) + { + return false; + } + if( readIndexToUse >= writeIndex ) + { + readIndexToUse = m_ReadIndex.load( std::memory_order_relaxed ); + } + + // power of two sizes ensures we can perform AND for a modulus + actualReadIndex = readIndexToUse & ms_cIndexMask; + + // Multiple potential readers mean we should check if the data is valid, + // using an atomic compare exchange + uint32_t previous = FLAG_CAN_READ; + bool bSuccess = m_Flags[ actualReadIndex ].compare_exchange_strong( previous, FLAG_INVALID, std::memory_order_acq_rel, std::memory_order_relaxed ); + if( bSuccess ) + { + break; + } + ++readIndexToUse; + + // Update read count + readCount = m_ReadCount.load( std::memory_order_relaxed ); + } + + // we update the read index using an atomic add, as we've only read one piece of data. + // this ensure consistency of the read index, and the above loop ensures readers + // only read from unread data + m_ReadCount.fetch_add(1, std::memory_order_relaxed ); + + // now read data, ensuring we do so after above reads & CAS + *pOut = m_Buffer[ actualReadIndex ]; + + m_Flags[ actualReadIndex ].store( FLAG_CAN_WRITE, std::memory_order_release ); + + return true; + } + + template inline + bool LockLessMultiReadPipe::WriterTryReadFront( T* pOut ) + { + uint32_t writeIndex = m_WriteIndex.load( std::memory_order_relaxed ); + uint32_t frontReadIndex = writeIndex; + + // Multiple potential readers mean we should check if the data is valid, + // using an atomic compare exchange - which acts as a form of lock (so not quite lockless really). + uint32_t actualReadIndex = 0; + while(true) + { + uint32_t readCount = m_ReadCount.load( std::memory_order_relaxed ); + // power of two sizes ensures we can use a simple calc without modulus + uint32_t numInPipe = writeIndex - readCount; + if( 0 == numInPipe ) + { + m_ReadIndex.store( readCount, std::memory_order_release ); + return false; + } + --frontReadIndex; + actualReadIndex = frontReadIndex & ms_cIndexMask; + uint32_t previous = FLAG_CAN_READ; + bool success = m_Flags[ actualReadIndex ].compare_exchange_strong( previous, FLAG_INVALID, std::memory_order_acq_rel, std::memory_order_relaxed ); + if( success ) + { + break; + } + else if( m_ReadIndex.load( std::memory_order_acquire ) >= frontReadIndex ) + { + return false; + } + } + + // now read data, ensuring we do so after above reads & CAS + *pOut = m_Buffer[ actualReadIndex ]; + + m_Flags[ actualReadIndex ].store( FLAG_CAN_WRITE, std::memory_order_relaxed ); + + m_WriteIndex.store(writeIndex-1, std::memory_order_relaxed); + return true; + } + + + template inline + bool LockLessMultiReadPipe::WriterTryWriteFront( const T& in ) + { + // The writer 'owns' the write index, and readers can only reduce + // the amount of data in the pipe. + // We get hold of both values for consistency and to reduce false sharing + // impacting more than one access + uint32_t writeIndex = m_WriteIndex; + + // power of two sizes ensures we can perform AND for a modulus + uint32_t actualWriteIndex = writeIndex & ms_cIndexMask; + + // a reader may still be reading this item, as there are multiple readers + if( m_Flags[ actualWriteIndex ].load(std::memory_order_acquire) != FLAG_CAN_WRITE ) + { + return false; // still being read, so have caught up with tail. + } + + // as we are the only writer we can update the data without atomics + // whilst the write index has not been updated + m_Buffer[ actualWriteIndex ] = in; + m_Flags[ actualWriteIndex ].store( FLAG_CAN_READ, std::memory_order_release ); + + m_WriteIndex.fetch_add(1, std::memory_order_relaxed); + return true; + } + + + // Lockless multiwriter intrusive list + // Type T must implement T* volatile pNext; + template class LocklessMultiWriteIntrusiveList + { + + std::atomic pHead; + T tail; + public: + LocklessMultiWriteIntrusiveList() : pHead( &tail ) + { + tail.pNext = NULL; + } + + bool IsListEmpty() const + { + return pHead == &tail; + } + + // Add - safe to perform from any thread + void WriterWriteFront( T* pNode_ ) + { + ENKI_ASSERT( pNode_ ); + pNode_->pNext = NULL; + T* pPrev = pHead.exchange( pNode_ ); + pPrev->pNext = pNode_; + } + + // Remove - only thread safe for owner + T* ReaderReadBack() + { + T* pTailPlus1 = tail.pNext; + if( pTailPlus1 ) + { + T* pTailPlus2 = pTailPlus1->pNext; + if( pTailPlus2 ) + { + //not head + tail.pNext = pTailPlus2; + } + else + { + tail.pNext = NULL; + T* pCompare = pTailPlus1; // we need preserve pTailPlus1 as compare will alter it on failure + // pTailPlus1 is the head, attempt swap with tail + if( !pHead.compare_exchange_strong( pCompare, &tail ) ) + { + // pCompare receives the revised pHead on failure. + // pTailPlus1 is no longer the head, so pTailPlus1->pNext should be non NULL + while( (T*)NULL == pTailPlus1->pNext ) {;} // wait for pNext to be updated as head may have just changed. + tail.pNext = pTailPlus1->pNext.load(); + pTailPlus1->pNext = NULL; + } + } + } + return pTailPlus1; + } + }; + +} diff --git a/Axiom/Jobs/TaskScheduler.cpp b/Axiom/Jobs/TaskScheduler.cpp new file mode 100644 index 00000000..db110b37 --- /dev/null +++ b/Axiom/Jobs/TaskScheduler.cpp @@ -0,0 +1,1537 @@ +// Copyright (c) 2013 Doug Binks +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. + +#include "TaskScheduler.h" +#include "LockLessMultiReadPipe.h" +#include +#include + +#if defined __i386__ || defined __x86_64__ +#include "x86intrin.h" +#elif defined _WIN32 +#include +#endif + +using namespace enki; + +#if defined(ENKI_CUSTOM_ALLOC_FILE_AND_LINE) +#define ENKI_FILE_AND_LINE __FILE__, __LINE__ +#else +namespace +{ + const char* gc_File = ""; + const uint32_t gc_Line = 0; +} +#define ENKI_FILE_AND_LINE gc_File, gc_Line +#endif + +// UWP and MinGW don't have GetActiveProcessorCount +#if defined(_WIN64) \ + && !defined(__MINGW32__) \ + && !(defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_PC_APP || WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP)) +#define ENKI_USE_WINDOWS_PROCESSOR_API +#endif + +#ifdef ENKI_USE_WINDOWS_PROCESSOR_API +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#include +#endif + +uint32_t enki::GetNumHardwareThreads() +{ +#ifdef ENKI_USE_WINDOWS_PROCESSOR_API + return GetActiveProcessorCount(ALL_PROCESSOR_GROUPS); +#else + return std::thread::hardware_concurrency(); +#endif +} + +namespace enki +{ + static constexpr int32_t gc_TaskStartCount = 2; + static constexpr int32_t gc_TaskAlmostCompleteCount = 1; // GetIsComplete() will return false, but execution is done and about to complete + static constexpr uint32_t gc_PipeSizeLog2 = 8; + static constexpr uint32_t gc_SpinCount = 10; + static constexpr uint32_t gc_SpinBackOffMultiplier = 100; + static constexpr uint32_t gc_MaxNumInitialPartitions = 8; + static constexpr uint32_t gc_MaxStolenPartitions = 1 << gc_PipeSizeLog2; + static constexpr uint32_t gc_CacheLineSize = 64; + // awaiting std::hardware_constructive_interference_size +} + +// thread_local not well supported yet by some older C++11 compilers. +// For XCode before version 8 thread_local is not defined, so add to your compile defines: ENKI_THREAD_LOCAL __thread +#ifndef ENKI_THREAD_LOCAL +#if defined(_MSC_VER) && _MSC_VER <= 1800 + #define ENKI_THREAD_LOCAL __declspec(thread) +// Removed below as XCode supports thread_local since version 8 +// #elif __APPLE__ +// // Apple thread_local currently not implemented in XCode before version 8 despite it being in Clang. +// #define ENKI_THREAD_LOCAL __thread +#else + #define ENKI_THREAD_LOCAL thread_local +#endif +#endif + + +// each software thread gets its own copy of gtl_threadNum, so this is safe to use as a static variable +static ENKI_THREAD_LOCAL uint32_t gtl_threadNum = enki::NO_THREAD_NUM; + +namespace enki +{ + struct SubTaskSet + { + ITaskSet* pTask; + TaskSetPartition partition; + }; + + // we derive class TaskPipe rather than typedef to get forward declaration working easily + class TaskPipe : public LockLessMultiReadPipe {}; + + enum ThreadState : int32_t + { + ENKI_THREAD_STATE_NONE, // shouldn't get this value + ENKI_THREAD_STATE_NOT_LAUNCHED, // for debug purposes - indicates enki task thread not yet launched + ENKI_THREAD_STATE_RUNNING, + ENKI_THREAD_STATE_PRIMARY_REGISTERED, // primary thread is the one enkiTS was initialized on + ENKI_THREAD_STATE_EXTERNAL_REGISTERED, + ENKI_THREAD_STATE_EXTERNAL_UNREGISTERED, + ENKI_THREAD_STATE_WAIT_TASK_COMPLETION, + ENKI_THREAD_STATE_WAIT_NEW_TASKS, + ENKI_THREAD_STATE_WAIT_NEW_PINNED_TASKS, + ENKI_THREAD_STATE_STOPPED, + }; + + struct ThreadArgs + { + uint32_t threadNum; + TaskScheduler* pTaskScheduler; + }; + + struct alignas(enki::gc_CacheLineSize) ThreadDataStore + { + semaphoreid_t* pWaitNewPinnedTaskSemaphore = nullptr; + std::atomic threadState = { ENKI_THREAD_STATE_NONE }; + uint32_t rndSeed = 0; + char prevent_false_Share[ enki::gc_CacheLineSize - sizeof(std::atomic) - sizeof(semaphoreid_t*) - sizeof( uint32_t ) ]; // required to prevent alignment padding warning + }; + constexpr size_t SIZEOFTHREADDATASTORE = sizeof( ThreadDataStore ); // for easier inspection + static_assert( SIZEOFTHREADDATASTORE == enki::gc_CacheLineSize, "ThreadDataStore may exhibit false sharing" ); + + class PinnedTaskList : public LocklessMultiWriteIntrusiveList {}; + + semaphoreid_t* SemaphoreCreate(); + void SemaphoreDelete( semaphoreid_t* pSemaphore_ ); + void SemaphoreWait( semaphoreid_t& semaphoreid ); + void SemaphoreSignal( semaphoreid_t& semaphoreid, int32_t countWaiting ); +} + +namespace +{ + SubTaskSet SplitTask( SubTaskSet& subTask_, uint32_t rangeToSplit_ ) + { + SubTaskSet splitTask = subTask_; + uint32_t rangeLeft = subTask_.partition.end - subTask_.partition.start; + rangeToSplit_ = std::min( rangeToSplit_, rangeLeft ); + splitTask.partition.end = subTask_.partition.start + rangeToSplit_; + subTask_.partition.start = splitTask.partition.end; + return splitTask; + } + + #if ( defined _WIN32 && ( defined _M_IX86 || defined _M_X64 ) ) || ( defined __i386__ || defined __x86_64__ ) + // Note: see https://software.intel.com/en-us/articles/a-common-construct-to-avoid-the-contention-of-threads-architecture-agnostic-spin-wait-loops + void SpinWait( uint32_t spinCount_ ) + { + uint64_t end = __rdtsc() + spinCount_; + while( __rdtsc() < end ) + { + _mm_pause(); + } + } + #else + void SpinWait( uint32_t spinCount_ ) + { + while( spinCount_ ) + { + // TODO: may have NOP or yield equiv + --spinCount_; + } + } + #endif + + void SafeCallback( ProfilerCallbackFunc func_, uint32_t threadnum_ ) + { + if( func_ != nullptr ) + { + func_( threadnum_ ); + } + } +} + + +ENKITS_API void* enki::DefaultAllocFunc( size_t align_, size_t size_, void* userData_, const char* file_, int line_ ) +{ + (void)userData_; (void)file_; (void)line_; + void* pRet; +#ifdef _WIN32 + pRet = (void*)_aligned_malloc( size_, align_ ); +#else + pRet = nullptr; + if( align_ <= size_ && align_ <= alignof(int64_t) ) + { + // no need for alignment, use malloc + pRet = malloc( size_ ); + } + else + { + int retval = posix_memalign( &pRet, align_, size_ ); + (void)retval; // unused + } +#endif + return pRet; +} + +ENKITS_API void enki::DefaultFreeFunc( void* ptr_, size_t size_, void* userData_, const char* file_, int line_ ) +{ + (void)size_; (void)userData_; (void)file_; (void)line_; +#ifdef _WIN32 + _aligned_free( ptr_ ); +#else + free( ptr_ ); +#endif +} + +bool TaskScheduler::RegisterExternalTaskThread() +{ + bool bRegistered = false; + while( !bRegistered && m_NumExternalTaskThreadsRegistered < (int32_t)m_Config.numExternalTaskThreads ) + { + for(uint32_t thread = GetNumFirstExternalTaskThread(); thread < GetNumFirstExternalTaskThread() + m_Config.numExternalTaskThreads; ++thread ) + { + ThreadState threadStateExpected = ENKI_THREAD_STATE_EXTERNAL_UNREGISTERED; + if( m_pThreadDataStore[thread].threadState.compare_exchange_strong( + threadStateExpected, ENKI_THREAD_STATE_EXTERNAL_REGISTERED ) ) + { + ++m_NumExternalTaskThreadsRegistered; + gtl_threadNum = thread; + bRegistered = true; + break; + } + } + } + return bRegistered; +} + +bool TaskScheduler::RegisterExternalTaskThread( uint32_t threadNumToRegister_ ) +{ + ENKI_ASSERT( threadNumToRegister_ >= GetNumFirstExternalTaskThread() ); + ENKI_ASSERT( threadNumToRegister_ < ( GetNumFirstExternalTaskThread() + m_Config.numExternalTaskThreads ) ); + ThreadState threadStateExpected = ENKI_THREAD_STATE_EXTERNAL_UNREGISTERED; + if( m_pThreadDataStore[threadNumToRegister_].threadState.compare_exchange_strong( + threadStateExpected, ENKI_THREAD_STATE_EXTERNAL_REGISTERED ) ) + { + ++m_NumExternalTaskThreadsRegistered; + gtl_threadNum = threadNumToRegister_; + return true; + } + return false; +} + + +void TaskScheduler::DeRegisterExternalTaskThread() +{ + ENKI_ASSERT( gtl_threadNum != enki::NO_THREAD_NUM ); + ENKI_ASSERT( gtl_threadNum >= GetNumFirstExternalTaskThread() ); + ThreadState threadState = m_pThreadDataStore[gtl_threadNum].threadState.load( std::memory_order_acquire ); + ENKI_ASSERT( threadState == ENKI_THREAD_STATE_EXTERNAL_REGISTERED ); + if( threadState == ENKI_THREAD_STATE_EXTERNAL_REGISTERED ) + { + --m_NumExternalTaskThreadsRegistered; + m_pThreadDataStore[gtl_threadNum].threadState.store( ENKI_THREAD_STATE_EXTERNAL_UNREGISTERED, std::memory_order_release ); + gtl_threadNum = enki::NO_THREAD_NUM; + } +} + +uint32_t TaskScheduler::GetNumRegisteredExternalTaskThreads() +{ + return m_NumExternalTaskThreadsRegistered; +} + +void TaskScheduler::TaskingThreadFunction( const ThreadArgs& args_ ) +{ + uint32_t threadNum = args_.threadNum; + TaskScheduler* pTS = args_.pTaskScheduler; + gtl_threadNum = threadNum; + + pTS->m_pThreadDataStore[threadNum].threadState.store( ENKI_THREAD_STATE_RUNNING, std::memory_order_release ); + SafeCallback( pTS->m_Config.profilerCallbacks.threadStart, threadNum ); + + uint32_t spinCount = 0; + uint32_t hintPipeToCheck_io = threadNum + 1; // does not need to be clamped. + while( pTS->GetIsRunningInt() ) + { + if( !pTS->TryRunTask( threadNum, hintPipeToCheck_io ) ) + { + // no tasks, will spin then wait + ++spinCount; + if( spinCount > gc_SpinCount ) + { + pTS->WaitForNewTasks( threadNum ); + } + else + { + uint32_t spinBackoffCount = spinCount * gc_SpinBackOffMultiplier; + SpinWait( spinBackoffCount ); + } + } + else + { + spinCount = 0; // have run a task so reset spin count. + } + } + + pTS->m_NumInternalTaskThreadsRunning.fetch_sub( 1, std::memory_order_release ); + pTS->m_pThreadDataStore[threadNum].threadState.store( ENKI_THREAD_STATE_STOPPED, std::memory_order_release ); + SafeCallback( pTS->m_Config.profilerCallbacks.threadStop, threadNum ); +} + + +void TaskScheduler::StartThreads() +{ + if( m_bHaveThreads ) + { + return; + } + + m_NumThreads = m_Config.numTaskThreadsToCreate + m_Config.numExternalTaskThreads + 1; + + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + m_pPipesPerThread[ priority ] = NewArray( m_NumThreads, ENKI_FILE_AND_LINE ); + m_pPinnedTaskListPerThread[ priority ] = NewArray( m_NumThreads, ENKI_FILE_AND_LINE ); + } + + m_pNewTaskSemaphore = SemaphoreNew(); + m_pTaskCompleteSemaphore = SemaphoreNew(); + + // we create one less thread than m_NumThreads as the main thread counts as one + m_pThreadDataStore = NewArray( m_NumThreads, ENKI_FILE_AND_LINE ); + m_pThreads = NewArray( m_NumThreads, ENKI_FILE_AND_LINE ); + m_bRunning = true; + m_bWaitforAllCalled = false; + m_bShutdownRequested = false; + + // current thread is primary enkiTS thread + m_pThreadDataStore[0].threadState = ENKI_THREAD_STATE_PRIMARY_REGISTERED; + gtl_threadNum = 0; + + for( uint32_t thread = GetNumFirstExternalTaskThread(); thread < m_Config.numExternalTaskThreads + GetNumFirstExternalTaskThread(); ++thread ) + { + m_pThreadDataStore[thread].threadState = ENKI_THREAD_STATE_EXTERNAL_UNREGISTERED; + } + for( uint32_t thread = m_Config.numExternalTaskThreads + GetNumFirstExternalTaskThread(); thread < m_NumThreads; ++thread ) + { + m_pThreadDataStore[thread].threadState = ENKI_THREAD_STATE_NOT_LAUNCHED; + } + + + // Create Wait New Pinned Task Semaphores and init rndSeed + for( uint32_t threadNum = 0; threadNum < m_NumThreads; ++threadNum ) + { + m_pThreadDataStore[threadNum].pWaitNewPinnedTaskSemaphore = SemaphoreNew(); + m_pThreadDataStore[threadNum].rndSeed = threadNum; + } + + // only launch threads once all thread states are set + for( uint32_t thread = m_Config.numExternalTaskThreads + GetNumFirstExternalTaskThread(); thread < m_NumThreads; ++thread ) + { + m_pThreads[thread] = std::thread( TaskingThreadFunction, ThreadArgs{ thread, this } ); + ++m_NumInternalTaskThreadsRunning; + } + + // ensure we have sufficient tasks to equally fill either all threads including main + // or just the threads we've launched, this is outside the first init as we want to be able + // to runtime change it + if( 1 == m_NumThreads ) + { + m_NumPartitions = 1; + m_NumInitialPartitions = 1; + } + else + { + // There could be more threads than hardware threads if external threads are + // being intended for blocking functionality such as io etc. + // We only need to partition for a maximum of the available processor parallelism. + uint32_t numThreadsToPartitionFor = std::min( m_NumThreads, GetNumHardwareThreads() ); + m_NumPartitions = numThreadsToPartitionFor * (numThreadsToPartitionFor - 1); + // ensure m_NumPartitions, m_NumInitialPartitions non zero, can happen if m_NumThreads > 1 && GetNumHardwareThreads() == 1 + m_NumPartitions = std::max( m_NumPartitions, (uint32_t)1 ); + m_NumInitialPartitions = std::max( numThreadsToPartitionFor - 1, (uint32_t)1 ); + m_NumInitialPartitions = std::min( m_NumInitialPartitions, gc_MaxNumInitialPartitions ); + } + +#ifdef ENKI_USE_WINDOWS_PROCESSOR_API + // x64 bit Windows may support >64 logical processors using processor groups, and only allocate threads to a default group. + // We need to detect this and distribute threads accordingly + if( GetNumHardwareThreads() > 64 && // only have processor groups if > 64 hardware threads + std::thread::hardware_concurrency() < GetNumHardwareThreads() && // if std::thread sees > 64 hardware threads no need to distribute + std::thread::hardware_concurrency() < m_NumThreads ) // no need to distribute if number of threads requested lower than std::thread sees + { + uint32_t numProcessorGroups = GetActiveProcessorGroupCount(); + GROUP_AFFINITY mainThreadAffinity; + BOOL success = GetThreadGroupAffinity( GetCurrentThread(), &mainThreadAffinity ); + ENKI_ASSERT( success ); + if( success ) + { + uint32_t mainProcessorGroup = mainThreadAffinity.Group; + uint32_t currLogicalProcess = GetActiveProcessorCount( (WORD)mainProcessorGroup ); // we start iteration at end of current process group's threads + + // If more threads are created than there are logical processors then we still want to distribute them evenly amongst groups + // so we iterate continuously around the groups until we reach m_NumThreads + uint32_t group = 0; + while( currLogicalProcess < m_NumThreads ) + { + ++group; // start at group 1 since we set currLogicalProcess to start of next group + uint32_t currGroup = ( group + mainProcessorGroup ) % numProcessorGroups; // we start at mainProcessorGroup, go round in circles + uint32_t groupNumLogicalProcessors = GetActiveProcessorCount( (WORD)currGroup ); + ENKI_ASSERT( groupNumLogicalProcessors <= 64 ); + uint64_t GROUPMASK = 0xFFFFFFFFFFFFFFFFULL >> (64-groupNumLogicalProcessors); // group mask should not have 1's where there are no processors + for( uint32_t groupLogicalProcess = 0; ( groupLogicalProcess < groupNumLogicalProcessors ) && ( currLogicalProcess < m_NumThreads ); ++groupLogicalProcess, ++currLogicalProcess ) + { + if( currLogicalProcess > m_Config.numExternalTaskThreads + GetNumFirstExternalTaskThread() ) + { + auto thread_handle = m_pThreads[currLogicalProcess].native_handle(); + + // From https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups + // If a thread is assigned to a different group than the process, the process's affinity is updated to include the thread's affinity + // and the process becomes a multi-group process. + GROUP_AFFINITY threadAffinity; + success = GetThreadGroupAffinity( thread_handle, &threadAffinity ); + ENKI_ASSERT(success); (void)success; + if( threadAffinity.Group != currGroup ) + { + threadAffinity.Group = (WORD)currGroup; + threadAffinity.Mask = GROUPMASK; + success = SetThreadGroupAffinity( thread_handle, &threadAffinity, nullptr ); + ENKI_ASSERT( success ); (void)success; + } + } + } + } + } + } +#endif + + m_bHaveThreads = true; +} + +void TaskScheduler::StopThreads( bool bWait_ ) +{ + // we set m_bWaitforAllCalled to true to ensure any task which loop using this status exit + m_bWaitforAllCalled.store( true, std::memory_order_release ); + + // set status + m_bShutdownRequested.store( true, std::memory_order_release ); + m_bRunning.store( false, std::memory_order_release ); + + if( m_bHaveThreads ) + { + + // wait for threads to quit before deleting data + while( bWait_ && m_NumInternalTaskThreadsRunning ) + { + // keep firing event to ensure all threads pick up state of m_bRunning + WakeThreadsForNewTasks(); + + for( uint32_t threadId = 0; threadId < m_NumThreads; ++threadId ) + { + // send wait for new pinned tasks signal to ensure any waiting are awoken + SemaphoreSignal( *m_pThreadDataStore[ threadId ].pWaitNewPinnedTaskSemaphore, 1 ); + } + } + + // detach threads starting with thread GetNumFirstExternalTaskThread() (as 0 is initialization thread). + for( uint32_t thread = m_Config.numExternalTaskThreads + GetNumFirstExternalTaskThread(); thread < m_NumThreads; ++thread ) + { + ENKI_ASSERT( m_pThreads[thread].joinable() ); + m_pThreads[thread].join(); + } + + // delete any Wait New Pinned Task Semaphores + for( uint32_t threadNum = 0; threadNum < m_NumThreads; ++threadNum ) + { + SemaphoreDelete( m_pThreadDataStore[threadNum].pWaitNewPinnedTaskSemaphore ); + } + + DeleteArray( m_pThreadDataStore, m_NumThreads, ENKI_FILE_AND_LINE ); + DeleteArray( m_pThreads, m_NumThreads, ENKI_FILE_AND_LINE ); + m_pThreadDataStore = 0; + m_pThreads = 0; + + SemaphoreDelete( m_pNewTaskSemaphore ); + m_pNewTaskSemaphore = 0; + SemaphoreDelete( m_pTaskCompleteSemaphore ); + m_pTaskCompleteSemaphore = 0; + + m_bHaveThreads = false; + m_NumThreadsWaitingForNewTasks = 0; + m_NumThreadsWaitingForTaskCompletion = 0; + m_NumInternalTaskThreadsRunning = 0; + m_NumExternalTaskThreadsRegistered = 0; + + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + DeleteArray( m_pPipesPerThread[ priority ], m_NumThreads, ENKI_FILE_AND_LINE ); + m_pPipesPerThread[ priority ] = NULL; + DeleteArray( m_pPinnedTaskListPerThread[ priority ], m_NumThreads, ENKI_FILE_AND_LINE ); + m_pPinnedTaskListPerThread[ priority ] = NULL; + } + m_NumThreads = 0; + } +} + +bool TaskScheduler::TryRunTask( uint32_t threadNum_, uint32_t& hintPipeToCheck_io_ ) +{ + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + if( TryRunTask( threadNum_, priority, hintPipeToCheck_io_ ) ) + { + return true; + } + } + return false; +} + +/* xxHash variant based on documentation on + https://github.com/Cyan4973/xxHash/blob/eec5700f4d62113b47ee548edbc4746f61ffb098/doc/xxhash_spec.md + + Copyright (c) Yann Collet + + Permission is granted to copy and distribute this document for any purpose and without charge, including translations into other languages and incorporation into compilations, provided that the copyright notice and this notice are preserved, and that any substantive changes or deletions from the original are clearly marked. Distribution of this document is unlimited. +*/ +static inline uint32_t Hash32( uint32_t in_ ) +{ + static const uint32_t PRIME32_2 = 2246822519U; // 0b10000101111010111100101001110111 + static const uint32_t PRIME32_3 = 3266489917U; // 0b11000010101100101010111000111101 + static const uint32_t PRIME32_5 = 374761393U; // 0b00010110010101100110011110110001 + static const uint32_t SEED = 0; // can configure seed if needed + + // less than 16 bytes of input so simplified hash steps + uint32_t acc = SEED + PRIME32_5; + acc += in_; + acc = acc ^ (acc >> 15); + acc = acc * PRIME32_2; + acc = acc ^ (acc >> 13); + acc = acc * PRIME32_3; + acc = acc ^ (acc >> 16); + return acc; +} + +bool TaskScheduler::TryRunTask( uint32_t threadNum_, uint32_t priority_, uint32_t& hintPipeToCheck_io_ ) +{ + // Run any tasks for this thread + RunPinnedTasks( threadNum_, priority_ ); + + // check for tasks + SubTaskSet subTask; + bool bHaveTask = m_pPipesPerThread[ priority_ ][ threadNum_ ].WriterTryReadFront( &subTask ); + + uint32_t threadToCheckStart = hintPipeToCheck_io_ % m_NumThreads; + uint32_t threadToCheck = threadToCheckStart; + uint32_t checkCount = 0; + if( !bHaveTask ) + { + bHaveTask = m_pPipesPerThread[ priority_ ][ threadToCheck ].ReaderTryReadBack( &subTask ); + if( !bHaveTask ) + { + // To prevent many threads checking the same task pipe for work we pseudorandomly distribute + // the starting thread which we start checking for tasks to run + uint32_t& rndSeed = m_pThreadDataStore[threadNum_].rndSeed; + ++rndSeed; + uint32_t threadToCheckOffset = Hash32( rndSeed * threadNum_ ); + while( !bHaveTask && checkCount < m_NumThreads ) + { + threadToCheck = ( threadToCheckOffset + checkCount ) % m_NumThreads; + if( threadToCheck != threadNum_ && threadToCheckOffset != threadToCheckStart ) + { + bHaveTask = m_pPipesPerThread[ priority_ ][ threadToCheck ].ReaderTryReadBack( &subTask ); + } + ++checkCount; + } + } + } + + if( bHaveTask ) + { + // update hint, will preserve value unless actually got task from another thread. + hintPipeToCheck_io_ = threadToCheck; + + uint32_t partitionSize = subTask.partition.end - subTask.partition.start; + if( subTask.pTask->m_RangeToRun < partitionSize ) + { + SubTaskSet taskToRun = SplitTask( subTask, subTask.pTask->m_RangeToRun ); + uint32_t rangeToSplit = subTask.pTask->m_RangeToRun; + if( threadNum_ != threadToCheck ) + { + // task was stolen from another thread + // in order to ensure other threads can get enough work we need to split into larger ranges + // these larger splits are then stolen and split themselves + // otherwise other threads must keep stealing from this thread, which may stall when pipe is full + rangeToSplit = std::max( rangeToSplit, (subTask.partition.end - subTask.partition.start) / gc_MaxStolenPartitions ); + } + SplitAndAddTask( threadNum_, subTask, rangeToSplit ); + taskToRun.pTask->ExecuteRange( taskToRun.partition, threadNum_ ); + int prevCount = taskToRun.pTask->m_RunningCount.fetch_sub(1,std::memory_order_acq_rel ); + if( gc_TaskStartCount == prevCount ) + { + TaskComplete( taskToRun.pTask, true, threadNum_ ); + } + } + else + { + // the task has already been divided up by AddTaskSetToPipe, so just run it + subTask.pTask->ExecuteRange( subTask.partition, threadNum_ ); + int prevCount = subTask.pTask->m_RunningCount.fetch_sub(1,std::memory_order_acq_rel ); + if( gc_TaskStartCount == prevCount ) + { + TaskComplete( subTask.pTask, true, threadNum_ ); + } + } + } + + return bHaveTask; + +} + +void TaskScheduler::TaskComplete( ICompletable* pTask_, bool bWakeThreads_, uint32_t threadNum_ ) +{ + // It must be impossible for a thread to enter the sleeping wait prior to the load of m_WaitingForTaskCount + // in this function, so we introduce a gc_TaskAlmostCompleteCount to prevent this. + ENKI_ASSERT( gc_TaskAlmostCompleteCount == pTask_->m_RunningCount.load( std::memory_order_acquire ) ); + bool bCallWakeThreads = bWakeThreads_ && pTask_->m_WaitingForTaskCount.load( std::memory_order_acquire ); + + Dependency* pDependent = pTask_->m_pDependents; + + // Do not access pTask_ below this line unless we have dependencies. + pTask_->m_RunningCount.store( 0, std::memory_order_release ); + + if( bCallWakeThreads ) + { + WakeThreadsForTaskCompletion(); + } + + while( pDependent ) + { + // access pTaskToRunOnCompletion member data before incrementing m_DependenciesCompletedCount so + // they do not get deleted when another thread completes the pTaskToRunOnCompletion + int32_t dependenciesCount = pDependent->pTaskToRunOnCompletion->m_DependenciesCount; + // get temp copy of pDependent so OnDependenciesComplete can delete task if needed. + Dependency* pDependentCurr = pDependent; + pDependent = pDependent->pNext; + int32_t prevDeps = pDependentCurr->pTaskToRunOnCompletion->m_DependenciesCompletedCount.fetch_add( 1, std::memory_order_release ); + ENKI_ASSERT( prevDeps < dependenciesCount ); + if( dependenciesCount == ( prevDeps + 1 ) ) + { + // reset dependencies + // only safe to access pDependentCurr here after above fetch_add because this is the thread + // which calls OnDependenciesComplete after store with memory_order_release + pDependentCurr->pTaskToRunOnCompletion->m_DependenciesCompletedCount.store( + 0, + std::memory_order_release ); + pDependentCurr->pTaskToRunOnCompletion->OnDependenciesComplete( this, threadNum_ ); + } + } +} + +bool TaskScheduler::HaveTasks( uint32_t threadNum_ ) +{ + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + for( uint32_t thread = 0; thread < m_NumThreads; ++thread ) + { + if( !m_pPipesPerThread[ priority ][ thread ].IsPipeEmpty() ) + { + return true; + } + } + if( !m_pPinnedTaskListPerThread[ priority ][ threadNum_ ].IsListEmpty() ) + { + return true; + } + } + return false; +} + +void TaskScheduler::WaitForNewTasks( uint32_t threadNum_ ) +{ + // We don't want to suspend this thread if there are task threads + // with pinned tasks suspended, as it could result in this thread + // being unsuspended and not the thread with pinned tasks + if( WakeSuspendedThreadsWithPinnedTasks( threadNum_ ) ) + { + return; + } + + // We increment the number of threads waiting here in order + // to ensure that the check for tasks occurs after the increment + // to prevent a task being added after a check, then the thread waiting. + // This will occasionally result in threads being mistakenly awoken, + // but they will then go back to sleep. + m_NumThreadsWaitingForNewTasks.fetch_add( 1, std::memory_order_acquire ); + ThreadState prevThreadState = m_pThreadDataStore[threadNum_].threadState.load( std::memory_order_relaxed ); + m_pThreadDataStore[threadNum_].threadState.store( ENKI_THREAD_STATE_WAIT_NEW_TASKS, std::memory_order_seq_cst ); + + if( HaveTasks( threadNum_ ) ) + { + m_NumThreadsWaitingForNewTasks.fetch_sub( 1, std::memory_order_release ); + } + else + { + SafeCallback( m_Config.profilerCallbacks.waitForNewTaskSuspendStart, threadNum_ ); + SemaphoreWait( *m_pNewTaskSemaphore ); + SafeCallback( m_Config.profilerCallbacks.waitForNewTaskSuspendStop, threadNum_ ); + } + + m_pThreadDataStore[threadNum_].threadState.store( prevThreadState, std::memory_order_release ); +} + +void TaskScheduler::WaitForTaskCompletion( const ICompletable* pCompletable_, uint32_t threadNum_ ) +{ + // We don't want to suspend this thread if there are task threads + // with pinned tasks suspended, as the completable could be a pinned task + // or it could be waiting on one. + if( WakeSuspendedThreadsWithPinnedTasks( threadNum_ ) ) + { + return; + } + + m_NumThreadsWaitingForTaskCompletion.fetch_add( 1, std::memory_order_acq_rel ); + pCompletable_->m_WaitingForTaskCount.fetch_add( 1, std::memory_order_acq_rel ); + ThreadState prevThreadState = m_pThreadDataStore[threadNum_].threadState.load( std::memory_order_relaxed ); + m_pThreadDataStore[threadNum_].threadState.store( ENKI_THREAD_STATE_WAIT_TASK_COMPLETION, std::memory_order_seq_cst ); + + // do not wait on semaphore if task in gc_TaskAlmostCompleteCount state. + if( gc_TaskAlmostCompleteCount >= pCompletable_->m_RunningCount.load( std::memory_order_acquire ) || HaveTasks( threadNum_ ) ) + { + m_NumThreadsWaitingForTaskCompletion.fetch_sub( 1, std::memory_order_acq_rel ); + } + else + { + SafeCallback( m_Config.profilerCallbacks.waitForTaskCompleteSuspendStart, threadNum_ ); + std::atomic_thread_fence(std::memory_order_acquire); + + SemaphoreWait( *m_pTaskCompleteSemaphore ); + if( !pCompletable_->GetIsComplete() ) + { + // This thread which may not the one which was supposed to be awoken + WakeThreadsForTaskCompletion(); + } + SafeCallback( m_Config.profilerCallbacks.waitForTaskCompleteSuspendStop, threadNum_ ); + } + + m_pThreadDataStore[threadNum_].threadState.store( prevThreadState, std::memory_order_release ); + pCompletable_->m_WaitingForTaskCount.fetch_sub( 1, std::memory_order_acq_rel ); +} + +void TaskScheduler::WakeThreadsForNewTasks() +{ + int32_t waiting = m_NumThreadsWaitingForNewTasks.load( std::memory_order_relaxed ); + while( waiting > 0 && !m_NumThreadsWaitingForNewTasks.compare_exchange_weak(waiting, 0, std::memory_order_release, std::memory_order_relaxed ) ) {} + + if( waiting > 0 ) + { + SemaphoreSignal( *m_pNewTaskSemaphore, waiting ); + } + + // We also wake tasks waiting for completion as they can run tasks + WakeThreadsForTaskCompletion(); +} + +void TaskScheduler::WakeThreadsForTaskCompletion() +{ + // m_NumThreadsWaitingForTaskCompletion can go negative as this indicates that + // we signalled more threads than the number which ended up waiting + int32_t waiting = m_NumThreadsWaitingForTaskCompletion.load( std::memory_order_relaxed ); + while( waiting > 0 && !m_NumThreadsWaitingForTaskCompletion.compare_exchange_weak(waiting, 0, std::memory_order_release, std::memory_order_relaxed ) ) {} + + if( waiting > 0 ) + { + SemaphoreSignal( *m_pTaskCompleteSemaphore, waiting ); + } +} + +bool TaskScheduler::WakeSuspendedThreadsWithPinnedTasks( uint32_t threadNum_ ) +{ + for( uint32_t t = 1; t < m_NumThreads; ++t ) + { + // distribute thread checks more evenly by starting at our thread number rather than 0. + uint32_t thread = ( threadNum_ + t ) % m_NumThreads; + + ThreadState state = m_pThreadDataStore[ thread ].threadState.load( std::memory_order_acquire ); + + ENKI_ASSERT( state != ENKI_THREAD_STATE_NONE ); + + if( state == ENKI_THREAD_STATE_WAIT_NEW_TASKS || state == ENKI_THREAD_STATE_WAIT_TASK_COMPLETION ) + { + // thread is suspended, check if it has pinned tasks + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + if( !m_pPinnedTaskListPerThread[ priority ][ thread ].IsListEmpty() ) + { + WakeThreadsForNewTasks(); + return true; + } + } + } + } + return false; +} + +void TaskScheduler::SplitAndAddTask( uint32_t threadNum_, SubTaskSet subTask_, uint32_t rangeToSplit_ ) +{ + int32_t numAdded = 0; + int32_t numNewTasksSinceNotification = 0; + + int32_t upperBoundNumToAdd = 2 + (int32_t)( ( subTask_.partition.end - subTask_.partition.start ) / rangeToSplit_ ); + + // ensure that an artificial completion is not registered whilst adding tasks by incrementing count + subTask_.pTask->m_RunningCount.fetch_add( upperBoundNumToAdd, std::memory_order_acquire ); + while( subTask_.partition.start != subTask_.partition.end ) + { + SubTaskSet taskToAdd = SplitTask( subTask_, rangeToSplit_ ); + + // add the partition to the pipe + ++numAdded; ++numNewTasksSinceNotification; + if( !m_pPipesPerThread[ subTask_.pTask->m_Priority ][ threadNum_ ].WriterTryWriteFront( taskToAdd ) ) + { + --numAdded; // we were unable to add the task + if( numNewTasksSinceNotification > 1 ) + { + WakeThreadsForNewTasks(); + } + numNewTasksSinceNotification = 0; + // alter range to run the appropriate fraction + if( taskToAdd.pTask->m_RangeToRun < taskToAdd.partition.end - taskToAdd.partition.start ) + { + taskToAdd.partition.end = taskToAdd.partition.start + taskToAdd.pTask->m_RangeToRun; + ENKI_ASSERT( taskToAdd.partition.end <= taskToAdd.pTask->m_SetSize ); + subTask_.partition.start = taskToAdd.partition.end; + } + taskToAdd.pTask->ExecuteRange( taskToAdd.partition, threadNum_ ); + } + } + int32_t countToRemove = upperBoundNumToAdd - numAdded; + ENKI_ASSERT( countToRemove > 0 ); + int prevCount = subTask_.pTask->m_RunningCount.fetch_sub( countToRemove, std::memory_order_acq_rel ); + if( countToRemove-1 + gc_TaskStartCount == prevCount ) + { + TaskComplete( subTask_.pTask, false, threadNum_ ); + } + + // WakeThreadsForNewTasks also calls WakeThreadsForTaskCompletion() so do not need to do so above + WakeThreadsForNewTasks(); +} + +TaskSchedulerConfig TaskScheduler::GetConfig() const +{ + return m_Config; +} + +void TaskScheduler::AddTaskSetToPipeInt( ITaskSet* pTaskSet_, uint32_t threadNum_ ) +{ + ENKI_ASSERT( pTaskSet_->m_RunningCount == gc_TaskStartCount ); + ThreadState prevThreadState = m_pThreadDataStore[threadNum_].threadState.load( std::memory_order_relaxed ); + m_pThreadDataStore[threadNum_].threadState.store( ENKI_THREAD_STATE_RUNNING, std::memory_order_relaxed ); + std::atomic_thread_fence(std::memory_order_acquire); + + + // divide task up and add to pipe + pTaskSet_->m_RangeToRun = pTaskSet_->m_SetSize / m_NumPartitions; + pTaskSet_->m_RangeToRun = std::max( pTaskSet_->m_RangeToRun, pTaskSet_->m_MinRange ); + // Note: if m_SetSize is < m_RangeToRun this will be handled by SplitTask and so does not need to be handled here + + uint32_t rangeToSplit = pTaskSet_->m_SetSize / m_NumInitialPartitions; + rangeToSplit = std::max( rangeToSplit, pTaskSet_->m_MinRange ); + + SubTaskSet subTask; + subTask.pTask = pTaskSet_; + subTask.partition.start = 0; + subTask.partition.end = pTaskSet_->m_SetSize; + SplitAndAddTask( threadNum_, subTask, rangeToSplit ); + int prevCount = pTaskSet_->m_RunningCount.fetch_sub(1, std::memory_order_acq_rel ); + if( gc_TaskStartCount == prevCount ) + { + TaskComplete( pTaskSet_, true, threadNum_ ); + } + + m_pThreadDataStore[threadNum_].threadState.store( prevThreadState, std::memory_order_release ); +} + +void TaskScheduler::AddTaskSetToPipe( ITaskSet* pTaskSet_ ) +{ + ENKI_ASSERT( pTaskSet_->m_RunningCount == 0 ); + InitDependencies( pTaskSet_ ); + pTaskSet_->m_RunningCount.store( gc_TaskStartCount, std::memory_order_relaxed ); + AddTaskSetToPipeInt( pTaskSet_, gtl_threadNum ); +} + +void TaskScheduler::AddPinnedTaskInt( IPinnedTask* pTask_ ) +{ + ENKI_ASSERT( pTask_->m_RunningCount == gc_TaskStartCount ); + m_pPinnedTaskListPerThread[ pTask_->m_Priority ][ pTask_->threadNum ].WriterWriteFront( pTask_ ); + + ThreadState statePinnedTaskThread = m_pThreadDataStore[ pTask_->threadNum ].threadState.load( std::memory_order_acquire ); + if( statePinnedTaskThread == ENKI_THREAD_STATE_WAIT_NEW_PINNED_TASKS ) + { + SemaphoreSignal( *m_pThreadDataStore[ pTask_->threadNum ].pWaitNewPinnedTaskSemaphore, 1 ); + } + else + { + WakeThreadsForNewTasks(); + } +} + +void TaskScheduler::AddPinnedTask( IPinnedTask* pTask_ ) +{ + ENKI_ASSERT( pTask_->m_RunningCount == 0 ); + InitDependencies( pTask_ ); + pTask_->m_RunningCount = gc_TaskStartCount; + AddPinnedTaskInt( pTask_ ); +} + +void TaskScheduler::InitDependencies( ICompletable* pCompletable_ ) +{ + // go through any dependencies and set their running count so they show as not complete + // and increment dependency count + if( pCompletable_->m_RunningCount.load( std::memory_order_relaxed ) ) + { + // already initialized + return; + } + Dependency* pDependent = pCompletable_->m_pDependents; + while( pDependent ) + { + InitDependencies( pDependent->pTaskToRunOnCompletion ); + pDependent->pTaskToRunOnCompletion->m_RunningCount.store( gc_TaskStartCount, std::memory_order_relaxed ); + pDependent = pDependent->pNext; + } +} + + +void TaskScheduler::RunPinnedTasks() +{ + ENKI_ASSERT( gtl_threadNum != enki::NO_THREAD_NUM ); + uint32_t threadNum = gtl_threadNum; + ThreadState prevThreadState = m_pThreadDataStore[threadNum].threadState.load( std::memory_order_relaxed ); + m_pThreadDataStore[threadNum].threadState.store( ENKI_THREAD_STATE_RUNNING, std::memory_order_relaxed ); + std::atomic_thread_fence(std::memory_order_acquire); + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + RunPinnedTasks( threadNum, priority ); + } + m_pThreadDataStore[threadNum].threadState.store( prevThreadState, std::memory_order_release ); +} + +void TaskScheduler::RunPinnedTasks( uint32_t threadNum_, uint32_t priority_ ) +{ + IPinnedTask* pPinnedTaskSet = NULL; + do + { + pPinnedTaskSet = m_pPinnedTaskListPerThread[ priority_ ][ threadNum_ ].ReaderReadBack(); + if( pPinnedTaskSet ) + { + pPinnedTaskSet->Execute(); + pPinnedTaskSet->m_RunningCount.fetch_sub(1,std::memory_order_acq_rel); + TaskComplete( pPinnedTaskSet, true, threadNum_ ); + } + } while( pPinnedTaskSet ); +} + +void TaskScheduler::WaitforTask( const ICompletable* pCompletable_, enki::TaskPriority priorityOfLowestToRun_ ) +{ + ENKI_ASSERT( gtl_threadNum != enki::NO_THREAD_NUM ); + uint32_t threadNum = gtl_threadNum; + uint32_t hintPipeToCheck_io = threadNum + 1; // does not need to be clamped. + + // waiting for a task is equivalent to 'running' for thread state purpose as we may run tasks whilst waiting + ThreadState prevThreadState = m_pThreadDataStore[threadNum].threadState.load( std::memory_order_relaxed ); + m_pThreadDataStore[threadNum].threadState.store( ENKI_THREAD_STATE_RUNNING, std::memory_order_relaxed ); + std::atomic_thread_fence(std::memory_order_acquire); + + + if( pCompletable_ && !pCompletable_->GetIsComplete() ) + { + SafeCallback( m_Config.profilerCallbacks.waitForTaskCompleteStart, threadNum ); + // We need to ensure that the task we're waiting on can complete even if we're the only thread, + // so we clamp the priorityOfLowestToRun_ to no smaller than the task we're waiting for + priorityOfLowestToRun_ = std::max( priorityOfLowestToRun_, pCompletable_->m_Priority ); + uint32_t spinCount = 0; + while( !pCompletable_->GetIsComplete() && GetIsRunningInt() ) + { + ++spinCount; + for( int priority = 0; priority <= priorityOfLowestToRun_; ++priority ) + { + if( TryRunTask( threadNum, priority, hintPipeToCheck_io ) ) + { + spinCount = 0; // reset spin as ran a task + break; + } + } + if( spinCount > gc_SpinCount ) + { + WaitForTaskCompletion( pCompletable_, threadNum ); + spinCount = 0; + } + else + { + uint32_t spinBackoffCount = spinCount * gc_SpinBackOffMultiplier; + SpinWait( spinBackoffCount ); + } + } + SafeCallback( m_Config.profilerCallbacks.waitForTaskCompleteStop, threadNum ); + } + else if( nullptr == pCompletable_ ) + { + for( int priority = 0; priority <= priorityOfLowestToRun_; ++priority ) + { + if( TryRunTask( threadNum, priority, hintPipeToCheck_io ) ) + { + break; + } + } + } + + m_pThreadDataStore[threadNum].threadState.store( prevThreadState, std::memory_order_release ); + +} + +class TaskSchedulerWaitTask : public IPinnedTask +{ + void Execute() override + { + // do nothing + } +}; + +void TaskScheduler::WaitforAll() +{ + ENKI_ASSERT( gtl_threadNum != enki::NO_THREAD_NUM ); + m_bWaitforAllCalled.store( true, std::memory_order_release ); + + bool bHaveTasks = true; + uint32_t ourThreadNum = gtl_threadNum; + uint32_t hintPipeToCheck_io = ourThreadNum + 1; // does not need to be clamped. + bool otherThreadsRunning = false; // account for this thread + uint32_t spinCount = 0; + TaskSchedulerWaitTask dummyWaitTask; + dummyWaitTask.threadNum = 0; + while( GetIsRunningInt() && ( bHaveTasks || otherThreadsRunning ) ) + { + bHaveTasks = TryRunTask( ourThreadNum, hintPipeToCheck_io ); + ++spinCount; + if( bHaveTasks ) + { + spinCount = 0; // reset spin as ran a task + } + if( spinCount > gc_SpinCount ) + { + // find a running thread and add a dummy wait task + int32_t countThreadsToCheck = m_NumThreads - 1; + bool bHaveThreadToWaitOn = false; + do + { + --countThreadsToCheck; + dummyWaitTask.threadNum = ( dummyWaitTask.threadNum + 1 ) % m_NumThreads; + + // We can only add a pinned task to wait on if we find an enki Task Thread which isn't this thread. + // Otherwise, we have to busy wait. + if( dummyWaitTask.threadNum != ourThreadNum && dummyWaitTask.threadNum > m_Config.numExternalTaskThreads ) + { + ThreadState state = m_pThreadDataStore[ dummyWaitTask.threadNum ].threadState.load( std::memory_order_acquire ); + if( state == ENKI_THREAD_STATE_RUNNING || state == ENKI_THREAD_STATE_WAIT_TASK_COMPLETION ) + { + bHaveThreadToWaitOn = true; + break; + } + } + } while( countThreadsToCheck ); + + if( bHaveThreadToWaitOn ) + { + ENKI_ASSERT( dummyWaitTask.threadNum != ourThreadNum ); + AddPinnedTask( &dummyWaitTask ); + WaitforTask( &dummyWaitTask ); + } + spinCount = 0; + } + else + { + uint32_t spinBackoffCount = spinCount * gc_SpinBackOffMultiplier; + SpinWait( spinBackoffCount ); + } + + // count threads running + otherThreadsRunning = false; + for(uint32_t thread = 0; thread < m_NumThreads && !otherThreadsRunning; ++thread ) + { + // ignore our thread + if( thread != ourThreadNum ) + { + switch( m_pThreadDataStore[thread].threadState.load( std::memory_order_acquire ) ) + { + case ENKI_THREAD_STATE_NONE: + ENKI_ASSERT(false); + break; + case ENKI_THREAD_STATE_NOT_LAUNCHED: + case ENKI_THREAD_STATE_RUNNING: + case ENKI_THREAD_STATE_WAIT_TASK_COMPLETION: + otherThreadsRunning = true; + break; + case ENKI_THREAD_STATE_WAIT_NEW_PINNED_TASKS: + otherThreadsRunning = true; + SemaphoreSignal( *m_pThreadDataStore[thread].pWaitNewPinnedTaskSemaphore, 1 ); + break; + case ENKI_THREAD_STATE_PRIMARY_REGISTERED: + case ENKI_THREAD_STATE_EXTERNAL_REGISTERED: + case ENKI_THREAD_STATE_EXTERNAL_UNREGISTERED: + case ENKI_THREAD_STATE_WAIT_NEW_TASKS: + case ENKI_THREAD_STATE_STOPPED: + break; + } + } + } + if( !otherThreadsRunning ) + { + // check there are no tasks + for(uint32_t thread = 0; thread < m_NumThreads && !otherThreadsRunning; ++thread ) + { + // ignore our thread + if( thread != ourThreadNum ) + { + otherThreadsRunning = HaveTasks( thread ); + } + } + } + } + + m_bWaitforAllCalled.store( false, std::memory_order_release ); +} + +void TaskScheduler::WaitforAllAndShutdown() +{ + m_bWaitforAllCalled.store( true, std::memory_order_release ); + m_bShutdownRequested.store( true, std::memory_order_release ); + if( m_bHaveThreads ) + { + WaitforAll(); + StopThreads(true); + } +} + +void TaskScheduler::ShutdownNow() +{ + m_bWaitforAllCalled.store( true, std::memory_order_release ); + m_bShutdownRequested.store( true, std::memory_order_release ); + if( m_bHaveThreads ) + { + StopThreads(true); + } +} + +void TaskScheduler::WaitForNewPinnedTasks() +{ + ENKI_ASSERT( gtl_threadNum != enki::NO_THREAD_NUM ); + uint32_t threadNum = gtl_threadNum; + ThreadState prevThreadState = m_pThreadDataStore[threadNum].threadState.load( std::memory_order_relaxed ); + m_pThreadDataStore[threadNum].threadState.store( ENKI_THREAD_STATE_WAIT_NEW_PINNED_TASKS, std::memory_order_seq_cst ); + + // check if have tasks inside threadState change but before waiting + bool bHavePinnedTasks = false; + for( int priority = 0; priority < TASK_PRIORITY_NUM; ++priority ) + { + if( !m_pPinnedTaskListPerThread[ priority ][ threadNum ].IsListEmpty() ) + { + bHavePinnedTasks = true; + break; + } + } + + if( !bHavePinnedTasks ) + { + SafeCallback( m_Config.profilerCallbacks.waitForNewTaskSuspendStart, threadNum ); + SemaphoreWait( *m_pThreadDataStore[threadNum].pWaitNewPinnedTaskSemaphore ); + SafeCallback( m_Config.profilerCallbacks.waitForNewTaskSuspendStop, threadNum ); + } + + m_pThreadDataStore[threadNum].threadState.store( prevThreadState, std::memory_order_release ); +} + + +uint32_t TaskScheduler::GetNumTaskThreads() const +{ + return m_NumThreads; +} + + +uint32_t TaskScheduler::GetThreadNum() const +{ + return gtl_threadNum; +} + +template +T* TaskScheduler::NewArray( size_t num_, const char* file_, int line_ ) +{ + T* pRet = (T*)m_Config.customAllocator.alloc( alignof(T), num_*sizeof(T), m_Config.customAllocator.userData, file_, line_ ); + if( !std::is_trivial::value ) + { + T* pCurr = pRet; + for( size_t i = 0; i < num_; ++i ) + { + void* pBuffer = pCurr; + pCurr = new(pBuffer) T; + ++pCurr; + } + } + return pRet; +} + +template +void TaskScheduler::DeleteArray( T* p_, size_t num_, const char* file_, int line_ ) +{ + if( !std::is_trivially_destructible::value ) + { + size_t i = num_; + while(i) + { + p_[--i].~T(); + } + } + m_Config.customAllocator.free( p_, sizeof(T)*num_, m_Config.customAllocator.userData, file_, line_ ); +} + +template +T* TaskScheduler::New( const char* file_, int line_, Args&&... args_ ) +{ + T* pRet = this->Alloc( file_, line_ ); + return new(pRet) T( std::forward(args_)... ); +} + +template< typename T > +void TaskScheduler::Delete( T* p_, const char* file_, int line_ ) +{ + p_->~T(); + this->Free(p_, file_, line_ ); +} + +template< typename T > +T* TaskScheduler::Alloc( const char* file_, int line_ ) +{ + T* pRet = (T*)m_Config.customAllocator.alloc( alignof(T), sizeof(T), m_Config.customAllocator.userData, file_, line_ ); + return pRet; +} + +template< typename T > +void TaskScheduler::Free( T* p_, const char* file_, int line_ ) +{ + m_Config.customAllocator.free( p_, sizeof(T), m_Config.customAllocator.userData, file_, line_ ); +} + +TaskScheduler::TaskScheduler() + : m_pPipesPerThread() + , m_pPinnedTaskListPerThread() + , m_NumThreads(0) + , m_pThreadDataStore(NULL) + , m_pThreads(NULL) + , m_bRunning(false) + , m_NumInternalTaskThreadsRunning(0) + , m_NumThreadsWaitingForNewTasks(0) + , m_NumThreadsWaitingForTaskCompletion(0) + , m_NumPartitions(0) + , m_pNewTaskSemaphore(NULL) + , m_pTaskCompleteSemaphore(NULL) + , m_NumInitialPartitions(0) + , m_bHaveThreads(false) + , m_NumExternalTaskThreadsRegistered(0) +{ +} + +TaskScheduler::~TaskScheduler() +{ + StopThreads( true ); // Stops threads, waiting for them. +} + +void TaskScheduler::Initialize( uint32_t numThreadsTotal_ ) +{ + ENKI_ASSERT( numThreadsTotal_ >= 1 ); + StopThreads( true ); // Stops threads, waiting for them. + m_Config.numTaskThreadsToCreate = numThreadsTotal_ - 1; + m_Config.numExternalTaskThreads = 0; + StartThreads();} + +void TaskScheduler::Initialize( TaskSchedulerConfig config_ ) +{ + StopThreads( true ); // Stops threads, waiting for them. + m_Config = config_; + StartThreads(); +} + +void TaskScheduler::Initialize() +{ + Initialize( std::thread::hardware_concurrency() ); +} + +// Semaphore implementation +#ifdef _WIN32 + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include + +namespace enki +{ + struct semaphoreid_t + { + HANDLE sem; + }; + + inline void SemaphoreCreate( semaphoreid_t& semaphoreid ) + { +#ifdef _XBOX_ONE + semaphoreid.sem = CreateSemaphoreExW( NULL, 0, MAXLONG, NULL, 0, SEMAPHORE_ALL_ACCESS ); +#else + semaphoreid.sem = CreateSemaphore( NULL, 0, MAXLONG, NULL ); +#endif + } + + inline void SemaphoreClose( semaphoreid_t& semaphoreid ) + { + CloseHandle( semaphoreid.sem ); + } + + inline void SemaphoreWait( semaphoreid_t& semaphoreid ) + { + DWORD retval = WaitForSingleObject( semaphoreid.sem, INFINITE ); + ENKI_ASSERT( retval != WAIT_FAILED ); + (void)retval; // only needed for ENKI_ASSERT + } + + inline void SemaphoreSignal( semaphoreid_t& semaphoreid, int32_t countWaiting ) + { + if( countWaiting ) + { + ReleaseSemaphore( semaphoreid.sem, countWaiting, NULL ); + } + } +} +#elif defined(__MACH__) + + +// OS X does not have POSIX semaphores +// Mach semaphores can now only be created by the kernel +// Named semaphores work, but would require unique name construction to ensure +// they are isolated to this process. +// Dispatch semaphores appear to be the way other developers use OSX Semaphores, e.g. Boost +// However the API could change +// OSX below 10.6 does not support dispatch, but I do not have an earlier OSX version +// to test alternatives +#include + +namespace enki +{ + + struct semaphoreid_t + { + dispatch_semaphore_t sem; + }; + + inline void SemaphoreCreate( semaphoreid_t& semaphoreid ) + { + semaphoreid.sem = dispatch_semaphore_create(0); + } + + inline void SemaphoreClose( semaphoreid_t& semaphoreid ) + { + dispatch_release( semaphoreid.sem ); + } + + inline void SemaphoreWait( semaphoreid_t& semaphoreid ) + { + dispatch_semaphore_wait( semaphoreid.sem, DISPATCH_TIME_FOREVER ); + } + + inline void SemaphoreSignal( semaphoreid_t& semaphoreid, int32_t countWaiting ) + { + while( countWaiting-- > 0 ) + { + dispatch_semaphore_signal( semaphoreid.sem ); + } + } +} + +#else // POSIX + +#include +#include + +namespace enki +{ + + struct semaphoreid_t + { + sem_t sem; + }; + + inline void SemaphoreCreate( semaphoreid_t& semaphoreid ) + { + int err = sem_init( &semaphoreid.sem, 0, 0 ); + ENKI_ASSERT( err == 0 ); + (void)err; + } + + inline void SemaphoreClose( semaphoreid_t& semaphoreid ) + { + sem_destroy( &semaphoreid.sem ); + } + + inline void SemaphoreWait( semaphoreid_t& semaphoreid ) + { + while( sem_wait( &semaphoreid.sem ) == -1 && errno == EINTR ) {} + } + + inline void SemaphoreSignal( semaphoreid_t& semaphoreid, int32_t countWaiting ) + { + while( countWaiting-- > 0 ) + { + sem_post( &semaphoreid.sem ); + } + } +} +#endif + +semaphoreid_t* TaskScheduler::SemaphoreNew() +{ + semaphoreid_t* pSemaphore = this->Alloc( ENKI_FILE_AND_LINE ); + SemaphoreCreate( *pSemaphore ); + return pSemaphore; +} + +void TaskScheduler::SemaphoreDelete( semaphoreid_t* pSemaphore_ ) +{ + SemaphoreClose( *pSemaphore_ ); + this->Free( pSemaphore_, ENKI_FILE_AND_LINE ); +} + +void TaskScheduler::SetCustomAllocator( CustomAllocator customAllocator_ ) +{ + m_Config.customAllocator = customAllocator_; +} + +Dependency::Dependency( const ICompletable* pDependencyTask_, ICompletable* pTaskToRunOnCompletion_ ) + : pTaskToRunOnCompletion( pTaskToRunOnCompletion_ ) + , pDependencyTask( pDependencyTask_ ) + , pNext( pDependencyTask->m_pDependents ) +{ + ENKI_ASSERT( pDependencyTask->GetIsComplete() ); + ENKI_ASSERT( pTaskToRunOnCompletion->GetIsComplete() ); + pDependencyTask->m_pDependents = this; + ++pTaskToRunOnCompletion->m_DependenciesCount; +} + +Dependency::Dependency( Dependency&& rhs_ ) noexcept +{ + pDependencyTask = rhs_.pDependencyTask; + pTaskToRunOnCompletion = rhs_.pTaskToRunOnCompletion; + pNext = rhs_.pNext; + if( rhs_.pDependencyTask ) + { + ENKI_ASSERT( rhs_.pTaskToRunOnCompletion ); + ENKI_ASSERT( rhs_.pDependencyTask->GetIsComplete() ); + ENKI_ASSERT( rhs_.pTaskToRunOnCompletion->GetIsComplete() ); + Dependency** ppDependent = &(pDependencyTask->m_pDependents); + while( *ppDependent ) + { + if( &rhs_ == *ppDependent ) + { + *ppDependent = this; + break; + } + ppDependent = &((*ppDependent)->pNext); + } + } +} + + +Dependency::~Dependency() +{ + ClearDependency(); +} + +void Dependency::SetDependency( const ICompletable* pDependencyTask_, ICompletable* pTaskToRunOnCompletion_ ) +{ + ClearDependency(); + ENKI_ASSERT( pDependencyTask_->GetIsComplete() ); + ENKI_ASSERT( pTaskToRunOnCompletion_->GetIsComplete() ); + pDependencyTask = pDependencyTask_; + pTaskToRunOnCompletion = pTaskToRunOnCompletion_; + pNext = pDependencyTask->m_pDependents; + pDependencyTask->m_pDependents = this; + ++pTaskToRunOnCompletion->m_DependenciesCount; +} + +void Dependency::ClearDependency() +{ + if( pDependencyTask ) + { + ENKI_ASSERT( pTaskToRunOnCompletion ); + ENKI_ASSERT( pDependencyTask->GetIsComplete() ); + ENKI_ASSERT( pTaskToRunOnCompletion->GetIsComplete() ); + ENKI_ASSERT( pTaskToRunOnCompletion->m_DependenciesCount > 0 ); + Dependency* pDependent = pDependencyTask->m_pDependents; + --pTaskToRunOnCompletion->m_DependenciesCount; + if( this == pDependent ) + { + pDependencyTask->m_pDependents = pDependent->pNext; + } + else + { + while( pDependent ) + { + Dependency* pPrev = pDependent; + pDependent = pDependent->pNext; + if( this == pDependent ) + { + pPrev->pNext = pDependent->pNext; + break; + } + } + } + } + pDependencyTask = NULL; + pDependencyTask = NULL; + pNext = NULL; +} diff --git a/Axiom/Jobs/TaskScheduler.h b/Axiom/Jobs/TaskScheduler.h new file mode 100644 index 00000000..9d1d962f --- /dev/null +++ b/Axiom/Jobs/TaskScheduler.h @@ -0,0 +1,583 @@ +// Copyright (c) 2013 Doug Binks +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgement in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. + +#pragma once + +#include +#include +#include +#include +#include + +// ENKITS_TASK_PRIORITIES_NUM can be set from 1 to 5. +// 1 corresponds to effectively no priorities. +#ifndef ENKITS_TASK_PRIORITIES_NUM + #define ENKITS_TASK_PRIORITIES_NUM 3 +#endif + +#ifndef ENKITS_API +#if defined(_WIN32) && defined(ENKITS_BUILD_DLL) + // Building enkiTS as a DLL + #define ENKITS_API __declspec(dllexport) +#elif defined(_WIN32) && defined(ENKITS_DLL) + // Using enkiTS as a DLL + #define ENKITS_API __declspec(dllimport) +#elif defined(__GNUC__) && defined(ENKITS_BUILD_DLL) + // Building enkiTS as a shared library + #define ENKITS_API __attribute__((visibility("default"))) +#else + #define ENKITS_API +#endif +#endif + +// Define ENKI_CUSTOM_ALLOC_FILE_AND_LINE (at project level) to get file and line report in custom allocators, +// this is default in Debug - to turn off define ENKI_CUSTOM_ALLOC_NO_FILE_AND_LINE +#ifndef ENKI_CUSTOM_ALLOC_FILE_AND_LINE +#if defined(_DEBUG ) && !defined(ENKI_CUSTOM_ALLOC_NO_FILE_AND_LINE) +#define ENKI_CUSTOM_ALLOC_FILE_AND_LINE +#endif +#endif + +#ifndef ENKI_ASSERT +#include +#define ENKI_ASSERT(x) assert(x) +#endif + +#if (!defined(_MSVC_LANG) && __cplusplus >= 201402L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L) +#define ENKI_DEPRECATED [[deprecated]] +#else +#define ENKI_DEPRECATED +#endif + +namespace enki +{ + struct TaskSetPartition + { + uint32_t start; + uint32_t end; + }; + + class TaskScheduler; + class TaskPipe; + class PinnedTaskList; + class Dependency; + struct ThreadArgs; + struct ThreadDataStore; + struct SubTaskSet; + struct semaphoreid_t; + + static constexpr uint32_t NO_THREAD_NUM = 0xFFFFFFFF; + + ENKITS_API uint32_t GetNumHardwareThreads(); + + enum TaskPriority + { + TASK_PRIORITY_HIGH = 0, +#if ( ENKITS_TASK_PRIORITIES_NUM > 3 ) + TASK_PRIORITY_MED_HI, +#endif +#if ( ENKITS_TASK_PRIORITIES_NUM > 2 ) + TASK_PRIORITY_MED, +#endif +#if ( ENKITS_TASK_PRIORITIES_NUM > 4 ) + TASK_PRIORITY_MED_LO, +#endif +#if ( ENKITS_TASK_PRIORITIES_NUM > 1 ) + TASK_PRIORITY_LOW, +#endif + TASK_PRIORITY_NUM + }; + + // ICompletable is a base class used to check for completion. + // Can be used with dependencies to wait for their completion. + // Derive from ITaskSet or IPinnedTask for running parallel tasks. + class ICompletable + { + public: + bool GetIsComplete() const { + return 0 == m_RunningCount.load( std::memory_order_acquire ); + } + + virtual ~ICompletable(); + + // Dependency helpers, see Dependencies.cpp + void SetDependency( Dependency& dependency_, const ICompletable* pDependencyTask_ ); + template void SetDependenciesArr( D& dependencyArray_ , const T(&taskArray_)[SIZE] ); + template void SetDependenciesArr( D& dependencyArray_, std::initializer_list taskpList_ ); + template void SetDependenciesArr( D(&dependencyArray_)[SIZE], const T(&taskArray_)[SIZE] ); + template void SetDependenciesArr( D(&dependencyArray_)[SIZE], std::initializer_list taskpList_ ); + template void SetDependenciesVec( D& dependencyVec_, const T(&taskArray_)[SIZE] ); + template void SetDependenciesVec( D& dependencyVec_, std::initializer_list taskpList_ ); + + TaskPriority m_Priority = TASK_PRIORITY_HIGH; + protected: + // Deriving from an ICompletable and overriding OnDependenciesComplete is advanced use. + // If you do override OnDependenciesComplete() call: + // ICompletable::OnDependenciesComplete( pTaskScheduler_, threadNum_ ); + // in your implementation. + virtual void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ); + private: + friend class TaskScheduler; + friend class Dependency; + std::atomic m_RunningCount = {0}; + std::atomic m_DependenciesCompletedCount = {0}; + int32_t m_DependenciesCount = 0; + mutable std::atomic m_WaitingForTaskCount = {0}; + mutable Dependency* m_pDependents = NULL; + }; + + // Subclass ITaskSet to create tasks. + // TaskSets can be re-used, but check completion first. + class ITaskSet : public ICompletable + { + public: + ITaskSet() = default; + ITaskSet( uint32_t setSize_ ) + : m_SetSize( setSize_ ) + {} + + ITaskSet( uint32_t setSize_, uint32_t minRange_ ) + : m_SetSize( setSize_ ) + , m_MinRange( minRange_ ) + , m_RangeToRun(minRange_) + {} + + // Execute range should be overloaded to process tasks. It will be called with a + // range_ where range.start >= 0; range.start < range.end; and range.end < m_SetSize; + // The range values should be mapped so that linearly processing them in order is cache friendly + // i.e. neighbouring values should be close together. + // threadnum_ should not be used for changing processing of data, its intended purpose + // is to allow per-thread data buckets for output. + virtual void ExecuteRange( TaskSetPartition range_, uint32_t threadnum_ ) = 0; + + // Set Size - usually the number of data items to be processed, see ExecuteRange. Defaults to 1 + uint32_t m_SetSize = 1; + + // Min Range - Minimum size of TaskSetPartition range when splitting a task set into partitions. + // Designed for reducing scheduling overhead by preventing set being + // divided up too small. Ranges passed to ExecuteRange will *not* be a multiple of this, + // only attempts to deliver range sizes larger than this most of the time. + // This should be set to a value which results in computation effort of at least 10k + // clock cycles to minimize task scheduler overhead. + // NOTE: The last partition will be smaller than m_MinRange if m_SetSize is not a multiple + // of m_MinRange. + // Also known as grain size in literature. + uint32_t m_MinRange = 1; + + private: + friend class TaskScheduler; + void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ) final; + uint32_t m_RangeToRun = 1; + }; + + // Subclass IPinnedTask to create tasks which can be run on a given thread only. + class IPinnedTask : public ICompletable + { + public: + IPinnedTask() = default; + IPinnedTask( uint32_t threadNum_ ) : threadNum(threadNum_) {} // default is to run a task on main thread + + // IPinnedTask needs to be non-abstract for intrusive list functionality. + // Should never be called as is, should be overridden. + virtual void Execute() { ENKI_ASSERT(false); } + + uint32_t threadNum = 0; // thread to run this pinned task on + std::atomic pNext = {NULL}; + private: + void OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ) final; + }; + + // TaskSet - a utility task set for creating tasks based on std::function. + typedef std::function TaskSetFunction; + class TaskSet : public ITaskSet + { + public: + TaskSet() = default; + TaskSet( TaskSetFunction func_ ) : m_Function( std::move(func_) ) {} + TaskSet( uint32_t setSize_, TaskSetFunction func_ ) : ITaskSet( setSize_ ), m_Function( std::move(func_) ) {} + + void ExecuteRange( TaskSetPartition range_, uint32_t threadnum_ ) override { m_Function( range_, threadnum_ ); } + TaskSetFunction m_Function; + }; + + // LambdaPinnedTask - a utility pinned task for creating tasks based on std::func. + typedef std::function PinnedTaskFunction; + class LambdaPinnedTask : public IPinnedTask + { + public: + LambdaPinnedTask() = default; + LambdaPinnedTask( PinnedTaskFunction func_ ) : m_Function( std::move(func_) ) {} + LambdaPinnedTask( uint32_t threadNum_, PinnedTaskFunction func_ ) : IPinnedTask( threadNum_ ), m_Function( std::move(func_) ) {} + + void Execute() override { m_Function(); } + PinnedTaskFunction m_Function; + }; + + class Dependency + { + public: + Dependency() = default; + Dependency( const Dependency& ) = delete; + ENKITS_API Dependency( Dependency&& ) noexcept; + ENKITS_API Dependency( const ICompletable* pDependencyTask_, ICompletable* pTaskToRunOnCompletion_ ); + ENKITS_API ~Dependency(); + + ENKITS_API void SetDependency( const ICompletable* pDependencyTask_, ICompletable* pTaskToRunOnCompletion_ ); + ENKITS_API void ClearDependency(); + ICompletable* GetTaskToRunOnCompletion() { return pTaskToRunOnCompletion; } + const ICompletable* GetDependencyTask() { return pDependencyTask; } + private: + friend class TaskScheduler; friend class ICompletable; + ICompletable* pTaskToRunOnCompletion = NULL; + const ICompletable* pDependencyTask = NULL; + Dependency* pNext = NULL; + }; + + // TaskScheduler implements several callbacks intended for profilers + typedef void (*ProfilerCallbackFunc)( uint32_t threadnum_ ); + struct ProfilerCallbacks + { + ProfilerCallbackFunc threadStart; + ProfilerCallbackFunc threadStop; + ProfilerCallbackFunc waitForNewTaskSuspendStart; // thread suspended waiting for new tasks + ProfilerCallbackFunc waitForNewTaskSuspendStop; // thread unsuspended + ProfilerCallbackFunc waitForTaskCompleteStart; // thread waiting for task completion + ProfilerCallbackFunc waitForTaskCompleteStop; // thread stopped waiting + ProfilerCallbackFunc waitForTaskCompleteSuspendStart; // thread suspended waiting task completion + ProfilerCallbackFunc waitForTaskCompleteSuspendStop; // thread unsuspended + }; + + // Custom allocator, set in TaskSchedulerConfig. Also see ENKI_CUSTOM_ALLOC_FILE_AND_LINE for file_ and line_ + typedef void* (*AllocFunc)( size_t align_, size_t size_, void* userData_, const char* file_, int line_ ); + typedef void (*FreeFunc)( void* ptr_, size_t size_, void* userData_, const char* file_, int line_ ); + ENKITS_API void* DefaultAllocFunc( size_t align_, size_t size_, void* userData_, const char* file_, int line_ ); + ENKITS_API void DefaultFreeFunc( void* ptr_, size_t size_, void* userData_, const char* file_, int line_ ); + struct CustomAllocator + { + AllocFunc alloc = DefaultAllocFunc; + FreeFunc free = DefaultFreeFunc; + void* userData = nullptr; + }; + + // TaskSchedulerConfig - configuration struct for advanced Initialize + struct TaskSchedulerConfig + { + // numTaskThreadsToCreate - Number of tasking threads the task scheduler will create. Must be > 0. + // Defaults to GetNumHardwareThreads()-1 threads as thread which calls initialize is thread 0. + uint32_t numTaskThreadsToCreate = GetNumHardwareThreads()-1; + + // numExternalTaskThreads - Advanced use. Number of external threads which need to use TaskScheduler API. + // See TaskScheduler::RegisterExternalTaskThread() for usage. + // Defaults to 0. The thread used to initialize the TaskScheduler can also use the TaskScheduler API. + // Thus there are (numTaskThreadsToCreate + numExternalTaskThreads + 1) able to use the API, with this + // defaulting to the number of hardware threads available to the system. + uint32_t numExternalTaskThreads = 0; + + ProfilerCallbacks profilerCallbacks = {}; + + CustomAllocator customAllocator; + }; + + class TaskScheduler + { + public: + ENKITS_API TaskScheduler(); + ENKITS_API ~TaskScheduler(); + + // Call an Initialize function before adding tasks. + + // Initialize() will create GetNumHardwareThreads()-1 tasking threads, which is + // sufficient to fill the system when including the main thread. + // Initialize can be called multiple times - it will wait for completion + // before re-initializing. + ENKITS_API void Initialize(); + + // Initialize( numThreadsTotal_ ) + // will create numThreadsTotal_-1 threads, as thread 0 is + // the thread on which the initialize was called. + // numThreadsTotal_ must be > 0 + ENKITS_API void Initialize( uint32_t numThreadsTotal_ ); + + // Initialize with advanced TaskSchedulerConfig settings. See TaskSchedulerConfig. + ENKITS_API void Initialize( TaskSchedulerConfig config_ ); + + // Get config. Can be called before Initialize to get the defaults. + ENKITS_API TaskSchedulerConfig GetConfig() const; + + // while( !GetIsShutdownRequested() ) {} can be used in tasks which loop, to check if enkiTS has been requested to shutdown. + // If GetIsShutdownRequested() returns true should then exit. Not required for finite tasks + // Safe to use with WaitforAllAndShutdown() and ShutdownNow() where this will be set + // Not safe to use with WaitforAll(), use GetIsWaitforAllCalled() instead. + inline bool GetIsShutdownRequested() const { return m_bShutdownRequested.load( std::memory_order_acquire ); } + + // while( !GetIsWaitforAllCalled() ) {} can be used in tasks which loop, to check if WaitforAll() has been called. + // If GetIsWaitforAllCalled() returns false should then exit. Not required for finite tasks + // This is intended to be used with code which calls WaitforAll(). + // This is also set when the task manager is shutting down, so no need to have an additional check for GetIsShutdownRequested() + inline bool GetIsWaitforAllCalled() const { return m_bWaitforAllCalled.load( std::memory_order_acquire ); } + + // Adds the TaskSet to pipe and returns if the pipe is not full. + // If the pipe is full, pTaskSet is run. + // should only be called from main thread, or within a task + ENKITS_API void AddTaskSetToPipe( ITaskSet* pTaskSet_ ); + + // Thread 0 is main thread, otherwise use threadNum + // Pinned tasks can be added from any thread + ENKITS_API void AddPinnedTask( IPinnedTask* pTask_ ); + + // This function will run any IPinnedTask* for current thread, but not run other + // Main thread should call this or use a wait to ensure its tasks are run. + ENKITS_API void RunPinnedTasks(); + + // Runs the TaskSets in pipe until true == pTaskSet->GetIsComplete(); + // Should only be called from thread which created the task scheduler, or within a task. + // If called with 0 it will try to run tasks, and return if none are available. + // To run only a subset of tasks, set priorityOfLowestToRun_ to a high priority. + // Default is lowest priority available. + // Only wait for child tasks of the current task otherwise a deadlock could occur. + // WaitforTask will exit if ShutdownNow() is called even if pCompletable_ is not complete. + ENKITS_API void WaitforTask( const ICompletable* pCompletable_, enki::TaskPriority priorityOfLowestToRun_ = TaskPriority(TASK_PRIORITY_NUM - 1) ); + + // Waits for all task sets to complete - not guaranteed to work unless we know we + // are in a situation where tasks aren't being continuously added. + // If you are running tasks which loop, make sure to check GetIsWaitforAllCalled() and exit + // WaitforAll will exit if ShutdownNow() is called even if there are still tasks to run or currently running + ENKITS_API void WaitforAll(); + + // Waits for all task sets to complete and shutdown threads - not guaranteed to work unless we know we + // are in a situation where tasks aren't being continuously added. + // This function can be safely called even if TaskScheduler::Initialize() has not been called. + ENKITS_API void WaitforAllAndShutdown(); + + // Shutdown threads without waiting for all tasks to complete. + // Intended to be used to exit an application quickly. + // This function can be safely called even if TaskScheduler::Initialize() has not been called. + // This function will still wait for any running tasks to exit before the task threads exit. + // ShutdownNow will cause tasks which have been added to the scheduler but not completed + // to be in an undefined state in which should not be re-launched. + ENKITS_API void ShutdownNow(); + + // Waits for the current thread to receive a PinnedTask. + // Will not run any tasks - use with RunPinnedTasks(). + // Can be used with both ExternalTaskThreads or with an enkiTS tasking thread to create + // a thread which only runs pinned tasks. If enkiTS threads are used can create + // extra enkiTS task threads to handle non-blocking computation via normal tasks. + ENKITS_API void WaitForNewPinnedTasks(); + + // Returns the number of threads created for running tasks + number of external threads + // plus 1 to account for the thread used to initialize the task scheduler. + // Equivalent to config values: numTaskThreadsToCreate + numExternalTaskThreads + 1. + // It is guaranteed that GetThreadNum() < GetNumTaskThreads() + ENKITS_API uint32_t GetNumTaskThreads() const; + + // Returns the current task threadNum. + // Will return 0 for thread which initialized the task scheduler, + // and NO_THREAD_NUM for all other non-enkiTS threads which have not been registered ( see RegisterExternalTaskThread() ), + // and < GetNumTaskThreads() for all registered and internal enkiTS threads. + // It is guaranteed that GetThreadNum() < GetNumTaskThreads() unless it is NO_THREAD_NUM + ENKITS_API uint32_t GetThreadNum() const; + + // Call on a thread to register the thread to use the TaskScheduling API. + // This is implicitly done for the thread which initializes the TaskScheduler + // Intended for developers who have threads who need to call the TaskScheduler API + // Returns true if successful, false if not. + // Can only have numExternalTaskThreads registered at any one time, which must be set + // at initialization time. + ENKITS_API bool RegisterExternalTaskThread(); + + // As RegisterExternalTaskThread() but explicitly requests a given thread number. + // threadNumToRegister_ must be >= GetNumFirstExternalTaskThread() + // and < ( GetNumFirstExternalTaskThread() + numExternalTaskThreads ). + ENKITS_API bool RegisterExternalTaskThread( uint32_t threadNumToRegister_ ); + + // Call on a thread on which RegisterExternalTaskThread has been called to deregister that thread. + ENKITS_API void DeRegisterExternalTaskThread(); + + // Get the number of registered external task threads. + ENKITS_API uint32_t GetNumRegisteredExternalTaskThreads(); + + // Get the thread number of the first external task thread. This thread + // is not guaranteed to be registered, but threads are registered in order + // from GetNumFirstExternalTaskThread() up to ( GetNumFirstExternalTaskThread() + numExternalTaskThreads ) + // Note that if numExternalTaskThreads == 0 a for loop using this will be valid: + // for( uint32_t externalThreadNum = GetNumFirstExternalTaskThread(); + // externalThreadNum < ( GetNumFirstExternalTaskThread() + numExternalTaskThreads + // ++externalThreadNum ) { // do something with externalThreadNum } + inline static constexpr uint32_t GetNumFirstExternalTaskThread() { return 1; } + + // ------------- Start DEPRECATED Functions ------------- + // DEPRECATED: use GetIsShutdownRequested() instead of GetIsRunning() in external code + // while( GetIsRunning() ) {} can be used in tasks which loop, to check if enkiTS has been shutdown. + // If GetIsRunning() returns false should then exit. Not required for finite tasks. + ENKI_DEPRECATED inline bool GetIsRunning() const { return !GetIsShutdownRequested(); } + + // DEPRECATED - WaitforTaskSet, deprecated interface use WaitforTask. + ENKI_DEPRECATED inline void WaitforTaskSet( const ICompletable* pCompletable_ ) { WaitforTask( pCompletable_ ); } + + // DEPRECATED - GetProfilerCallbacks. Use TaskSchedulerConfig instead. + // Returns the ProfilerCallbacks structure so that it can be modified to + // set the callbacks. Should be set prior to initialization. + ENKI_DEPRECATED inline ProfilerCallbacks* GetProfilerCallbacks() { return &m_Config.profilerCallbacks; } + // ------------- End DEPRECATED Functions ------------- + + private: + friend class ICompletable; friend class ITaskSet; friend class IPinnedTask; + static void TaskingThreadFunction( const ThreadArgs& args_ ); + bool HaveTasks( uint32_t threadNum_ ); + void WaitForNewTasks( uint32_t threadNum_ ); + void WaitForTaskCompletion( const ICompletable* pCompletable_, uint32_t threadNum_ ); + void RunPinnedTasks( uint32_t threadNum_, uint32_t priority_ ); + bool TryRunTask( uint32_t threadNum_, uint32_t& hintPipeToCheck_io_ ); + bool TryRunTask( uint32_t threadNum_, uint32_t priority_, uint32_t& hintPipeToCheck_io_ ); + void StartThreads(); + void StopThreads( bool bWait_ ); + void SplitAndAddTask( uint32_t threadNum_, SubTaskSet subTask_, uint32_t rangeToSplit_ ); + void WakeThreadsForNewTasks(); + void WakeThreadsForTaskCompletion(); + bool WakeSuspendedThreadsWithPinnedTasks( uint32_t threadNum_ ); + void InitDependencies( ICompletable* pCompletable_ ); + inline bool GetIsRunningInt() const { return m_bRunning.load( std::memory_order_acquire ); } + + ENKITS_API void TaskComplete( ICompletable* pTask_, bool bWakeThreads_, uint32_t threadNum_ ); + ENKITS_API void AddTaskSetToPipeInt( ITaskSet* pTaskSet_, uint32_t threadNum_ ); + ENKITS_API void AddPinnedTaskInt( IPinnedTask* pTask_ ); + + template< typename T > T* NewArray( size_t num_, const char* file_, int line_ ); + template< typename T > void DeleteArray( T* p_, size_t num_, const char* file_, int line_ ); + template T* New( const char* file_, int line_, Args&&... args_ ); + template< typename T > void Delete( T* p_, const char* file_, int line_ ); + template< typename T > T* Alloc( const char* file_, int line_ ); + template< typename T > void Free( T* p_, const char* file_, int line_ ); + semaphoreid_t* SemaphoreNew(); + void SemaphoreDelete( semaphoreid_t* pSemaphore_ ); + + TaskPipe* m_pPipesPerThread[ TASK_PRIORITY_NUM ]; + PinnedTaskList* m_pPinnedTaskListPerThread[ TASK_PRIORITY_NUM ]; + + uint32_t m_NumThreads; + ThreadDataStore* m_pThreadDataStore; + std::thread* m_pThreads; + std::atomic m_bRunning; + std::atomic m_bShutdownRequested; + std::atomic m_bWaitforAllCalled; + std::atomic m_NumInternalTaskThreadsRunning; + std::atomic m_NumThreadsWaitingForNewTasks; + std::atomic m_NumThreadsWaitingForTaskCompletion; + uint32_t m_NumPartitions; + semaphoreid_t* m_pNewTaskSemaphore; + semaphoreid_t* m_pTaskCompleteSemaphore; + uint32_t m_NumInitialPartitions; + bool m_bHaveThreads; + TaskSchedulerConfig m_Config; + std::atomic m_NumExternalTaskThreadsRegistered; + + TaskScheduler( const TaskScheduler& nocopy_ ); + TaskScheduler& operator=( const TaskScheduler& nocopy_ ); + + protected: + void SetCustomAllocator( CustomAllocator customAllocator_ ); // for C interface + }; + + inline void ICompletable::OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ) + { + m_RunningCount.fetch_sub( 1, std::memory_order_acq_rel ); + pTaskScheduler_->TaskComplete( this, true, threadNum_ ); + } + + inline void ITaskSet::OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ) + { + pTaskScheduler_->AddTaskSetToPipeInt( this, threadNum_ ); + } + + inline void IPinnedTask::OnDependenciesComplete( TaskScheduler* pTaskScheduler_, uint32_t threadNum_ ) + { + (void)threadNum_; + pTaskScheduler_->AddPinnedTaskInt( this ); + } + + inline ICompletable::~ICompletable() + { + ENKI_ASSERT( GetIsComplete() ); // this task is still waiting to run + Dependency* pDependency = m_pDependents; + while( pDependency ) + { + Dependency* pNext = pDependency->pNext; + pDependency->pDependencyTask = NULL; + pDependency->pNext = NULL; + pDependency = pNext; + } + } + + inline void ICompletable::SetDependency( Dependency& dependency_, const ICompletable* pDependencyTask_ ) + { + ENKI_ASSERT( pDependencyTask_ != this ); + dependency_.SetDependency( pDependencyTask_, this ); + } + + template + void ICompletable::SetDependenciesArr( D& dependencyArray_ , const T(&taskArray_)[SIZE] ) { + static_assert( std::tuple_size::value >= SIZE, "Size of dependency array too small" ); + for( int i = 0; i < SIZE; ++i ) + { + dependencyArray_[i].SetDependency( &taskArray_[i], this ); + } + } + template + void ICompletable::SetDependenciesArr( D& dependencyArray_, std::initializer_list taskpList_ ) { + ENKI_ASSERT( std::tuple_size::value >= taskpList_.size() ); + int i = 0; + for( auto pTask : taskpList_ ) + { + dependencyArray_[i++].SetDependency( pTask, this ); + } + } + template + void ICompletable::SetDependenciesArr( D(&dependencyArray_)[SIZE], const T(&taskArray_)[SIZE] ) { + for( int i = 0; i < SIZE; ++i ) + { + dependencyArray_[i].SetDependency( &taskArray_[i], this ); + } + } + template + void ICompletable::SetDependenciesArr( D(&dependencyArray_)[SIZE], std::initializer_list taskpList_ ) { + ENKI_ASSERT( SIZE >= taskpList_.size() ); + int i = 0; + for( auto pTask : taskpList_ ) + { + dependencyArray_[i++].SetDependency( pTask, this ); + } + } + template + void ICompletable::SetDependenciesVec( D& dependencyVec_, const T(&taskArray_)[SIZE] ) { + dependencyVec_.resize( SIZE ); + for( int i = 0; i < SIZE; ++i ) + { + dependencyVec_[i].SetDependency( &taskArray_[i], this ); + } + } + + template + void ICompletable::SetDependenciesVec( D& dependencyVec_, std::initializer_list taskpList_ ) { + dependencyVec_.resize( taskpList_.size() ); + int i = 0; + for( auto pTask : taskpList_ ) + { + dependencyVec_[i++].SetDependency( pTask, this ); + } + } +} diff --git a/Axiom/Remote/AxiomSessionEndpoint.cpp b/Axiom/Net/Remote/AxiomSessionEndpoint.cpp similarity index 100% rename from Axiom/Remote/AxiomSessionEndpoint.cpp rename to Axiom/Net/Remote/AxiomSessionEndpoint.cpp diff --git a/Axiom/Remote/AxiomSessionEndpoint.h b/Axiom/Net/Remote/AxiomSessionEndpoint.h similarity index 100% rename from Axiom/Remote/AxiomSessionEndpoint.h rename to Axiom/Net/Remote/AxiomSessionEndpoint.h diff --git a/Axiom/Remote/SessionTransport.h b/Axiom/Net/Remote/SessionTransport.h similarity index 100% rename from Axiom/Remote/SessionTransport.h rename to Axiom/Net/Remote/SessionTransport.h diff --git a/Axiom/Physics/PhysicsWorld.h b/Axiom/Physics/PhysicsWorld.h index 7e75e222..3aa7ea98 100644 --- a/Axiom/Physics/PhysicsWorld.h +++ b/Axiom/Physics/PhysicsWorld.h @@ -1,6 +1,6 @@ #pragma once -#include "Session/EditorSession.h" +#include "Session/RuntimeSceneState.h" #include #include @@ -9,6 +9,7 @@ namespace Axiom { struct PhysicsTransformUpdate { + SceneObjectHandle ObjectHandle{}; std::string ObjectId; EditorTransformDetails WorldTransform; }; @@ -21,7 +22,7 @@ class PhysicsWorld final { bool IsAvailable() const; bool IsRunning() const; - void Start(const EditorSceneState &Scene); + void Start(const RuntimeSceneState &Scene); void Stop(); std::vector Step(float DeltaTimeSeconds); diff --git a/Axiom/Physics/PhysicsWorld.cpp b/Axiom/Physics/PhysicsWorldEnabled.cpp similarity index 75% rename from Axiom/Physics/PhysicsWorld.cpp rename to Axiom/Physics/PhysicsWorldEnabled.cpp index 6e01edd5..0c79d463 100644 --- a/Axiom/Physics/PhysicsWorld.cpp +++ b/Axiom/Physics/PhysicsWorldEnabled.cpp @@ -2,10 +2,6 @@ #include -#include -#include - -#if AXIOM_ENABLE_PHYSICS #include #include #include @@ -19,15 +15,15 @@ #include #include +#include +#include + #include #include #include #include -#endif namespace Axiom { - -#if AXIOM_ENABLE_PHYSICS namespace { constexpr JPH::ObjectLayer kStaticObjectLayer = 0; @@ -151,11 +147,10 @@ JoltRuntime &GetJoltRuntime() { } } // namespace -#endif struct PhysicsWorld::Impl { -#if AXIOM_ENABLE_PHYSICS struct BodyRecord { + SceneObjectHandle ObjectHandle{}; std::string ObjectId; EditorPhysicsBodyType BodyType{EditorPhysicsBodyType::None}; JPH::BodyID BodyId; @@ -169,7 +164,8 @@ struct PhysicsWorld::Impl { std::unique_ptr JobSystem; std::unique_ptr System; std::vector Bodies; - std::unordered_map BodyIndexByObjectId; + std::unordered_map + BodyIndexByHandle; bool Running{false}; Impl() { @@ -182,14 +178,17 @@ struct PhysicsWorld::Impl { System->SetGravity(JPH::Vec3(0.0f, -9.81f, 0.0f)); } - JPH::ShapeRefC BuildShape(const EditorPhysicsProperties &Physics, const glm::vec3 &Scale) { - switch (Physics.ColliderType) { + JPH::ShapeRefC BuildShape(const RuntimeSceneBodyState &Body) { + switch (Body.ColliderType) { case EditorPhysicsColliderType::Box: - return new JPH::BoxShape(ToJoltVec3(Physics.BoxHalfExtents * Scale)); + return new JPH::BoxShape( + ToJoltVec3(Body.BoxHalfExtents * Body.WorldTransform.Scale)); case EditorPhysicsColliderType::Sphere: - return new JPH::SphereShape(Physics.SphereRadius * - std::max({std::abs(Scale.x), std::abs(Scale.y), - std::abs(Scale.z)})); + return new JPH::SphereShape( + Body.SphereRadius * + std::max({std::abs(Body.WorldTransform.Scale.x), + std::abs(Body.WorldTransform.Scale.y), + std::abs(Body.WorldTransform.Scale.z)})); default: return nullptr; } @@ -198,7 +197,7 @@ struct PhysicsWorld::Impl { void Reset() { if (System == nullptr) { Bodies.clear(); - BodyIndexByObjectId.clear(); + BodyIndexByHandle.clear(); Running = false; return; } @@ -211,38 +210,22 @@ struct PhysicsWorld::Impl { } } Bodies.clear(); - BodyIndexByObjectId.clear(); + BodyIndexByHandle.clear(); Running = false; } -#endif }; -PhysicsWorld::PhysicsWorld() { -#if AXIOM_ENABLE_PHYSICS - m_Impl = std::make_unique(); -#endif -} +PhysicsWorld::PhysicsWorld() : m_Impl(std::make_unique()) {} PhysicsWorld::~PhysicsWorld() = default; -bool PhysicsWorld::IsAvailable() const { -#if AXIOM_ENABLE_PHYSICS - return m_Impl != nullptr; -#else - return false; -#endif -} +bool PhysicsWorld::IsAvailable() const { return m_Impl != nullptr; } bool PhysicsWorld::IsRunning() const { -#if AXIOM_ENABLE_PHYSICS return m_Impl != nullptr && m_Impl->Running; -#else - return false; -#endif } -void PhysicsWorld::Start(const EditorSceneState &Scene) { -#if AXIOM_ENABLE_PHYSICS +void PhysicsWorld::Start(const RuntimeSceneState &Scene) { if (m_Impl == nullptr) { return; } @@ -250,80 +233,75 @@ void PhysicsWorld::Start(const EditorSceneState &Scene) { m_Impl->Reset(); auto &BodyInterface = m_Impl->System->GetBodyInterface(); - for (const auto &[ObjectId, Details] : Scene.ObjectDetailsById) { - if (!Details.Transform.has_value() || !Details.Physics.has_value()) { + for (const RuntimeSceneBodyState &Body : Scene.Bodies) { + if (Body.BodyType == EditorPhysicsBodyType::None || + Body.ColliderType == EditorPhysicsColliderType::None) { continue; } - const EditorPhysicsProperties &Physics = *Details.Physics; - if (Physics.BodyType == EditorPhysicsBodyType::None || - Physics.ColliderType == EditorPhysicsColliderType::None) { + if (Body.MaterialIndex >= Scene.Materials.size()) { + A_CORE_WARN("PhysicsWorld: body '{}' referenced invalid material index {}", + Body.ObjectId, Body.MaterialIndex); continue; } - const EditorTransformDetails &Transform = Details.WorldTransform.has_value() - ? *Details.WorldTransform - : *Details.Transform; + const RuntimePhysicsMaterial &Material = Scene.Materials[Body.MaterialIndex]; - JPH::ShapeRefC Shape = m_Impl->BuildShape(Physics, Transform.Scale); + JPH::ShapeRefC Shape = m_Impl->BuildShape(Body); if (Shape == nullptr) { continue; } const JPH::EMotionType MotionType = - Physics.BodyType == EditorPhysicsBodyType::Dynamic + Body.BodyType == EditorPhysicsBodyType::Dynamic ? JPH::EMotionType::Dynamic : JPH::EMotionType::Static; const JPH::ObjectLayer Layer = - Physics.BodyType == EditorPhysicsBodyType::Dynamic ? kDynamicObjectLayer - : kStaticObjectLayer; - - JPH::BodyCreationSettings Settings( - Shape.GetPtr(), ToJoltRVec3(Transform.Location), - ToJoltQuatDegrees(Transform.RotationDegrees), MotionType, Layer); - Settings.mFriction = Physics.Friction; - Settings.mRestitution = Physics.Restitution; - if (Physics.BodyType == EditorPhysicsBodyType::Dynamic && - Physics.Mass > 0.0f) { - Settings.mMassPropertiesOverride.mMass = Physics.Mass; + Body.BodyType == EditorPhysicsBodyType::Dynamic ? kDynamicObjectLayer + : kStaticObjectLayer; + + JPH::BodyCreationSettings Settings(Shape.GetPtr(), + ToJoltRVec3(Body.WorldTransform.Location), + ToJoltQuatDegrees( + Body.WorldTransform.RotationDegrees), + MotionType, Layer); + Settings.mFriction = Material.Friction; + Settings.mRestitution = Material.Restitution; + if (Body.BodyType == EditorPhysicsBodyType::Dynamic && Body.Mass > 0.0f) { + Settings.mMassPropertiesOverride.mMass = Body.Mass; Settings.mOverrideMassProperties = JPH::EOverrideMassProperties::CalculateInertia; } const JPH::BodyID BodyId = BodyInterface.CreateAndAddBody( - Settings, Physics.BodyType == EditorPhysicsBodyType::Dynamic + Settings, Body.BodyType == EditorPhysicsBodyType::Dynamic ? JPH::EActivation::Activate : JPH::EActivation::DontActivate); if (BodyId.IsInvalid()) { - A_CORE_WARN("PhysicsWorld: failed to create body for '{}'", ObjectId); + A_CORE_WARN("PhysicsWorld: failed to create body for '{}'", Body.ObjectId); continue; } const size_t Index = m_Impl->Bodies.size(); - m_Impl->Bodies.push_back({.ObjectId = ObjectId, - .BodyType = Physics.BodyType, + m_Impl->Bodies.push_back({.ObjectId = Body.ObjectId, + .ObjectHandle = Body.ObjectHandle, + .BodyType = Body.BodyType, .BodyId = BodyId, - .Scale = Transform.Scale}); - m_Impl->BodyIndexByObjectId.emplace(ObjectId, Index); + .Scale = Body.WorldTransform.Scale}); + m_Impl->BodyIndexByHandle.emplace(Body.ObjectHandle, Index); } m_Impl->Running = true; -#else - (void)Scene; -#endif } void PhysicsWorld::Stop() { -#if AXIOM_ENABLE_PHYSICS if (m_Impl != nullptr) { m_Impl->Reset(); } -#endif } std::vector PhysicsWorld::Step(float DeltaTimeSeconds) { std::vector Updates; -#if AXIOM_ENABLE_PHYSICS if (m_Impl == nullptr || !m_Impl->Running || DeltaTimeSeconds <= 0.0f) { return Updates; } @@ -338,6 +316,7 @@ std::vector PhysicsWorld::Step(float DeltaTimeSeconds) { } Updates.push_back({ + .ObjectHandle = Body.ObjectHandle, .ObjectId = Body.ObjectId, .WorldTransform = EditorTransformDetails{ @@ -349,9 +328,6 @@ std::vector PhysicsWorld::Step(float DeltaTimeSeconds) { }, }); } -#else - (void)DeltaTimeSeconds; -#endif return Updates; } diff --git a/Axiom/Physics/Session/EditorPhysicsController.cpp b/Axiom/Physics/Session/EditorPhysicsController.cpp new file mode 100644 index 00000000..273c2f74 --- /dev/null +++ b/Axiom/Physics/Session/EditorPhysicsController.cpp @@ -0,0 +1,161 @@ +#include "Session/EditorPhysicsController.h" + +#include "Physics/PhysicsWorld.h" +#include "Session/EditorSceneStateManager.h" + +#include + +#include +#include + +namespace Axiom { +namespace { +TextureSourceDataRef CloneTextureSourceData( + const TextureSourceDataRef &Texture) { + if (!Texture) { + return nullptr; + } + + auto Copy = std::make_shared(); + Copy->Width = Texture->Width; + Copy->Height = Texture->Height; + Copy->Pixels = Texture->Pixels; + return Copy; +} + +std::shared_ptr +CloneMaterialInstance(const std::shared_ptr &Material) { + if (!Material) { + return nullptr; + } + + auto Copy = std::make_shared(); + Copy->BaseColorTexture = CloneTextureSourceData(Material->BaseColorTexture); + Copy->BaseColorFactor = Material->BaseColorFactor; + Copy->Metallic = Material->Metallic; + Copy->Roughness = Material->Roughness; + Copy->TextureAssetPath = Material->TextureAssetPath; + return Copy; +} + +EditorSceneState CloneEditorSceneState(const EditorSceneState &Scene) { + EditorSceneState Copy = Scene; + for (auto &MeshInstance : Copy.MeshInstances) { + MeshInstance.Material = CloneMaterialInstance(MeshInstance.Material); + } + return Copy; +} + +RuntimeSceneState BuildRuntimeSceneState(const EditorSceneState &Scene) { + RuntimeSceneState RuntimeScene; + RuntimeScene.Materials.push_back({}); + + struct MaterialKey { + float Friction{0.2f}; + float Restitution{0.0f}; + + bool operator==(const MaterialKey &) const = default; + }; + + struct MaterialKeyHash { + size_t operator()(const MaterialKey &Key) const noexcept { + const uint32_t FrictionBits = std::bit_cast(Key.Friction); + const uint32_t RestitutionBits = std::bit_cast(Key.Restitution); + return (static_cast(FrictionBits) << 32u) ^ + static_cast(RestitutionBits); + } + }; + + std::unordered_map MaterialIndices; + MaterialIndices.emplace(MaterialKey{}, 0u); + + for (const auto &[ObjectId, Details] : Scene.ObjectDetailsById) { + if (!Details.Transform.has_value() || !Details.Physics.has_value()) { + continue; + } + + const EditorPhysicsProperties &Physics = *Details.Physics; + if (Physics.BodyType == EditorPhysicsBodyType::None || + Physics.ColliderType == EditorPhysicsColliderType::None) { + continue; + } + + const EditorTransformDetails &WorldTransform = + Details.WorldTransform.has_value() ? *Details.WorldTransform + : *Details.Transform; + const MaterialKey Key{.Friction = Physics.Friction, + .Restitution = Physics.Restitution}; + const auto [It, Inserted] = + MaterialIndices.emplace( + Key, static_cast(RuntimeScene.Materials.size())); + if (Inserted) { + RuntimeScene.Materials.push_back( + {.Friction = Physics.Friction, .Restitution = Physics.Restitution}); + } + + RuntimeScene.Bodies.push_back({ + .ObjectHandle = Details.Handle, + .ObjectId = ObjectId, + .WorldTransform = WorldTransform, + .BodyType = Physics.BodyType, + .ColliderType = Physics.ColliderType, + .BoxHalfExtents = Physics.BoxHalfExtents, + .SphereRadius = Physics.SphereRadius, + .Mass = Physics.Mass, + .MaterialIndex = It->second, + }); + } + + return RuntimeScene; +} +} // namespace + +EditorPhysicsController::EditorPhysicsController(EditorSession &Session) + : m_Session(Session) {} + +void EditorPhysicsController::EnsurePhysicsWorldStarted() { + if (m_PhysicsWorld == nullptr) { + m_PhysicsWorld = std::make_unique(); + } + if (!m_PhysicsWorld->IsAvailable()) { + A_CORE_WARN("EditorSession: physics requested but backend is unavailable"); + return; + } + m_PhysicsWorld->Start(BuildRuntimeSceneState(m_Session.GetState().Scene)); +} + +void EditorPhysicsController::StopPhysicsWorld() { + if (m_PhysicsWorld != nullptr) { + m_PhysicsWorld->Stop(); + } +} + +void EditorPhysicsController::StepRuntimePhysics(float DeltaTimeSeconds) { + if (m_Session.GetRuntimeState() != EditorRuntimeState::Playing || + m_PhysicsWorld == nullptr || !m_PhysicsWorld->IsRunning()) { + return; + } + + for (const PhysicsTransformUpdate &Update : m_PhysicsWorld->Step(DeltaTimeSeconds)) { + const EditorObjectDetails *Existing = + Update.ObjectHandle ? m_Session.FindObjectDetails(Update.ObjectHandle) + : m_Session.FindObjectDetails(Update.ObjectId); + if (Existing == nullptr) { + continue; + } + + EditorTransformDetails Applied = Update.WorldTransform; + if (Existing->WorldTransform.has_value()) { + Applied.Scale = Existing->WorldTransform->Scale; + } else if (Existing->Transform.has_value()) { + Applied.Scale = Existing->Transform->Scale; + } + m_Session.ApplyRuntimeWorldTransform(Existing->ObjectId, Applied); + } +} + +void AttachEditorPhysicsController(EditorSession &Session) { + Session.SetRuntimePhysicsController( + std::make_unique(Session)); +} +} // namespace Axiom diff --git a/Axiom/Physics/Session/EditorPhysicsController.h b/Axiom/Physics/Session/EditorPhysicsController.h new file mode 100644 index 00000000..4312c66e --- /dev/null +++ b/Axiom/Physics/Session/EditorPhysicsController.h @@ -0,0 +1,22 @@ +#pragma once + +#include "Session/EditorRuntimePhysicsController.h" +#include "Physics/PhysicsWorld.h" +#include "Session/EditorSession.h" + +namespace Axiom { +class EditorPhysicsController final : public IEditorRuntimePhysicsController { +public: + explicit EditorPhysicsController(EditorSession &Session); + + void EnsurePhysicsWorldStarted() override; + void StopPhysicsWorld() override; + void StepRuntimePhysics(float DeltaTimeSeconds) override; + +private: + EditorSession &m_Session; + std::unique_ptr m_PhysicsWorld; +}; + +void AttachEditorPhysicsController(EditorSession &Session); +} // namespace Axiom diff --git a/Axiom/RHI/IRHI.h b/Axiom/RHI/IRHI.h new file mode 100644 index 00000000..1a81c4b9 --- /dev/null +++ b/Axiom/RHI/IRHI.h @@ -0,0 +1,228 @@ +#pragma once + +#include "Renderer/RendererTypes.h" + +#include +#include +#include +#include + +namespace Axiom { +enum class RHIQueueType : uint8_t { + Graphics = 0, + Compute, + Transfer, +}; + +enum class RHIPipelineType : uint8_t { + Graphics = 0, + Compute, +}; + +enum class RHICommandStage : uint8_t { + All = 0, + Draw, + ColorAttachmentOutput, + Compute, + Transfer, +}; + +enum class RHITextureDimension : uint8_t { + Texture2D = 0, + Texture3D, + TextureCube, +}; + +enum class RHITextureFormat : uint16_t { + Unknown = 0, + RGBA8Unorm, + RGBA16Float, + BGRA8Unorm, + D32Float, + D32FloatS8, +}; + +enum class RHIBufferUsage : uint64_t { + None = 0, + TransferSrc = 1ull << 0, + TransferDst = 1ull << 1, + Vertex = 1ull << 2, + Index = 1ull << 3, + Uniform = 1ull << 4, + Storage = 1ull << 5, + Indirect = 1ull << 6, + Readback = 1ull << 7, +}; + +enum class RHITextureUsage : uint64_t { + None = 0, + TransferSrc = 1ull << 0, + TransferDst = 1ull << 1, + Sampled = 1ull << 2, + Storage = 1ull << 3, + ColorAttachment = 1ull << 4, + DepthStencilAttachment = 1ull << 5, + Present = 1ull << 6, +}; + +inline constexpr RHIBufferUsage operator|(RHIBufferUsage Left, + RHIBufferUsage Right) { + return static_cast(static_cast(Left) | + static_cast(Right)); +} + +inline constexpr RHITextureUsage operator|(RHITextureUsage Left, + RHITextureUsage Right) { + return static_cast(static_cast(Left) | + static_cast(Right)); +} + +struct RHIBufferDesc { + size_t Size{0}; + RHIBufferUsage Usage{RHIBufferUsage::None}; + bool HostVisible{false}; + bool HostCoherent{false}; + bool PersistentlyMapped{false}; +}; + +struct RHITextureDesc { + RHITextureDimension Dimension{RHITextureDimension::Texture2D}; + RHITextureFormat Format{RHITextureFormat::Unknown}; + RHITextureUsage Usage{RHITextureUsage::None}; + uint32_t Width{0}; + uint32_t Height{0}; + uint32_t DepthOrArrayLayers{1}; + uint32_t MipLevels{1}; +}; + +struct RHIPipelineDesc { + RHIPipelineType Type{RHIPipelineType::Graphics}; +}; + +struct RHIDeviceCreateInfo { + RenderSurfacePtr TargetSurface; + IViewportFrameOutput *FrameOutput{nullptr}; + uint32_t Width{0}; + uint32_t Height{0}; + RendererAttachmentRequirements AttachmentRequirements{}; +}; + +class IRHIQueue; +class IRHICommandList; +class IRHIBuffer; +class IRHITexture; +class IRHIPipeline; +class IRHIDescriptorTable; +class IRHIBindGroup; +class IRHISwapchain; +class IRHIFence; +class IRHISemaphore; + +struct RHIQueueWaitInfo { + IRHISemaphore *Semaphore{nullptr}; + uint64_t Value{0}; + RHICommandStage Stage{RHICommandStage::All}; +}; + +struct RHIQueueSignalInfo { + IRHISemaphore *Semaphore{nullptr}; + uint64_t Value{0}; + RHICommandStage Stage{RHICommandStage::All}; +}; + +class IRHICommandList { +public: + virtual ~IRHICommandList() = default; + + virtual void Begin() = 0; + virtual void End() = 0; + [[nodiscard]] virtual bool IsRecording() const = 0; + [[nodiscard]] virtual RHIQueueType GetQueueType() const = 0; +}; + +class IRHIQueue { +public: + virtual ~IRHIQueue() = default; + + [[nodiscard]] virtual RHIQueueType GetType() const = 0; + virtual void Submit(IRHICommandList &CommandList, + std::span WaitSemaphores = {}, + std::span SignalSemaphores = {}, + IRHIFence *Fence = nullptr) = 0; +}; + +class IRHIBuffer { +public: + virtual ~IRHIBuffer() = default; + + [[nodiscard]] virtual const RHIBufferDesc &GetDesc() const = 0; +}; + +class IRHITexture { +public: + virtual ~IRHITexture() = default; + + [[nodiscard]] virtual const RHITextureDesc &GetDesc() const = 0; +}; + +class IRHIPipeline { +public: + virtual ~IRHIPipeline() = default; + + [[nodiscard]] virtual RHIPipelineType GetType() const = 0; +}; + +class IRHIDescriptorTable { +public: + virtual ~IRHIDescriptorTable() = default; +}; + +class IRHIBindGroup { +public: + virtual ~IRHIBindGroup() = default; +}; + +class IRHISwapchain { +public: + virtual ~IRHISwapchain() = default; +}; + +class IRHIFence { +public: + virtual ~IRHIFence() = default; +}; + +class IRHISemaphore { +public: + virtual ~IRHISemaphore() = default; + + [[nodiscard]] virtual bool IsTimeline() const = 0; +}; + +class IRHIDevice { +public: + virtual ~IRHIDevice() = default; + + virtual void Init(const RHIDeviceCreateInfo &CreateInfo) = 0; + virtual void Shutdown() = 0; + virtual void BeginFrame() = 0; + virtual void WaitIdle() = 0; + + [[nodiscard]] virtual IRHIQueue *GetQueue(RHIQueueType Type) = 0; + [[nodiscard]] virtual std::unique_ptr + CreateCommandList(RHIQueueType Type) = 0; + [[nodiscard]] virtual std::unique_ptr + CreateBuffer(const RHIBufferDesc &Desc) = 0; + [[nodiscard]] virtual std::unique_ptr + CreateTexture(const RHITextureDesc &Desc) = 0; + [[nodiscard]] virtual std::unique_ptr + CreatePipeline(const RHIPipelineDesc &Desc) = 0; + [[nodiscard]] virtual std::unique_ptr + CreateDescriptorTable() = 0; + [[nodiscard]] virtual std::unique_ptr CreateBindGroup() = 0; + [[nodiscard]] virtual std::unique_ptr CreateSwapchain() = 0; + [[nodiscard]] virtual std::unique_ptr CreateFence() = 0; + [[nodiscard]] virtual std::unique_ptr + CreateSemaphore(bool Timeline) = 0; +}; +} // namespace Axiom diff --git a/Axiom/RHI/RHIFactory.h b/Axiom/RHI/RHIFactory.h new file mode 100644 index 00000000..002bb455 --- /dev/null +++ b/Axiom/RHI/RHIFactory.h @@ -0,0 +1,9 @@ +#pragma once + +#include "RHI/IRHI.h" + +#include + +namespace Axiom { +std::unique_ptr CreateRHIDevice(RendererBackendType BackendType); +} // namespace Axiom diff --git a/Axiom/Renderer/ForwardRenderer.cpp b/Axiom/Renderer/ForwardRenderer.cpp deleted file mode 100644 index 805a0855..00000000 --- a/Axiom/Renderer/ForwardRenderer.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "Renderer/ForwardRenderer.h" - -#include "Renderer/RenderScene.h" -#include "Renderer/RendererBackend.h" - -namespace Axiom { -void ForwardRenderer::Init(RendererBackend *Backend) { m_Backend = Backend; } - -void ForwardRenderer::Shutdown() { m_Backend = nullptr; } - -void ForwardRenderer::Render(RenderScene &Scene) { - if (!Scene.Submissions.empty()) { - m_Backend->RenderSceneMeshes(Scene); - return; - } - - m_Backend->RenderFallbackBackground(Scene); -} -} // namespace Axiom diff --git a/Axiom/Renderer/ForwardRenderer.h b/Axiom/Renderer/ForwardRenderer.h deleted file mode 100644 index f3193e2c..00000000 --- a/Axiom/Renderer/ForwardRenderer.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "Renderer/RenderTechnique.h" - -namespace Axiom { -class ForwardRenderer final : public RenderTechnique { -public: - void Init(RendererBackend *Backend) override; - void Shutdown() override; - void Render(RenderScene &Scene) override; - -private: - RendererBackend *m_Backend{nullptr}; -}; -} // namespace Axiom diff --git a/Axiom/Renderer/Material.h b/Axiom/Renderer/Material.h index e2e28c24..71929ea8 100644 --- a/Axiom/Renderer/Material.h +++ b/Axiom/Renderer/Material.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -36,15 +38,30 @@ struct HDRTextureSourceData { using HDRTextureSourceDataRef = std::shared_ptr; +struct MaterialHandle { + uint32_t Value{0}; + + [[nodiscard]] bool IsValid() const { return Value != 0; } + auto operator<=>(const MaterialHandle &) const = default; +}; + +struct MaterialHandleHash { + size_t operator()(const MaterialHandle &Handle) const noexcept { + return std::hash{}(Handle.Value); + } +}; + struct MaterialInstance { TextureSourceDataRef BaseColorTexture; glm::vec4 BaseColorFactor{1.0f}; float Metallic{0.0f}; float Roughness{0.5f}; + uint64_t Revision{0}; // Content-relative path of the standalone texture assigned via // SetMaterialTextureCommand; empty if the texture came from the mesh asset. std::string TextureAssetPath; }; - -using MaterialInstanceRef = std::shared_ptr; +inline void MarkMaterialInstanceDirty(MaterialInstance &Material) { + ++Material.Revision; +} } // namespace Axiom diff --git a/Axiom/Renderer/Mesh.cpp b/Axiom/Renderer/Mesh.cpp new file mode 100644 index 00000000..660dae58 --- /dev/null +++ b/Axiom/Renderer/Mesh.cpp @@ -0,0 +1,42 @@ +#include "Renderer/Mesh.h" + +#include +#include + +namespace Axiom { +namespace { +std::mutex g_RenderMeshDebugDataMutex; +std::deque g_RenderMeshDebugData{ + RenderMeshSubmissionDebugData{}}; +} // namespace + +RenderMeshSubmissionDebugDataId +RegisterRenderMeshSubmissionDebugData(RenderMeshSubmissionDebugData Data) { + std::scoped_lock Lock(g_RenderMeshDebugDataMutex); + g_RenderMeshDebugData.push_back(std::move(Data)); + return static_cast( + g_RenderMeshDebugData.size() - 1); +} + +const RenderMeshSubmissionDebugData * +TryGetRenderMeshSubmissionDebugData(RenderMeshSubmissionDebugDataId Id) { + std::scoped_lock Lock(g_RenderMeshDebugDataMutex); + if (Id == 0 || Id >= g_RenderMeshDebugData.size()) { + return nullptr; + } + return &g_RenderMeshDebugData[Id]; +} + +std::string_view +GetRenderMeshSubmissionDebugName(RenderMeshSubmissionDebugDataId Id) { + if (const auto *DebugData = TryGetRenderMeshSubmissionDebugData(Id); + DebugData != nullptr) { + return DebugData->Name; + } + return {}; +} + +MeshHandle GetMeshHandle(const MeshRef &Mesh) { + return Mesh != nullptr ? Mesh->GetHandle() : MeshHandle{}; +} +} // namespace Axiom diff --git a/Axiom/Renderer/Mesh.h b/Axiom/Renderer/Mesh.h index 4bc23a27..4cf0a4bb 100644 --- a/Axiom/Renderer/Mesh.h +++ b/Axiom/Renderer/Mesh.h @@ -7,9 +7,12 @@ #include #include +#include #include #include #include +#include +#include #include #include @@ -20,10 +23,11 @@ enum class MeshRenderPath { }; struct MeshVertex { - glm::vec4 Position{0.0f, 0.0f, 0.0f, 1.0f}; - glm::vec4 Normal{0.0f, 0.0f, 1.0f, 0.0f}; + glm::vec3 Position{0.0f, 0.0f, 0.0f}; + glm::vec3 Normal{0.0f, 0.0f, 1.0f}; glm::vec2 TexCoord{0.0f, 0.0f}; }; +static_assert(sizeof(MeshVertex) == 32); struct MeshData { std::vector Vertices; @@ -36,7 +40,7 @@ struct MeshSceneData { struct MeshInstanceData { std::string Name; MeshData Mesh; - MaterialInstanceRef Material; + std::shared_ptr Material; glm::mat4 Transform{1.0f}; }; @@ -48,19 +52,75 @@ struct MeshSceneLoadOptions { std::unordered_set ComputeMeshNames; }; +struct MeshCreateOptions { + bool KeepCpuData{false}; +}; + +struct MeshHandle { + uint64_t Value{0}; + + [[nodiscard]] bool IsValid() const { return Value != 0; } + auto operator<=>(const MeshHandle &) const = default; +}; + +struct MeshHandleHash { + size_t operator()(const MeshHandle &Handle) const noexcept { + return std::hash{}(Handle.Value); + } +}; + class Mesh { public: virtual ~Mesh() = default; + + [[nodiscard]] MeshHandle GetHandle() const { return m_Handle; } + [[nodiscard]] bool HasHandle() const { return m_Handle.IsValid(); } + void AssignHandle(MeshHandle Handle) { m_Handle = Handle; } + +private: + MeshHandle m_Handle{}; }; using MeshRef = std::shared_ptr; -struct RenderMeshSubmission { +MeshHandle GetMeshHandle(const MeshRef &Mesh); + +struct RenderMeshResource { + MeshHandle Handle{}; MeshRef Mesh; - MaterialInstanceRef Material; + + [[nodiscard]] bool IsValid() const { + return Handle.IsValid() && Mesh != nullptr; + } +}; + +struct RenderMeshSubmissionDebugData { std::string Name; +}; + +using RenderMeshSubmissionDebugDataId = uint32_t; + +RenderMeshSubmissionDebugDataId +RegisterRenderMeshSubmissionDebugData(RenderMeshSubmissionDebugData Data); +const RenderMeshSubmissionDebugData * +TryGetRenderMeshSubmissionDebugData(RenderMeshSubmissionDebugDataId Id); +std::string_view +GetRenderMeshSubmissionDebugName(RenderMeshSubmissionDebugDataId Id); + +struct RenderMeshSubmission { + MeshHandle MeshHandle{}; + // Material handles are resolved through the renderer-owned material registry. + // Producers must keep the registered material valid for the frame in which + // this submission is consumed. + MaterialHandle MaterialHandle{}; + RenderMeshSubmissionDebugDataId DebugDataId{0}; MeshRenderPath RenderPath{MeshRenderPath::Graphics}; glm::mat4 Transform{1.0f}; bool Translucent{false}; }; + +struct LoadedMeshScene { + std::vector Resources; + std::vector Submissions; +}; } // namespace Axiom diff --git a/Axiom/Renderer/RenderCommand.cpp b/Axiom/Renderer/RenderCommand.cpp index 61454bb5..e2c8e41d 100644 --- a/Axiom/Renderer/RenderCommand.cpp +++ b/Axiom/Renderer/RenderCommand.cpp @@ -1,10 +1,9 @@ #include "Renderer/RenderCommand.h" namespace Axiom { -RenderScene *RenderCommand::s_ActiveScene = nullptr; +thread_local RenderScene *RenderCommand::s_ActiveScene = nullptr; void RenderCommand::BeginScene(RenderScene &Scene) { - Scene.Reset(); s_ActiveScene = &Scene; } diff --git a/Axiom/Renderer/RenderCommand.h b/Axiom/Renderer/RenderCommand.h index 46e3f805..49feaf59 100644 --- a/Axiom/Renderer/RenderCommand.h +++ b/Axiom/Renderer/RenderCommand.h @@ -18,6 +18,6 @@ class RenderCommand { static void EndScene(); private: - static RenderScene *s_ActiveScene; + static thread_local RenderScene *s_ActiveScene; }; } // namespace Axiom diff --git a/Axiom/Renderer/RenderScene.cpp b/Axiom/Renderer/RenderScene.cpp index 886fa50b..311ea294 100644 --- a/Axiom/Renderer/RenderScene.cpp +++ b/Axiom/Renderer/RenderScene.cpp @@ -2,11 +2,16 @@ namespace Axiom { void RenderScene::Reset() { + FrameNumber = 0; + CpuFrameMs = 0.0f; ActiveCamera = nullptr; BackgroundColor = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); Submissions.clear(); GizmoOverlay.reset(); Sun.reset(); LightBillboards.clear(); + SkyboxColorTop = glm::vec3(0.08f, 0.09f, 0.14f); + SkyboxColorBottom = glm::vec3(0.14f, 0.24f, 0.38f); + SkyboxHDRTexture.reset(); } } // namespace Axiom diff --git a/Axiom/Renderer/RenderScene.h b/Axiom/Renderer/RenderScene.h index 2f04686f..f6d8a979 100644 --- a/Axiom/Renderer/RenderScene.h +++ b/Axiom/Renderer/RenderScene.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -36,10 +37,34 @@ struct LightBillboardOverlay { float PixelSize{48.0f}; }; +struct VisibleSubmission { + uint32_t SubmissionIndex{0}; + MeshHandle MeshHandle{}; + float SortDepth{0.0f}; +}; + +struct VisibleSubmissionList { + std::vector OpaqueGraphics; + std::vector TranslucentGraphics; + std::vector Compute; + + void Clear() { + OpaqueGraphics.clear(); + TranslucentGraphics.clear(); + Compute.clear(); + } + + [[nodiscard]] bool Empty() const { + return OpaqueGraphics.empty() && TranslucentGraphics.empty() && Compute.empty(); + } +}; + class RenderScene { public: void Reset(); + uint64_t FrameNumber{0}; + float CpuFrameMs{0.0f}; const Camera *ActiveCamera{nullptr}; glm::vec4 BackgroundColor{1.0f, 0.0f, 0.0f, 1.0f}; std::vector Submissions; diff --git a/Axiom/Renderer/RenderSurface.h b/Axiom/Renderer/RenderSurface.h index 7f825f07..5178aec7 100644 --- a/Axiom/Renderer/RenderSurface.h +++ b/Axiom/Renderer/RenderSurface.h @@ -1,63 +1,3 @@ #pragma once -#include "Core/Window.h" - -#include -#include - -namespace Axiom { -enum class RenderSurfaceKind { Window, Offscreen }; - -class IRenderSurface { -public: - virtual ~IRenderSurface() = default; - - [[nodiscard]] virtual RenderSurfaceKind GetKind() const = 0; - [[nodiscard]] virtual bool SupportsPresentation() const = 0; - [[nodiscard]] virtual uint32_t GetWidth() const = 0; - [[nodiscard]] virtual uint32_t GetHeight() const = 0; - [[nodiscard]] virtual void *GetNativeWindowHandle() const = 0; -}; - -class WindowRenderSurface final : public IRenderSurface { -public: - explicit WindowRenderSurface(Window &TargetWindow) : m_TargetWindow(TargetWindow) {} - - [[nodiscard]] RenderSurfaceKind GetKind() const override { - return RenderSurfaceKind::Window; - } - [[nodiscard]] bool SupportsPresentation() const override { return true; } - [[nodiscard]] uint32_t GetWidth() const override { - return m_TargetWindow.GetWidth(); - } - [[nodiscard]] uint32_t GetHeight() const override { - return m_TargetWindow.GetHeight(); - } - [[nodiscard]] void *GetNativeWindowHandle() const override { - return m_TargetWindow.GetNativeHandle(); - } - -private: - Window &m_TargetWindow; -}; - -class OffscreenRenderSurface final : public IRenderSurface { -public: - OffscreenRenderSurface(uint32_t Width, uint32_t Height) - : m_Width(Width), m_Height(Height) {} - - [[nodiscard]] RenderSurfaceKind GetKind() const override { - return RenderSurfaceKind::Offscreen; - } - [[nodiscard]] bool SupportsPresentation() const override { return false; } - [[nodiscard]] uint32_t GetWidth() const override { return m_Width; } - [[nodiscard]] uint32_t GetHeight() const override { return m_Height; } - [[nodiscard]] void *GetNativeWindowHandle() const override { return nullptr; } - -private: - uint32_t m_Width{0}; - uint32_t m_Height{0}; -}; - -using RenderSurfacePtr = std::shared_ptr; -} // namespace Axiom +#include "Core/RenderRuntime.h" diff --git a/Axiom/Renderer/RenderTechnique.h b/Axiom/Renderer/RenderTechnique.h deleted file mode 100644 index d6e4f790..00000000 --- a/Axiom/Renderer/RenderTechnique.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -namespace Axiom { -class RendererBackend; -class RenderScene; - -class RenderTechnique { -public: - virtual ~RenderTechnique() = default; - - virtual void Init(RendererBackend *Backend) = 0; - virtual void Shutdown() = 0; - virtual void Render(RenderScene &Scene) = 0; -}; -} // namespace Axiom diff --git a/Axiom/Renderer/Renderer.cpp b/Axiom/Renderer/Renderer.cpp index a38b43cd..ec726e28 100644 --- a/Axiom/Renderer/Renderer.cpp +++ b/Axiom/Renderer/Renderer.cpp @@ -1,36 +1,49 @@ #include "Renderer/Renderer.h" #include "Assets/MeshAsset.h" -#include "Renderer/ForwardRenderer.h" +#include "Core/Threading.h" #include "Renderer/RenderCommand.h" -#include "Renderer/Vulkan/VulkanRendererBackend.h" +#include "RHI/RHIFactory.h" #include "Core/Log.h" +#include #include +#include +#include +#include namespace Axiom { -Renderer *Renderer::s_Instance = nullptr; +namespace { +constexpr size_t kInvalidSceneIndex = static_cast(-1); +} Renderer::~Renderer() { Shutdown(); } -Renderer &Renderer::Get() { return *s_Instance; } - void Renderer::Init(const RendererCreateInfo &CreateInfo) { if (m_IsInitialized) { return; } - switch (CreateInfo.BackendType) { - case RendererBackendType::Vulkan: - m_Backend = std::make_unique(); - break; + m_CreateInfo = CreateInfo; + m_AttachmentRequirements = CreateInfo.AttachmentRequirements; + m_EnableThreadedRendering = CreateInfo.EnableThreadedRendering; + m_PendingCpuFrameMs = 0.0f; + m_NextFrameNumber = 0; + m_LastKnownFrameStats = {}; + m_FreeSceneIndices.clear(); + m_QueuedSceneIndices.clear(); + m_RecordingSceneIndex = kInvalidSceneIndex; + m_RenderThreadFailure.reset(); + m_RenderThreadReady = false; + m_RenderThreadExitRequested = false; + + if (IsThreadedRenderEnabled()) { + StartThreadedRenderer(CreateInfo); + } else { + InitializeBackendOnCurrentThread(CreateInfo); } - m_Backend->Init(CreateInfo); - m_Technique = std::make_unique(); - m_Technique->Init(m_Backend.get()); - s_Instance = this; m_IsInitialized = true; } @@ -39,25 +52,59 @@ void Renderer::Shutdown() { return; } - m_Scene.Reset(); - m_Technique->Shutdown(); - m_Technique.reset(); - m_Backend->Shutdown(); - m_Backend.reset(); - if (s_Instance == this) { - s_Instance = nullptr; + if (m_RecordingSceneIndex != kInvalidSceneIndex) { + RenderCommand::EndScene(); + m_Scenes[m_RecordingSceneIndex].Reset(); + m_RecordingSceneIndex = kInvalidSceneIndex; + } + + if (IsThreadedRenderEnabled()) { + StopThreadedRenderer(); + } else { + for (RenderScene &Scene : m_Scenes) { + Scene.Reset(); + } + ShutdownBackendOnCurrentThread(); } + + m_CreateInfo.reset(); + m_AttachmentRequirements = {}; + m_EnableThreadedRendering = false; + m_RenderThreadFailure.reset(); + m_LastKnownFrameStats = {}; m_IsInitialized = false; } void Renderer::BeginFrame() { - m_Backend->BeginFrame(); - RenderCommand::BeginScene(m_Scene); + if (IsThreadedRenderEnabled()) { + RenderScene &Scene = AcquireRecordingScene(); + Scene.FrameNumber = ++m_NextFrameNumber; + Scene.CpuFrameMs = m_PendingCpuFrameMs; + RenderCommand::BeginScene(Scene); + return; + } + + RenderScene &Scene = m_Scenes[0]; + Scene.Reset(); + Scene.FrameNumber = ++m_NextFrameNumber; + Scene.CpuFrameMs = m_PendingCpuFrameMs; + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->BeginFrame(); + } + RenderCommand::BeginScene(Scene); } void Renderer::Render() { + if (IsThreadedRenderEnabled()) { + return; + } + + if (m_SceneRenderer == nullptr) { + return; + } + const auto StartTime = std::chrono::steady_clock::now(); - m_Technique->Render(m_Scene); + m_SceneRenderer->Render(m_Scenes[0]); const auto EndTime = std::chrono::steady_clock::now(); UpdateCpuRenderTime( std::chrono::duration(EndTime - StartTime).count()); @@ -65,49 +112,191 @@ void Renderer::Render() { void Renderer::EndFrame() { RenderCommand::EndScene(); - m_Backend->RenderImGui(); - m_Backend->EndFrame(); + + if (IsThreadedRenderEnabled()) { + const size_t SceneIndex = m_RecordingSceneIndex; + m_RecordingSceneIndex = kInvalidSceneIndex; + { + std::scoped_lock Lock(m_RenderThreadMutex); + m_QueuedSceneIndices.push_back(SceneIndex); + } + m_RenderThreadCv.notify_all(); + return; + } + + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->RenderImGui(); + m_SceneRenderer->EndFrame(); + m_LastKnownFrameStats = m_SceneRenderer->GetFrameStats(); + } } void Renderer::SetViewMode(RendererViewMode ViewMode) { - if (m_Backend != nullptr) { - m_Backend->SetViewMode(ViewMode); + if (IsThreadedRenderEnabled()) { + InvokeOnRenderThread([this, ViewMode]() { + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->SetViewMode(ViewMode); + } + }); + return; + } + + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->SetViewMode(ViewMode); } } void Renderer::SetViewportFrameUser(SessionUserId User) { - if (m_Backend != nullptr) { - m_Backend->SetViewportFrameUser(User); + if (IsThreadedRenderEnabled()) { + InvokeOnRenderThread([this, User]() { + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->SetViewportFrameUser(User); + } + }); + return; + } + + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->SetViewportFrameUser(User); } } void Renderer::SetViewportFrameOutput(IViewportFrameOutput *FrameOutput) { - if (m_Backend != nullptr) { - m_Backend->SetViewportFrameOutput(FrameOutput); + if (IsThreadedRenderEnabled()) { + InvokeOnRenderThread([this, FrameOutput]() { + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->SetViewportFrameOutput(FrameOutput); + } + }); + return; + } + + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->SetViewportFrameOutput(FrameOutput); } } std::optional Renderer::ConsumeCapturedFrame() { - return m_Backend->ConsumeCapturedFrame(); + if (IsThreadedRenderEnabled()) { + return InvokeOnRenderThread>([this]() { + return m_SceneRenderer != nullptr ? m_SceneRenderer->ConsumeCapturedFrame() + : std::nullopt; + }); + } + + return m_SceneRenderer != nullptr ? m_SceneRenderer->ConsumeCapturedFrame() + : std::nullopt; } void Renderer::SetCpuFrameTime(float CpuFrameMs) { - m_Backend->AccessFrameStats().CpuFrameMs = CpuFrameMs; + m_PendingCpuFrameMs = CpuFrameMs; + m_LastKnownFrameStats.CpuFrameMs = CpuFrameMs; + if (!IsThreadedRenderEnabled() && m_SceneRenderer != nullptr) { + m_SceneRenderer->AccessFrameStats().CpuFrameMs = CpuFrameMs; + } +} + +RendererFrameStats Renderer::GetFrameStats() const { + if (IsThreadedRenderEnabled()) { + return const_cast(this)->InvokeOnRenderThread( + [this]() { + return m_SceneRenderer != nullptr ? m_SceneRenderer->GetFrameStats() + : RendererFrameStats{}; + }); + } + + return m_SceneRenderer != nullptr ? m_SceneRenderer->GetFrameStats() + : m_LastKnownFrameStats; +} + +void Renderer::WaitForIdle() { + if (IsThreadedRenderEnabled()) { + { + std::unique_lock Lock(m_RenderThreadMutex); + m_RenderThreadCv.wait(Lock, [this]() { + return m_QueuedSceneIndices.empty() && + m_FreeSceneIndices.size() == m_Scenes.size() && + m_RecordingSceneIndex == kInvalidSceneIndex; + }); + } + InvokeOnRenderThread([this]() { + if (m_RhiDevice != nullptr) { + m_RhiDevice->WaitIdle(); + } + }); + return; + } + + if (m_RhiDevice != nullptr) { + m_RhiDevice->WaitIdle(); + } +} + +std::shared_ptr Renderer::CreateMesh(const MeshData &MeshData, + const MeshCreateOptions &Options) { + if (IsThreadedRenderEnabled()) { + return InvokeOnRenderThread>([this, MeshData, Options]() { + return m_SceneRenderer != nullptr ? m_SceneRenderer->CreateMesh(MeshData, Options) + : nullptr; + }); + } + + return m_SceneRenderer != nullptr ? m_SceneRenderer->CreateMesh(MeshData, Options) + : nullptr; +} + +MaterialHandle Renderer::CreateMaterialHandle(const MaterialInstance &Material) { + if (IsThreadedRenderEnabled()) { + return InvokeOnRenderThread([this, Material]() { + return m_SceneRenderer != nullptr + ? m_SceneRenderer->CreateMaterialHandle(Material) + : MaterialHandle{}; + }); + } + + return m_SceneRenderer != nullptr + ? m_SceneRenderer->CreateMaterialHandle(Material) + : MaterialHandle{}; } -const RendererFrameStats &Renderer::GetFrameStats() const { - return m_Backend->GetFrameStats(); +void Renderer::UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material) { + if (m_SceneRenderer == nullptr || !Handle.IsValid()) { + return; + } + + if (IsThreadedRenderEnabled()) { + InvokeOnRenderThread([this, Handle, Material]() { + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->UpdateMaterialHandle(Handle, Material); + } + }); + return; + } + + m_SceneRenderer->UpdateMaterialHandle(Handle, Material); } -std::shared_ptr Renderer::CreateMesh(const MeshData &MeshData) { - return m_Backend != nullptr ? m_Backend->CreateMesh(MeshData) : nullptr; +RenderMeshResource Renderer::CreateMeshResource(const MeshData &MeshData, + const MeshCreateOptions &Options) { + RenderMeshResource Resource; + Resource.Mesh = CreateMesh(MeshData, Options); + Resource.Handle = GetMeshHandle(Resource.Mesh); + if (Resource.Mesh != nullptr) { + assert(Resource.Handle.IsValid() && + "Renderer backend returned a mesh without a valid handle"); + } + return Resource; } void Renderer::UpdateCpuRenderTime(float CpuRenderMs) { - m_Backend->AccessFrameStats().CpuRenderMs = CpuRenderMs; + m_LastKnownFrameStats.CpuRenderMs = CpuRenderMs; + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->AccessFrameStats().CpuRenderMs = CpuRenderMs; + } } -std::vector +LoadedMeshScene Renderer::LoadMeshSceneFromFile(const std::filesystem::path &Path, const MeshSceneLoadOptions &Options) { auto SceneData = Assets::LoadBasicMeshAsset(Path); @@ -116,11 +305,12 @@ Renderer::LoadMeshSceneFromFile(const std::filesystem::path &Path, return {}; } - std::vector Result; - Result.reserve(SceneData->Instances.size()); + LoadedMeshScene Result; + Result.Resources.reserve(SceneData->Instances.size()); + Result.Submissions.reserve(SceneData->Instances.size()); for (const auto &Instance : SceneData->Instances) { - auto Mesh = m_Backend->CreateMesh(Instance.Mesh); - if (!Mesh) { + RenderMeshResource Resource = CreateMeshResource(Instance.Mesh); + if (!Resource.IsValid()) { continue; } @@ -128,14 +318,244 @@ Renderer::LoadMeshSceneFromFile(const std::filesystem::path &Path, Options.ComputeMeshNames.contains(Instance.Name) ? MeshRenderPath::Compute : Options.DefaultRenderPath; - Result.push_back( - {.Mesh = Mesh, - .Material = Instance.Material, - .Name = Instance.Name, + Result.Resources.push_back(Resource); + Result.Submissions.push_back( + {.MeshHandle = Result.Resources.back().Handle, + .MaterialHandle = + Instance.Material != nullptr + ? CreateMaterialHandle(*Instance.Material) + : MaterialHandle{}, + .DebugDataId = RegisterRenderMeshSubmissionDebugData( + {.Name = Instance.Name}), .RenderPath = RenderPath, .Transform = Instance.Transform}); } return Result; } + +template +Result Renderer::InvokeOnRenderThread(std::function Function) { + if (!IsThreadedRenderEnabled()) { + return Function(); + } + + auto Promise = std::make_shared>(); + std::future Future = Promise->get_future(); + { + std::scoped_lock Lock(m_RenderThreadMutex); + m_RenderThreadCommands.push_back( + [Promise, Function = std::move(Function)]() mutable { + try { + if constexpr (std::is_void_v) { + Function(); + Promise->set_value(); + } else { + Promise->set_value(Function()); + } + } catch (...) { + Promise->set_exception(std::current_exception()); + } + }); + } + m_RenderThreadCv.notify_all(); + + if constexpr (std::is_void_v) { + Future.get(); + } else { + return Future.get(); + } +} + +void Renderer::InvokeOnRenderThread(std::function Function) { + InvokeOnRenderThread(std::move(Function)); +} + +void Renderer::StartThreadedRenderer(const RendererCreateInfo &CreateInfo) { + for (size_t SceneIndex = 0; SceneIndex < m_Scenes.size(); ++SceneIndex) { + m_Scenes[SceneIndex].Reset(); + m_FreeSceneIndices.push_back(SceneIndex); + } + + m_RenderThread = std::thread([this]() { RenderThreadMain(); }); + + std::unique_lock Lock(m_RenderThreadMutex); + m_RenderThreadCv.wait(Lock, [this]() { + return m_RenderThreadReady || m_RenderThreadFailure.has_value(); + }); + if (m_RenderThreadFailure.has_value()) { + throw std::runtime_error(*m_RenderThreadFailure); + } + + (void)CreateInfo; +} + +void Renderer::StopThreadedRenderer() { + { + std::unique_lock Lock(m_RenderThreadMutex); + m_RenderThreadCv.wait(Lock, [this]() { + return m_QueuedSceneIndices.empty() && + m_FreeSceneIndices.size() == m_Scenes.size() && + m_RecordingSceneIndex == kInvalidSceneIndex && + m_RenderThreadCommands.empty(); + }); + m_RenderThreadExitRequested = true; + } + m_RenderThreadCv.notify_all(); + + if (m_RenderThread.joinable()) { + m_RenderThread.join(); + } + + std::scoped_lock Lock(m_RenderThreadMutex); + m_FreeSceneIndices.clear(); + m_QueuedSceneIndices.clear(); + m_RenderThreadCommands.clear(); + m_RenderThreadReady = false; + m_RenderThreadExitRequested = false; +} + +void Renderer::RenderThreadMain() { + try { + Threading::SetCurrentThreadName("Axiom Render Thread"); + + if (!m_CreateInfo.has_value()) { + throw std::runtime_error("Renderer create info missing for render thread"); + } + + InitializeBackendOnCurrentThread(*m_CreateInfo); + + { + std::scoped_lock Lock(m_RenderThreadMutex); + m_RenderThreadReady = true; + } + m_RenderThreadCv.notify_all(); + + while (true) { + std::function Command; + size_t SceneIndex = kInvalidSceneIndex; + { + std::unique_lock Lock(m_RenderThreadMutex); + m_RenderThreadCv.wait(Lock, [this]() { + return m_RenderThreadExitRequested || !m_RenderThreadCommands.empty() || + !m_QueuedSceneIndices.empty(); + }); + + if (!m_QueuedSceneIndices.empty()) { + SceneIndex = m_QueuedSceneIndices.front(); + m_QueuedSceneIndices.pop_front(); + } else if (!m_RenderThreadCommands.empty()) { + Command = std::move(m_RenderThreadCommands.front()); + m_RenderThreadCommands.pop_front(); + } else if (m_RenderThreadExitRequested) { + break; + } + } + + if (Command) { + Command(); + m_RenderThreadCv.notify_all(); + continue; + } + + if (SceneIndex != kInvalidSceneIndex) { + RenderSceneOnCurrentThread(m_Scenes[SceneIndex]); + ReleaseRenderedScene(SceneIndex); + } + } + + ShutdownBackendOnCurrentThread(); + } catch (const std::exception &Error) { + std::scoped_lock Lock(m_RenderThreadMutex); + m_RenderThreadFailure = Error.what(); + m_RenderThreadReady = true; + m_RenderThreadCv.notify_all(); + } +} + +void Renderer::InitializeBackendOnCurrentThread( + const RendererCreateInfo &CreateInfo) { + m_RhiDevice = CreateRHIDevice(CreateInfo.BackendType); + assert(m_RhiDevice != nullptr && "RHI device factory returned null"); + if (m_RhiDevice == nullptr) { + return; + } + + m_RhiDevice->Init({.TargetSurface = CreateInfo.TargetSurface, + .FrameOutput = CreateInfo.FrameOutput, + .Width = CreateInfo.Width, + .Height = CreateInfo.Height, + .AttachmentRequirements = m_AttachmentRequirements}); + m_SceneRenderer = + std::make_unique(*m_RhiDevice, CreateInfo.BackendType); + m_SceneRenderer->Init(CreateInfo); +} + +void Renderer::ShutdownBackendOnCurrentThread() { + for (RenderScene &Scene : m_Scenes) { + Scene.Reset(); + } + + if (m_RhiDevice != nullptr) { + m_RhiDevice->WaitIdle(); + } + if (m_SceneRenderer != nullptr) { + m_SceneRenderer->Shutdown(); + m_SceneRenderer.reset(); + } + if (m_RhiDevice != nullptr) { + m_RhiDevice->Shutdown(); + m_RhiDevice.reset(); + } +} + +void Renderer::RenderSceneOnCurrentThread(RenderScene &Scene) { + if (m_SceneRenderer == nullptr) { + return; + } + + if (m_CreateInfo.has_value() && m_CreateInfo->ThreadedRenderSceneStartCallback) { + m_CreateInfo->ThreadedRenderSceneStartCallback(Scene.FrameNumber); + } + + const auto StartTime = std::chrono::steady_clock::now(); + m_SceneRenderer->BeginFrame(); + m_SceneRenderer->AccessFrameStats().CpuFrameMs = Scene.CpuFrameMs; + m_SceneRenderer->Render(Scene); + // Threaded rendering currently skips renderer-owned ImGui composition so the + // game thread can stay authoritative for UI/input state. + m_SceneRenderer->EndFrame(); + const auto EndTime = std::chrono::steady_clock::now(); + m_SceneRenderer->AccessFrameStats().CpuRenderMs = + std::chrono::duration(EndTime - StartTime).count(); + m_LastKnownFrameStats = m_SceneRenderer->GetFrameStats(); + + if (m_CreateInfo.has_value() && + m_CreateInfo->ThreadedRenderSceneCompleteCallback) { + m_CreateInfo->ThreadedRenderSceneCompleteCallback(Scene.FrameNumber); + } +} + +RenderScene &Renderer::AcquireRecordingScene() { + std::unique_lock Lock(m_RenderThreadMutex); + m_RenderThreadCv.wait(Lock, [this]() { return !m_FreeSceneIndices.empty(); }); + m_RecordingSceneIndex = m_FreeSceneIndices.front(); + m_FreeSceneIndices.pop_front(); + RenderScene &Scene = m_Scenes[m_RecordingSceneIndex]; + Scene.Reset(); + return Scene; +} + +void Renderer::ReleaseRenderedScene(size_t SceneIndex) { + { + std::scoped_lock Lock(m_RenderThreadMutex); + m_Scenes[SceneIndex].Reset(); + m_FreeSceneIndices.push_back(SceneIndex); + } + m_RenderThreadCv.notify_all(); +} + +bool Renderer::IsThreadedRenderEnabled() const { + return AXIOM_THREADED_RENDER != 0 && m_EnableThreadedRendering; +} } // namespace Axiom diff --git a/Axiom/Renderer/Renderer.h b/Axiom/Renderer/Renderer.h index 8cea6441..77302b9c 100644 --- a/Axiom/Renderer/Renderer.h +++ b/Axiom/Renderer/Renderer.h @@ -1,13 +1,20 @@ #pragma once -#include "Renderer/RendererBackend.h" #include "Renderer/RenderScene.h" -#include "Renderer/RenderSurface.h" -#include "Renderer/RenderTechnique.h" +#include "Renderer/RendererTypes.h" +#include "Renderer/SceneRenderer.h" +#include "RHI/IRHI.h" +#include +#include +#include #include +#include +#include #include #include +#include +#include namespace Axiom { class Renderer { @@ -15,8 +22,6 @@ class Renderer { Renderer() = default; ~Renderer(); - static Renderer &Get(); - Renderer(const Renderer &) = delete; Renderer &operator=(const Renderer &) = delete; @@ -30,22 +35,56 @@ class Renderer { void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput); std::optional ConsumeCapturedFrame(); void SetCpuFrameTime(float CpuFrameMs); - const RendererFrameStats &GetFrameStats() const; - std::shared_ptr CreateMesh(const MeshData &MeshData); - std::vector + RendererFrameStats GetFrameStats() const; + void WaitForIdle(); + std::shared_ptr + CreateMesh(const MeshData &MeshData, const MeshCreateOptions &Options = {}); + MaterialHandle CreateMaterialHandle(const MaterialInstance &Material); + void UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material); + RenderMeshResource + CreateMeshResource(const MeshData &MeshData, + const MeshCreateOptions &Options = {}); + LoadedMeshScene LoadMeshSceneFromFile( const std::filesystem::path &Path, const MeshSceneLoadOptions &Options = {}); private: + template + Result InvokeOnRenderThread(std::function Function); + void InvokeOnRenderThread(std::function Function); + void StartThreadedRenderer(const RendererCreateInfo &CreateInfo); + void StopThreadedRenderer(); + void RenderThreadMain(); + void InitializeBackendOnCurrentThread(const RendererCreateInfo &CreateInfo); + void ShutdownBackendOnCurrentThread(); + void RenderSceneOnCurrentThread(RenderScene &Scene); + RenderScene &AcquireRecordingScene(); + void ReleaseRenderedScene(size_t SceneIndex); void UpdateCpuRenderTime(float CpuRenderMs); + bool IsThreadedRenderEnabled() const; private: - static Renderer *s_Instance; - - std::unique_ptr m_Backend; - std::unique_ptr m_Technique; - RenderScene m_Scene; + std::unique_ptr m_RhiDevice; + std::unique_ptr m_SceneRenderer; + RendererAttachmentRequirements m_AttachmentRequirements{}; + std::optional m_CreateInfo; + std::array m_Scenes; + std::deque m_FreeSceneIndices; + std::deque m_QueuedSceneIndices; + std::deque> m_RenderThreadCommands; + mutable std::mutex m_RenderThreadMutex; + std::condition_variable m_RenderThreadCv; + std::thread m_RenderThread; + size_t m_RecordingSceneIndex{static_cast(-1)}; + bool m_EnableThreadedRendering{false}; + bool m_RenderThreadReady{false}; + bool m_RenderThreadExitRequested{false}; + std::optional m_RenderThreadFailure; + float m_PendingCpuFrameMs{0.0f}; + uint64_t m_NextFrameNumber{0}; + RendererFrameStats m_LastKnownFrameStats{}; bool m_IsInitialized{false}; }; } // namespace Axiom diff --git a/Axiom/Renderer/RendererBackend.h b/Axiom/Renderer/RendererBackend.h deleted file mode 100644 index 7c4c72a5..00000000 --- a/Axiom/Renderer/RendererBackend.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "Renderer/Mesh.h" -#include "Renderer/RenderSurface.h" -#include "Renderer/ViewportFrameOutput.h" - -#include - -#include -#include -#include -#include -#include -#include - -namespace Axiom { -class RenderScene; -class IRenderSurface; -struct MeshData; - -enum class RendererBackendType { Vulkan }; -enum class RendererViewMode : uint32_t { - Lit = 0, - Unlit = 1, - Wireframe = 2, -}; - -struct RendererCreateInfo { - Window *TargetWindow{nullptr}; - RenderSurfacePtr TargetSurface; - IViewportFrameOutput *FrameOutput{nullptr}; - uint32_t Width{0}; - uint32_t Height{0}; - RendererBackendType BackendType{RendererBackendType::Vulkan}; -}; - -struct CapturedFrame { - uint64_t FrameIndex{0}; - uint32_t Width{0}; - uint32_t Height{0}; - std::vector Pixels; -}; - -struct RenderFrameInfo { - uint64_t FrameIndex{0}; -}; - -struct RendererFrameStats { - float CpuFrameMs{0.0f}; - float CpuRenderMs{0.0f}; - float GpuBackgroundMs{0.0f}; - float GpuMeshMs{0.0f}; - uint32_t SubmittedMeshCount{0}; - uint32_t FrustumCulledMeshCount{0}; - uint32_t OcclusionCulledMeshCount{0}; - uint32_t MeshSubmissionCount{0}; - uint32_t TriangleCount{0}; - glm::uvec2 DrawExtent{0u, 0u}; -}; - -class RendererBackend { -public: - virtual ~RendererBackend() = default; - - virtual void Init(const RendererCreateInfo &CreateInfo) = 0; - virtual void Shutdown() = 0; - virtual void BeginFrame() = 0; - virtual std::shared_ptr CreateMesh(const MeshData &Mesh) = 0; - virtual void RenderSceneMeshes(RenderScene &Scene) = 0; - virtual void RenderFallbackBackground(RenderScene &Scene) = 0; - virtual RendererFrameStats &AccessFrameStats() = 0; - virtual const RendererFrameStats &GetFrameStats() const = 0; - virtual void RenderImGui() = 0; - virtual void EndFrame() = 0; - virtual void SetViewMode(RendererViewMode ViewMode) = 0; - virtual void SetViewportFrameUser(SessionUserId User) = 0; - virtual void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput) = 0; - virtual std::optional ConsumeCapturedFrame() = 0; -}; -} // namespace Axiom diff --git a/Axiom/Renderer/RendererFrameModule.cpp b/Axiom/Renderer/RendererFrameModule.cpp new file mode 100644 index 00000000..34c581ba --- /dev/null +++ b/Axiom/Renderer/RendererFrameModule.cpp @@ -0,0 +1,37 @@ +#include "Renderer/RendererFrameModule.h" + +#include "Core/Application.h" +#include "Renderer/Renderer.h" + +namespace Axiom { +std::string_view RendererFrameModule::GetName() const { + return "Core.RendererFrame"; +} + +bool RendererFrameModule::Initialize(Application &App) { + (void)App; + return true; +} + +void RendererFrameModule::Update(const ModuleUpdateContext &Context) { + switch (Context.Phase) { + case ModuleUpdatePhase::FrameStart: + Context.App.GetRenderer().SetCpuFrameTime(Context.DeltaTimeSeconds * + 1000.0f); + break; + case ModuleUpdatePhase::RenderBegin: + Context.App.GetRenderer().BeginFrame(); + break; + case ModuleUpdatePhase::Render: + Context.App.GetRenderer().Render(); + break; + case ModuleUpdatePhase::RenderEnd: + Context.App.GetRenderer().EndFrame(); + break; + case ModuleUpdatePhase::ImGuiRender: + break; + } +} + +void RendererFrameModule::Shutdown(Application &App) { (void)App; } +} // namespace Axiom diff --git a/Axiom/Renderer/RendererFrameModule.h b/Axiom/Renderer/RendererFrameModule.h new file mode 100644 index 00000000..f60f97b8 --- /dev/null +++ b/Axiom/Renderer/RendererFrameModule.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Core/IModule.h" + +namespace Axiom { +class RendererFrameModule final : public IModule { +public: + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; +}; +} // namespace Axiom diff --git a/Axiom/Renderer/RendererTypes.h b/Axiom/Renderer/RendererTypes.h new file mode 100644 index 00000000..26aa1744 --- /dev/null +++ b/Axiom/Renderer/RendererTypes.h @@ -0,0 +1,77 @@ +#pragma once + +#include "Core/RenderRuntime.h" + +#include + +#include +#include +#include +#include +#include + +namespace Axiom { +#ifndef AXIOM_THREADED_RENDER +#define AXIOM_THREADED_RENDER 0 +#endif + +enum class RendererBackendType : uint32_t { + Vulkan = 0, +}; + +enum class RendererTechniqueType : uint32_t { + Forward = 0, +}; + +struct RendererAttachmentRequirements { + bool NeedsGBuffer{false}; + uint32_t GBufferColorTargetCount{0}; + + constexpr bool operator==(const RendererAttachmentRequirements &) const = + default; +}; + +struct RendererCreateInfo { + RenderSurfacePtr TargetSurface; + IViewportFrameOutput *FrameOutput{nullptr}; + uint32_t Width{0}; + uint32_t Height{0}; + bool EnableThreadedRendering{AXIOM_THREADED_RENDER != 0}; + std::function ThreadedRenderSceneStartCallback; + std::function ThreadedRenderSceneCompleteCallback; + RendererBackendType BackendType{RendererBackendType::Vulkan}; + RendererTechniqueType Technique{RendererTechniqueType::Forward}; + RendererAttachmentRequirements AttachmentRequirements{}; +}; + +struct CapturedFrame { + uint64_t FrameIndex{0}; + uint32_t Width{0}; + uint32_t Height{0}; + std::vector Pixels; +}; + +struct RenderFrameInfo { + uint64_t FrameIndex{0}; +}; + +struct RendererFrameStats { + float CpuFrameMs{0.0f}; + float CpuRenderMs{0.0f}; + float GpuBackgroundMs{0.0f}; + float GpuMeshMs{0.0f}; + uint32_t SubmittedMeshCount{0}; + uint32_t FrustumCulledMeshCount{0}; + uint32_t OcclusionCulledMeshCount{0}; + uint32_t MeshSubmissionCount{0}; + uint32_t TriangleCount{0}; + glm::uvec2 DrawExtent{0u, 0u}; +#if !defined(NDEBUG) + uint32_t DebugGraphicsMaterialDescriptorUpdates{0}; + uint32_t DebugOpaqueMaterialDescriptorBinds{0}; + uint32_t DebugTranslucentMaterialDescriptorBinds{0}; + uint32_t DebugOpaqueUniqueMaterialCount{0}; + uint32_t DebugTranslucentUniqueMaterialCount{0}; +#endif +}; +} // namespace Axiom diff --git a/Axiom/Renderer/SceneRenderer.cpp b/Axiom/Renderer/SceneRenderer.cpp new file mode 100644 index 00000000..65117cff --- /dev/null +++ b/Axiom/Renderer/SceneRenderer.cpp @@ -0,0 +1,105 @@ +#include "Renderer/SceneRenderer.h" + +#include "AxiomRHI/SceneRendererBackendFactory.h" + +namespace Axiom { +SceneRenderer::SceneRenderer(IRHIDevice &Device, RendererBackendType BackendType) + : m_Device(Device), m_BackendType(BackendType) {} + +SceneRenderer::~SceneRenderer() { Shutdown(); } + +void SceneRenderer::Init(const RendererCreateInfo &CreateInfo) { + if (m_Backend != nullptr) { + return; + } + + m_Backend = CreateSceneRendererBackend(m_Device, m_BackendType); + if (m_Backend != nullptr) { + m_Backend->Init(m_Device, CreateInfo); + } +} + +void SceneRenderer::Shutdown() { + if (m_Backend == nullptr) { + return; + } + + m_Backend->Shutdown(); + m_Backend.reset(); +} + +void SceneRenderer::BeginFrame() { + if (m_Backend != nullptr) { + m_Backend->BeginFrame(); + } +} + +std::shared_ptr +SceneRenderer::CreateMesh(const MeshData &MeshData, + const MeshCreateOptions &Options) { + return m_Backend != nullptr ? m_Backend->CreateMesh(MeshData, Options) + : nullptr; +} + +MaterialHandle +SceneRenderer::CreateMaterialHandle(const MaterialInstance &Material) { + return m_Backend != nullptr ? m_Backend->CreateMaterialHandle(Material) + : MaterialHandle{}; +} + +void SceneRenderer::UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material) { + if (m_Backend != nullptr) { + m_Backend->UpdateMaterialHandle(Handle, Material); + } +} + +void SceneRenderer::Render(RenderScene &Scene) { + if (m_Backend != nullptr) { + m_Backend->Render(Scene); + } +} + +void SceneRenderer::RenderImGui() { + if (m_Backend != nullptr) { + m_Backend->RenderImGui(); + } +} + +void SceneRenderer::EndFrame() { + if (m_Backend != nullptr) { + m_Backend->EndFrame(); + } +} + +void SceneRenderer::SetViewMode(RendererViewMode ViewMode) { + if (m_Backend != nullptr) { + m_Backend->SetViewMode(ViewMode); + } +} + +void SceneRenderer::SetViewportFrameUser(SessionUserId User) { + if (m_Backend != nullptr) { + m_Backend->SetViewportFrameUser(User); + } +} + +void SceneRenderer::SetViewportFrameOutput(IViewportFrameOutput *FrameOutput) { + if (m_Backend != nullptr) { + m_Backend->SetViewportFrameOutput(FrameOutput); + } +} + +std::optional SceneRenderer::ConsumeCapturedFrame() { + return m_Backend != nullptr ? m_Backend->ConsumeCapturedFrame() + : std::nullopt; +} + +RendererFrameStats &SceneRenderer::AccessFrameStats() { + return m_Backend->AccessFrameStats(); +} + +const RendererFrameStats &SceneRenderer::GetFrameStats() const { + return m_Backend->GetFrameStats(); +} +} // namespace Axiom diff --git a/Axiom/Renderer/SceneRenderer.h b/Axiom/Renderer/SceneRenderer.h new file mode 100644 index 00000000..7cb4e099 --- /dev/null +++ b/Axiom/Renderer/SceneRenderer.h @@ -0,0 +1,46 @@ +#pragma once + +#include "Renderer/RendererTypes.h" +#include "RHI/IRHI.h" +#include "Renderer/Material.h" +#include "Renderer/Mesh.h" +#include "Renderer/RenderScene.h" + +#include +#include + +namespace Axiom { +class ISceneRendererBackend; + +class SceneRenderer { +public: + SceneRenderer(IRHIDevice &Device, RendererBackendType BackendType); + ~SceneRenderer(); + + SceneRenderer(const SceneRenderer &) = delete; + SceneRenderer &operator=(const SceneRenderer &) = delete; + + void Init(const RendererCreateInfo &CreateInfo); + void Shutdown(); + void BeginFrame(); + std::shared_ptr + CreateMesh(const MeshData &MeshData, const MeshCreateOptions &Options = {}); + MaterialHandle CreateMaterialHandle(const MaterialInstance &Material); + void UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material); + void Render(RenderScene &Scene); + void RenderImGui(); + void EndFrame(); + void SetViewMode(RendererViewMode ViewMode); + void SetViewportFrameUser(SessionUserId User); + void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput); + std::optional ConsumeCapturedFrame(); + RendererFrameStats &AccessFrameStats(); + const RendererFrameStats &GetFrameStats() const; + +private: + IRHIDevice &m_Device; + RendererBackendType m_BackendType; + std::unique_ptr m_Backend; +}; +} // namespace Axiom diff --git a/Axiom/Renderer/VideoEncoderFactory.cpp b/Axiom/Renderer/VideoEncoderFactory.cpp index 59491cc9..fc774f66 100644 --- a/Axiom/Renderer/VideoEncoderFactory.cpp +++ b/Axiom/Renderer/VideoEncoderFactory.cpp @@ -1,17 +1,9 @@ #include "Renderer/VideoEncoderFactory.h" -#include "Core/Platform.h" +#include "HAL/PlatformMedia.h" namespace Axiom { -#if AXIOM_PLATFORM_MACOS -std::unique_ptr CreateMacOSVideoToolboxH264Encoder(); -#endif - std::unique_ptr CreateDefaultVideoEncoder() { -#if AXIOM_PLATFORM_MACOS - return CreateMacOSVideoToolboxH264Encoder(); -#else - return nullptr; -#endif + return HAL::CreatePlatformVideoEncoder(); } } // namespace Axiom diff --git a/Axiom/Renderer/ViewportFrameOutput.h b/Axiom/Renderer/ViewportFrameOutput.h index e0c3bee4..5178aec7 100644 --- a/Axiom/Renderer/ViewportFrameOutput.h +++ b/Axiom/Renderer/ViewportFrameOutput.h @@ -1,29 +1,3 @@ #pragma once -#include "Session/SessionTypes.h" - -#include -#include -#include - -namespace Axiom { -enum class ViewportFrameFormat : uint8_t { - R16G16B16A16Float, - R8G8B8A8Unorm, -}; - -struct ViewportFrame { - uint64_t FrameIndex{0}; - uint32_t Width{0}; - uint32_t Height{0}; - ViewportFrameFormat Format{ViewportFrameFormat::R16G16B16A16Float}; - std::span Pixels; - SessionUserId User{}; -}; - -class IViewportFrameOutput { -public: - virtual ~IViewportFrameOutput() = default; - virtual void OnViewportFrame(const ViewportFrame &Frame) = 0; -}; -} // namespace Axiom +#include "Core/RenderRuntime.h" diff --git a/Axiom/Renderer/Vulkan/VulkanDescriptors.cpp b/Axiom/Renderer/Vulkan/VulkanDescriptors.cpp deleted file mode 100644 index 6e187e23..00000000 --- a/Axiom/Renderer/Vulkan/VulkanDescriptors.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "Renderer/Vulkan/VulkanDescriptors.h" -#include - -void DescriptorLayoutBuilder::AddBinding(uint32_t Binding, - VkDescriptorType Type) { - VkDescriptorSetLayoutBinding NewBind{}; - NewBind.binding = Binding; - NewBind.descriptorType = Type; - NewBind.descriptorCount = 1; - - Bindings.push_back(NewBind); -} - -void DescriptorLayoutBuilder::Clear() { Bindings.clear(); } - -VkDescriptorSetLayout -DescriptorLayoutBuilder::Build(VkDevice Device, VkShaderStageFlags StageFlags, - void *pNext, - VkDescriptorSetLayoutCreateFlags Flags) { - for (auto &Binding : Bindings) { - Binding.stageFlags |= StageFlags; - } - - VkDescriptorSetLayoutCreateInfo Info = { - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - Info.pNext = pNext; - - Info.pBindings = Bindings.data(); - Info.bindingCount = static_cast(Bindings.size()); - - Info.flags = Flags; - - VkDescriptorSetLayout Set; - VK_CHECK(vkCreateDescriptorSetLayout(Device, &Info, VK_NULL_HANDLE, &Set)); - - return Set; -} - -void DescriptorAllocator::InitPool(VkDevice Device, uint32_t MaxSets, - std::span PoolRatios) { - std::vector PoolSizes; - for (PoolSizeRatio Ratio : PoolRatios) { - PoolSizes.push_back(VkDescriptorPoolSize{ - .type = Ratio.Type, - .descriptorCount = static_cast(Ratio.Ratio * MaxSets)}); - } - - VkDescriptorPoolCreateInfo PoolInfo = { - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; - PoolInfo.maxSets = MaxSets; - - PoolInfo.pPoolSizes = PoolSizes.data(); - PoolInfo.poolSizeCount = static_cast(PoolSizes.size()); - - PoolInfo.flags = 0; - - vkCreateDescriptorPool(Device, &PoolInfo, VK_NULL_HANDLE, &Pool); -} - -void DescriptorAllocator::ClearDescriptors(VkDevice Device) { - vkResetDescriptorPool(Device, Pool, 0); -} - -void DescriptorAllocator::DestroyPool(VkDevice Device) { - vkDestroyDescriptorPool(Device, Pool, VK_NULL_HANDLE); -} - -VkDescriptorSet DescriptorAllocator::Allocate(VkDevice Device, - VkDescriptorSetLayout Layout) { - VkDescriptorSetAllocateInfo AllocInfo = { - .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - AllocInfo.pNext = VK_NULL_HANDLE; - AllocInfo.descriptorPool = Pool; - AllocInfo.descriptorSetCount = 1; - AllocInfo.pSetLayouts = &Layout; - - VkDescriptorSet DescriptorSet; - VK_CHECK(vkAllocateDescriptorSets(Device, &AllocInfo, &DescriptorSet)); - - return DescriptorSet; -} - diff --git a/Axiom/Renderer/Vulkan/VulkanMaterialResources.cpp b/Axiom/Renderer/Vulkan/VulkanMaterialResources.cpp deleted file mode 100644 index fe61291d..00000000 --- a/Axiom/Renderer/Vulkan/VulkanMaterialResources.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "Renderer/Vulkan/VulkanMaterialResources.h" - -#include "Renderer/Vulkan/VulkanBuffer.h" -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanImage.h" -#include "Renderer/Vulkan/VulkanInitializers.h" - -#include -#include -#include - -namespace Axiom { -void VulkanMaterialResources::Init(const CreateInfo &CreateInfo) { - m_Allocator = CreateInfo.Allocator; - m_Device = CreateInfo.Device; - m_GraphicsQueue = CreateInfo.GraphicsQueue; - m_DeletionQueue = CreateInfo.DeletionQueue; - m_ImmediateSubmit = CreateInfo.ImmediateSubmit; -} - -void VulkanMaterialResources::Shutdown() { - m_MaterialImageViews.clear(); - m_FallbackTexture = {}; -} - -void VulkanMaterialResources::InitFallbackTexture() { - TextureSourceData CheckerTexture{}; - constexpr uint32_t TextureSize = 64; - constexpr uint32_t CellSize = 8; - constexpr std::array Purple = {0xA0, 0x20, 0xF0, 0xFF}; - constexpr std::array Black = {0x00, 0x00, 0x00, 0xFF}; - - CheckerTexture.Width = TextureSize; - CheckerTexture.Height = TextureSize; - CheckerTexture.Pixels.resize(TextureSize * TextureSize * 4); - - for (uint32_t Y = 0; Y < TextureSize; ++Y) { - for (uint32_t X = 0; X < TextureSize; ++X) { - const bool UsePurple = ((X / CellSize) + (Y / CellSize)) % 2 == 0; - const auto &Color = UsePurple ? Purple : Black; - const size_t PixelIndex = - (static_cast(Y) * TextureSize + X) * 4; - CheckerTexture.Pixels[PixelIndex + 0] = Color[0]; - CheckerTexture.Pixels[PixelIndex + 1] = Color[1]; - CheckerTexture.Pixels[PixelIndex + 2] = Color[2]; - CheckerTexture.Pixels[PixelIndex + 3] = Color[3]; - } - } - - m_FallbackTexture = CreateTextureImage(CheckerTexture); -} - -AllocatedImage -VulkanMaterialResources::CreateTextureImage(const TextureSourceData &TextureData) { - assert(TextureData.IsValid()); - - AllocatedImage TextureImage{}; - TextureImage.ImageFormat = VK_FORMAT_R8G8B8A8_UNORM; - TextureImage.ImageExtent = {TextureData.Width, TextureData.Height, 1}; - - VkImageCreateInfo ImageInfo = VkInit::ImageCreateInfo( - TextureImage.ImageFormat, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - TextureImage.ImageExtent); - - VmaAllocationCreateInfo AllocationInfo{}; - AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; - AllocationInfo.requiredFlags = - VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - - VK_CHECK(vmaCreateImage(m_Allocator, &ImageInfo, &AllocationInfo, - &TextureImage.Image, &TextureImage.Allocation, - VK_NULL_HANDLE)); - - VkImageViewCreateInfo ViewInfo = VkInit::ImageViewCreateInfo( - TextureImage.ImageFormat, TextureImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); - VK_CHECK(vkCreateImageView(m_Device, &ViewInfo, VK_NULL_HANDLE, - &TextureImage.ImageView)); - - auto StagingBuffer = VkBufferUtil::CreateBuffer( - m_Allocator, TextureData.Pixels.size(), VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - VMA_MEMORY_USAGE_CPU_ONLY, - VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | - VMA_ALLOCATION_CREATE_MAPPED_BIT); - std::memcpy(StagingBuffer.Info.pMappedData, TextureData.Pixels.data(), - TextureData.Pixels.size()); - - m_ImmediateSubmit([&](VkCommandBuffer CommandBuffer) { - VkUtil::TransitionImage(CommandBuffer, TextureImage.Image, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - - VkBufferImageCopy CopyRegion{}; - CopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - CopyRegion.imageSubresource.layerCount = 1; - CopyRegion.imageExtent = TextureImage.ImageExtent; - - vkCmdCopyBufferToImage(CommandBuffer, StagingBuffer.Buffer, TextureImage.Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &CopyRegion); - - VkUtil::TransitionImage(CommandBuffer, TextureImage.Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - }); - - VkBufferUtil::DestroyBuffer(m_Allocator, StagingBuffer); - - const AllocatedImage UploadedImage = TextureImage; - m_DeletionQueue->PushFunction([this, UploadedImage]() mutable { - vkDestroyImageView(m_Device, UploadedImage.ImageView, VK_NULL_HANDLE); - vmaDestroyImage(m_Allocator, UploadedImage.Image, UploadedImage.Allocation); - }); - - return TextureImage; -} - -VkImageView -VulkanMaterialResources::ResolveMaterialTextureView(const MaterialInstanceRef &Material) { - if (!Material || !Material->BaseColorTexture || - !Material->BaseColorTexture->IsValid()) { - return m_FallbackTexture.ImageView; - } - - auto It = m_MaterialImageViews.find(Material.get()); - if (It != m_MaterialImageViews.end()) { - return It->second; - } - - const AllocatedImage TextureImage = - CreateTextureImage(*Material->BaseColorTexture); - m_MaterialImageViews.emplace(Material.get(), TextureImage.ImageView); - return TextureImage.ImageView; -} -} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanMaterialResources.h b/Axiom/Renderer/Vulkan/VulkanMaterialResources.h deleted file mode 100644 index b61d689e..00000000 --- a/Axiom/Renderer/Vulkan/VulkanMaterialResources.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#include "Renderer/Material.h" -#include "Renderer/Vulkan/VulkanTypes.h" - -#include -#include - -namespace Axiom { -struct DeletionQueue; - -class VulkanMaterialResources { -public: - struct CreateInfo { - VmaAllocator Allocator{nullptr}; - VkDevice Device{VK_NULL_HANDLE}; - VkQueue GraphicsQueue{VK_NULL_HANDLE}; - DeletionQueue *DeletionQueue{nullptr}; - std::function &&)> - ImmediateSubmit; - }; - - void Init(const CreateInfo &CreateInfo); - void Shutdown(); - - void InitFallbackTexture(); - VkImageView ResolveMaterialTextureView(const MaterialInstanceRef &Material); - VkImageView GetFallbackTextureView() const { return m_FallbackTexture.ImageView; } - -private: - AllocatedImage CreateTextureImage(const TextureSourceData &TextureData); - - VmaAllocator m_Allocator{nullptr}; - VkDevice m_Device{VK_NULL_HANDLE}; - VkQueue m_GraphicsQueue{VK_NULL_HANDLE}; - DeletionQueue *m_DeletionQueue{nullptr}; - std::function &&)> - m_ImmediateSubmit; - AllocatedImage m_FallbackTexture; - std::unordered_map m_MaterialImageViews; -}; -} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp b/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp deleted file mode 100644 index 62c90c1d..00000000 --- a/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp +++ /dev/null @@ -1,1788 +0,0 @@ -#include "Renderer/Vulkan/VulkanRendererBackend.h" - -#include "Assets/SvgTexture.h" -#include "Renderer/Camera.h" -#include "Renderer/RenderScene.h" -#include "Renderer/Vulkan/VulkanBuffer.h" -#include "Renderer/Vulkan/VulkanDescriptors.h" -#include "Renderer/Vulkan/VulkanImage.h" -#include "Renderer/Vulkan/VulkanInitializers.h" -#include "Renderer/Vulkan/VulkanMesh.h" -#include "Renderer/Vulkan/VulkanPipeline.h" - -#include -#include - -#include -#include - -#define GLFW_INCLUDE_VULKAN -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "Core/Log.h" - -Axiom::VulkanRendererBackend *g_LoadedEngine = nullptr; - -#ifndef AXIOM_CONTENT_DIR -#define AXIOM_CONTENT_DIR "Content" -#endif - -namespace Axiom { -namespace { -void TransitionImageRange(VkCommandBuffer CommandBuffer, VkImage Image, - VkImageLayout OldLayout, VkImageLayout NewLayout, - VkImageAspectFlags AspectMask, uint32_t BaseMipLevel, - uint32_t LevelCount) { - VkImageMemoryBarrier2 ImageBarrier{ - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - .srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - .dstAccessMask = - VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT, - .oldLayout = OldLayout, - .newLayout = NewLayout, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = Image, - .subresourceRange = - {.aspectMask = AspectMask, - .baseMipLevel = BaseMipLevel, - .levelCount = LevelCount, - .baseArrayLayer = 0, - .layerCount = 1}}; - VkDependencyInfo DependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .imageMemoryBarrierCount = 1, - .pImageMemoryBarriers = &ImageBarrier}; - vkCmdPipelineBarrier2(CommandBuffer, &DependencyInfo); -} -} // namespace - -VulkanRendererBackend &VulkanRendererBackend::Get() { return *g_LoadedEngine; } - -VulkanRendererBackend *VulkanRendererBackend::TryGet() { return g_LoadedEngine; } - -void VulkanRendererBackend::Init(const RendererCreateInfo &CreateInfo) { - assert(CreateInfo.TargetSurface != nullptr); - assert(g_LoadedEngine == nullptr); - g_LoadedEngine = this; - - m_Surface = CreateInfo.TargetSurface; - m_FrameOutput = CreateInfo.FrameOutput; - m_HasPresentationSurface = m_Surface->SupportsPresentation(); - m_EnableImGui = m_HasPresentationSurface; - m_Window = m_HasPresentationSurface - ? static_cast(m_Surface->GetNativeWindowHandle()) - : nullptr; - m_WindowExtent = {CreateInfo.Width, CreateInfo.Height}; - - m_Context.Init(m_Surface->GetNativeWindowHandle(), m_HasPresentationSurface); - m_Device.Init(m_Context); - - VkPhysicalDeviceProperties DeviceProperties{}; - vkGetPhysicalDeviceProperties(m_Device.PhysicalDevice, &DeviceProperties); - m_TimestampPeriod = DeviceProperties.limits.timestampPeriod; - - m_CommandContext.Init(m_Device.Device, m_Device.GraphicsQueueFamily); - m_MaterialResources.Init({.Allocator = m_Device.Allocator, - .Device = m_Device.Device, - .GraphicsQueue = m_Device.GraphicsQueue, - .DeletionQueue = &m_MainDeletionQueue, - .ImmediateSubmit = - [this](std::function && - Function) { - ImmediateSubmit(std::move(Function)); - }}); - m_OcclusionCulling.Init(m_Device.Device, m_Device.Allocator); - - InitSwapchain(); - InitHzbResources(); - InitViewportReadbackBuffers(); - InitDescriptors(); - InitTextureResources(); - InitPipelines(); - InitMeshFrameResources(); - if (m_EnableImGui) { - m_ImGuiRenderer.Init({.WindowHandle = m_Window, - .Instance = m_Context.Instance, - .PhysicalDevice = m_Device.PhysicalDevice, - .Device = m_Device.Device, - .Queue = m_Device.GraphicsQueue, - .QueueFamily = m_Device.GraphicsQueueFamily, - .SwapchainImageFormat = m_Swapchain.ImageFormat, - .DeletionQueue = &m_MainDeletionQueue}); - } - - m_IsInitialized = true; - - A_CORE_INFO("Vulkan Engine set up was successful: {0}", - m_IsInitialized ? "True" : "False"); -} - -void VulkanRendererBackend::InitSwapchain() { - if (m_HasPresentationSurface) { - m_Swapchain.Init(m_Context, m_Device, m_WindowExtent.width, - m_WindowExtent.height); - } else { - m_Swapchain.ImageFormat = VK_FORMAT_B8G8R8A8_UNORM; - m_Swapchain.Extent = m_WindowExtent; - } - - VkExtent3D DrawImageExtent = {m_WindowExtent.width, m_WindowExtent.height, 1}; - - m_DrawImage.ImageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; - m_DrawImage.ImageExtent = DrawImageExtent; - - VkImageUsageFlags DrawImageUsages{}; - DrawImageUsages |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - DrawImageUsages |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; - DrawImageUsages |= VK_IMAGE_USAGE_STORAGE_BIT; - DrawImageUsages |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - - VkImageCreateInfo DrawInfo = VkInit::ImageCreateInfo( - m_DrawImage.ImageFormat, DrawImageUsages, m_DrawImage.ImageExtent); - - VmaAllocationCreateInfo DrawAllocInfo{}; - DrawAllocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; - DrawAllocInfo.requiredFlags = - VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - - VK_CHECK(vmaCreateImage(m_Device.Allocator, &DrawInfo, &DrawAllocInfo, - &m_DrawImage.Image, &m_DrawImage.Allocation, - VK_NULL_HANDLE)); - - VkImageViewCreateInfo DrawViewInfo = VkInit::ImageViewCreateInfo( - m_DrawImage.ImageFormat, m_DrawImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); - VK_CHECK(vkCreateImageView(m_Device.Device, &DrawViewInfo, VK_NULL_HANDLE, - &m_DrawImage.ImageView)); - - m_DepthImage.ImageFormat = VK_FORMAT_R32_UINT; - m_DepthImage.ImageExtent = DrawImageExtent; - - VkImageUsageFlags DepthUsages{}; - DepthUsages |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; - DepthUsages |= VK_IMAGE_USAGE_STORAGE_BIT; - - VkImageCreateInfo DepthInfo = VkInit::ImageCreateInfo( - m_DepthImage.ImageFormat, DepthUsages, m_DepthImage.ImageExtent); - VK_CHECK(vmaCreateImage(m_Device.Allocator, &DepthInfo, &DrawAllocInfo, - &m_DepthImage.Image, &m_DepthImage.Allocation, - VK_NULL_HANDLE)); - - VkImageViewCreateInfo DepthViewInfo = VkInit::ImageViewCreateInfo( - m_DepthImage.ImageFormat, m_DepthImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); - VK_CHECK(vkCreateImageView(m_Device.Device, &DepthViewInfo, VK_NULL_HANDLE, - &m_DepthImage.ImageView)); - - m_RasterDepthImage.ImageFormat = VK_FORMAT_D32_SFLOAT; - m_RasterDepthImage.ImageExtent = DrawImageExtent; - - VkImageUsageFlags RasterDepthUsages{}; - RasterDepthUsages |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; - RasterDepthUsages |= VK_IMAGE_USAGE_SAMPLED_BIT; - - VkImageCreateInfo RasterDepthInfo = VkInit::ImageCreateInfo( - m_RasterDepthImage.ImageFormat, RasterDepthUsages, - m_RasterDepthImage.ImageExtent); - VK_CHECK(vmaCreateImage(m_Device.Allocator, &RasterDepthInfo, &DrawAllocInfo, - &m_RasterDepthImage.Image, - &m_RasterDepthImage.Allocation, VK_NULL_HANDLE)); - - VkImageViewCreateInfo RasterDepthViewInfo = VkInit::ImageViewCreateInfo( - m_RasterDepthImage.ImageFormat, m_RasterDepthImage.Image, - VK_IMAGE_ASPECT_DEPTH_BIT); - VK_CHECK(vkCreateImageView(m_Device.Device, &RasterDepthViewInfo, - VK_NULL_HANDLE, &m_RasterDepthImage.ImageView)); - - m_MainDeletionQueue.PushFunction([this]() { - for (auto &CaptureFrame : m_OffscreenCaptureFrames) { - VkBufferUtil::DestroyBuffer(m_Device.Allocator, CaptureFrame.ReadbackBuffer); - CaptureFrame = {}; - } - vkDestroyImageView(m_Device.Device, m_RasterDepthImage.ImageView, - VK_NULL_HANDLE); - vmaDestroyImage(m_Device.Allocator, m_RasterDepthImage.Image, - m_RasterDepthImage.Allocation); - vkDestroyImageView(m_Device.Device, m_DepthImage.ImageView, VK_NULL_HANDLE); - vmaDestroyImage(m_Device.Allocator, m_DepthImage.Image, - m_DepthImage.Allocation); - vkDestroyImageView(m_Device.Device, m_DrawImage.ImageView, VK_NULL_HANDLE); - vmaDestroyImage(m_Device.Allocator, m_DrawImage.Image, - m_DrawImage.Allocation); - }); -} - -void VulkanRendererBackend::InitViewportReadbackBuffers() { - const size_t BufferSize = - static_cast(m_DrawImage.ImageExtent.width) * - static_cast(m_DrawImage.ImageExtent.height) * sizeof(uint16_t) * - 4u; - for (auto &CaptureFrame : m_OffscreenCaptureFrames) { - CaptureFrame.ReadbackBuffer = VkBufferUtil::CreateBuffer( - m_Device.Allocator, BufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT, - VMA_MEMORY_USAGE_GPU_TO_CPU, - VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | - VMA_ALLOCATION_CREATE_MAPPED_BIT); - CaptureFrame.HasPendingReadback = false; - CaptureFrame.SubmittedFrameNumber = 0; - CaptureFrame.SubmittedUser = {}; - } -} - -void VulkanRendererBackend::InitHzbResources() { - const VkExtent2D BaseExtent = {m_DrawImage.ImageExtent.width, - m_DrawImage.ImageExtent.height}; - const uint32_t MipCount = ComputeHzbMipCount(BaseExtent); - - m_HzbMipImageViews.clear(); - m_HzbMipExtents.clear(); - m_HzbMipOffsets.clear(); - m_HzbReadbackBufferSize = 0; - m_HzbImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; - - m_HzbImage.ImageFormat = VK_FORMAT_R32_SFLOAT; - m_HzbImage.ImageExtent = {BaseExtent.width, BaseExtent.height, 1}; - - VkImageCreateInfo HzbInfo = - VkInit::ImageCreateInfo(m_HzbImage.ImageFormat, - VK_IMAGE_USAGE_STORAGE_BIT | - VK_IMAGE_USAGE_SAMPLED_BIT | - VK_IMAGE_USAGE_TRANSFER_SRC_BIT, - m_HzbImage.ImageExtent); - HzbInfo.mipLevels = MipCount; - - VmaAllocationCreateInfo AllocationInfo{}; - AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; - AllocationInfo.requiredFlags = - VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VK_CHECK(vmaCreateImage(m_Device.Allocator, &HzbInfo, &AllocationInfo, - &m_HzbImage.Image, &m_HzbImage.Allocation, - VK_NULL_HANDLE)); - - m_HzbMipImageViews.reserve(MipCount); - m_HzbMipExtents.reserve(MipCount); - m_HzbMipOffsets.reserve(MipCount); - - for (uint32_t MipLevel = 0; MipLevel < MipCount; ++MipLevel) { - const VkExtent2D MipExtent = ComputeHzbMipExtent(BaseExtent, MipLevel); - m_HzbMipExtents.push_back(MipExtent); - m_HzbMipOffsets.push_back(m_HzbReadbackBufferSize); - m_HzbReadbackBufferSize += - static_cast(MipExtent.width) * - static_cast(MipExtent.height) * sizeof(float); - - VkImageViewCreateInfo ViewInfo = VkInit::ImageViewCreateInfo( - m_HzbImage.ImageFormat, m_HzbImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); - ViewInfo.subresourceRange.baseMipLevel = MipLevel; - ViewInfo.subresourceRange.levelCount = 1; - - VkImageView MipView = VK_NULL_HANDLE; - VK_CHECK(vkCreateImageView(m_Device.Device, &ViewInfo, VK_NULL_HANDLE, - &MipView)); - m_HzbMipImageViews.push_back(MipView); - } - - m_MainDeletionQueue.PushFunction([this]() { - for (VkImageView MipView : m_HzbMipImageViews) { - if (MipView != VK_NULL_HANDLE) { - vkDestroyImageView(m_Device.Device, MipView, VK_NULL_HANDLE); - } - } - m_HzbMipImageViews.clear(); - vmaDestroyImage(m_Device.Allocator, m_HzbImage.Image, m_HzbImage.Allocation); - m_HzbImage = {}; - }); -} - -void VulkanRendererBackend::Shutdown() { - A_CORE_INFO("Running Vulkan renderer cleanup..."); - - if (m_IsInitialized) { - vkDeviceWaitIdle(m_Device.Device); - m_MaterialResources.Shutdown(); - - for (auto &Frame : m_MeshFrames) { - VkBufferUtil::DestroyBuffer(m_Device.Allocator, Frame.CameraBuffer); - VkBufferUtil::DestroyBuffer(m_Device.Allocator, Frame.HzbReadbackBuffer); - } - - m_CommandContext.Shutdown(m_Device.Device); - m_MainDeletionQueue.Flush(); - m_Swapchain.Shutdown(m_Device); - m_Device.Shutdown(); - m_Context.Shutdown(); - - m_IsInitialized = false; - } - - g_LoadedEngine = nullptr; -} - -void VulkanRendererBackend::InitDescriptors() { - const uint32_t MaxSets = 4 + - (FRAME_OVERLAP * (MaxMeshSubmissionsPerFrame + 2)) + - (MaxMeshSubmissionsPerFrame * 2) + - static_cast(m_HzbMipImageViews.size()); - std::vector Sizes = { - {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 4.0f}, - {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6.0f}, - {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2.0f}, - {VK_DESCRIPTOR_TYPE_SAMPLER, 2.0f}, - {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4.0f}, - {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2.0f}}; - - m_GlobalDescriptorAllocator.InitPool(m_Device.Device, MaxSets, Sizes); - - { - DescriptorLayoutBuilder Builder; - Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); - m_DrawImageDescriptorLayout = - Builder.Build(m_Device.Device, VK_SHADER_STAGE_COMPUTE_BIT); - } - - { - DescriptorLayoutBuilder Builder; - Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); - Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); - m_HzbReduceDescriptorLayout = - Builder.Build(m_Device.Device, VK_SHADER_STAGE_COMPUTE_BIT); - } - - { - DescriptorLayoutBuilder Builder; - Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); - Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE); - Builder.AddBinding(2, VK_DESCRIPTOR_TYPE_SAMPLER); - m_MeshGraphicsFrameDescriptorLayout = - Builder.Build(m_Device.Device, - VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); - } - - { - DescriptorLayoutBuilder Builder; - Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); - Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); - Builder.AddBinding(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); - m_MeshComputeFrameDescriptorLayout = - Builder.Build(m_Device.Device, VK_SHADER_STAGE_COMPUTE_BIT); - } - - { - DescriptorLayoutBuilder Builder; - Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); - Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); - Builder.AddBinding(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); - m_MeshDescriptorLayout = - Builder.Build(m_Device.Device, VK_SHADER_STAGE_COMPUTE_BIT); - } - - m_DrawImageDescriptorSet = m_GlobalDescriptorAllocator.Allocate( - m_Device.Device, m_DrawImageDescriptorLayout); - - VkDescriptorImageInfo ImageInfo{}; - ImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - ImageInfo.imageView = m_DrawImage.ImageView; - - VkWriteDescriptorSet DrawImageWrite = VkInit::WriteDescriptorSet( - VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, m_DrawImageDescriptorSet, &ImageInfo, 0); - - vkUpdateDescriptorSets(m_Device.Device, 1, &DrawImageWrite, 0, - VK_NULL_HANDLE); - - VkSamplerCreateInfo SamplerInfo{ - .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .magFilter = VK_FILTER_NEAREST, - .minFilter = VK_FILTER_NEAREST, - .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, - .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .mipLodBias = 0.0f, - .anisotropyEnable = VK_FALSE, - .compareEnable = VK_FALSE, - .minLod = 0.0f, - .maxLod = 0.0f, - .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE, - .unnormalizedCoordinates = VK_FALSE}; - VK_CHECK(vkCreateSampler(m_Device.Device, &SamplerInfo, VK_NULL_HANDLE, - &m_LinearDepthSampler)); - - SamplerInfo.magFilter = VK_FILTER_LINEAR; - SamplerInfo.minFilter = VK_FILTER_LINEAR; - SamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; - SamplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; - SamplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; - SamplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; - VK_CHECK(vkCreateSampler(m_Device.Device, &SamplerInfo, VK_NULL_HANDLE, - &m_TextureSampler)); - - m_HzbReduceDescriptorSets.clear(); - m_HzbReduceDescriptorSets.reserve(m_HzbMipImageViews.size()); - for (size_t MipLevel = 0; MipLevel < m_HzbMipImageViews.size(); ++MipLevel) { - VkDescriptorSet DescriptorSet = m_GlobalDescriptorAllocator.Allocate( - m_Device.Device, m_HzbReduceDescriptorLayout); - m_HzbReduceDescriptorSets.push_back(DescriptorSet); - - VkDescriptorImageInfo DestinationImageInfo{}; - DestinationImageInfo.imageView = m_HzbMipImageViews[MipLevel]; - DestinationImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - - VkDescriptorImageInfo SourceImageInfo{}; - SourceImageInfo.sampler = m_LinearDepthSampler; - SourceImageInfo.imageView = - (MipLevel == 0) ? m_RasterDepthImage.ImageView - : m_HzbMipImageViews[MipLevel - 1]; - SourceImageInfo.imageLayout = - (MipLevel == 0) ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL - : VK_IMAGE_LAYOUT_GENERAL; - - std::array Writes = { - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - DescriptorSet, &DestinationImageInfo, 0), - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - DescriptorSet, &SourceImageInfo, 1)}; - vkUpdateDescriptorSets(m_Device.Device, static_cast(Writes.size()), - Writes.data(), 0, VK_NULL_HANDLE); - } - - m_MainDeletionQueue.PushFunction([this]() { - vkDestroySampler(m_Device.Device, m_TextureSampler, VK_NULL_HANDLE); - vkDestroySampler(m_Device.Device, m_LinearDepthSampler, VK_NULL_HANDLE); - m_GlobalDescriptorAllocator.DestroyPool(m_Device.Device); - vkDestroyDescriptorSetLayout(m_Device.Device, m_MeshDescriptorLayout, - VK_NULL_HANDLE); - vkDestroyDescriptorSetLayout(m_Device.Device, - m_MeshComputeFrameDescriptorLayout, - VK_NULL_HANDLE); - vkDestroyDescriptorSetLayout(m_Device.Device, - m_MeshGraphicsFrameDescriptorLayout, - VK_NULL_HANDLE); - vkDestroyDescriptorSetLayout(m_Device.Device, m_HzbReduceDescriptorLayout, - VK_NULL_HANDLE); - vkDestroyDescriptorSetLayout(m_Device.Device, m_DrawImageDescriptorLayout, - VK_NULL_HANDLE); - }); -} - -void VulkanRendererBackend::InitTextureResources() { - m_MaterialResources.InitFallbackTexture(); - const std::filesystem::path LightIconPath = - std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine" / "lightbulb.svg"; - if (const auto IconTexture = Assets::LoadSvgTextureFromFile(LightIconPath)) { - m_LightBillboardMaterial = std::make_shared(); - m_LightBillboardMaterial->BaseColorTexture = IconTexture; - } else { - A_CORE_WARN("Failed to load light billboard icon from {0}; using fallback texture", - LightIconPath.string()); - m_LightBillboardMaterial = std::make_shared(); - } -} - -void VulkanRendererBackend::InitPipelines() { - InitBackgroundPipelines(); - InitMeshPipelines(); - InitGizmoPipeline(); - InitLightBillboardPipeline(); -} - -void VulkanRendererBackend::InitGizmoPipeline() { - m_GizmoRenderer.Init({.Device = m_Device.Device, - .DrawImageFormat = m_DrawImage.ImageFormat}, - m_MainDeletionQueue); -} - -void VulkanRendererBackend::InitLightBillboardPipeline() { - const VkImageView TextureView = - m_MaterialResources.ResolveMaterialTextureView(m_LightBillboardMaterial); - m_LightBillboardRenderer.Init( - {.Device = m_Device.Device, - .DrawImageFormat = m_DrawImage.ImageFormat, - .DescriptorAllocator = &m_GlobalDescriptorAllocator, - .TextureView = TextureView, - .TextureSampler = m_TextureSampler}, - m_MainDeletionQueue); -} - -void VulkanRendererBackend::InitBackgroundPipelines() { - VkPipelineLayoutCreateInfo ComputeLayout = - VkInit::PipelineLayoutCreateInfo(); - ComputeLayout.pSetLayouts = &m_DrawImageDescriptorLayout; - ComputeLayout.setLayoutCount = 1; - - VkPushConstantRange PushConstant{}; - PushConstant.offset = 0; - PushConstant.size = sizeof(ComputePushConstants); - PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - - ComputeLayout.pPushConstantRanges = &PushConstant; - ComputeLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &ComputeLayout, - VK_NULL_HANDLE, &m_GradientPipelineLayout)); - - VkShaderModule ComputeDrawShader; - const std::string ShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/gradient_color.comp.spv"; - if (!VkUtil::LoadShaderModule(ShaderPath.c_str(), m_Device.Device, - &ComputeDrawShader)) { - A_ERROR("Error when loading the compute shader: {0}", ShaderPath); - Axiom::Log::Flush(); - abort(); - } - - VkPipelineShaderStageCreateInfo StageInfo = - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, - ComputeDrawShader); - - VkComputePipelineCreateInfo ComputePipelineCreateInfo = { - .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .stage = StageInfo, - .layout = m_GradientPipelineLayout}; - - VK_CHECK(vkCreateComputePipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &ComputePipelineCreateInfo, - VK_NULL_HANDLE, &m_GradientPipeline)); - - vkDestroyShaderModule(m_Device.Device, ComputeDrawShader, VK_NULL_HANDLE); - - m_MainDeletionQueue.PushFunction([this]() { - vkDestroyPipelineLayout(m_Device.Device, m_GradientPipelineLayout, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_GradientPipeline, VK_NULL_HANDLE); - }); - - // --- HDR skybox pipeline ------------------------------------------------- - { - DescriptorLayoutBuilder Builder; - Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); - m_HDRSkyboxDescriptorLayout = - Builder.Build(m_Device.Device, VK_SHADER_STAGE_COMPUTE_BIT); - } - - VkSamplerCreateInfo HDRSamplerInfo{ - .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .magFilter = VK_FILTER_LINEAR, - .minFilter = VK_FILTER_LINEAR, - .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, - .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, - .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - .mipLodBias = 0.0f, - .anisotropyEnable = VK_FALSE, - .compareEnable = VK_FALSE, - .minLod = 0.0f, - .maxLod = 0.0f, - .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, - .unnormalizedCoordinates = VK_FALSE}; - VK_CHECK(vkCreateSampler(m_Device.Device, &HDRSamplerInfo, VK_NULL_HANDLE, - &m_HDRSkyboxSampler)); - - const std::array HDRSetLayouts = { - m_DrawImageDescriptorLayout, m_HDRSkyboxDescriptorLayout}; - - VkPipelineLayoutCreateInfo HDRLayout = VkInit::PipelineLayoutCreateInfo(); - HDRLayout.pSetLayouts = HDRSetLayouts.data(); - HDRLayout.setLayoutCount = static_cast(HDRSetLayouts.size()); - - VkPushConstantRange HDRPushConstant{}; - HDRPushConstant.offset = 0; - HDRPushConstant.size = sizeof(glm::mat4); - HDRPushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - - HDRLayout.pPushConstantRanges = &HDRPushConstant; - HDRLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &HDRLayout, VK_NULL_HANDLE, - &m_HDRSkyboxPipelineLayout)); - - VkShaderModule HDRShader; - const std::string HDRShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/skybox_hdr.comp.spv"; - if (!VkUtil::LoadShaderModule(HDRShaderPath.c_str(), m_Device.Device, - &HDRShader)) { - A_ERROR("Error when loading the HDR skybox compute shader: {0}", - HDRShaderPath); - Axiom::Log::Flush(); - abort(); - } - - VkPipelineShaderStageCreateInfo HDRStage = - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, - HDRShader); - - VkComputePipelineCreateInfo HDRPipelineInfo = { - .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .stage = HDRStage, - .layout = m_HDRSkyboxPipelineLayout}; - - VK_CHECK(vkCreateComputePipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &HDRPipelineInfo, VK_NULL_HANDLE, - &m_HDRSkyboxPipeline)); - - vkDestroyShaderModule(m_Device.Device, HDRShader, VK_NULL_HANDLE); - - m_MainDeletionQueue.PushFunction([this]() { - DestroyHDRSkyboxTexture(); - if (m_HDRSkyboxSampler != VK_NULL_HANDLE) { - vkDestroySampler(m_Device.Device, m_HDRSkyboxSampler, VK_NULL_HANDLE); - m_HDRSkyboxSampler = VK_NULL_HANDLE; - } - vkDestroyPipeline(m_Device.Device, m_HDRSkyboxPipeline, VK_NULL_HANDLE); - vkDestroyPipelineLayout(m_Device.Device, m_HDRSkyboxPipelineLayout, - VK_NULL_HANDLE); - vkDestroyDescriptorSetLayout(m_Device.Device, m_HDRSkyboxDescriptorLayout, - VK_NULL_HANDLE); - }); -} - -void VulkanRendererBackend::InitMeshPipelines() { - std::array ComputeLayouts = { - m_MeshComputeFrameDescriptorLayout, m_MeshDescriptorLayout}; - - { - VkPipelineLayoutCreateInfo ComputeLayout = - VkInit::PipelineLayoutCreateInfo(); - ComputeLayout.pSetLayouts = &m_HzbReduceDescriptorLayout; - ComputeLayout.setLayoutCount = 1; - - VkPushConstantRange PushConstant{}; - PushConstant.offset = 0; - PushConstant.size = sizeof(HzbReducePushConstants); - PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - ComputeLayout.pPushConstantRanges = &PushConstant; - ComputeLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &ComputeLayout, - VK_NULL_HANDLE, - &m_HzbReducePipelineLayout)); - } - - { - VkPipelineLayoutCreateInfo ComputeLayout = - VkInit::PipelineLayoutCreateInfo(); - ComputeLayout.pSetLayouts = ComputeLayouts.data(); - ComputeLayout.setLayoutCount = - static_cast(ComputeLayouts.size()); - - VkPushConstantRange PushConstant{}; - PushConstant.offset = 0; - PushConstant.size = sizeof(MeshProjectPushConstants); - PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - ComputeLayout.pPushConstantRanges = &PushConstant; - ComputeLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &ComputeLayout, - VK_NULL_HANDLE, - &m_MeshProjectPipelineLayout)); - } - - { - VkPipelineLayoutCreateInfo ComputeLayout = - VkInit::PipelineLayoutCreateInfo(); - ComputeLayout.pSetLayouts = ComputeLayouts.data(); - ComputeLayout.setLayoutCount = - static_cast(ComputeLayouts.size()); - - VkPushConstantRange PushConstant{}; - PushConstant.offset = 0; - PushConstant.size = sizeof(MeshRasterPushConstants); - PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; - ComputeLayout.pPushConstantRanges = &PushConstant; - ComputeLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &ComputeLayout, - VK_NULL_HANDLE, &m_MeshPipelineLayout)); - } - - VkPushConstantRange PushConstant{}; - PushConstant.offset = 0; - PushConstant.size = sizeof(MeshGraphicsPushConstants); - PushConstant.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; - - VkPipelineLayoutCreateInfo GraphicsLayout = VkInit::PipelineLayoutCreateInfo(); - GraphicsLayout.pSetLayouts = &m_MeshGraphicsFrameDescriptorLayout; - GraphicsLayout.setLayoutCount = 1; - GraphicsLayout.pPushConstantRanges = &PushConstant; - GraphicsLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &GraphicsLayout, - VK_NULL_HANDLE, - &m_MeshGraphicsPipelineLayout)); - - VkPipelineLayoutCreateInfo DepthLayout = VkInit::PipelineLayoutCreateInfo(); - DepthLayout.pSetLayouts = &m_MeshGraphicsFrameDescriptorLayout; - DepthLayout.setLayoutCount = 1; - DepthLayout.pPushConstantRanges = &PushConstant; - DepthLayout.pushConstantRangeCount = 1; - - VK_CHECK(vkCreatePipelineLayout(m_Device.Device, &DepthLayout, - VK_NULL_HANDLE, &m_MeshDepthPipelineLayout)); - - VkShaderModule HzbReduceShader; - const std::string HzbReduceShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/hzb_reduce.comp.spv"; - if (!VkUtil::LoadShaderModule(HzbReduceShaderPath.c_str(), m_Device.Device, - &HzbReduceShader)) { - A_ERROR("Error when loading the HZB reduction shader: {0}", - HzbReduceShaderPath); - Axiom::Log::Flush(); - abort(); - } - - VkPipelineShaderStageCreateInfo HzbReduceStageInfo = - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, - HzbReduceShader); - - VkComputePipelineCreateInfo HzbReducePipelineCreateInfo = { - .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .stage = HzbReduceStageInfo, - .layout = m_HzbReducePipelineLayout}; - - VK_CHECK(vkCreateComputePipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &HzbReducePipelineCreateInfo, - VK_NULL_HANDLE, &m_HzbReducePipeline)); - vkDestroyShaderModule(m_Device.Device, HzbReduceShader, VK_NULL_HANDLE); - - VkShaderModule MeshProjectShader; - const std::string MeshProjectShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh_project.comp.spv"; - if (!VkUtil::LoadShaderModule(MeshProjectShaderPath.c_str(), m_Device.Device, - &MeshProjectShader)) { - A_ERROR("Error when loading the mesh projection shader: {0}", - MeshProjectShaderPath); - Axiom::Log::Flush(); - abort(); - } - - VkPipelineShaderStageCreateInfo MeshProjectStageInfo = - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, - MeshProjectShader); - - VkComputePipelineCreateInfo MeshProjectPipelineCreateInfo = { - .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .stage = MeshProjectStageInfo, - .layout = m_MeshProjectPipelineLayout}; - - VK_CHECK(vkCreateComputePipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &MeshProjectPipelineCreateInfo, - VK_NULL_HANDLE, &m_MeshProjectPipeline)); - vkDestroyShaderModule(m_Device.Device, MeshProjectShader, VK_NULL_HANDLE); - - VkShaderModule MeshShader; - const std::string MeshShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh_raster.comp.spv"; - if (!VkUtil::LoadShaderModule(MeshShaderPath.c_str(), m_Device.Device, - &MeshShader)) { - A_ERROR("Error when loading the mesh compute shader: {0}", MeshShaderPath); - Axiom::Log::Flush(); - abort(); - } - - VkPipelineShaderStageCreateInfo MeshStageInfo = - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, - MeshShader); - - VkComputePipelineCreateInfo MeshPipelineCreateInfo = { - .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .stage = MeshStageInfo, - .layout = m_MeshPipelineLayout}; - - VK_CHECK(vkCreateComputePipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &MeshPipelineCreateInfo, VK_NULL_HANDLE, - &m_MeshPipeline)); - vkDestroyShaderModule(m_Device.Device, MeshShader, VK_NULL_HANDLE); - - VkShaderModule VertexShader; - const std::string VertexShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh.vert.spv"; - if (!VkUtil::LoadShaderModule(VertexShaderPath.c_str(), m_Device.Device, - &VertexShader)) { - A_ERROR("Error when loading the mesh vertex shader: {0}", VertexShaderPath); - Axiom::Log::Flush(); - abort(); - } - - VkShaderModule FragmentShader; - const std::string FragmentShaderPath = - std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh.frag.spv"; - if (!VkUtil::LoadShaderModule(FragmentShaderPath.c_str(), m_Device.Device, - &FragmentShader)) { - A_ERROR("Error when loading the mesh fragment shader: {0}", - FragmentShaderPath); - Axiom::Log::Flush(); - abort(); - } - - std::array ShaderStages = { - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT, - VertexShader), - VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT, - FragmentShader)}; - - VkVertexInputBindingDescription BindingDescription{ - .binding = 0, - .stride = sizeof(MeshVertex), - .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}; - std::array AttributeDescriptions = { - VkVertexInputAttributeDescription{0, 0, VK_FORMAT_R32G32B32A32_SFLOAT, - offsetof(MeshVertex, Position)}, - VkVertexInputAttributeDescription{1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, - offsetof(MeshVertex, Normal)}, - VkVertexInputAttributeDescription{2, 0, VK_FORMAT_R32G32_SFLOAT, - offsetof(MeshVertex, TexCoord)}}; - - VkPipelineVertexInputStateCreateInfo VertexInputInfo{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .vertexBindingDescriptionCount = 1, - .pVertexBindingDescriptions = &BindingDescription, - .vertexAttributeDescriptionCount = - static_cast(AttributeDescriptions.size()), - .pVertexAttributeDescriptions = AttributeDescriptions.data()}; - - VkPipelineInputAssemblyStateCreateInfo InputAssembly{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, - .primitiveRestartEnable = VK_FALSE}; - - VkPipelineViewportStateCreateInfo ViewportState{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .viewportCount = 1, - .scissorCount = 1}; - - VkPipelineRasterizationStateCreateInfo Rasterizer{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .depthClampEnable = VK_FALSE, - .rasterizerDiscardEnable = VK_FALSE, - .polygonMode = VK_POLYGON_MODE_FILL, - .cullMode = VK_CULL_MODE_NONE, - .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, - .depthBiasEnable = VK_FALSE, - .lineWidth = 1.0f}; - - VkPipelineMultisampleStateCreateInfo Multisampling{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, - .sampleShadingEnable = VK_FALSE}; - - VkPipelineDepthStencilStateCreateInfo DepthStencil{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .depthTestEnable = VK_TRUE, - .depthWriteEnable = VK_TRUE, - .depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL, - .depthBoundsTestEnable = VK_FALSE, - .stencilTestEnable = VK_FALSE}; - - VkPipelineColorBlendAttachmentState ColorBlendAttachment{}; - ColorBlendAttachment.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | - VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - ColorBlendAttachment.blendEnable = VK_FALSE; - - VkPipelineColorBlendStateCreateInfo ColorBlending{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .logicOpEnable = VK_FALSE, - .attachmentCount = 1, - .pAttachments = &ColorBlendAttachment}; - - std::array DynamicStates = {VK_DYNAMIC_STATE_VIEWPORT, - VK_DYNAMIC_STATE_SCISSOR}; - VkPipelineDynamicStateCreateInfo DynamicState{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .dynamicStateCount = static_cast(DynamicStates.size()), - .pDynamicStates = DynamicStates.data()}; - - VkPipelineRenderingCreateInfo RenderingInfo{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .colorAttachmentCount = 1, - .pColorAttachmentFormats = &m_DrawImage.ImageFormat, - .depthAttachmentFormat = m_RasterDepthImage.ImageFormat, - .stencilAttachmentFormat = VK_FORMAT_UNDEFINED}; - - VkGraphicsPipelineCreateInfo PipelineInfo{ - .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, - .pNext = &RenderingInfo, - .stageCount = static_cast(ShaderStages.size()), - .pStages = ShaderStages.data(), - .pVertexInputState = &VertexInputInfo, - .pInputAssemblyState = &InputAssembly, - .pViewportState = &ViewportState, - .pRasterizationState = &Rasterizer, - .pMultisampleState = &Multisampling, - .pDepthStencilState = &DepthStencil, - .pColorBlendState = &ColorBlending, - .pDynamicState = &DynamicState, - .layout = m_MeshGraphicsPipelineLayout, - .renderPass = VK_NULL_HANDLE, - .subpass = 0}; - - VK_CHECK(vkCreateGraphicsPipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &PipelineInfo, VK_NULL_HANDLE, - &m_MeshGraphicsPipeline)); - - VkPipelineColorBlendAttachmentState AlphaBlendAttachment = ColorBlendAttachment; - AlphaBlendAttachment.blendEnable = VK_TRUE; - AlphaBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; - AlphaBlendAttachment.dstColorBlendFactor = - VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - AlphaBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; - AlphaBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; - AlphaBlendAttachment.dstAlphaBlendFactor = - VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - AlphaBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; - VkPipelineColorBlendStateCreateInfo AlphaBlending = ColorBlending; - AlphaBlending.pAttachments = &AlphaBlendAttachment; - VkPipelineDepthStencilStateCreateInfo AlphaDepthStencil = DepthStencil; - AlphaDepthStencil.depthWriteEnable = VK_FALSE; - VkGraphicsPipelineCreateInfo AlphaPipelineInfo = PipelineInfo; - AlphaPipelineInfo.pColorBlendState = &AlphaBlending; - AlphaPipelineInfo.pDepthStencilState = &AlphaDepthStencil; - VK_CHECK(vkCreateGraphicsPipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &AlphaPipelineInfo, VK_NULL_HANDLE, - &m_MeshGraphicsAlphaBlendPipeline)); - - VkPipelineColorBlendAttachmentState DepthOnlyColorAttachment{}; - DepthOnlyColorAttachment.colorWriteMask = 0; - VkPipelineColorBlendStateCreateInfo DepthOnlyBlending = ColorBlending; - DepthOnlyBlending.pAttachments = &DepthOnlyColorAttachment; - - VkPipelineRenderingCreateInfo DepthRenderingInfo{ - .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .colorAttachmentCount = 0, - .pColorAttachmentFormats = VK_NULL_HANDLE, - .depthAttachmentFormat = m_RasterDepthImage.ImageFormat, - .stencilAttachmentFormat = VK_FORMAT_UNDEFINED}; - VkGraphicsPipelineCreateInfo DepthPipelineInfo = PipelineInfo; - DepthPipelineInfo.pNext = &DepthRenderingInfo; - DepthPipelineInfo.pColorBlendState = &DepthOnlyBlending; - DepthPipelineInfo.layout = m_MeshDepthPipelineLayout; - DepthPipelineInfo.stageCount = 1; - DepthPipelineInfo.pStages = &ShaderStages[0]; - VK_CHECK(vkCreateGraphicsPipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &DepthPipelineInfo, VK_NULL_HANDLE, - &m_MeshDepthPipeline)); - - VkPipelineRasterizationStateCreateInfo WireframeRasterizer = Rasterizer; - WireframeRasterizer.polygonMode = VK_POLYGON_MODE_LINE; - VkPipelineDepthStencilStateCreateInfo WireframeDepthStencil = DepthStencil; - WireframeDepthStencil.depthTestEnable = VK_FALSE; - WireframeDepthStencil.depthWriteEnable = VK_FALSE; - VkGraphicsPipelineCreateInfo WireframePipelineInfo = PipelineInfo; - WireframePipelineInfo.pRasterizationState = &WireframeRasterizer; - WireframePipelineInfo.pDepthStencilState = &WireframeDepthStencil; - VK_CHECK(vkCreateGraphicsPipelines(m_Device.Device, VK_NULL_HANDLE, 1, - &WireframePipelineInfo, VK_NULL_HANDLE, - &m_MeshWireframePipeline)); - - vkDestroyShaderModule(m_Device.Device, VertexShader, VK_NULL_HANDLE); - vkDestroyShaderModule(m_Device.Device, FragmentShader, VK_NULL_HANDLE); - - m_MainDeletionQueue.PushFunction([this]() { - vkDestroyPipelineLayout(m_Device.Device, m_HzbReducePipelineLayout, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_HzbReducePipeline, VK_NULL_HANDLE); - vkDestroyPipelineLayout(m_Device.Device, m_MeshProjectPipelineLayout, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_MeshProjectPipeline, VK_NULL_HANDLE); - vkDestroyPipelineLayout(m_Device.Device, m_MeshPipelineLayout, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_MeshPipeline, VK_NULL_HANDLE); - vkDestroyPipelineLayout(m_Device.Device, m_MeshDepthPipelineLayout, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_MeshDepthPipeline, VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_MeshWireframePipeline, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_MeshGraphicsAlphaBlendPipeline, - VK_NULL_HANDLE); - vkDestroyPipelineLayout(m_Device.Device, m_MeshGraphicsPipelineLayout, - VK_NULL_HANDLE); - vkDestroyPipeline(m_Device.Device, m_MeshGraphicsPipeline, VK_NULL_HANDLE); - }); -} - -void VulkanRendererBackend::InitMeshFrameResources() { - for (auto &Frame : m_MeshFrames) { - Frame.CameraBuffer = VkBufferUtil::CreateBuffer( - m_Device.Allocator, sizeof(CameraFrameUniform), - VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU, - VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | - VMA_ALLOCATION_CREATE_MAPPED_BIT); - Frame.HzbReadbackBuffer = VkBufferUtil::CreateBuffer( - m_Device.Allocator, static_cast(m_HzbReadbackBufferSize), - VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_TO_CPU, - VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | - VMA_ALLOCATION_CREATE_MAPPED_BIT); - - Frame.DepthFrameDescriptorSet = m_GlobalDescriptorAllocator.Allocate( - m_Device.Device, m_MeshGraphicsFrameDescriptorLayout); - for (VkDescriptorSet &DescriptorSet : Frame.GraphicsFrameDescriptorSets) { - DescriptorSet = m_GlobalDescriptorAllocator.Allocate( - m_Device.Device, m_MeshGraphicsFrameDescriptorLayout); - } - Frame.ComputeFrameDescriptorSet = m_GlobalDescriptorAllocator.Allocate( - m_Device.Device, m_MeshComputeFrameDescriptorLayout); - - VkQueryPoolCreateInfo QueryPoolInfo{ - .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, - .pNext = VK_NULL_HANDLE, - .flags = 0, - .queryType = VK_QUERY_TYPE_TIMESTAMP, - .queryCount = TimestampQueryCount, - .pipelineStatistics = 0}; - VK_CHECK(vkCreateQueryPool(m_Device.Device, &QueryPoolInfo, VK_NULL_HANDLE, - &Frame.TimestampQueryPool)); - } - - m_MainDeletionQueue.PushFunction([this]() { - for (auto &Frame : m_MeshFrames) { - if (Frame.TimestampQueryPool != VK_NULL_HANDLE) { - vkDestroyQueryPool(m_Device.Device, Frame.TimestampQueryPool, - VK_NULL_HANDLE); - Frame.TimestampQueryPool = VK_NULL_HANDLE; - } - } - }); -} - -std::shared_ptr -VulkanRendererBackend::CreateMesh(const MeshData &MeshSource) { - return VulkanMesh::Create(MeshSource, m_Device.Allocator, m_Device.Device, - m_Device.GraphicsQueue, GetCurrentFrame().CommandPool, - m_GlobalDescriptorAllocator, m_MeshDescriptorLayout); -} - -void VulkanRendererBackend::CollectFrameStats(MeshFrameResources &Frame) { - if (!Frame.HasValidTimestamps) { - return; - } - - uint64_t Timestamps[TimestampQueryCount] = {}; - const VkResult Result = vkGetQueryPoolResults( - m_Device.Device, Frame.TimestampQueryPool, 0, TimestampQueryCount, - sizeof(Timestamps), Timestamps, sizeof(uint64_t), - VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); - if (Result != VK_SUCCESS) { - return; - } - - const auto ToMilliseconds = [this](uint64_t Start, uint64_t End) { - if (End <= Start) { - return 0.0f; - } - - const double Nanoseconds = - static_cast(End - Start) * static_cast(m_TimestampPeriod); - return static_cast(Nanoseconds / 1'000'000.0); - }; - - m_FrameStats.GpuBackgroundMs = ToMilliseconds(Timestamps[0], Timestamps[1]); - m_FrameStats.GpuMeshMs = ToMilliseconds(Timestamps[2], Timestamps[3]); -} - -void VulkanRendererBackend::DrawBackground(VkCommandBuffer CommandBuffer) { - SyncHDRSkyboxTexture(); - - const bool UseHDR = m_LoadedHDRSkyboxData != nullptr && - m_HDRSkyboxDescriptorSet != VK_NULL_HANDLE && - m_ActiveScene != nullptr && - m_ActiveScene->ActiveCamera != nullptr && - !m_ActiveScene->ActiveCamera->IsOrthographic(); - - if (UseHDR) { - vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - m_HDRSkyboxPipeline); - const std::array Sets = {m_DrawImageDescriptorSet, - m_HDRSkyboxDescriptorSet}; - vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - m_HDRSkyboxPipelineLayout, 0, - static_cast(Sets.size()), Sets.data(), 0, - VK_NULL_HANDLE); - - const glm::mat4 InverseViewProj = glm::inverse( - m_ActiveScene->ActiveCamera->GetViewProjectionMatrix()); - - vkCmdPushConstants(CommandBuffer, m_HDRSkyboxPipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(glm::mat4), - glm::value_ptr(InverseViewProj)); - - vkCmdDispatch(CommandBuffer, - static_cast(std::ceil(m_DrawExtent.width / 16.0f)), - static_cast(std::ceil(m_DrawExtent.height / 16.0f)), - 1); - return; - } - - vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - m_GradientPipeline); - vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - m_GradientPipelineLayout, 0, 1, - &m_DrawImageDescriptorSet, 0, VK_NULL_HANDLE); - - ComputePushConstants PC; - if (m_ActiveScene) { - PC.data1 = glm::vec4(m_ActiveScene->SkyboxColorTop, 1.0f); - PC.data2 = glm::vec4(m_ActiveScene->SkyboxColorBottom, 1.0f); - } else { - PC.data1 = glm::vec4(0.08f, 0.09f, 0.14f, 1.0f); - PC.data2 = glm::vec4(0.14f, 0.24f, 0.38f, 1.0f); - } - - vkCmdPushConstants(CommandBuffer, m_GradientPipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(ComputePushConstants), &PC); - - vkCmdDispatch(CommandBuffer, - static_cast(std::ceil(m_DrawExtent.width / 16.0f)), - static_cast(std::ceil(m_DrawExtent.height / 16.0f)), 1); -} - -void VulkanRendererBackend::SyncHDRSkyboxTexture() { - const HDRTextureSourceDataRef Wanted = - m_ActiveScene ? m_ActiveScene->SkyboxHDRTexture : nullptr; - if (Wanted == m_LoadedHDRSkyboxData) { - return; - } - - // Defer destruction of the previous image onto the current frame's deletion - // queue. That queue flushes when this frame slot is reused, after its - // RenderFence is signaled — guaranteeing every command buffer that bound the - // old image/descriptor has finished executing on the GPU. - if (m_HDRSkyboxImage.Image != VK_NULL_HANDLE) { - AllocatedImage OldImage = m_HDRSkyboxImage; - GetCurrentFrame().DeletionQueue.PushFunction( - [Device = m_Device.Device, Allocator = m_Device.Allocator, - OldImage]() mutable { - if (OldImage.ImageView != VK_NULL_HANDLE) { - vkDestroyImageView(Device, OldImage.ImageView, VK_NULL_HANDLE); - } - if (OldImage.Image != VK_NULL_HANDLE) { - vmaDestroyImage(Allocator, OldImage.Image, OldImage.Allocation); - } - }); - m_HDRSkyboxImage = {}; - // The descriptor set is pool-allocated; drop the handle. The pool itself - // is destroyed at shutdown. - m_HDRSkyboxDescriptorSet = VK_NULL_HANDLE; - } - - if (Wanted) { - UploadHDRSkyboxTexture(*Wanted); - } - m_LoadedHDRSkyboxData = Wanted; -} - -void VulkanRendererBackend::UploadHDRSkyboxTexture( - const HDRTextureSourceData &Texture) { - if (!Texture.IsValid()) { - A_CORE_WARN("HDR skybox: refusing to upload invalid texture ({}x{})", - Texture.Width, Texture.Height); - return; - } - - m_HDRSkyboxImage.ImageFormat = VK_FORMAT_R32G32B32A32_SFLOAT; - m_HDRSkyboxImage.ImageExtent = {Texture.Width, Texture.Height, 1}; - - VkImageCreateInfo ImageInfo = VkInit::ImageCreateInfo( - m_HDRSkyboxImage.ImageFormat, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - m_HDRSkyboxImage.ImageExtent); - - VmaAllocationCreateInfo AllocationInfo{}; - AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; - AllocationInfo.requiredFlags = - VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - - VK_CHECK(vmaCreateImage(m_Device.Allocator, &ImageInfo, &AllocationInfo, - &m_HDRSkyboxImage.Image, &m_HDRSkyboxImage.Allocation, - VK_NULL_HANDLE)); - - VkImageViewCreateInfo ViewInfo = VkInit::ImageViewCreateInfo( - m_HDRSkyboxImage.ImageFormat, m_HDRSkyboxImage.Image, - VK_IMAGE_ASPECT_COLOR_BIT); - VK_CHECK(vkCreateImageView(m_Device.Device, &ViewInfo, VK_NULL_HANDLE, - &m_HDRSkyboxImage.ImageView)); - - const VkDeviceSize ByteCount = - static_cast(Texture.Pixels.size()) * sizeof(float); - auto StagingBuffer = VkBufferUtil::CreateBuffer( - m_Device.Allocator, ByteCount, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, - VMA_MEMORY_USAGE_CPU_ONLY, - VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | - VMA_ALLOCATION_CREATE_MAPPED_BIT); - std::memcpy(StagingBuffer.Info.pMappedData, Texture.Pixels.data(), ByteCount); - - ImmediateSubmit([&](VkCommandBuffer CommandBuffer) { - VkUtil::TransitionImage(CommandBuffer, m_HDRSkyboxImage.Image, - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - - VkBufferImageCopy Region{}; - Region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - Region.imageSubresource.layerCount = 1; - Region.imageExtent = m_HDRSkyboxImage.ImageExtent; - vkCmdCopyBufferToImage(CommandBuffer, StagingBuffer.Buffer, - m_HDRSkyboxImage.Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &Region); - - VkUtil::TransitionImage(CommandBuffer, m_HDRSkyboxImage.Image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - }); - - VkBufferUtil::DestroyBuffer(m_Device.Allocator, StagingBuffer); - - m_HDRSkyboxDescriptorSet = m_GlobalDescriptorAllocator.Allocate( - m_Device.Device, m_HDRSkyboxDescriptorLayout); - - VkDescriptorImageInfo SamplerImage{}; - SamplerImage.sampler = m_HDRSkyboxSampler; - SamplerImage.imageView = m_HDRSkyboxImage.ImageView; - SamplerImage.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - VkWriteDescriptorSet Write = VkInit::WriteDescriptorSet( - VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, m_HDRSkyboxDescriptorSet, - &SamplerImage, 0); - vkUpdateDescriptorSets(m_Device.Device, 1, &Write, 0, VK_NULL_HANDLE); -} - -void VulkanRendererBackend::DestroyHDRSkyboxTexture() { - if (m_HDRSkyboxImage.ImageView != VK_NULL_HANDLE) { - vkDestroyImageView(m_Device.Device, m_HDRSkyboxImage.ImageView, - VK_NULL_HANDLE); - m_HDRSkyboxImage.ImageView = VK_NULL_HANDLE; - } - if (m_HDRSkyboxImage.Image != VK_NULL_HANDLE) { - vmaDestroyImage(m_Device.Allocator, m_HDRSkyboxImage.Image, - m_HDRSkyboxImage.Allocation); - m_HDRSkyboxImage.Image = VK_NULL_HANDLE; - m_HDRSkyboxImage.Allocation = VK_NULL_HANDLE; - } - // The descriptor set lives in the pool; we drop the handle and rely on the - // pool being destroyed at shutdown. - m_HDRSkyboxDescriptorSet = VK_NULL_HANDLE; -} - -void VulkanRendererBackend::BuildHzb(VkCommandBuffer CommandBuffer, - MeshFrameResources &Frame) { - if (m_HzbReduceDescriptorSets.empty()) { - Frame.HasValidOcclusionData = false; - return; - } - - VkUtil::TransitionImage(CommandBuffer, m_RasterDepthImage.Image, - VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL); - TransitionImageRange(CommandBuffer, m_HzbImage.Image, m_HzbImageLayout, - VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT, 0, - static_cast(m_HzbMipImageViews.size())); - m_HzbImageLayout = VK_IMAGE_LAYOUT_GENERAL; - - vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - m_HzbReducePipeline); - for (size_t MipLevel = 0; MipLevel < m_HzbReduceDescriptorSets.size(); - ++MipLevel) { - vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - m_HzbReducePipelineLayout, 0, 1, - &m_HzbReduceDescriptorSets[MipLevel], 0, - VK_NULL_HANDLE); - - const VkExtent2D SourceExtent = - (MipLevel == 0) ? VkExtent2D{m_DrawExtent.width, m_DrawExtent.height} - : m_HzbMipExtents[MipLevel - 1]; - const VkExtent2D DestinationExtent = m_HzbMipExtents[MipLevel]; - HzbReducePushConstants PushConstants{}; - PushConstants.Dimensions = glm::uvec4(SourceExtent.width, SourceExtent.height, - DestinationExtent.width, - DestinationExtent.height); - vkCmdPushConstants(CommandBuffer, m_HzbReducePipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(HzbReducePushConstants), &PushConstants); - - vkCmdDispatch(CommandBuffer, - static_cast( - std::ceil(DestinationExtent.width / 8.0f)), - static_cast( - std::ceil(DestinationExtent.height / 8.0f)), - 1); - - TransitionImageRange(CommandBuffer, m_HzbImage.Image, VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_ASPECT_COLOR_BIT, - static_cast(MipLevel), 1); - } - - TransitionImageRange(CommandBuffer, m_HzbImage.Image, VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - VK_IMAGE_ASPECT_COLOR_BIT, 0, - static_cast(m_HzbMipImageViews.size())); - m_HzbImageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; - - for (size_t MipLevel = 0; MipLevel < m_HzbMipExtents.size(); ++MipLevel) { - VkBufferImageCopy CopyRegion{}; - CopyRegion.bufferOffset = m_HzbMipOffsets[MipLevel]; - CopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - CopyRegion.imageSubresource.mipLevel = static_cast(MipLevel); - CopyRegion.imageSubresource.layerCount = 1; - CopyRegion.imageExtent = {m_HzbMipExtents[MipLevel].width, - m_HzbMipExtents[MipLevel].height, 1}; - vkCmdCopyImageToBuffer(CommandBuffer, m_HzbImage.Image, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - Frame.HzbReadbackBuffer.Buffer, 1, &CopyRegion); - } - - VkBufferMemoryBarrier2 ReadbackBarrier{ - .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, - .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, - .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .buffer = Frame.HzbReadbackBuffer.Buffer, - .offset = 0, - .size = Frame.HzbReadbackBuffer.Size}; - VkDependencyInfo ReadbackDependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .bufferMemoryBarrierCount = 1, - .pBufferMemoryBarriers = &ReadbackBarrier}; - vkCmdPipelineBarrier2(CommandBuffer, &ReadbackDependencyInfo); - - VkUtil::TransitionImage(CommandBuffer, m_RasterDepthImage.Image, - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, - VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); - Frame.HasValidOcclusionData = true; -} - -void VulkanRendererBackend::ClearDepthImage(VkCommandBuffer CommandBuffer) { - const auto PreviousLayout = - (m_FrameNumber == 0) ? VK_IMAGE_LAYOUT_UNDEFINED - : VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; - VkUtil::TransitionImage(CommandBuffer, m_RasterDepthImage.Image, - PreviousLayout, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); -} - -void VulkanRendererBackend::DrawMeshes(VkCommandBuffer CommandBuffer, - RenderScene &Scene) { - m_SceneRenderer.RenderScenePasses( - {.CommandBuffer = CommandBuffer, - .Scene = Scene, - .Frame = GetCurrentMeshFrame(), - .FrameStats = m_FrameStats, - .MeshFrames = m_MeshFrames, - .CommandContext = m_CommandContext, - .MaterialResources = m_MaterialResources, - .OcclusionCulling = m_OcclusionCulling, - .Device = m_Device.Device, - .Allocator = m_Device.Allocator, - .FrameNumber = m_FrameNumber, - .DrawExtent = m_DrawExtent, - .ViewMode = m_ViewMode, - .DrawImage = m_DrawImage, - .RasterDepthImage = m_RasterDepthImage, - .LinearDepthSampler = m_LinearDepthSampler, - .TextureSampler = m_TextureSampler, - .MeshProjectPipeline = m_MeshProjectPipeline, - .MeshProjectPipelineLayout = m_MeshProjectPipelineLayout, - .MeshPipeline = m_MeshPipeline, - .MeshPipelineLayout = m_MeshPipelineLayout, - .MeshGraphicsPipeline = m_MeshGraphicsPipeline, - .MeshGraphicsAlphaBlendPipeline = m_MeshGraphicsAlphaBlendPipeline, - .MeshGraphicsPipelineLayout = m_MeshGraphicsPipelineLayout, - .MeshWireframePipeline = m_MeshWireframePipeline, - .MeshDepthPipeline = m_MeshDepthPipeline, - .MeshDepthPipelineLayout = m_MeshDepthPipelineLayout, - .HzbMipExtents = m_HzbMipExtents, - .HzbMipOffsets = m_HzbMipOffsets, - .BuildHzb = - [this](VkCommandBuffer DrawCommandBuffer, MeshFrameResources &Frame) { - BuildHzb(DrawCommandBuffer, Frame); - }, - .WarnOnce = - [this](const char *Message) { - if (!m_HasWarnedMeshSubmissionOverflow) { - A_CORE_WARN("{0}", Message); - m_HasWarnedMeshSubmissionOverflow = true; - } - }}); -} - -void VulkanRendererBackend::RecordOffscreenCapture( - VkCommandBuffer CommandBuffer, const AllocatedBuffer &ReadbackBuffer) { - VkBufferImageCopy CopyRegion{}; - CopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - CopyRegion.imageSubresource.mipLevel = 0; - CopyRegion.imageSubresource.layerCount = 1; - CopyRegion.imageExtent = {m_DrawExtent.width, m_DrawExtent.height, 1}; - - vkCmdCopyImageToBuffer(CommandBuffer, m_DrawImage.Image, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - ReadbackBuffer.Buffer, 1, &CopyRegion); - - VkBufferMemoryBarrier2 ReadbackBarrier{ - .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, - .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, - .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .buffer = ReadbackBuffer.Buffer, - .offset = 0, - .size = ReadbackBuffer.Size}; - VkDependencyInfo ReadbackDependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .bufferMemoryBarrierCount = 1, - .pBufferMemoryBarriers = &ReadbackBarrier}; - vkCmdPipelineBarrier2(CommandBuffer, &ReadbackDependencyInfo); -} - -float VulkanRendererBackend::HalfToFloat(uint16_t Value) { - const uint32_t Sign = static_cast(Value & 0x8000u) << 16u; - const uint32_t Exponent = (Value & 0x7C00u) >> 10u; - const uint32_t Mantissa = Value & 0x03FFu; - - uint32_t ResultBits = 0; - if (Exponent == 0u) { - if (Mantissa == 0u) { - ResultBits = Sign; - } else { - uint32_t ShiftedMantissa = Mantissa; - uint32_t AdjustedExponent = 127u - 15u + 1u; - while ((ShiftedMantissa & 0x0400u) == 0u) { - ShiftedMantissa <<= 1u; - --AdjustedExponent; - } - ShiftedMantissa &= 0x03FFu; - ResultBits = - Sign | (AdjustedExponent << 23u) | (ShiftedMantissa << 13u); - } - } else if (Exponent == 0x1Fu) { - ResultBits = Sign | 0x7F800000u | (Mantissa << 13u); - } else { - ResultBits = - Sign | ((Exponent + (127u - 15u)) << 23u) | (Mantissa << 13u); - } - - return std::bit_cast(ResultBits); -} - -uint8_t VulkanRendererBackend::LinearToByte(float Value) { - return static_cast( - std::round(std::clamp(Value, 0.0f, 1.0f) * 255.0f)); -} - -void VulkanRendererBackend::ConvertCapturedFrameToRgba8( - const AllocatedBuffer &ReadbackBuffer, uint64_t FrameNumber) { - if (ReadbackBuffer.Info.pMappedData == nullptr) { - return; - } - - vmaInvalidateAllocation(m_Device.Allocator, ReadbackBuffer.Allocation, 0, - ReadbackBuffer.Size); - - CapturedFrame Frame{}; - Frame.FrameIndex = FrameNumber; - Frame.Width = m_DrawExtent.width; - Frame.Height = m_DrawExtent.height; - Frame.Pixels.resize(static_cast(Frame.Width) * Frame.Height * 4u); - - const auto *Source = - static_cast(ReadbackBuffer.Info.pMappedData); - auto *Destination = reinterpret_cast(Frame.Pixels.data()); - for (size_t PixelIndex = 0; - PixelIndex < - static_cast(Frame.Width) * static_cast(Frame.Height); - ++PixelIndex) { - const size_t SourceIndex = PixelIndex * 4u; - const size_t DestinationIndex = PixelIndex * 4u; - Destination[DestinationIndex + 0] = - LinearToByte(HalfToFloat(Source[SourceIndex + 0])); - Destination[DestinationIndex + 1] = - LinearToByte(HalfToFloat(Source[SourceIndex + 1])); - Destination[DestinationIndex + 2] = - LinearToByte(HalfToFloat(Source[SourceIndex + 2])); - Destination[DestinationIndex + 3] = - LinearToByte(HalfToFloat(Source[SourceIndex + 3])); - } - - m_CapturedFrame = std::move(Frame); -} - -void VulkanRendererBackend::Draw() { - auto &CurrentFrame = - m_CommandContext.PrepareFrame(m_Device.Device, m_FrameNumber); - auto &MeshFrame = GetCurrentMeshFrame(); - auto &CaptureFrame = m_OffscreenCaptureFrames[m_FrameNumber % FRAME_OVERLAP]; - - CollectFrameStats(MeshFrame); - m_CapturedFrame.reset(); - if (!m_HasPresentationSurface) { - PublishCompletedOffscreenFrame(m_FrameNumber); - } - - uint32_t SwapchainImageIndex = 0; - if (m_HasPresentationSurface) { - SwapchainImageIndex = m_Swapchain.AcquireNextImage( - m_Device.Device, CurrentFrame.SwapchainSemaphore); - } - - VkCommandBuffer CommandBuffer = CurrentFrame.MainCommandBuffer; - VK_CHECK(vkResetCommandBuffer(CommandBuffer, 0)); - - VkCommandBufferBeginInfo CommandBufferBeginInfo = - VkInit::CommandBufferBeginInfo( - VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); - - m_DrawExtent.width = m_DrawImage.ImageExtent.width; - m_DrawExtent.height = m_DrawImage.ImageExtent.height; - m_FrameStats.DrawExtent = {m_DrawExtent.width, m_DrawExtent.height}; - m_FrameStats.SubmittedMeshCount = 0; - m_FrameStats.FrustumCulledMeshCount = 0; - m_FrameStats.OcclusionCulledMeshCount = 0; - m_FrameStats.MeshSubmissionCount = 0; - m_FrameStats.TriangleCount = 0; - MeshFrame.HasValidTimestamps = true; - MeshFrame.HasValidOcclusionData = false; - - VK_CHECK(vkBeginCommandBuffer(CommandBuffer, &CommandBufferBeginInfo)); - vkCmdResetQueryPool(CommandBuffer, MeshFrame.TimestampQueryPool, 0, - TimestampQueryCount); - - VkUtil::TransitionImage(CommandBuffer, m_DrawImage.Image, - VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); - - vkCmdWriteTimestamp2(CommandBuffer, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - MeshFrame.TimestampQueryPool, 0); - DrawBackground(CommandBuffer); - vkCmdWriteTimestamp2(CommandBuffer, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - MeshFrame.TimestampQueryPool, 1); - - ClearDepthImage(CommandBuffer); - VkUtil::TransitionImage(CommandBuffer, m_DrawImage.Image, - VK_IMAGE_LAYOUT_GENERAL, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); - - vkCmdWriteTimestamp2(CommandBuffer, - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, - MeshFrame.TimestampQueryPool, 2); - if (m_ActiveScene != nullptr && !m_ActiveScene->Submissions.empty() && - m_ActiveScene->ActiveCamera != nullptr) { - DrawMeshes(CommandBuffer, *m_ActiveScene); - } - vkCmdWriteTimestamp2(CommandBuffer, - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, - MeshFrame.TimestampQueryPool, 3); - - if (m_ActiveScene != nullptr && m_LightBillboardRenderer.IsInitialized()) { - m_LightBillboardRenderer.DrawLightBillboards( - CommandBuffer, m_DrawExtent, m_DrawImage.ImageView, *m_ActiveScene); - } - - if (m_ActiveScene != nullptr && m_GizmoRenderer.IsInitialized()) { - m_GizmoRenderer.DrawGizmoOverlay(CommandBuffer, m_DrawExtent, - m_DrawImage.ImageView, *m_ActiveScene); - } - - VkUtil::TransitionImage(CommandBuffer, m_DrawImage.Image, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - if (m_HasPresentationSurface) { - VkUtil::TransitionImage(CommandBuffer, m_Swapchain.Images[SwapchainImageIndex], - VK_IMAGE_LAYOUT_UNDEFINED, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); - - VkUtil::CopyImageToImage(CommandBuffer, m_DrawImage.Image, - m_Swapchain.Images[SwapchainImageIndex], m_DrawExtent, - m_Swapchain.Extent); - - VkUtil::TransitionImage(CommandBuffer, m_Swapchain.Images[SwapchainImageIndex], - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); - - if (m_EnableImGui) { - m_ImGuiRenderer.RecordDrawData(CommandBuffer, m_Swapchain.Extent, - m_Swapchain.ImageViews[SwapchainImageIndex]); - } - - VkUtil::TransitionImage(CommandBuffer, m_Swapchain.Images[SwapchainImageIndex], - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); - } else { - RecordOffscreenCapture(CommandBuffer, CaptureFrame.ReadbackBuffer); - } - - VK_CHECK(vkEndCommandBuffer(CommandBuffer)); - - VkCommandBufferSubmitInfo CommandBufferSubmitInfo = - VkInit::CommandBufferSubmitInfo(CommandBuffer); - if (m_HasPresentationSurface) { - VkSemaphoreSubmitInfo SwapchainSemaphoreSubmitInfo = - VkInit::SemaphoreSubmitInfo( - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT_KHR, - CurrentFrame.SwapchainSemaphore); - VkSemaphoreSubmitInfo RenderSemaphoreSubmitInfo = - VkInit::SemaphoreSubmitInfo(VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - CurrentFrame.RenderSemaphore); - - VkSubmitInfo2 Submit = - VkInit::SubmitInfo(&CommandBufferSubmitInfo, &RenderSemaphoreSubmitInfo, - &SwapchainSemaphoreSubmitInfo); - VK_CHECK(vkQueueSubmit2(m_Device.GraphicsQueue, 1, &Submit, - CurrentFrame.RenderFence)); - - m_Swapchain.Present(m_Device.GraphicsQueue, SwapchainImageIndex, - CurrentFrame.RenderSemaphore); - } else { - VkSubmitInfo2 Submit = - VkInit::SubmitInfo(&CommandBufferSubmitInfo, VK_NULL_HANDLE, - VK_NULL_HANDLE); - VK_CHECK(vkQueueSubmit2(m_Device.GraphicsQueue, 1, &Submit, - CurrentFrame.RenderFence)); - CaptureFrame.HasPendingReadback = true; - CaptureFrame.SubmittedFrameNumber = m_FrameNumber; - CaptureFrame.SubmittedUser = m_ViewportFrameUser; - } - - m_FrameNumber++; -} - -void VulkanRendererBackend::ImmediateSubmit( - std::function &&Function) { - m_CommandContext.ImmediateSubmit(m_Device.Device, m_Device.GraphicsQueue, - std::move(Function)); -} - -void VulkanRendererBackend::EnqueueDeferredDestroy( - std::function &&Function) { - m_MainDeletionQueue.PushFunction(std::move(Function)); -} - -void VulkanRendererBackend::BeginFrame() { - m_StopRendering = - m_HasPresentationSurface && glfwGetWindowAttrib(m_Window, GLFW_ICONIFIED); - m_RenderFallbackBackground = false; - m_ActiveScene = nullptr; - if (m_StopRendering) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - return; - } - - if (m_EnableImGui) { - m_ImGuiRenderer.BeginFrame(); - } -} - -void VulkanRendererBackend::RenderSceneMeshes(RenderScene &Scene) { - m_ActiveScene = &Scene; -} - -void VulkanRendererBackend::RenderFallbackBackground(RenderScene &Scene) { - (void)Scene; - m_RenderFallbackBackground = true; -} - -RendererFrameStats &VulkanRendererBackend::AccessFrameStats() { - return m_FrameStats; -} - -const RendererFrameStats &VulkanRendererBackend::GetFrameStats() const { - return m_FrameStats; -} - -void VulkanRendererBackend::RenderImGui() { - if (m_StopRendering || !m_EnableImGui) { - return; - } - - m_ImGuiRenderer.BuildStatsUiAndRender(m_FrameStats, m_ViewMode); -} - -void VulkanRendererBackend::EndFrame() { - if (m_StopRendering) { - return; - } - - Draw(); -} - -void VulkanRendererBackend::SetViewMode(RendererViewMode ViewMode) { - m_ViewMode = ViewMode; -} - -void VulkanRendererBackend::SetViewportFrameUser(SessionUserId User) { - m_ViewportFrameUser = User; -} - -void VulkanRendererBackend::SetViewportFrameOutput( - IViewportFrameOutput *FrameOutput) { - m_FrameOutput = FrameOutput; -} - -std::optional VulkanRendererBackend::ConsumeCapturedFrame() { - std::optional Result = std::move(m_CapturedFrame); - m_CapturedFrame.reset(); - return Result; -} - -void VulkanRendererBackend::PublishCompletedOffscreenFrame(uint64_t FrameNumber) { - auto &CaptureFrame = m_OffscreenCaptureFrames[FrameNumber % FRAME_OVERLAP]; - if (!CaptureFrame.HasPendingReadback) { - return; - } - - ConvertCapturedFrameToRgba8(CaptureFrame.ReadbackBuffer, - CaptureFrame.SubmittedFrameNumber); - CaptureFrame.HasPendingReadback = false; - if (m_FrameOutput == nullptr || !m_CapturedFrame.has_value()) { - return; - } - - const CapturedFrame &Captured = *m_CapturedFrame; - const auto *Bytes = - reinterpret_cast(Captured.Pixels.data()); - m_FrameOutput->OnViewportFrame({ - .FrameIndex = Captured.FrameIndex, - .Width = Captured.Width, - .Height = Captured.Height, - .Format = ViewportFrameFormat::R8G8B8A8Unorm, - .Pixels = std::span(Bytes, Captured.Pixels.size()), - .User = CaptureFrame.SubmittedUser, - }); -} -} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanRendererBackend.h b/Axiom/Renderer/Vulkan/VulkanRendererBackend.h deleted file mode 100644 index 7efb8412..00000000 --- a/Axiom/Renderer/Vulkan/VulkanRendererBackend.h +++ /dev/null @@ -1,180 +0,0 @@ -#pragma once - -#include "Renderer/RendererBackend.h" -#include "Renderer/RenderSurface.h" -#include "Renderer/Vulkan/VulkanCommandContext.h" -#include "Renderer/Vulkan/VulkanContext.h" -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanDescriptors.h" -#include "Renderer/Vulkan/VulkanDevice.h" -#include "Renderer/Vulkan/VulkanGizmoRenderer.h" -#include "Renderer/Vulkan/VulkanImGuiRenderer.h" -#include "Renderer/Vulkan/VulkanLightBillboardRenderer.h" -#include "Renderer/Vulkan/VulkanMaterialResources.h" -#include "Renderer/Vulkan/VulkanOcclusionCulling.h" -#include "Renderer/Vulkan/VulkanRendererTypes.h" -#include "Renderer/Vulkan/VulkanSceneRenderer.h" -#include "Renderer/Vulkan/VulkanSwapchain.h" - -#include -#include -#include -#include -#include -#include - -struct GLFWwindow; - -namespace Axiom { -class VulkanRendererBackend final : public RendererBackend { -public: - static VulkanRendererBackend &Get(); - static VulkanRendererBackend *TryGet(); - - void Init(const RendererCreateInfo &CreateInfo) override; - void Shutdown() override; - void BeginFrame() override; - std::shared_ptr CreateMesh(const MeshData &Mesh) override; - void RenderSceneMeshes(RenderScene &Scene) override; - void RenderFallbackBackground(RenderScene &Scene) override; - RendererFrameStats &AccessFrameStats() override; - const RendererFrameStats &GetFrameStats() const override; - void RenderImGui() override; - void EndFrame() override; - void SetViewMode(RendererViewMode ViewMode) override; - void SetViewportFrameUser(SessionUserId User) override; - void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput) override; - std::optional ConsumeCapturedFrame() override; - - void ImmediateSubmit(std::function &&Function); - void EnqueueDeferredDestroy(std::function &&Function); - bool IsInitialized() const { return m_IsInitialized; } - -private: - void InitSwapchain(); - void InitDescriptors(); - void InitTextureResources(); - void InitPipelines(); - void InitBackgroundPipelines(); - void InitMeshPipelines(); - void InitGizmoPipeline(); - void InitLightBillboardPipeline(); - void InitMeshFrameResources(); - - void InitHzbResources(); - void CollectFrameStats(MeshFrameResources &Frame); - void DrawBackground(VkCommandBuffer CommandBuffer); - void SyncHDRSkyboxTexture(); - void UploadHDRSkyboxTexture(const HDRTextureSourceData &Texture); - void DestroyHDRSkyboxTexture(); - void DrawMeshes(VkCommandBuffer CommandBuffer, RenderScene &Scene); - void BuildHzb(VkCommandBuffer CommandBuffer, MeshFrameResources &Frame); - void RecordOffscreenCapture(VkCommandBuffer CommandBuffer, - const AllocatedBuffer &ReadbackBuffer); - void ClearDepthImage(VkCommandBuffer CommandBuffer); - void InitViewportReadbackBuffers(); - void PublishCompletedOffscreenFrame(uint64_t FrameNumber); - void Draw(); - static float HalfToFloat(uint16_t Value); - static uint8_t LinearToByte(float Value); - void ConvertCapturedFrameToRgba8(const AllocatedBuffer &ReadbackBuffer, - uint64_t FrameNumber); - - struct OffscreenCaptureFrame { - AllocatedBuffer ReadbackBuffer; - bool HasPendingReadback{false}; - uint64_t SubmittedFrameNumber{0}; - SessionUserId SubmittedUser{}; - }; - - FrameData &GetCurrentFrame() { - return m_CommandContext.GetFrame(m_FrameNumber); - } - - MeshFrameResources &GetCurrentMeshFrame() { - return m_MeshFrames[m_FrameNumber % FRAME_OVERLAP]; - } - -private: - bool m_IsInitialized{false}; - uint64_t m_FrameNumber{0}; - bool m_StopRendering{false}; - bool m_RenderFallbackBackground{false}; - VkExtent2D m_WindowExtent{1700, 900}; - bool m_HasPresentationSurface{false}; - bool m_EnableImGui{true}; - - GLFWwindow *m_Window{nullptr}; - RenderSurfacePtr m_Surface; - IViewportFrameOutput *m_FrameOutput{nullptr}; - - VulkanContext m_Context; - VulkanDevice m_Device; - VulkanSwapchain m_Swapchain; - VulkanCommandContext m_CommandContext; - - DescriptorAllocator m_GlobalDescriptorAllocator; - VkDescriptorSet m_DrawImageDescriptorSet{VK_NULL_HANDLE}; - VkDescriptorSetLayout m_DrawImageDescriptorLayout{VK_NULL_HANDLE}; - VkDescriptorSetLayout m_HzbReduceDescriptorLayout{VK_NULL_HANDLE}; - VkDescriptorSetLayout m_MeshGraphicsFrameDescriptorLayout{VK_NULL_HANDLE}; - VkDescriptorSetLayout m_MeshComputeFrameDescriptorLayout{VK_NULL_HANDLE}; - VkDescriptorSetLayout m_MeshDescriptorLayout{VK_NULL_HANDLE}; - VkSampler m_LinearDepthSampler{VK_NULL_HANDLE}; - VkSampler m_TextureSampler{VK_NULL_HANDLE}; - - VkPipeline m_GradientPipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_GradientPipelineLayout{VK_NULL_HANDLE}; - VkPipeline m_HDRSkyboxPipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_HDRSkyboxPipelineLayout{VK_NULL_HANDLE}; - VkDescriptorSetLayout m_HDRSkyboxDescriptorLayout{VK_NULL_HANDLE}; - VkDescriptorSet m_HDRSkyboxDescriptorSet{VK_NULL_HANDLE}; - VkSampler m_HDRSkyboxSampler{VK_NULL_HANDLE}; - AllocatedImage m_HDRSkyboxImage{}; - HDRTextureSourceDataRef m_LoadedHDRSkyboxData{nullptr}; - VkPipeline m_HzbReducePipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_HzbReducePipelineLayout{VK_NULL_HANDLE}; - VkPipeline m_MeshProjectPipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_MeshProjectPipelineLayout{VK_NULL_HANDLE}; - VkPipeline m_MeshPipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_MeshPipelineLayout{VK_NULL_HANDLE}; - VkPipeline m_MeshGraphicsPipeline{VK_NULL_HANDLE}; - VkPipeline m_MeshGraphicsAlphaBlendPipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_MeshGraphicsPipelineLayout{VK_NULL_HANDLE}; - VkPipeline m_MeshWireframePipeline{VK_NULL_HANDLE}; - VkPipeline m_MeshDepthPipeline{VK_NULL_HANDLE}; - VkPipelineLayout m_MeshDepthPipelineLayout{VK_NULL_HANDLE}; - - DeletionQueue m_MainDeletionQueue; - - AllocatedImage m_DrawImage; - AllocatedImage m_DepthImage; - AllocatedImage m_RasterDepthImage; - AllocatedImage m_HzbImage; - VkExtent2D m_DrawExtent{}; - std::array m_OffscreenCaptureFrames{}; - std::vector m_HzbMipImageViews; - std::vector m_HzbReduceDescriptorSets; - std::vector m_HzbMipExtents; - std::vector m_HzbMipOffsets; - VkDeviceSize m_HzbReadbackBufferSize{0}; - VkImageLayout m_HzbImageLayout{VK_IMAGE_LAYOUT_UNDEFINED}; - - std::array m_MeshFrames{}; - VulkanGizmoRenderer m_GizmoRenderer; - VulkanImGuiRenderer m_ImGuiRenderer; - VulkanLightBillboardRenderer m_LightBillboardRenderer; - VulkanMaterialResources m_MaterialResources; - VulkanOcclusionCulling m_OcclusionCulling; - VulkanSceneRenderer m_SceneRenderer; - MaterialInstanceRef m_LightBillboardMaterial; - RenderScene *m_ActiveScene{nullptr}; - RendererFrameStats m_FrameStats{}; - float m_TimestampPeriod{0.0f}; - float m_RenderScale{0.5f}; - bool m_HasWarnedMeshSubmissionOverflow{false}; - RendererViewMode m_ViewMode{RendererViewMode::Lit}; - SessionUserId m_ViewportFrameUser{}; - std::optional m_CapturedFrame; -}; -} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanSceneRenderer.cpp b/Axiom/Renderer/Vulkan/VulkanSceneRenderer.cpp deleted file mode 100644 index 53e85c1a..00000000 --- a/Axiom/Renderer/Vulkan/VulkanSceneRenderer.cpp +++ /dev/null @@ -1,630 +0,0 @@ -#include "Renderer/Vulkan/VulkanSceneRenderer.h" - -#include "Renderer/Camera.h" -#include "Renderer/Vulkan/VulkanInitializers.h" -#include "Renderer/Vulkan/VulkanMesh.h" - -#include -#include - -#include -#include -#include - -namespace Axiom { -namespace { -glm::vec3 TransformPoint(const glm::mat4 &Transform, const glm::vec3 &Point) { - return glm::vec3(Transform * glm::vec4(Point, 1.0f)); -} -} // namespace - -CameraFrameUniform -VulkanSceneRenderer::BuildCameraData(const RenderContext &Context) { - auto &Camera = *Context.Scene.ActiveCamera; - - CameraFrameUniform CameraData{}; - CameraData.View = Camera.GetViewMatrix(); - CameraData.Projection = Camera.GetProjectionMatrix(); - CameraData.ViewProjection = Camera.GetViewProjectionMatrix(); - CameraData.CameraPosition = glm::vec4(Camera.GetPosition(), 1.0f); - CameraData.ViewportSize = - glm::vec4(static_cast(Context.DrawExtent.width), - static_cast(Context.DrawExtent.height), 0.0f, 0.0f); - CameraData.RenderOptions.x = static_cast(Context.ViewMode); - - if (Context.Scene.Sun.has_value()) { - const auto &Sun = *Context.Scene.Sun; - const glm::vec3 Dir = glm::normalize(Sun.Direction); - CameraData.LightDirectionAndIntensity = glm::vec4(Dir, Sun.Intensity); - CameraData.LightColorAndEnabled = glm::vec4(Sun.Color, 1.0f); - } - - return CameraData; -} - -glm::vec3 VulkanSceneRenderer::ComputeWorldCenter( - const RenderMeshSubmission &Submission, const VulkanMesh &Mesh) { - const glm::vec3 LocalCenter = (Mesh.BoundsMin + Mesh.BoundsMax) * 0.5f; - return TransformPoint(Submission.Transform, LocalCenter); -} - -void VulkanSceneRenderer::BindMeshBuffers( - VkCommandBuffer CommandBuffer, const std::shared_ptr &MeshRef) { - VkDeviceSize VertexOffset = 0; - vkCmdBindVertexBuffers(CommandBuffer, 0, 1, &MeshRef->VertexBuffer.Buffer, - &VertexOffset); - vkCmdBindIndexBuffer(CommandBuffer, MeshRef->IndexBuffer.Buffer, 0, - VK_INDEX_TYPE_UINT32); -} - -void VulkanSceneRenderer::UpdateComputeFrameDescriptors( - const RenderContext &Context, - const VkDescriptorBufferInfo &CameraBufferInfo) { - VkDescriptorBufferInfo MutableCameraBufferInfo = CameraBufferInfo; - VkDescriptorImageInfo ColorImageInfo{}; - ColorImageInfo.imageView = Context.DrawImage.ImageView; - ColorImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - - VkDescriptorImageInfo DepthImageInfo{}; - DepthImageInfo.sampler = Context.LinearDepthSampler; - DepthImageInfo.imageView = Context.RasterDepthImage.ImageView; - DepthImageInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - - std::array ComputeFrameWrites = { - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, - Context.Frame.ComputeFrameDescriptorSet, - &ColorImageInfo, 0), - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - Context.Frame.ComputeFrameDescriptorSet, - &DepthImageInfo, 1), - VkInit::WriteDescriptorBuffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - Context.Frame.ComputeFrameDescriptorSet, - &MutableCameraBufferInfo, 2)}; - vkUpdateDescriptorSets(Context.Device, - static_cast(ComputeFrameWrites.size()), - ComputeFrameWrites.data(), 0, VK_NULL_HANDLE); -} - -void VulkanSceneRenderer::UpdateDepthFrameDescriptors( - const RenderContext &Context, - const VkDescriptorBufferInfo &CameraBufferInfo) { - VkDescriptorBufferInfo MutableCameraBufferInfo = CameraBufferInfo; - VkDescriptorImageInfo DefaultTextureImageInfo{}; - DefaultTextureImageInfo.imageView = - Context.MaterialResources.GetFallbackTextureView(); - DefaultTextureImageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkDescriptorImageInfo DefaultTextureSamplerInfo{}; - DefaultTextureSamplerInfo.sampler = Context.TextureSampler; - std::array DefaultGraphicsFrameWrites = { - VkInit::WriteDescriptorBuffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - Context.Frame.DepthFrameDescriptorSet, - &MutableCameraBufferInfo, 0), - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, - Context.Frame.DepthFrameDescriptorSet, - &DefaultTextureImageInfo, 1), - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_SAMPLER, - Context.Frame.DepthFrameDescriptorSet, - &DefaultTextureSamplerInfo, 2)}; - vkUpdateDescriptorSets(Context.Device, - static_cast(DefaultGraphicsFrameWrites.size()), - DefaultGraphicsFrameWrites.data(), 0, VK_NULL_HANDLE); -} - -void VulkanSceneRenderer::UpdateGraphicsFrameDescriptors( - const RenderContext &Context, VkDescriptorSet GraphicsDescriptorSet, - VkImageView TextureView, const VkDescriptorBufferInfo &CameraBufferInfo) { - VkDescriptorBufferInfo MutableCameraBufferInfo = CameraBufferInfo; - VkDescriptorImageInfo GraphicsTextureImageInfo{}; - GraphicsTextureImageInfo.imageView = TextureView; - GraphicsTextureImageInfo.imageLayout = - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - VkDescriptorImageInfo GraphicsTextureSamplerInfo{}; - GraphicsTextureSamplerInfo.sampler = Context.TextureSampler; - std::array GraphicsFrameWrites = { - VkInit::WriteDescriptorBuffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - GraphicsDescriptorSet, - &MutableCameraBufferInfo, 0), - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, - GraphicsDescriptorSet, - &GraphicsTextureImageInfo, 1), - VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_SAMPLER, - GraphicsDescriptorSet, - &GraphicsTextureSamplerInfo, 2)}; - vkUpdateDescriptorSets(Context.Device, - static_cast(GraphicsFrameWrites.size()), - GraphicsFrameWrites.data(), 0, VK_NULL_HANDLE); -} - -void VulkanSceneRenderer::RecordDepthPrepass( - const RenderContext &Context, const VkViewport &Viewport, - const VkRect2D &Scissor, - const std::vector &GraphicsSubmissions, - const std::vector &ComputeSubmissions) { - if (GraphicsSubmissions.empty() && ComputeSubmissions.empty()) { - return; - } - - VkRenderingAttachmentInfo DepthOnlyAttachment = - VkInit::DepthAttachmentInfo(Context.RasterDepthImage.ImageView, - VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); - VkRenderingInfo DepthOnlyRenderingInfo{ - .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, - .pNext = VK_NULL_HANDLE, - .renderArea = VkRect2D{VkOffset2D{0, 0}, Context.DrawExtent}, - .layerCount = 1, - .colorAttachmentCount = 0, - .pColorAttachments = VK_NULL_HANDLE, - .pDepthAttachment = &DepthOnlyAttachment, - .pStencilAttachment = VK_NULL_HANDLE}; - - vkCmdBeginRendering(Context.CommandBuffer, &DepthOnlyRenderingInfo); - vkCmdBindPipeline(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - Context.MeshDepthPipeline); - vkCmdSetViewport(Context.CommandBuffer, 0, 1, &Viewport); - vkCmdSetScissor(Context.CommandBuffer, 0, 1, &Scissor); - vkCmdBindDescriptorSets(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - Context.MeshDepthPipelineLayout, 0, 1, - &Context.Frame.DepthFrameDescriptorSet, 0, - VK_NULL_HANDLE); - - auto RecordSubmission = [&](const VisibleMeshSubmission &VisibleSubmission) { - MeshGraphicsPushConstants PushConstants{}; - PushConstants.Model = VisibleSubmission.Submission->Transform; - vkCmdPushConstants(Context.CommandBuffer, Context.MeshDepthPipelineLayout, - VK_SHADER_STAGE_VERTEX_BIT, 0, - sizeof(MeshGraphicsPushConstants), &PushConstants); - BindMeshBuffers(Context.CommandBuffer, VisibleSubmission.Mesh); - vkCmdDrawIndexed(Context.CommandBuffer, VisibleSubmission.Mesh->IndexCount, 1, - 0, 0, 0); - }; - - for (const auto &VisibleSubmission : GraphicsSubmissions) { - RecordSubmission(VisibleSubmission); - } - for (const auto &VisibleSubmission : ComputeSubmissions) { - RecordSubmission(VisibleSubmission); - } - - vkCmdEndRendering(Context.CommandBuffer); -} - -void VulkanSceneRenderer::RecordComputePass( - const RenderContext &Context, - const std::vector &ComputeSubmissions) { - for (const auto &VisibleSubmission : ComputeSubmissions) { - std::array DescriptorSets = { - Context.Frame.ComputeFrameDescriptorSet, VisibleSubmission.Mesh->DescriptorSet}; - - vkCmdBindPipeline(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - Context.MeshProjectPipeline); - vkCmdBindDescriptorSets(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - Context.MeshProjectPipelineLayout, 0, - static_cast(DescriptorSets.size()), - DescriptorSets.data(), 0, VK_NULL_HANDLE); - - MeshProjectPushConstants ProjectPushConstants{}; - ProjectPushConstants.Model = VisibleSubmission.Submission->Transform; - ProjectPushConstants.Counts.x = VisibleSubmission.Mesh->VertexCount; - vkCmdPushConstants(Context.CommandBuffer, Context.MeshProjectPipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(MeshProjectPushConstants), &ProjectPushConstants); - - const uint32_t VertexGroupCount = - std::max(1u, (VisibleSubmission.Mesh->VertexCount + 63u) / 64u); - vkCmdDispatch(Context.CommandBuffer, VertexGroupCount, 1, 1); - - VkBufferMemoryBarrier2 ProjectedVertexBarrier{ - .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .buffer = VisibleSubmission.Mesh->ProjectedVertexBuffer.Buffer, - .offset = 0, - .size = VisibleSubmission.Mesh->ProjectedVertexBuffer.Size}; - VkDependencyInfo ProjectDependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .bufferMemoryBarrierCount = 1, - .pBufferMemoryBarriers = &ProjectedVertexBarrier}; - vkCmdPipelineBarrier2(Context.CommandBuffer, &ProjectDependencyInfo); - - vkCmdBindPipeline(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - Context.MeshPipeline); - vkCmdBindDescriptorSets(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, - Context.MeshPipelineLayout, 0, - static_cast(DescriptorSets.size()), - DescriptorSets.data(), 0, VK_NULL_HANDLE); - - MeshRasterPushConstants RasterPushConstants{}; - RasterPushConstants.Counts.x = VisibleSubmission.Mesh->TriangleCount; - vkCmdPushConstants(Context.CommandBuffer, Context.MeshPipelineLayout, - VK_SHADER_STAGE_COMPUTE_BIT, 0, - sizeof(MeshRasterPushConstants), &RasterPushConstants); - - const uint32_t GroupCount = - std::max(1u, (VisibleSubmission.Mesh->TriangleCount + 63u) / 64u); - vkCmdDispatch(Context.CommandBuffer, GroupCount, 1, 1); - - VkImageMemoryBarrier2 DrawImageBarrier{ - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .dstAccessMask = - VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_GENERAL, - .newLayout = VK_IMAGE_LAYOUT_GENERAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = Context.DrawImage.Image, - .subresourceRange = - VkInit::ImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT)}; - VkDependencyInfo ComputeDependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .imageMemoryBarrierCount = 1, - .pImageMemoryBarriers = &DrawImageBarrier}; - vkCmdPipelineBarrier2(Context.CommandBuffer, &ComputeDependencyInfo); - } -} - -void VulkanSceneRenderer::RecordGraphicsPass( - const RenderContext &Context, const VkViewport &Viewport, - const VkRect2D &Scissor, - const std::vector &GraphicsSubmissions, - const VkDescriptorBufferInfo &CameraBufferInfo, bool ForceWireframe) { - if (GraphicsSubmissions.empty()) { - return; - } - - VkRenderingAttachmentInfo ColorAttachment = - VkInit::AttachmentInfo(Context.DrawImage.ImageView, VK_NULL_HANDLE, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); - VkRenderingAttachmentInfo DepthAttachment{ - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .pNext = VK_NULL_HANDLE, - .imageView = Context.RasterDepthImage.ImageView, - .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE}; - VkRenderingInfo RenderingInfo = - VkInit::RenderingInfo(Context.DrawExtent, &ColorAttachment, &DepthAttachment); - - vkCmdBeginRendering(Context.CommandBuffer, &RenderingInfo); - vkCmdBindPipeline(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - ForceWireframe ? Context.MeshWireframePipeline - : Context.MeshGraphicsPipeline); - vkCmdSetViewport(Context.CommandBuffer, 0, 1, &Viewport); - vkCmdSetScissor(Context.CommandBuffer, 0, 1, &Scissor); - - for (size_t SubmissionIndex = 0; SubmissionIndex < GraphicsSubmissions.size(); - ++SubmissionIndex) { - const auto &VisibleSubmission = GraphicsSubmissions[SubmissionIndex]; - VkDescriptorSet GraphicsDescriptorSet = - Context.Frame.GraphicsFrameDescriptorSets[SubmissionIndex]; - UpdateGraphicsFrameDescriptors( - Context, GraphicsDescriptorSet, - Context.MaterialResources.ResolveMaterialTextureView( - VisibleSubmission.Submission->Material), - CameraBufferInfo); - vkCmdBindDescriptorSets(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - Context.MeshGraphicsPipelineLayout, 0, 1, - &GraphicsDescriptorSet, 0, VK_NULL_HANDLE); - MeshGraphicsPushConstants PushConstants{}; - PushConstants.Model = VisibleSubmission.Submission->Transform; - if (VisibleSubmission.Submission->Material) { - PushConstants.BaseColorFactor = VisibleSubmission.Submission->Material->BaseColorFactor; - PushConstants.Metallic = VisibleSubmission.Submission->Material->Metallic; - PushConstants.Roughness = VisibleSubmission.Submission->Material->Roughness; - } - vkCmdPushConstants(Context.CommandBuffer, Context.MeshGraphicsPipelineLayout, - VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, - sizeof(MeshGraphicsPushConstants), &PushConstants); - BindMeshBuffers(Context.CommandBuffer, VisibleSubmission.Mesh); - vkCmdDrawIndexed(Context.CommandBuffer, VisibleSubmission.Mesh->IndexCount, 1, - 0, 0, 0); - } - - vkCmdEndRendering(Context.CommandBuffer); -} - -void VulkanSceneRenderer::RecordTranslucentGraphicsPass( - const RenderContext &Context, const VkViewport &Viewport, - const VkRect2D &Scissor, - const std::vector &GraphicsSubmissions, - const VkDescriptorBufferInfo &CameraBufferInfo, bool ForceWireframe) { - if (GraphicsSubmissions.empty()) { - return; - } - - VkRenderingAttachmentInfo ColorAttachment = - VkInit::AttachmentInfo(Context.DrawImage.ImageView, VK_NULL_HANDLE, - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); - VkRenderingAttachmentInfo DepthAttachment{ - .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, - .pNext = VK_NULL_HANDLE, - .imageView = Context.RasterDepthImage.ImageView, - .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, - .storeOp = VK_ATTACHMENT_STORE_OP_STORE}; - VkRenderingInfo RenderingInfo = - VkInit::RenderingInfo(Context.DrawExtent, &ColorAttachment, &DepthAttachment); - - vkCmdBeginRendering(Context.CommandBuffer, &RenderingInfo); - vkCmdBindPipeline(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - ForceWireframe ? Context.MeshWireframePipeline - : Context.MeshGraphicsAlphaBlendPipeline); - vkCmdSetViewport(Context.CommandBuffer, 0, 1, &Viewport); - vkCmdSetScissor(Context.CommandBuffer, 0, 1, &Scissor); - - for (size_t SubmissionIndex = 0; SubmissionIndex < GraphicsSubmissions.size(); - ++SubmissionIndex) { - const auto &VisibleSubmission = GraphicsSubmissions[SubmissionIndex]; - VkDescriptorSet GraphicsDescriptorSet = - Context.Frame.GraphicsFrameDescriptorSets[SubmissionIndex]; - UpdateGraphicsFrameDescriptors( - Context, GraphicsDescriptorSet, - Context.MaterialResources.ResolveMaterialTextureView( - VisibleSubmission.Submission->Material), - CameraBufferInfo); - vkCmdBindDescriptorSets(Context.CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, - Context.MeshGraphicsPipelineLayout, 0, 1, - &GraphicsDescriptorSet, 0, VK_NULL_HANDLE); - MeshGraphicsPushConstants PushConstants{}; - PushConstants.Model = VisibleSubmission.Submission->Transform; - if (VisibleSubmission.Submission->Material) { - PushConstants.BaseColorFactor = - VisibleSubmission.Submission->Material->BaseColorFactor; - PushConstants.Metallic = VisibleSubmission.Submission->Material->Metallic; - PushConstants.Roughness = - VisibleSubmission.Submission->Material->Roughness; - } - vkCmdPushConstants(Context.CommandBuffer, Context.MeshGraphicsPipelineLayout, - VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, - sizeof(MeshGraphicsPushConstants), &PushConstants); - BindMeshBuffers(Context.CommandBuffer, VisibleSubmission.Mesh); - vkCmdDrawIndexed(Context.CommandBuffer, VisibleSubmission.Mesh->IndexCount, 1, - 0, 0, 0); - } - - vkCmdEndRendering(Context.CommandBuffer); -} - -void VulkanSceneRenderer::RenderScenePasses(const RenderContext &Context) const { - auto &Camera = *Context.Scene.ActiveCamera; - CameraFrameUniform CameraData = BuildCameraData(Context); - std::memcpy(Context.Frame.CameraBuffer.Info.pMappedData, &CameraData, - sizeof(CameraFrameUniform)); - - VkDescriptorBufferInfo CameraBufferInfo = VkInit::BufferInfo( - Context.Frame.CameraBuffer.Buffer, 0, Context.Frame.CameraBuffer.Size); - UpdateComputeFrameDescriptors(Context, CameraBufferInfo); - UpdateDepthFrameDescriptors(Context, CameraBufferInfo); - - const size_t SubmissionCount = - std::min(Context.Scene.Submissions.size(), - static_cast(MaxMeshSubmissionsPerFrame)); - const bool ForceWireframe = Context.ViewMode == RendererViewMode::Wireframe; - if (Context.Scene.Submissions.size() > MaxMeshSubmissionsPerFrame) { - Context.WarnOnce("Scene submitted meshes exceeding MaxMeshSubmissionsPerFrame."); - } - - Context.FrameStats.SubmittedMeshCount = static_cast(SubmissionCount); - Context.FrameStats.FrustumCulledMeshCount = 0; - Context.FrameStats.OcclusionCulledMeshCount = 0; - Context.FrameStats.MeshSubmissionCount = 0; - Context.FrameStats.TriangleCount = 0; - - std::vector Candidates; - std::vector OpaqueGraphicsSubmissions; - std::vector TranslucentGraphicsSubmissions; - std::vector ComputeSubmissions; - Candidates.reserve(SubmissionCount); - OpaqueGraphicsSubmissions.reserve(SubmissionCount); - TranslucentGraphicsSubmissions.reserve(SubmissionCount); - ComputeSubmissions.reserve(SubmissionCount); - - for (size_t Index = 0; Index < SubmissionCount; ++Index) { - const auto &Submission = Context.Scene.Submissions[Index]; - auto VulkanMeshRef = std::dynamic_pointer_cast(Submission.Mesh); - if (!VulkanMeshRef) { - continue; - } - - if (!ForceWireframe && - !Context.OcclusionCulling.IsBoundsVisible( - CameraData.ViewProjection, Submission.Transform, - VulkanMeshRef->BoundsMin, VulkanMeshRef->BoundsMax)) { - ++Context.FrameStats.FrustumCulledMeshCount; - continue; - } - - const glm::vec3 WorldCenter = ComputeWorldCenter(Submission, *VulkanMeshRef); - const glm::vec3 Delta = WorldCenter - Camera.GetPosition(); - Candidates.push_back({.Submission = &Submission, - .Mesh = std::move(VulkanMeshRef), - .SortDepth = glm::dot(Delta, Delta)}); - } - - if (!ForceWireframe) { - std::sort(Candidates.begin(), Candidates.end(), - [](const CandidateSubmission &Left, - const CandidateSubmission &Right) { - return Left.SortDepth < Right.SortDepth; - }); - } - - const MeshFrameResources *PreviousOcclusionFrame = - ForceWireframe - ? nullptr - : Context.OcclusionCulling.GetPreviousOcclusionFrame( - Context.CommandContext, Context.MeshFrames, Context.FrameNumber); - bool UseOcclusion = false; - if (PreviousOcclusionFrame != nullptr) { - vmaInvalidateAllocation(Context.Allocator, - PreviousOcclusionFrame->HzbReadbackBuffer.Allocation, 0, - VK_WHOLE_SIZE); - UseOcclusion = Context.OcclusionCulling.ShouldUsePreviousOcclusionData( - *PreviousOcclusionFrame, CameraData); - } - - for (const CandidateSubmission &Candidate : Candidates) { - if (UseOcclusion && - Context.OcclusionCulling.IsOccludedByPreviousFrame( - *PreviousOcclusionFrame, Candidate.Submission->Transform, - Candidate.Mesh->BoundsMin, Candidate.Mesh->BoundsMax, - Context.HzbMipExtents, Context.HzbMipOffsets)) { - ++Context.FrameStats.OcclusionCulledMeshCount; - continue; - } - - VisibleMeshSubmission VisibleSubmission{ - .Submission = Candidate.Submission, - .Mesh = Candidate.Mesh, - .SortDepth = Candidate.SortDepth, - }; - if (!ForceWireframe && - Candidate.Submission->RenderPath == MeshRenderPath::Compute) { - ComputeSubmissions.push_back(VisibleSubmission); - } else if (Candidate.Submission->Translucent) { - TranslucentGraphicsSubmissions.push_back(VisibleSubmission); - } else { - OpaqueGraphicsSubmissions.push_back(VisibleSubmission); - } - - ++Context.FrameStats.MeshSubmissionCount; - Context.FrameStats.TriangleCount += Candidate.Mesh->TriangleCount; - } - - VkViewport Viewport{0.0f, 0.0f, static_cast(Context.DrawExtent.width), - static_cast(Context.DrawExtent.height), 0.0f, 1.0f}; - VkRect2D Scissor{{0, 0}, Context.DrawExtent}; - - RecordDepthPrepass(Context, Viewport, Scissor, OpaqueGraphicsSubmissions, - ComputeSubmissions); - - Context.BuildHzb(Context.CommandBuffer, Context.Frame); - Context.Frame.HzbViewProjection = CameraData.ViewProjection; - Context.Frame.HzbViewportSize = glm::vec2(CameraData.ViewportSize); - - if (ComputeSubmissions.empty()) { - RecordGraphicsPass(Context, Viewport, Scissor, OpaqueGraphicsSubmissions, - CameraBufferInfo, ForceWireframe); - if (!TranslucentGraphicsSubmissions.empty()) { - std::sort(TranslucentGraphicsSubmissions.begin(), - TranslucentGraphicsSubmissions.end(), - [](const VisibleMeshSubmission &Left, - const VisibleMeshSubmission &Right) { - return Left.SortDepth > Right.SortDepth; - }); - RecordTranslucentGraphicsPass(Context, Viewport, Scissor, - TranslucentGraphicsSubmissions, - CameraBufferInfo, ForceWireframe); - } - return; - } - - VkImageMemoryBarrier2 RasterDepthToReadBarrier{ - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT, - .srcAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, - .oldLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = Context.RasterDepthImage.Image, - .subresourceRange = { - .aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1}}; - VkImageMemoryBarrier2 DrawImageToGeneralBarrier{ - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, - .pNext = VK_NULL_HANDLE, - .srcStageMask = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, - .srcAccessMask = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, - .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, - .dstAccessMask = - VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_GENERAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = Context.DrawImage.Image, - .subresourceRange = - VkInit::ImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT)}; - std::array ToComputeBarriers = { - RasterDepthToReadBarrier, DrawImageToGeneralBarrier}; - VkDependencyInfo ToComputeDependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .imageMemoryBarrierCount = - static_cast(ToComputeBarriers.size()), - .pImageMemoryBarriers = ToComputeBarriers.data()}; - vkCmdPipelineBarrier2(Context.CommandBuffer, &ToComputeDependencyInfo); - - RecordComputePass(Context, ComputeSubmissions); - - VkImageMemoryBarrier2 DrawImageToColorAttachmentBarrier = - DrawImageToGeneralBarrier; - DrawImageToColorAttachmentBarrier.srcStageMask = - VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; - DrawImageToColorAttachmentBarrier.srcAccessMask = - VK_ACCESS_2_SHADER_WRITE_BIT; - DrawImageToColorAttachmentBarrier.dstStageMask = - VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; - DrawImageToColorAttachmentBarrier.dstAccessMask = - VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT; - DrawImageToColorAttachmentBarrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL; - DrawImageToColorAttachmentBarrier.newLayout = - VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - - VkImageMemoryBarrier2 RasterDepthToAttachmentBarrier = - RasterDepthToReadBarrier; - RasterDepthToAttachmentBarrier.srcStageMask = - VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; - RasterDepthToAttachmentBarrier.srcAccessMask = VK_ACCESS_2_SHADER_READ_BIT; - RasterDepthToAttachmentBarrier.dstStageMask = - VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | - VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; - RasterDepthToAttachmentBarrier.dstAccessMask = - VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | - VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; - RasterDepthToAttachmentBarrier.oldLayout = - VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; - RasterDepthToAttachmentBarrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; - - std::array ToGraphicsBarriers = { - DrawImageToColorAttachmentBarrier, RasterDepthToAttachmentBarrier}; - VkDependencyInfo ToGraphicsDependencyInfo{ - .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, - .pNext = VK_NULL_HANDLE, - .imageMemoryBarrierCount = - static_cast(ToGraphicsBarriers.size()), - .pImageMemoryBarriers = ToGraphicsBarriers.data()}; - vkCmdPipelineBarrier2(Context.CommandBuffer, &ToGraphicsDependencyInfo); - - RecordGraphicsPass(Context, Viewport, Scissor, OpaqueGraphicsSubmissions, - CameraBufferInfo, ForceWireframe); - if (!TranslucentGraphicsSubmissions.empty()) { - std::sort(TranslucentGraphicsSubmissions.begin(), - TranslucentGraphicsSubmissions.end(), - [](const VisibleMeshSubmission &Left, - const VisibleMeshSubmission &Right) { - return Left.SortDepth > Right.SortDepth; - }); - RecordTranslucentGraphicsPass(Context, Viewport, Scissor, - TranslucentGraphicsSubmissions, - CameraBufferInfo, ForceWireframe); - } -} -} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanSceneRenderer.h b/Axiom/Renderer/Vulkan/VulkanSceneRenderer.h deleted file mode 100644 index cd961b76..00000000 --- a/Axiom/Renderer/Vulkan/VulkanSceneRenderer.h +++ /dev/null @@ -1,100 +0,0 @@ -#pragma once - -#include "Renderer/RendererBackend.h" -#include "Renderer/RenderScene.h" -#include "Renderer/Vulkan/VulkanMaterialResources.h" -#include "Renderer/Vulkan/VulkanOcclusionCulling.h" -#include "Renderer/Vulkan/VulkanRendererTypes.h" - -#include -#include - -namespace Axiom { -class VulkanMesh; - -class VulkanSceneRenderer { -public: - struct RenderContext { - VkCommandBuffer CommandBuffer{VK_NULL_HANDLE}; - RenderScene &Scene; - MeshFrameResources &Frame; - RendererFrameStats &FrameStats; - const std::array &MeshFrames; - const VulkanCommandContext &CommandContext; - VulkanMaterialResources &MaterialResources; - const VulkanOcclusionCulling &OcclusionCulling; - VkDevice Device{VK_NULL_HANDLE}; - VmaAllocator Allocator{nullptr}; - uint64_t FrameNumber{0}; - VkExtent2D DrawExtent{}; - RendererViewMode ViewMode{RendererViewMode::Lit}; - AllocatedImage DrawImage; - AllocatedImage RasterDepthImage; - VkSampler LinearDepthSampler{VK_NULL_HANDLE}; - VkSampler TextureSampler{VK_NULL_HANDLE}; - VkPipeline MeshProjectPipeline{VK_NULL_HANDLE}; - VkPipelineLayout MeshProjectPipelineLayout{VK_NULL_HANDLE}; - VkPipeline MeshPipeline{VK_NULL_HANDLE}; - VkPipelineLayout MeshPipelineLayout{VK_NULL_HANDLE}; - VkPipeline MeshGraphicsPipeline{VK_NULL_HANDLE}; - VkPipeline MeshGraphicsAlphaBlendPipeline{VK_NULL_HANDLE}; - VkPipelineLayout MeshGraphicsPipelineLayout{VK_NULL_HANDLE}; - VkPipeline MeshWireframePipeline{VK_NULL_HANDLE}; - VkPipeline MeshDepthPipeline{VK_NULL_HANDLE}; - VkPipelineLayout MeshDepthPipelineLayout{VK_NULL_HANDLE}; - const std::vector &HzbMipExtents; - const std::vector &HzbMipOffsets; - std::function BuildHzb; - std::function WarnOnce; - }; - - void RenderScenePasses(const RenderContext &Context) const; - -private: - struct VisibleMeshSubmission { - const RenderMeshSubmission *Submission{nullptr}; - std::shared_ptr Mesh; - float SortDepth{0.0f}; - }; - - struct CandidateSubmission { - const RenderMeshSubmission *Submission{nullptr}; - std::shared_ptr Mesh; - float SortDepth{0.0f}; - }; - - static CameraFrameUniform BuildCameraData(const RenderContext &Context); - static glm::vec3 ComputeWorldCenter(const RenderMeshSubmission &Submission, - const VulkanMesh &Mesh); - static void BindMeshBuffers(VkCommandBuffer CommandBuffer, - const std::shared_ptr &MeshRef); - static void UpdateComputeFrameDescriptors(const RenderContext &Context, - const VkDescriptorBufferInfo - &CameraBufferInfo); - static void UpdateDepthFrameDescriptors(const RenderContext &Context, - const VkDescriptorBufferInfo - &CameraBufferInfo); - static void UpdateGraphicsFrameDescriptors( - const RenderContext &Context, VkDescriptorSet GraphicsDescriptorSet, - VkImageView TextureView, - const VkDescriptorBufferInfo &CameraBufferInfo); - static void RecordDepthPrepass( - const RenderContext &Context, const VkViewport &Viewport, - const VkRect2D &Scissor, - const std::vector &GraphicsSubmissions, - const std::vector &ComputeSubmissions); - static void RecordComputePass( - const RenderContext &Context, - const std::vector &ComputeSubmissions); - static void RecordGraphicsPass( - const RenderContext &Context, const VkViewport &Viewport, - const VkRect2D &Scissor, - const std::vector &GraphicsSubmissions, - const VkDescriptorBufferInfo &CameraBufferInfo, bool ForceWireframe); - static void RecordTranslucentGraphicsPass( - const RenderContext &Context, const VkViewport &Viewport, - const VkRect2D &Scissor, - const std::vector &GraphicsSubmissions, - const VkDescriptorBufferInfo &CameraBufferInfo, bool ForceWireframe); -}; -} // namespace Axiom diff --git a/Axiom/Scene/CoreInstance/Instance.cpp b/Axiom/Scene/CoreInstance/Instance.cpp new file mode 100644 index 00000000..be15b1b5 --- /dev/null +++ b/Axiom/Scene/CoreInstance/Instance.cpp @@ -0,0 +1,114 @@ +#include "CoreInstance/Instance.h" + +#include "CoreInstance/InstancePool.h" + +namespace Axiom { +void Instance::SetParent(InstanceHandle Parent) { + if (m_Parent == Parent || IsDestroyed() || m_Pool == nullptr) { + return; + } + if (Parent && m_Pool->Resolve(Parent) == nullptr) { + return; + } + + InstanceHandle AncestorHandle = Parent; + while (AncestorHandle) { + if (AncestorHandle == m_Self) { + return; + } + + const Instance *Ancestor = m_Pool->Resolve(AncestorHandle); + if (Ancestor == nullptr) { + break; + } + AncestorHandle = Ancestor->GetParent(); + } + + if (m_Parent) { + if (Instance *OldParent = m_Pool->Resolve(m_Parent); OldParent != nullptr) { + OldParent->RemoveChildInternal(m_Self); + } + } + + m_Parent = Parent; + + if (m_Parent) { + if (Instance *NewParent = m_Pool->Resolve(m_Parent); NewParent != nullptr) { + NewParent->AddChildInternal(m_Self); + } + } +} + +std::string Instance::GetFullName() const { + std::vector Chain; + const Instance *Current = this; + size_t TotalSize = 0; + + while (Current != nullptr) { + Chain.push_back(Current); + TotalSize += Current->GetName().size(); + + if (!Current->m_Parent || Current->m_Pool == nullptr) { + break; + } + Current = Current->m_Pool->Resolve(Current->m_Parent); + } + + if (Chain.empty()) { + return {}; + } + + TotalSize += Chain.size() - 1; + std::string Output; + Output.reserve(TotalSize); + + for (auto It = Chain.rbegin(); It != Chain.rend(); ++It) { + if (!Output.empty()) { + Output.push_back('.'); + } + Output.append((*It)->GetName()); + } + + return Output; +} + +InstanceHandle Instance::FindFirstChild(const std::string &Name) const { + if (m_Pool == nullptr) { + return {}; + } + + for (const InstanceHandle ChildHandle : m_Children) { + const Instance *Child = m_Pool->Resolve(ChildHandle); + if (Child != nullptr && Child->GetName() == Name) { + return ChildHandle; + } + } + + return {}; +} + +void Instance::BindToPool(InstancePool &Pool, InstanceHandle Self) { + m_Pool = &Pool; + m_Self = Self; + m_Parent = {}; + m_Children.clear(); + m_IsDestroyed = false; +} + +void Instance::AddChildInternal(InstanceHandle Child) { + m_Children.push_back(Child); + OnChildAdded(Child); +} + +void Instance::RemoveChildInternal(InstanceHandle Child) { + const auto It = std::find(m_Children.begin(), m_Children.end(), Child); + if (It != m_Children.end()) { + m_Children.erase(It); + OnChildRemoved(Child); + } +} + +const Instance *Instance::ResolveHandle(InstanceHandle Handle) const { + return m_Pool != nullptr ? m_Pool->Resolve(Handle) : nullptr; +} +} // namespace Axiom diff --git a/Axiom/Scene/CoreInstance/Instance.h b/Axiom/Scene/CoreInstance/Instance.h new file mode 100644 index 00000000..941d436a --- /dev/null +++ b/Axiom/Scene/CoreInstance/Instance.h @@ -0,0 +1,84 @@ +#pragma once + +#include "CoreInstance/InstanceHandle.h" +#include "CoreInstance/InstanceType.h" + +#include +#include +#include +#include + +namespace Axiom { +class InstancePool; + +#define AX_INSTANCE_BODY(TypeName) \ + static constexpr InstanceType StaticType() { return InstanceType::TypeName; } \ + InstanceType GetType() const override { return StaticType(); } + +class Instance { +public: + static constexpr InstanceType StaticType() { return InstanceType::Instance; } + + Instance() = default; + explicit Instance(const std::string &Name) : m_Name(Name) {} + virtual ~Instance() = default; + + Instance(const Instance &) = delete; + void operator=(const Instance &) = delete; + + void SetName(const std::string &Name) { m_Name = Name; } + const std::string &GetName() const { return m_Name; } + + void SetParent(InstanceHandle Parent); + InstanceHandle GetParent() const { return m_Parent; } + const std::vector &GetChildren() const { return m_Children; } + + virtual InstanceType GetType() const = 0; + std::string GetFullName() const; + + bool IsDestroyed() const { return m_IsDestroyed; } + InstanceHandle GetHandle() const { return m_Self; } + + template bool IsA() const { + static_assert(std::is_base_of_v, "T must inherit from Instance"); + return GetType() == T::StaticType(); + } + + InstanceHandle FindFirstChild(const std::string &Name) const; + + template InstanceHandle FindFirstChildOfClass() const { + static_assert(std::is_base_of_v, "T must inherit from Instance"); + for (const InstanceHandle ChildHandle : m_Children) { + const Instance *Child = ResolveHandle(ChildHandle); + if (Child != nullptr && Child->GetType() == T::StaticType()) { + return ChildHandle; + } + } + + return {}; + } + + virtual void OnCreate() {} + virtual void OnDestroy() {} + + virtual void OnChildAdded(InstanceHandle Child) { (void)Child; } + virtual void OnChildRemoved(InstanceHandle Child) { (void)Child; } + +protected: + friend class InstancePool; + + void BindToPool(InstancePool &Pool, InstanceHandle Self); + +private: + void AddChildInternal(InstanceHandle Child); + void RemoveChildInternal(InstanceHandle Child); + const Instance *ResolveHandle(InstanceHandle Handle) const; + + std::string m_Name = "Instance"; + InstancePool *m_Pool = nullptr; + InstanceHandle m_Self{}; + InstanceHandle m_Parent{}; + std::vector m_Children; + bool m_IsDestroyed = false; +}; +} // namespace Axiom diff --git a/Axiom/Scene/CoreInstance/InstanceHandle.h b/Axiom/Scene/CoreInstance/InstanceHandle.h new file mode 100644 index 00000000..8f23e0d8 --- /dev/null +++ b/Axiom/Scene/CoreInstance/InstanceHandle.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace Axiom { +struct InstanceHandle { + uint32_t Index{0}; + uint32_t Generation{0}; + + constexpr explicit operator bool() const { return Index != 0; } + constexpr bool operator==(const InstanceHandle &) const = default; +}; +} // namespace Axiom diff --git a/Axiom/Scene/CoreInstance/InstancePool.cpp b/Axiom/Scene/CoreInstance/InstancePool.cpp new file mode 100644 index 00000000..d1bbe315 --- /dev/null +++ b/Axiom/Scene/CoreInstance/InstancePool.cpp @@ -0,0 +1,199 @@ +#include "CoreInstance/InstancePool.h" + +#include "CoreInstance/Instance.h" + +namespace Axiom { +InstancePool::InstancePool(InstancePool &&Other) noexcept + : m_DataModels(std::move(Other.m_DataModels)), + m_Folders(std::move(Other.m_Folders)), + m_MeshObjects(std::move(Other.m_MeshObjects)), + m_Lights(std::move(Other.m_Lights)), + m_Cameras(std::move(Other.m_Cameras)), + m_Actors(std::move(Other.m_Actors)), + m_Registry(std::move(Other.m_Registry)), + m_FreeRegistryEntries(std::move(Other.m_FreeRegistryEntries)) { + RebindPoolPointers(); +} + +InstancePool &InstancePool::operator=(InstancePool &&Other) noexcept { + if (this == &Other) { + return *this; + } + + m_DataModels = std::move(Other.m_DataModels); + m_Folders = std::move(Other.m_Folders); + m_MeshObjects = std::move(Other.m_MeshObjects); + m_Lights = std::move(Other.m_Lights); + m_Cameras = std::move(Other.m_Cameras); + m_Actors = std::move(Other.m_Actors); + m_Registry = std::move(Other.m_Registry); + m_FreeRegistryEntries = std::move(Other.m_FreeRegistryEntries); + RebindPoolPointers(); + return *this; +} + +void InstancePool::Destroy(InstanceHandle Handle) { + Instance *Object = Resolve(Handle); + if (Object == nullptr || Object->m_IsDestroyed) { + return; + } + + Object->m_IsDestroyed = true; + Object->OnDestroy(); + + if (Object->m_Parent) { + if (Instance *Parent = Resolve(Object->m_Parent); Parent != nullptr) { + Parent->RemoveChildInternal(Handle); + } + Object->m_Parent = {}; + } + + const std::vector Children = Object->m_Children; + Object->m_Children.clear(); + + for (const InstanceHandle ChildHandle : Children) { + if (Instance *Child = Resolve(ChildHandle); Child != nullptr) { + Child->m_Parent = {}; + Destroy(ChildHandle); + } + } + + const uint32_t RegistryIndex = Handle.Index - 1; + RegistryEntry &Entry = m_Registry[RegistryIndex]; + DispatchByType(Entry.Type, [&](auto &Arena) { Arena.Destroy(Entry.LocalIndex); }); + Entry.Occupied = false; + Entry.Generation = DispatchByType( + Entry.Type, [&](const auto &Arena) { return Arena.GetGeneration(Entry.LocalIndex); }); + m_FreeRegistryEntries.push_back(RegistryIndex); +} + +Instance *InstancePool::Resolve(InstanceHandle Handle) { + const RegistryEntry *Entry = FindRegistryEntry(Handle); + if (Entry == nullptr) { + return nullptr; + } + + return DispatchByType(Entry->Type, [&](auto &Arena) -> Instance * { + return Arena.Resolve(Entry->LocalIndex); + }); +} + +const Instance *InstancePool::Resolve(InstanceHandle Handle) const { + const RegistryEntry *Entry = FindRegistryEntry(Handle); + if (Entry == nullptr) { + return nullptr; + } + + return DispatchByType(Entry->Type, [&](const auto &Arena) -> const Instance * { + return Arena.Resolve(Entry->LocalIndex); + }); +} + +uint32_t InstancePool::AllocateRegistryEntry(InstanceType Type, uint32_t LocalIndex, + uint32_t Generation) { + if (!m_FreeRegistryEntries.empty()) { + const uint32_t Index = m_FreeRegistryEntries.back(); + m_FreeRegistryEntries.pop_back(); + m_Registry[Index] = RegistryEntry{ + .Type = Type, + .LocalIndex = LocalIndex, + .Generation = Generation, + .Occupied = true, + }; + return Index; + } + + const uint32_t Index = static_cast(m_Registry.size()); + m_Registry.push_back(RegistryEntry{ + .Type = Type, + .LocalIndex = LocalIndex, + .Generation = Generation, + .Occupied = true, + }); + return Index; +} + +const InstancePool::RegistryEntry * +InstancePool::FindRegistryEntry(InstanceHandle Handle) const { + if (!Handle || Handle.Index - 1 >= m_Registry.size()) { + return nullptr; + } + + const RegistryEntry &Entry = m_Registry[Handle.Index - 1]; + if (!Entry.Occupied || Entry.Generation != Handle.Generation) { + return nullptr; + } + return &Entry; +} + +void InstancePool::RebindPoolPointers() { + for (uint32_t Index = 0; Index < m_Registry.size(); ++Index) { + RegistryEntry &Entry = m_Registry[Index]; + if (!Entry.Occupied) { + continue; + } + + Instance *Object = DispatchByType(Entry.Type, [&](auto &Arena) -> Instance * { + return Arena.Resolve(Entry.LocalIndex); + }); + if (Object != nullptr) { + Object->m_Pool = this; + } + } +} + +template <> InstancePool::TypedSlabArena &InstancePool::GetArena() { + return m_DataModels; +} +template <> +InstancePool::TypedSlabArena &InstancePool::GetArena() { + return m_Folders; +} +template <> +InstancePool::TypedSlabArena & +InstancePool::GetArena() { + return m_MeshObjects; +} +template <> InstancePool::TypedSlabArena &InstancePool::GetArena() { + return m_Lights; +} +template <> +InstancePool::TypedSlabArena &InstancePool::GetArena() { + return m_Cameras; +} +template <> InstancePool::TypedSlabArena &InstancePool::GetArena() { + return m_Actors; +} + +template <> +const InstancePool::TypedSlabArena & +InstancePool::GetArena() const { + return m_DataModels; +} +template <> +const InstancePool::TypedSlabArena & +InstancePool::GetArena() const { + return m_Folders; +} +template <> +const InstancePool::TypedSlabArena & +InstancePool::GetArena() const { + return m_MeshObjects; +} +template <> +const InstancePool::TypedSlabArena & +InstancePool::GetArena() const { + return m_Lights; +} +template <> +const InstancePool::TypedSlabArena & +InstancePool::GetArena() const { + return m_Cameras; +} +template <> +const InstancePool::TypedSlabArena & +InstancePool::GetArena() const { + return m_Actors; +} + +} // namespace Axiom diff --git a/Axiom/Scene/CoreInstance/InstancePool.h b/Axiom/Scene/CoreInstance/InstancePool.h new file mode 100644 index 00000000..4d5b54e4 --- /dev/null +++ b/Axiom/Scene/CoreInstance/InstancePool.h @@ -0,0 +1,223 @@ +#pragma once + +#include "CoreInstance/InstanceHandle.h" +#include "CoreInstance/InstanceType.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Axiom { +class Instance; +class DataModel; +class SceneFolder; +class SceneMeshObject; +class SceneLight; +class SceneCamera; +class SceneActor; + +class InstancePool { +public: + InstancePool() = default; + InstancePool(InstancePool &&Other) noexcept; + InstancePool(const InstancePool &) = delete; + InstancePool &operator=(const InstancePool &) = delete; + InstancePool &operator=(InstancePool &&Other) noexcept; + + template + InstanceHandle Create(const std::string &Name = "Instance"); + + void Destroy(InstanceHandle Handle); + + Instance *Resolve(InstanceHandle Handle); + const Instance *Resolve(InstanceHandle Handle) const; + + template T *ResolveAs(InstanceHandle Handle); + template const T *ResolveAs(InstanceHandle Handle) const; + +private: + template class TypedSlabArena { + public: + TypedSlabArena() = default; + TypedSlabArena(TypedSlabArena &&) noexcept = default; + TypedSlabArena(const TypedSlabArena &) = delete; + TypedSlabArena &operator=(const TypedSlabArena &) = delete; + TypedSlabArena &operator=(TypedSlabArena &&) noexcept = default; + + ~TypedSlabArena() { + for (uint32_t Index = 0; Index < m_Size; ++Index) { + Slot &Entry = SlotAt(Index); + if (Entry.Occupied) { + Entry.Ptr()->~T(); + } + } + } + + std::pair Construct(const std::string &Name) { + const uint32_t Index = AllocateSlot(); + Slot &Entry = SlotAt(Index); + new (Entry.Storage) T(Name); + Entry.Occupied = true; + return {Index, Entry.Ptr()}; + } + + void Destroy(uint32_t Index) { + Slot &Entry = SlotAt(Index); + if (!Entry.Occupied) { + return; + } + + Entry.Ptr()->~T(); + Entry.Occupied = false; + ++Entry.Generation; + m_FreeSlots.push_back(Index); + } + + T *Resolve(uint32_t Index) { + Slot &Entry = SlotAt(Index); + return Entry.Occupied ? Entry.Ptr() : nullptr; + } + + const T *Resolve(uint32_t Index) const { + const Slot &Entry = SlotAt(Index); + return Entry.Occupied ? Entry.Ptr() : nullptr; + } + + uint32_t GetGeneration(uint32_t Index) const { return SlotAt(Index).Generation; } + + private: + struct Slot { + alignas(T) std::byte Storage[sizeof(T)]; + uint32_t Generation{1}; + bool Occupied{false}; + + T *Ptr() { return reinterpret_cast(Storage); } + const T *Ptr() const { return reinterpret_cast(Storage); } + }; + + uint32_t AllocateSlot() { + if (!m_FreeSlots.empty()) { + const uint32_t Index = m_FreeSlots.back(); + m_FreeSlots.pop_back(); + return Index; + } + + const uint32_t Index = m_Size++; + if (Index / SlabCapacity >= m_Slabs.size()) { + m_Slabs.push_back(std::make_unique(SlabCapacity)); + } + return Index; + } + + Slot &SlotAt(uint32_t Index) { + return m_Slabs[Index / SlabCapacity][Index % SlabCapacity]; + } + + const Slot &SlotAt(uint32_t Index) const { + return m_Slabs[Index / SlabCapacity][Index % SlabCapacity]; + } + + std::vector> m_Slabs; + std::vector m_FreeSlots; + uint32_t m_Size{0}; + }; + + struct RegistryEntry { + InstanceType Type{InstanceType::Instance}; + uint32_t LocalIndex{0}; + uint32_t Generation{0}; + bool Occupied{false}; + }; + + template TypedSlabArena &GetArena(); + template const TypedSlabArena &GetArena() const; + + uint32_t AllocateRegistryEntry(InstanceType Type, uint32_t LocalIndex, + uint32_t Generation); + const RegistryEntry *FindRegistryEntry(InstanceHandle Handle) const; + + template decltype(auto) DispatchByType(InstanceType Type, Fn &&Function); + template + decltype(auto) DispatchByType(InstanceType Type, Fn &&Function) const; + void RebindPoolPointers(); + + TypedSlabArena m_DataModels; + TypedSlabArena m_Folders; + TypedSlabArena m_MeshObjects; + TypedSlabArena m_Lights; + TypedSlabArena m_Cameras; + TypedSlabArena m_Actors; + std::vector m_Registry; + std::vector m_FreeRegistryEntries; +}; +} // namespace Axiom + +#include "CoreInstance/Instance.h" +#include "CoreInstance/SceneInstances.h" + +namespace Axiom { + +template +InstanceHandle InstancePool::Create(const std::string &Name) { + static_assert(std::is_base_of_v, "T must inherit from Instance"); + + auto &Arena = GetArena(); + const auto [LocalIndex, Object] = Arena.Construct(Name); + const uint32_t RegistryIndex = + AllocateRegistryEntry(T::StaticType(), LocalIndex, + Arena.GetGeneration(LocalIndex)); + const InstanceHandle Handle{RegistryIndex + 1, m_Registry[RegistryIndex].Generation}; + + Object->BindToPool(*this, Handle); + Object->OnCreate(); + return Handle; +} + +template T *InstancePool::ResolveAs(InstanceHandle Handle) { + Instance *Resolved = Resolve(Handle); + if (Resolved == nullptr || Resolved->GetType() != T::StaticType()) { + return nullptr; + } + return static_cast(Resolved); +} + +template const T *InstancePool::ResolveAs(InstanceHandle Handle) const { + const Instance *Resolved = Resolve(Handle); + if (Resolved == nullptr || Resolved->GetType() != T::StaticType()) { + return nullptr; + } + return static_cast(Resolved); +} + +template +decltype(auto) InstancePool::DispatchByType(InstanceType Type, Fn &&Function) { + switch (Type) { + case InstanceType::DataModel: return Function(m_DataModels); + case InstanceType::SceneFolder: return Function(m_Folders); + case InstanceType::SceneMeshObject: return Function(m_MeshObjects); + case InstanceType::SceneLight: return Function(m_Lights); + case InstanceType::SceneCamera: return Function(m_Cameras); + case InstanceType::SceneActor: return Function(m_Actors); + case InstanceType::Instance: return Function(m_DataModels); + } + return Function(m_DataModels); +} + +template +decltype(auto) InstancePool::DispatchByType(InstanceType Type, Fn &&Function) const { + switch (Type) { + case InstanceType::DataModel: return Function(m_DataModels); + case InstanceType::SceneFolder: return Function(m_Folders); + case InstanceType::SceneMeshObject: return Function(m_MeshObjects); + case InstanceType::SceneLight: return Function(m_Lights); + case InstanceType::SceneCamera: return Function(m_Cameras); + case InstanceType::SceneActor: return Function(m_Actors); + case InstanceType::Instance: return Function(m_DataModels); + } + return Function(m_DataModels); +} +} // namespace Axiom diff --git a/Axiom/Scene/CoreInstance/InstanceType.h b/Axiom/Scene/CoreInstance/InstanceType.h new file mode 100644 index 00000000..3c200423 --- /dev/null +++ b/Axiom/Scene/CoreInstance/InstanceType.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace Axiom { +enum class InstanceType : uint16_t { + Instance, + DataModel, + SceneFolder, + SceneMeshObject, + SceneLight, + SceneCamera, + SceneActor, +}; +} // namespace Axiom diff --git a/Axiom/CoreInstance/SceneInstances.h b/Axiom/Scene/CoreInstance/SceneInstances.h similarity index 75% rename from Axiom/CoreInstance/SceneInstances.h rename to Axiom/Scene/CoreInstance/SceneInstances.h index 2e94710e..dea84b43 100644 --- a/Axiom/CoreInstance/SceneInstances.h +++ b/Axiom/Scene/CoreInstance/SceneInstances.h @@ -6,38 +6,38 @@ namespace Axiom { class DataModel final : public Instance { public: - DataModel() : Instance("DataModel") {} - GENERATED_BODY(DataModel) + explicit DataModel(const std::string &Name = "DataModel") : Instance(Name) {} + AX_INSTANCE_BODY(DataModel) }; class SceneFolder final : public Instance { public: explicit SceneFolder(const std::string &Name = "Folder") : Instance(Name) {} - GENERATED_BODY(SceneFolder) + AX_INSTANCE_BODY(SceneFolder) }; class SceneMeshObject final : public Instance { public: explicit SceneMeshObject(const std::string &Name = "Mesh") : Instance(Name) {} - GENERATED_BODY(SceneMeshObject) + AX_INSTANCE_BODY(SceneMeshObject) }; class SceneLight final : public Instance { public: explicit SceneLight(const std::string &Name = "Light") : Instance(Name) {} - GENERATED_BODY(SceneLight) + AX_INSTANCE_BODY(SceneLight) }; class SceneCamera final : public Instance { public: explicit SceneCamera(const std::string &Name = "Camera") : Instance(Name) {} - GENERATED_BODY(SceneCamera) + AX_INSTANCE_BODY(SceneCamera) }; class SceneActor final : public Instance { public: explicit SceneActor(const std::string &Name = "Actor") : Instance(Name) {} - GENERATED_BODY(SceneActor) + AX_INSTANCE_BODY(SceneActor) }; } // namespace Axiom diff --git a/Axiom/Project/ProjectSystem.cpp b/Axiom/Scene/Project/ProjectSystem.cpp similarity index 81% rename from Axiom/Project/ProjectSystem.cpp rename to Axiom/Scene/Project/ProjectSystem.cpp index 29b9e119..4310935a 100644 --- a/Axiom/Project/ProjectSystem.cpp +++ b/Axiom/Scene/Project/ProjectSystem.cpp @@ -6,6 +6,10 @@ #include "Assets/SceneFile.h" #include "Core/Log.h" +#include +#include +#include + #include #include #include @@ -38,163 +42,14 @@ constexpr std::string_view kDefaultStarterScriptClassName = "StarterScript"; constexpr std::string_view kCsProjectTypeGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; -std::string EscapeJsonString(std::string_view Value) { - std::string Result; - Result.reserve(Value.size() + 4); - for (const char Character : Value) { - switch (Character) { - case '\\': - Result += "\\\\"; - break; - case '"': - Result += "\\\""; - break; - case '\n': - Result += "\\n"; - break; - case '\r': - Result += "\\r"; - break; - case '\t': - Result += "\\t"; - break; - default: - Result.push_back(Character); - break; - } - } - return Result; +std::string SerializePrettyJson(const rapidjson::Document &Document) { + rapidjson::StringBuffer Buffer; + rapidjson::PrettyWriter Writer(Buffer); + Writer.SetIndent(' ', 2); + Document.Accept(Writer); + return std::string(Buffer.GetString(), Buffer.GetSize()) + '\n'; } -class JsonObjectParser { -public: - explicit JsonObjectParser(std::string_view Text) : m_Text(Text) {} - - bool ParseObject(std::unordered_map &Out) { - SkipWhitespace(); - if (!Consume('{')) { - return false; - } - - SkipWhitespace(); - if (Consume('}')) { - return true; - } - - while (m_Index < m_Text.size()) { - const auto Key = ParseString(); - if (!Key.has_value()) { - return false; - } - - SkipWhitespace(); - if (!Consume(':')) { - return false; - } - - SkipWhitespace(); - std::string Value; - if (Peek() == '"') { - const auto Parsed = ParseString(); - if (!Parsed.has_value()) { - return false; - } - Value = *Parsed; - } else { - const size_t Start = m_Index; - while (m_Index < m_Text.size()) { - const char Character = m_Text[m_Index]; - if (Character == ',' || Character == '}' || - std::isspace(static_cast(Character))) { - break; - } - ++m_Index; - } - Value = std::string(m_Text.substr(Start, m_Index - Start)); - } - Out.emplace(*Key, std::move(Value)); - - SkipWhitespace(); - if (Consume('}')) { - return true; - } - if (!Consume(',')) { - return false; - } - SkipWhitespace(); - } - - return false; - } - -private: - std::optional ParseString() { - if (!Consume('"')) { - return std::nullopt; - } - - std::string Result; - while (m_Index < m_Text.size()) { - const char Character = m_Text[m_Index++]; - if (Character == '"') { - return Result; - } - if (Character == '\\') { - if (m_Index >= m_Text.size()) { - return std::nullopt; - } - const char Escaped = m_Text[m_Index++]; - switch (Escaped) { - case '\\': - case '"': - case '/': - Result.push_back(Escaped); - break; - case 'n': - Result.push_back('\n'); - break; - case 'r': - Result.push_back('\r'); - break; - case 't': - Result.push_back('\t'); - break; - default: - return std::nullopt; - } - continue; - } - Result.push_back(Character); - } - return std::nullopt; - } - - void SkipWhitespace() { - while (m_Index < m_Text.size() && - std::isspace(static_cast(m_Text[m_Index]))) { - ++m_Index; - } - } - - bool Consume(char Expected) { - if (Peek() != Expected) { - return false; - } - ++m_Index; - return true; - } - - char Peek() const { - if (m_Index >= m_Text.size()) { - return '\0'; - } - return m_Text[m_Index]; - } - - std::string_view m_Text; - size_t m_Index{0}; -}; - std::string BuildProjectId(std::string_view Slug) { // Stable enough for v1 scaffold creation without adding a UUID dependency. std::hash Hasher; @@ -572,24 +427,53 @@ bool CopyDirectoryTree(const std::filesystem::path &Source, bool SavePackageManifestFile(const ProjectDescriptor &Project, const ProjectPackageResult &PackageResult) { - std::ostringstream Stream; - Stream << "{\n" - << " \"version\": 1,\n" - << " \"projectId\": \"" << EscapeJsonString(Project.Manifest.ProjectId) - << "\",\n" - << " \"name\": \"" << EscapeJsonString(Project.Manifest.Name) << "\",\n" - << " \"slug\": \"" << EscapeJsonString(Project.Manifest.Slug) << "\",\n" - << " \"contentMode\": \"cooked-only-v1\",\n" - << " \"sceneAsset\": \"Content/Cooked/scene.wscene\",\n" - << " \"cookedDir\": \"Content/Cooked\",\n" - << " \"assetCookManifest\": \"Content/Cooked/AssetCookManifest.json\",\n" - << " \"engineContentDir\": \"Content/Engine\",\n" - << " \"cookedSourceAssetCount\": " - << PackageResult.Cook.CookedSourceAssetCount << ",\n" - << " \"manifestEntryCount\": " << PackageResult.Cook.ManifestEntryCount - << "\n" - << "}\n"; - return WriteTextFile(Project.Output.PackageManifestPath, Stream.str()); + rapidjson::Document Document; + Document.SetObject(); + auto &Allocator = Document.GetAllocator(); + + Document.AddMember("version", 1u, Allocator); + Document.AddMember( + "projectId", + rapidjson::Value(Project.Manifest.ProjectId.c_str(), + static_cast( + Project.Manifest.ProjectId.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember( + "name", + rapidjson::Value(Project.Manifest.Name.c_str(), + static_cast(Project.Manifest.Name.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember( + "slug", + rapidjson::Value(Project.Manifest.Slug.c_str(), + static_cast(Project.Manifest.Slug.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember("contentMode", "cooked-only-v1", Allocator); + Document.AddMember("sceneAsset", "Content/Cooked/scene.wscene", Allocator); + Document.AddMember("cookedDir", "Content/Cooked", Allocator); + Document.AddMember("assetCookManifest", + "Content/Cooked/AssetCookManifest.json", Allocator); + Document.AddMember("engineContentDir", "Content/Engine", Allocator); + Document.AddMember( + "cookedSourceAssetCount", + rapidjson::Value() + .SetUint64( + static_cast(PackageResult.Cook.CookedSourceAssetCount)), + Allocator); + Document.AddMember( + "manifestEntryCount", + rapidjson::Value().SetUint64( + static_cast(PackageResult.Cook.ManifestEntryCount)), + Allocator); + + return WriteTextFile(Project.Output.PackageManifestPath, + SerializePrettyJson(Document)); } } // namespace @@ -749,24 +633,51 @@ bool SaveProjectManifest(const std::filesystem::path &ManifestPath, return false; } - std::ofstream File(ManifestPath); - if (!File.is_open()) { - A_CORE_ERROR("ProjectSystem: failed to open manifest '{}'", - ManifestPath.string()); - return false; - } - - File << "{\n" - << " \"version\": " << Manifest.Version << ",\n" - << " \"projectId\": \"" << EscapeJsonString(Manifest.ProjectId) << "\",\n" - << " \"name\": \"" << EscapeJsonString(Manifest.Name) << "\",\n" - << " \"slug\": \"" << EscapeJsonString(Manifest.Slug) << "\",\n" - << " \"scriptAssemblyName\": \"" - << EscapeJsonString(Manifest.ScriptAssemblyName) << "\",\n" - << " \"scriptRootNamespace\": \"" - << EscapeJsonString(Manifest.ScriptRootNamespace) << "\"\n" - << "}\n"; - return File.good(); + rapidjson::Document Document; + Document.SetObject(); + auto &Allocator = Document.GetAllocator(); + + Document.AddMember("version", Manifest.Version, Allocator); + Document.AddMember( + "projectId", + rapidjson::Value(Manifest.ProjectId.c_str(), + static_cast( + Manifest.ProjectId.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember( + "name", + rapidjson::Value(Manifest.Name.c_str(), + static_cast(Manifest.Name.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember( + "slug", + rapidjson::Value(Manifest.Slug.c_str(), + static_cast(Manifest.Slug.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember( + "scriptAssemblyName", + rapidjson::Value( + Manifest.ScriptAssemblyName.c_str(), + static_cast(Manifest.ScriptAssemblyName.size()), + Allocator) + .Move(), + Allocator); + Document.AddMember( + "scriptRootNamespace", + rapidjson::Value( + Manifest.ScriptRootNamespace.c_str(), + static_cast(Manifest.ScriptRootNamespace.size()), + Allocator) + .Move(), + Allocator); + + return WriteTextFile(ManifestPath, SerializePrettyJson(Document)); } std::optional @@ -776,44 +687,56 @@ LoadProjectManifest(const std::filesystem::path &ManifestPath) { return std::nullopt; } - JsonObjectParser Parser(Text); - std::unordered_map Fields; - if (!Parser.ParseObject(Fields)) { + rapidjson::Document Document; + Document.ParseInsitu(Text.data()); + if (Document.HasParseError() || !Document.IsObject()) { A_CORE_WARN("ProjectSystem: failed to parse manifest '{}'", ManifestPath.string()); return std::nullopt; } - const auto VersionIt = Fields.find("version"); - const auto ProjectIdIt = Fields.find("projectId"); - const auto NameIt = Fields.find("name"); - const auto SlugIt = Fields.find("slug"); - if (VersionIt == Fields.end() || ProjectIdIt == Fields.end() || - NameIt == Fields.end() || SlugIt == Fields.end()) { + const auto VersionIt = Document.FindMember("version"); + const auto ProjectIdIt = Document.FindMember("projectId"); + const auto NameIt = Document.FindMember("name"); + const auto SlugIt = Document.FindMember("slug"); + if (VersionIt == Document.MemberEnd() || ProjectIdIt == Document.MemberEnd() || + NameIt == Document.MemberEnd() || SlugIt == Document.MemberEnd() || + !VersionIt->value.IsUint() || !ProjectIdIt->value.IsString() || + !NameIt->value.IsString() || !SlugIt->value.IsString()) { return std::nullopt; } - const auto Version = ParseUint32(VersionIt->second); - if (!Version.has_value() || !IsValidProjectSlug(SlugIt->second)) { + const auto Version = ParseUint32( + std::to_string(static_cast(VersionIt->value.GetUint()))); + const std::string_view Slug(SlugIt->value.GetString(), + SlugIt->value.GetStringLength()); + const std::string_view Name(NameIt->value.GetString(), + NameIt->value.GetStringLength()); + if (!Version.has_value() || !IsValidProjectSlug(Slug)) { return std::nullopt; } return ProjectManifest{ .Version = *Version, - .ProjectId = ProjectIdIt->second, - .Name = NameIt->second, - .Slug = SlugIt->second, - .ScriptAssemblyName = [&Fields, &NameIt]() { - const auto ScriptAssemblyIt = Fields.find("scriptAssemblyName"); - return ScriptAssemblyIt != Fields.end() - ? ScriptAssemblyIt->second - : BuildScriptAssemblyName(NameIt->second); + .ProjectId = std::string(ProjectIdIt->value.GetString(), + ProjectIdIt->value.GetStringLength()), + .Name = std::string(Name), + .Slug = std::string(Slug), + .ScriptAssemblyName = [&Document, Name]() { + const auto ScriptAssemblyIt = Document.FindMember("scriptAssemblyName"); + return ScriptAssemblyIt != Document.MemberEnd() && + ScriptAssemblyIt->value.IsString() + ? std::string(ScriptAssemblyIt->value.GetString(), + ScriptAssemblyIt->value.GetStringLength()) + : BuildScriptAssemblyName(Name); }(), - .ScriptRootNamespace = [&Fields, &NameIt]() { - const auto ScriptNamespaceIt = Fields.find("scriptRootNamespace"); - return ScriptNamespaceIt != Fields.end() - ? ScriptNamespaceIt->second - : BuildScriptRootNamespace(NameIt->second); + .ScriptRootNamespace = [&Document, Name]() { + const auto ScriptNamespaceIt = Document.FindMember("scriptRootNamespace"); + return ScriptNamespaceIt != Document.MemberEnd() && + ScriptNamespaceIt->value.IsString() + ? std::string(ScriptNamespaceIt->value.GetString(), + ScriptNamespaceIt->value.GetStringLength()) + : BuildScriptRootNamespace(Name); }(), }; } @@ -827,37 +750,24 @@ bool SaveDefaultSceneFile(const std::filesystem::path &SceneFilePath) { return false; } - std::ofstream File(SceneFilePath); - if (!File.is_open()) { - A_CORE_ERROR("ProjectSystem: failed to open scene file '{}'", - SceneFilePath.string()); - return false; - } - - File << "{\n" - << " \"version\": 1,\n" - << " \"meshAsset\": \"\",\n" - << " \"nodes\": [\n" - << " {\n" - << " \"id\": \"world\",\n" - << " \"parentId\": null,\n" - << " \"displayName\": \"World\",\n" - << " \"kind\": \"Folder\",\n" - << " \"visible\": true\n" - << " }\n" - << " ],\n" - << " \"objects\": [\n" - << " {\n" - << " \"id\": \"world\",\n" - << " \"displayName\": \"World\",\n" - << " \"kind\": \"Folder\",\n" - << " \"visible\": true,\n" - << " \"supportsTransform\": false,\n" - << " \"transformReadOnly\": true\n" - << " }\n" - << " ]\n" - << "}\n"; - return File.good(); + EditorSceneState Scene; + Scene.Items = {{ + .Id = "world", + .DisplayName = "World", + .Kind = EditorSceneItemKind::Folder, + .Visible = true, + }}; + Scene.ObjectDetailsById.emplace( + "world", + EditorObjectDetails{ + .ObjectId = "world", + .DisplayName = "World", + .Kind = EditorSceneItemKind::Folder, + .Visible = true, + .SupportsTransform = false, + .TransformReadOnly = true, + }); + return Assets::SaveSceneToFile(SceneFilePath, Scene); } std::optional diff --git a/Axiom/Project/ProjectSystem.h b/Axiom/Scene/Project/ProjectSystem.h similarity index 100% rename from Axiom/Project/ProjectSystem.h rename to Axiom/Scene/Project/ProjectSystem.h diff --git a/Axiom/Session/BufferedEditorInputSource.cpp b/Axiom/Scene/Session/BufferedEditorInputSource.cpp similarity index 100% rename from Axiom/Session/BufferedEditorInputSource.cpp rename to Axiom/Scene/Session/BufferedEditorInputSource.cpp diff --git a/Axiom/Session/BufferedEditorInputSource.h b/Axiom/Scene/Session/BufferedEditorInputSource.h similarity index 100% rename from Axiom/Session/BufferedEditorInputSource.h rename to Axiom/Scene/Session/BufferedEditorInputSource.h diff --git a/Axiom/Session/EditorCommand.h b/Axiom/Scene/Session/EditorCommand.h similarity index 100% rename from Axiom/Session/EditorCommand.h rename to Axiom/Scene/Session/EditorCommand.h diff --git a/Axiom/Scene/Session/EditorCommandDispatcher.cpp b/Axiom/Scene/Session/EditorCommandDispatcher.cpp new file mode 100644 index 00000000..7d7b9059 --- /dev/null +++ b/Axiom/Scene/Session/EditorCommandDispatcher.cpp @@ -0,0 +1,451 @@ +#include "Session/EditorCommandDispatcher.h" + +#include "Assets/AssetCooker.h" +#include "Assets/MeshAsset.h" +#include "Session/EditorSceneStateManager.h" +#include "Session/EditorSessionValidationModule.h" + +#include + +#include + +#include + +namespace Axiom { +namespace { +std::string CommandTypeName(const EditorCommandPayload &Payload) { + if (std::holds_alternative(Payload)) return "update_viewport_camera"; + if (std::holds_alternative(Payload)) return "set_viewport_camera_pose"; + if (std::holds_alternative(Payload)) return "set_camera_projection"; + if (std::holds_alternative(Payload)) return "set_look_active"; + if (std::holds_alternative(Payload)) return "select_object"; + if (std::holds_alternative(Payload)) return "rename_object"; + if (std::holds_alternative(Payload)) return "set_object_visibility"; + if (std::holds_alternative(Payload)) return "create_object"; + if (std::holds_alternative(Payload)) return "create_mesh_object"; + if (std::holds_alternative(Payload)) return "duplicate_object"; + if (std::holds_alternative(Payload)) return "delete_object"; + if (std::holds_alternative(Payload)) return "reparent_object"; + if (std::holds_alternative(Payload)) return "attach_script"; + if (std::holds_alternative(Payload)) return "detach_script"; + if (std::holds_alternative(Payload)) return "set_mesh_asset"; + if (std::holds_alternative(Payload)) return "set_light_properties"; + if (std::holds_alternative(Payload)) return "set_material_properties"; + if (std::holds_alternative(Payload)) return "set_material_texture"; + if (std::holds_alternative(Payload)) return "set_physics_properties"; + if (std::holds_alternative(Payload)) return "play_session"; + if (std::holds_alternative(Payload)) return "pause_session"; + if (std::holds_alternative(Payload)) return "resume_session"; + if (std::holds_alternative(Payload)) return "stop_session"; + if (std::holds_alternative(Payload)) return "set_world_settings"; + if (std::holds_alternative(Payload)) return "place_actor"; + return "set_transform"; +} + +bool ShouldPublishCommandAcknowledgedEvent(const EditorCommandPayload &Payload) { + return !std::holds_alternative(Payload); +} + +bool IsNearlyZero(const glm::vec3 &Value) { + return glm::dot(Value, Value) <= 0.0f; +} + +std::string DefaultUserDisplayName(SessionUserId User) { + if (User.Value == 1) { + return "Host"; + } + return "User " + std::to_string(User.Value - 1); +} +} // namespace + +EditorCommandDispatcher::EditorCommandDispatcher(EditorSession &Session) + : m_Session(Session) {} + +void EditorCommandDispatcher::ProcessCommand( + const QueuedEditorCommand &QueuedCommand) { + std::string FailureReason; + if (!m_Session.ValidateCommand(QueuedCommand, FailureReason)) { + m_Session.PublishEvent({.Payload = CommandRejectedEvent{ + .User = QueuedCommand.Context.User, + .RejectedCommand = QueuedCommand.Id, + .Reason = FailureReason, + }}); + return; + } + + m_Session.EnsureViewport(QueuedCommand.Context.User); + std::visit( + [this, &QueuedCommand](const auto &Command) { + HandleCommand(QueuedCommand, Command); + }, + QueuedCommand.Command.Payload); + + if (ShouldPublishCommandAcknowledgedEvent(QueuedCommand.Command.Payload)) { + m_Session.PublishEvent({.Payload = CommandAcknowledgedEvent{ + .User = QueuedCommand.Context.User, + .AcknowledgedCommand = QueuedCommand.Id, + .CommandType = + CommandTypeName(QueuedCommand.Command.Payload), + }}); + } +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const UpdateViewportCameraCommand &Command) { + EditorViewportState &Viewport = m_Session.EnsureViewport(QueuedCommand.Context.User); + + bool CameraChanged = false; + if (!IsNearlyZero(Command.WorldMovement)) { + Viewport.Camera.MoveWorld(Command.WorldMovement); + CameraChanged = true; + } + + if (Viewport.IsLooking && Command.CursorPosition.has_value()) { + if (Viewport.HasLastCursorPosition) { + const glm::dvec2 Delta = *Command.CursorPosition - Viewport.LastCursorPosition; + if (Delta.x != 0.0 || Delta.y != 0.0) { + Viewport.Camera.SetRotation( + Viewport.Camera.GetYawDegrees() + + static_cast(Delta.x) * + m_Session.GetConfig().MouseSensitivity, + Viewport.Camera.GetPitchDegrees() - + static_cast(Delta.y) * + m_Session.GetConfig().MouseSensitivity); + CameraChanged = true; + } + } + + Viewport.LastCursorPosition = *Command.CursorPosition; + Viewport.HasLastCursorPosition = true; + } + + if (CameraChanged) { + m_Session.PublishEvent({.Payload = ViewportCameraUpdatedEvent{ + .User = QueuedCommand.Context.User, + .Position = Viewport.Camera.GetPosition(), + .YawDegrees = Viewport.Camera.GetYawDegrees(), + .PitchDegrees = Viewport.Camera.GetPitchDegrees(), + }}); + } +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetViewportCameraPoseCommand &Command) { + EditorViewportState &Viewport = m_Session.EnsureViewport(QueuedCommand.Context.User); + Viewport.Camera.SetPosition(Command.Position); + Viewport.Camera.SetRotation(Command.YawDegrees, Command.PitchDegrees); + m_Session.PublishEvent({.Payload = ViewportCameraUpdatedEvent{ + .User = QueuedCommand.Context.User, + .Position = Viewport.Camera.GetPosition(), + .YawDegrees = Viewport.Camera.GetYawDegrees(), + .PitchDegrees = Viewport.Camera.GetPitchDegrees(), + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetCameraProjectionCommand &Command) { + EditorViewportState &Viewport = m_Session.EnsureViewport(QueuedCommand.Context.User); + Viewport.ProjectionType = Command.ProjectionType; + if (Command.ProjectionType == CameraProjectionType::Orthographic) { + Viewport.Camera.SetOrthographic( + Viewport.OrthoHeight, m_Session.GetConfig().CameraAspectRatio, + m_Session.GetConfig().CameraNearPlane, + m_Session.GetConfig().CameraFarPlane); + } else { + Viewport.Camera.SetPerspective( + m_Session.GetConfig().CameraVerticalFovDegrees, + m_Session.GetConfig().CameraAspectRatio, + m_Session.GetConfig().CameraNearPlane, + m_Session.GetConfig().CameraFarPlane); + } +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetLookActiveCommand &Command) { + EditorViewportState &Viewport = m_Session.EnsureViewport(QueuedCommand.Context.User); + const bool StateChanged = Viewport.IsLooking != Command.IsLooking; + Viewport.IsLooking = Command.IsLooking; + + if (Command.IsLooking && Command.CursorPosition.has_value()) { + Viewport.LastCursorPosition = *Command.CursorPosition; + Viewport.HasLastCursorPosition = true; + } else if (!Command.IsLooking) { + Viewport.HasLastCursorPosition = false; + } + + if (StateChanged) { + m_Session.PublishEvent({.Payload = LookStateChangedEvent{ + .User = QueuedCommand.Context.User, + .IsLooking = Viewport.IsLooking, + }}); + } +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SelectObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const SceneObjectHandle Handle = m_Session.ResolveObjectHandle(Command.ObjectId); + if (!Handle) { + return; + } + if (m_Session.HasSelectedObjectHandle(QueuedCommand.Context.User, Handle)) { + return; + } + + m_Session.SetSelectedObject(QueuedCommand.Context.User, Command.ObjectId, + Handle); + m_Session.PublishEvent({.Payload = SelectionChangedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = Command.ObjectId, + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const RenameObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + if (Details->DisplayName == Command.DisplayName) return; + + Details->DisplayName = Command.DisplayName; + m_Session.UpdateSceneItemDisplayName(Command.ObjectId, Command.DisplayName); + m_Session.PublishEvent({.Payload = ObjectRenamedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = Command.ObjectId, + .DisplayName = Command.DisplayName, + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetObjectVisibilityCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + if (Details->Visible == Command.Visible) return; + + Details->Visible = Command.Visible; + m_Session.UpdateSceneItemVisibility(Command.ObjectId, Command.Visible); + m_Session.PublishEvent({.Payload = ObjectVisibilityChangedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = Command.ObjectId, + .Visible = Command.Visible, + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const CreateObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const InstanceHandle WorldFolder = m_Session.EnsureWorldFolder(); + if (!WorldFolder) return; + + const EditorSceneItemKind Kind = + Command.TemplateId == "Mesh" ? EditorSceneItemKind::Mesh : + Command.TemplateId == "Light" ? EditorSceneItemKind::Light : + Command.TemplateId == "Camera" ? EditorSceneItemKind::Camera : + Command.TemplateId == "Actor" ? EditorSceneItemKind::Actor : + EditorSceneItemKind::Folder; + const std::string ObjectId = + m_Session.BuildUniqueObjectId(Command.TemplateId); + const std::string DisplayName = + m_Session.BuildUniqueDisplayName(Command.TemplateId); + const bool Transformable = Kind != EditorSceneItemKind::Folder; + const std::optional InitTransform = + Transformable ? std::optional{EditorTransformDetails{}} : std::nullopt; + + m_Session.InsertObjectDetails(EditorObjectDetails{ + .Handle = m_Session.EnsureHandleForObjectId(ObjectId), + .ObjectId = ObjectId, + .DisplayName = DisplayName, + .Kind = Kind, + .Visible = true, + .SupportsTransform = Transformable, + .TransformReadOnly = false, + .Transform = InitTransform, + .WorldTransform = InitTransform, + }); + + if (const InstanceHandle Node = + m_Session.CreateInstanceForTemplate(Command.TemplateId, ObjectId); + Node) { + Instance *NodePtr = m_Session.GetInstancePool().Resolve(Node); + if (NodePtr == nullptr) return; + NodePtr->SetParent(WorldFolder); + } + + m_Session.SyncItemsFromTree(); + m_Session.RebuildSceneHandleState(); + m_Session.PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = ObjectId, + .DisplayName = DisplayName, + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const CreateMeshObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const InstanceHandle WorldFolder = m_Session.EnsureWorldFolder(); + if (!WorldFolder) return; + + const std::string ObjectId = + m_Session.BuildUniqueObjectId("Mesh"); + const std::string DisplayName = + m_Session.BuildUniqueDisplayName("Mesh"); + const EditorTransformDetails Transform{ + .Location = Command.Location, + .RotationDegrees = Command.RotationDegrees, + .Scale = Command.Scale, + }; + + m_Session.InsertObjectDetails(EditorObjectDetails{ + .Handle = m_Session.EnsureHandleForObjectId(ObjectId), + .ObjectId = ObjectId, + .DisplayName = DisplayName, + .Kind = EditorSceneItemKind::Mesh, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = Transform, + .WorldTransform = Transform, + }); + + if (const InstanceHandle Node = + m_Session.CreateInstanceForTemplate("Mesh", ObjectId); + Node) { + Instance *NodePtr = m_Session.GetInstancePool().Resolve(Node); + if (NodePtr == nullptr) return; + NodePtr->SetParent(WorldFolder); + } + + m_Session.SyncItemsFromTree(); + m_Session.RebuildSceneHandleState(); + m_Session.PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = ObjectId, + .DisplayName = DisplayName, + }}); + + HandleCommand(QueuedCommand, SetMeshAssetCommand{ + .ObjectId = ObjectId, + .AssetPath = Command.AssetPath, + }); + HandleCommand(QueuedCommand, SetTransformCommand{ + .ObjectId = ObjectId, + .Location = Command.Location, + .RotationDegrees = Command.RotationDegrees, + .Scale = Command.Scale, + }); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const DuplicateObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const InstanceHandle Source = m_Session.FindInstanceById(Command.ObjectId); + if (!Source) return; + const Instance *SourceNode = m_Session.GetInstancePool().Resolve(Source); + if (SourceNode == nullptr) return; + const InstanceHandle Parent = SourceNode->GetParent(); + if (!Parent) return; + + std::vector NewDetails; + m_Session.DeepCloneSubtree(Source, Parent, NewDetails); + m_Session.SyncItemsFromTree(); + m_Session.RebuildSceneHandleState(); + if (!NewDetails.empty()) { + m_Session.PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = NewDetails.front().ObjectId, + .DisplayName = NewDetails.front().DisplayName, + }}); + } +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const DeleteObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const InstanceHandle Target = m_Session.FindInstanceById(Command.ObjectId); + if (!Target) return; + + for (const std::string &Id : + m_Session.CollectDescendantIds(Target)) { + m_Session.RemoveSceneObject(Id); + m_Session.ClearSelectionsForObject(Id); + } + + m_Session.GetInstancePool().Destroy(Target); + m_Session.SyncItemsFromTree(); + m_Session.RebuildSceneHandleState(); + m_Session.PublishEvent({.Payload = ObjectDeletedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = Command.ObjectId, + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const ReparentObjectCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const InstanceHandle Target = m_Session.FindInstanceById(Command.ObjectId); + const InstanceHandle NewParent = m_Session.FindInstanceById(Command.NewParentId); + if (!Target || !NewParent) return; + Instance *TargetNode = m_Session.GetInstancePool().Resolve(Target); + if (TargetNode == nullptr) return; + if (TargetNode->GetParent() == NewParent) return; + + TargetNode->SetParent(NewParent); + m_Session.SyncItemsFromTree(); + m_Session.RebuildSceneHandleState(); + m_Session.RecomputeSubtreeWorldTransforms(Target); + m_Session.PublishEvent({.Payload = ObjectReparentedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = Command.ObjectId, + .NewParentId = Command.NewParentId, + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetTransformCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + m_Session.ApplyWorldTransform( + Command.ObjectId, + EditorTransformDetails{ + .Location = Command.Location, + .RotationDegrees = Command.RotationDegrees, + .Scale = Command.Scale, + }, + QueuedCommand.Context.User, true); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &, + const AttachScriptCommand &Command) { + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + Details->ScriptClass = Command.ScriptClassName; + A_CORE_INFO("EditorSession: attached script '{}' to '{}'", + Command.ScriptClassName, Command.ObjectId); + m_Session.PublishEvent({ScriptClassChangedEvent{ + .ObjectId = Command.ObjectId, + .ScriptClass = Command.ScriptClassName, + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &, + const DetachScriptCommand &Command) { + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + Details->ScriptClass = std::nullopt; + A_CORE_INFO("EditorSession: detached script from '{}'", Command.ObjectId); + m_Session.PublishEvent({ScriptClassChangedEvent{ + .ObjectId = Command.ObjectId, + .ScriptClass = std::nullopt, + }}); +} + +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorCommandDispatcher.h b/Axiom/Scene/Session/EditorCommandDispatcher.h new file mode 100644 index 00000000..5a21ba2d --- /dev/null +++ b/Axiom/Scene/Session/EditorCommandDispatcher.h @@ -0,0 +1,68 @@ +#pragma once + +#include "Session/EditorSession.h" + +namespace Axiom { +class EditorCommandDispatcher { +public: + explicit EditorCommandDispatcher(EditorSession &Session); + + void ProcessCommand(const QueuedEditorCommand &QueuedCommand); + +private: + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const UpdateViewportCameraCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetViewportCameraPoseCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetCameraProjectionCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetLookActiveCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SelectObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const RenameObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetObjectVisibilityCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const CreateObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const CreateMeshObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const DuplicateObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const DeleteObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const ReparentObjectCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetTransformCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const AttachScriptCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const DetachScriptCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetMeshAssetCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetLightPropertiesCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetMaterialPropertiesCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetMaterialTextureCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetPhysicsPropertiesCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PlaySessionCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PauseSessionCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const ResumeSessionCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const StopSessionCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetWorldSettingsCommand &Command); + void HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PlaceActorCommand &Command); + + EditorSession &m_Session; +}; +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorCommandDispatcherRuntime.cpp b/Axiom/Scene/Session/EditorCommandDispatcherRuntime.cpp new file mode 100644 index 00000000..1c51c886 --- /dev/null +++ b/Axiom/Scene/Session/EditorCommandDispatcherRuntime.cpp @@ -0,0 +1,341 @@ +#include "Session/EditorCommandDispatcher.h" + +#include "Assets/AssetCooker.h" +#include "Assets/MeshAsset.h" +#include "Session/EditorSceneStateManager.h" + +#include + +#include + +namespace Axiom { +namespace { +void CookMeshAssetBestEffort(const std::filesystem::path &ContentDir, + std::string_view RelativeAssetPath) { + if (ContentDir.empty() || RelativeAssetPath.empty()) { + return; + } + + const auto Cooked = Assets::CookMeshAsset(ContentDir, RelativeAssetPath); + if (!Cooked.has_value()) { + A_CORE_WARN("EditorSession: failed to cook mesh asset '{}'", + std::string(RelativeAssetPath)); + } +} + +void CookTextureAssetBestEffort(const std::filesystem::path &ContentDir, + std::string_view RelativeAssetPath) { + if (ContentDir.empty() || RelativeAssetPath.empty()) { + return; + } + + const auto Cooked = Assets::CookTextureAsset(ContentDir, RelativeAssetPath); + if (!Cooked.has_value()) { + A_CORE_WARN("EditorSession: failed to cook texture asset '{}'", + std::string(RelativeAssetPath)); + } +} +} // namespace + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const SetMeshAssetCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + if (m_Session.GetContentDir().empty()) { + A_CORE_WARN("SetMeshAsset: content directory not configured"); + return; + } + + const std::filesystem::path AssetRelative{Command.AssetPath}; + const bool IsEngineAsset = + !AssetRelative.empty() && *AssetRelative.begin() == "Engine"; + std::filesystem::path EffectiveContentDir = m_Session.GetContentDir(); + std::filesystem::path EffectiveRelative = AssetRelative; + if (IsEngineAsset && !m_Session.GetEngineContentDir().empty()) { + EffectiveContentDir = m_Session.GetEngineContentDir(); + auto It = AssetRelative.begin(); + ++It; + EffectiveRelative.clear(); + for (; It != AssetRelative.end(); ++It) { + EffectiveRelative /= *It; + } + } + + CookMeshAssetBestEffort(EffectiveContentDir, EffectiveRelative.string()); + const std::filesystem::path FullPath = EffectiveContentDir / EffectiveRelative; + const auto SceneData = Assets::LoadBasicMeshAsset(FullPath); + if (!SceneData.has_value() || SceneData->Instances.empty()) { + A_CORE_WARN("SetMeshAsset: failed to load '{}' for object '{}'", + Command.AssetPath, Command.ObjectId); + return; + } + + m_Session.ExpandMeshAssetIntoScene(Command.ObjectId, *SceneData, + Command.AssetPath); + m_Session.RecomputeSubtreeWorldTransforms( + m_Session.FindInstanceById(Command.ObjectId)); + + A_CORE_INFO("SetMeshAsset: assigned '{}' to object '{}'", + Command.AssetPath, Command.ObjectId); + m_Session.PublishEvent({.Payload = MeshAssetChangedEvent{ + .ObjectId = Command.ObjectId, + .AssetPath = Command.AssetPath, + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetLightPropertiesCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + + if (!Details->Light.has_value()) { + Details->Light = EditorLightProperties{}; + } + Details->Light->Color = Command.Color; + Details->Light->Intensity = Command.Intensity; + m_Session.PublishEvent({.Payload = LightPropertiesChangedEvent{ + .ObjectId = Command.ObjectId, + .Color = Command.Color, + .Intensity = Command.Intensity, + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetMaterialPropertiesCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + + if (!Details->Material.has_value()) { + Details->Material = EditorMaterialProperties{}; + } + Details->Material->BaseColorFactor = Command.BaseColorFactor; + Details->Material->Metallic = Command.Metallic; + Details->Material->Roughness = Command.Roughness; + + if (EditorSceneMeshInstance *MeshInstance = + m_Session.FindMutableSceneMeshInstance(Command.ObjectId); + MeshInstance != nullptr && MeshInstance->Material) { + MeshInstance->Material->BaseColorFactor = Command.BaseColorFactor; + MeshInstance->Material->Metallic = Command.Metallic; + MeshInstance->Material->Roughness = Command.Roughness; + MarkMaterialInstanceDirty(*MeshInstance->Material); + } + + m_Session.PublishEvent({.Payload = MaterialPropertiesChangedEvent{ + .ObjectId = Command.ObjectId, + .BaseColorFactor = Command.BaseColorFactor, + .Metallic = Command.Metallic, + .Roughness = Command.Roughness, + }}); +} + +void EditorCommandDispatcher::HandleCommand( + const QueuedEditorCommand &QueuedCommand, + const SetMaterialTextureCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + + EditorSceneMeshInstance *MeshInstance = + m_Session.FindMutableSceneMeshInstance(Command.ObjectId); + if (MeshInstance == nullptr || !MeshInstance->Material) { + return; + } + + if (Command.TextureAssetPath.empty()) { + MeshInstance->Material->BaseColorTexture = nullptr; + MeshInstance->Material->TextureAssetPath.clear(); + } else { + if (m_Session.GetContentDir().empty()) { + A_CORE_WARN("SetMaterialTexture: content directory not configured"); + return; + } + CookTextureAssetBestEffort(m_Session.GetContentDir(), Command.TextureAssetPath); + const auto FullPath = m_Session.GetContentDir() / Command.TextureAssetPath; + auto Loaded = Assets::LoadTextureFromFile(FullPath); + if (!Loaded) { + A_CORE_WARN("SetMaterialTexture: failed to load '{}' for object '{}'", + Command.TextureAssetPath, Command.ObjectId); + return; + } + MeshInstance->Material->BaseColorTexture = std::move(Loaded); + MeshInstance->Material->TextureAssetPath = Command.TextureAssetPath; + } + MarkMaterialInstanceDirty(*MeshInstance->Material); + + if (!Details->Material.has_value()) { + Details->Material = EditorMaterialProperties{}; + } + Details->Material->TextureAssetPath = + Command.TextureAssetPath.empty() + ? std::nullopt + : std::optional(Command.TextureAssetPath); + + A_CORE_INFO("SetMaterialTexture: assigned '{}' to object '{}'", + Command.TextureAssetPath, Command.ObjectId); + m_Session.PublishEvent({.Payload = MaterialTextureChangedEvent{ + .ObjectId = Command.ObjectId, + .TextureAssetPath = Command.TextureAssetPath, + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &, + const SetPhysicsPropertiesCommand &Command) { + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Command.ObjectId); + if (Details == nullptr) return; + + Details->Physics = Command.Physics; + if (Command.Physics.BodyType == EditorPhysicsBodyType::None && + Command.Physics.ColliderType == EditorPhysicsColliderType::None) { + Details->Physics.reset(); + } + + m_Session.PublishEvent({.Payload = PhysicsPropertiesChangedEvent{ + .ObjectId = Command.ObjectId, + .Physics = Details->Physics.value_or( + EditorPhysicsProperties{}), + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PlaySessionCommand &) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + m_Session.CaptureRuntimeSceneSnapshot(); + m_Session.SetRuntimeState(EditorRuntimeState::Playing); + m_Session.EnsureRuntimePhysicsWorldStarted(); + m_Session.PublishEvent({.Payload = RuntimeStateChangedEvent{ + .User = QueuedCommand.Context.User, + .State = m_Session.GetRuntimeState(), + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PauseSessionCommand &) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + m_Session.SetRuntimeState(EditorRuntimeState::Paused); + m_Session.PublishEvent({.Payload = RuntimeStateChangedEvent{ + .User = QueuedCommand.Context.User, + .State = m_Session.GetRuntimeState(), + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const ResumeSessionCommand &) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + m_Session.SetRuntimeState(EditorRuntimeState::Playing); + m_Session.PublishEvent({.Payload = RuntimeStateChangedEvent{ + .User = QueuedCommand.Context.User, + .State = m_Session.GetRuntimeState(), + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const StopSessionCommand &) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + m_Session.StopRuntimePhysicsWorld(); + m_Session.RestoreRuntimeSceneSnapshot(); + m_Session.SetRuntimeState(EditorRuntimeState::Edit); + m_Session.PublishEvent({.Payload = RuntimeStateChangedEvent{ + .User = QueuedCommand.Context.User, + .State = m_Session.GetRuntimeState(), + }}); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &, + const SetWorldSettingsCommand &Command) { + m_Session.SetWorldSettings(Command.Settings); +} + +void EditorCommandDispatcher::HandleCommand(const QueuedEditorCommand &QueuedCommand, + const PlaceActorCommand &Command) { + m_Session.EnsurePresence(QueuedCommand.Context.User); + const InstanceHandle WorldFolder = m_Session.EnsureWorldFolder(); + if (!WorldFolder) return; + + const std::string ActorId = m_Session.BuildUniqueObjectId("Actor"); + const std::string ActorDisplayName = m_Session.BuildUniqueDisplayName("Actor"); + const EditorTransformDetails ActorTransform{.Location = Command.Location}; + m_Session.InsertObjectDetails(EditorObjectDetails{ + .Handle = m_Session.EnsureHandleForObjectId(ActorId), + .ObjectId = ActorId, + .DisplayName = ActorDisplayName, + .Kind = EditorSceneItemKind::Actor, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = ActorTransform, + .WorldTransform = ActorTransform, + }); + const InstanceHandle ActorNodeHandle = + m_Session.CreateInstanceForTemplate("Actor", ActorId); + if (Instance *ActorNode = m_Session.GetInstancePool().Resolve(ActorNodeHandle); + ActorNode != nullptr) { + ActorNode->SetParent(WorldFolder); + } + + std::string ChildId; + std::string ChildDisplayName; + if (!Command.ChildTemplateId.empty()) { + const EditorSceneItemKind ChildKind = + Command.ChildTemplateId == "Mesh" ? EditorSceneItemKind::Mesh : + Command.ChildTemplateId == "Light" ? EditorSceneItemKind::Light : + Command.ChildTemplateId == "Camera" ? EditorSceneItemKind::Camera : + Command.ChildTemplateId == "Actor" ? EditorSceneItemKind::Actor : + EditorSceneItemKind::Folder; + ChildId = m_Session.BuildUniqueObjectId(Command.ChildTemplateId); + ChildDisplayName = + m_Session.BuildUniqueDisplayName(Command.ChildTemplateId); + const bool ChildTransformable = ChildKind != EditorSceneItemKind::Folder; + m_Session.InsertObjectDetails(EditorObjectDetails{ + .Handle = m_Session.EnsureHandleForObjectId(ChildId), + .ObjectId = ChildId, + .DisplayName = ChildDisplayName, + .Kind = ChildKind, + .Visible = true, + .SupportsTransform = ChildTransformable, + .TransformReadOnly = false, + .Transform = ChildTransformable + ? std::optional{EditorTransformDetails{}} + : std::nullopt, + .WorldTransform = ChildTransformable + ? std::optional{EditorTransformDetails{}} + : std::nullopt, + }); + const InstanceHandle ChildNodeHandle = + m_Session.CreateInstanceForTemplate(Command.ChildTemplateId, ChildId); + if (Instance *ChildNode = m_Session.GetInstancePool().Resolve(ChildNodeHandle); + ChildNode != nullptr) { + ChildNode->SetParent(ActorNodeHandle ? ActorNodeHandle : WorldFolder); + } + } + + m_Session.SyncItemsFromTree(); + m_Session.RebuildSceneHandleState(); + m_Session.PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = ActorId, + .DisplayName = ActorDisplayName, + }}); + if (!ChildId.empty()) { + m_Session.PublishEvent({.Payload = ObjectCreatedEvent{ + .User = QueuedCommand.Context.User, + .ObjectId = ChildId, + .DisplayName = ChildDisplayName, + }}); + if (!Command.ChildMeshAssetPath.empty()) { + HandleCommand(QueuedCommand, SetMeshAssetCommand{ + .ObjectId = ChildId, + .AssetPath = Command.ChildMeshAssetPath, + }); + } + } + + HandleCommand(QueuedCommand, SetTransformCommand{ + .ObjectId = ActorId, + .Location = Command.Location, + }); +} +} // namespace Axiom diff --git a/Axiom/Session/EditorEvent.h b/Axiom/Scene/Session/EditorEvent.h similarity index 100% rename from Axiom/Session/EditorEvent.h rename to Axiom/Scene/Session/EditorEvent.h diff --git a/Axiom/Session/EditorInputSource.h b/Axiom/Scene/Session/EditorInputSource.h similarity index 100% rename from Axiom/Session/EditorInputSource.h rename to Axiom/Scene/Session/EditorInputSource.h diff --git a/Axiom/Session/EditorMessageBus.cpp b/Axiom/Scene/Session/EditorMessageBus.cpp similarity index 100% rename from Axiom/Session/EditorMessageBus.cpp rename to Axiom/Scene/Session/EditorMessageBus.cpp diff --git a/Axiom/Session/EditorMessageBus.h b/Axiom/Scene/Session/EditorMessageBus.h similarity index 100% rename from Axiom/Session/EditorMessageBus.h rename to Axiom/Scene/Session/EditorMessageBus.h diff --git a/Axiom/Scene/Session/EditorRuntimePhysicsController.h b/Axiom/Scene/Session/EditorRuntimePhysicsController.h new file mode 100644 index 00000000..7ba481f2 --- /dev/null +++ b/Axiom/Scene/Session/EditorRuntimePhysicsController.h @@ -0,0 +1,14 @@ +#pragma once + +namespace Axiom { + +class IEditorRuntimePhysicsController { +public: + virtual ~IEditorRuntimePhysicsController() = default; + + virtual void EnsurePhysicsWorldStarted() = 0; + virtual void StopPhysicsWorld() = 0; + virtual void StepRuntimePhysics(float DeltaTimeSeconds) = 0; +}; + +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSceneRendererAdapter.cpp b/Axiom/Scene/Session/EditorSceneRendererAdapter.cpp new file mode 100644 index 00000000..2321b0e8 --- /dev/null +++ b/Axiom/Scene/Session/EditorSceneRendererAdapter.cpp @@ -0,0 +1,105 @@ +#include "Session/EditorSceneRendererAdapter.h" + +#include "Core/Application.h" +#include "Renderer/Renderer.h" + +#include +#include + +namespace Axiom { +EditorSceneRendererAdapter::EditorSceneRendererAdapter( + CreateMeshResourceFn CreateMeshResource, + CreateMaterialHandleFn CreateMaterialHandle, + UpdateMaterialHandleFn UpdateMaterialHandle) + : m_CreateMeshResource(std::move(CreateMeshResource)), + m_CreateMaterialHandle(std::move(CreateMaterialHandle)), + m_UpdateMaterialHandle(std::move(UpdateMaterialHandle)) { + if (!m_CreateMeshResource) { + m_CreateMeshResource = [](const MeshData &Mesh) { + return Application::Get().GetRenderer().CreateMeshResource(Mesh); + }; + } + if (!m_CreateMaterialHandle) { + m_CreateMaterialHandle = [](const MaterialInstance &Material) { + Application *App = Application::TryGet(); + Renderer *Renderer = App != nullptr ? App->TryGetRenderer() : nullptr; + return Renderer != nullptr ? Renderer->CreateMaterialHandle(Material) + : MaterialHandle{}; + }; + } + if (!m_UpdateMaterialHandle) { + m_UpdateMaterialHandle = [](MaterialHandle Handle, + const MaterialInstance &Material) { + Application *App = Application::TryGet(); + Renderer *Renderer = App != nullptr ? App->TryGetRenderer() : nullptr; + if (Renderer != nullptr) { + Renderer->UpdateMaterialHandle(Handle, Material); + } + }; + } +} + +std::vector +EditorSceneRendererAdapter::BuildRenderSubmissions(const EditorSession &Session) { + const EditorSessionState &State = Session.GetState(); + std::unordered_set LiveObjectIds; + LiveObjectIds.reserve(State.Scene.MeshInstances.size()); + + std::vector Submissions; + Submissions.reserve(State.Scene.MeshInstances.size()); + for (const EditorSceneMeshInstance &Instance : State.Scene.MeshInstances) { + LiveObjectIds.insert(Instance.ObjectId); + + const auto DetailsIt = State.Scene.ObjectDetailsById.find(Instance.ObjectId); + if (DetailsIt != State.Scene.ObjectDetailsById.end() && + !DetailsIt->second.Visible) { + continue; + } + + auto &Cached = m_MeshesByObjectId[Instance.ObjectId]; + if (!Cached.Resource.IsValid() || + Cached.AssetRelativePath != Instance.AssetRelativePath) { + Cached.Resource = m_CreateMeshResource(Instance.Mesh); + Cached.RenderPath = Instance.RenderPath; + Cached.AssetRelativePath = Instance.AssetRelativePath; + assert((Cached.Resource.Mesh == nullptr || Cached.Resource.Handle.IsValid()) && + "Cached mesh resource must retain a valid opaque mesh handle"); + } + if (Cached.DebugDataId == 0) { + Cached.DebugDataId = + RegisterRenderMeshSubmissionDebugData({.Name = Instance.ObjectId}); + } + if (Instance.Material != nullptr) { + if (!Cached.MaterialHandle.IsValid()) { + Cached.MaterialHandle = m_CreateMaterialHandle(*Instance.Material); + } else { + m_UpdateMaterialHandle(Cached.MaterialHandle, *Instance.Material); + } + } else { + Cached.MaterialHandle = {}; + } + + if (!Cached.Resource.IsValid()) { + continue; + } + + Submissions.push_back({ + .MeshHandle = Cached.Resource.Handle, + .MaterialHandle = Cached.MaterialHandle, + .DebugDataId = Cached.DebugDataId, + .RenderPath = Cached.RenderPath, + .Transform = Instance.Transform, + }); + } + + for (auto It = m_MeshesByObjectId.begin(); It != m_MeshesByObjectId.end();) { + if (!LiveObjectIds.contains(It->first)) { + It = m_MeshesByObjectId.erase(It); + } else { + ++It; + } + } + + return Submissions; +} +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSceneRendererAdapter.h b/Axiom/Scene/Session/EditorSceneRendererAdapter.h new file mode 100644 index 00000000..e3fa40b3 --- /dev/null +++ b/Axiom/Scene/Session/EditorSceneRendererAdapter.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Renderer/Mesh.h" +#include "Renderer/Material.h" +#include "Session/EditorSession.h" + +#include +#include + +namespace Axiom { +class EditorSceneRendererAdapter { +public: + using CreateMeshResourceFn = + std::function; + using CreateMaterialHandleFn = + std::function; + using UpdateMaterialHandleFn = + std::function; + + explicit EditorSceneRendererAdapter( + CreateMeshResourceFn CreateMeshResource = {}, + CreateMaterialHandleFn CreateMaterialHandle = {}, + UpdateMaterialHandleFn UpdateMaterialHandle = {}); + + std::vector + BuildRenderSubmissions(const EditorSession &Session); + +private: + struct CachedMeshInstance { + RenderMeshResource Resource; + MeshRenderPath RenderPath{MeshRenderPath::Graphics}; + std::string AssetRelativePath; + RenderMeshSubmissionDebugDataId DebugDataId{0}; + MaterialHandle MaterialHandle{}; + }; + + CreateMeshResourceFn m_CreateMeshResource; + CreateMaterialHandleFn m_CreateMaterialHandle; + UpdateMaterialHandleFn m_UpdateMaterialHandle; + std::unordered_map m_MeshesByObjectId; +}; +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSceneStateManager.cpp b/Axiom/Scene/Session/EditorSceneStateManager.cpp new file mode 100644 index 00000000..56f30808 --- /dev/null +++ b/Axiom/Scene/Session/EditorSceneStateManager.cpp @@ -0,0 +1,567 @@ +#include "Session/EditorSceneStateManager.h" + +#include "Assets/MeshAsset.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace Axiom { +namespace { +EditorSceneItemKind KindForType(InstanceType Type) { + switch (Type) { + case InstanceType::SceneMeshObject: return EditorSceneItemKind::Mesh; + case InstanceType::SceneLight: return EditorSceneItemKind::Light; + case InstanceType::SceneCamera: return EditorSceneItemKind::Camera; + case InstanceType::SceneActor: return EditorSceneItemKind::Actor; + case InstanceType::Instance: + case InstanceType::DataModel: + case InstanceType::SceneFolder: return EditorSceneItemKind::Folder; + } + return EditorSceneItemKind::Folder; +} + +std::string_view TemplateIdForKind(EditorSceneItemKind Kind) { + switch (Kind) { + case EditorSceneItemKind::Mesh: return "Mesh"; + case EditorSceneItemKind::Light: return "Light"; + case EditorSceneItemKind::Camera: return "Camera"; + case EditorSceneItemKind::Actor: return "Actor"; + default: return "Folder"; + } +} + +bool SupportsTransformForKind(EditorSceneItemKind Kind) { + return Kind != EditorSceneItemKind::Folder; +} + +InstanceHandle FindInstanceById(const InstancePool &Pool, InstanceHandle Root, + std::string_view Id) { + const Instance *RootNode = Pool.Resolve(Root); + if (!RootNode) return {}; + if (RootNode->GetName() == Id) return Root; + for (const InstanceHandle Child : RootNode->GetChildren()) { + if (const InstanceHandle Found = FindInstanceById(Pool, Child, Id)) { + return Found; + } + } + return {}; +} + +glm::vec3 AbsVec3(const glm::vec3 &Value) { + return glm::vec3(std::abs(Value.x), std::abs(Value.y), std::abs(Value.z)); +} + +void ExpandBounds(const glm::vec3 &BoundsMin, const glm::vec3 &BoundsMax, + const glm::mat4 &Transform, glm::vec3 &OutMin, + glm::vec3 &OutMax, bool &HasBounds) { + const std::array Corners = { + glm::vec3(BoundsMin.x, BoundsMin.y, BoundsMin.z), + glm::vec3(BoundsMax.x, BoundsMin.y, BoundsMin.z), + glm::vec3(BoundsMin.x, BoundsMax.y, BoundsMin.z), + glm::vec3(BoundsMax.x, BoundsMax.y, BoundsMin.z), + glm::vec3(BoundsMin.x, BoundsMin.y, BoundsMax.z), + glm::vec3(BoundsMax.x, BoundsMin.y, BoundsMax.z), + glm::vec3(BoundsMin.x, BoundsMax.y, BoundsMax.z), + glm::vec3(BoundsMax.x, BoundsMax.y, BoundsMax.z), + }; + + for (const glm::vec3 &Corner : Corners) { + const glm::vec3 WorldCorner = glm::vec3(Transform * glm::vec4(Corner, 1.0f)); + if (!HasBounds) { + OutMin = WorldCorner; + OutMax = WorldCorner; + HasBounds = true; + continue; + } + OutMin = glm::min(OutMin, WorldCorner); + OutMax = glm::max(OutMax, WorldCorner); + } +} + +glm::mat4 BuildTransformMatrix(const EditorTransformDetails &Transform) { + glm::mat4 Matrix(1.0f); + Matrix = glm::translate(Matrix, Transform.Location); + Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.y), + glm::vec3(0.0f, 1.0f, 0.0f)); + Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.x), + glm::vec3(1.0f, 0.0f, 0.0f)); + Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.z), + glm::vec3(0.0f, 0.0f, 1.0f)); + Matrix = glm::scale(Matrix, Transform.Scale); + return Matrix; +} +} // namespace + +EditorSceneStateManager::EditorSceneStateManager(EditorSession &Session) + : m_Session(Session) {} + +void EditorSceneStateManager::SetSceneState(EditorSceneState SceneState) { + m_Session.ReplaceSceneItems(std::move(SceneState.Items)); + m_Session.ReplaceSceneMeshInstances(std::move(SceneState.MeshInstances)); + m_Session.ReplaceObjectDetails(std::move(SceneState.ObjectDetailsById)); + m_Session.ReplaceCollaborationStatesByObjectId( + std::move(SceneState.CollaborationByObjectId)); + m_Session.ReplaceWorldSettings(std::move(SceneState.WorldSettings)); + m_Session.RebuildSceneHandleState(); + m_Session.RefreshWorldSettingsHDR("SetSceneState"); + for (const auto &MeshInst : m_Session.GetSceneMeshInstances()) { + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(MeshInst.ObjectId); + if (Details != nullptr && MeshInst.Material && !Details->Material.has_value()) { + Details->Material = EditorMaterialProperties{ + .BaseColorFactor = MeshInst.Material->BaseColorFactor, + .Metallic = MeshInst.Material->Metallic, + .Roughness = MeshInst.Material->Roughness, + }; + } + } + RebuildInstanceTree(m_Session.GetSceneItems(), m_Session.GetSceneRoot()); + PruneInvalidSelections(); + RecomputeAllWorldTransforms(); +} + +void EditorSceneStateManager::SetSceneItems(std::vector SceneItems) { + m_Session.ReplaceSceneItems(std::move(SceneItems)); + m_Session.RebuildSceneHandleState(); + RebuildInstanceTree(m_Session.GetSceneItems(), m_Session.GetSceneRoot()); + PruneInvalidSelections(); + RecomputeAllWorldTransforms(); +} + +void EditorSceneStateManager::SetObjectDetails( + std::vector ObjectDetails) { + m_Session.ReplaceObjectDetails(BuildObjectDetailsMap(std::move(ObjectDetails))); + m_Session.RebuildSceneHandleState(); + RecomputeAllWorldTransforms(); +} + +const EditorSceneItem * +EditorSceneStateManager::FindSceneItem(std::string_view ObjectId) const { + return FindSceneItemRecursive(m_Session.GetSceneItems(), ObjectId); +} + +std::unordered_map +EditorSceneStateManager::BuildObjectDetailsMap( + std::vector ObjectDetails) { + std::unordered_map DetailsByObjectId; + DetailsByObjectId.reserve(ObjectDetails.size()); + for (EditorObjectDetails &Details : ObjectDetails) { + DetailsByObjectId.emplace(Details.ObjectId, std::move(Details)); + } + return DetailsByObjectId; +} + +void EditorSceneStateManager::InitSceneRoot() { + m_Session.SetSceneRoot(m_Session.GetInstancePool().Create()); + const InstanceHandle World = + m_Session.GetInstancePool().Create("world"); + if (Instance *WorldNode = m_Session.GetInstancePool().Resolve(World); + WorldNode != nullptr) { + WorldNode->SetParent(m_Session.GetSceneRoot()); + } +} + +InstanceHandle EditorSceneStateManager::FindWorldFolder() const { + if (!m_Session.GetSceneRoot()) return {}; + const Instance *Root = + m_Session.GetInstancePool().Resolve(m_Session.GetSceneRoot()); + if (Root == nullptr) return {}; + for (const InstanceHandle ChildHandle : Root->GetChildren()) { + const Instance *Child = m_Session.GetInstancePool().Resolve(ChildHandle); + if (Child != nullptr && Child->GetType() == InstanceType::SceneFolder && + Child->GetName() == "world") { + return ChildHandle; + } + } + return {}; +} + +InstanceHandle EditorSceneStateManager::EnsureWorldFolder() { + if (!m_Session.GetSceneRoot()) { + InitSceneRoot(); + } + + auto EnsureWorldDetails = [this]() { + if (m_Session.HasObjectDetails("world")) { + return; + } + m_Session.InsertObjectDetails(EditorObjectDetails{ + .Handle = m_Session.EnsureHandleForObjectId("world"), + .ObjectId = "world", + .DisplayName = "World", + .Kind = EditorSceneItemKind::Folder, + .Visible = true, + .SupportsTransform = false, + .TransformReadOnly = true, + }); + }; + + if (const InstanceHandle World = FindWorldFolder(); World) { + EnsureWorldDetails(); + return World; + } + + EnsureWorldDetails(); + const InstanceHandle World = + m_Session.GetInstancePool().Create("world"); + if (Instance *WorldNode = m_Session.GetInstancePool().Resolve(World); + WorldNode != nullptr) { + WorldNode->SetParent(m_Session.GetSceneRoot()); + } + SyncItemsFromTree(); + return World; +} + +void EditorSceneStateManager::RebuildInstanceTree( + const std::vector &Items, InstanceHandle Parent) { + Instance *ParentNode = m_Session.GetInstancePool().Resolve(Parent); + if (!ParentNode) return; + const std::vector OldChildren = ParentNode->GetChildren(); + for (const InstanceHandle Child : OldChildren) { + m_Session.GetInstancePool().Destroy(Child); + } + for (const EditorSceneItem &Item : Items) { + const InstanceHandle Node = + CreateInstanceForTemplate(std::string(TemplateIdForKind(Item.Kind)), Item.Id); + if (!Node) continue; + if (Instance *NodePtr = m_Session.GetInstancePool().Resolve(Node); + NodePtr != nullptr) { + NodePtr->SetParent(Parent); + } + if (!Item.Children.empty()) { + RebuildInstanceTree(Item.Children, Node); + } + } +} + +void EditorSceneStateManager::SyncItemsFromTree() { + m_Session.ClearSceneItems(); + if (!m_Session.GetSceneRoot()) return; + const Instance *Root = + m_Session.GetInstancePool().Resolve(m_Session.GetSceneRoot()); + if (Root == nullptr) return; + for (const InstanceHandle Child : Root->GetChildren()) { + m_Session.AddSceneItem(BuildItemFromInstance(Child)); + } +} + +EditorSceneItem EditorSceneStateManager::BuildItemFromInstance(InstanceHandle Node) const { + const Instance *NodePtr = m_Session.GetInstancePool().Resolve(Node); + if (NodePtr == nullptr) { + return {}; + } + EditorSceneItem Item; + Item.Handle = m_Session.ResolveObjectHandle(NodePtr->GetName()); + Item.Id = NodePtr->GetName(); + Item.Kind = KindForInstance(Node); + Item.Visible = true; + Item.DisplayName = NodePtr->GetName(); + if (const EditorObjectDetails *Details = + m_Session.FindObjectDetails(NodePtr->GetName()); + Details != nullptr) { + Item.DisplayName = Details->DisplayName; + Item.Visible = Details->Visible; + Item.Kind = Details->Kind; + } + for (const InstanceHandle Child : NodePtr->GetChildren()) { + Item.Children.push_back(BuildItemFromInstance(Child)); + } + return Item; +} + +InstanceHandle EditorSceneStateManager::CreateInstanceForTemplate( + const std::string &TemplateId, const std::string &ObjectId) const { + if (TemplateId == "Folder") + return m_Session.GetInstancePool().Create(ObjectId); + if (TemplateId == "Mesh") + return m_Session.GetInstancePool().Create(ObjectId); + if (TemplateId == "Light") + return m_Session.GetInstancePool().Create(ObjectId); + if (TemplateId == "Camera") + return m_Session.GetInstancePool().Create(ObjectId); + if (TemplateId == "Actor") + return m_Session.GetInstancePool().Create(ObjectId); + return {}; +} + +EditorSceneItemKind +EditorSceneStateManager::KindForInstance(InstanceHandle Node) const { + const Instance *NodePtr = m_Session.GetInstancePool().Resolve(Node); + return NodePtr != nullptr ? KindForType(NodePtr->GetType()) + : EditorSceneItemKind::Folder; +} + +bool EditorSceneStateManager::IsValidTemplateId(const std::string &TemplateId) const { + return TemplateId == "Folder" || TemplateId == "Mesh" || + TemplateId == "Light" || TemplateId == "Camera" || + TemplateId == "Actor"; +} + +std::vector +EditorSceneStateManager::CollectDescendantIds(InstanceHandle Root) const { + std::vector Ids; + std::vector Stack{Root}; + while (!Stack.empty()) { + const InstanceHandle CurHandle = Stack.back(); + Stack.pop_back(); + const Instance *Cur = m_Session.GetInstancePool().Resolve(CurHandle); + if (Cur == nullptr) continue; + Ids.push_back(Cur->GetName()); + for (const InstanceHandle Child : Cur->GetChildren()) { + Stack.push_back(Child); + } + } + return Ids; +} + +void EditorSceneStateManager::DeepCloneSubtree( + InstanceHandle Source, InstanceHandle DestParent, + std::vector &OutNewDetails) { + const Instance *SourceNode = m_Session.GetInstancePool().Resolve(Source); + if (SourceNode == nullptr) return; + const std::string NewId = BuildUniqueObjectId(SourceNode->GetName()); + const EditorSceneItemKind Kind = KindForInstance(Source); + std::string BaseDisplayName = SourceNode->GetName(); + EditorObjectDetails NewDetails; + NewDetails.Kind = Kind; + NewDetails.Visible = true; + NewDetails.SupportsTransform = SupportsTransformForKind(Kind); + NewDetails.TransformReadOnly = false; + + if (const EditorObjectDetails *ExistingDetails = + m_Session.FindObjectDetails(SourceNode->GetName()); + ExistingDetails != nullptr) { + BaseDisplayName = ExistingDetails->DisplayName; + NewDetails.Visible = ExistingDetails->Visible; + NewDetails.Transform = ExistingDetails->Transform; + NewDetails.WorldTransform = ExistingDetails->WorldTransform; + } else if (NewDetails.SupportsTransform) { + NewDetails.Transform = EditorTransformDetails{}; + NewDetails.WorldTransform = EditorTransformDetails{}; + } + + NewDetails.ObjectId = NewId; + NewDetails.Handle = m_Session.EnsureHandleForObjectId(NewId); + NewDetails.DisplayName = BuildUniqueDisplayName(BaseDisplayName); + m_Session.InsertObjectDetails(NewDetails); + OutNewDetails.push_back(NewDetails); + + const InstanceHandle Clone = + CreateInstanceForTemplate(std::string(TemplateIdForKind(Kind)), NewId); + if (Clone) { + if (Instance *CloneNode = m_Session.GetInstancePool().Resolve(Clone); + CloneNode != nullptr) { + CloneNode->SetParent(DestParent); + } + for (const InstanceHandle Child : SourceNode->GetChildren()) { + DeepCloneSubtree(Child, Clone, OutNewDetails); + } + } +} + +std::string EditorSceneStateManager::BuildUniqueObjectId( + std::string_view BaseObjectId) const { + if (!IsSceneObjectIdInUse(BaseObjectId)) return std::string(BaseObjectId); + for (int N = 2;; ++N) { + std::string Candidate = + std::string(BaseObjectId) + "_" + std::to_string(N); + if (!IsSceneObjectIdInUse(Candidate)) return Candidate; + } +} + +std::string EditorSceneStateManager::BuildUniqueDisplayName( + std::string_view BaseDisplayName) const { + if (!IsSceneDisplayNameInUse(BaseDisplayName)) { + return std::string(BaseDisplayName); + } + for (int N = 2;; ++N) { + std::string Candidate = + std::string(BaseDisplayName) + " " + std::to_string(N); + if (!IsSceneDisplayNameInUse(Candidate)) return Candidate; + } +} + +bool EditorSceneStateManager::UpdateSceneItemDisplayName( + std::vector &Items, std::string_view ObjectId, + std::string_view DisplayName) { + for (EditorSceneItem &Item : Items) { + if (Item.Id == ObjectId) { + Item.DisplayName = std::string(DisplayName); + return true; + } + if (UpdateSceneItemDisplayName(Item.Children, ObjectId, DisplayName)) { + return true; + } + } + return false; +} + +bool EditorSceneStateManager::UpdateSceneItemVisibility( + std::vector &Items, std::string_view ObjectId, + bool Visible) { + for (EditorSceneItem &Item : Items) { + if (Item.Id == ObjectId) { + Item.Visible = Visible; + return true; + } + if (UpdateSceneItemVisibility(Item.Children, ObjectId, Visible)) { + return true; + } + } + return false; +} + +void EditorSceneStateManager::RemoveSceneObject(std::string_view ObjectId) { + const std::string Id(ObjectId); + m_Session.EraseObjectDetails(Id); + m_Session.RemoveCollaborationState(Id); + m_Session.RemoveSceneMeshInstances(Id); +} + +glm::mat4 +EditorSceneStateManager::ComputeWorldTransformMatrix(InstanceHandle Node) const { + if (!Node) return glm::mat4(1.0f); + std::vector Chain; + InstanceHandle Cur = Node; + while (Cur && Cur != m_Session.GetSceneRoot()) { + const Instance *CurrentNode = m_Session.GetInstancePool().Resolve(Cur); + if (CurrentNode == nullptr) { + break; + } + Chain.push_back(Cur); + Cur = CurrentNode->GetParent(); + } + + glm::mat4 World(1.0f); + for (auto It = Chain.rbegin(); It != Chain.rend(); ++It) { + const Instance *NodePtr = m_Session.GetInstancePool().Resolve(*It); + if (NodePtr == nullptr) { + continue; + } + if (const EditorObjectDetails *Details = + m_Session.FindObjectDetails(NodePtr->GetName()); + Details != nullptr && Details->Transform.has_value()) { + World = World * BuildTransformMatrix(*Details->Transform); + } + } + return World; +} + +EditorTransformDetails +EditorSceneStateManager::DecomposeMatrix(const glm::mat4 &Matrix) const { + const glm::vec3 Location = glm::vec3(Matrix[3]); + glm::vec3 Col0 = glm::vec3(Matrix[0]); + glm::vec3 Col1 = glm::vec3(Matrix[1]); + glm::vec3 Col2 = glm::vec3(Matrix[2]); + const float ScaleX = glm::length(Col0); + const float ScaleY = glm::length(Col1); + const float ScaleZ = glm::length(Col2); + if (ScaleX > 0.0f) Col0 /= ScaleX; + if (ScaleY > 0.0f) Col1 /= ScaleY; + if (ScaleZ > 0.0f) Col2 /= ScaleZ; + const float AngleX = + glm::degrees(glm::asin(glm::clamp(-Col2.y, -1.0f, 1.0f))); + const float AngleY = glm::degrees(glm::atan(Col2.x, Col2.z)); + const float AngleZ = glm::degrees(glm::atan(Col0.y, Col1.y)); + return EditorTransformDetails{ + .Location = Location, + .RotationDegrees = {AngleX, AngleY, AngleZ}, + .Scale = {ScaleX, ScaleY, ScaleZ}, + }; +} + +void EditorSceneStateManager::RecomputeSubtreeWorldTransforms(InstanceHandle Node) { + const Instance *NodePtr = m_Session.GetInstancePool().Resolve(Node); + if (!NodePtr) return; + const SceneObjectHandle Handle = m_Session.ResolveObjectHandle(NodePtr->GetName()); + const std::string &Id = NodePtr->GetName(); + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(Id); + if (Details != nullptr && Details->Transform.has_value()) { + const glm::mat4 WorldMatrix = ComputeWorldTransformMatrix(Node); + Details->WorldTransform = DecomposeMatrix(WorldMatrix); + m_Session.UpdateSceneMeshInstanceTransform(Handle, WorldMatrix); + } + + for (const InstanceHandle Child : NodePtr->GetChildren()) { + RecomputeSubtreeWorldTransforms(Child); + } +} + +void EditorSceneStateManager::RecomputeAllWorldTransforms() { + if (!m_Session.GetSceneRoot()) return; + const Instance *Root = + m_Session.GetInstancePool().Resolve(m_Session.GetSceneRoot()); + if (Root == nullptr) return; + for (const InstanceHandle Child : Root->GetChildren()) { + RecomputeSubtreeWorldTransforms(Child); + } +} + +void EditorSceneStateManager::ApplyWorldTransform( + std::string_view ObjectId, const EditorTransformDetails &WorldTransform, + SessionUserId User, bool ShouldPublishEvent) { + EditorObjectDetails *Details = m_Session.FindMutableObjectDetails(ObjectId); + if (Details == nullptr) return; + const SceneObjectHandle Handle = m_Session.ResolveObjectHandle(ObjectId); + + const glm::mat4 WorldMatrix = BuildTransformMatrix(WorldTransform); + EditorTransformDetails LocalTransform = WorldTransform; + const InstanceHandle NodeHandle = + FindInstanceById(m_Session.GetInstancePool(), m_Session.GetSceneRoot(), ObjectId); + const Instance *Node = m_Session.GetInstancePool().Resolve(NodeHandle); + if (Node != nullptr && Node->GetParent() && + Node->GetParent() != m_Session.GetSceneRoot()) { + const glm::mat4 ParentWorld = ComputeWorldTransformMatrix(Node->GetParent()); + LocalTransform = DecomposeMatrix(glm::inverse(ParentWorld) * WorldMatrix); + } + + Details->Transform = LocalTransform; + Details->WorldTransform = WorldTransform; + m_Session.UpdateSceneMeshInstanceTransform(Handle, WorldMatrix); + + if (Node != nullptr) { + for (const InstanceHandle Child : Node->GetChildren()) { + RecomputeSubtreeWorldTransforms(Child); + } + } + + if (ShouldPublishEvent) { + m_Session.PublishEvent({.Payload = ObjectTransformUpdatedEvent{ + .User = User, + .ObjectId = std::string(ObjectId), + .Location = WorldTransform.Location, + .RotationDegrees = WorldTransform.RotationDegrees, + .Scale = WorldTransform.Scale, + }}); + } +} + +const EditorSceneItem *EditorSceneStateManager::FindSceneItemRecursive( + const std::vector &Items, std::string_view ObjectId) const { + for (const EditorSceneItem &Item : Items) { + if (Item.Id == ObjectId) { + return &Item; + } + if (const EditorSceneItem *Child = + FindSceneItemRecursive(Item.Children, ObjectId); + Child != nullptr) { + return Child; + } + } + return nullptr; +} + +bool EditorSceneStateManager::IsSceneObjectIdInUse(std::string_view ObjectId) const { + return m_Session.HasObjectDetails(ObjectId); +} + +bool EditorSceneStateManager::IsSceneDisplayNameInUse( + std::string_view DisplayName) const { + return m_Session.HasObjectDisplayName(DisplayName); +} +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSceneStateManager.h b/Axiom/Scene/Session/EditorSceneStateManager.h new file mode 100644 index 00000000..53f0de40 --- /dev/null +++ b/Axiom/Scene/Session/EditorSceneStateManager.h @@ -0,0 +1,66 @@ +#pragma once + +#include "Session/EditorSession.h" + +namespace Axiom { +class EditorSceneStateManager { +public: + explicit EditorSceneStateManager(EditorSession &Session); + + void SetSceneState(EditorSceneState SceneState); + void SetSceneItems(std::vector SceneItems); + void SetObjectDetails(std::vector ObjectDetails); + + const EditorSceneItem *FindSceneItem(std::string_view ObjectId) const; + const EditorSceneItem * + FindSceneItemRecursive(const std::vector &Items, + std::string_view ObjectId) const; + + static std::unordered_map + BuildObjectDetailsMap(std::vector ObjectDetails); + + void InitSceneRoot(); + InstanceHandle FindWorldFolder() const; + InstanceHandle EnsureWorldFolder(); + void RebuildInstanceTree(const std::vector &Items, + InstanceHandle Parent); + void SyncItemsFromTree(); + EditorSceneItem BuildItemFromInstance(InstanceHandle Node) const; + InstanceHandle CreateInstanceForTemplate(const std::string &TemplateId, + const std::string &ObjectId) const; + EditorSceneItemKind KindForInstance(InstanceHandle Node) const; + bool IsValidTemplateId(const std::string &TemplateId) const; + std::vector CollectDescendantIds(InstanceHandle Root) const; + void DeepCloneSubtree(InstanceHandle Source, InstanceHandle DestParent, + std::vector &OutNewDetails); + + std::string BuildUniqueObjectId(std::string_view BaseObjectId) const; + std::string BuildUniqueDisplayName(std::string_view BaseDisplayName) const; + static bool UpdateSceneItemDisplayName(std::vector &Items, + std::string_view ObjectId, + std::string_view DisplayName); + static bool UpdateSceneItemVisibility(std::vector &Items, + std::string_view ObjectId, bool Visible); + void RemoveSceneObject(std::string_view ObjectId); + void RemoveGeneratedAssetChildren(std::string_view RootObjectId); + void ExpandMeshAssetIntoScene(std::string_view RootObjectId, + const MeshSceneData &SceneData, + std::string_view AssetPath); + void ClearSelectionsForObject(std::string_view ObjectId); + void PruneInvalidSelections(); + + glm::mat4 ComputeWorldTransformMatrix(InstanceHandle Node) const; + EditorTransformDetails DecomposeMatrix(const glm::mat4 &Matrix) const; + void RecomputeSubtreeWorldTransforms(InstanceHandle Node); + void RecomputeAllWorldTransforms(); + void ApplyWorldTransform(std::string_view ObjectId, + const EditorTransformDetails &WorldTransform, + SessionUserId User, bool PublishEvent); + +private: + bool IsSceneObjectIdInUse(std::string_view ObjectId) const; + bool IsSceneDisplayNameInUse(std::string_view DisplayName) const; + + EditorSession &m_Session; +}; +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSceneStateManagerAssets.cpp b/Axiom/Scene/Session/EditorSceneStateManagerAssets.cpp new file mode 100644 index 00000000..c18cbe66 --- /dev/null +++ b/Axiom/Scene/Session/EditorSceneStateManagerAssets.cpp @@ -0,0 +1,265 @@ +#include "Session/EditorSceneStateManager.h" + +#include + +#include +#include + +namespace Axiom { +namespace { +InstanceHandle FindInstanceById(const InstancePool &Pool, InstanceHandle Root, + std::string_view Id) { + const Instance *RootNode = Pool.Resolve(Root); + if (!RootNode) return {}; + if (RootNode->GetName() == Id) return Root; + for (const InstanceHandle Child : RootNode->GetChildren()) { + if (const InstanceHandle Found = FindInstanceById(Pool, Child, Id)) { + return Found; + } + } + return {}; +} + +glm::vec3 AbsVec3(const glm::vec3 &Value) { + return glm::vec3(std::abs(Value.x), std::abs(Value.y), std::abs(Value.z)); +} + +void ExpandBounds(const glm::vec3 &BoundsMin, const glm::vec3 &BoundsMax, + const glm::mat4 &Transform, glm::vec3 &OutMin, + glm::vec3 &OutMax, bool &HasBounds) { + const std::array Corners = { + glm::vec3(BoundsMin.x, BoundsMin.y, BoundsMin.z), + glm::vec3(BoundsMax.x, BoundsMin.y, BoundsMin.z), + glm::vec3(BoundsMin.x, BoundsMax.y, BoundsMin.z), + glm::vec3(BoundsMax.x, BoundsMax.y, BoundsMin.z), + glm::vec3(BoundsMin.x, BoundsMin.y, BoundsMax.z), + glm::vec3(BoundsMax.x, BoundsMin.y, BoundsMax.z), + glm::vec3(BoundsMin.x, BoundsMax.y, BoundsMax.z), + glm::vec3(BoundsMax.x, BoundsMax.y, BoundsMax.z), + }; + + for (const glm::vec3 &Corner : Corners) { + const glm::vec3 WorldCorner = glm::vec3(Transform * glm::vec4(Corner, 1.0f)); + if (!HasBounds) { + OutMin = WorldCorner; + OutMax = WorldCorner; + HasBounds = true; + continue; + } + OutMin = glm::min(OutMin, WorldCorner); + OutMax = glm::max(OutMax, WorldCorner); + } +} + +std::optional +BuildDefaultStaticMeshPhysics(const MeshSceneData &SceneData, + const EditorTransformDetails &RootTransform) { + glm::vec3 CombinedMin(0.0f); + glm::vec3 CombinedMax(0.0f); + bool HasBounds = false; + + for (const auto &Instance : SceneData.Instances) { + ExpandBounds(Instance.Mesh.BoundsMin, Instance.Mesh.BoundsMax, + Instance.Transform, CombinedMin, CombinedMax, HasBounds); + } + if (!HasBounds) { + return std::nullopt; + } + + glm::vec3 HalfExtents = glm::max(glm::abs(CombinedMin), glm::abs(CombinedMax)); + HalfExtents *= AbsVec3(RootTransform.Scale); + HalfExtents = glm::max(HalfExtents, glm::vec3(0.01f)); + return EditorPhysicsProperties{ + .BodyType = EditorPhysicsBodyType::Static, + .ColliderType = EditorPhysicsColliderType::Box, + .BoxHalfExtents = HalfExtents, + }; +} + +std::string SanitizeGeneratedAssetToken(std::string_view Value) { + std::string Out; + Out.reserve(Value.size()); + for (const char Character : Value) { + if ((Character >= 'a' && Character <= 'z') || + (Character >= 'A' && Character <= 'Z') || + (Character >= '0' && Character <= '9')) { + Out.push_back(Character); + } else { + Out.push_back('_'); + } + } + while (!Out.empty() && Out.back() == '_') { + Out.pop_back(); + } + return Out.empty() ? "mesh" : Out; +} + +std::string BuildGeneratedAssetChildId(std::string_view RootObjectId, + std::string_view InstanceName, + size_t InstanceIndex) { + return std::string(RootObjectId) + "__asset_" + std::to_string(InstanceIndex) + + "_" + SanitizeGeneratedAssetToken(InstanceName); +} + +std::string ResolveGeneratedAssetChildDisplayName(std::string_view InstanceName, + size_t InstanceIndex) { + if (!InstanceName.empty()) { + return std::string(InstanceName); + } + return "Mesh " + std::to_string(InstanceIndex + 1); +} +} // namespace + +void EditorSceneStateManager::RemoveGeneratedAssetChildren( + std::string_view RootObjectId) { + const InstanceHandle RootHandle = + FindInstanceById(m_Session.GetInstancePool(), m_Session.GetSceneRoot(), + RootObjectId); + const Instance *Root = m_Session.GetInstancePool().Resolve(RootHandle); + if (Root == nullptr) return; + + std::vector GeneratedChildIds; + for (const InstanceHandle ChildHandle : Root->GetChildren()) { + const Instance *Child = m_Session.GetInstancePool().Resolve(ChildHandle); + if (Child == nullptr) continue; + const EditorObjectDetails *Details = m_Session.FindObjectDetails(Child->GetName()); + if (Details == nullptr) continue; + if (!Details->IsGeneratedAssetChild || + !Details->GeneratedFromAssetRootId.has_value() || + *Details->GeneratedFromAssetRootId != RootObjectId) { + continue; + } + GeneratedChildIds.push_back(Child->GetName()); + } + + for (const std::string &ChildId : GeneratedChildIds) { + const InstanceHandle ChildHandle = + FindInstanceById(m_Session.GetInstancePool(), m_Session.GetSceneRoot(), + ChildId); + if (!ChildHandle) continue; + for (const std::string &DescendantId : CollectDescendantIds(ChildHandle)) { + RemoveSceneObject(DescendantId); + ClearSelectionsForObject(DescendantId); + } + m_Session.GetInstancePool().Destroy(ChildHandle); + } +} + +void EditorSceneStateManager::ExpandMeshAssetIntoScene( + std::string_view RootObjectId, const MeshSceneData &SceneData, + std::string_view AssetPath) { + EditorObjectDetails *RootDetails = m_Session.FindMutableObjectDetails(RootObjectId); + if (RootDetails == nullptr) return; + + const InstanceHandle RootHandle = + FindInstanceById(m_Session.GetInstancePool(), m_Session.GetSceneRoot(), + RootObjectId); + Instance *Root = m_Session.GetInstancePool().Resolve(RootHandle); + if (Root == nullptr) return; + + RemoveGeneratedAssetChildren(RootObjectId); + m_Session.RemoveSceneMeshInstances(RootObjectId); + + RootDetails->IsGeneratedAssetChild = false; + RootDetails->GeneratedFromAssetRootId = std::nullopt; + RootDetails->AssetRelativePath = std::string(AssetPath); + if (!RootDetails->Physics.has_value()) { + const EditorTransformDetails RootTransform = + RootDetails->Transform.value_or(EditorTransformDetails{}); + RootDetails->Physics = BuildDefaultStaticMeshPhysics(SceneData, RootTransform); + } + + if (SceneData.Instances.size() == 1) { + const auto &First = SceneData.Instances.front(); + m_Session.AddSceneMeshInstance(EditorSceneMeshInstance{ + .ObjectHandle = m_Session.ResolveObjectHandle(RootObjectId), + .ObjectId = std::string(RootObjectId), + .Mesh = First.Mesh, + .Material = First.Material, + .RenderPath = MeshRenderPath::Graphics, + .Transform = glm::mat4(1.0f), + .AssetRelativePath = std::string(AssetPath), + }); + if (First.Material) { + RootDetails->Material = EditorMaterialProperties{ + .BaseColorFactor = First.Material->BaseColorFactor, + .Metallic = First.Material->Metallic, + .Roughness = First.Material->Roughness, + .TextureAssetPath = First.Material->TextureAssetPath.empty() + ? std::nullopt + : std::optional( + First.Material->TextureAssetPath), + }; + } + SyncItemsFromTree(); + return; + } + + RootDetails->Material = std::nullopt; + for (size_t InstanceIndex = 0; InstanceIndex < SceneData.Instances.size(); + ++InstanceIndex) { + const auto &SourceInstance = SceneData.Instances[InstanceIndex]; + const std::string ChildId = BuildGeneratedAssetChildId( + RootObjectId, SourceInstance.Name, InstanceIndex); + const std::string ChildDisplayName = ResolveGeneratedAssetChildDisplayName( + SourceInstance.Name, InstanceIndex); + const EditorTransformDetails ChildLocalTransform = + DecomposeMatrix(SourceInstance.Transform); + + m_Session.UpsertObjectDetails(EditorObjectDetails{ + .Handle = m_Session.EnsureHandleForObjectId(ChildId), + .ObjectId = ChildId, + .DisplayName = ChildDisplayName, + .Kind = EditorSceneItemKind::Mesh, + .Visible = RootDetails->Visible, + .IsGeneratedAssetChild = true, + .SupportsTransform = true, + .TransformReadOnly = true, + .Transform = ChildLocalTransform, + .Material = SourceInstance.Material + ? std::optional( + EditorMaterialProperties{ + .BaseColorFactor = + SourceInstance.Material->BaseColorFactor, + .Metallic = SourceInstance.Material->Metallic, + .Roughness = SourceInstance.Material->Roughness, + .TextureAssetPath = + SourceInstance.Material->TextureAssetPath.empty() + ? std::nullopt + : std::optional( + SourceInstance.Material + ->TextureAssetPath), + }) + : std::nullopt, + .GeneratedFromAssetRootId = std::string(RootObjectId), + }); + + const InstanceHandle Child = CreateInstanceForTemplate("Mesh", ChildId); + if (Instance *ChildNode = m_Session.GetInstancePool().Resolve(Child); + ChildNode != nullptr) { + ChildNode->SetParent(RootHandle); + } + + m_Session.AddSceneMeshInstance(EditorSceneMeshInstance{ + .ObjectHandle = m_Session.ResolveObjectHandle(ChildId), + .ObjectId = ChildId, + .Mesh = SourceInstance.Mesh, + .Material = SourceInstance.Material, + .RenderPath = MeshRenderPath::Graphics, + .Transform = SourceInstance.Transform, + }); + } + + m_Session.RebuildSceneHandleState(); + SyncItemsFromTree(); +} + +void EditorSceneStateManager::ClearSelectionsForObject(std::string_view ObjectId) { + const SceneObjectHandle Handle = m_Session.ResolveObjectHandle(ObjectId); + m_Session.ClearSelectedObjectsForHandle(Handle); +} + +void EditorSceneStateManager::PruneInvalidSelections() { + m_Session.PruneInvalidSelectedObjects(); +} +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSession.cpp b/Axiom/Scene/Session/EditorSession.cpp new file mode 100644 index 00000000..66340f48 --- /dev/null +++ b/Axiom/Scene/Session/EditorSession.cpp @@ -0,0 +1,1010 @@ +#include "Session/EditorSession.h" + +#include "Assets/AssetCooker.h" +#include "Assets/CookedTextureAsset.h" +#include "Assets/IAssetSource.h" +#include "Assets/MeshAsset.h" +#include "Session/EditorCommandDispatcher.h" +#include "Session/EditorRuntimePhysicsController.h" +#include "Session/EditorSceneStateManager.h" +#include "Session/EditorSessionValidationModule.h" + +#include + +#include +#include +#include + +namespace Axiom { +namespace { +std::string DefaultUserDisplayName(SessionUserId User) { + if (User.Value == 1) { + return "Host"; + } + return "User " + std::to_string(User.Value - 1); +} + +std::string DefaultPresentationColor(SessionUserId User) { + static constexpr const char *Palette[] = { + "#F97316", "#22C55E", "#0EA5E9", "#F59E0B", + "#EF4444", "#14B8A6", "#8B5CF6", "#84CC16", + }; + return Palette[User.Value % (sizeof(Palette) / sizeof(Palette[0]))]; +} + +std::string PresenceStateName(EditorUserPresenceState State) { + switch (State) { + case EditorUserPresenceState::Connected: return "connected"; + case EditorUserPresenceState::Away: return "away"; + case EditorUserPresenceState::Disconnected: return "disconnected"; + } + return "connected"; +} + +bool IsHostUser(SessionUserId User) { + return User.Value == 1; +} + +bool IsWhitespace(char Value) { + return Value == ' ' || Value == '\t' || Value == '\n' || Value == '\r' || + Value == '\f' || Value == '\v'; +} + +TextureSourceDataRef CloneTextureSourceData( + const TextureSourceDataRef &Texture) { + if (!Texture) { + return nullptr; + } + + auto Copy = std::make_shared(); + Copy->Width = Texture->Width; + Copy->Height = Texture->Height; + Copy->Pixels = Texture->Pixels; + return Copy; +} + +std::shared_ptr +CloneMaterialInstance(const std::shared_ptr &Material) { + if (!Material) { + return nullptr; + } + + auto Copy = std::make_shared(); + Copy->BaseColorTexture = CloneTextureSourceData(Material->BaseColorTexture); + Copy->BaseColorFactor = Material->BaseColorFactor; + Copy->Metallic = Material->Metallic; + Copy->Roughness = Material->Roughness; + Copy->TextureAssetPath = Material->TextureAssetPath; + return Copy; +} + +EditorSceneState CloneEditorSceneState(const EditorSceneState &Scene) { + EditorSceneState Copy = Scene; + for (auto &MeshInstance : Copy.MeshInstances) { + MeshInstance.Material = CloneMaterialInstance(MeshInstance.Material); + } + return Copy; +} + +void CookHDRTextureAssetBestEffort(const std::filesystem::path &ContentDir, + std::string_view RelativeAssetPath) { + if (ContentDir.empty() || RelativeAssetPath.empty()) { + return; + } + + const auto Cooked = Assets::CookHDRTextureAsset(ContentDir, RelativeAssetPath); + if (!Cooked.has_value()) { + A_CORE_WARN("EditorSession: failed to cook HDR texture asset '{}'", + std::string(RelativeAssetPath)); + } +} + +void HydrateWorldSettingsHDRData(EditorWorldSettings &Settings, + const std::filesystem::path &ContentDir, + const std::filesystem::path &EngineContentDir, + std::string_view LogContext) { + if (Settings.SkyboxHDRPath.empty()) { + Settings.SkyboxHDRData = nullptr; + return; + } + if (Settings.SkyboxHDRData) { + return; + } + if (ContentDir.empty()) { + A_CORE_WARN("{}: content directory not configured; cannot load HDR '{}'", + LogContext, Settings.SkyboxHDRPath); + return; + } + + const std::filesystem::path HDRRelativePath(Settings.SkyboxHDRPath); + const bool IsEngineAsset = + !HDRRelativePath.empty() && *HDRRelativePath.begin() == "Engine"; + std::filesystem::path EffectiveContentDir = ContentDir; + std::filesystem::path EffectiveRelativePath = HDRRelativePath; + if (IsEngineAsset && !EngineContentDir.empty()) { + EffectiveContentDir = EngineContentDir; + auto It = HDRRelativePath.begin(); + ++It; + EffectiveRelativePath.clear(); + for (; It != HDRRelativePath.end(); ++It) { + EffectiveRelativePath /= *It; + } + } + + const auto FullPath = EffectiveContentDir / EffectiveRelativePath; + if (std::filesystem::exists(FullPath)) { + CookHDRTextureAssetBestEffort(EffectiveContentDir, + EffectiveRelativePath.generic_string()); + } + auto Loaded = Assets::LoadHDRTextureFromFile(FullPath); + if (!Loaded) { + const Assets::CookedAssetSource CookedSource(EffectiveContentDir); + if (CookedSource.HasManifest()) { + const auto CookedPath = CookedSource.Resolve( + Assets::AssetIdFromRelativePath(EffectiveRelativePath)); + if (CookedPath.has_value()) { + const auto CookedHDR = Assets::LoadCookedHDRTextureAsset(*CookedPath); + if (CookedHDR.has_value()) { + Loaded = std::make_shared(*CookedHDR); + } + } + } + } + if (!Loaded) { + A_CORE_WARN("{}: failed to load HDR '{}'", LogContext, + Settings.SkyboxHDRPath); + } + Settings.SkyboxHDRData = std::move(Loaded); +} + +InstanceHandle FindInstanceByIdRecursive(const InstancePool &Pool, + InstanceHandle Root, + std::string_view Id) { + const Instance *RootNode = Pool.Resolve(Root); + if (RootNode == nullptr) return {}; + if (RootNode->GetName() == Id) return Root; + for (const InstanceHandle Child : RootNode->GetChildren()) { + if (const InstanceHandle Found = FindInstanceByIdRecursive(Pool, Child, Id)) { + return Found; + } + } + return {}; +} + +const EditorSceneItem *FindSceneItemByHandleRecursive( + const std::vector &Items, SceneObjectHandle Handle) { + for (const EditorSceneItem &Item : Items) { + if (Item.Handle == Handle) { + return &Item; + } + if (const EditorSceneItem *Found = + FindSceneItemByHandleRecursive(Item.Children, Handle); + Found != nullptr) { + return Found; + } + } + return nullptr; +} +} // namespace + +EditorSession::EditorSession(SessionId Session, EditorSessionConfig Config) + : m_Config(Config), + m_State({.Session = Session}), + m_CommandDispatcher(std::make_unique(*this)), + m_SceneStateManager(std::make_unique(*this)), + m_ValidationModule(std::make_unique(*this)) { + m_SceneStateManager->InitSceneRoot(); +} + +EditorSession::~EditorSession() = default; +EditorSession::EditorSession(EditorSession &&) noexcept = default; +EditorSession &EditorSession::operator=(EditorSession &&) noexcept = default; + +void EditorSession::Submit(const CommandContext &Context, + const EditorCommand &Command) { + m_MessageBus.EnqueueCommand(Context, Command); +} + +void EditorSession::Tick(float DeltaTimeSeconds) { + m_MessageBus.DispatchQueuedCommands( + [this](const QueuedEditorCommand &QueuedCommand) { + m_CommandDispatcher->ProcessCommand(QueuedCommand); + }); + StepRuntimePhysics(DeltaTimeSeconds); +} + +void EditorSession::Subscribe(IEditorEventSubscriber *Subscriber) { + m_MessageBus.Subscribe(Subscriber); +} + +void EditorSession::Unsubscribe(IEditorEventSubscriber *Subscriber) { + m_MessageBus.Unsubscribe(Subscriber); +} + +void EditorSession::SetContentDir(std::filesystem::path ContentDir) { + m_ContentDir = std::move(ContentDir); + if (!m_State.Scene.WorldSettings.SkyboxHDRPath.empty()) { + SetWorldSettingsHDRData(nullptr); + RefreshWorldSettingsHDR("SetContentDir"); + } +} + +void EditorSession::SetEngineContentDir(std::filesystem::path EngineContentDir) { + m_EngineContentDir = std::move(EngineContentDir); + if (!m_State.Scene.WorldSettings.SkyboxHDRPath.empty()) { + SetWorldSettingsHDRData(nullptr); + RefreshWorldSettingsHDR("SetEngineContentDir"); + } +} + +void EditorSession::EnsureViewportState(SessionUserId User) { + EnsureViewport(User); +} + +void EditorSession::SetPresenceState(SessionUserId User, + EditorUserPresenceState State) { + const auto [It, Inserted] = m_State.PresenceByUser.try_emplace(User); + EditorUserPresence &Presence = It->second; + if (Inserted) { + Presence.User = User; + Presence.DisplayName = DefaultUserDisplayName(User); + Presence.IsLocal = User.Value == 1; + } + if (!Inserted && Presence.State == State) return; + + Presence.State = State; + PublishPresenceChangedEvent(User); +} + +void EditorSession::SetSceneState(EditorSceneState SceneState) { + m_SceneStateManager->SetSceneState(std::move(SceneState)); +} + +void EditorSession::SetSceneMeshInstances( + std::vector SceneMeshInstances) { + ReplaceSceneMeshInstances(std::move(SceneMeshInstances)); + RebuildSceneHandleState(); +} + +void EditorSession::SetSceneItems(std::vector SceneItems) { + m_SceneStateManager->SetSceneItems(std::move(SceneItems)); +} + +void EditorSession::SetObjectDetails( + std::vector ObjectDetails) { + m_SceneStateManager->SetObjectDetails(std::move(ObjectDetails)); +} + +void EditorSession::ReplaceSceneItems(std::vector SceneItems) { + m_State.Scene.Items = std::move(SceneItems); +} + +void EditorSession::ClearSceneItems() { + m_State.Scene.Items.clear(); +} + +void EditorSession::AddSceneItem(EditorSceneItem Item) { + m_State.Scene.Items.push_back(std::move(Item)); +} + +void EditorSession::ReplaceSceneMeshInstances( + std::vector SceneMeshInstances) { + m_State.Scene.MeshInstances = std::move(SceneMeshInstances); +} + +void EditorSession::ReplaceWorldSettings(EditorWorldSettings Settings) { + m_State.Scene.WorldSettings = std::move(Settings); +} + +void EditorSession::SetWorldSettingsHDRData(HDRTextureSourceDataRef HDRData) { + m_State.Scene.WorldSettings.SkyboxHDRData = std::move(HDRData); +} + +void EditorSession::SetPresence(std::vector Presence) { + m_State.PresenceByUser.clear(); + for (EditorUserPresence &Entry : Presence) { + m_State.PresenceByUser.emplace(Entry.User, std::move(Entry)); + } +} + +void EditorSession::SetObjectCollaborationStates( + std::vector CollaborationStates) { + m_State.Scene.CollaborationByObjectId.clear(); + for (EditorObjectCollaborationState &Entry : CollaborationStates) { + m_State.Scene.CollaborationByObjectId.emplace(Entry.ObjectId, std::move(Entry)); + } + RebuildSceneHandleState(); +} + +const EditorViewportState *EditorSession::FindViewport(SessionUserId User) const { + const auto It = m_State.Viewports.find(User); + return It != m_State.Viewports.end() ? &It->second : nullptr; +} + +const EditorSceneItem *EditorSession::FindSceneItem(std::string_view ObjectId) const { + return m_SceneStateManager->FindSceneItem(ObjectId); +} + +const EditorSceneItem *EditorSession::FindSceneItem(SceneObjectHandle Handle) const { + if (!Handle) { + return nullptr; + } + return FindSceneItemByHandleRecursive(m_State.Scene.Items, Handle); +} + +const std::string *EditorSession::FindSelectedObjectId(SessionUserId User) const { + const auto It = m_State.SelectedObjectIds.find(User); + return It != m_State.SelectedObjectIds.end() ? &It->second : nullptr; +} + +const SceneObjectHandle *EditorSession::FindSelectedObjectHandle( + SessionUserId User) const { + const auto It = m_SelectedObjectHandles.find(User); + return It != m_SelectedObjectHandles.end() ? &It->second : nullptr; +} + +bool EditorSession::HasSelectedObjectHandle(SessionUserId User, + SceneObjectHandle Handle) const { + const SceneObjectHandle *SelectedHandle = FindSelectedObjectHandle(User); + return SelectedHandle != nullptr && *SelectedHandle == Handle; +} + +void EditorSession::SetSelectedObject(SessionUserId User, std::string_view ObjectId, + SceneObjectHandle Handle) { + m_SelectedObjectHandles[User] = Handle; + m_State.SelectedObjectIds[User] = std::string(ObjectId); +} + +void EditorSession::ClearSelectedObjectsForHandle(SceneObjectHandle Handle) { + for (auto It = m_SelectedObjectHandles.begin(); It != m_SelectedObjectHandles.end();) { + if (It->second == Handle) { + m_State.SelectedObjectIds.erase(It->first); + It = m_SelectedObjectHandles.erase(It); + } else { + ++It; + } + } +} + +void EditorSession::PruneInvalidSelectedObjects() { + for (auto It = m_SelectedObjectHandles.begin(); It != m_SelectedObjectHandles.end();) { + if (FindSceneItem(It->second) == nullptr) { + m_State.SelectedObjectIds.erase(It->first); + It = m_SelectedObjectHandles.erase(It); + continue; + } + + if (const std::string *ObjectId = ResolveObjectId(It->second); + ObjectId != nullptr) { + m_State.SelectedObjectIds[It->first] = *ObjectId; + } + ++It; + } +} + +const EditorObjectDetails *EditorSession::FindObjectDetails( + std::string_view ObjectId) const { + const auto It = m_State.Scene.ObjectDetailsById.find(std::string(ObjectId)); + return It != m_State.Scene.ObjectDetailsById.end() ? &It->second : nullptr; +} + +const EditorObjectDetails *EditorSession::FindObjectDetails( + SceneObjectHandle Handle) const { + if (const std::string *ObjectId = ResolveObjectId(Handle); ObjectId != nullptr) { + return FindObjectDetails(*ObjectId); + } + return nullptr; +} + +EditorObjectDetails *EditorSession::FindMutableObjectDetails( + std::string_view ObjectId) { + const auto It = m_State.Scene.ObjectDetailsById.find(std::string(ObjectId)); + return It != m_State.Scene.ObjectDetailsById.end() ? &It->second : nullptr; +} + +bool EditorSession::HasObjectDetails(std::string_view ObjectId) const { + return m_State.Scene.ObjectDetailsById.count(std::string(ObjectId)) > 0; +} + +bool EditorSession::HasObjectDisplayName(std::string_view DisplayName) const { + for (const auto &[ObjectId, Details] : m_State.Scene.ObjectDetailsById) { + (void)ObjectId; + if (Details.DisplayName == DisplayName) { + return true; + } + } + return false; +} + +bool EditorSession::InsertObjectDetails(EditorObjectDetails Details) { + const std::string ObjectId = Details.ObjectId; + return m_State.Scene.ObjectDetailsById + .emplace(ObjectId, std::move(Details)) + .second; +} + +bool EditorSession::UpsertObjectDetails(EditorObjectDetails Details) { + const std::string ObjectId = Details.ObjectId; + m_State.Scene.ObjectDetailsById[ObjectId] = std::move(Details); + return true; +} + +void EditorSession::EraseObjectDetails(std::string_view ObjectId) { + m_State.Scene.ObjectDetailsById.erase(std::string(ObjectId)); +} + +void EditorSession::ReplaceObjectDetails( + std::unordered_map ObjectDetailsById) { + m_State.Scene.ObjectDetailsById = std::move(ObjectDetailsById); +} + +const EditorObjectDetails *EditorSession::FindSelectedObjectDetails( + SessionUserId User) const { + if (const SceneObjectHandle *SelectedHandle = FindSelectedObjectHandle(User); + SelectedHandle != nullptr) { + return FindObjectDetails(*SelectedHandle); + } + const std::string *SelectedObjectId = FindSelectedObjectId(User); + return SelectedObjectId != nullptr ? FindObjectDetails(*SelectedObjectId) + : nullptr; +} + +const EditorUserPresence *EditorSession::FindPresence(SessionUserId User) const { + const auto It = m_State.PresenceByUser.find(User); + return It != m_State.PresenceByUser.end() ? &It->second : nullptr; +} + +EditorParticipant EditorSession::BuildParticipant(SessionUserId User) const { + EditorParticipant Participant{}; + Participant.User = User; + Participant.DisplayName = DefaultUserDisplayName(User); + Participant.PresentationColor = DefaultPresentationColor(User); + + if (const EditorUserPresence *Presence = FindPresence(User); Presence != nullptr) { + Participant.DisplayName = Presence->DisplayName; + Participant.State = Presence->State; + Participant.IsLocal = Presence->IsLocal; + } + if (const std::string *SelectedObjectId = FindSelectedObjectId(User); + SelectedObjectId != nullptr) { + Participant.SelectedObjectId = *SelectedObjectId; + } + if (const EditorViewportState *Viewport = FindViewport(User); Viewport != nullptr) { + Participant.Camera = EditorParticipant::CameraState{ + .Position = Viewport->Camera.GetPosition(), + .YawDegrees = Viewport->Camera.GetYawDegrees(), + .PitchDegrees = Viewport->Camera.GetPitchDegrees(), + }; + } + + return Participant; +} + +std::vector EditorSession::BuildParticipants( + SessionUserId CurrentUser) const { + std::vector Participants; + Participants.reserve(m_State.PresenceByUser.size()); + for (const auto &[User, Presence] : m_State.PresenceByUser) { + (void)Presence; + EditorParticipant Participant = BuildParticipant(User); + Participant.IsLocal = User.Value == CurrentUser.Value; + Participants.push_back(std::move(Participant)); + } + return Participants; +} + +SessionUserId EditorSession::ResolveRuntimeControllerUser() const { + if (m_State.RuntimeControllerUser.has_value()) { + const SessionUserId User = *m_State.RuntimeControllerUser; + if (const EditorUserPresence *Presence = FindPresence(User); + Presence != nullptr && + Presence->State != EditorUserPresenceState::Disconnected) { + return User; + } + } + + std::optional Candidate; + for (const auto &[User, Presence] : m_State.PresenceByUser) { + if (Presence.State == EditorUserPresenceState::Disconnected || IsHostUser(User)) { + continue; + } + if (!Candidate.has_value() || User.Value < Candidate->Value) { + Candidate = User; + } + } + return Candidate.value_or(SessionUserId{1}); +} + +const EditorObjectCollaborationState *EditorSession::FindCollaborationState( + std::string_view ObjectId) const { + return FindCollaborationState(ResolveObjectHandle(ObjectId)); +} + +const EditorObjectCollaborationState *EditorSession::FindCollaborationState( + SceneObjectHandle Handle) const { + const auto It = m_CollaborationByHandle.find(Handle); + return It != m_CollaborationByHandle.end() ? &It->second : nullptr; +} + +void EditorSession::ReplaceCollaborationStatesByObjectId( + std::unordered_map + CollaborationByObjectId) { + m_State.Scene.CollaborationByObjectId = std::move(CollaborationByObjectId); +} + +void EditorSession::RemoveCollaborationState(std::string_view ObjectId) { + const std::string Key(ObjectId); + m_State.Scene.CollaborationByObjectId.erase(Key); + m_CollaborationByHandle.erase(ResolveObjectHandle(Key)); +} + +SceneObjectHandle EditorSession::ResolveObjectHandle(std::string_view ObjectId) const { + const auto It = m_ObjectHandleById.find(std::string(ObjectId)); + return It != m_ObjectHandleById.end() ? It->second : SceneObjectHandle{}; +} + +const std::string *EditorSession::ResolveObjectId(SceneObjectHandle Handle) const { + const auto It = m_ObjectIdByHandle.find(Handle); + return It != m_ObjectIdByHandle.end() ? &It->second : nullptr; +} + +void EditorSession::AcquireLock(const std::string &ObjectId, SessionUserId User) { + const SceneObjectHandle Handle = ResolveObjectHandle(ObjectId); + if (!Handle) { + return; + } + + auto &Collab = m_CollaborationByHandle[Handle]; + if (Collab.LockState == EditorObjectLockState::Locked && Collab.LockOwner != User) { + return; + } + Collab.ObjectHandle = Handle; + Collab.ObjectId = ObjectId; + Collab.LockState = EditorObjectLockState::Locked; + Collab.LockOwner = User; + m_State.Scene.CollaborationByObjectId[ObjectId] = Collab; + PublishEvent({.Payload = ObjectLockChangedEvent{ + .ObjectId = ObjectId, + .LockState = EditorObjectLockState::Locked, + .LockOwner = User, + }}); +} + +void EditorSession::ReleaseLock(const std::string &ObjectId, SessionUserId User) { + const SceneObjectHandle Handle = ResolveObjectHandle(ObjectId); + const auto It = m_CollaborationByHandle.find(Handle); + if (It == m_CollaborationByHandle.end()) return; + if (It->second.LockOwner != User) return; + + It->second.LockState = EditorObjectLockState::Unlocked; + It->second.LockOwner = std::nullopt; + m_State.Scene.CollaborationByObjectId[ObjectId] = It->second; + PublishEvent({.Payload = ObjectLockChangedEvent{ + .ObjectId = ObjectId, + .LockState = EditorObjectLockState::Unlocked, + .LockOwner = std::nullopt, + }}); +} + +void EditorSession::ReleaseAllLocksForUser(SessionUserId User) { + for (auto &[Handle, Collab] : m_CollaborationByHandle) { + if (Collab.LockOwner == User && Collab.LockState == EditorObjectLockState::Locked) { + Collab.LockState = EditorObjectLockState::Unlocked; + Collab.LockOwner = std::nullopt; + if (const std::string *ObjectId = ResolveObjectId(Handle); + ObjectId != nullptr) { + m_State.Scene.CollaborationByObjectId[*ObjectId] = Collab; + } + PublishEvent({.Payload = ObjectLockChangedEvent{ + .ObjectId = + ResolveObjectId(Handle) != nullptr + ? *ResolveObjectId(Handle) + : std::string(), + .LockState = EditorObjectLockState::Unlocked, + .LockOwner = std::nullopt, + }}); + } + } +} + +void EditorSession::PublishPresenceChangedEvent(SessionUserId User) { + const EditorParticipant Participant = BuildParticipant(User); + PublishEvent({.Payload = PresenceChangedEvent{ + .User = User, + .DisplayName = Participant.DisplayName, + .IsLocal = Participant.IsLocal, + .PresenceState = PresenceStateName(Participant.State), + .SelectedObjectId = Participant.SelectedObjectId, + }}); +} + +EditorUserPresence &EditorSession::EnsurePresence(SessionUserId User) { + const auto [It, Inserted] = m_State.PresenceByUser.try_emplace(User); + if (Inserted) { + It->second.User = User; + It->second.DisplayName = DefaultUserDisplayName(User); + It->second.State = EditorUserPresenceState::Connected; + It->second.IsLocal = User.Value == 1; + } else { + It->second.State = EditorUserPresenceState::Connected; + } + return It->second; +} + +EditorViewportState &EditorSession::EnsureViewport(SessionUserId User) { + EnsurePresence(User); + const auto [It, Inserted] = m_State.Viewports.try_emplace(User); + if (Inserted) { + It->second.Camera.LookAt(m_Config.InitialCameraPosition, + m_Config.InitialCameraTarget); + It->second.Camera.SetPerspective( + m_Config.CameraVerticalFovDegrees, m_Config.CameraAspectRatio, + m_Config.CameraNearPlane, m_Config.CameraFarPlane); + } + return It->second; +} + +SceneObjectHandle EditorSession::AllocateSceneObjectHandle() { + return SceneObjectHandle{m_NextSceneObjectHandle.Value++}; +} + +SceneObjectHandle EditorSession::EnsureHandleForObjectId( + std::string_view ObjectId, SceneObjectHandle PreferredHandle) { + if (ObjectId.empty()) { + return {}; + } + + const std::string Key(ObjectId); + if (const auto Existing = m_ObjectHandleById.find(Key); + Existing != m_ObjectHandleById.end()) { + return Existing->second; + } + + SceneObjectHandle Handle = PreferredHandle; + if (!Handle || m_ObjectIdByHandle.contains(Handle)) { + Handle = AllocateSceneObjectHandle(); + } else if (Handle.Value >= m_NextSceneObjectHandle.Value) { + m_NextSceneObjectHandle.Value = Handle.Value + 1; + } + + m_ObjectHandleById.emplace(Key, Handle); + m_ObjectIdByHandle.emplace(Handle, Key); + return Handle; +} + +void EditorSession::RebuildSceneHandleState() { + std::unordered_map PreviousHandleById = + m_ObjectHandleById; + std::unordered_map + NewObjectIdByHandle; + std::unordered_map NewHandleById; + std::unordered_set UsedHandles; + uint64_t MaxHandle = 0; + + auto ReserveHandle = [&](std::string_view ObjectId, + SceneObjectHandle PreferredHandle) { + if (ObjectId.empty()) { + return SceneObjectHandle{}; + } + + const std::string Key(ObjectId); + if (const auto Assigned = NewHandleById.find(Key); Assigned != NewHandleById.end()) { + return Assigned->second; + } + + SceneObjectHandle Handle = PreferredHandle; + if (!Handle) { + if (const auto Existing = PreviousHandleById.find(Key); + Existing != PreviousHandleById.end()) { + Handle = Existing->second; + } + } + if (!Handle || UsedHandles.contains(Handle.Value)) { + uint64_t Candidate = + std::max(m_NextSceneObjectHandle.Value, MaxHandle + 1); + while (UsedHandles.contains(Candidate)) { + ++Candidate; + } + Handle = SceneObjectHandle{Candidate}; + } + + UsedHandles.insert(Handle.Value); + MaxHandle = std::max(MaxHandle, Handle.Value); + NewHandleById[Key] = Handle; + NewObjectIdByHandle[Handle] = Key; + return Handle; + }; + + std::function &)> AssignItemHandles = + [&](std::vector &Items) { + for (EditorSceneItem &Item : Items) { + Item.Handle = ReserveHandle(Item.Id, Item.Handle); + AssignItemHandles(Item.Children); + } + }; + AssignItemHandles(m_State.Scene.Items); + + for (auto &[ObjectId, Details] : m_State.Scene.ObjectDetailsById) { + Details.Handle = ReserveHandle(ObjectId, Details.Handle); + } + for (EditorSceneMeshInstance &Instance : m_State.Scene.MeshInstances) { + Instance.ObjectHandle = ReserveHandle(Instance.ObjectId, Instance.ObjectHandle); + } + for (auto &[ObjectId, Collab] : m_State.Scene.CollaborationByObjectId) { + Collab.ObjectHandle = ReserveHandle(ObjectId, Collab.ObjectHandle); + Collab.ObjectId = ObjectId; + } + + m_ObjectHandleById = std::move(NewHandleById); + m_ObjectIdByHandle = std::move(NewObjectIdByHandle); + m_NextSceneObjectHandle.Value = std::max(m_NextSceneObjectHandle.Value, + MaxHandle + 1); + + m_CollaborationByHandle.clear(); + for (auto &[ObjectId, Collab] : m_State.Scene.CollaborationByObjectId) { + Collab.ObjectHandle = ResolveObjectHandle(ObjectId); + m_CollaborationByHandle[Collab.ObjectHandle] = Collab; + } + + m_SelectedObjectHandles.clear(); + for (auto It = m_State.SelectedObjectIds.begin(); It != m_State.SelectedObjectIds.end();) { + const SceneObjectHandle Handle = ResolveObjectHandle(It->second); + if (!Handle) { + It = m_State.SelectedObjectIds.erase(It); + continue; + } + m_SelectedObjectHandles[It->first] = Handle; + ++It; + } +} + +InstanceHandle EditorSession::FindInstanceById(std::string_view ObjectId) const { + return FindInstanceByIdRecursive(m_InstancePool, m_SceneRoot, ObjectId); +} + +bool EditorSession::IsValidTemplateId(const std::string &TemplateId) const { + return m_SceneStateManager->IsValidTemplateId(TemplateId); +} + +std::vector +EditorSession::CollectDescendantIds(InstanceHandle Root) const { + return m_SceneStateManager->CollectDescendantIds(Root); +} + +bool EditorSession::ValidateCommand(const QueuedEditorCommand &QueuedCommand, + std::string &FailureReason) const { + return m_ValidationModule != nullptr && + m_ValidationModule->ValidateCommand(QueuedCommand, FailureReason); +} + +InstanceHandle EditorSession::EnsureWorldFolder() { + return m_SceneStateManager != nullptr ? m_SceneStateManager->EnsureWorldFolder() + : InstanceHandle{}; +} + +std::string EditorSession::BuildUniqueObjectId(std::string_view BaseObjectId) const { + return m_SceneStateManager != nullptr + ? m_SceneStateManager->BuildUniqueObjectId(BaseObjectId) + : std::string(BaseObjectId); +} + +std::string +EditorSession::BuildUniqueDisplayName(std::string_view BaseDisplayName) const { + return m_SceneStateManager != nullptr + ? m_SceneStateManager->BuildUniqueDisplayName(BaseDisplayName) + : std::string(BaseDisplayName); +} + +InstanceHandle EditorSession::CreateInstanceForTemplate( + const std::string &TemplateId, const std::string &ObjectId) const { + return m_SceneStateManager != nullptr + ? m_SceneStateManager->CreateInstanceForTemplate(TemplateId, ObjectId) + : InstanceHandle{}; +} + +void EditorSession::SyncItemsFromTree() { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->SyncItemsFromTree(); + } +} + +void EditorSession::DeepCloneSubtree( + InstanceHandle Source, InstanceHandle DestParent, + std::vector &OutNewDetails) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->DeepCloneSubtree(Source, DestParent, OutNewDetails); + } +} + +void EditorSession::RemoveSceneObject(std::string_view ObjectId) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->RemoveSceneObject(ObjectId); + } +} + +bool EditorSession::UpdateSceneItemDisplayName(std::string_view ObjectId, + std::string_view DisplayName) { + return EditorSceneStateManager::UpdateSceneItemDisplayName( + m_State.Scene.Items, ObjectId, DisplayName); +} + +bool EditorSession::UpdateSceneItemVisibility(std::string_view ObjectId, + bool Visible) { + return EditorSceneStateManager::UpdateSceneItemVisibility( + m_State.Scene.Items, ObjectId, Visible); +} + +void EditorSession::RemoveGeneratedAssetChildren(std::string_view RootObjectId) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->RemoveGeneratedAssetChildren(RootObjectId); + } +} + +void EditorSession::ExpandMeshAssetIntoScene(std::string_view RootObjectId, + const MeshSceneData &SceneData, + std::string_view AssetPath) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->ExpandMeshAssetIntoScene(RootObjectId, SceneData, + AssetPath); + } +} + +EditorSceneMeshInstance * +EditorSession::FindMutableSceneMeshInstance(std::string_view ObjectId) { + const auto It = std::find_if( + m_State.Scene.MeshInstances.begin(), m_State.Scene.MeshInstances.end(), + [&ObjectId](const EditorSceneMeshInstance &MeshInstance) { + return MeshInstance.ObjectId == ObjectId; + }); + return It != m_State.Scene.MeshInstances.end() ? &*It : nullptr; +} + +void EditorSession::AddSceneMeshInstance(EditorSceneMeshInstance Instance) { + m_State.Scene.MeshInstances.push_back(std::move(Instance)); +} + +void EditorSession::RemoveSceneMeshInstances(std::string_view ObjectId) { + const std::string Id(ObjectId); + m_State.Scene.MeshInstances.erase( + std::remove_if(m_State.Scene.MeshInstances.begin(), + m_State.Scene.MeshInstances.end(), + [&Id](const EditorSceneMeshInstance &MeshInstance) { + return MeshInstance.ObjectId == Id; + }), + m_State.Scene.MeshInstances.end()); +} + +bool EditorSession::UpdateSceneMeshInstanceTransform( + SceneObjectHandle Handle, const glm::mat4 &Transform) { + for (EditorSceneMeshInstance &Instance : m_State.Scene.MeshInstances) { + if (Instance.ObjectHandle == Handle) { + Instance.Transform = Transform; + return true; + } + } + return false; +} + +void EditorSession::ClearSelectionsForObject(std::string_view ObjectId) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->ClearSelectionsForObject(ObjectId); + } +} + +void EditorSession::PruneInvalidSelections() { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->PruneInvalidSelections(); + } +} + +void EditorSession::RecomputeSubtreeWorldTransforms(InstanceHandle Node) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->RecomputeSubtreeWorldTransforms(Node); + } +} + +void EditorSession::RecomputeAllWorldTransforms() { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->RecomputeAllWorldTransforms(); + } +} + +void EditorSession::ApplyWorldTransform( + std::string_view ObjectId, const EditorTransformDetails &WorldTransform, + SessionUserId User, bool ShouldPublishEvent) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->ApplyWorldTransform(ObjectId, WorldTransform, User, + ShouldPublishEvent); + } +} + +void EditorSession::SetWorldSettings(const EditorWorldSettings &Settings) { + const std::string PreviousHDRPath = m_State.Scene.WorldSettings.SkyboxHDRPath; + HDRTextureSourceDataRef PreviousHDRData = m_State.Scene.WorldSettings.SkyboxHDRData; + + ReplaceWorldSettings(Settings); + if (Settings.SkyboxHDRPath.empty()) { + SetWorldSettingsHDRData(nullptr); + } else if (Settings.SkyboxHDRPath == PreviousHDRPath && PreviousHDRData) { + SetWorldSettingsHDRData(std::move(PreviousHDRData)); + } else { + RefreshWorldSettingsHDR("SetWorldSettings"); + } +} + +void EditorSession::RefreshWorldSettingsHDR(std::string_view LogContext) { + HydrateWorldSettingsHDRData(m_State.Scene.WorldSettings, m_ContentDir, + m_EngineContentDir, LogContext); +} + +void EditorSession::CaptureRuntimeSceneSnapshot() { + m_RuntimeSceneSnapshot = RuntimeSceneSnapshot{ + .Scene = CloneEditorSceneState(m_State.Scene), + .SelectedObjectIds = m_State.SelectedObjectIds, + .SelectedObjectHandles = m_SelectedObjectHandles, + }; +} + +bool EditorSession::RestoreRuntimeSceneSnapshot() { + if (!m_RuntimeSceneSnapshot.has_value()) { + return false; + } + + SetSceneState(std::move(m_RuntimeSceneSnapshot->Scene)); + m_State.SelectedObjectIds = std::move(m_RuntimeSceneSnapshot->SelectedObjectIds); + m_SelectedObjectHandles = + std::move(m_RuntimeSceneSnapshot->SelectedObjectHandles); + PruneInvalidSelections(); + m_RuntimeSceneSnapshot.reset(); + return true; +} + +bool EditorSession::IsBlankString(std::string_view Value) { + for (const char Character : Value) { + if (!IsWhitespace(Character)) { + return false; + } + } + return true; +} + +void EditorSession::PublishScriptError(const std::string &ObjectId, + const std::string &Message) { + PublishEvent({ScriptErrorEvent{.ObjectId = ObjectId, .Message = Message}}); +} + +void EditorSession::ApplyRuntimeWorldTransform( + std::string_view ObjectId, const EditorTransformDetails &Transform) { + if (m_SceneStateManager != nullptr) { + m_SceneStateManager->ApplyWorldTransform( + std::string(ObjectId), Transform, SessionUserId{1}, true); + } +} + +void EditorSession::SetRuntimePhysicsController( + std::unique_ptr Controller) { + m_RuntimePhysicsController = std::move(Controller); +} + +void EditorSession::EnsureRuntimePhysicsWorldStarted() { + if (m_RuntimePhysicsController != nullptr) { + m_RuntimePhysicsController->EnsurePhysicsWorldStarted(); + } +} + +void EditorSession::StopRuntimePhysicsWorld() { + if (m_RuntimePhysicsController != nullptr) { + m_RuntimePhysicsController->StopPhysicsWorld(); + } +} + +void EditorSession::StepRuntimePhysics(float DeltaTimeSeconds) { + if (m_RuntimePhysicsController != nullptr) { + m_RuntimePhysicsController->StepRuntimePhysics(DeltaTimeSeconds); + } +} + +void EditorSession::PublishEvent(const EditorEvent &Event) { + m_MessageBus.PublishEvent(Event); +} +} // namespace Axiom diff --git a/Axiom/Session/EditorSession.h b/Axiom/Scene/Session/EditorSession.h similarity index 55% rename from Axiom/Session/EditorSession.h rename to Axiom/Scene/Session/EditorSession.h index 0b9a2ff5..7b4adf6d 100644 --- a/Axiom/Session/EditorSession.h +++ b/Axiom/Scene/Session/EditorSession.h @@ -1,11 +1,13 @@ #pragma once +#include "CoreInstance/InstancePool.h" #include "CoreInstance/SceneInstances.h" #include "Renderer/Camera.h" #include "Renderer/Mesh.h" #include "Session/EditorCommand.h" #include "Session/EditorEvent.h" #include "Session/EditorMessageBus.h" +#include "Session/RuntimeSceneState.h" #include #include @@ -19,7 +21,10 @@ #include namespace Axiom { -class PhysicsWorld; +class EditorCommandDispatcher; +class IEditorRuntimePhysicsController; +class EditorSceneStateManager; +class EditorSessionValidationModule; struct EditorSessionConfig { glm::vec3 InitialCameraPosition{0.0f, 0.8f, 3.5f}; @@ -44,6 +49,7 @@ struct EditorViewportState { enum class EditorSceneItemKind { Folder, Mesh, Light, Camera, Actor }; struct EditorSceneItem { + SceneObjectHandle Handle{}; std::string Id; std::string DisplayName; EditorSceneItemKind Kind{EditorSceneItemKind::Mesh}; @@ -51,26 +57,21 @@ struct EditorSceneItem { std::vector Children; }; -struct EditorTransformDetails { - glm::vec3 Location{0.0f}; - glm::vec3 RotationDegrees{0.0f}; - glm::vec3 Scale{1.0f}; -}; - struct EditorLightProperties { glm::vec3 Color{1.0f}; float Intensity{1.0f}; - glm::vec3 Direction{0.35f, 0.7f, 0.2f}; // world-space (need not be normalized) + glm::vec3 Direction{0.35f, 0.7f, 0.2f}; }; struct EditorMaterialProperties { glm::vec4 BaseColorFactor{1.0f}; float Metallic{0.0f}; float Roughness{0.5f}; - std::optional TextureAssetPath; // content-relative path, nullopt = embedded + std::optional TextureAssetPath; }; struct EditorObjectDetails { + SceneObjectHandle Handle{}; std::string ObjectId; std::string DisplayName; EditorSceneItemKind Kind{EditorSceneItemKind::Mesh}; @@ -78,14 +79,14 @@ struct EditorObjectDetails { bool IsGeneratedAssetChild{false}; bool SupportsTransform{false}; bool TransformReadOnly{true}; - std::optional Transform; // local-space - std::optional WorldTransform; // world-space (computed) - std::optional ScriptClass; // C# script class name (Actor objects only) - std::optional Light; // Light objects only - std::optional Material; // Mesh objects only + std::optional Transform; + std::optional WorldTransform; + std::optional ScriptClass; + std::optional Light; + std::optional Material; std::optional Physics; std::optional GeneratedFromAssetRootId; - std::string AssetRelativePath; // content-relative path when assigned directly to this object + std::string AssetRelativePath; }; enum class EditorUserPresenceState { Connected, Away, Disconnected }; @@ -115,18 +116,20 @@ struct EditorParticipant { }; struct EditorObjectCollaborationState { + SceneObjectHandle ObjectHandle{}; std::string ObjectId; EditorObjectLockState LockState{EditorObjectLockState::Unlocked}; std::optional LockOwner; }; struct EditorSceneMeshInstance { + SceneObjectHandle ObjectHandle{}; std::string ObjectId; MeshData Mesh; - MaterialInstanceRef Material; + std::shared_ptr Material; MeshRenderPath RenderPath{MeshRenderPath::Graphics}; glm::mat4 Transform{1.0f}; - std::string AssetRelativePath; // content-relative path, empty if using startup default + std::string AssetRelativePath; }; struct EditorSceneState { @@ -168,13 +171,12 @@ class EditorSession final : public IEditorCommandSink { void Subscribe(IEditorEventSubscriber *Subscriber); void Unsubscribe(IEditorEventSubscriber *Subscriber); - // Must be called before SetMeshAssetCommand can be processed. void SetContentDir(std::filesystem::path ContentDir); const std::filesystem::path &GetContentDir() const { return m_ContentDir; } - - // Optional fallback for engine-bundled assets (paths prefixed with "Engine/"). void SetEngineContentDir(std::filesystem::path EngineContentDir); - const std::filesystem::path &GetEngineContentDir() const { return m_EngineContentDir; } + const std::filesystem::path &GetEngineContentDir() const { + return m_EngineContentDir; + } void EnsureViewportState(SessionUserId User); void SetPresenceState(SessionUserId User, EditorUserPresenceState State); @@ -189,150 +191,153 @@ class EditorSession final : public IEditorCommandSink { const EditorSessionState &GetState() const { return m_State; } const EditorSessionConfig &GetConfig() const { return m_Config; } - const DataModel *GetSceneRoot() const { return m_SceneRoot.get(); } + InstanceHandle GetSceneRoot() const { return m_SceneRoot; } + void SetSceneRoot(InstanceHandle SceneRoot) { m_SceneRoot = SceneRoot; } + const InstancePool &GetInstancePool() const { return m_InstancePool; } + InstancePool &GetInstancePool() { return m_InstancePool; } const EditorViewportState *FindViewport(SessionUserId User) const; + EditorViewportState &EnsureViewport(SessionUserId User); const EditorSceneItem *FindSceneItem(std::string_view ObjectId) const; + const EditorSceneItem *FindSceneItem(SceneObjectHandle Handle) const; const std::string *FindSelectedObjectId(SessionUserId User) const; + const SceneObjectHandle *FindSelectedObjectHandle(SessionUserId User) const; + bool HasSelectedObjectHandle(SessionUserId User, SceneObjectHandle Handle) const; + void SetSelectedObject(SessionUserId User, std::string_view ObjectId, + SceneObjectHandle Handle); + void ClearSelectedObjectsForHandle(SceneObjectHandle Handle); + void PruneInvalidSelectedObjects(); const EditorObjectDetails *FindObjectDetails(std::string_view ObjectId) const; + const EditorObjectDetails *FindObjectDetails(SceneObjectHandle Handle) const; + EditorObjectDetails *FindMutableObjectDetails(std::string_view ObjectId); + bool HasObjectDetails(std::string_view ObjectId) const; + bool HasObjectDisplayName(std::string_view DisplayName) const; + bool InsertObjectDetails(EditorObjectDetails Details); + bool UpsertObjectDetails(EditorObjectDetails Details); + void EraseObjectDetails(std::string_view ObjectId); + void ReplaceObjectDetails( + std::unordered_map ObjectDetailsById); const EditorObjectDetails *FindSelectedObjectDetails(SessionUserId User) const; const EditorUserPresence *FindPresence(SessionUserId User) const; + EditorUserPresence &EnsurePresence(SessionUserId User); EditorParticipant BuildParticipant(SessionUserId User) const; std::vector BuildParticipants(SessionUserId CurrentUser) const; SessionUserId ResolveRuntimeControllerUser() const; const EditorObjectCollaborationState *FindCollaborationState( std::string_view ObjectId) const; + const EditorObjectCollaborationState *FindCollaborationState( + SceneObjectHandle Handle) const; + const auto &GetCollaborationByObjectId() const { + return m_State.Scene.CollaborationByObjectId; + } + void ReplaceCollaborationStatesByObjectId( + std::unordered_map + CollaborationByObjectId); + void RemoveCollaborationState(std::string_view ObjectId); EditorRuntimeState GetRuntimeState() const { return m_State.RuntimeState; } + SceneObjectHandle ResolveObjectHandle(std::string_view ObjectId) const; + const std::string *ResolveObjectId(SceneObjectHandle Handle) const; + SceneObjectHandle EnsureHandleForObjectId( + std::string_view ObjectId, SceneObjectHandle PreferredHandle = {}); + void RebuildSceneHandleState(); + InstanceHandle FindInstanceById(std::string_view ObjectId) const; + bool IsValidTemplateId(const std::string &TemplateId) const; + std::vector CollectDescendantIds(InstanceHandle Root) const; - void AcquireLock(const std::string &ObjectId, SessionUserId User); - void ReleaseLock(const std::string &ObjectId, SessionUserId User); - void ReleaseAllLocksForUser(SessionUserId User); - void PublishScriptError(const std::string &ObjectId, const std::string &Message); + const auto &GetSceneItems() const { return m_State.Scene.Items; } + void ReplaceSceneItems(std::vector SceneItems); + void ClearSceneItems(); + void AddSceneItem(EditorSceneItem Item); + const auto &GetSceneMeshInstances() const { return m_State.Scene.MeshInstances; } + void ReplaceSceneMeshInstances( + std::vector SceneMeshInstances); + void ReplaceWorldSettings(EditorWorldSettings Settings); + const EditorWorldSettings &GetWorldSettings() const { + return m_State.Scene.WorldSettings; + } + void SetWorldSettingsHDRData(HDRTextureSourceDataRef HDRData); -private: - static std::unordered_map - BuildObjectDetailsMap(std::vector ObjectDetails); - static bool IsBlankString(std::string_view Value); + bool ValidateCommand(const QueuedEditorCommand &QueuedCommand, + std::string &FailureReason) const; + void PublishEvent(const EditorEvent &Event); + InstanceHandle EnsureWorldFolder(); std::string BuildUniqueObjectId(std::string_view BaseObjectId) const; std::string BuildUniqueDisplayName(std::string_view BaseDisplayName) const; - bool IsSceneObjectIdInUse(std::string_view ObjectId) const; - bool IsSceneDisplayNameInUse(std::string_view DisplayName) const; - bool UpdateSceneItemDisplayName(std::vector &Items, - std::string_view ObjectId, - std::string_view DisplayName); - bool UpdateSceneItemVisibility(std::vector &Items, - std::string_view ObjectId, bool Visible); - bool RemoveSceneItem(std::vector &Items, - std::string_view ObjectId); - EditorSceneItem *FindSceneItemMutable(std::vector &Items, - std::string_view ObjectId); + InstanceHandle CreateInstanceForTemplate(const std::string &TemplateId, + const std::string &ObjectId) const; + void SyncItemsFromTree(); + void DeepCloneSubtree(InstanceHandle Source, InstanceHandle DestParent, + std::vector &OutNewDetails); void RemoveSceneObject(std::string_view ObjectId); - void ClearSelectionsForObject(std::string_view ObjectId); - void PruneInvalidSelections(); + bool UpdateSceneItemDisplayName(std::string_view ObjectId, + std::string_view DisplayName); + bool UpdateSceneItemVisibility(std::string_view ObjectId, bool Visible); void RemoveGeneratedAssetChildren(std::string_view RootObjectId); void ExpandMeshAssetIntoScene(std::string_view RootObjectId, const MeshSceneData &SceneData, std::string_view AssetPath); - // Instance tree management - void InitSceneRoot(); - Instance *FindWorldFolder() const; - Instance *EnsureWorldFolder(); - void RebuildInstanceTree(const std::vector &Items, - Instance *Parent); - void SyncItemsFromTree(); - EditorSceneItem BuildItemFromInstance(const Instance *Node) const; - Instance *CreateInstanceForTemplate(const std::string &TemplateId, - const std::string &ObjectId); - void DeepCloneSubtree(const Instance *Source, Instance *DestParent, - std::vector &OutNewDetails); - std::vector CollectDescendantIds(const Instance *Root) const; - EditorSceneItemKind KindForInstance(const Instance *Node) const; - bool IsValidTemplateId(const std::string &TemplateId) const; - glm::mat4 ComputeWorldTransformMatrix(const Instance *Node) const; - EditorTransformDetails DecomposeMatrix(const glm::mat4 &Matrix) const; - void RecomputeSubtreeWorldTransforms(const Instance *Node); + EditorSceneMeshInstance *FindMutableSceneMeshInstance(std::string_view ObjectId); + void AddSceneMeshInstance(EditorSceneMeshInstance Instance); + void RemoveSceneMeshInstances(std::string_view ObjectId); + bool UpdateSceneMeshInstanceTransform(SceneObjectHandle Handle, + const glm::mat4 &Transform); + void ClearSelectionsForObject(std::string_view ObjectId); + void PruneInvalidSelections(); + void RecomputeSubtreeWorldTransforms(InstanceHandle Node); void RecomputeAllWorldTransforms(); - void PublishPresenceChangedEvent(SessionUserId User); - EditorUserPresence &EnsurePresence(SessionUserId User); - EditorViewportState &EnsureViewport(SessionUserId User); - const EditorSceneItem *FindSceneItemRecursive( - const std::vector &Items, std::string_view ObjectId) const; - void ProcessCommand(const QueuedEditorCommand &QueuedCommand); - bool ValidateCommand(const QueuedEditorCommand &QueuedCommand, - std::string &FailureReason); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const UpdateViewportCameraCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetViewportCameraPoseCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetCameraProjectionCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetLookActiveCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SelectObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const RenameObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetObjectVisibilityCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const CreateObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const CreateMeshObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const DuplicateObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const DeleteObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const ReparentObjectCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetTransformCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const AttachScriptCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const DetachScriptCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetMeshAssetCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetLightPropertiesCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetMaterialPropertiesCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetMaterialTextureCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetPhysicsPropertiesCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const PlaySessionCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const PauseSessionCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const ResumeSessionCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const StopSessionCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetWorldSettingsCommand &Command); - void HandleCommand(const QueuedEditorCommand &QueuedCommand, - const PlaceActorCommand &Command); void ApplyWorldTransform(std::string_view ObjectId, const EditorTransformDetails &WorldTransform, SessionUserId User, bool PublishEvent); - void EnsurePhysicsWorldStarted(); - void StopPhysicsWorld(); - void StepRuntimePhysics(float DeltaTimeSeconds); - void PublishEvent(const EditorEvent &Event); + void SetWorldSettings(const EditorWorldSettings &Settings); + void RefreshWorldSettingsHDR(std::string_view LogContext); + void CaptureRuntimeSceneSnapshot(); + bool RestoreRuntimeSceneSnapshot(); + void SetRuntimeState(EditorRuntimeState State) { m_State.RuntimeState = State; } + void EnsureRuntimePhysicsWorldStarted(); + void StopRuntimePhysicsWorld(); + + void AcquireLock(const std::string &ObjectId, SessionUserId User); + void ReleaseLock(const std::string &ObjectId, SessionUserId User); + void ReleaseAllLocksForUser(SessionUserId User); + void PublishScriptError(const std::string &ObjectId, const std::string &Message); + void ApplyRuntimeWorldTransform(std::string_view ObjectId, + const EditorTransformDetails &Transform); + void SetRuntimePhysicsController( + std::unique_ptr Controller); + static bool IsBlankString(std::string_view Value); private: struct RuntimeSceneSnapshot { EditorSceneState Scene; std::unordered_map SelectedObjectIds; + std::unordered_map + SelectedObjectHandles; }; + void PublishPresenceChangedEvent(SessionUserId User); + SceneObjectHandle AllocateSceneObjectHandle(); + void StepRuntimePhysics(float DeltaTimeSeconds); + EditorSessionConfig m_Config; EditorSessionState m_State; EditorMessageBus m_MessageBus; - std::unique_ptr m_SceneRoot; + std::unique_ptr m_CommandDispatcher; + std::unique_ptr m_RuntimePhysicsController; + std::unique_ptr m_SceneStateManager; + std::unique_ptr m_ValidationModule; + InstancePool m_InstancePool; + InstanceHandle m_SceneRoot{}; std::filesystem::path m_ContentDir; std::filesystem::path m_EngineContentDir; std::optional m_RuntimeSceneSnapshot; - std::unique_ptr m_PhysicsWorld; + SceneObjectHandle m_NextSceneObjectHandle{1}; + std::unordered_map m_ObjectHandleById; + std::unordered_map + m_ObjectIdByHandle; + std::unordered_map + m_SelectedObjectHandles; + std::unordered_map + m_CollaborationByHandle; }; } // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSessionValidationModule.cpp b/Axiom/Scene/Session/EditorSessionValidationModule.cpp new file mode 100644 index 00000000..83824ad9 --- /dev/null +++ b/Axiom/Scene/Session/EditorSessionValidationModule.cpp @@ -0,0 +1,495 @@ +#include "Session/EditorSessionValidationModule.h" + +#include "Assets/AssetCooker.h" +#include "Assets/MeshAsset.h" + +#include + +namespace Axiom { +namespace { +InstanceHandle FindInstanceById(const InstancePool &Pool, InstanceHandle Root, + std::string_view Id) { + const Instance *RootNode = Pool.Resolve(Root); + if (RootNode == nullptr) { + return {}; + } + if (RootNode->GetName() == Id) { + return Root; + } + for (const InstanceHandle Child : RootNode->GetChildren()) { + if (const InstanceHandle Found = FindInstanceById(Pool, Child, Id)) { + return Found; + } + } + return {}; +} + +void CookMeshAssetBestEffort(const std::filesystem::path &ContentDir, + std::string_view RelativeAssetPath) { + if (ContentDir.empty() || RelativeAssetPath.empty()) { + return; + } + + const auto Cooked = Assets::CookMeshAsset(ContentDir, RelativeAssetPath); + if (!Cooked.has_value()) { + A_CORE_WARN("EditorSession: failed to cook mesh asset '{}'", + std::string(RelativeAssetPath)); + } +} + +bool IsAuthoringMutationCommand(const EditorCommandPayload &Payload) { + return std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload) || + std::holds_alternative(Payload); +} + +bool IsPositive(const glm::vec3 &Value) { + return Value.x > 0.0f && Value.y > 0.0f && Value.z > 0.0f; +} +} // namespace + +EditorSessionValidationModule::EditorSessionValidationModule( + EditorSession &Session) + : m_Session(Session) {} + +bool EditorSessionValidationModule::ValidateCommand( + const QueuedEditorCommand &QueuedCommand, std::string &FailureReason) const { + if (QueuedCommand.Context.Session != m_Session.GetState().Session) { + FailureReason = "Command targeted a different session."; + return false; + } + + if ((std::holds_alternative(QueuedCommand.Command.Payload) || + std::holds_alternative( + QueuedCommand.Command.Payload) || + std::holds_alternative( + QueuedCommand.Command.Payload) || + std::holds_alternative( + QueuedCommand.Command.Payload)) && + QueuedCommand.Context.User.Value != + m_Session.ResolveRuntimeControllerUser().Value) { + FailureReason = + "Only the current simulation host can control simulation state."; + return false; + } + + if (!QueuedCommand.Context.IsScriptContext && + m_Session.GetRuntimeState() != EditorRuntimeState::Edit && + IsAuthoringMutationCommand(QueuedCommand.Command.Payload)) { + FailureReason = + "Authoring edits are disabled while shared simulation is active."; + return false; + } + + const EditorViewportState &Viewport = const_cast(m_Session) + .EnsureViewport( + QueuedCommand.Context.User); + + if (const auto *CameraCommand = std::get_if( + &QueuedCommand.Command.Payload)) { + if (Viewport.IsLooking && !CameraCommand->CursorPosition.has_value()) { + FailureReason = "Look-enabled camera updates require cursor position."; + return false; + } + } + + if (const auto *SelectionCommand = + std::get_if(&QueuedCommand.Command.Payload)) { + if (SelectionCommand->ObjectId.empty()) { + FailureReason = "Selection commands require a non-empty object id."; + return false; + } + if (m_Session.FindSceneItem(SelectionCommand->ObjectId) == nullptr) { + FailureReason = "Selection targeted an unknown object."; + return false; + } + } + + { + std::string SingleId; + if (const auto *C = + std::get_if(&QueuedCommand.Command.Payload)) + SingleId = C->ObjectId; + else if (const auto *C = + std::get_if(&QueuedCommand.Command.Payload)) + SingleId = C->ObjectId; + else if (const auto *C = std::get_if( + &QueuedCommand.Command.Payload)) + SingleId = C->ObjectId; + else if (const auto *C = + std::get_if(&QueuedCommand.Command.Payload)) + SingleId = C->ObjectId; + else if (const auto *C = std::get_if( + &QueuedCommand.Command.Payload)) + SingleId = C->ObjectId; + else if (const auto *C = std::get_if( + &QueuedCommand.Command.Payload)) + SingleId = C->ObjectId; + if (!SingleId.empty()) { + const EditorObjectCollaborationState *Collab = + m_Session.FindCollaborationState(SingleId); + if (Collab != nullptr && + Collab->LockState == EditorObjectLockState::Locked && + Collab->LockOwner.has_value() && + *Collab->LockOwner != QueuedCommand.Context.User) { + FailureReason = "Object is locked by another user."; + return false; + } + } + } + + if (const auto *TransformCommand = + std::get_if(&QueuedCommand.Command.Payload)) { + if (TransformCommand->ObjectId.empty()) { + FailureReason = "Transform commands require a non-empty object id."; + return false; + } + + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(TransformCommand->ObjectId); + if (Details == nullptr) { + FailureReason = "Transform targeted an unknown object."; + return false; + } + if (!Details->SupportsTransform || !Details->Transform.has_value()) { + FailureReason = "This object does not support transform edits."; + return false; + } + if (Details->TransformReadOnly) { + FailureReason = "This object is read-only."; + return false; + } + if (TransformCommand->Scale.x <= 0.0f || + TransformCommand->Scale.y <= 0.0f || + TransformCommand->Scale.z <= 0.0f) { + FailureReason = "Scale values must be greater than zero."; + return false; + } + } + + if (const auto *PhysicsCommand = std::get_if( + &QueuedCommand.Command.Payload)) { + if (PhysicsCommand->ObjectId.empty()) { + FailureReason = "Physics commands require a non-empty object id."; + return false; + } + + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(PhysicsCommand->ObjectId); + if (Details == nullptr) { + FailureReason = "Physics targeted an unknown object."; + return false; + } + if (!Details->SupportsTransform) { + FailureReason = "Physics can only be assigned to transformable objects."; + return false; + } + if (PhysicsCommand->Physics.BodyType == EditorPhysicsBodyType::Dynamic && + PhysicsCommand->Physics.Mass <= 0.0f) { + FailureReason = "Dynamic physics bodies require a positive mass."; + return false; + } + if (PhysicsCommand->Physics.ColliderType == + EditorPhysicsColliderType::Box && + !IsPositive(PhysicsCommand->Physics.BoxHalfExtents)) { + FailureReason = "Box colliders require positive half extents."; + return false; + } + if (PhysicsCommand->Physics.ColliderType == + EditorPhysicsColliderType::Sphere && + PhysicsCommand->Physics.SphereRadius <= 0.0f) { + FailureReason = "Sphere colliders require a positive radius."; + return false; + } + if (PhysicsCommand->Physics.Friction < 0.0f) { + FailureReason = "Physics friction must be zero or greater."; + return false; + } + if (PhysicsCommand->Physics.Restitution < 0.0f) { + FailureReason = "Physics restitution must be zero or greater."; + return false; + } + } + + if (const auto *RenameCommand = + std::get_if(&QueuedCommand.Command.Payload)) { + if (RenameCommand->ObjectId.empty()) { + FailureReason = "Rename commands require a non-empty object id."; + return false; + } + if (m_Session.FindSceneItem(RenameCommand->ObjectId) == nullptr) { + FailureReason = "Rename targeted an unknown object."; + return false; + } + if (RenameCommand->DisplayName.empty() || + EditorSession::IsBlankString(RenameCommand->DisplayName)) { + FailureReason = "Rename commands require a non-empty display name."; + return false; + } + } + + if (const auto *VisibilityCommand = std::get_if( + &QueuedCommand.Command.Payload)) { + if (VisibilityCommand->ObjectId.empty()) { + FailureReason = "Visibility commands require a non-empty object id."; + return false; + } + if (m_Session.FindSceneItem(VisibilityCommand->ObjectId) == nullptr) { + FailureReason = "Visibility targeted an unknown object."; + return false; + } + } + + if (const auto *CreateCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (CreateCmd->TemplateId.empty()) { + FailureReason = "Create commands require a non-empty TemplateId."; + return false; + } + if (!m_Session.IsValidTemplateId(CreateCmd->TemplateId)) { + FailureReason = "Unknown TemplateId: " + CreateCmd->TemplateId + "."; + return false; + } + } + + if (const auto *CreateMeshCmd = std::get_if( + &QueuedCommand.Command.Payload)) { + if (CreateMeshCmd->AssetPath.empty()) { + FailureReason = "CreateMeshObject requires a non-empty asset path."; + return false; + } + if (CreateMeshCmd->Scale.x <= 0.0f || CreateMeshCmd->Scale.y <= 0.0f || + CreateMeshCmd->Scale.z <= 0.0f) { + FailureReason = "Scale values must be greater than zero."; + return false; + } + if (m_Session.GetContentDir().empty()) { + FailureReason = "CreateMeshObject requires a configured content directory."; + return false; + } + CookMeshAssetBestEffort(m_Session.GetContentDir(), CreateMeshCmd->AssetPath); + const std::filesystem::path FullPath = + m_Session.GetContentDir() / CreateMeshCmd->AssetPath; + const auto SceneData = Assets::LoadBasicMeshAsset(FullPath); + if (!SceneData.has_value() || SceneData->Instances.empty()) { + FailureReason = "CreateMeshObject failed to load mesh asset: " + + CreateMeshCmd->AssetPath + "."; + return false; + } + } + + if (const auto *DupCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (DupCmd->ObjectId.empty()) { + FailureReason = "Duplicate commands require a non-empty object id."; + return false; + } + if (m_Session.FindSceneItem(DupCmd->ObjectId) == nullptr) { + FailureReason = "Duplicate targeted an unknown object."; + return false; + } + } + + if (const auto *DelCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (DelCmd->ObjectId.empty()) { + FailureReason = "Delete commands require a non-empty object id."; + return false; + } + if (m_Session.FindSceneItem(DelCmd->ObjectId) == nullptr) { + FailureReason = "Delete targeted an unknown object."; + return false; + } + if (DelCmd->ObjectId == "world") { + FailureReason = "The world folder cannot be deleted."; + return false; + } + } + + if (const auto *ReparentCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (ReparentCmd->ObjectId.empty()) { + FailureReason = "Reparent commands require a non-empty object id."; + return false; + } + if (ReparentCmd->NewParentId.empty()) { + FailureReason = "Reparent commands require a non-empty new parent id."; + return false; + } + if (m_Session.FindSceneItem(ReparentCmd->ObjectId) == nullptr) { + FailureReason = "Reparent targeted an unknown object."; + return false; + } + if (m_Session.FindSceneItem(ReparentCmd->NewParentId) == nullptr) { + FailureReason = "Reparent new parent is an unknown object."; + return false; + } + if (ReparentCmd->ObjectId == ReparentCmd->NewParentId) { + FailureReason = "Cannot reparent an object onto itself."; + return false; + } + if (ReparentCmd->ObjectId == "world") { + FailureReason = "The world folder cannot be reparented."; + return false; + } + const InstanceHandle TargetHandle = + FindInstanceById(m_Session.GetInstancePool(), m_Session.GetSceneRoot(), + ReparentCmd->ObjectId); + if (TargetHandle) { + for (const std::string &DescId : + m_Session.CollectDescendantIds(TargetHandle)) { + if (DescId == ReparentCmd->NewParentId) { + FailureReason = + "Cannot reparent an object onto one of its descendants."; + return false; + } + } + } + } + + if (const auto *AttachCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (AttachCmd->ObjectId.empty()) { + FailureReason = "AttachScript requires a non-empty object id."; + return false; + } + if (AttachCmd->ScriptClassName.empty()) { + FailureReason = "AttachScript requires a non-empty script class name."; + return false; + } + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(AttachCmd->ObjectId); + if (Details == nullptr) { + FailureReason = "AttachScript targeted an unknown object."; + return false; + } + if (Details->Kind != EditorSceneItemKind::Actor) { + FailureReason = "Scripts can only be attached to Actor objects."; + return false; + } + } + + if (const auto *DetachCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (DetachCmd->ObjectId.empty()) { + FailureReason = "DetachScript requires a non-empty object id."; + return false; + } + if (m_Session.FindObjectDetails(DetachCmd->ObjectId) == nullptr) { + FailureReason = "DetachScript targeted an unknown object."; + return false; + } + } + + if (const auto *MeshAssetCmd = + std::get_if(&QueuedCommand.Command.Payload)) { + if (MeshAssetCmd->ObjectId.empty()) { + FailureReason = "SetMeshAsset requires a non-empty object id."; + return false; + } + if (MeshAssetCmd->AssetPath.empty()) { + FailureReason = "SetMeshAsset requires a non-empty asset path."; + return false; + } + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(MeshAssetCmd->ObjectId); + if (Details == nullptr) { + FailureReason = "SetMeshAsset targeted an unknown object."; + return false; + } + if (Details->Kind != EditorSceneItemKind::Mesh && + Details->Kind != EditorSceneItemKind::Actor) { + FailureReason = "SetMeshAsset target must be a Mesh or Actor object."; + return false; + } + } + + if (const auto *LightCmd = std::get_if( + &QueuedCommand.Command.Payload)) { + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(LightCmd->ObjectId); + if (Details == nullptr) { + FailureReason = "SetLightProperties targeted an unknown object."; + return false; + } + if (Details->Kind != EditorSceneItemKind::Light) { + FailureReason = "SetLightProperties target must be a Light object."; + return false; + } + } + + if (const auto *MatCmd = std::get_if( + &QueuedCommand.Command.Payload)) { + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(MatCmd->ObjectId); + if (Details == nullptr) { + FailureReason = "SetMaterialProperties targeted an unknown object."; + return false; + } + if (Details->Kind != EditorSceneItemKind::Mesh) { + FailureReason = "SetMaterialProperties target must be a Mesh object."; + return false; + } + } + + if (const auto *TexCmd = std::get_if( + &QueuedCommand.Command.Payload)) { + const EditorObjectDetails *Details = + m_Session.FindObjectDetails(TexCmd->ObjectId); + if (Details == nullptr) { + FailureReason = "SetMaterialTexture targeted an unknown object."; + return false; + } + if (Details->Kind != EditorSceneItemKind::Mesh) { + FailureReason = "SetMaterialTexture target must be a Mesh object."; + return false; + } + } + + if (std::holds_alternative(QueuedCommand.Command.Payload)) { + if (m_Session.GetRuntimeState() != EditorRuntimeState::Edit) { + FailureReason = "PlaySession is only valid while in edit mode."; + return false; + } + } + + if (std::holds_alternative( + QueuedCommand.Command.Payload)) { + if (m_Session.GetRuntimeState() != EditorRuntimeState::Playing) { + FailureReason = "PauseSession is only valid while playing."; + return false; + } + } + + if (std::holds_alternative( + QueuedCommand.Command.Payload)) { + if (m_Session.GetRuntimeState() != EditorRuntimeState::Paused) { + FailureReason = "ResumeSession is only valid while paused."; + return false; + } + } + + if (std::holds_alternative(QueuedCommand.Command.Payload)) { + if (m_Session.GetRuntimeState() == EditorRuntimeState::Edit) { + FailureReason = "StopSession is only valid while simulation is active."; + return false; + } + } + + return true; +} +} // namespace Axiom diff --git a/Axiom/Scene/Session/EditorSessionValidationModule.h b/Axiom/Scene/Session/EditorSessionValidationModule.h new file mode 100644 index 00000000..bcc7945b --- /dev/null +++ b/Axiom/Scene/Session/EditorSessionValidationModule.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace Axiom { +class EditorSessionValidationModule { +public: + explicit EditorSessionValidationModule(EditorSession &Session); + + bool ValidateCommand(const QueuedEditorCommand &QueuedCommand, + std::string &FailureReason) const; + +private: + EditorSession &m_Session; +}; +} // namespace Axiom diff --git a/Axiom/Session/MeshPicking.h b/Axiom/Scene/Session/MeshPicking.h similarity index 100% rename from Axiom/Session/MeshPicking.h rename to Axiom/Scene/Session/MeshPicking.h diff --git a/Axiom/Scene/Session/RuntimeSceneState.h b/Axiom/Scene/Session/RuntimeSceneState.h new file mode 100644 index 00000000..394ca6df --- /dev/null +++ b/Axiom/Scene/Session/RuntimeSceneState.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Session/SessionTypes.h" + +#include +#include +#include + +#include + +namespace Axiom { +struct RuntimePhysicsMaterial { + float Friction{0.2f}; + float Restitution{0.0f}; +}; + +struct RuntimeSceneBodyState { + SceneObjectHandle ObjectHandle{}; + std::string ObjectId; + EditorTransformDetails WorldTransform; + EditorPhysicsBodyType BodyType{EditorPhysicsBodyType::None}; + EditorPhysicsColliderType ColliderType{EditorPhysicsColliderType::None}; + glm::vec3 BoxHalfExtents{0.5f, 0.5f, 0.5f}; + float SphereRadius{0.5f}; + float Mass{1.0f}; + uint32_t MaterialIndex{0}; +}; + +struct RuntimeSceneState { + std::vector Materials; + std::vector Bodies; +}; +} // namespace Axiom diff --git a/Axiom/Session/SessionTypes.h b/Axiom/Scene/Session/SessionTypes.h similarity index 81% rename from Axiom/Session/SessionTypes.h rename to Axiom/Scene/Session/SessionTypes.h index a005ca45..750b6317 100644 --- a/Axiom/Session/SessionTypes.h +++ b/Axiom/Scene/Session/SessionTypes.h @@ -10,6 +10,12 @@ #include namespace Axiom { +struct EditorTransformDetails { + glm::vec3 Location{0.0f}; + glm::vec3 RotationDegrees{0.0f}; + glm::vec3 Scale{1.0f}; +}; + struct CommandId { uint64_t Value{0}; auto operator<=>(const CommandId &) const = default; @@ -30,6 +36,18 @@ struct SessionUserId { auto operator<=>(const SessionUserId &) const = default; }; +struct SceneObjectHandle { + uint64_t Value{0}; + auto operator<=>(const SceneObjectHandle &) const = default; + explicit operator bool() const { return Value != 0; } +}; + +struct SceneObjectHandleHash { + size_t operator()(const SceneObjectHandle &Handle) const noexcept { + return static_cast(Handle.Value); + } +}; + struct SessionUserIdHash { size_t operator()(const SessionUserId &Id) const noexcept { return static_cast(Id.Value); diff --git a/Axiom/Session/StartupScene.cpp b/Axiom/Scene/Session/StartupScene.cpp similarity index 100% rename from Axiom/Session/StartupScene.cpp rename to Axiom/Scene/Session/StartupScene.cpp diff --git a/Axiom/Session/StartupScene.h b/Axiom/Scene/Session/StartupScene.h similarity index 100% rename from Axiom/Session/StartupScene.h rename to Axiom/Scene/Session/StartupScene.h diff --git a/Axiom/Scripting/InternalCalls.h b/Axiom/Scripting/InternalCalls.h index 7f19ddb8..eb12cb56 100644 --- a/Axiom/Scripting/InternalCalls.h +++ b/Axiom/Scripting/InternalCalls.h @@ -3,10 +3,8 @@ #include #include -#if AXIOM_SCRIPTING_ENABLED #include #include -#endif namespace Axiom::InternalCalls { @@ -15,7 +13,6 @@ namespace Axiom::InternalCalls { void Bind(EditorSession &Session, SessionId Id, SessionUserId UserId, bool IsRestricted); -#if AXIOM_SCRIPTING_ENABLED // Function pointer targets — registered with Coral via AddInternalCall. // Signatures must match the C# delegate* unmanaged<> fields in GameObject.cs / // ScriptSecurity.cs. @@ -26,6 +23,5 @@ void GameObject_SetTransform(Coral::String ObjectId, const EditorTransformDetails *InTransform); Coral::Bool32 GameObject_GetVisible(Coral::String ObjectId); Coral::Bool32 ScriptSecurity_IsRestricted(); -#endif } // namespace Axiom::InternalCalls diff --git a/Axiom/Scripting/InternalCalls.cpp b/Axiom/Scripting/InternalCallsEnabled.cpp similarity index 57% rename from Axiom/Scripting/InternalCalls.cpp rename to Axiom/Scripting/InternalCallsEnabled.cpp index fc97f7b0..4d8064f8 100644 --- a/Axiom/Scripting/InternalCalls.cpp +++ b/Axiom/Scripting/InternalCallsEnabled.cpp @@ -3,11 +3,6 @@ #include #include -#if AXIOM_SCRIPTING_ENABLED -#include -#include -#endif - namespace { Axiom::EditorSession *s_Session = nullptr; Axiom::SessionId s_SessionId{1}; @@ -25,33 +20,26 @@ void Bind(EditorSession &Session, SessionId Id, SessionUserId UserId, s_IsRestricted = IsRestricted; } -// --------------------------------------------------------------------------- -// Internal call implementations -// These are called via Coral's function-pointer dispatch from managed C# code. -// Naming convention used in AddInternalCall: -// "WraithEngine.+s_,WraithEngine.Managed" -// --------------------------------------------------------------------------- - -#if AXIOM_SCRIPTING_ENABLED - void GameObject_GetName(Coral::String ObjectId, Coral::String *OutName) { if (!s_Session || !OutName) { - if (OutName) + if (OutName) { *OutName = Coral::String{}; + } return; } - std::string id = ObjectId; - const auto *Details = s_Session->FindObjectDetails(id); + std::string Id = ObjectId; + const auto *Details = s_Session->FindObjectDetails(Id); *OutName = Details ? Coral::String::New(Details->DisplayName) - : Coral::String::New(id); + : Coral::String::New(Id); } void GameObject_GetTransform(Coral::String ObjectId, - EditorTransformDetails *OutTransform) { - if (!s_Session || !OutTransform) + EditorTransformDetails *OutTransform) { + if (!s_Session || !OutTransform) { return; - std::string id = ObjectId; - const auto *Details = s_Session->FindObjectDetails(id); + } + std::string Id = ObjectId; + const auto *Details = s_Session->FindObjectDetails(Id); if (Details && Details->Transform.has_value()) { *OutTransform = *Details->Transform; } else { @@ -60,12 +48,13 @@ void GameObject_GetTransform(Coral::String ObjectId, } void GameObject_SetTransform(Coral::String ObjectId, - const EditorTransformDetails *InTransform) { - if (!s_Session || !InTransform) + const EditorTransformDetails *InTransform) { + if (!s_Session || !InTransform) { return; - std::string id = ObjectId; + } + std::string Id = ObjectId; CommandContext Ctx{s_SessionId, s_UserId, 0, 0.0f, true}; - SetTransformCommand Cmd{.ObjectId = std::move(id), + SetTransformCommand Cmd{.ObjectId = std::move(Id), .Location = InTransform->Location, .RotationDegrees = InTransform->RotationDegrees, .Scale = InTransform->Scale}; @@ -73,10 +62,11 @@ void GameObject_SetTransform(Coral::String ObjectId, } Coral::Bool32 GameObject_GetVisible(Coral::String ObjectId) { - if (!s_Session) + if (!s_Session) { return 1u; - std::string id = ObjectId; - const auto *Details = s_Session->FindObjectDetails(id); + } + std::string Id = ObjectId; + const auto *Details = s_Session->FindObjectDetails(Id); return Details ? static_cast(Details->Visible ? 1u : 0u) : 1u; } @@ -84,6 +74,4 @@ Coral::Bool32 ScriptSecurity_IsRestricted() { return static_cast(s_IsRestricted ? 1u : 0u); } -#endif // AXIOM_SCRIPTING_ENABLED - } // namespace Axiom::InternalCalls diff --git a/Axiom/Scripting/ScriptHost.h b/Axiom/Scripting/ScriptHost.h index feb527ce..a9bccd5a 100644 --- a/Axiom/Scripting/ScriptHost.h +++ b/Axiom/Scripting/ScriptHost.h @@ -1,19 +1,12 @@ #pragma once +#include #include #include -#include #include +#include #include -#include -#include - -#if AXIOM_SCRIPTING_ENABLED -#include -#include -#include -#endif namespace Axiom { class EditorSession; @@ -32,7 +25,7 @@ enum class ScriptTrustProfile { Restricted, Trusted }; class ScriptHost final : public IEditorEventSubscriber { public: - ScriptHost() = default; + ScriptHost(); ~ScriptHost(); ScriptHost(const ScriptHost &) = delete; @@ -57,9 +50,8 @@ class ScriptHost final : public IEditorEventSubscriber { // before the reload. No-op if no user assembly has been loaded yet. void ReloadUserAssembly(); - // Start/stop the kqueue-based file watcher that auto-reloads when the - // assembly on disk changes. Only available when AXIOM_SCRIPTING_WATCH=1 - // (macOS only). No-op on other platforms / when the flag is off. + // Start/stop the HAL-managed file watcher that auto-reloads when the + // assembly on disk changes. Only available when AXIOM_SCRIPTING_WATCH=1. void StartFileWatcher(); void StopFileWatcher(); @@ -92,12 +84,6 @@ class ScriptHost final : public IEditorEventSubscriber { return m_UserAssemblyPath; } -#if AXIOM_SCRIPTING_ENABLED - Coral::HostInstance &GetHost() { return m_Host; } - Coral::AssemblyLoadContext &GetEngineALC() { return m_EngineALC; } - Coral::ManagedAssembly *GetEngineAssembly() { return m_EngineAssembly; } -#endif - private: bool IsSimulationRunning() const; void InstantiateAllEligibleScripts(); @@ -114,14 +100,8 @@ class ScriptHost final : public IEditorEventSubscriber { void DestroyAllScripts(); private: -#if AXIOM_SCRIPTING_ENABLED - Coral::HostInstance m_Host; - Coral::AssemblyLoadContext m_EngineALC; - Coral::ManagedAssembly *m_EngineAssembly{nullptr}; - Coral::AssemblyLoadContext m_UserALC; - Coral::ManagedAssembly *m_UserAssembly{nullptr}; - std::unordered_map m_ScriptInstances; -#endif + struct Impl; + std::unique_ptr m_Impl; EditorSession *m_Session{nullptr}; std::filesystem::path m_ManagedDir; // directory of WraithEngine.Managed.dll std::filesystem::path m_UserAssemblyPath; @@ -129,9 +109,7 @@ class ScriptHost final : public IEditorEventSubscriber { bool m_Initialized{false}; bool m_EngineAssemblyLoaded{false}; bool m_UserAssemblyLoaded{false}; - // File watcher (kqueue, macOS, AXIOM_SCRIPTING_WATCH=1 only) - std::thread m_WatcherThread; - std::atomic m_WatcherRunning{false}; + std::unique_ptr m_FileWatcher; }; } // namespace Axiom diff --git a/Axiom/Scripting/ScriptHost.cpp b/Axiom/Scripting/ScriptHostEnabled.cpp similarity index 59% rename from Axiom/Scripting/ScriptHost.cpp rename to Axiom/Scripting/ScriptHostEnabled.cpp index 78df2082..831a4c88 100644 --- a/Axiom/Scripting/ScriptHost.cpp +++ b/Axiom/Scripting/ScriptHostEnabled.cpp @@ -1,25 +1,37 @@ #include "ScriptHost.h" + #include "InternalCalls.h" +#include "HAL/FileWatcher.h" + #include #include -#if AXIOM_SCRIPTING_WATCH -#include -#include -#include -#include -#endif +#include +#include +#include + +#include namespace Axiom { +struct ScriptHost::Impl { + Coral::HostInstance Host; + Coral::AssemblyLoadContext EngineALC; + Coral::ManagedAssembly *EngineAssembly{nullptr}; + Coral::AssemblyLoadContext UserALC; + Coral::ManagedAssembly *UserAssembly{nullptr}; + std::unordered_map ScriptInstances; +}; + +ScriptHost::ScriptHost() = default; + bool ScriptHost::IsSimulationRunning() const { return m_Session != nullptr && m_Session->GetRuntimeState() == EditorRuntimeState::Playing; } void ScriptHost::InstantiateAllEligibleScripts() { -#if AXIOM_SCRIPTING_ENABLED if (!m_UserAssemblyLoaded || m_Session == nullptr || !IsSimulationRunning()) { return; } @@ -30,7 +42,6 @@ void ScriptHost::InstantiateAllEligibleScripts() { InstantiateScript(Id, *Details.ScriptClass); } } -#endif } ScriptHost::~ScriptHost() { @@ -42,7 +53,7 @@ ScriptHost::~ScriptHost() { void ScriptHost::Initialize(const std::filesystem::path &CoralManagedDir, ScriptTrustProfile TrustProfile) { m_TrustProfile = TrustProfile; -#if AXIOM_SCRIPTING_ENABLED + Coral::HostSettings Settings{ .CoralDirectory = CoralManagedDir.string(), .MessageCallback = @@ -65,7 +76,8 @@ void ScriptHost::Initialize(const std::filesystem::path &CoralManagedDir, }, }; - auto Status = m_Host.Initialize(std::move(Settings)); + m_Impl = std::make_unique(); + auto Status = m_Impl->Host.Initialize(std::move(Settings)); switch (Status) { case Coral::CoralInitStatus::Success: @@ -75,27 +87,24 @@ void ScriptHost::Initialize(const std::filesystem::path &CoralManagedDir, break; case Coral::CoralInitStatus::DotNetNotFound: A_CORE_ERROR("ScriptHost: .NET runtime not found — scripting unavailable"); + m_Impl.reset(); break; case Coral::CoralInitStatus::CoralManagedNotFound: A_CORE_WARN("ScriptHost: Coral.Managed.dll not found at '{}' — scripting " "unavailable", CoralManagedDir.string()); + m_Impl.reset(); break; case Coral::CoralInitStatus::CoralManagedInitError: A_CORE_ERROR("ScriptHost: Coral.Managed failed to initialize — scripting " "unavailable"); + m_Impl.reset(); break; } -#else - (void)CoralManagedDir; - A_CORE_WARN("ScriptHost: scripting support was not compiled in " - "(AXIOM_SCRIPTING_ENABLED=0)"); -#endif } void ScriptHost::LoadEngineAssembly(const std::filesystem::path &ManagedDir) { -#if AXIOM_SCRIPTING_ENABLED - if (!m_Initialized) { + if (!m_Initialized || m_Impl == nullptr) { A_CORE_WARN("ScriptHost: cannot load engine assembly — host not initialized"); return; } @@ -108,8 +117,8 @@ void ScriptHost::LoadEngineAssembly(const std::filesystem::path &ManagedDir) { } m_ManagedDir = ManagedDir; - m_EngineALC = m_Host.CreateAssemblyLoadContext("WraithEngine"); - auto &Assembly = m_EngineALC.LoadAssembly(DllPath.string()); + m_Impl->EngineALC = m_Impl->Host.CreateAssemblyLoadContext("WraithEngine"); + auto &Assembly = m_Impl->EngineALC.LoadAssembly(DllPath.string()); if (Assembly.GetLoadStatus() != Coral::AssemblyLoadStatus::Success) { A_CORE_ERROR("ScriptHost: failed to load WraithEngine.Managed.dll (status {})", @@ -117,58 +126,49 @@ void ScriptHost::LoadEngineAssembly(const std::filesystem::path &ManagedDir) { return; } - m_EngineAssembly = &Assembly; + m_Impl->EngineAssembly = &Assembly; m_EngineAssemblyLoaded = true; A_CORE_INFO("ScriptHost: engine assembly loaded ({})", Assembly.GetName()); -#else - (void)ManagedDir; -#endif } void ScriptHost::RegisterInternalCalls(EditorSession &Session, SessionId Id, SessionUserId UserId) { m_Session = &Session; -#if AXIOM_SCRIPTING_ENABLED - if (!m_EngineAssemblyLoaded) { + if (!m_EngineAssemblyLoaded || m_Impl == nullptr || + m_Impl->EngineAssembly == nullptr) { A_CORE_WARN("ScriptHost: cannot register internal calls — engine assembly " "not loaded"); return; } - // Bind the session pointer, credentials, and trust profile InternalCalls::Bind(Session, Id, UserId, m_TrustProfile == ScriptTrustProfile::Restricted); - m_EngineAssembly->AddInternalCall( + m_Impl->EngineAssembly->AddInternalCall( "WraithEngine.GameObject", "s_GetName", reinterpret_cast(&InternalCalls::GameObject_GetName)); - m_EngineAssembly->AddInternalCall( + m_Impl->EngineAssembly->AddInternalCall( "WraithEngine.GameObject", "s_GetTransform", reinterpret_cast(&InternalCalls::GameObject_GetTransform)); - m_EngineAssembly->AddInternalCall( + m_Impl->EngineAssembly->AddInternalCall( "WraithEngine.GameObject", "s_SetTransform", reinterpret_cast(&InternalCalls::GameObject_SetTransform)); - m_EngineAssembly->AddInternalCall( + m_Impl->EngineAssembly->AddInternalCall( "WraithEngine.GameObject", "s_GetVisible", reinterpret_cast(&InternalCalls::GameObject_GetVisible)); - m_EngineAssembly->AddInternalCall( + m_Impl->EngineAssembly->AddInternalCall( "WraithEngine.Internal.ScriptSecurity", "s_IsRestricted", reinterpret_cast(&InternalCalls::ScriptSecurity_IsRestricted)); - m_EngineAssembly->UploadInternalCalls(); + m_Impl->EngineAssembly->UploadInternalCalls(); A_CORE_INFO("ScriptHost: internal calls registered (trust={})", m_TrustProfile == ScriptTrustProfile::Restricted ? "Restricted" : "Trusted"); -#else - (void)Id; - (void)UserId; -#endif } void ScriptHost::LoadUserAssembly(const std::filesystem::path &AssemblyPath) { -#if AXIOM_SCRIPTING_ENABLED - if (!m_Initialized) { + if (!m_Initialized || m_Impl == nullptr) { A_CORE_WARN("ScriptHost: cannot load user assembly — host not initialized"); return; } @@ -180,19 +180,11 @@ void ScriptHost::LoadUserAssembly(const std::filesystem::path &AssemblyPath) { return; } - // In Restricted mode, validate the assembly FILE before making any changes to - // the ALC state. ValidateUserAssemblyResult uses PEReader to inspect the - // manifest references directly from disk — no loading required. Validating - // first keeps s_CachedTypes intact (UnloadAssemblyLoadContext clears it - // globally) so the InvokeStaticMethod round-trip works without a - // RefreshTypeCache call at this point. We use the return-value form because - // Coral routes managed exceptions through ExceptionCallback rather than - // rethrowing them as C++ exceptions, making try/catch unreliable here. if (m_TrustProfile == ScriptTrustProfile::Restricted && - m_EngineAssembly != nullptr) { + m_Impl->EngineAssembly != nullptr) { auto PathStr = Coral::String::New(AssemblyPath.string()); auto ErrorStr = - m_EngineAssembly + m_Impl->EngineAssembly ->GetLocalType("WraithEngine.Internal.ScriptSecurity") .InvokeStaticMethod("ValidateUserAssemblyResult", PathStr); @@ -204,12 +196,11 @@ void ScriptHost::LoadUserAssembly(const std::filesystem::path &AssemblyPath) { if (!ErrorMsg.empty()) { A_CORE_ERROR("ScriptHost: user assembly REJECTED by security policy — {}", ErrorMsg); - // Unload the existing assembly — a failed replace leaves nothing live. DestroyAllScripts(); if (m_UserAssemblyLoaded) { - m_Host.UnloadAssemblyLoadContext(m_UserALC); - m_EngineAssembly->RefreshTypeCache(); - m_UserAssembly = nullptr; + m_Impl->Host.UnloadAssemblyLoadContext(m_Impl->UserALC); + m_Impl->EngineAssembly->RefreshTypeCache(); + m_Impl->UserAssembly = nullptr; m_UserAssemblyLoaded = false; } return; @@ -218,27 +209,17 @@ void ScriptHost::LoadUserAssembly(const std::filesystem::path &AssemblyPath) { "ScriptHost: user assembly passed Restricted-mode security validation"); } - // Tear down existing instances and unload the previous ALC (if any) before - // creating a fresh one. Necessary when LoadUserAssembly is called more than once. DestroyAllScripts(); if (m_UserAssemblyLoaded) { - m_Host.UnloadAssemblyLoadContext(m_UserALC); - // UnloadAssemblyLoadContext clears the managed s_CachedTypes globally. - // Repopulate the engine assembly's types so that subsequent CreateObject / - // InvokeMethod calls (e.g. during InstantiateScript below) work correctly. - m_EngineAssembly->RefreshTypeCache(); - m_UserAssembly = nullptr; + m_Impl->Host.UnloadAssemblyLoadContext(m_Impl->UserALC); + m_Impl->EngineAssembly->RefreshTypeCache(); + m_Impl->UserAssembly = nullptr; m_UserAssemblyLoaded = false; } - // Pass the engine managed dir so Coral's ResolveAssembly can find - // WraithEngine.Managed.dll. The cross-ALC sharing patch in AssemblyLoader.cs - // ensures the UserScripts ALC reuses the engine ALC's already-loaded copy - // (with populated internal-call function pointers) rather than loading a - // fresh duplicate. - m_UserALC = m_Host.CreateAssemblyLoadContext("UserScripts", - m_ManagedDir.string()); - auto &Assembly = m_UserALC.LoadAssembly(AssemblyPath.string()); + m_Impl->UserALC = m_Impl->Host.CreateAssemblyLoadContext( + "UserScripts", m_ManagedDir.string()); + auto &Assembly = m_Impl->UserALC.LoadAssembly(AssemblyPath.string()); if (Assembly.GetLoadStatus() != Coral::AssemblyLoadStatus::Success) { A_CORE_ERROR("ScriptHost: failed to load user assembly '{}' (status {})", @@ -247,20 +228,16 @@ void ScriptHost::LoadUserAssembly(const std::filesystem::path &AssemblyPath) { return; } - m_UserAssembly = &Assembly; + m_Impl->UserAssembly = &Assembly; m_UserAssemblyPath = AssemblyPath; m_UserAssemblyLoaded = true; A_CORE_INFO("ScriptHost: user assembly loaded ({})", Assembly.GetName()); InstantiateAllEligibleScripts(); -#else - (void)AssemblyPath; -#endif } void ScriptHost::ReloadUserAssembly() { -#if AXIOM_SCRIPTING_ENABLED - if (!m_UserAssemblyLoaded) { + if (!m_UserAssemblyLoaded || m_Impl == nullptr) { A_CORE_WARN("ScriptHost: reload requested but no user assembly is loaded"); return; } @@ -268,8 +245,6 @@ void ScriptHost::ReloadUserAssembly() { A_CORE_INFO("ScriptHost: reloading user assembly '{}'", m_UserAssemblyPath.string()); - // 1. Snapshot which objects had scripts — we need to re-instantiate them - // after the ALC is gone (m_ScriptInstances will be cleared). std::vector> ToReinstate; if (m_Session != nullptr) { for (const auto &[Id, Details] : @@ -281,15 +256,11 @@ void ScriptHost::ReloadUserAssembly() { } } - // 2. Validate BEFORE unloading — s_CachedTypes is still intact at this point. - // PEReader inspects the file directly, so the DLL doesn't need to be loaded - // into any ALC. We validate here (not after reload) to avoid a second - // TypeCache desync on the failure path. if (m_TrustProfile == ScriptTrustProfile::Restricted && - m_EngineAssembly != nullptr) { + m_Impl->EngineAssembly != nullptr) { auto PathStr = Coral::String::New(m_UserAssemblyPath.string()); auto ErrorStr = - m_EngineAssembly + m_Impl->EngineAssembly ->GetLocalType("WraithEngine.Internal.ScriptSecurity") .InvokeStaticMethod("ValidateUserAssemblyResult", PathStr); @@ -301,24 +272,20 @@ void ScriptHost::ReloadUserAssembly() { if (!ErrorMsg.empty()) { A_CORE_ERROR( "ScriptHost: reload REJECTED by security policy — {}", ErrorMsg); - return; // leave existing assembly in place; nothing changed yet + return; } } - // 3. Call OnDestroy on all live instances and clear them DestroyAllScripts(); - // 4. Unload the old ALC. This clears the managed s_CachedTypes globally; - // repopulate engine assembly types immediately so InstantiateScript works. - m_Host.UnloadAssemblyLoadContext(m_UserALC); - m_EngineAssembly->RefreshTypeCache(); - m_UserAssembly = nullptr; + m_Impl->Host.UnloadAssemblyLoadContext(m_Impl->UserALC); + m_Impl->EngineAssembly->RefreshTypeCache(); + m_Impl->UserAssembly = nullptr; m_UserAssemblyLoaded = false; - // 5. Create a fresh ALC and reload the assembly from the cached path. - m_UserALC = m_Host.CreateAssemblyLoadContext("UserScripts", - m_ManagedDir.string()); - auto &Assembly = m_UserALC.LoadAssembly(m_UserAssemblyPath.string()); + m_Impl->UserALC = m_Impl->Host.CreateAssemblyLoadContext( + "UserScripts", m_ManagedDir.string()); + auto &Assembly = m_Impl->UserALC.LoadAssembly(m_UserAssemblyPath.string()); if (Assembly.GetLoadStatus() != Coral::AssemblyLoadStatus::Success) { A_CORE_ERROR("ScriptHost: reload failed — could not load '{}' (status {})", @@ -327,48 +294,37 @@ void ScriptHost::ReloadUserAssembly() { return; } - m_UserAssembly = &Assembly; + m_Impl->UserAssembly = &Assembly; m_UserAssemblyLoaded = true; A_CORE_INFO("ScriptHost: user assembly reloaded ({})", Assembly.GetName()); - // 6. Re-instantiate every script that existed before the reload, but only - // while the session is actively simulating. if (IsSimulationRunning()) { for (const auto &[ObjectId, ClassName] : ToReinstate) { InstantiateScript(ObjectId, ClassName); } } -#endif } void ScriptHost::Tick(float DeltaTimeSeconds) { -#if AXIOM_SCRIPTING_ENABLED - if (!IsSimulationRunning()) { + if (!IsSimulationRunning() || m_Impl == nullptr) { return; } - for (auto &[ObjectId, Instance] : m_ScriptInstances) { + for (auto &[ObjectId, Instance] : m_Impl->ScriptInstances) { try { Instance.InvokeMethod("OnTick", DeltaTimeSeconds); } catch (const std::exception &Ex) { A_CORE_ERROR("ScriptHost: OnTick threw for '{}': {}", ObjectId, Ex.what()); } } -#else - (void)DeltaTimeSeconds; -#endif } void ScriptHost::OnEditorEvent(const PublishedEditorEvent &Event) { -#if AXIOM_SCRIPTING_ENABLED std::visit( [&](const auto &Payload) { using T = std::decay_t; if constexpr (std::is_same_v) { - // A new object just appeared in the scene — if it's an Actor with a - // ScriptClass already set (e.g. loaded from scene.json and immediately - // re-created), instantiate the script. if (m_UserAssemblyLoaded && m_Session != nullptr && IsSimulationRunning()) { const auto *Details = @@ -382,7 +338,6 @@ void ScriptHost::OnEditorEvent(const PublishedEditorEvent &Event) { } else if constexpr (std::is_same_v) { DestroyScript(Payload.ObjectId); } else if constexpr (std::is_same_v) { - // AttachScript / DetachScript acknowledged if (m_UserAssemblyLoaded && IsSimulationRunning()) { if (Payload.ScriptClass.has_value()) { InstantiateScript(Payload.ObjectId, *Payload.ScriptClass); @@ -399,134 +354,79 @@ void ScriptHost::OnEditorEvent(const PublishedEditorEvent &Event) { } }, Event.Event.Payload); -#else - (void)Event; -#endif } void ScriptHost::Shutdown() { StopFileWatcher(); -#if AXIOM_SCRIPTING_ENABLED - if (m_Initialized) { + if (m_Initialized && m_Impl != nullptr) { DestroyAllScripts(); - m_Host.Shutdown(); + m_Impl->Host.Shutdown(); m_Initialized = false; m_EngineAssemblyLoaded = false; m_UserAssemblyLoaded = false; - m_UserAssembly = nullptr; - m_EngineAssembly = nullptr; m_Session = nullptr; + m_Impl.reset(); A_CORE_INFO("ScriptHost shutdown"); } -#endif } -// --------------------------------------------------------------------------- -// File watcher -// --------------------------------------------------------------------------- - void ScriptHost::StartFileWatcher() { -#if AXIOM_SCRIPTING_WATCH - if (m_WatcherRunning.load()) { - return; // already running - } +#if !AXIOM_SCRIPTING_WATCH + A_CORE_WARN("ScriptHost: file watcher not available " + "(rebuild with -DAXIOM_SCRIPTING_WATCH=ON)"); + return; +#endif + if (m_UserAssemblyPath.empty()) { A_CORE_WARN("ScriptHost: StartFileWatcher called before LoadUserAssembly"); return; } - m_WatcherRunning.store(true); - m_WatcherThread = std::thread([this] { - const std::filesystem::path WatchPath = m_UserAssemblyPath; - - // Watch the parent directory — the DLL itself may be replaced atomically - // (deleted + rewritten) which would invalidate a vnode watch on the file. - const std::filesystem::path WatchDir = WatchPath.parent_path(); - - const int KQ = kqueue(); - if (KQ < 0) { - A_CORE_ERROR("ScriptHost watcher: kqueue() failed"); - m_WatcherRunning.store(false); - return; - } - - const int DirFd = open(WatchDir.c_str(), O_RDONLY | O_EVTONLY); - if (DirFd < 0) { - close(KQ); - A_CORE_ERROR("ScriptHost watcher: could not open '{}' for watching", - WatchDir.string()); - m_WatcherRunning.store(false); - return; - } - - struct kevent Change; - EV_SET(&Change, DirFd, EVFILT_VNODE, - EV_ADD | EV_CLEAR, - NOTE_WRITE | NOTE_EXTEND | NOTE_RENAME, - 0, nullptr); - kevent(KQ, &Change, 1, nullptr, 0, nullptr); - - std::filesystem::file_time_type LastMtime{}; - if (std::filesystem::exists(WatchPath)) { - LastMtime = std::filesystem::last_write_time(WatchPath); - } - - A_CORE_INFO("ScriptHost watcher: watching '{}' for changes", - WatchPath.string()); + if (m_FileWatcher != nullptr && m_FileWatcher->IsWatching()) { + return; + } - while (m_WatcherRunning.load()) { - struct kevent Event; - struct timespec Timeout{1, 0}; // 1-second timeout so we can check the stop flag - const int N = kevent(KQ, nullptr, 0, &Event, 1, &Timeout); - if (N <= 0) { - continue; // timeout or error — loop back and check stop flag - } + if (m_FileWatcher == nullptr) { + m_FileWatcher = HAL::CreateFileWatcher(); + } - // Directory was modified — check if our DLL's mtime changed - if (!std::filesystem::exists(WatchPath)) { - continue; - } - const auto NewMtime = std::filesystem::last_write_time(WatchPath); - if (NewMtime != LastMtime) { - LastMtime = NewMtime; - A_CORE_INFO("ScriptHost watcher: assembly change detected, reloading"); - ReloadUserAssembly(); - } - } + std::string Error; + if (!m_FileWatcher->StartWatching( + m_UserAssemblyPath, + [this]() { + A_CORE_INFO("ScriptHost watcher: assembly change detected, reloading"); + ReloadUserAssembly(); + }, + Error)) { + A_CORE_WARN("ScriptHost: file watcher unavailable: {}", Error); + m_FileWatcher.reset(); + return; + } - close(DirFd); - close(KQ); - A_CORE_INFO("ScriptHost watcher: stopped"); - }); -#else - A_CORE_WARN("ScriptHost: file watcher not available " - "(rebuild with -DAXIOM_SCRIPTING_WATCH=ON)"); -#endif + A_CORE_INFO("ScriptHost watcher: watching '{}'", + m_UserAssemblyPath.string()); } void ScriptHost::StopFileWatcher() { - if (m_WatcherRunning.exchange(false) && m_WatcherThread.joinable()) { - m_WatcherThread.join(); + if (m_FileWatcher != nullptr) { + m_FileWatcher->StopWatching(); + m_FileWatcher.reset(); + A_CORE_INFO("ScriptHost watcher: stopped"); } } -// --------------------------------------------------------------------------- -// Private helpers -// --------------------------------------------------------------------------- - void ScriptHost::InstantiateScript(const std::string &ObjectId, const std::string &ScriptClassName) { -#if AXIOM_SCRIPTING_ENABLED - if (!m_UserAssemblyLoaded || m_UserAssembly == nullptr) { + if (!m_UserAssemblyLoaded || m_Impl == nullptr || + m_Impl->UserAssembly == nullptr) { A_CORE_WARN("ScriptHost: cannot instantiate '{}' — no user assembly loaded", ScriptClassName); return; } - // Destroy any stale instance first DestroyScript(ObjectId); - auto &Type = m_UserAssembly->GetType(ScriptClassName); + auto &Type = m_Impl->UserAssembly->GetType(ScriptClassName); if (!Type) { A_CORE_ERROR("ScriptHost: type '{}' not found in user assembly", ScriptClassName); @@ -534,8 +434,6 @@ void ScriptHost::InstantiateScript(const std::string &ObjectId, } Coral::ManagedObject Instance = Type.CreateInstance(); - - // Hand the objectId down so Script.GameObject can build a GameObject handle Instance.SetFieldValue("_ObjectId", ObjectId); try { @@ -548,19 +446,19 @@ void ScriptHost::InstantiateScript(const std::string &ObjectId, } } - m_ScriptInstances.emplace(ObjectId, std::move(Instance)); + m_Impl->ScriptInstances.emplace(ObjectId, std::move(Instance)); A_CORE_INFO("ScriptHost: instantiated '{}' on '{}'", ScriptClassName, ObjectId); -#else - (void)ObjectId; - (void)ScriptClassName; -#endif } void ScriptHost::DestroyScript(const std::string &ObjectId) { -#if AXIOM_SCRIPTING_ENABLED - auto It = m_ScriptInstances.find(ObjectId); - if (It == m_ScriptInstances.end()) + if (m_Impl == nullptr) { return; + } + + auto It = m_Impl->ScriptInstances.find(ObjectId); + if (It == m_Impl->ScriptInstances.end()) { + return; + } try { It->second.InvokeMethod("OnDestroy"); @@ -571,16 +469,16 @@ void ScriptHost::DestroyScript(const std::string &ObjectId) { } } - m_ScriptInstances.erase(It); + m_Impl->ScriptInstances.erase(It); A_CORE_INFO("ScriptHost: destroyed script on '{}'", ObjectId); -#else - (void)ObjectId; -#endif } void ScriptHost::DestroyAllScripts() { -#if AXIOM_SCRIPTING_ENABLED - for (auto &[ObjectId, Instance] : m_ScriptInstances) { + if (m_Impl == nullptr) { + return; + } + + for (auto &[ObjectId, Instance] : m_Impl->ScriptInstances) { try { Instance.InvokeMethod("OnDestroy"); } catch (const std::exception &Ex) { @@ -588,8 +486,7 @@ void ScriptHost::DestroyAllScripts() { ObjectId, Ex.what()); } } - m_ScriptInstances.clear(); -#endif + m_Impl->ScriptInstances.clear(); } } // namespace Axiom diff --git a/Axiom/Session/EditorSceneRendererAdapter.cpp b/Axiom/Session/EditorSceneRendererAdapter.cpp deleted file mode 100644 index 2f78c354..00000000 --- a/Axiom/Session/EditorSceneRendererAdapter.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "Session/EditorSceneRendererAdapter.h" - -#include "Renderer/Renderer.h" - -#include - -namespace Axiom { -std::vector -EditorSceneRendererAdapter::BuildRenderSubmissions(const EditorSession &Session) { - const EditorSessionState &State = Session.GetState(); - std::unordered_set LiveObjectIds; - LiveObjectIds.reserve(State.Scene.MeshInstances.size()); - - std::vector Submissions; - Submissions.reserve(State.Scene.MeshInstances.size()); - for (const EditorSceneMeshInstance &Instance : State.Scene.MeshInstances) { - LiveObjectIds.insert(Instance.ObjectId); - - const auto DetailsIt = State.Scene.ObjectDetailsById.find(Instance.ObjectId); - if (DetailsIt != State.Scene.ObjectDetailsById.end() && - !DetailsIt->second.Visible) { - continue; - } - - auto &Cached = m_MeshesByObjectId[Instance.ObjectId]; - if (Cached.Mesh == nullptr || Cached.AssetRelativePath != Instance.AssetRelativePath) { - Cached.Mesh = Renderer::Get().CreateMesh(Instance.Mesh); - Cached.RenderPath = Instance.RenderPath; - Cached.AssetRelativePath = Instance.AssetRelativePath; - } - - if (Cached.Mesh == nullptr) { - continue; - } - - Submissions.push_back({ - .Mesh = Cached.Mesh, - .Material = Instance.Material, // always live — picks up material edits - .Name = Instance.ObjectId, - .RenderPath = Cached.RenderPath, - .Transform = Instance.Transform, - }); - } - - for (auto It = m_MeshesByObjectId.begin(); It != m_MeshesByObjectId.end();) { - if (!LiveObjectIds.contains(It->first)) { - It = m_MeshesByObjectId.erase(It); - } else { - ++It; - } - } - - return Submissions; -} -} // namespace Axiom diff --git a/Axiom/Session/EditorSceneRendererAdapter.h b/Axiom/Session/EditorSceneRendererAdapter.h deleted file mode 100644 index 1035c600..00000000 --- a/Axiom/Session/EditorSceneRendererAdapter.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "Renderer/Mesh.h" -#include "Session/EditorSession.h" - -#include - -namespace Axiom { -class EditorSceneRendererAdapter { -public: - std::vector - BuildRenderSubmissions(const EditorSession &Session); - -private: - struct CachedMeshInstance { - MeshRef Mesh; - MeshRenderPath RenderPath{MeshRenderPath::Graphics}; - std::string AssetRelativePath; - }; - - std::unordered_map m_MeshesByObjectId; -}; -} // namespace Axiom diff --git a/Axiom/Session/EditorSession.cpp b/Axiom/Session/EditorSession.cpp deleted file mode 100644 index d3a139fc..00000000 --- a/Axiom/Session/EditorSession.cpp +++ /dev/null @@ -1,2492 +0,0 @@ -#include "Session/EditorSession.h" - -#include "Assets/AssetCooker.h" -#include "Assets/CookedTextureAsset.h" -#include "Assets/IAssetSource.h" -#include "Assets/MeshAsset.h" -#include "Physics/PhysicsWorld.h" - -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace Axiom { -namespace { -void CookMeshAssetBestEffort(const std::filesystem::path &ContentDir, - std::string_view RelativeAssetPath) { - if (ContentDir.empty() || RelativeAssetPath.empty()) { - return; - } - - const auto Cooked = Assets::CookMeshAsset(ContentDir, RelativeAssetPath); - if (!Cooked.has_value()) { - A_CORE_WARN("EditorSession: failed to cook mesh asset '{}'", - std::string(RelativeAssetPath)); - } -} - -void CookTextureAssetBestEffort(const std::filesystem::path &ContentDir, - std::string_view RelativeAssetPath) { - if (ContentDir.empty() || RelativeAssetPath.empty()) { - return; - } - - const auto Cooked = Assets::CookTextureAsset(ContentDir, RelativeAssetPath); - if (!Cooked.has_value()) { - A_CORE_WARN("EditorSession: failed to cook texture asset '{}'", - std::string(RelativeAssetPath)); - } -} - -void CookHDRTextureAssetBestEffort(const std::filesystem::path &ContentDir, - std::string_view RelativeAssetPath) { - if (ContentDir.empty() || RelativeAssetPath.empty()) { - return; - } - - const auto Cooked = Assets::CookHDRTextureAsset(ContentDir, RelativeAssetPath); - if (!Cooked.has_value()) { - A_CORE_WARN("EditorSession: failed to cook HDR texture asset '{}'", - std::string(RelativeAssetPath)); - } -} - -void HydrateWorldSettingsHDRData(EditorWorldSettings &Settings, - const std::filesystem::path &ContentDir, - const std::filesystem::path &EngineContentDir, - std::string_view LogContext) { - if (Settings.SkyboxHDRPath.empty()) { - Settings.SkyboxHDRData = nullptr; - return; - } - if (Settings.SkyboxHDRData) { - return; - } - if (ContentDir.empty()) { - A_CORE_WARN("{}: content directory not configured; cannot load HDR '{}'", - LogContext, Settings.SkyboxHDRPath); - return; - } - - const std::filesystem::path HDRRelativePath(Settings.SkyboxHDRPath); - const bool IsEngineAsset = - !HDRRelativePath.empty() && *HDRRelativePath.begin() == "Engine"; - std::filesystem::path EffectiveContentDir = ContentDir; - std::filesystem::path EffectiveRelativePath = HDRRelativePath; - if (IsEngineAsset && !EngineContentDir.empty()) { - EffectiveContentDir = EngineContentDir; - auto It = HDRRelativePath.begin(); - ++It; // skip "Engine" - EffectiveRelativePath.clear(); - for (; It != HDRRelativePath.end(); ++It) { - EffectiveRelativePath /= *It; - } - } - - const auto FullPath = EffectiveContentDir / EffectiveRelativePath; - if (std::filesystem::exists(FullPath)) { - CookHDRTextureAssetBestEffort(EffectiveContentDir, - EffectiveRelativePath.generic_string()); - } - auto Loaded = Assets::LoadHDRTextureFromFile(FullPath); - if (!Loaded) { - const Assets::CookedAssetSource CookedSource(EffectiveContentDir); - if (CookedSource.HasManifest()) { - const auto CookedPath = CookedSource.Resolve( - Assets::AssetIdFromRelativePath(EffectiveRelativePath)); - if (CookedPath.has_value()) { - const auto CookedHDR = Assets::LoadCookedHDRTextureAsset(*CookedPath); - if (CookedHDR.has_value()) { - Loaded = std::make_shared(*CookedHDR); - } - } - } - } - if (!Loaded) { - A_CORE_WARN("{}: failed to load HDR '{}'", LogContext, - Settings.SkyboxHDRPath); - } - Settings.SkyboxHDRData = std::move(Loaded); -} - -std::string DefaultUserDisplayName(SessionUserId User) { - if (User.Value == 1) { - return "Host"; - } - return "User " + std::to_string(User.Value - 1); -} - -std::string PresenceStateName(EditorUserPresenceState State) { - switch (State) { - case EditorUserPresenceState::Connected: - return "connected"; - case EditorUserPresenceState::Away: - return "away"; - case EditorUserPresenceState::Disconnected: - return "disconnected"; - } - - return "connected"; -} - -bool IsHostUser(SessionUserId User) { return User.Value == 1; } - -glm::vec3 AbsVec3(const glm::vec3 &Value) { - return glm::vec3(std::abs(Value.x), std::abs(Value.y), std::abs(Value.z)); -} - -void ExpandBounds(const glm::vec3 &BoundsMin, const glm::vec3 &BoundsMax, - const glm::mat4 &Transform, glm::vec3 &OutMin, - glm::vec3 &OutMax, bool &HasBounds) { - const std::array Corners = { - glm::vec3(BoundsMin.x, BoundsMin.y, BoundsMin.z), - glm::vec3(BoundsMax.x, BoundsMin.y, BoundsMin.z), - glm::vec3(BoundsMin.x, BoundsMax.y, BoundsMin.z), - glm::vec3(BoundsMax.x, BoundsMax.y, BoundsMin.z), - glm::vec3(BoundsMin.x, BoundsMin.y, BoundsMax.z), - glm::vec3(BoundsMax.x, BoundsMin.y, BoundsMax.z), - glm::vec3(BoundsMin.x, BoundsMax.y, BoundsMax.z), - glm::vec3(BoundsMax.x, BoundsMax.y, BoundsMax.z), - }; - - for (const glm::vec3 &Corner : Corners) { - const glm::vec3 WorldCorner = glm::vec3(Transform * glm::vec4(Corner, 1.0f)); - if (!HasBounds) { - OutMin = WorldCorner; - OutMax = WorldCorner; - HasBounds = true; - continue; - } - OutMin = glm::min(OutMin, WorldCorner); - OutMax = glm::max(OutMax, WorldCorner); - } -} - -std::optional -BuildDefaultStaticMeshPhysics(const MeshSceneData &SceneData, - const EditorTransformDetails &RootTransform) { - glm::vec3 CombinedMin(0.0f); - glm::vec3 CombinedMax(0.0f); - bool HasBounds = false; - - for (const auto &Instance : SceneData.Instances) { - ExpandBounds(Instance.Mesh.BoundsMin, Instance.Mesh.BoundsMax, - Instance.Transform, CombinedMin, CombinedMax, HasBounds); - } - - if (!HasBounds) { - return std::nullopt; - } - - glm::vec3 HalfExtents = glm::max(glm::abs(CombinedMin), glm::abs(CombinedMax)); - HalfExtents *= AbsVec3(RootTransform.Scale); - HalfExtents = glm::max(HalfExtents, glm::vec3(0.01f)); - - return EditorPhysicsProperties{ - .BodyType = EditorPhysicsBodyType::Static, - .ColliderType = EditorPhysicsColliderType::Box, - .BoxHalfExtents = HalfExtents, - }; -} - -std::string DefaultPresentationColor(SessionUserId User) { - static constexpr const char *Palette[] = { - "#F97316", "#22C55E", "#0EA5E9", "#F59E0B", - "#EF4444", "#14B8A6", "#8B5CF6", "#84CC16", - }; - return Palette[User.Value % (sizeof(Palette) / sizeof(Palette[0]))]; -} - -std::string CommandTypeName(const EditorCommandPayload &Payload) { - if (std::holds_alternative(Payload)) { - return "update_viewport_camera"; - } - if (std::holds_alternative(Payload)) { - return "set_viewport_camera_pose"; - } - if (std::holds_alternative(Payload)) { - return "set_camera_projection"; - } - if (std::holds_alternative(Payload)) { - return "set_look_active"; - } - if (std::holds_alternative(Payload)) { - return "select_object"; - } - if (std::holds_alternative(Payload)) { - return "rename_object"; - } - if (std::holds_alternative(Payload)) { - return "set_object_visibility"; - } - if (std::holds_alternative(Payload)) { - return "create_object"; - } - if (std::holds_alternative(Payload)) { - return "create_mesh_object"; - } - if (std::holds_alternative(Payload)) { - return "duplicate_object"; - } - if (std::holds_alternative(Payload)) { - return "delete_object"; - } - if (std::holds_alternative(Payload)) { - return "reparent_object"; - } - if (std::holds_alternative(Payload)) { - return "attach_script"; - } - if (std::holds_alternative(Payload)) { - return "detach_script"; - } - if (std::holds_alternative(Payload)) { - return "set_mesh_asset"; - } - if (std::holds_alternative(Payload)) { - return "set_light_properties"; - } - if (std::holds_alternative(Payload)) { - return "set_material_properties"; - } - if (std::holds_alternative(Payload)) { - return "set_material_texture"; - } - if (std::holds_alternative(Payload)) { - return "set_physics_properties"; - } - if (std::holds_alternative(Payload)) { - return "play_session"; - } - if (std::holds_alternative(Payload)) { - return "pause_session"; - } - if (std::holds_alternative(Payload)) { - return "resume_session"; - } - if (std::holds_alternative(Payload)) { - return "stop_session"; - } - if (std::holds_alternative(Payload)) { - return "set_world_settings"; - } - if (std::holds_alternative(Payload)) { - return "place_actor"; - } - return "set_transform"; -} - -bool IsAuthoringMutationCommand(const EditorCommandPayload &Payload) { - return std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload) || - std::holds_alternative(Payload); -} - -EditorSceneItemKind KindForClassName(std::string_view ClassName) { - if (ClassName == "SceneMeshObject") return EditorSceneItemKind::Mesh; - if (ClassName == "SceneLight") return EditorSceneItemKind::Light; - if (ClassName == "SceneCamera") return EditorSceneItemKind::Camera; - if (ClassName == "SceneActor") return EditorSceneItemKind::Actor; - return EditorSceneItemKind::Folder; -} - -EditorSceneItemKind KindForTemplateId(std::string_view TemplateId) { - if (TemplateId == "Mesh") return EditorSceneItemKind::Mesh; - if (TemplateId == "Light") return EditorSceneItemKind::Light; - if (TemplateId == "Camera") return EditorSceneItemKind::Camera; - if (TemplateId == "Actor") return EditorSceneItemKind::Actor; - return EditorSceneItemKind::Folder; -} - -std::string_view TemplateIdForKind(EditorSceneItemKind Kind) { - switch (Kind) { - case EditorSceneItemKind::Mesh: return "Mesh"; - case EditorSceneItemKind::Light: return "Light"; - case EditorSceneItemKind::Camera: return "Camera"; - case EditorSceneItemKind::Actor: return "Actor"; - default: return "Folder"; - } -} - -bool SupportsTransformForKind(EditorSceneItemKind Kind) { - return Kind != EditorSceneItemKind::Folder; -} - -Instance *FindInstanceById(Instance *Root, std::string_view Id) { - if (!Root) return nullptr; - if (Root->GetName() == Id) return Root; - for (Instance *Child : Root->GetChildren()) { - if (Instance *Found = FindInstanceById(Child, Id)) - return Found; - } - return nullptr; -} - -bool ShouldPublishCommandAcknowledgedEvent(const EditorCommandPayload &Payload) { - return !std::holds_alternative(Payload); -} - -bool IsNearlyZero(const glm::vec3 &Value) { - return glm::dot(Value, Value) <= 0.0f; -} - -bool IsPositive(const glm::vec3 &Value) { - return Value.x > 0.0f && Value.y > 0.0f && Value.z > 0.0f; -} - -bool IsWhitespace(char Value) { - return Value == ' ' || Value == '\t' || Value == '\n' || Value == '\r' || - Value == '\f' || Value == '\v'; -} - -std::string SanitizeGeneratedAssetToken(std::string_view Value) { - std::string Out; - Out.reserve(Value.size()); - for (const char Character : Value) { - if ((Character >= 'a' && Character <= 'z') || - (Character >= 'A' && Character <= 'Z') || - (Character >= '0' && Character <= '9')) { - Out.push_back(Character); - } else { - Out.push_back('_'); - } - } - - while (!Out.empty() && Out.back() == '_') { - Out.pop_back(); - } - - if (Out.empty()) { - return "mesh"; - } - return Out; -} - -std::string BuildGeneratedAssetChildId(std::string_view RootObjectId, - std::string_view InstanceName, - size_t InstanceIndex) { - return std::string(RootObjectId) + "__asset_" + std::to_string(InstanceIndex) + - "_" + SanitizeGeneratedAssetToken(InstanceName); -} - -std::string ResolveGeneratedAssetChildDisplayName(std::string_view InstanceName, - size_t InstanceIndex) { - if (!InstanceName.empty()) { - return std::string(InstanceName); - } - return "Mesh " + std::to_string(InstanceIndex + 1); -} - -glm::mat4 BuildTransformMatrix(const EditorTransformDetails &Transform) { - glm::mat4 Matrix(1.0f); - Matrix = glm::translate(Matrix, Transform.Location); - Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.y), - glm::vec3(0.0f, 1.0f, 0.0f)); - Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.x), - glm::vec3(1.0f, 0.0f, 0.0f)); - Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.z), - glm::vec3(0.0f, 0.0f, 1.0f)); - Matrix = glm::scale(Matrix, Transform.Scale); - return Matrix; -} - -TextureSourceDataRef CloneTextureSourceData( - const TextureSourceDataRef &Texture) { - if (!Texture) { - return nullptr; - } - - auto Copy = std::make_shared(); - Copy->Width = Texture->Width; - Copy->Height = Texture->Height; - Copy->Pixels = Texture->Pixels; - return Copy; -} - -MaterialInstanceRef CloneMaterialInstance(const MaterialInstanceRef &Material) { - if (!Material) { - return nullptr; - } - - auto Copy = std::make_shared(); - Copy->BaseColorTexture = CloneTextureSourceData(Material->BaseColorTexture); - Copy->BaseColorFactor = Material->BaseColorFactor; - Copy->Metallic = Material->Metallic; - Copy->Roughness = Material->Roughness; - Copy->TextureAssetPath = Material->TextureAssetPath; - return Copy; -} - -EditorSceneState CloneEditorSceneState(const EditorSceneState &Scene) { - EditorSceneState Copy = Scene; - for (auto &MeshInstance : Copy.MeshInstances) { - MeshInstance.Material = CloneMaterialInstance(MeshInstance.Material); - } - return Copy; -} -} // namespace - -EditorSession::EditorSession(SessionId Session, EditorSessionConfig Config) - : m_Config(Config), m_State({.Session = Session}) { - InitSceneRoot(); -} - -EditorSession::~EditorSession() = default; -EditorSession::EditorSession(EditorSession &&) noexcept = default; -EditorSession &EditorSession::operator=(EditorSession &&) noexcept = default; - -void EditorSession::Submit(const CommandContext &Context, - const EditorCommand &Command) { - m_MessageBus.EnqueueCommand(Context, Command); -} - -void EditorSession::Tick(float DeltaTimeSeconds) { - m_MessageBus.DispatchQueuedCommands( - [this](const QueuedEditorCommand &QueuedCommand) { - ProcessCommand(QueuedCommand); - }); - StepRuntimePhysics(DeltaTimeSeconds); -} - -void EditorSession::Subscribe(IEditorEventSubscriber *Subscriber) { - m_MessageBus.Subscribe(Subscriber); -} - -void EditorSession::Unsubscribe(IEditorEventSubscriber *Subscriber) { - m_MessageBus.Unsubscribe(Subscriber); -} - -void EditorSession::EnsureViewportState(SessionUserId User) { - EnsureViewport(User); -} - -void EditorSession::SetPresenceState(SessionUserId User, - EditorUserPresenceState State) { - const auto [It, Inserted] = m_State.PresenceByUser.try_emplace(User); - EditorUserPresence &Presence = It->second; - if (Inserted) { - Presence.User = User; - Presence.DisplayName = DefaultUserDisplayName(User); - Presence.IsLocal = User.Value == 1; - } - if (!Inserted && Presence.State == State) { - return; - } - - Presence.State = State; - PublishPresenceChangedEvent(User); -} - -void EditorSession::SetSceneState(EditorSceneState SceneState) { - m_State.Scene = std::move(SceneState); - HydrateWorldSettingsHDRData(m_State.Scene.WorldSettings, m_ContentDir, - m_EngineContentDir, - "SetSceneState"); - // Populate Material on object details from mesh instances so the inspector - // can display and edit material properties for mesh objects. - for (const auto &MeshInst : m_State.Scene.MeshInstances) { - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(MeshInst.ObjectId); - if (DetailsIt != m_State.Scene.ObjectDetailsById.end() && - MeshInst.Material && !DetailsIt->second.Material.has_value()) { - DetailsIt->second.Material = EditorMaterialProperties{ - .BaseColorFactor = MeshInst.Material->BaseColorFactor, - .Metallic = MeshInst.Material->Metallic, - .Roughness = MeshInst.Material->Roughness, - }; - } - } - RebuildInstanceTree(m_State.Scene.Items, m_SceneRoot.get()); - PruneInvalidSelections(); - RecomputeAllWorldTransforms(); -} - -void EditorSession::SetSceneMeshInstances( - std::vector SceneMeshInstances) { - m_State.Scene.MeshInstances = std::move(SceneMeshInstances); -} - -void EditorSession::SetSceneItems(std::vector SceneItems) { - m_State.Scene.Items = std::move(SceneItems); - RebuildInstanceTree(m_State.Scene.Items, m_SceneRoot.get()); - PruneInvalidSelections(); - RecomputeAllWorldTransforms(); -} - -void EditorSession::SetObjectDetails( - std::vector ObjectDetails) { - m_State.Scene.ObjectDetailsById = BuildObjectDetailsMap(std::move(ObjectDetails)); - RecomputeAllWorldTransforms(); -} - -void EditorSession::SetPresence(std::vector Presence) { - m_State.PresenceByUser.clear(); - for (EditorUserPresence &Entry : Presence) { - m_State.PresenceByUser.emplace(Entry.User, std::move(Entry)); - } -} - -void EditorSession::SetObjectCollaborationStates( - std::vector CollaborationStates) { - m_State.Scene.CollaborationByObjectId.clear(); - for (EditorObjectCollaborationState &Entry : CollaborationStates) { - m_State.Scene.CollaborationByObjectId.emplace(Entry.ObjectId, - std::move(Entry)); - } -} - -const EditorViewportState *EditorSession::FindViewport(SessionUserId User) const { - const auto It = m_State.Viewports.find(User); - return It != m_State.Viewports.end() ? &It->second : nullptr; -} - -const EditorSceneItem *EditorSession::FindSceneItem(std::string_view ObjectId) const { - return FindSceneItemRecursive(m_State.Scene.Items, ObjectId); -} - -const std::string *EditorSession::FindSelectedObjectId(SessionUserId User) const { - const auto It = m_State.SelectedObjectIds.find(User); - return It != m_State.SelectedObjectIds.end() ? &It->second : nullptr; -} - -const EditorObjectDetails *EditorSession::FindObjectDetails( - std::string_view ObjectId) const { - const auto It = m_State.Scene.ObjectDetailsById.find(std::string(ObjectId)); - return It != m_State.Scene.ObjectDetailsById.end() ? &It->second : nullptr; -} - -const EditorObjectDetails *EditorSession::FindSelectedObjectDetails( - SessionUserId User) const { - const std::string *SelectedObjectId = FindSelectedObjectId(User); - return SelectedObjectId != nullptr ? FindObjectDetails(*SelectedObjectId) : nullptr; -} - -const EditorUserPresence *EditorSession::FindPresence(SessionUserId User) const { - const auto It = m_State.PresenceByUser.find(User); - return It != m_State.PresenceByUser.end() ? &It->second : nullptr; -} - -EditorParticipant EditorSession::BuildParticipant(SessionUserId User) const { - EditorParticipant Participant{}; - Participant.User = User; - Participant.DisplayName = DefaultUserDisplayName(User); - Participant.PresentationColor = DefaultPresentationColor(User); - - if (const EditorUserPresence *Presence = FindPresence(User); Presence != nullptr) { - Participant.DisplayName = Presence->DisplayName; - Participant.State = Presence->State; - Participant.IsLocal = Presence->IsLocal; - } - - if (const std::string *SelectedObjectId = FindSelectedObjectId(User); - SelectedObjectId != nullptr) { - Participant.SelectedObjectId = *SelectedObjectId; - } - - if (const EditorViewportState *Viewport = FindViewport(User); - Viewport != nullptr) { - Participant.Camera = EditorParticipant::CameraState{ - .Position = Viewport->Camera.GetPosition(), - .YawDegrees = Viewport->Camera.GetYawDegrees(), - .PitchDegrees = Viewport->Camera.GetPitchDegrees(), - }; - } - - return Participant; -} - -std::vector EditorSession::BuildParticipants( - SessionUserId CurrentUser) const { - std::vector Participants; - Participants.reserve(m_State.PresenceByUser.size()); - - for (const auto &[User, Presence] : m_State.PresenceByUser) { - (void)Presence; - EditorParticipant Participant = BuildParticipant(User); - Participant.IsLocal = User.Value == CurrentUser.Value; - Participants.push_back(std::move(Participant)); - } - - return Participants; -} - -SessionUserId EditorSession::ResolveRuntimeControllerUser() const { - if (m_State.RuntimeControllerUser.has_value()) { - const SessionUserId User = *m_State.RuntimeControllerUser; - if (const EditorUserPresence *Presence = FindPresence(User); - Presence != nullptr && - Presence->State != EditorUserPresenceState::Disconnected) { - return User; - } - } - - std::optional Candidate; - for (const auto &[User, Presence] : m_State.PresenceByUser) { - if (Presence.State == EditorUserPresenceState::Disconnected || - IsHostUser(User)) { - continue; - } - if (!Candidate.has_value() || User.Value < Candidate->Value) { - Candidate = User; - } - } - - return Candidate.value_or(SessionUserId{1}); -} - -const EditorObjectCollaborationState *EditorSession::FindCollaborationState( - std::string_view ObjectId) const { - const auto It = - m_State.Scene.CollaborationByObjectId.find(std::string(ObjectId)); - return It != m_State.Scene.CollaborationByObjectId.end() ? &It->second - : nullptr; -} - -std::unordered_map -EditorSession::BuildObjectDetailsMap( - std::vector ObjectDetails) { - std::unordered_map DetailsByObjectId; - DetailsByObjectId.reserve(ObjectDetails.size()); - for (EditorObjectDetails &Details : ObjectDetails) { - DetailsByObjectId.emplace(Details.ObjectId, std::move(Details)); - } - return DetailsByObjectId; -} - -void EditorSession::InitSceneRoot() { - m_SceneRoot = std::make_unique(); - Instance::Create("world")->SetParent(m_SceneRoot.get()); -} - -Instance *EditorSession::FindWorldFolder() const { - if (!m_SceneRoot) return nullptr; - for (Instance *Child : m_SceneRoot->GetChildren()) { - if (Child->IsA() && Child->GetName() == "world") - return Child; - } - return nullptr; -} - -Instance *EditorSession::EnsureWorldFolder() { - if (!m_SceneRoot) { - InitSceneRoot(); - } - - auto EnsureWorldDetails = [this]() { - if (m_State.Scene.ObjectDetailsById.find("world") != - m_State.Scene.ObjectDetailsById.end()) { - return; - } - m_State.Scene.ObjectDetailsById.emplace( - "world", EditorObjectDetails{ - .ObjectId = "world", - .DisplayName = "World", - .Kind = EditorSceneItemKind::Folder, - .Visible = true, - .SupportsTransform = false, - .TransformReadOnly = true, - }); - }; - - if (Instance *World = FindWorldFolder(); World != nullptr) { - EnsureWorldDetails(); - return World; - } - - EnsureWorldDetails(); - - Instance *World = Instance::Create("world"); - World->SetParent(m_SceneRoot.get()); - SyncItemsFromTree(); - return World; -} - -void EditorSession::RebuildInstanceTree(const std::vector &Items, - Instance *Parent) { - if (!Parent) return; - std::vector OldChildren = Parent->GetChildren(); - for (Instance *Child : OldChildren) - Child->Destroy(); - for (const EditorSceneItem &Item : Items) { - Instance *Node = CreateInstanceForTemplate( - std::string(TemplateIdForKind(Item.Kind)), Item.Id); - if (!Node) continue; - Node->SetParent(Parent); - if (!Item.Children.empty()) - RebuildInstanceTree(Item.Children, Node); - } -} - -void EditorSession::SyncItemsFromTree() { - m_State.Scene.Items.clear(); - if (!m_SceneRoot) return; - for (const Instance *Child : m_SceneRoot->GetChildren()) - m_State.Scene.Items.push_back(BuildItemFromInstance(Child)); -} - -EditorSceneItem EditorSession::BuildItemFromInstance(const Instance *Node) const { - EditorSceneItem Item; - Item.Id = Node->GetName(); - Item.Kind = KindForInstance(Node); - Item.Visible = true; - Item.DisplayName = Node->GetName(); - const auto It = m_State.Scene.ObjectDetailsById.find(Node->GetName()); - if (It != m_State.Scene.ObjectDetailsById.end()) { - Item.DisplayName = It->second.DisplayName; - Item.Visible = It->second.Visible; - Item.Kind = It->second.Kind; - } - for (const Instance *Child : Node->GetChildren()) - Item.Children.push_back(BuildItemFromInstance(Child)); - return Item; -} - -Instance *EditorSession::CreateInstanceForTemplate(const std::string &TemplateId, - const std::string &ObjectId) { - if (TemplateId == "Folder") return Instance::Create(ObjectId); - if (TemplateId == "Mesh") return Instance::Create(ObjectId); - if (TemplateId == "Light") return Instance::Create(ObjectId); - if (TemplateId == "Camera") return Instance::Create(ObjectId); - if (TemplateId == "Actor") return Instance::Create(ObjectId); - return nullptr; -} - -EditorSceneItemKind EditorSession::KindForInstance(const Instance *Node) const { - return KindForClassName(Node->GetClassName()); -} - -bool EditorSession::IsValidTemplateId(const std::string &TemplateId) const { - return TemplateId == "Folder" || TemplateId == "Mesh" || - TemplateId == "Light" || TemplateId == "Camera" || - TemplateId == "Actor"; -} - -std::vector EditorSession::CollectDescendantIds( - const Instance *Root) const { - std::vector Ids; - std::vector Stack{Root}; - while (!Stack.empty()) { - const Instance *Cur = Stack.back(); - Stack.pop_back(); - Ids.push_back(Cur->GetName()); - for (const Instance *Child : Cur->GetChildren()) - Stack.push_back(Child); - } - return Ids; -} - -void EditorSession::DeepCloneSubtree(const Instance *Source, Instance *DestParent, - std::vector &OutNewDetails) { - const std::string NewId = BuildUniqueObjectId(Source->GetName()); - const EditorSceneItemKind Kind = KindForInstance(Source); - std::string BaseDisplayName = Source->GetName(); - EditorObjectDetails NewDetails; - NewDetails.Kind = Kind; - NewDetails.Visible = true; - NewDetails.SupportsTransform = SupportsTransformForKind(Kind); - NewDetails.TransformReadOnly = false; - - const auto ExistIt = m_State.Scene.ObjectDetailsById.find(Source->GetName()); - if (ExistIt != m_State.Scene.ObjectDetailsById.end()) { - BaseDisplayName = ExistIt->second.DisplayName; - NewDetails.Visible = ExistIt->second.Visible; - NewDetails.Transform = ExistIt->second.Transform; - NewDetails.WorldTransform = ExistIt->second.WorldTransform; - } else if (NewDetails.SupportsTransform) { - NewDetails.Transform = EditorTransformDetails{}; - NewDetails.WorldTransform = EditorTransformDetails{}; - } - - NewDetails.ObjectId = NewId; - NewDetails.DisplayName = BuildUniqueDisplayName(BaseDisplayName); - - m_State.Scene.ObjectDetailsById.emplace(NewId, NewDetails); - OutNewDetails.push_back(NewDetails); - - Instance *Clone = CreateInstanceForTemplate( - std::string(TemplateIdForKind(Kind)), NewId); - if (Clone) { - Clone->SetParent(DestParent); - for (const Instance *Child : Source->GetChildren()) - DeepCloneSubtree(Child, Clone, OutNewDetails); - } -} - -bool EditorSession::IsBlankString(std::string_view Value) { - for (const char Character : Value) { - if (!IsWhitespace(Character)) { - return false; - } - } - return true; -} - -std::string EditorSession::BuildUniqueObjectId(std::string_view Base) const { - if (!IsSceneObjectIdInUse(Base)) return std::string(Base); - for (int N = 2; ; ++N) { - std::string C = std::string(Base) + "_" + std::to_string(N); - if (!IsSceneObjectIdInUse(C)) return C; - } -} - -std::string EditorSession::BuildUniqueDisplayName(std::string_view Base) const { - if (!IsSceneDisplayNameInUse(Base)) return std::string(Base); - for (int N = 2; ; ++N) { - std::string C = std::string(Base) + " " + std::to_string(N); - if (!IsSceneDisplayNameInUse(C)) return C; - } -} - -bool EditorSession::IsSceneObjectIdInUse(std::string_view ObjectId) const { - return m_State.Scene.ObjectDetailsById.count(std::string(ObjectId)) > 0; -} - -bool EditorSession::IsSceneDisplayNameInUse(std::string_view DisplayName) const { - for (const auto &[Id, D] : m_State.Scene.ObjectDetailsById) { - if (D.DisplayName == DisplayName) return true; - } - return false; -} - -bool EditorSession::UpdateSceneItemDisplayName(std::vector &Items, - std::string_view ObjectId, - std::string_view DisplayName) { - for (EditorSceneItem &Item : Items) { - if (Item.Id == ObjectId) { - Item.DisplayName = std::string(DisplayName); - return true; - } - - if (UpdateSceneItemDisplayName(Item.Children, ObjectId, DisplayName)) { - return true; - } - } - - return false; -} - -bool EditorSession::UpdateSceneItemVisibility(std::vector &Items, - std::string_view ObjectId, - bool Visible) { - for (EditorSceneItem &Item : Items) { - if (Item.Id == ObjectId) { - Item.Visible = Visible; - return true; - } - - if (UpdateSceneItemVisibility(Item.Children, ObjectId, Visible)) { - return true; - } - } - - return false; -} - -bool EditorSession::RemoveSceneItem(std::vector &Items, - std::string_view ObjectId) { - for (auto It = Items.begin(); It != Items.end(); ++It) { - if (It->Id == ObjectId) { - Items.erase(It); - return true; - } - if (RemoveSceneItem(It->Children, ObjectId)) return true; - } - return false; -} - -EditorSceneItem *EditorSession::FindSceneItemMutable( - std::vector &Items, std::string_view ObjectId) { - for (EditorSceneItem &Item : Items) { - if (Item.Id == ObjectId) return &Item; - if (EditorSceneItem *Found = FindSceneItemMutable(Item.Children, ObjectId)) - return Found; - } - return nullptr; -} - -void EditorSession::RemoveSceneObject(std::string_view ObjectId) { - const std::string Id(ObjectId); - m_State.Scene.ObjectDetailsById.erase(Id); - m_State.Scene.CollaborationByObjectId.erase(Id); - m_State.Scene.MeshInstances.erase( - std::remove_if(m_State.Scene.MeshInstances.begin(), - m_State.Scene.MeshInstances.end(), - [&Id](const EditorSceneMeshInstance &M) { - return M.ObjectId == Id; - }), - m_State.Scene.MeshInstances.end()); -} - -void EditorSession::RemoveGeneratedAssetChildren(std::string_view RootObjectId) { - Instance *Root = FindInstanceById(m_SceneRoot.get(), RootObjectId); - if (Root == nullptr) { - return; - } - - std::vector GeneratedChildIds; - for (Instance *Child : Root->GetChildren()) { - const auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Child->GetName()); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - continue; - } - if (!DetailsIt->second.IsGeneratedAssetChild || - !DetailsIt->second.GeneratedFromAssetRootId.has_value() || - *DetailsIt->second.GeneratedFromAssetRootId != RootObjectId) { - continue; - } - GeneratedChildIds.push_back(Child->GetName()); - } - - for (const std::string &ChildId : GeneratedChildIds) { - Instance *Child = FindInstanceById(m_SceneRoot.get(), ChildId); - if (Child == nullptr) { - continue; - } - for (const std::string &DescendantId : CollectDescendantIds(Child)) { - RemoveSceneObject(DescendantId); - ClearSelectionsForObject(DescendantId); - } - Child->Destroy(); - } -} - -void EditorSession::ExpandMeshAssetIntoScene(std::string_view RootObjectId, - const MeshSceneData &SceneData, - std::string_view AssetPath) { - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(std::string(RootObjectId)); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - Instance *Root = FindInstanceById(m_SceneRoot.get(), RootObjectId); - if (Root == nullptr) { - return; - } - - RemoveGeneratedAssetChildren(RootObjectId); - - m_State.Scene.MeshInstances.erase( - std::remove_if(m_State.Scene.MeshInstances.begin(), - m_State.Scene.MeshInstances.end(), - [&](const EditorSceneMeshInstance &Instance) { - return Instance.ObjectId == RootObjectId; - }), - m_State.Scene.MeshInstances.end()); - - EditorObjectDetails &RootDetails = DetailsIt->second; - RootDetails.IsGeneratedAssetChild = false; - RootDetails.GeneratedFromAssetRootId = std::nullopt; - RootDetails.AssetRelativePath = std::string(AssetPath); - if (!RootDetails.Physics.has_value()) { - const EditorTransformDetails RootTransform = - RootDetails.Transform.value_or(EditorTransformDetails{}); - RootDetails.Physics = BuildDefaultStaticMeshPhysics(SceneData, RootTransform); - } - - if (SceneData.Instances.size() == 1) { - const auto &First = SceneData.Instances.front(); - m_State.Scene.MeshInstances.push_back(EditorSceneMeshInstance{ - .ObjectId = std::string(RootObjectId), - .Mesh = First.Mesh, - .Material = First.Material, - .RenderPath = MeshRenderPath::Graphics, - .Transform = glm::mat4(1.0f), - .AssetRelativePath = std::string(AssetPath), - }); - - if (First.Material) { - RootDetails.Material = EditorMaterialProperties{ - .BaseColorFactor = First.Material->BaseColorFactor, - .Metallic = First.Material->Metallic, - .Roughness = First.Material->Roughness, - .TextureAssetPath = First.Material->TextureAssetPath.empty() - ? std::nullopt - : std::optional( - First.Material->TextureAssetPath), - }; - } - SyncItemsFromTree(); - return; - } - - RootDetails.Material = std::nullopt; - - for (size_t InstanceIndex = 0; InstanceIndex < SceneData.Instances.size(); - ++InstanceIndex) { - const auto &SourceInstance = SceneData.Instances[InstanceIndex]; - const std::string ChildId = BuildGeneratedAssetChildId( - RootObjectId, SourceInstance.Name, InstanceIndex); - const std::string ChildDisplayName = ResolveGeneratedAssetChildDisplayName( - SourceInstance.Name, InstanceIndex); - const EditorTransformDetails ChildLocalTransform = - DecomposeMatrix(SourceInstance.Transform); - - m_State.Scene.ObjectDetailsById[ChildId] = EditorObjectDetails{ - .ObjectId = ChildId, - .DisplayName = ChildDisplayName, - .Kind = EditorSceneItemKind::Mesh, - .Visible = RootDetails.Visible, - .IsGeneratedAssetChild = true, - .SupportsTransform = true, - .TransformReadOnly = true, - .Transform = ChildLocalTransform, - .Material = SourceInstance.Material - ? std::optional( - EditorMaterialProperties{ - .BaseColorFactor = - SourceInstance.Material->BaseColorFactor, - .Metallic = SourceInstance.Material->Metallic, - .Roughness = - SourceInstance.Material->Roughness, - .TextureAssetPath = - SourceInstance.Material->TextureAssetPath - .empty() - ? std::nullopt - : std::optional( - SourceInstance.Material - ->TextureAssetPath), - }) - : std::nullopt, - .GeneratedFromAssetRootId = std::string(RootObjectId), - }; - - Instance *Child = CreateInstanceForTemplate("Mesh", ChildId); - if (Child != nullptr) { - Child->SetParent(Root); - } - - m_State.Scene.MeshInstances.push_back(EditorSceneMeshInstance{ - .ObjectId = ChildId, - .Mesh = SourceInstance.Mesh, - .Material = SourceInstance.Material, - .RenderPath = MeshRenderPath::Graphics, - .Transform = SourceInstance.Transform, - }); - } - - SyncItemsFromTree(); -} - -void EditorSession::ClearSelectionsForObject(std::string_view ObjectId) { - for (auto It = m_State.SelectedObjectIds.begin(); - It != m_State.SelectedObjectIds.end();) { - It = (It->second == ObjectId) ? m_State.SelectedObjectIds.erase(It) - : std::next(It); - } -} - -void EditorSession::PruneInvalidSelections() { - for (auto It = m_State.SelectedObjectIds.begin(); - It != m_State.SelectedObjectIds.end();) { - if (FindSceneItem(It->second) == nullptr) { - It = m_State.SelectedObjectIds.erase(It); - } else { - ++It; - } - } -} - -glm::mat4 EditorSession::ComputeWorldTransformMatrix(const Instance *Node) const { - if (!Node) return glm::mat4(1.0f); - std::vector Chain; - const Instance *Cur = Node; - while (Cur && Cur != m_SceneRoot.get()) { - Chain.push_back(Cur); - Cur = Cur->GetParent(); - } - glm::mat4 World(1.0f); - for (auto It = Chain.rbegin(); It != Chain.rend(); ++It) { - const auto DetailsIt = m_State.Scene.ObjectDetailsById.find((*It)->GetName()); - if (DetailsIt != m_State.Scene.ObjectDetailsById.end() && - DetailsIt->second.Transform.has_value()) { - World = World * BuildTransformMatrix(*DetailsIt->second.Transform); - } - } - return World; -} - -EditorTransformDetails EditorSession::DecomposeMatrix(const glm::mat4 &Matrix) const { - const glm::vec3 Location = glm::vec3(Matrix[3]); - glm::vec3 Col0 = glm::vec3(Matrix[0]); - glm::vec3 Col1 = glm::vec3(Matrix[1]); - glm::vec3 Col2 = glm::vec3(Matrix[2]); - const float ScaleX = glm::length(Col0); - const float ScaleY = glm::length(Col1); - const float ScaleZ = glm::length(Col2); - if (ScaleX > 0.0f) Col0 /= ScaleX; - if (ScaleY > 0.0f) Col1 /= ScaleY; - if (ScaleZ > 0.0f) Col2 /= ScaleZ; - // YXZ Euler decomposition matching BuildTransformMatrix order (Ry * Rx * Rz) - const float AngleX = glm::degrees(glm::asin(glm::clamp(-Col2.y, -1.0f, 1.0f))); - const float AngleY = glm::degrees(glm::atan(Col2.x, Col2.z)); - const float AngleZ = glm::degrees(glm::atan(Col0.y, Col1.y)); - return EditorTransformDetails{ - .Location = Location, - .RotationDegrees = {AngleX, AngleY, AngleZ}, - .Scale = {ScaleX, ScaleY, ScaleZ}, - }; -} - -void EditorSession::RecomputeSubtreeWorldTransforms(const Instance *Node) { - if (!Node) return; - const std::string &Id = Node->GetName(); - const auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Id); - if (DetailsIt != m_State.Scene.ObjectDetailsById.end() && - DetailsIt->second.Transform.has_value()) { - const glm::mat4 WorldMatrix = ComputeWorldTransformMatrix(Node); - DetailsIt->second.WorldTransform = DecomposeMatrix(WorldMatrix); - for (EditorSceneMeshInstance &Inst : m_State.Scene.MeshInstances) { - if (Inst.ObjectId == Id) { - Inst.Transform = WorldMatrix; - break; - } - } - } - for (const Instance *Child : Node->GetChildren()) - RecomputeSubtreeWorldTransforms(Child); -} - -void EditorSession::RecomputeAllWorldTransforms() { - if (!m_SceneRoot) return; - for (const Instance *Child : m_SceneRoot->GetChildren()) - RecomputeSubtreeWorldTransforms(Child); -} - -void EditorSession::AcquireLock(const std::string &ObjectId, SessionUserId User) { - auto &Collab = m_State.Scene.CollaborationByObjectId[ObjectId]; - if (Collab.LockState == EditorObjectLockState::Locked && Collab.LockOwner != User) { - return; - } - Collab.ObjectId = ObjectId; - Collab.LockState = EditorObjectLockState::Locked; - Collab.LockOwner = User; - PublishEvent({.Payload = ObjectLockChangedEvent{ - .ObjectId = ObjectId, - .LockState = EditorObjectLockState::Locked, - .LockOwner = User, - }}); -} - -void EditorSession::ReleaseLock(const std::string &ObjectId, SessionUserId User) { - const auto It = m_State.Scene.CollaborationByObjectId.find(ObjectId); - if (It == m_State.Scene.CollaborationByObjectId.end()) { - return; - } - if (It->second.LockOwner != User) { - return; - } - It->second.LockState = EditorObjectLockState::Unlocked; - It->second.LockOwner = std::nullopt; - PublishEvent({.Payload = ObjectLockChangedEvent{ - .ObjectId = ObjectId, - .LockState = EditorObjectLockState::Unlocked, - .LockOwner = std::nullopt, - }}); -} - -void EditorSession::ReleaseAllLocksForUser(SessionUserId User) { - for (auto &[ObjectId, Collab] : m_State.Scene.CollaborationByObjectId) { - if (Collab.LockOwner == User && Collab.LockState == EditorObjectLockState::Locked) { - Collab.LockState = EditorObjectLockState::Unlocked; - Collab.LockOwner = std::nullopt; - PublishEvent({.Payload = ObjectLockChangedEvent{ - .ObjectId = ObjectId, - .LockState = EditorObjectLockState::Unlocked, - .LockOwner = std::nullopt, - }}); - } - } -} - -void EditorSession::PublishPresenceChangedEvent(SessionUserId User) { - const EditorParticipant Participant = BuildParticipant(User); - PublishEvent({.Payload = PresenceChangedEvent{ - .User = User, - .DisplayName = Participant.DisplayName, - .IsLocal = Participant.IsLocal, - .PresenceState = PresenceStateName(Participant.State), - .SelectedObjectId = Participant.SelectedObjectId, - }}); -} - -EditorUserPresence &EditorSession::EnsurePresence(SessionUserId User) { - const auto [It, Inserted] = m_State.PresenceByUser.try_emplace(User); - if (Inserted) { - It->second.User = User; - It->second.DisplayName = DefaultUserDisplayName(User); - It->second.State = EditorUserPresenceState::Connected; - It->second.IsLocal = User.Value == 1; - } else { - It->second.State = EditorUserPresenceState::Connected; - } - - return It->second; -} - -EditorViewportState &EditorSession::EnsureViewport(SessionUserId User) { - EnsurePresence(User); - const auto [It, Inserted] = m_State.Viewports.try_emplace(User); - if (Inserted) { - It->second.Camera.LookAt(m_Config.InitialCameraPosition, - m_Config.InitialCameraTarget); - It->second.Camera.SetPerspective( - m_Config.CameraVerticalFovDegrees, m_Config.CameraAspectRatio, - m_Config.CameraNearPlane, m_Config.CameraFarPlane); - } - - return It->second; -} - -const EditorSceneItem *EditorSession::FindSceneItemRecursive( - const std::vector &Items, std::string_view ObjectId) const { - for (const EditorSceneItem &Item : Items) { - if (Item.Id == ObjectId) { - return &Item; - } - - if (const EditorSceneItem *Child = - FindSceneItemRecursive(Item.Children, ObjectId); - Child != nullptr) { - return Child; - } - } - - return nullptr; -} - -void EditorSession::ProcessCommand(const QueuedEditorCommand &QueuedCommand) { - std::string FailureReason; - if (!ValidateCommand(QueuedCommand, FailureReason)) { - PublishEvent({.Payload = CommandRejectedEvent{ - .User = QueuedCommand.Context.User, - .RejectedCommand = QueuedCommand.Id, - .Reason = FailureReason, - }}); - return; - } - - EnsureViewport(QueuedCommand.Context.User); - std::visit( - [this, &QueuedCommand](const auto &Command) { - HandleCommand(QueuedCommand, Command); - }, - QueuedCommand.Command.Payload); - - if (ShouldPublishCommandAcknowledgedEvent(QueuedCommand.Command.Payload)) { - PublishEvent({.Payload = CommandAcknowledgedEvent{ - .User = QueuedCommand.Context.User, - .AcknowledgedCommand = QueuedCommand.Id, - .CommandType = - CommandTypeName(QueuedCommand.Command.Payload), - }}); - } -} - -bool EditorSession::ValidateCommand(const QueuedEditorCommand &QueuedCommand, - std::string &FailureReason) { - if (QueuedCommand.Context.Session != m_State.Session) { - FailureReason = "Command targeted a different session."; - return false; - } - - if ((std::holds_alternative(QueuedCommand.Command.Payload) || - std::holds_alternative(QueuedCommand.Command.Payload) || - std::holds_alternative(QueuedCommand.Command.Payload) || - std::holds_alternative(QueuedCommand.Command.Payload)) && - QueuedCommand.Context.User.Value != ResolveRuntimeControllerUser().Value) { - FailureReason = - "Only the current simulation host can control simulation state."; - return false; - } - - if (!QueuedCommand.Context.IsScriptContext && - m_State.RuntimeState != EditorRuntimeState::Edit && - IsAuthoringMutationCommand(QueuedCommand.Command.Payload)) { - FailureReason = - "Authoring edits are disabled while shared simulation is active."; - return false; - } - - const EditorViewportState &Viewport = - const_cast(this)->EnsureViewport(QueuedCommand.Context.User); - - if (const auto *CameraCommand = - std::get_if( - &QueuedCommand.Command.Payload)) { - if (Viewport.IsLooking && !CameraCommand->CursorPosition.has_value()) { - FailureReason = "Look-enabled camera updates require cursor position."; - return false; - } - } - - if (const auto *SelectionCommand = - std::get_if(&QueuedCommand.Command.Payload)) { - if (SelectionCommand->ObjectId.empty()) { - FailureReason = "Selection commands require a non-empty object id."; - return false; - } - if (FindSceneItem(SelectionCommand->ObjectId) == nullptr) { - FailureReason = "Selection targeted an unknown object."; - return false; - } - } - - // Lock guard: reject mutating commands if another user owns the lock. - { - const std::string *LockedObjectId = nullptr; - std::string SingleId; - if (const auto *C = std::get_if(&QueuedCommand.Command.Payload)) - SingleId = C->ObjectId; - else if (const auto *C = std::get_if(&QueuedCommand.Command.Payload)) - SingleId = C->ObjectId; - else if (const auto *C = std::get_if(&QueuedCommand.Command.Payload)) - SingleId = C->ObjectId; - else if (const auto *C = std::get_if(&QueuedCommand.Command.Payload)) - SingleId = C->ObjectId; - else if (const auto *C = std::get_if(&QueuedCommand.Command.Payload)) - SingleId = C->ObjectId; - else if (const auto *C = std::get_if(&QueuedCommand.Command.Payload)) - SingleId = C->ObjectId; - if (!SingleId.empty()) { - const auto CollabIt = m_State.Scene.CollaborationByObjectId.find(SingleId); - if (CollabIt != m_State.Scene.CollaborationByObjectId.end() && - CollabIt->second.LockState == EditorObjectLockState::Locked && - CollabIt->second.LockOwner.has_value() && - *CollabIt->second.LockOwner != QueuedCommand.Context.User) { - FailureReason = "Object is locked by another user."; - return false; - } - } - } - - if (const auto *TransformCommand = - std::get_if(&QueuedCommand.Command.Payload)) { - if (TransformCommand->ObjectId.empty()) { - FailureReason = "Transform commands require a non-empty object id."; - return false; - } - - const EditorObjectDetails *Details = FindObjectDetails(TransformCommand->ObjectId); - if (Details == nullptr) { - FailureReason = "Transform targeted an unknown object."; - return false; - } - if (!Details->SupportsTransform || !Details->Transform.has_value()) { - FailureReason = "This object does not support transform edits."; - return false; - } - if (Details->TransformReadOnly) { - FailureReason = "This object is read-only."; - return false; - } - if (TransformCommand->Scale.x <= 0.0f || TransformCommand->Scale.y <= 0.0f || - TransformCommand->Scale.z <= 0.0f) { - FailureReason = "Scale values must be greater than zero."; - return false; - } - } - - if (const auto *PhysicsCommand = - std::get_if(&QueuedCommand.Command.Payload)) { - if (PhysicsCommand->ObjectId.empty()) { - FailureReason = "Physics commands require a non-empty object id."; - return false; - } - - const EditorObjectDetails *Details = FindObjectDetails(PhysicsCommand->ObjectId); - if (Details == nullptr) { - FailureReason = "Physics targeted an unknown object."; - return false; - } - if (!Details->SupportsTransform) { - FailureReason = "Physics can only be assigned to transformable objects."; - return false; - } - if (PhysicsCommand->Physics.BodyType == EditorPhysicsBodyType::Dynamic && - PhysicsCommand->Physics.Mass <= 0.0f) { - FailureReason = "Dynamic physics bodies require a positive mass."; - return false; - } - if (PhysicsCommand->Physics.ColliderType == EditorPhysicsColliderType::Box && - !IsPositive(PhysicsCommand->Physics.BoxHalfExtents)) { - FailureReason = "Box colliders require positive half extents."; - return false; - } - if (PhysicsCommand->Physics.ColliderType == EditorPhysicsColliderType::Sphere && - PhysicsCommand->Physics.SphereRadius <= 0.0f) { - FailureReason = "Sphere colliders require a positive radius."; - return false; - } - if (PhysicsCommand->Physics.Friction < 0.0f) { - FailureReason = "Physics friction must be zero or greater."; - return false; - } - if (PhysicsCommand->Physics.Restitution < 0.0f) { - FailureReason = "Physics restitution must be zero or greater."; - return false; - } - } - - if (const auto *RenameCommand = - std::get_if(&QueuedCommand.Command.Payload)) { - if (RenameCommand->ObjectId.empty()) { - FailureReason = "Rename commands require a non-empty object id."; - return false; - } - if (FindSceneItem(RenameCommand->ObjectId) == nullptr) { - FailureReason = "Rename targeted an unknown object."; - return false; - } - if (RenameCommand->DisplayName.empty() || - IsBlankString(RenameCommand->DisplayName)) { - FailureReason = "Rename commands require a non-empty display name."; - return false; - } - } - - if (const auto *VisibilityCommand = - std::get_if( - &QueuedCommand.Command.Payload)) { - if (VisibilityCommand->ObjectId.empty()) { - FailureReason = "Visibility commands require a non-empty object id."; - return false; - } - if (FindSceneItem(VisibilityCommand->ObjectId) == nullptr) { - FailureReason = "Visibility targeted an unknown object."; - return false; - } - } - - if (const auto *CreateCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (CreateCmd->TemplateId.empty()) { - FailureReason = "Create commands require a non-empty TemplateId."; - return false; - } - if (!IsValidTemplateId(CreateCmd->TemplateId)) { - FailureReason = "Unknown TemplateId: " + CreateCmd->TemplateId + "."; - return false; - } - } - - if (const auto *CreateMeshCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (CreateMeshCmd->AssetPath.empty()) { - FailureReason = "CreateMeshObject requires a non-empty asset path."; - return false; - } - if (CreateMeshCmd->Scale.x <= 0.0f || CreateMeshCmd->Scale.y <= 0.0f || - CreateMeshCmd->Scale.z <= 0.0f) { - FailureReason = "Scale values must be greater than zero."; - return false; - } - if (m_ContentDir.empty()) { - FailureReason = "CreateMeshObject requires a configured content directory."; - return false; - } - CookMeshAssetBestEffort(m_ContentDir, CreateMeshCmd->AssetPath); - const std::filesystem::path FullPath = m_ContentDir / CreateMeshCmd->AssetPath; - const auto SceneData = Assets::LoadBasicMeshAsset(FullPath); - if (!SceneData.has_value() || SceneData->Instances.empty()) { - FailureReason = "CreateMeshObject failed to load mesh asset: " + - CreateMeshCmd->AssetPath + "."; - return false; - } - } - - if (const auto *DupCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (DupCmd->ObjectId.empty()) { - FailureReason = "Duplicate commands require a non-empty object id."; - return false; - } - if (FindSceneItem(DupCmd->ObjectId) == nullptr) { - FailureReason = "Duplicate targeted an unknown object."; - return false; - } - } - - if (const auto *DelCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (DelCmd->ObjectId.empty()) { - FailureReason = "Delete commands require a non-empty object id."; - return false; - } - if (FindSceneItem(DelCmd->ObjectId) == nullptr) { - FailureReason = "Delete targeted an unknown object."; - return false; - } - if (DelCmd->ObjectId == "world") { - FailureReason = "The world folder cannot be deleted."; - return false; - } - } - - if (const auto *ReparentCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (ReparentCmd->ObjectId.empty()) { - FailureReason = "Reparent commands require a non-empty object id."; - return false; - } - if (ReparentCmd->NewParentId.empty()) { - FailureReason = "Reparent commands require a non-empty new parent id."; - return false; - } - if (FindSceneItem(ReparentCmd->ObjectId) == nullptr) { - FailureReason = "Reparent targeted an unknown object."; - return false; - } - if (FindSceneItem(ReparentCmd->NewParentId) == nullptr) { - FailureReason = "Reparent new parent is an unknown object."; - return false; - } - if (ReparentCmd->ObjectId == ReparentCmd->NewParentId) { - FailureReason = "Cannot reparent an object onto itself."; - return false; - } - if (ReparentCmd->ObjectId == "world") { - FailureReason = "The world folder cannot be reparented."; - return false; - } - // Reject if new parent is a descendant of the object (would create cycle) - const Instance *Target = - FindInstanceById(m_SceneRoot.get(), ReparentCmd->ObjectId); - if (Target != nullptr) { - for (const std::string &DescId : CollectDescendantIds(Target)) { - if (DescId == ReparentCmd->NewParentId) { - FailureReason = "Cannot reparent an object onto one of its descendants."; - return false; - } - } - } - } - - if (const auto *AttachCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (AttachCmd->ObjectId.empty()) { - FailureReason = "AttachScript requires a non-empty object id."; - return false; - } - if (AttachCmd->ScriptClassName.empty()) { - FailureReason = "AttachScript requires a non-empty script class name."; - return false; - } - const EditorObjectDetails *Details = FindObjectDetails(AttachCmd->ObjectId); - if (Details == nullptr) { - FailureReason = "AttachScript targeted an unknown object."; - return false; - } - if (Details->Kind != EditorSceneItemKind::Actor) { - FailureReason = "Scripts can only be attached to Actor objects."; - return false; - } - } - - if (const auto *DetachCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (DetachCmd->ObjectId.empty()) { - FailureReason = "DetachScript requires a non-empty object id."; - return false; - } - if (FindObjectDetails(DetachCmd->ObjectId) == nullptr) { - FailureReason = "DetachScript targeted an unknown object."; - return false; - } - } - - if (const auto *MeshAssetCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - if (MeshAssetCmd->ObjectId.empty()) { - FailureReason = "SetMeshAsset requires a non-empty object id."; - return false; - } - if (MeshAssetCmd->AssetPath.empty()) { - FailureReason = "SetMeshAsset requires a non-empty asset path."; - return false; - } - const EditorObjectDetails *Details = FindObjectDetails(MeshAssetCmd->ObjectId); - if (Details == nullptr) { - FailureReason = "SetMeshAsset targeted an unknown object."; - return false; - } - if (Details->Kind != EditorSceneItemKind::Mesh && - Details->Kind != EditorSceneItemKind::Actor) { - FailureReason = "SetMeshAsset target must be a Mesh or Actor object."; - return false; - } - } - - if (const auto *LightCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - const EditorObjectDetails *Details = FindObjectDetails(LightCmd->ObjectId); - if (Details == nullptr) { - FailureReason = "SetLightProperties targeted an unknown object."; - return false; - } - if (Details->Kind != EditorSceneItemKind::Light) { - FailureReason = "SetLightProperties target must be a Light object."; - return false; - } - } - - if (const auto *MatCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - const EditorObjectDetails *Details = FindObjectDetails(MatCmd->ObjectId); - if (Details == nullptr) { - FailureReason = "SetMaterialProperties targeted an unknown object."; - return false; - } - if (Details->Kind != EditorSceneItemKind::Mesh) { - FailureReason = "SetMaterialProperties target must be a Mesh object."; - return false; - } - } - - if (const auto *TexCmd = - std::get_if(&QueuedCommand.Command.Payload)) { - const EditorObjectDetails *Details = FindObjectDetails(TexCmd->ObjectId); - if (Details == nullptr) { - FailureReason = "SetMaterialTexture targeted an unknown object."; - return false; - } - if (Details->Kind != EditorSceneItemKind::Mesh) { - FailureReason = "SetMaterialTexture target must be a Mesh object."; - return false; - } - } - - if (std::holds_alternative(QueuedCommand.Command.Payload)) { - if (m_State.RuntimeState != EditorRuntimeState::Edit) { - FailureReason = "PlaySession is only valid while in edit mode."; - return false; - } - } - - if (std::holds_alternative(QueuedCommand.Command.Payload)) { - if (m_State.RuntimeState != EditorRuntimeState::Playing) { - FailureReason = "PauseSession is only valid while playing."; - return false; - } - } - - if (std::holds_alternative(QueuedCommand.Command.Payload)) { - if (m_State.RuntimeState != EditorRuntimeState::Paused) { - FailureReason = "ResumeSession is only valid while paused."; - return false; - } - } - - if (std::holds_alternative(QueuedCommand.Command.Payload)) { - if (m_State.RuntimeState == EditorRuntimeState::Edit) { - FailureReason = "StopSession is only valid while simulation is active."; - return false; - } - } - - // SetWorldSettingsCommand requires no specific validation since colors are just vec3 - - return true; -} - -void EditorSession::HandleCommand( - const QueuedEditorCommand &QueuedCommand, - const UpdateViewportCameraCommand &Command) { - EditorViewportState &Viewport = EnsureViewport(QueuedCommand.Context.User); - - bool CameraChanged = false; - if (!IsNearlyZero(Command.WorldMovement)) { - Viewport.Camera.MoveLocal(Command.WorldMovement); - CameraChanged = true; - } - - if (Viewport.IsLooking && Command.CursorPosition.has_value()) { - if (Viewport.HasLastCursorPosition) { - const glm::dvec2 Delta = *Command.CursorPosition - Viewport.LastCursorPosition; - if (Delta.x != 0.0 || Delta.y != 0.0) { - Viewport.Camera.SetRotation( - Viewport.Camera.GetYawDegrees() + - static_cast(Delta.x) * m_Config.MouseSensitivity, - Viewport.Camera.GetPitchDegrees() - - static_cast(Delta.y) * m_Config.MouseSensitivity); - CameraChanged = true; - } - } - - Viewport.LastCursorPosition = *Command.CursorPosition; - Viewport.HasLastCursorPosition = true; - } - - if (CameraChanged) { - PublishEvent({.Payload = ViewportCameraUpdatedEvent{ - .User = QueuedCommand.Context.User, - .Position = Viewport.Camera.GetPosition(), - .YawDegrees = Viewport.Camera.GetYawDegrees(), - .PitchDegrees = Viewport.Camera.GetPitchDegrees(), - }}); - } -} - -void EditorSession::HandleCommand( - const QueuedEditorCommand &QueuedCommand, - const SetViewportCameraPoseCommand &Command) { - EditorViewportState &Viewport = EnsureViewport(QueuedCommand.Context.User); - Viewport.Camera.SetPosition(Command.Position); - Viewport.Camera.SetRotation(Command.YawDegrees, Command.PitchDegrees); - PublishEvent({.Payload = ViewportCameraUpdatedEvent{ - .User = QueuedCommand.Context.User, - .Position = Viewport.Camera.GetPosition(), - .YawDegrees = Viewport.Camera.GetYawDegrees(), - .PitchDegrees = Viewport.Camera.GetPitchDegrees(), - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetCameraProjectionCommand &Command) { - EditorViewportState &Viewport = EnsureViewport(QueuedCommand.Context.User); - Viewport.ProjectionType = Command.ProjectionType; - if (Command.ProjectionType == CameraProjectionType::Orthographic) { - Viewport.Camera.SetOrthographic(Viewport.OrthoHeight, - m_Config.CameraAspectRatio, - m_Config.CameraNearPlane, - m_Config.CameraFarPlane); - } else { - Viewport.Camera.SetPerspective(m_Config.CameraVerticalFovDegrees, - m_Config.CameraAspectRatio, - m_Config.CameraNearPlane, - m_Config.CameraFarPlane); - } -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetLookActiveCommand &Command) { - EditorViewportState &Viewport = EnsureViewport(QueuedCommand.Context.User); - const bool StateChanged = Viewport.IsLooking != Command.IsLooking; - Viewport.IsLooking = Command.IsLooking; - - if (Command.IsLooking && Command.CursorPosition.has_value()) { - Viewport.LastCursorPosition = *Command.CursorPosition; - Viewport.HasLastCursorPosition = true; - } else if (!Command.IsLooking) { - Viewport.HasLastCursorPosition = false; - } - - if (StateChanged) { - PublishEvent({.Payload = LookStateChangedEvent{ - .User = QueuedCommand.Context.User, - .IsLooking = Viewport.IsLooking, - }}); - } -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SelectObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - const auto Existing = m_State.SelectedObjectIds.find(QueuedCommand.Context.User); - if (Existing != m_State.SelectedObjectIds.end() && - Existing->second == Command.ObjectId) { - return; - } - - m_State.SelectedObjectIds[QueuedCommand.Context.User] = Command.ObjectId; - PublishEvent({.Payload = SelectionChangedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = Command.ObjectId, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const RenameObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - if (DetailsIt->second.DisplayName == Command.DisplayName) { - return; - } - - DetailsIt->second.DisplayName = Command.DisplayName; - UpdateSceneItemDisplayName(m_State.Scene.Items, Command.ObjectId, - Command.DisplayName); - - PublishEvent({.Payload = ObjectRenamedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = Command.ObjectId, - .DisplayName = Command.DisplayName, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetObjectVisibilityCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - if (DetailsIt->second.Visible == Command.Visible) { - return; - } - - DetailsIt->second.Visible = Command.Visible; - UpdateSceneItemVisibility(m_State.Scene.Items, Command.ObjectId, Command.Visible); - - PublishEvent({.Payload = ObjectVisibilityChangedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = Command.ObjectId, - .Visible = Command.Visible, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const CreateObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - Instance *WorldFolder = EnsureWorldFolder(); - if (WorldFolder == nullptr) { - return; - } - const EditorSceneItemKind Kind = KindForTemplateId(Command.TemplateId); - const std::string ObjectId = BuildUniqueObjectId(Command.TemplateId); - const std::string DisplayName = BuildUniqueDisplayName(Command.TemplateId); - - const bool Transformable = SupportsTransformForKind(Kind); - const std::optional InitTransform = - Transformable ? std::optional{EditorTransformDetails{}} : std::nullopt; - m_State.Scene.ObjectDetailsById.emplace( - ObjectId, - EditorObjectDetails{ - .ObjectId = ObjectId, - .DisplayName = DisplayName, - .Kind = Kind, - .Visible = true, - .SupportsTransform = Transformable, - .TransformReadOnly = false, - .Transform = InitTransform, - .WorldTransform = InitTransform, - }); - - if (Instance *Node = CreateInstanceForTemplate(Command.TemplateId, ObjectId)) - Node->SetParent(WorldFolder); - - SyncItemsFromTree(); - PublishEvent({.Payload = ObjectCreatedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = ObjectId, - .DisplayName = DisplayName, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const CreateMeshObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - Instance *WorldFolder = EnsureWorldFolder(); - if (WorldFolder == nullptr) { - return; - } - - const std::string ObjectId = BuildUniqueObjectId("Mesh"); - const std::string DisplayName = BuildUniqueDisplayName("Mesh"); - const EditorTransformDetails Transform{ - .Location = Command.Location, - .RotationDegrees = Command.RotationDegrees, - .Scale = Command.Scale, - }; - - m_State.Scene.ObjectDetailsById.emplace( - ObjectId, - EditorObjectDetails{ - .ObjectId = ObjectId, - .DisplayName = DisplayName, - .Kind = EditorSceneItemKind::Mesh, - .Visible = true, - .SupportsTransform = true, - .TransformReadOnly = false, - .Transform = Transform, - .WorldTransform = Transform, - }); - - if (Instance *Node = CreateInstanceForTemplate("Mesh", ObjectId)) { - Node->SetParent(WorldFolder); - } - - SyncItemsFromTree(); - PublishEvent({.Payload = ObjectCreatedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = ObjectId, - .DisplayName = DisplayName, - }}); - - HandleCommand(QueuedCommand, SetMeshAssetCommand{ - .ObjectId = ObjectId, - .AssetPath = Command.AssetPath, - }); - HandleCommand(QueuedCommand, SetTransformCommand{ - .ObjectId = ObjectId, - .Location = Command.Location, - .RotationDegrees = Command.RotationDegrees, - .Scale = Command.Scale, - }); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const DuplicateObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - Instance *Source = FindInstanceById(m_SceneRoot.get(), Command.ObjectId); - if (!Source) return; - Instance *Parent = Source->GetParent(); - if (!Parent) return; - - std::vector NewDetails; - DeepCloneSubtree(Source, Parent, NewDetails); - SyncItemsFromTree(); - - if (!NewDetails.empty()) { - PublishEvent({.Payload = ObjectCreatedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = NewDetails.front().ObjectId, - .DisplayName = NewDetails.front().DisplayName, - }}); - } -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const DeleteObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - Instance *Target = FindInstanceById(m_SceneRoot.get(), Command.ObjectId); - if (!Target) return; - - for (const std::string &Id : CollectDescendantIds(Target)) { - RemoveSceneObject(Id); - ClearSelectionsForObject(Id); - } - - Target->Destroy(); - SyncItemsFromTree(); - PublishEvent({.Payload = ObjectDeletedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = Command.ObjectId, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const ReparentObjectCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - Instance *Target = FindInstanceById(m_SceneRoot.get(), Command.ObjectId); - Instance *NewParent = FindInstanceById(m_SceneRoot.get(), Command.NewParentId); - if (!Target || !NewParent) return; - if (Target->GetParent() == NewParent) return; - - Target->SetParent(NewParent); - SyncItemsFromTree(); - RecomputeSubtreeWorldTransforms(Target); - PublishEvent({.Payload = ObjectReparentedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = Command.ObjectId, - .NewParentId = Command.NewParentId, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetTransformCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - ApplyWorldTransform( - Command.ObjectId, - EditorTransformDetails{ - .Location = Command.Location, - .RotationDegrees = Command.RotationDegrees, - .Scale = Command.Scale, - }, - QueuedCommand.Context.User, true); -} - -void EditorSession::ApplyWorldTransform(std::string_view ObjectId, - const EditorTransformDetails &WorldTD, - SessionUserId User, - bool ShouldPublish) { - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(std::string(ObjectId)); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - const glm::mat4 WorldMatrix = BuildTransformMatrix(WorldTD); - - EditorTransformDetails LocalTD = WorldTD; - const Instance *Node = FindInstanceById(m_SceneRoot.get(), ObjectId); - if (Node && Node->GetParent() && Node->GetParent() != m_SceneRoot.get()) { - const glm::mat4 ParentWorld = ComputeWorldTransformMatrix(Node->GetParent()); - LocalTD = DecomposeMatrix(glm::inverse(ParentWorld) * WorldMatrix); - } - - DetailsIt->second.Transform = LocalTD; - DetailsIt->second.WorldTransform = WorldTD; - - for (EditorSceneMeshInstance &Inst : m_State.Scene.MeshInstances) { - if (Inst.ObjectId == ObjectId) { - Inst.Transform = WorldMatrix; - break; - } - } - - if (Node) { - for (const Instance *Child : Node->GetChildren()) { - RecomputeSubtreeWorldTransforms(Child); - } - } - - if (ShouldPublish) { - PublishEvent({.Payload = ObjectTransformUpdatedEvent{ - .User = User, - .ObjectId = std::string(ObjectId), - .Location = WorldTD.Location, - .RotationDegrees = WorldTD.RotationDegrees, - .Scale = WorldTD.Scale, - }}); - } -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const AttachScriptCommand &Command) { - auto It = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (It == m_State.Scene.ObjectDetailsById.end()) - return; - It->second.ScriptClass = Command.ScriptClassName; - A_CORE_INFO("EditorSession: attached script '{}' to '{}'", - Command.ScriptClassName, Command.ObjectId); - PublishEvent({ScriptClassChangedEvent{.ObjectId = Command.ObjectId, - .ScriptClass = Command.ScriptClassName}}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const DetachScriptCommand &Command) { - auto It = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (It == m_State.Scene.ObjectDetailsById.end()) - return; - It->second.ScriptClass = std::nullopt; - A_CORE_INFO("EditorSession: detached script from '{}'", Command.ObjectId); - PublishEvent({ScriptClassChangedEvent{.ObjectId = Command.ObjectId, - .ScriptClass = std::nullopt}}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetMeshAssetCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - - if (m_ContentDir.empty()) { - A_CORE_WARN("SetMeshAsset: content directory not configured"); - return; - } - - // Resolve "Engine/" prefix to the engine content directory. - const std::filesystem::path AssetRelative{Command.AssetPath}; - const bool IsEngineAsset = - !AssetRelative.empty() && *AssetRelative.begin() == "Engine"; - std::filesystem::path EffectiveContentDir = m_ContentDir; - std::filesystem::path EffectiveRelative = AssetRelative; - if (IsEngineAsset && !m_EngineContentDir.empty()) { - EffectiveContentDir = m_EngineContentDir; - auto It = AssetRelative.begin(); - ++It; // skip "Engine" - EffectiveRelative.clear(); - for (; It != AssetRelative.end(); ++It) EffectiveRelative /= *It; - } - - CookMeshAssetBestEffort(EffectiveContentDir, EffectiveRelative.string()); - const std::filesystem::path FullPath = EffectiveContentDir / EffectiveRelative; - const auto SceneData = Assets::LoadBasicMeshAsset(FullPath); - if (!SceneData.has_value() || SceneData->Instances.empty()) { - A_CORE_WARN("SetMeshAsset: failed to load '{}' for object '{}'", - Command.AssetPath, Command.ObjectId); - return; - } - - const auto &First = SceneData->Instances[0]; - (void)First; - ExpandMeshAssetIntoScene(Command.ObjectId, *SceneData, Command.AssetPath); - RecomputeSubtreeWorldTransforms( - FindInstanceById(m_SceneRoot.get(), Command.ObjectId)); - - A_CORE_INFO("SetMeshAsset: assigned '{}' to object '{}'", - Command.AssetPath, Command.ObjectId); - PublishEvent({.Payload = MeshAssetChangedEvent{ - .ObjectId = Command.ObjectId, - .AssetPath = Command.AssetPath, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetLightPropertiesCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - if (!DetailsIt->second.Light.has_value()) { - DetailsIt->second.Light = EditorLightProperties{}; - } - DetailsIt->second.Light->Color = Command.Color; - DetailsIt->second.Light->Intensity = Command.Intensity; - - PublishEvent({.Payload = LightPropertiesChangedEvent{ - .ObjectId = Command.ObjectId, - .Color = Command.Color, - .Intensity = Command.Intensity, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetMaterialPropertiesCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - if (!DetailsIt->second.Material.has_value()) { - DetailsIt->second.Material = EditorMaterialProperties{}; - } - DetailsIt->second.Material->BaseColorFactor = Command.BaseColorFactor; - DetailsIt->second.Material->Metallic = Command.Metallic; - DetailsIt->second.Material->Roughness = Command.Roughness; - - auto MeshIt = std::find_if(m_State.Scene.MeshInstances.begin(), - m_State.Scene.MeshInstances.end(), - [&](const EditorSceneMeshInstance &M) { - return M.ObjectId == Command.ObjectId; - }); - if (MeshIt != m_State.Scene.MeshInstances.end() && MeshIt->Material) { - MeshIt->Material->BaseColorFactor = Command.BaseColorFactor; - MeshIt->Material->Metallic = Command.Metallic; - MeshIt->Material->Roughness = Command.Roughness; - } - - PublishEvent({.Payload = MaterialPropertiesChangedEvent{ - .ObjectId = Command.ObjectId, - .BaseColorFactor = Command.BaseColorFactor, - .Metallic = Command.Metallic, - .Roughness = Command.Roughness, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetMaterialTextureCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) - return; - - auto MeshIt = std::find_if(m_State.Scene.MeshInstances.begin(), - m_State.Scene.MeshInstances.end(), - [&](const EditorSceneMeshInstance &M) { - return M.ObjectId == Command.ObjectId; - }); - if (MeshIt == m_State.Scene.MeshInstances.end() || !MeshIt->Material) - return; - - if (Command.TextureAssetPath.empty()) { - // Clear the override — the mesh asset's embedded texture (if any) remains - MeshIt->Material->BaseColorTexture = nullptr; - MeshIt->Material->TextureAssetPath.clear(); - } else { - if (m_ContentDir.empty()) { - A_CORE_WARN("SetMaterialTexture: content directory not configured"); - return; - } - CookTextureAssetBestEffort(m_ContentDir, Command.TextureAssetPath); - const auto FullPath = m_ContentDir / Command.TextureAssetPath; - auto Loaded = Assets::LoadTextureFromFile(FullPath); - if (!Loaded) { - A_CORE_WARN("SetMaterialTexture: failed to load '{}' for object '{}'", - Command.TextureAssetPath, Command.ObjectId); - return; - } - MeshIt->Material->BaseColorTexture = std::move(Loaded); - MeshIt->Material->TextureAssetPath = Command.TextureAssetPath; - } - - if (!DetailsIt->second.Material.has_value()) - DetailsIt->second.Material = EditorMaterialProperties{}; - DetailsIt->second.Material->TextureAssetPath = - Command.TextureAssetPath.empty() - ? std::nullopt - : std::optional(Command.TextureAssetPath); - - A_CORE_INFO("SetMaterialTexture: assigned '{}' to object '{}'", - Command.TextureAssetPath, Command.ObjectId); - PublishEvent({.Payload = MaterialTextureChangedEvent{ - .ObjectId = Command.ObjectId, - .TextureAssetPath = Command.TextureAssetPath, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetPhysicsPropertiesCommand &Command) { - (void)QueuedCommand; - - auto DetailsIt = m_State.Scene.ObjectDetailsById.find(Command.ObjectId); - if (DetailsIt == m_State.Scene.ObjectDetailsById.end()) { - return; - } - - DetailsIt->second.Physics = Command.Physics; - if (Command.Physics.BodyType == EditorPhysicsBodyType::None && - Command.Physics.ColliderType == EditorPhysicsColliderType::None) { - DetailsIt->second.Physics.reset(); - } - - PublishEvent({.Payload = PhysicsPropertiesChangedEvent{ - .ObjectId = Command.ObjectId, - .Physics = DetailsIt->second.Physics.value_or(EditorPhysicsProperties{}), - }}); -} - -void EditorSession::EnsurePhysicsWorldStarted() { - if (m_PhysicsWorld == nullptr) { - m_PhysicsWorld = std::make_unique(); - } - if (!m_PhysicsWorld->IsAvailable()) { - A_CORE_WARN("EditorSession: physics requested but backend is unavailable"); - return; - } - m_PhysicsWorld->Start(m_State.Scene); -} - -void EditorSession::StopPhysicsWorld() { - if (m_PhysicsWorld != nullptr) { - m_PhysicsWorld->Stop(); - } -} - -void EditorSession::StepRuntimePhysics(float DeltaTimeSeconds) { - if (m_State.RuntimeState != EditorRuntimeState::Playing || - m_PhysicsWorld == nullptr || !m_PhysicsWorld->IsRunning()) { - return; - } - - for (const PhysicsTransformUpdate &Update : m_PhysicsWorld->Step(DeltaTimeSeconds)) { - const EditorObjectDetails *Existing = FindObjectDetails(Update.ObjectId); - if (Existing == nullptr) { - continue; - } - - EditorTransformDetails Applied = Update.WorldTransform; - if (Existing->WorldTransform.has_value()) { - Applied.Scale = Existing->WorldTransform->Scale; - } else if (Existing->Transform.has_value()) { - Applied.Scale = Existing->Transform->Scale; - } - ApplyWorldTransform(Update.ObjectId, Applied, SessionUserId{1}, true); - } -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const PlaySessionCommand &Command) { - (void)Command; - EnsurePresence(QueuedCommand.Context.User); - m_RuntimeSceneSnapshot = RuntimeSceneSnapshot{ - .Scene = CloneEditorSceneState(m_State.Scene), - .SelectedObjectIds = m_State.SelectedObjectIds, - }; - m_State.RuntimeState = EditorRuntimeState::Playing; - EnsurePhysicsWorldStarted(); - PublishEvent({.Payload = RuntimeStateChangedEvent{ - .User = QueuedCommand.Context.User, - .State = m_State.RuntimeState, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const PauseSessionCommand &Command) { - (void)Command; - EnsurePresence(QueuedCommand.Context.User); - m_State.RuntimeState = EditorRuntimeState::Paused; - PublishEvent({.Payload = RuntimeStateChangedEvent{ - .User = QueuedCommand.Context.User, - .State = m_State.RuntimeState, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const ResumeSessionCommand &Command) { - (void)Command; - EnsurePresence(QueuedCommand.Context.User); - m_State.RuntimeState = EditorRuntimeState::Playing; - PublishEvent({.Payload = RuntimeStateChangedEvent{ - .User = QueuedCommand.Context.User, - .State = m_State.RuntimeState, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const StopSessionCommand &Command) { - (void)Command; - EnsurePresence(QueuedCommand.Context.User); - StopPhysicsWorld(); - if (m_RuntimeSceneSnapshot.has_value()) { - SetSceneState(std::move(m_RuntimeSceneSnapshot->Scene)); - m_State.SelectedObjectIds = std::move(m_RuntimeSceneSnapshot->SelectedObjectIds); - PruneInvalidSelections(); - m_RuntimeSceneSnapshot.reset(); - } - m_State.RuntimeState = EditorRuntimeState::Edit; - PublishEvent({.Payload = RuntimeStateChangedEvent{ - .User = QueuedCommand.Context.User, - .State = m_State.RuntimeState, - }}); -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const SetWorldSettingsCommand &Command) { - (void)QueuedCommand; - const std::string PreviousHDRPath = m_State.Scene.WorldSettings.SkyboxHDRPath; - HDRTextureSourceDataRef PreviousHDRData = m_State.Scene.WorldSettings.SkyboxHDRData; - - m_State.Scene.WorldSettings = Command.Settings; - - if (Command.Settings.SkyboxHDRPath.empty()) { - m_State.Scene.WorldSettings.SkyboxHDRData = nullptr; - } else if (Command.Settings.SkyboxHDRPath == PreviousHDRPath && - PreviousHDRData) { - // Path unchanged and we already have the data loaded — reuse it. - m_State.Scene.WorldSettings.SkyboxHDRData = std::move(PreviousHDRData); - } else { - HydrateWorldSettingsHDRData(m_State.Scene.WorldSettings, m_ContentDir, - m_EngineContentDir, - "SetWorldSettings"); - } -} - -void EditorSession::HandleCommand(const QueuedEditorCommand &QueuedCommand, - const PlaceActorCommand &Command) { - EnsurePresence(QueuedCommand.Context.User); - Instance *WorldFolder = EnsureWorldFolder(); - if (!WorldFolder) return; - - // Create the Actor parent - const std::string ActorId = BuildUniqueObjectId("Actor"); - const std::string ActorDisplayName = BuildUniqueDisplayName("Actor"); - const EditorTransformDetails ActorTransform{.Location = Command.Location}; - m_State.Scene.ObjectDetailsById.emplace( - ActorId, - EditorObjectDetails{ - .ObjectId = ActorId, - .DisplayName = ActorDisplayName, - .Kind = EditorSceneItemKind::Actor, - .Visible = true, - .SupportsTransform = true, - .TransformReadOnly = false, - .Transform = ActorTransform, - .WorldTransform = ActorTransform, - }); - Instance *ActorNode = CreateInstanceForTemplate("Actor", ActorId); - if (ActorNode) ActorNode->SetParent(WorldFolder); - - // Create the child object (if a template was specified) - std::string ChildId; - std::string ChildDisplayName; - if (!Command.ChildTemplateId.empty()) { - const EditorSceneItemKind ChildKind = KindForTemplateId(Command.ChildTemplateId); - ChildId = BuildUniqueObjectId(Command.ChildTemplateId); - ChildDisplayName = BuildUniqueDisplayName(Command.ChildTemplateId); - const bool ChildTransformable = SupportsTransformForKind(ChildKind); - m_State.Scene.ObjectDetailsById.emplace( - ChildId, - EditorObjectDetails{ - .ObjectId = ChildId, - .DisplayName = ChildDisplayName, - .Kind = ChildKind, - .Visible = true, - .SupportsTransform = ChildTransformable, - .TransformReadOnly = false, - .Transform = ChildTransformable - ? std::optional{EditorTransformDetails{}} - : std::nullopt, - .WorldTransform = ChildTransformable - ? std::optional{EditorTransformDetails{}} - : std::nullopt, - }); - if (Instance *ChildNode = - CreateInstanceForTemplate(Command.ChildTemplateId, ChildId)) { - ChildNode->SetParent(ActorNode ? ActorNode : WorldFolder); - } - } - - SyncItemsFromTree(); - PublishEvent({.Payload = ObjectCreatedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = ActorId, - .DisplayName = ActorDisplayName, - }}); - if (!ChildId.empty()) { - PublishEvent({.Payload = ObjectCreatedEvent{ - .User = QueuedCommand.Context.User, - .ObjectId = ChildId, - .DisplayName = ChildDisplayName, - }}); - if (!Command.ChildMeshAssetPath.empty()) { - HandleCommand(QueuedCommand, SetMeshAssetCommand{ - .ObjectId = ChildId, - .AssetPath = Command.ChildMeshAssetPath, - }); - } - } - - // Apply world-space location to the actor - HandleCommand(QueuedCommand, SetTransformCommand{ - .ObjectId = ActorId, - .Location = Command.Location, - }); -} - -void EditorSession::SetContentDir(std::filesystem::path ContentDir) { - m_ContentDir = std::move(ContentDir); - if (!m_State.Scene.WorldSettings.SkyboxHDRPath.empty()) { - m_State.Scene.WorldSettings.SkyboxHDRData = nullptr; - HydrateWorldSettingsHDRData(m_State.Scene.WorldSettings, m_ContentDir, - m_EngineContentDir, "SetContentDir"); - } -} - -void EditorSession::SetEngineContentDir(std::filesystem::path EngineContentDir) { - m_EngineContentDir = std::move(EngineContentDir); - if (!m_State.Scene.WorldSettings.SkyboxHDRPath.empty()) { - m_State.Scene.WorldSettings.SkyboxHDRData = nullptr; - HydrateWorldSettingsHDRData(m_State.Scene.WorldSettings, m_ContentDir, - m_EngineContentDir, "SetEngineContentDir"); - } -} - -void EditorSession::PublishScriptError(const std::string &ObjectId, - const std::string &Message) { - PublishEvent({ScriptErrorEvent{.ObjectId = ObjectId, .Message = Message}}); -} - -void EditorSession::PublishEvent(const EditorEvent &Event) { - m_MessageBus.PublishEvent(Event); -} -} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/RHIFactory.cpp b/AxiomInternal/AxiomRHI/RHIFactory.cpp new file mode 100644 index 00000000..5e467e6b --- /dev/null +++ b/AxiomInternal/AxiomRHI/RHIFactory.cpp @@ -0,0 +1,14 @@ +#include "RHI/RHIFactory.h" + +#include "AxiomRHI/Vulkan/VulkanRhiDevice.h" + +namespace Axiom { +std::unique_ptr CreateRHIDevice(RendererBackendType BackendType) { + switch (BackendType) { + case RendererBackendType::Vulkan: + return std::make_unique(); + } + + return nullptr; +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/SceneRendererBackendFactory.cpp b/AxiomInternal/AxiomRHI/SceneRendererBackendFactory.cpp new file mode 100644 index 00000000..20ee9611 --- /dev/null +++ b/AxiomInternal/AxiomRHI/SceneRendererBackendFactory.cpp @@ -0,0 +1,17 @@ +#include "AxiomRHI/SceneRendererBackendFactory.h" + +#include "AxiomRHI/Vulkan/VulkanSceneRenderer.h" + +namespace Axiom { +std::unique_ptr +CreateSceneRendererBackend(IRHIDevice &Device, RendererBackendType BackendType) { + (void)Device; + + switch (BackendType) { + case RendererBackendType::Vulkan: + return std::make_unique(); + } + + return nullptr; +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/SceneRendererBackendFactory.h b/AxiomInternal/AxiomRHI/SceneRendererBackendFactory.h new file mode 100644 index 00000000..213844a5 --- /dev/null +++ b/AxiomInternal/AxiomRHI/SceneRendererBackendFactory.h @@ -0,0 +1,38 @@ +#pragma once + +#include "Renderer/Material.h" +#include "Renderer/Mesh.h" +#include "Renderer/RenderScene.h" +#include "Renderer/RendererTypes.h" +#include "RHI/IRHI.h" + +#include +#include + +namespace Axiom { +class ISceneRendererBackend { +public: + virtual ~ISceneRendererBackend() = default; + + virtual void Init(IRHIDevice &Device, const RendererCreateInfo &CreateInfo) = 0; + virtual void Shutdown() = 0; + virtual void BeginFrame() = 0; + virtual std::shared_ptr + CreateMesh(const MeshData &Mesh, const MeshCreateOptions &Options) = 0; + virtual MaterialHandle CreateMaterialHandle(const MaterialInstance &Material) = 0; + virtual void UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material) = 0; + virtual void Render(RenderScene &Scene) = 0; + virtual void RenderImGui() = 0; + virtual void EndFrame() = 0; + virtual void SetViewMode(RendererViewMode ViewMode) = 0; + virtual void SetViewportFrameUser(SessionUserId User) = 0; + virtual void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput) = 0; + virtual std::optional ConsumeCapturedFrame() = 0; + virtual RendererFrameStats &AccessFrameStats() = 0; + [[nodiscard]] virtual const RendererFrameStats &GetFrameStats() const = 0; +}; + +std::unique_ptr +CreateSceneRendererBackend(IRHIDevice &Device, RendererBackendType BackendType); +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/GPUResourceQueue.h b/AxiomInternal/AxiomRHI/Vulkan/GPUResourceQueue.h new file mode 100644 index 00000000..903749bc --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/GPUResourceQueue.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +namespace Axiom { +class GPUResourceQueue { +public: + void Enqueue(std::function Function) { + std::scoped_lock Lock(m_Mutex); + m_Deletors.push_back(std::move(Function)); + } + + void Flush() { + std::deque> Pending; + { + std::scoped_lock Lock(m_Mutex); + Pending.swap(m_Deletors); + } + + for (auto It = Pending.rbegin(); It != Pending.rend(); ++It) { + (*It)(); + } + } + +private: + std::mutex m_Mutex; + std::deque> m_Deletors; +}; +} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanImGuiRenderer.cpp b/AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.cpp similarity index 88% rename from Axiom/Renderer/Vulkan/VulkanImGuiRenderer.cpp rename to AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.cpp index fe8188f3..4abc0943 100644 --- a/Axiom/Renderer/Vulkan/VulkanImGuiRenderer.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.cpp @@ -1,6 +1,6 @@ -#include "Renderer/Vulkan/VulkanImGuiRenderer.h" +#include "AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.h" -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" #include #include @@ -127,6 +127,18 @@ void VulkanImGuiRenderer::BuildStatsUiAndRender(RendererFrameStats &FrameStats, ImGui::Text("Triangles: %u", FrameStats.TriangleCount); ImGui::Text("Draw extent: %u x %u", FrameStats.DrawExtent.x, FrameStats.DrawExtent.y); +#if !defined(NDEBUG) + ImGui::Text("Material descriptor updates: %u", + FrameStats.DebugGraphicsMaterialDescriptorUpdates); + ImGui::Text("Opaque material descriptor binds: %u", + FrameStats.DebugOpaqueMaterialDescriptorBinds); + ImGui::Text("Opaque unique materials: %u", + FrameStats.DebugOpaqueUniqueMaterialCount); + ImGui::Text("Translucent material descriptor binds: %u", + FrameStats.DebugTranslucentMaterialDescriptorBinds); + ImGui::Text("Translucent unique materials: %u", + FrameStats.DebugTranslucentUniqueMaterialCount); +#endif int SelectedMode = static_cast(ViewMode); const char *ModeLabels[] = {"Lit", "Unlit", "Wireframe"}; if (ImGui::Combo("View Mode", &SelectedMode, ModeLabels, diff --git a/Axiom/Renderer/Vulkan/VulkanImGuiRenderer.h b/AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.h similarity index 87% rename from Axiom/Renderer/Vulkan/VulkanImGuiRenderer.h rename to AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.h index c1760633..031655dd 100644 --- a/Axiom/Renderer/Vulkan/VulkanImGuiRenderer.h +++ b/AxiomInternal/AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.h @@ -1,8 +1,8 @@ #pragma once -#include "Renderer/RendererBackend.h" -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "Renderer/RendererTypes.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" namespace Axiom { class VulkanImGuiRenderer { diff --git a/Axiom/Renderer/Vulkan/VulkanBuffer.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.cpp similarity index 97% rename from Axiom/Renderer/Vulkan/VulkanBuffer.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.cpp index e8dd85bd..ec0fe36a 100644 --- a/Axiom/Renderer/Vulkan/VulkanBuffer.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.cpp @@ -1,6 +1,6 @@ -#include "Renderer/Vulkan/VulkanBuffer.h" +#include "AxiomRHI/Vulkan/VulkanBuffer.h" -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanBuffer.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.h similarity index 94% rename from Axiom/Renderer/Vulkan/VulkanBuffer.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.h index c549426d..a51495e6 100644 --- a/Axiom/Renderer/Vulkan/VulkanBuffer.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanBuffer.h @@ -1,6 +1,6 @@ #pragma once -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanCommandContext.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.cpp similarity index 97% rename from Axiom/Renderer/Vulkan/VulkanCommandContext.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.cpp index c76342a9..eb336ff9 100644 --- a/Axiom/Renderer/Vulkan/VulkanCommandContext.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.cpp @@ -1,6 +1,6 @@ -#include "Renderer/Vulkan/VulkanCommandContext.h" +#include "AxiomRHI/Vulkan/VulkanCommandContext.h" -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" namespace Axiom { void VulkanCommandContext::Init(VkDevice Device, uint32_t GraphicsQueueFamily) { diff --git a/Axiom/Renderer/Vulkan/VulkanCommandContext.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.h similarity index 92% rename from Axiom/Renderer/Vulkan/VulkanCommandContext.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.h index b431d6f6..d4737c3d 100644 --- a/Axiom/Renderer/Vulkan/VulkanCommandContext.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanCommandContext.h @@ -1,7 +1,7 @@ #pragma once -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanContext.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanContext.cpp similarity index 70% rename from Axiom/Renderer/Vulkan/VulkanContext.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanContext.cpp index be51c4fd..9008b26c 100644 --- a/Axiom/Renderer/Vulkan/VulkanContext.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanContext.cpp @@ -1,16 +1,14 @@ -#include "Renderer/Vulkan/VulkanContext.h" +#include "AxiomRHI/Vulkan/VulkanContext.h" -#include +#include "Renderer/RenderSurface.h" -#define GLFW_INCLUDE_VULKAN -#include +#include #include #include #include "Core/Log.h" #include "Core/VulkanLoader.h" -#include "Renderer/Vulkan/VulkanStringUtils.h" namespace { constexpr bool bUseValidationLayers = true; @@ -28,10 +26,25 @@ VulkanDebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT MessageSeverity, VkDebugSeverityToString(MessageSeverity), pCallbackData->pMessage); return VK_FALSE; } + +[[nodiscard]] const char * +PresentationSurfaceResultToString( + Axiom::PresentationSurfaceResult Result) { + switch (Result) { + case Axiom::PresentationSurfaceResult::Success: + return "success"; + case Axiom::PresentationSurfaceResult::Unsupported: + return "unsupported"; + case Axiom::PresentationSurfaceResult::InitializationFailed: + return "initialization failed"; + } + + return "unknown"; +} } // namespace namespace Axiom { -void VulkanContext::Init(void *WindowHandle, bool CreateSurface) { +void VulkanContext::Init(const IRenderSurface &Surface) { const VulkanLoaderInfo &LoaderInfo = GetVulkanLoaderInfo(); if (!LoaderInfo.IsAvailable) { A_CORE_CRITICAL("Failed to resolve a Vulkan loader for Vulkan context init"); @@ -45,26 +58,30 @@ void VulkanContext::Init(void *WindowHandle, bool CreateSurface) { A_CORE_INFO("Volk successfully initialized using {0}", LoaderInfo.UsesCustomLoader ? "custom loader" : "default loader"); - if (CreateSurface && !glfwVulkanSupported()) { - A_CORE_CRITICAL("GLFW reports Vulkan is not supported on this machine!"); + if (Surface.SupportsPresentation() && + !Surface.SupportsPresentationBackend(PresentationBackendType::Vulkan)) { + A_CORE_CRITICAL( + "The active render surface does not support Vulkan presentation."); Axiom::Log::Flush(); std::abort(); } vkb::InstanceBuilder Builder = [&LoaderInfo]() { if (LoaderInfo.UsesCustomLoader) { - return vkb::InstanceBuilder{LoaderInfo.ProcAddr}; + return vkb::InstanceBuilder{ + reinterpret_cast(LoaderInfo.ProcAddr)}; } return vkb::InstanceBuilder{}; }(); - if (!CreateSurface) { + if (!Surface.SupportsPresentation()) { Builder.set_headless(); } vkb::Result SystemInfoReturn = LoaderInfo.UsesCustomLoader ? vkb::SystemInfo::get_system_info( - LoaderInfo.ProcAddr) + reinterpret_cast( + LoaderInfo.ProcAddr)) : vkb::SystemInfo::get_system_info(); if (!SystemInfoReturn) { A_CORE_CRITICAL( @@ -107,12 +124,13 @@ void VulkanContext::Init(void *WindowHandle, bool CreateSurface) { volkLoadInstance(Instance); - if (CreateSurface) { - const VkResult SurfaceResult = glfwCreateWindowSurface( - Instance, static_cast(WindowHandle), nullptr, &Surface); - if (SurfaceResult != VK_SUCCESS) { + if (Surface.SupportsPresentation()) { + const PresentationSurfaceResult SurfaceResult = + Surface.CreatePresentationSurface(PresentationBackendType::Vulkan, + Instance, &this->Surface); + if (SurfaceResult != PresentationSurfaceResult::Success) { A_CORE_CRITICAL("Failed to create Vulkan window surface: {0}", - VkResultToString(SurfaceResult)); + PresentationSurfaceResultToString(SurfaceResult)); Axiom::Log::Flush(); std::abort(); } diff --git a/Axiom/Renderer/Vulkan/VulkanContext.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanContext.h similarity index 69% rename from Axiom/Renderer/Vulkan/VulkanContext.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanContext.h index d590ae0d..3cbdf317 100644 --- a/Axiom/Renderer/Vulkan/VulkanContext.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanContext.h @@ -1,14 +1,16 @@ #pragma once -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" #include namespace Axiom { +class IRenderSurface; + class VulkanContext { public: - void Init(void *WindowHandle, bool CreateSurface); + void Init(const IRenderSurface &Surface); void Shutdown(); VkInstance Instance{VK_NULL_HANDLE}; @@ -20,4 +22,3 @@ class VulkanContext { DeletionQueue m_DeletionQueue; }; } // namespace Axiom - diff --git a/Axiom/Renderer/Vulkan/VulkanDeletionQueue.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanDeletionQueue.h similarity index 100% rename from Axiom/Renderer/Vulkan/VulkanDeletionQueue.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanDeletionQueue.h diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.cpp new file mode 100644 index 00000000..74a8d95e --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.cpp @@ -0,0 +1,125 @@ +#include "AxiomRHI/Vulkan/VulkanDescriptors.h" + +#include "Core/Log.h" + +#include + +#include + +void DescriptorLayoutBuilder::AddBinding(uint32_t Binding, + VkDescriptorType Type) { + VkDescriptorSetLayoutBinding NewBind{}; + NewBind.binding = Binding; + NewBind.descriptorType = Type; + NewBind.descriptorCount = 1; + + Bindings.push_back(NewBind); +} + +void DescriptorLayoutBuilder::Clear() { Bindings.clear(); } + +VkDescriptorSetLayout +DescriptorLayoutBuilder::Build(VkDevice Device, VkShaderStageFlags StageFlags, + void *pNext, + VkDescriptorSetLayoutCreateFlags Flags) { + for (auto &Binding : Bindings) { + Binding.stageFlags |= StageFlags; + } + + VkDescriptorSetLayoutCreateInfo Info = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + Info.pNext = pNext; + + Info.pBindings = Bindings.data(); + Info.bindingCount = static_cast(Bindings.size()); + + Info.flags = Flags; + + VkDescriptorSetLayout Set; + VK_CHECK(vkCreateDescriptorSetLayout(Device, &Info, VK_NULL_HANDLE, &Set)); + + return Set; +} + +VkDescriptorPool DescriptorAllocator::CreatePool(VkDevice Device, + uint32_t MaxSets) { + std::vector PoolSizes; + PoolSizes.reserve(m_PoolRatios.size()); + for (PoolSizeRatio Ratio : m_PoolRatios) { + PoolSizes.push_back(VkDescriptorPoolSize{ + .type = Ratio.Type, + .descriptorCount = + std::max(1u, static_cast(Ratio.Ratio * MaxSets))}); + } + + VkDescriptorPoolCreateInfo PoolInfo = { + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + PoolInfo.maxSets = MaxSets; + + PoolInfo.pPoolSizes = PoolSizes.data(); + PoolInfo.poolSizeCount = static_cast(PoolSizes.size()); + + PoolInfo.flags = 0; + + VkDescriptorPool Pool = VK_NULL_HANDLE; + VK_CHECK(vkCreateDescriptorPool(Device, &PoolInfo, VK_NULL_HANDLE, &Pool)); + return Pool; +} + +void DescriptorAllocator::InitPool(VkDevice Device, uint32_t MaxSets, + std::span PoolRatios) { + m_PoolRatios.assign(PoolRatios.begin(), PoolRatios.end()); + m_Pools.clear(); + m_NextPoolMaxSets = std::max(1u, MaxSets); + m_Pools.push_back(CreatePool(Device, m_NextPoolMaxSets)); +} + +void DescriptorAllocator::ClearDescriptors(VkDevice Device) { + for (VkDescriptorPool Pool : m_Pools) { + vkResetDescriptorPool(Device, Pool, 0); + } +} + +void DescriptorAllocator::DestroyPool(VkDevice Device) { + for (VkDescriptorPool Pool : m_Pools) { + vkDestroyDescriptorPool(Device, Pool, VK_NULL_HANDLE); + } + m_Pools.clear(); + m_PoolRatios.clear(); + m_NextPoolMaxSets = 0; +} + +VkDescriptorSet DescriptorAllocator::Allocate(VkDevice Device, + VkDescriptorSetLayout Layout) { + VkDescriptorSetAllocateInfo AllocInfo{ + .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, + .pNext = VK_NULL_HANDLE, + .descriptorPool = VK_NULL_HANDLE, + .descriptorSetCount = 1, + .pSetLayouts = &Layout}; + + for (;;) { + for (VkDescriptorPool Pool : m_Pools) { + AllocInfo.descriptorPool = Pool; + VkDescriptorSet DescriptorSet = VK_NULL_HANDLE; + const VkResult Result = + vkAllocateDescriptorSets(Device, &AllocInfo, &DescriptorSet); + if (Result == VK_SUCCESS) { + return DescriptorSet; + } + if (Result != VK_ERROR_OUT_OF_POOL_MEMORY && + Result != VK_ERROR_FRAGMENTED_POOL) { + A_CORE_ERROR("Detected Vulkan error: {0}", VkResultToString(Result)); + Axiom::Log::Flush(); + abort(); + } + } + + const uint32_t NewPoolMaxSets = std::max(64u, m_NextPoolMaxSets); + A_CORE_WARN( + "Descriptor pool exhausted; allocating an additional pool with capacity for {0} descriptor sets.", + NewPoolMaxSets); + m_Pools.push_back(CreatePool(Device, NewPoolMaxSets)); + m_NextPoolMaxSets = std::max(NewPoolMaxSets + 1, NewPoolMaxSets * 2); + } +} diff --git a/Axiom/Renderer/Vulkan/VulkanDescriptors.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.h similarity index 76% rename from Axiom/Renderer/Vulkan/VulkanDescriptors.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.h index 608c716e..2acad687 100644 --- a/Axiom/Renderer/Vulkan/VulkanDescriptors.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanDescriptors.h @@ -1,6 +1,6 @@ #pragma once -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" struct DescriptorLayoutBuilder { std::vector Bindings; @@ -18,13 +18,17 @@ struct DescriptorAllocator { float Ratio; }; - VkDescriptorPool Pool; - void InitPool(VkDevice Device, uint32_t MaxSets, std::span PoolRatios); void ClearDescriptors(VkDevice Device); void DestroyPool(VkDevice Device); VkDescriptorSet Allocate(VkDevice Device, VkDescriptorSetLayout Layout); -}; +private: + VkDescriptorPool CreatePool(VkDevice Device, uint32_t MaxSets); + + std::vector m_PoolRatios; + std::vector m_Pools; + uint32_t m_NextPoolMaxSets{0}; +}; diff --git a/Axiom/Renderer/Vulkan/VulkanDevice.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.cpp similarity index 81% rename from Axiom/Renderer/Vulkan/VulkanDevice.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.cpp index 75475567..ce5958e0 100644 --- a/Axiom/Renderer/Vulkan/VulkanDevice.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.cpp @@ -1,6 +1,6 @@ -#include "Renderer/Vulkan/VulkanDevice.h" +#include "AxiomRHI/Vulkan/VulkanDevice.h" -#include "Renderer/Vulkan/VulkanContext.h" +#include "AxiomRHI/Vulkan/VulkanContext.h" #include @@ -48,6 +48,16 @@ void VulkanDevice::Init(VulkanContext &Context) { GraphicsQueue = VkbDevice.get_queue(vkb::QueueType::graphics).value(); GraphicsQueueFamily = VkbDevice.get_queue_index(vkb::QueueType::graphics).value(); + auto TransferQueueResult = VkbDevice.get_queue(vkb::QueueType::transfer); + auto TransferQueueFamilyResult = + VkbDevice.get_queue_index(vkb::QueueType::transfer); + if (TransferQueueResult && TransferQueueFamilyResult) { + TransferQueue = TransferQueueResult.value(); + TransferQueueFamily = TransferQueueFamilyResult.value(); + } else { + TransferQueue = GraphicsQueue; + TransferQueueFamily = GraphicsQueueFamily; + } VmaVulkanFunctions VulkanFunctions = {}; VulkanFunctions.vkGetInstanceProcAddr = vkGetInstanceProcAddr; diff --git a/Axiom/Renderer/Vulkan/VulkanDevice.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.h similarity index 73% rename from Axiom/Renderer/Vulkan/VulkanDevice.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.h index d8c314ec..482c36bb 100644 --- a/Axiom/Renderer/Vulkan/VulkanDevice.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanDevice.h @@ -1,7 +1,7 @@ #pragma once -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" typedef struct VmaAllocator_T *VmaAllocator; @@ -17,6 +17,8 @@ class VulkanDevice { VkDevice Device{VK_NULL_HANDLE}; VkQueue GraphicsQueue{VK_NULL_HANDLE}; uint32_t GraphicsQueueFamily{0}; + VkQueue TransferQueue{VK_NULL_HANDLE}; + uint32_t TransferQueueFamily{0}; VmaAllocator Allocator{nullptr}; private: diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.cpp new file mode 100644 index 00000000..1e88c386 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.cpp @@ -0,0 +1,755 @@ +#include "AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.h" + +#include "Assets/SvgTexture.h" +#include "Core/HeadlessRuntimeInstrumentation.h" +#include "Core/Log.h" +#include "Renderer/Camera.h" +#include "Renderer/RenderSurface.h" +#include "AxiomRHI/Vulkan/VulkanContext.h" +#include "AxiomRHI/Vulkan/VulkanImage.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanRhiObjects.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace Axiom { +#ifndef AXIOM_CONTENT_DIR +#define AXIOM_CONTENT_DIR "Content" +#endif + +namespace { +void TransitionImageRange(VkCommandBuffer CommandBuffer, VkImage Image, + VkImageLayout OldLayout, VkImageLayout NewLayout, + VkImageAspectFlags AspectMask, uint32_t BaseMipLevel, + uint32_t LevelCount) { + const VkImageMemoryBarrier2 ImageBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .dstAccessMask = + VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT, + .oldLayout = OldLayout, + .newLayout = NewLayout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = Image, + .subresourceRange = + {.aspectMask = AspectMask, + .baseMipLevel = BaseMipLevel, + .levelCount = LevelCount, + .baseArrayLayer = 0, + .layerCount = 1}}; + const VkDependencyInfo DependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &ImageBarrier}; + vkCmdPipelineBarrier2(CommandBuffer, &DependencyInfo); +} +} // namespace + +void VulkanDrawSubmissionSystem::Init(const CreateInfo &CreateInfo) { + m_Surface = CreateInfo.Surface; + m_Context = &CreateInfo.Context; + m_Device = &CreateInfo.Device; + m_CommandContext = &CreateInfo.CommandContext; + m_Resources = &CreateInfo.Resources; + m_Pipelines = &CreateInfo.Pipelines; + m_MaterialResources = &CreateInfo.MaterialResources; + m_OcclusionCulling = &CreateInfo.OcclusionCulling; + m_EnableImGui = CreateInfo.EnableImGui; + m_HasPresentationSurface = CreateInfo.HasPresentationSurface; + m_RecordPreparedScenePasses = CreateInfo.RecordPreparedScenePasses; + + VkPhysicalDeviceProperties DeviceProperties{}; + vkGetPhysicalDeviceProperties(m_Device->PhysicalDevice, &DeviceProperties); + m_TimestampPeriod = DeviceProperties.limits.timestampPeriod; + + InitTransferQueue(); + InitSpecializedRenderers(); + + if (m_EnableImGui) { + m_ImGuiRenderer.Init({.WindowHandle = m_Surface->GetNativeWindowHandle(), + .Instance = m_Context->Instance, + .PhysicalDevice = m_Device->PhysicalDevice, + .Device = m_Device->Device, + .Queue = m_Device->GraphicsQueue, + .QueueFamily = m_Device->GraphicsQueueFamily, + .SwapchainImageFormat = + m_Resources->GetSwapchain().ImageFormat, + .DeletionQueue = &m_RendererDeletionQueue}); + } + + m_IsInitialized = true; +} + +void VulkanDrawSubmissionSystem::SetRecordPreparedScenePasses( + std::function + RecordPreparedScenePasses) { + m_RecordPreparedScenePasses = std::move(RecordPreparedScenePasses); +} + +void VulkanDrawSubmissionSystem::InitSpecializedRenderers() { + m_MaterialResources->InitFallbackTexture(); + const std::filesystem::path LightIconPath = + std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine" / "lightbulb.svg"; + MaterialInstance LightBillboardMaterial{}; + if (const auto IconTexture = Assets::LoadSvgTextureFromFile(LightIconPath)) { + LightBillboardMaterial.BaseColorTexture = IconTexture; + } else { + A_CORE_WARN( + "Failed to load light billboard icon from {0}; using fallback texture", + LightIconPath.string()); + } + m_LightBillboardMaterialHandle = + m_MaterialResources->CreateMaterialHandle(LightBillboardMaterial); + + m_GizmoRenderer.Init({.Device = m_Device->Device, + .DrawImageFormat = m_Resources->GetDrawImage().ImageFormat}, + m_RendererDeletionQueue); + const VkImageView TextureView = + m_MaterialResources->ResolveMaterialTextureView( + m_MaterialResources->ResolveMaterialHandle(m_LightBillboardMaterialHandle)); + m_LightBillboardRenderer.Init( + {.Device = m_Device->Device, + .DrawImageFormat = m_Resources->GetDrawImage().ImageFormat, + .DescriptorAllocator = &m_Resources->GetDescriptorAllocator(), + .TextureView = TextureView, + .TextureSampler = m_Resources->GetTextureSampler()}, + m_RendererDeletionQueue); +} + +void VulkanDrawSubmissionSystem::InitTransferQueue() { + m_TransferQueue = m_Device->TransferQueue; + m_TransferQueueFamily = m_Device->TransferQueueFamily; + + VkCommandPoolCreateInfo CommandPoolInfo{ + .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = m_TransferQueueFamily}; + VK_CHECK(vkCreateCommandPool(m_Device->Device, &CommandPoolInfo, + VK_NULL_HANDLE, &m_TransferCommandPool)); + + VkSemaphoreTypeCreateInfo TimelineTypeInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, + .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE, + .initialValue = 0}; + VkSemaphoreCreateInfo SemaphoreInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = &TimelineTypeInfo}; + VK_CHECK(vkCreateSemaphore(m_Device->Device, &SemaphoreInfo, VK_NULL_HANDLE, + &m_TransferTimelineSemaphore)); +} + +void VulkanDrawSubmissionSystem::Shutdown() { + CollectCompletedTransfers(); + vkDeviceWaitIdle(m_Device->Device); + CollectCompletedTransfers(); + m_RendererDeletionQueue.Flush(); + ShutdownTransferQueue(); +} + +void VulkanDrawSubmissionSystem::ShutdownTransferQueue() { + for (auto &PendingTransfer : m_PendingTransfers) { + if (PendingTransfer.Cleanup) { + PendingTransfer.Cleanup(); + } + if (PendingTransfer.CommandBuffer != VK_NULL_HANDLE) { + vkFreeCommandBuffers(m_Device->Device, m_TransferCommandPool, 1, + &PendingTransfer.CommandBuffer); + } + } + m_PendingTransfers.clear(); + + if (m_TransferTimelineSemaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(m_Device->Device, m_TransferTimelineSemaphore, + VK_NULL_HANDLE); + m_TransferTimelineSemaphore = VK_NULL_HANDLE; + } + if (m_TransferCommandPool != VK_NULL_HANDLE) { + vkDestroyCommandPool(m_Device->Device, m_TransferCommandPool, VK_NULL_HANDLE); + m_TransferCommandPool = VK_NULL_HANDLE; + } +} + +void VulkanDrawSubmissionSystem::CollectCompletedTransfers() { + if (m_TransferTimelineSemaphore == VK_NULL_HANDLE) { + return; + } + + uint64_t CompletedValue = 0; + VK_CHECK(vkGetSemaphoreCounterValue(m_Device->Device, + m_TransferTimelineSemaphore, + &CompletedValue)); + + auto It = m_PendingTransfers.begin(); + while (It != m_PendingTransfers.end()) { + if (It->SignalValue > CompletedValue) { + ++It; + continue; + } + + if (It->Cleanup) { + It->Cleanup(); + } + if (It->CommandBuffer != VK_NULL_HANDLE) { + vkFreeCommandBuffers(m_Device->Device, m_TransferCommandPool, 1, + &It->CommandBuffer); + } + It = m_PendingTransfers.erase(It); + } +} + +void VulkanDrawSubmissionSystem::SubmitTransferUpload( + std::function &&RecordUpload, + std::function &&Cleanup) { + CollectCompletedTransfers(); + + VkCommandBufferAllocateInfo AllocateInfo = + VkInit::CommandBufferAllocateInfo(m_TransferCommandPool, 1); + VkCommandBuffer CommandBuffer = VK_NULL_HANDLE; + VK_CHECK(vkAllocateCommandBuffers(m_Device->Device, &AllocateInfo, + &CommandBuffer)); + + const VkCommandBufferBeginInfo BeginInfo = + VkInit::CommandBufferBeginInfo(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + VK_CHECK(vkBeginCommandBuffer(CommandBuffer, &BeginInfo)); + RecordUpload(CommandBuffer); + VK_CHECK(vkEndCommandBuffer(CommandBuffer)); + + const uint64_t SignalValue = ++m_NextTransferSignalValue; + const VkCommandBufferSubmitInfo CommandInfo = + VkInit::CommandBufferSubmitInfo(CommandBuffer); + const VkSemaphoreSubmitInfo SignalInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = m_TransferTimelineSemaphore, + .value = SignalValue, + .stageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT}; + const VkSubmitInfo2 SubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &CommandInfo, + .signalSemaphoreInfoCount = 1, + .pSignalSemaphoreInfos = &SignalInfo}; + VK_CHECK(vkQueueSubmit2(m_TransferQueue, 1, &SubmitInfo, VK_NULL_HANDLE)); + + m_LastGraphicsWaitValue = SignalValue; + m_PendingTransfers.push_back( + {.SignalValue = SignalValue, + .CommandBuffer = CommandBuffer, + .Cleanup = std::move(Cleanup)}); +} + +void VulkanDrawSubmissionSystem::BeginFrame(bool StopRendering) { + if (StopRendering || !m_EnableImGui) { + return; + } + m_ImGuiRenderer.BeginFrame(); +} + +void VulkanDrawSubmissionSystem::RenderImGui(bool StopRendering, + RendererViewMode &ViewMode) { + if (StopRendering || !m_EnableImGui) { + return; + } + m_ImGuiRenderer.BuildStatsUiAndRender(m_FrameStats, ViewMode); +} + +void VulkanDrawSubmissionSystem::CollectFrameStats(MeshFrameResources &Frame) { + if (!Frame.HasValidTimestamps) { + return; + } + + uint64_t Timestamps[TimestampQueryCount] = {}; + const VkResult Result = vkGetQueryPoolResults( + m_Device->Device, Frame.TimestampQueryPool, 0, TimestampQueryCount, + sizeof(Timestamps), Timestamps, sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + if (Result != VK_SUCCESS) { + return; + } + + const auto ToMilliseconds = [this](uint64_t Start, uint64_t End) { + if (End <= Start) { + return 0.0f; + } + const double Nanoseconds = + static_cast(End - Start) * static_cast(m_TimestampPeriod); + return static_cast(Nanoseconds / 1'000'000.0); + }; + + m_FrameStats.GpuBackgroundMs = ToMilliseconds(Timestamps[0], Timestamps[1]); + m_FrameStats.GpuMeshMs = ToMilliseconds(Timestamps[2], Timestamps[3]); +} + +void VulkanDrawSubmissionSystem::BuildHzb(VkCommandBuffer CommandBuffer, + MeshFrameResources &Frame) { + if (m_Resources->GetHzbReduceDescriptorSets().empty()) { + Frame.HasValidOcclusionData = false; + return; + } + + const VkExtent2D DrawExtent = {m_Resources->GetDrawImage().ImageExtent.width, + m_Resources->GetDrawImage().ImageExtent.height}; + VkUtil::TransitionImage(CommandBuffer, m_Resources->GetRasterDepthImage().Image, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL); + TransitionImageRange( + CommandBuffer, m_Resources->GetHzbImage().Image, + m_Resources->GetHzbImageLayout(), VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_ASPECT_COLOR_BIT, 0, + static_cast(m_Resources->GetHzbMipImageViews().size())); + m_Resources->SetHzbImageLayout(VK_IMAGE_LAYOUT_GENERAL); + + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Pipelines->GetHzbReducePipeline()); + for (size_t MipLevel = 0; + MipLevel < m_Resources->GetHzbReduceDescriptorSets().size(); ++MipLevel) { + const VkDescriptorSet DescriptorSet = + m_Resources->GetHzbReduceDescriptorSets()[MipLevel]; + vkCmdBindDescriptorSets(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Pipelines->GetHzbReducePipelineLayout(), 0, 1, + &DescriptorSet, 0, VK_NULL_HANDLE); + + const VkExtent2D SourceExtent = + (MipLevel == 0) ? DrawExtent : m_Resources->GetHzbMipExtents()[MipLevel - 1]; + const VkExtent2D DestinationExtent = + m_Resources->GetHzbMipExtents()[MipLevel]; + HzbReducePushConstants PushConstants{}; + PushConstants.Dimensions = + glm::uvec4(SourceExtent.width, SourceExtent.height, + DestinationExtent.width, DestinationExtent.height); + vkCmdPushConstants(CommandBuffer, m_Pipelines->GetHzbReducePipelineLayout(), + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), + &PushConstants); + vkCmdDispatch(CommandBuffer, + static_cast(std::ceil(DestinationExtent.width / 8.0f)), + static_cast(std::ceil(DestinationExtent.height / 8.0f)), + 1); + TransitionImageRange(CommandBuffer, m_Resources->GetHzbImage().Image, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_ASPECT_COLOR_BIT, static_cast(MipLevel), + 1); + } + + TransitionImageRange( + CommandBuffer, m_Resources->GetHzbImage().Image, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_ASPECT_COLOR_BIT, 0, + static_cast(m_Resources->GetHzbMipImageViews().size())); + m_Resources->SetHzbImageLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + + for (size_t MipLevel = 0; MipLevel < m_Resources->GetHzbMipExtents().size(); + ++MipLevel) { + VkBufferImageCopy CopyRegion{}; + CopyRegion.bufferOffset = m_Resources->GetHzbMipOffsets()[MipLevel]; + CopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + CopyRegion.imageSubresource.mipLevel = static_cast(MipLevel); + CopyRegion.imageSubresource.layerCount = 1; + CopyRegion.imageExtent = {m_Resources->GetHzbMipExtents()[MipLevel].width, + m_Resources->GetHzbMipExtents()[MipLevel].height, + 1}; + vkCmdCopyImageToBuffer(CommandBuffer, m_Resources->GetHzbImage().Image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + Frame.HzbReadbackBuffer.Buffer, 1, &CopyRegion); + } + + VkBufferMemoryBarrier2 ReadbackBarrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, + .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = Frame.HzbReadbackBuffer.Buffer, + .size = Frame.HzbReadbackBuffer.Size}; + const VkDependencyInfo ReadbackDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &ReadbackBarrier}; + vkCmdPipelineBarrier2(CommandBuffer, &ReadbackDependencyInfo); + + VkUtil::TransitionImage(CommandBuffer, m_Resources->GetRasterDepthImage().Image, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + Frame.HasValidOcclusionData = true; +} + +void VulkanDrawSubmissionSystem::ClearDepthImage(VkCommandBuffer CommandBuffer, + uint64_t FrameNumber) { + const auto PreviousLayout = + (FrameNumber == 0) ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; + VkUtil::TransitionImage(CommandBuffer, m_Resources->GetRasterDepthImage().Image, + PreviousLayout, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); +} + +void VulkanDrawSubmissionSystem::DrawMeshes(VkCommandBuffer CommandBuffer, + RenderScene &Scene, + uint64_t FrameNumber, + RendererViewMode ViewMode) { + if (!m_RecordPreparedScenePasses) { + return; + } + m_RecordPreparedScenePasses(CommandBuffer, Scene, FrameNumber, ViewMode); +} + +void VulkanDrawSubmissionSystem::RecordOffscreenCapture( + VkCommandBuffer CommandBuffer, const AllocatedBuffer &ReadbackBuffer, + VkExtent2D DrawExtent) { + VkBufferImageCopy CopyRegion{}; + CopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + CopyRegion.imageSubresource.layerCount = 1; + CopyRegion.imageExtent = {DrawExtent.width, DrawExtent.height, 1}; + vkCmdCopyImageToBuffer(CommandBuffer, m_Resources->GetDrawImage().Image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + ReadbackBuffer.Buffer, 1, &CopyRegion); + + VkBufferMemoryBarrier2 ReadbackBarrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT, + .srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_HOST_BIT, + .dstAccessMask = VK_ACCESS_2_HOST_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = ReadbackBuffer.Buffer, + .size = ReadbackBuffer.Size}; + const VkDependencyInfo ReadbackDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &ReadbackBarrier}; + vkCmdPipelineBarrier2(CommandBuffer, &ReadbackDependencyInfo); +} + +float VulkanDrawSubmissionSystem::HalfToFloat(uint16_t Value) { + const uint32_t Sign = static_cast(Value & 0x8000u) << 16u; + const uint32_t Exponent = (Value & 0x7C00u) >> 10u; + const uint32_t Mantissa = Value & 0x03FFu; + uint32_t ResultBits = 0; + if (Exponent == 0u) { + if (Mantissa == 0u) { + ResultBits = Sign; + } else { + uint32_t ShiftedMantissa = Mantissa; + uint32_t AdjustedExponent = 127u - 15u + 1u; + while ((ShiftedMantissa & 0x0400u) == 0u) { + ShiftedMantissa <<= 1u; + --AdjustedExponent; + } + ShiftedMantissa &= 0x03FFu; + ResultBits = + Sign | (AdjustedExponent << 23u) | (ShiftedMantissa << 13u); + } + } else if (Exponent == 0x1Fu) { + ResultBits = Sign | 0x7F800000u | (Mantissa << 13u); + } else { + ResultBits = + Sign | ((Exponent + (127u - 15u)) << 23u) | (Mantissa << 13u); + } + return std::bit_cast(ResultBits); +} + +uint8_t VulkanDrawSubmissionSystem::LinearToByte(float Value) { + return static_cast( + std::round(std::clamp(Value, 0.0f, 1.0f) * 255.0f)); +} + +std::optional VulkanDrawSubmissionSystem::ConvertCapturedFrameToRgba8( + const AllocatedBuffer &ReadbackBuffer, uint64_t FrameNumber, + VkExtent2D DrawExtent) { + if (ReadbackBuffer.Info.pMappedData == nullptr) { + return std::nullopt; + } + + vmaInvalidateAllocation(m_Device->Allocator, ReadbackBuffer.Allocation, 0, + ReadbackBuffer.Size); + CapturedFrame Frame{}; + Frame.FrameIndex = FrameNumber; + Frame.Width = DrawExtent.width; + Frame.Height = DrawExtent.height; + Frame.Pixels.resize(static_cast(Frame.Width) * Frame.Height * 4u); + + const auto *Source = static_cast(ReadbackBuffer.Info.pMappedData); + auto *Destination = reinterpret_cast(Frame.Pixels.data()); + for (size_t PixelIndex = 0; + PixelIndex < static_cast(Frame.Width) * Frame.Height; + ++PixelIndex) { + const size_t SourceIndex = PixelIndex * 4u; + const size_t DestinationIndex = PixelIndex * 4u; + Destination[DestinationIndex + 0] = + LinearToByte(HalfToFloat(Source[SourceIndex + 0])); + Destination[DestinationIndex + 1] = + LinearToByte(HalfToFloat(Source[SourceIndex + 1])); + Destination[DestinationIndex + 2] = + LinearToByte(HalfToFloat(Source[SourceIndex + 2])); + Destination[DestinationIndex + 3] = + LinearToByte(HalfToFloat(Source[SourceIndex + 3])); + } + + return Frame; +} + +void VulkanDrawSubmissionSystem::PublishCompletedOffscreenFrame( + VulkanResourceManager::OffscreenCaptureFrame &CaptureFrame, VkExtent2D DrawExtent, + IViewportFrameOutput *FrameOutput) { + const auto CapturedFrameResult = ConvertCapturedFrameToRgba8( + CaptureFrame.ReadbackBuffer, CaptureFrame.SubmittedFrameNumber, DrawExtent); + CaptureFrame.HasPendingReadback = false; + HeadlessRuntimeInstrumentation::RecordOffscreenReadbackCompleted( + CaptureFrame.SubmittedFrameNumber, CaptureFrame.SubmittedUser, + CountPendingOffscreenReadbacks()); + if (!CapturedFrameResult.has_value()) { + return; + } + + m_CapturedFrames.push_back(std::move(*CapturedFrameResult)); + while (m_CapturedFrames.size() > FRAME_OVERLAP) { + m_CapturedFrames.pop_front(); + } + + if (FrameOutput == nullptr) { + return; + } + + const CapturedFrame &Captured = m_CapturedFrames.back(); + const auto *Bytes = reinterpret_cast(Captured.Pixels.data()); + FrameOutput->OnViewportFrame({ + .FrameIndex = Captured.FrameIndex, + .Width = Captured.Width, + .Height = Captured.Height, + .Format = ViewportFrameFormat::R8G8B8A8Unorm, + .Pixels = std::span(Bytes, Captured.Pixels.size()), + .User = CaptureFrame.SubmittedUser, + }); +} + +void VulkanDrawSubmissionSystem::PublishCompletedOffscreenFrames( + IViewportFrameOutput *FrameOutput) { + const VkExtent2D DrawExtent = {m_Resources->GetDrawImage().ImageExtent.width, + m_Resources->GetDrawImage().ImageExtent.height}; + auto &CaptureFrames = m_Resources->GetOffscreenCaptureFrames(); + std::vector ReadyCaptureFrames; + ReadyCaptureFrames.reserve(CaptureFrames.size()); + for (auto &CaptureFrame : CaptureFrames) { + if (!CaptureFrame.HasPendingReadback) { + continue; + } + + FrameData &SubmittedFrame = m_CommandContext->GetFrame( + CaptureFrame.SubmittedFrameNumber); + if (vkGetFenceStatus(m_Device->Device, SubmittedFrame.RenderFence) != + VK_SUCCESS) { + continue; + } + + ReadyCaptureFrames.push_back(&CaptureFrame); + } + + std::sort(ReadyCaptureFrames.begin(), ReadyCaptureFrames.end(), + [](const VulkanResourceManager::OffscreenCaptureFrame *Left, + const VulkanResourceManager::OffscreenCaptureFrame *Right) { + return Left->SubmittedFrameNumber < Right->SubmittedFrameNumber; + }); + + for (auto *CaptureFrame : ReadyCaptureFrames) { + PublishCompletedOffscreenFrame(*CaptureFrame, DrawExtent, FrameOutput); + } + + HeadlessRuntimeInstrumentation::RecordPendingOffscreenReadbacks( + CountPendingOffscreenReadbacks()); +} + +void VulkanDrawSubmissionSystem::DrawFrame(const FrameRequest &Request) { + CollectCompletedTransfers(); + + auto &CurrentFrame = + m_CommandContext->PrepareFrame(m_Device->Device, Request.FrameNumber); + auto &MeshFrame = m_Resources->GetMeshFrame(Request.FrameNumber); + auto &CaptureFrame = m_Resources->GetOffscreenCaptureFrame(Request.FrameNumber); + + CollectFrameStats(MeshFrame); + if (!m_HasPresentationSurface) { + const VkExtent2D DrawExtent = {m_Resources->GetDrawImage().ImageExtent.width, + m_Resources->GetDrawImage().ImageExtent.height}; + if (CaptureFrame.HasPendingReadback) { + // This slot shares the same frame fence index. PrepareFrame already waited + // for that fence, so the prior capture can be drained before reuse. + PublishCompletedOffscreenFrame(CaptureFrame, DrawExtent, Request.FrameOutput); + } + HeadlessRuntimeInstrumentation::RecordPendingOffscreenReadbacks( + CountPendingOffscreenReadbacks()); + PublishCompletedOffscreenFrames(Request.FrameOutput); + assert(!CaptureFrame.HasPendingReadback && + "Offscreen capture slot must be free before re-recording"); + } + + uint32_t SwapchainImageIndex = 0; + if (m_HasPresentationSurface) { + SwapchainImageIndex = m_Resources->GetSwapchain().AcquireNextImage( + m_Device->Device, CurrentFrame.SwapchainSemaphore); + } + + VulkanCommandList CommandList( + m_Device->Device, CurrentFrame.CommandPool, CurrentFrame.MainCommandBuffer, + RHIQueueType::Graphics, false); + VkCommandBuffer CommandBuffer = CommandList.GetCommandBuffer(); + + const VkExtent2D DrawExtent = {m_Resources->GetDrawImage().ImageExtent.width, + m_Resources->GetDrawImage().ImageExtent.height}; + m_FrameStats.DrawExtent = {DrawExtent.width, DrawExtent.height}; + MeshFrame.HasValidTimestamps = true; + MeshFrame.HasValidOcclusionData = false; + + CommandList.Begin(); + vkCmdResetQueryPool(CommandBuffer, MeshFrame.TimestampQueryPool, 0, + TimestampQueryCount); + + VkUtil::TransitionImage(CommandBuffer, m_Resources->GetDrawImage().Image, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); + vkCmdWriteTimestamp2(CommandBuffer, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + MeshFrame.TimestampQueryPool, 0); + vkCmdWriteTimestamp2(CommandBuffer, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + MeshFrame.TimestampQueryPool, 1); + + ClearDepthImage(CommandBuffer, Request.FrameNumber); + vkCmdWriteTimestamp2(CommandBuffer, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + MeshFrame.TimestampQueryPool, 2); + if (Request.ActiveScene != nullptr) { + DrawMeshes(CommandBuffer, *Request.ActiveScene, Request.FrameNumber, + Request.ViewMode); + } + vkCmdWriteTimestamp2(CommandBuffer, + VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, + MeshFrame.TimestampQueryPool, 3); + + if (Request.ActiveScene != nullptr && m_LightBillboardRenderer.IsInitialized()) { + m_LightBillboardRenderer.DrawLightBillboards( + CommandBuffer, DrawExtent, m_Resources->GetDrawImage().ImageView, + *Request.ActiveScene); + } + if (Request.ActiveScene != nullptr && m_GizmoRenderer.IsInitialized()) { + m_GizmoRenderer.DrawGizmoOverlay( + CommandBuffer, DrawExtent, m_Resources->GetDrawImage().ImageView, + *Request.ActiveScene); + } + + VkUtil::TransitionImage(CommandBuffer, m_Resources->GetDrawImage().Image, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + if (m_HasPresentationSurface) { + VkUtil::TransitionImage( + CommandBuffer, m_Resources->GetSwapchain().Images[SwapchainImageIndex], + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + VkUtil::CopyImageToImage(CommandBuffer, m_Resources->GetDrawImage().Image, + m_Resources->GetSwapchain().Images[SwapchainImageIndex], + DrawExtent, m_Resources->GetSwapchain().Extent); + VkUtil::TransitionImage( + CommandBuffer, m_Resources->GetSwapchain().Images[SwapchainImageIndex], + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + if (m_EnableImGui) { + m_ImGuiRenderer.RecordDrawData( + CommandBuffer, m_Resources->GetSwapchain().Extent, + m_Resources->GetSwapchain().ImageViews[SwapchainImageIndex]); + } + VkUtil::TransitionImage( + CommandBuffer, m_Resources->GetSwapchain().Images[SwapchainImageIndex], + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else { + RecordOffscreenCapture(CommandBuffer, CaptureFrame.ReadbackBuffer, DrawExtent); + } + CommandList.End(); + + std::array WaitInfos{}; + std::array, 2> WaitSemaphores{}; + uint32_t WaitCount = 0; + if (m_HasPresentationSurface) { + const uint32_t WaitIndex = WaitCount++; + WaitSemaphores[WaitIndex].emplace(m_Device->Device, + CurrentFrame.SwapchainSemaphore, false, + false); + WaitInfos[WaitIndex] = { + .Semaphore = &*WaitSemaphores[WaitIndex], + .Value = 0, + .Stage = RHICommandStage::ColorAttachmentOutput, + }; + } + if (m_LastGraphicsWaitValue > 0) { + const uint32_t WaitIndex = WaitCount++; + WaitSemaphores[WaitIndex].emplace(m_Device->Device, + m_TransferTimelineSemaphore, true, false); + WaitInfos[WaitIndex] = { + .Semaphore = &*WaitSemaphores[WaitIndex], + .Value = m_LastGraphicsWaitValue, + .Stage = RHICommandStage::All, + }; + } + + if (m_HasPresentationSurface) { + VulkanSemaphore RenderSemaphore(m_Device->Device, CurrentFrame.RenderSemaphore, + false, false); + VulkanFence RenderFence(m_Device->Device, CurrentFrame.RenderFence, false); + const RHIQueueSignalInfo RenderSignal{ + .Semaphore = &RenderSemaphore, + .Value = 0, + .Stage = RHICommandStage::All, + }; + VulkanQueue GraphicsQueue(m_Device->GraphicsQueue, RHIQueueType::Graphics); + GraphicsQueue.Submit(CommandList, + std::span(WaitInfos.data(), + WaitCount), + std::span(&RenderSignal, 1), + &RenderFence); + m_Resources->GetSwapchain().Present(m_Device->GraphicsQueue, + SwapchainImageIndex, + CurrentFrame.RenderSemaphore); + } else { + VulkanFence RenderFence(m_Device->Device, CurrentFrame.RenderFence, false); + VulkanQueue GraphicsQueue(m_Device->GraphicsQueue, RHIQueueType::Graphics); + GraphicsQueue.Submit(CommandList, + std::span(WaitInfos.data(), + WaitCount), + {}, &RenderFence); + CaptureFrame.HasPendingReadback = true; + CaptureFrame.SubmittedFrameNumber = Request.FrameNumber; + CaptureFrame.SubmittedUser = Request.ViewportFrameUser; + HeadlessRuntimeInstrumentation::RecordOffscreenReadbackSubmitted( + Request.FrameNumber, Request.ViewportFrameUser, + CountPendingOffscreenReadbacks()); + } +} + +size_t VulkanDrawSubmissionSystem::CountPendingOffscreenReadbacks() const { + size_t PendingReadbacks = 0; + for (const auto &CaptureFrame : m_Resources->GetOffscreenCaptureFrames()) { + if (CaptureFrame.HasPendingReadback) { + ++PendingReadbacks; + } + } + return PendingReadbacks; +} + +std::optional VulkanDrawSubmissionSystem::ConsumeCapturedFrame() { + if (m_CapturedFrames.empty()) { + return std::nullopt; + } + + CapturedFrame Result = std::move(m_CapturedFrames.front()); + m_CapturedFrames.pop_front(); + return Result; +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.h new file mode 100644 index 00000000..e7df6220 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.h @@ -0,0 +1,132 @@ +#pragma once + +#include "Renderer/RenderScene.h" +#include "Renderer/RenderSurface.h" +#include "AxiomRHI/Vulkan/VulkanCommandContext.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanDevice.h" +#include "AxiomRHI/Vulkan/VulkanGizmoRenderer.h" +#include "AxiomRHI/Vulkan/ImGui/VulkanImGuiRenderer.h" +#include "AxiomRHI/Vulkan/VulkanLightBillboardRenderer.h" +#include "AxiomRHI/Vulkan/VulkanMaterialResources.h" +#include "AxiomRHI/Vulkan/VulkanOcclusionCulling.h" +#include "AxiomRHI/Vulkan/VulkanPipelineLibrary.h" +#include "AxiomRHI/Vulkan/VulkanResourceManager.h" + +#include +#include +#include +#include + +namespace Axiom { +class VulkanContext; + +class VulkanDrawSubmissionSystem { +public: + struct CreateInfo { + IRenderSurface *Surface{nullptr}; + VulkanContext &Context; + VulkanDevice &Device; + VulkanCommandContext &CommandContext; + VulkanResourceManager &Resources; + VulkanPipelineLibrary &Pipelines; + VulkanMaterialResources &MaterialResources; + VulkanOcclusionCulling &OcclusionCulling; + bool EnableImGui{false}; + bool HasPresentationSurface{false}; + std::function + RecordPreparedScenePasses; + std::function DestroyResourceManagerHDRTexture; + }; + + struct FrameRequest { + uint64_t FrameNumber{0}; + RenderScene *ActiveScene{nullptr}; + RendererViewMode ViewMode{RendererViewMode::Lit}; + SessionUserId ViewportFrameUser{}; + IViewportFrameOutput *FrameOutput{nullptr}; + }; + + void Init(const CreateInfo &CreateInfo); + void Shutdown(); + void SetRecordPreparedScenePasses( + std::function RecordPreparedScenePasses); + + void BeginFrame(bool StopRendering); + void RenderImGui(bool StopRendering, RendererViewMode &ViewMode); + void DrawFrame(const FrameRequest &Request); + void SubmitTransferUpload(std::function &&RecordUpload, + std::function &&Cleanup); + void BuildHzb(VkCommandBuffer CommandBuffer, MeshFrameResources &Frame); + + bool IsInitialized() const { return m_IsInitialized; } + bool IsImGuiEnabled() const { return m_EnableImGui; } + RendererFrameStats &AccessFrameStats() { return m_FrameStats; } + const RendererFrameStats &GetFrameStats() const { return m_FrameStats; } + std::optional ConsumeCapturedFrame(); + +private: + struct PendingTransfer { + uint64_t SignalValue{0}; + VkCommandBuffer CommandBuffer{VK_NULL_HANDLE}; + std::function Cleanup; + }; + + void InitSpecializedRenderers(); + void InitTransferQueue(); + void ShutdownTransferQueue(); + void CollectCompletedTransfers(); + void CollectFrameStats(MeshFrameResources &Frame); + void ClearDepthImage(VkCommandBuffer CommandBuffer, uint64_t FrameNumber); + void DrawMeshes(VkCommandBuffer CommandBuffer, RenderScene &Scene, + uint64_t FrameNumber, RendererViewMode ViewMode); + void RecordOffscreenCapture(VkCommandBuffer CommandBuffer, + const AllocatedBuffer &ReadbackBuffer, + VkExtent2D DrawExtent); + void PublishCompletedOffscreenFrame( + VulkanResourceManager::OffscreenCaptureFrame &CaptureFrame, + VkExtent2D DrawExtent, IViewportFrameOutput *FrameOutput); + void PublishCompletedOffscreenFrames(IViewportFrameOutput *FrameOutput); + size_t CountPendingOffscreenReadbacks() const; + static float HalfToFloat(uint16_t Value); + static uint8_t LinearToByte(float Value); + std::optional + ConvertCapturedFrameToRgba8(const AllocatedBuffer &ReadbackBuffer, + uint64_t FrameNumber, VkExtent2D DrawExtent); + +private: + IRenderSurface *m_Surface{nullptr}; + VulkanContext *m_Context{nullptr}; + VulkanDevice *m_Device{nullptr}; + VulkanCommandContext *m_CommandContext{nullptr}; + VulkanResourceManager *m_Resources{nullptr}; + VulkanPipelineLibrary *m_Pipelines{nullptr}; + VulkanMaterialResources *m_MaterialResources{nullptr}; + VulkanOcclusionCulling *m_OcclusionCulling{nullptr}; + bool m_EnableImGui{false}; + bool m_HasPresentationSurface{false}; + bool m_IsInitialized{false}; + std::function + m_RecordPreparedScenePasses; + + VulkanGizmoRenderer m_GizmoRenderer; + VulkanImGuiRenderer m_ImGuiRenderer; + VulkanLightBillboardRenderer m_LightBillboardRenderer; + MaterialHandle m_LightBillboardMaterialHandle{}; + + RendererFrameStats m_FrameStats{}; + std::deque m_CapturedFrames; + float m_TimestampPeriod{0.0f}; + + VkQueue m_TransferQueue{VK_NULL_HANDLE}; + uint32_t m_TransferQueueFamily{0}; + VkCommandPool m_TransferCommandPool{VK_NULL_HANDLE}; + VkSemaphore m_TransferTimelineSemaphore{VK_NULL_HANDLE}; + uint64_t m_NextTransferSignalValue{0}; + uint64_t m_LastGraphicsWaitValue{0}; + std::vector m_PendingTransfers; + DeletionQueue m_RendererDeletionQueue; +}; +} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanGizmoRenderer.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.cpp similarity index 98% rename from Axiom/Renderer/Vulkan/VulkanGizmoRenderer.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.cpp index bda2793c..d272041f 100644 --- a/Axiom/Renderer/Vulkan/VulkanGizmoRenderer.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.cpp @@ -1,9 +1,9 @@ -#include "Renderer/Vulkan/VulkanGizmoRenderer.h" +#include "AxiomRHI/Vulkan/VulkanGizmoRenderer.h" #include "Core/Log.h" #include "Renderer/Camera.h" -#include "Renderer/Vulkan/VulkanInitializers.h" -#include "Renderer/Vulkan/VulkanPipeline.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanPipeline.h" #include #include diff --git a/Axiom/Renderer/Vulkan/VulkanGizmoRenderer.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.h similarity index 88% rename from Axiom/Renderer/Vulkan/VulkanGizmoRenderer.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.h index 38955cdb..af09ea2f 100644 --- a/Axiom/Renderer/Vulkan/VulkanGizmoRenderer.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanGizmoRenderer.h @@ -1,8 +1,8 @@ #pragma once #include "Renderer/RenderScene.h" -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" namespace Axiom { diff --git a/Axiom/Renderer/Vulkan/VulkanImage.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanImage.cpp similarity index 97% rename from Axiom/Renderer/Vulkan/VulkanImage.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanImage.cpp index d9d526f6..453126f9 100644 --- a/Axiom/Renderer/Vulkan/VulkanImage.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanImage.cpp @@ -1,8 +1,8 @@ #include -#include "Renderer/Vulkan/VulkanImage.h" +#include "AxiomRHI/Vulkan/VulkanImage.h" -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" namespace VkUtil { namespace { diff --git a/Axiom/Renderer/Vulkan/VulkanImage.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanImage.h similarity index 100% rename from Axiom/Renderer/Vulkan/VulkanImage.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanImage.h diff --git a/Axiom/Renderer/Vulkan/VulkanInitializers.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanInitializers.cpp similarity index 99% rename from Axiom/Renderer/Vulkan/VulkanInitializers.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanInitializers.cpp index 85562675..acd0854b 100644 --- a/Axiom/Renderer/Vulkan/VulkanInitializers.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanInitializers.cpp @@ -1,4 +1,4 @@ -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" //> init_cmd VkCommandPoolCreateInfo diff --git a/Axiom/Renderer/Vulkan/VulkanInitializers.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanInitializers.h similarity index 100% rename from Axiom/Renderer/Vulkan/VulkanInitializers.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanInitializers.h diff --git a/Axiom/Renderer/Vulkan/VulkanLightBillboardRenderer.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.cpp similarity index 98% rename from Axiom/Renderer/Vulkan/VulkanLightBillboardRenderer.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.cpp index 3d475172..072e8928 100644 --- a/Axiom/Renderer/Vulkan/VulkanLightBillboardRenderer.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.cpp @@ -1,9 +1,9 @@ -#include "Renderer/Vulkan/VulkanLightBillboardRenderer.h" +#include "AxiomRHI/Vulkan/VulkanLightBillboardRenderer.h" #include "Core/Log.h" #include "Renderer/Camera.h" -#include "Renderer/Vulkan/VulkanInitializers.h" -#include "Renderer/Vulkan/VulkanPipeline.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanPipeline.h" #include "Session/MeshPicking.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanLightBillboardRenderer.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.h similarity index 87% rename from Axiom/Renderer/Vulkan/VulkanLightBillboardRenderer.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.h index 1569e738..1f547165 100644 --- a/Axiom/Renderer/Vulkan/VulkanLightBillboardRenderer.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanLightBillboardRenderer.h @@ -1,9 +1,9 @@ #pragma once #include "Renderer/RenderScene.h" -#include "Renderer/Vulkan/VulkanDeletionQueue.h" -#include "Renderer/Vulkan/VulkanDescriptors.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanDeletionQueue.h" +#include "AxiomRHI/Vulkan/VulkanDescriptors.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" namespace Axiom { diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.cpp new file mode 100644 index 00000000..49ab93bb --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.cpp @@ -0,0 +1,181 @@ +#include "AxiomRHI/Vulkan/VulkanMaterialResources.h" + +#include "AxiomRHI/Vulkan/VulkanInitializers.h" + +#include +#include + +namespace Axiom { +void VulkanMaterialResources::Init(const CreateInfo &CreateInfo) { + m_Device = CreateInfo.Device; + m_DescriptorAllocator = CreateInfo.DescriptorAllocator; + m_MaterialDescriptorSetLayout = CreateInfo.MaterialDescriptorSetLayout; + m_TextureSampler = CreateInfo.TextureSampler; + m_CreateTextureImage = CreateInfo.CreateTextureImage; +} + +void VulkanMaterialResources::Shutdown() { + m_MaterialsByHandle.clear(); + m_MaterialImageViews.clear(); + m_MaterialDescriptorSets.clear(); + m_FallbackTexture = {}; + m_NextMaterialHandleValue = 1; +#if !defined(NDEBUG) + m_DebugGraphicsMaterialDescriptorUpdates = 0; +#endif +} + +void VulkanMaterialResources::InitFallbackTexture() { + TextureSourceData CheckerTexture{}; + constexpr uint32_t TextureSize = 64; + constexpr uint32_t CellSize = 8; + constexpr std::array Purple = {0xA0, 0x20, 0xF0, 0xFF}; + constexpr std::array Black = {0x00, 0x00, 0x00, 0xFF}; + + CheckerTexture.Width = TextureSize; + CheckerTexture.Height = TextureSize; + CheckerTexture.Pixels.resize(TextureSize * TextureSize * 4); + + for (uint32_t Y = 0; Y < TextureSize; ++Y) { + for (uint32_t X = 0; X < TextureSize; ++X) { + const bool UsePurple = ((X / CellSize) + (Y / CellSize)) % 2 == 0; + const auto &Color = UsePurple ? Purple : Black; + const size_t PixelIndex = + (static_cast(Y) * TextureSize + X) * 4; + CheckerTexture.Pixels[PixelIndex + 0] = Color[0]; + CheckerTexture.Pixels[PixelIndex + 1] = Color[1]; + CheckerTexture.Pixels[PixelIndex + 2] = Color[2]; + CheckerTexture.Pixels[PixelIndex + 3] = Color[3]; + } + } + + m_FallbackTexture = m_CreateTextureImage(CheckerTexture); +} + +MaterialHandle +VulkanMaterialResources::CreateMaterialHandle(const MaterialInstance &Material) { + MaterialHandle Handle{m_NextMaterialHandleValue++}; + auto [It, Inserted] = m_MaterialsByHandle.emplace( + Handle, std::make_unique(Material)); + assert(Inserted && "Allocated duplicate material handle"); + (void)It; + return Handle; +} + +void VulkanMaterialResources::UpdateMaterialHandle( + MaterialHandle Handle, const MaterialInstance &Material) { + if (!Handle.IsValid()) { + return; + } + + auto It = m_MaterialsByHandle.find(Handle); + if (It == m_MaterialsByHandle.end()) { + auto [InsertedIt, Inserted] = m_MaterialsByHandle.emplace( + Handle, std::make_unique(Material)); + assert(Inserted && "Failed to insert material for valid handle"); + (void)InsertedIt; + if (Handle.Value >= m_NextMaterialHandleValue) { + m_NextMaterialHandleValue = Handle.Value + 1; + } + return; + } + + *It->second = Material; +} + +const MaterialInstance * +VulkanMaterialResources::ResolveMaterialHandle(MaterialHandle Handle) const { + if (!Handle.IsValid()) { + return nullptr; + } + + const auto It = m_MaterialsByHandle.find(Handle); + return It != m_MaterialsByHandle.end() ? It->second.get() : nullptr; +} + +VkImageView +VulkanMaterialResources::ResolveMaterialTextureView(const MaterialInstance *Material) { + if (!Material || !Material->BaseColorTexture || + !Material->BaseColorTexture->IsValid()) { + return m_FallbackTexture.ImageView; + } + + auto It = m_MaterialImageViews.find(Material); + if (It != m_MaterialImageViews.end()) { + return It->second; + } + + const AllocatedImage TextureImage = + m_CreateTextureImage(*Material->BaseColorTexture); + m_MaterialImageViews.emplace(Material, TextureImage.ImageView); + return TextureImage.ImageView; +} + +VkDescriptorSet +VulkanMaterialResources::ResolveMaterialDescriptorSet(const MaterialInstance *Material) { + const MaterialInstance *MaterialKey = Material; + const uint64_t MaterialRevision = Material ? Material->Revision : 0; + const TextureSourceData *TextureSource = + (Material && Material->BaseColorTexture && + Material->BaseColorTexture->IsValid()) + ? Material->BaseColorTexture.get() + : nullptr; + + auto It = m_MaterialDescriptorSets.find(MaterialKey); + if (It != m_MaterialDescriptorSets.end() && + It->second.TextureSource != TextureSource) { + m_MaterialImageViews.erase(MaterialKey); + } + + const VkImageView TextureView = ResolveMaterialTextureView(Material); + if (It != m_MaterialDescriptorSets.end() && + It->second.Revision == MaterialRevision && + It->second.TextureView == TextureView) { + return It->second.DescriptorSet; + } + + MaterialDescriptorCacheEntry *Entry = nullptr; + if (It == m_MaterialDescriptorSets.end()) { + MaterialDescriptorCacheEntry NewEntry{}; + NewEntry.DescriptorSet = m_DescriptorAllocator->Allocate( + m_Device, m_MaterialDescriptorSetLayout); + auto [InsertedIt, Inserted] = + m_MaterialDescriptorSets.emplace(MaterialKey, NewEntry); + (void)Inserted; + Entry = &InsertedIt->second; + } else { + Entry = &It->second; + } + + VkDescriptorImageInfo GraphicsTextureImageInfo{}; + GraphicsTextureImageInfo.imageView = TextureView; + GraphicsTextureImageInfo.imageLayout = + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + VkDescriptorImageInfo GraphicsTextureSamplerInfo{}; + GraphicsTextureSamplerInfo.sampler = m_TextureSampler; + + const std::array GraphicsMaterialWrites = { + VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, + Entry->DescriptorSet, &GraphicsTextureImageInfo, + 1), + VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_SAMPLER, + Entry->DescriptorSet, + &GraphicsTextureSamplerInfo, 2)}; + vkUpdateDescriptorSets(m_Device, + static_cast(GraphicsMaterialWrites.size()), + GraphicsMaterialWrites.data(), 0, VK_NULL_HANDLE); + Entry->TextureView = TextureView; + Entry->TextureSource = TextureSource; + Entry->Revision = MaterialRevision; +#if !defined(NDEBUG) + ++m_DebugGraphicsMaterialDescriptorUpdates; +#endif + return Entry->DescriptorSet; +} + +#if !defined(NDEBUG) +void VulkanMaterialResources::ResetDebugCounters() { + m_DebugGraphicsMaterialDescriptorUpdates = 0; +} +#endif +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.h new file mode 100644 index 00000000..b70837f7 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanMaterialResources.h @@ -0,0 +1,64 @@ +#pragma once + +#include "Renderer/Material.h" +#include "AxiomRHI/Vulkan/VulkanDescriptors.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" + +#include +#include +#include + +namespace Axiom { +class VulkanMaterialResources { +public: + struct CreateInfo { + VkDevice Device{VK_NULL_HANDLE}; + DescriptorAllocator *DescriptorAllocator{nullptr}; + VkDescriptorSetLayout MaterialDescriptorSetLayout{VK_NULL_HANDLE}; + VkSampler TextureSampler{VK_NULL_HANDLE}; + std::function CreateTextureImage; + }; + + void Init(const CreateInfo &CreateInfo); + void Shutdown(); + + void InitFallbackTexture(); + MaterialHandle CreateMaterialHandle(const MaterialInstance &Material); + void UpdateMaterialHandle(MaterialHandle Handle, const MaterialInstance &Material); + const MaterialInstance *ResolveMaterialHandle(MaterialHandle Handle) const; + VkImageView ResolveMaterialTextureView(const MaterialInstance *Material); + VkDescriptorSet ResolveMaterialDescriptorSet(const MaterialInstance *Material); + VkImageView GetFallbackTextureView() const { return m_FallbackTexture.ImageView; } +#if !defined(NDEBUG) + void ResetDebugCounters(); + uint32_t GetDebugGraphicsMaterialDescriptorUpdates() const { + return m_DebugGraphicsMaterialDescriptorUpdates; + } +#endif + +private: + struct MaterialDescriptorCacheEntry { + VkDescriptorSet DescriptorSet{VK_NULL_HANDLE}; + VkImageView TextureView{VK_NULL_HANDLE}; + const TextureSourceData *TextureSource{nullptr}; + uint64_t Revision{0}; + }; + + VkDevice m_Device{VK_NULL_HANDLE}; + DescriptorAllocator *m_DescriptorAllocator{nullptr}; + VkDescriptorSetLayout m_MaterialDescriptorSetLayout{VK_NULL_HANDLE}; + VkSampler m_TextureSampler{VK_NULL_HANDLE}; + std::function m_CreateTextureImage; + AllocatedImage m_FallbackTexture; + std::unordered_map, + MaterialHandleHash> + m_MaterialsByHandle; + std::unordered_map m_MaterialImageViews; + std::unordered_map + m_MaterialDescriptorSets; + uint32_t m_NextMaterialHandleValue{1}; +#if !defined(NDEBUG) + uint32_t m_DebugGraphicsMaterialDescriptorUpdates{0}; +#endif +}; +} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanMesh.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.cpp similarity index 80% rename from Axiom/Renderer/Vulkan/VulkanMesh.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.cpp index d8f39b24..bcc4ff51 100644 --- a/Axiom/Renderer/Vulkan/VulkanMesh.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.cpp @@ -1,25 +1,21 @@ -#include "Renderer/Vulkan/VulkanMesh.h" +#include "AxiomRHI/Vulkan/VulkanMesh.h" -#include "Renderer/Vulkan/VulkanRendererBackend.h" -#include "Renderer/Vulkan/VulkanBuffer.h" -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanBuffer.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" #include #include namespace Axiom { -VulkanMesh::VulkanMesh(MeshData SourceData, VmaAllocator InAllocator) - : Allocator(InAllocator), CpuData(std::move(SourceData)), - BoundsMin(CpuData.BoundsMin), BoundsMax(CpuData.BoundsMax) {} +VulkanMesh::VulkanMesh(VmaAllocator InAllocator) : Allocator(InAllocator) {} VulkanMesh::~VulkanMesh() { - if (auto *Backend = VulkanRendererBackend::TryGet(); - Backend != nullptr && Backend->IsInitialized()) { + if (std::shared_ptr Queue = ResourceQueue.lock()) { AllocatedBuffer VertexBufferCopy = VertexBuffer; AllocatedBuffer IndexBufferCopy = IndexBuffer; AllocatedBuffer ProjectedVertexBufferCopy = ProjectedVertexBuffer; VmaAllocator AllocatorCopy = Allocator; - Backend->EnqueueDeferredDestroy( + Queue->Enqueue( [AllocatorCopy, VertexBufferCopy, IndexBufferCopy, ProjectedVertexBufferCopy]() mutable { VkBufferUtil::DestroyBuffer(AllocatorCopy, VertexBufferCopy); @@ -42,8 +38,13 @@ VulkanMesh::Create(const MeshData &MeshSource, VmaAllocator Allocator, VkDevice Device, VkQueue GraphicsQueue, VkCommandPool CommandPool, ::DescriptorAllocator &DescriptorAllocator, + const std::shared_ptr &ResourceQueue, + const MeshCreateOptions &Options, VkDescriptorSetLayout MeshDescriptorLayout) { - auto MeshRef = std::make_shared(MeshSource, Allocator); + auto MeshRef = std::make_shared(Allocator); + MeshRef->ResourceQueue = ResourceQueue; + MeshRef->BoundsMin = MeshSource.BoundsMin; + MeshRef->BoundsMax = MeshSource.BoundsMax; MeshRef->VertexCount = static_cast(MeshSource.Vertices.size()); MeshRef->IndexCount = static_cast(MeshSource.Indices.size()); MeshRef->TriangleCount = MeshRef->IndexCount / 3; @@ -85,6 +86,18 @@ VulkanMesh::Create(const MeshData &MeshSource, VmaAllocator Allocator, vkUpdateDescriptorSets(Device, static_cast(Writes.size()), Writes.data(), 0, VK_NULL_HANDLE); + if (Options.KeepCpuData) { + MeshRef->CpuData = MeshSource; + } + return MeshRef; } + +size_t VulkanMesh::RetainedCpuBytes() const { + if (!CpuData.has_value()) { + return 0; + } + return CpuData->Vertices.size() * sizeof(MeshVertex) + + CpuData->Indices.size() * sizeof(uint32_t); +} } // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanMesh.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.h similarity index 59% rename from Axiom/Renderer/Vulkan/VulkanMesh.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.h index 3f209deb..f87e49da 100644 --- a/Axiom/Renderer/Vulkan/VulkanMesh.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanMesh.h @@ -1,26 +1,30 @@ #pragma once #include "Renderer/Mesh.h" -#include "Renderer/Vulkan/VulkanDescriptors.h" -#include "Renderer/Vulkan/VulkanRendererTypes.h" -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/GPUResourceQueue.h" +#include "AxiomRHI/Vulkan/VulkanDescriptors.h" +#include "AxiomRHI/Vulkan/VulkanRendererTypes.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" #include +#include namespace Axiom { class VulkanMesh final : public Mesh { public: - explicit VulkanMesh(MeshData SourceData, VmaAllocator InAllocator); + explicit VulkanMesh(VmaAllocator InAllocator); ~VulkanMesh() override; static std::shared_ptr Create(const MeshData &MeshSource, VmaAllocator Allocator, VkDevice Device, VkQueue GraphicsQueue, VkCommandPool CommandPool, ::DescriptorAllocator &DescriptorAllocator, + const std::shared_ptr &ResourceQueue, + const MeshCreateOptions &Options, VkDescriptorSetLayout MeshDescriptorLayout); VmaAllocator Allocator{nullptr}; - MeshData CpuData; + std::optional CpuData; AllocatedBuffer VertexBuffer; AllocatedBuffer IndexBuffer; AllocatedBuffer ProjectedVertexBuffer; @@ -30,5 +34,8 @@ class VulkanMesh final : public Mesh { uint32_t VertexCount{0}; uint32_t IndexCount{0}; uint32_t TriangleCount{0}; + std::weak_ptr ResourceQueue; + bool KeepsCpuData() const { return CpuData.has_value(); } + size_t RetainedCpuBytes() const; }; } // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanOcclusionCulling.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.cpp similarity index 99% rename from Axiom/Renderer/Vulkan/VulkanOcclusionCulling.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.cpp index 9d22419f..72665665 100644 --- a/Axiom/Renderer/Vulkan/VulkanOcclusionCulling.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.cpp @@ -1,4 +1,4 @@ -#include "Renderer/Vulkan/VulkanOcclusionCulling.h" +#include "AxiomRHI/Vulkan/VulkanOcclusionCulling.h" #include #include diff --git a/Axiom/Renderer/Vulkan/VulkanOcclusionCulling.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.h similarity index 93% rename from Axiom/Renderer/Vulkan/VulkanOcclusionCulling.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.h index 7b4a9dba..0a6529a7 100644 --- a/Axiom/Renderer/Vulkan/VulkanOcclusionCulling.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanOcclusionCulling.h @@ -1,7 +1,7 @@ #pragma once -#include "Renderer/Vulkan/VulkanCommandContext.h" -#include "Renderer/Vulkan/VulkanRendererTypes.h" +#include "AxiomRHI/Vulkan/VulkanCommandContext.h" +#include "AxiomRHI/Vulkan/VulkanRendererTypes.h" #include #include diff --git a/Axiom/Renderer/Vulkan/VulkanPipeline.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.cpp similarity index 93% rename from Axiom/Renderer/Vulkan/VulkanPipeline.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.cpp index 587f4dfe..89b9ce6c 100644 --- a/Axiom/Renderer/Vulkan/VulkanPipeline.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.cpp @@ -1,5 +1,5 @@ -#include "Renderer/Vulkan/VulkanPipeline.h" -#include "Renderer/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanPipeline.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanPipeline.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.h similarity index 78% rename from Axiom/Renderer/Vulkan/VulkanPipeline.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.h index 35b782cd..05c58c73 100644 --- a/Axiom/Renderer/Vulkan/VulkanPipeline.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipeline.h @@ -1,6 +1,6 @@ #pragma once -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" namespace VkUtil { bool LoadShaderModule(const char *FilePath, VkDevice Device, diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.cpp new file mode 100644 index 00000000..88c15fde --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.cpp @@ -0,0 +1,472 @@ +#include "AxiomRHI/Vulkan/VulkanPipelineLibrary.h" + +#include "Core/Log.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanMesh.h" +#include "AxiomRHI/Vulkan/VulkanPipeline.h" +#include "AxiomRHI/Vulkan/VulkanRendererTypes.h" + +#include +#include +#include + +namespace Axiom { +#ifndef AXIOM_CONTENT_DIR +#define AXIOM_CONTENT_DIR "Content" +#endif + +void VulkanPipelineLibrary::Init(const CreateInfo &CreateInfo) { + m_Device = CreateInfo.Device; + m_DrawImageFormat = CreateInfo.DrawImageFormat; + m_RasterDepthFormat = CreateInfo.RasterDepthFormat; + m_DrawImageDescriptorLayout = CreateInfo.DrawImageDescriptorLayout; + m_HzbReduceDescriptorLayout = CreateInfo.HzbReduceDescriptorLayout; + m_MeshGraphicsFrameDescriptorLayout = + CreateInfo.MeshGraphicsFrameDescriptorLayout; + m_MeshGraphicsMaterialDescriptorLayout = + CreateInfo.MeshGraphicsMaterialDescriptorLayout; + m_MeshComputeFrameDescriptorLayout = + CreateInfo.MeshComputeFrameDescriptorLayout; + m_MeshDescriptorLayout = CreateInfo.MeshDescriptorLayout; + m_HDRSkyboxDescriptorLayout = CreateInfo.HDRSkyboxDescriptorLayout; + + InitBackgroundPipelines(); + InitMeshPipelines(); +} + +void VulkanPipelineLibrary::Shutdown() { + if (m_HzbReducePipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_HzbReducePipelineLayout, VK_NULL_HANDLE); + m_HzbReducePipelineLayout = VK_NULL_HANDLE; + } + if (m_HzbReducePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_HzbReducePipeline, VK_NULL_HANDLE); + m_HzbReducePipeline = VK_NULL_HANDLE; + } + if (m_MeshProjectPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_MeshProjectPipelineLayout, VK_NULL_HANDLE); + m_MeshProjectPipelineLayout = VK_NULL_HANDLE; + } + if (m_MeshProjectPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_MeshProjectPipeline, VK_NULL_HANDLE); + m_MeshProjectPipeline = VK_NULL_HANDLE; + } + if (m_MeshPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_MeshPipelineLayout, VK_NULL_HANDLE); + m_MeshPipelineLayout = VK_NULL_HANDLE; + } + if (m_MeshPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_MeshPipeline, VK_NULL_HANDLE); + m_MeshPipeline = VK_NULL_HANDLE; + } + if (m_MeshDepthPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_MeshDepthPipelineLayout, VK_NULL_HANDLE); + m_MeshDepthPipelineLayout = VK_NULL_HANDLE; + } + if (m_MeshDepthPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_MeshDepthPipeline, VK_NULL_HANDLE); + m_MeshDepthPipeline = VK_NULL_HANDLE; + } + if (m_MeshWireframePipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_MeshWireframePipeline, VK_NULL_HANDLE); + m_MeshWireframePipeline = VK_NULL_HANDLE; + } + if (m_MeshGraphicsAlphaBlendPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_MeshGraphicsAlphaBlendPipeline, VK_NULL_HANDLE); + m_MeshGraphicsAlphaBlendPipeline = VK_NULL_HANDLE; + } + if (m_MeshGraphicsPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_MeshGraphicsPipelineLayout, + VK_NULL_HANDLE); + m_MeshGraphicsPipelineLayout = VK_NULL_HANDLE; + } + if (m_MeshGraphicsPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_MeshGraphicsPipeline, VK_NULL_HANDLE); + m_MeshGraphicsPipeline = VK_NULL_HANDLE; + } + if (m_HDRSkyboxPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_HDRSkyboxPipeline, VK_NULL_HANDLE); + m_HDRSkyboxPipeline = VK_NULL_HANDLE; + } + if (m_HDRSkyboxPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_HDRSkyboxPipelineLayout, + VK_NULL_HANDLE); + m_HDRSkyboxPipelineLayout = VK_NULL_HANDLE; + } + if (m_GradientPipeline != VK_NULL_HANDLE) { + vkDestroyPipeline(m_Device, m_GradientPipeline, VK_NULL_HANDLE); + m_GradientPipeline = VK_NULL_HANDLE; + } + if (m_GradientPipelineLayout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(m_Device, m_GradientPipelineLayout, VK_NULL_HANDLE); + m_GradientPipelineLayout = VK_NULL_HANDLE; + } +} + +void VulkanPipelineLibrary::InitBackgroundPipelines() { + VkPipelineLayoutCreateInfo ComputeLayout = VkInit::PipelineLayoutCreateInfo(); + ComputeLayout.pSetLayouts = &m_DrawImageDescriptorLayout; + ComputeLayout.setLayoutCount = 1; + + VkPushConstantRange PushConstant{}; + PushConstant.size = sizeof(ComputePushConstants); + PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + ComputeLayout.pPushConstantRanges = &PushConstant; + ComputeLayout.pushConstantRangeCount = 1; + + VK_CHECK(vkCreatePipelineLayout(m_Device, &ComputeLayout, VK_NULL_HANDLE, + &m_GradientPipelineLayout)); + + VkShaderModule ComputeDrawShader; + const std::string ShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/gradient_color.comp.spv"; + if (!VkUtil::LoadShaderModule(ShaderPath.c_str(), m_Device, + &ComputeDrawShader)) { + A_ERROR("Error when loading the compute shader: {0}", ShaderPath); + Axiom::Log::Flush(); + abort(); + } + + const VkPipelineShaderStageCreateInfo StageInfo = + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, + ComputeDrawShader); + const VkComputePipelineCreateInfo ComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = StageInfo, + .layout = m_GradientPipelineLayout}; + VK_CHECK(vkCreateComputePipelines(m_Device, VK_NULL_HANDLE, 1, + &ComputePipelineCreateInfo, VK_NULL_HANDLE, + &m_GradientPipeline)); + vkDestroyShaderModule(m_Device, ComputeDrawShader, VK_NULL_HANDLE); + + const std::array HDRSetLayouts = { + m_DrawImageDescriptorLayout, m_HDRSkyboxDescriptorLayout}; + VkPipelineLayoutCreateInfo HDRLayout = VkInit::PipelineLayoutCreateInfo(); + HDRLayout.pSetLayouts = HDRSetLayouts.data(); + HDRLayout.setLayoutCount = static_cast(HDRSetLayouts.size()); + + VkPushConstantRange HDRPushConstant{}; + HDRPushConstant.size = sizeof(glm::mat4); + HDRPushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + HDRLayout.pPushConstantRanges = &HDRPushConstant; + HDRLayout.pushConstantRangeCount = 1; + + VK_CHECK(vkCreatePipelineLayout(m_Device, &HDRLayout, VK_NULL_HANDLE, + &m_HDRSkyboxPipelineLayout)); + + VkShaderModule HDRShader; + const std::string HDRShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/skybox_hdr.comp.spv"; + if (!VkUtil::LoadShaderModule(HDRShaderPath.c_str(), m_Device, &HDRShader)) { + A_ERROR("Error when loading the HDR skybox compute shader: {0}", + HDRShaderPath); + Axiom::Log::Flush(); + abort(); + } + + const VkPipelineShaderStageCreateInfo HDRStage = + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, + HDRShader); + const VkComputePipelineCreateInfo HDRPipelineInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = HDRStage, + .layout = m_HDRSkyboxPipelineLayout}; + VK_CHECK(vkCreateComputePipelines(m_Device, VK_NULL_HANDLE, 1, + &HDRPipelineInfo, VK_NULL_HANDLE, + &m_HDRSkyboxPipeline)); + vkDestroyShaderModule(m_Device, HDRShader, VK_NULL_HANDLE); +} + +void VulkanPipelineLibrary::InitMeshPipelines() { + const std::array ComputeLayouts = { + m_MeshComputeFrameDescriptorLayout, m_MeshDescriptorLayout}; + + { + VkPipelineLayoutCreateInfo LayoutInfo = VkInit::PipelineLayoutCreateInfo(); + LayoutInfo.pSetLayouts = &m_HzbReduceDescriptorLayout; + LayoutInfo.setLayoutCount = 1; + + VkPushConstantRange PushConstant{}; + PushConstant.size = sizeof(HzbReducePushConstants); + PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + LayoutInfo.pPushConstantRanges = &PushConstant; + LayoutInfo.pushConstantRangeCount = 1; + + VK_CHECK(vkCreatePipelineLayout(m_Device, &LayoutInfo, VK_NULL_HANDLE, + &m_HzbReducePipelineLayout)); + } + + { + VkPipelineLayoutCreateInfo LayoutInfo = VkInit::PipelineLayoutCreateInfo(); + LayoutInfo.pSetLayouts = ComputeLayouts.data(); + LayoutInfo.setLayoutCount = static_cast(ComputeLayouts.size()); + + VkPushConstantRange PushConstant{}; + PushConstant.size = sizeof(MeshProjectPushConstants); + PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + LayoutInfo.pPushConstantRanges = &PushConstant; + LayoutInfo.pushConstantRangeCount = 1; + VK_CHECK(vkCreatePipelineLayout(m_Device, &LayoutInfo, VK_NULL_HANDLE, + &m_MeshProjectPipelineLayout)); + } + + { + VkPipelineLayoutCreateInfo LayoutInfo = VkInit::PipelineLayoutCreateInfo(); + LayoutInfo.pSetLayouts = ComputeLayouts.data(); + LayoutInfo.setLayoutCount = static_cast(ComputeLayouts.size()); + + VkPushConstantRange PushConstant{}; + PushConstant.size = sizeof(MeshRasterPushConstants); + PushConstant.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + LayoutInfo.pPushConstantRanges = &PushConstant; + LayoutInfo.pushConstantRangeCount = 1; + VK_CHECK(vkCreatePipelineLayout(m_Device, &LayoutInfo, VK_NULL_HANDLE, + &m_MeshPipelineLayout)); + } + + VkPushConstantRange GraphicsPushConstant{}; + GraphicsPushConstant.size = sizeof(MeshGraphicsPushConstants); + GraphicsPushConstant.stageFlags = + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT; + + const std::array GraphicsSetLayouts = { + m_MeshGraphicsFrameDescriptorLayout, + m_MeshGraphicsMaterialDescriptorLayout}; + VkPipelineLayoutCreateInfo GraphicsLayout = VkInit::PipelineLayoutCreateInfo(); + GraphicsLayout.pSetLayouts = GraphicsSetLayouts.data(); + GraphicsLayout.setLayoutCount = static_cast(GraphicsSetLayouts.size()); + GraphicsLayout.pPushConstantRanges = &GraphicsPushConstant; + GraphicsLayout.pushConstantRangeCount = 1; + VK_CHECK(vkCreatePipelineLayout(m_Device, &GraphicsLayout, VK_NULL_HANDLE, + &m_MeshGraphicsPipelineLayout)); + + VkPipelineLayoutCreateInfo DepthLayout = VkInit::PipelineLayoutCreateInfo(); + DepthLayout.pSetLayouts = &m_MeshGraphicsFrameDescriptorLayout; + DepthLayout.setLayoutCount = 1; + DepthLayout.pPushConstantRanges = &GraphicsPushConstant; + DepthLayout.pushConstantRangeCount = 1; + VK_CHECK(vkCreatePipelineLayout(m_Device, &DepthLayout, VK_NULL_HANDLE, + &m_MeshDepthPipelineLayout)); + + VkShaderModule HzbReduceShader; + const std::string HzbReduceShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/hzb_reduce.comp.spv"; + if (!VkUtil::LoadShaderModule(HzbReduceShaderPath.c_str(), m_Device, + &HzbReduceShader)) { + A_ERROR("Error when loading the HZB reduction shader: {0}", + HzbReduceShaderPath); + Axiom::Log::Flush(); + abort(); + } + const VkPipelineShaderStageCreateInfo HzbReduceStageInfo = + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, + HzbReduceShader); + const VkComputePipelineCreateInfo HzbReducePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = HzbReduceStageInfo, + .layout = m_HzbReducePipelineLayout}; + VK_CHECK(vkCreateComputePipelines(m_Device, VK_NULL_HANDLE, 1, + &HzbReducePipelineCreateInfo, VK_NULL_HANDLE, + &m_HzbReducePipeline)); + vkDestroyShaderModule(m_Device, HzbReduceShader, VK_NULL_HANDLE); + + VkShaderModule MeshProjectShader; + const std::string MeshProjectShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh_project.comp.spv"; + if (!VkUtil::LoadShaderModule(MeshProjectShaderPath.c_str(), m_Device, + &MeshProjectShader)) { + A_ERROR("Error when loading the mesh projection shader: {0}", + MeshProjectShaderPath); + Axiom::Log::Flush(); + abort(); + } + const VkPipelineShaderStageCreateInfo MeshProjectStageInfo = + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, + MeshProjectShader); + const VkComputePipelineCreateInfo MeshProjectPipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = MeshProjectStageInfo, + .layout = m_MeshProjectPipelineLayout}; + VK_CHECK(vkCreateComputePipelines(m_Device, VK_NULL_HANDLE, 1, + &MeshProjectPipelineCreateInfo, VK_NULL_HANDLE, + &m_MeshProjectPipeline)); + vkDestroyShaderModule(m_Device, MeshProjectShader, VK_NULL_HANDLE); + + VkShaderModule MeshShader; + const std::string MeshShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh_raster.comp.spv"; + if (!VkUtil::LoadShaderModule(MeshShaderPath.c_str(), m_Device, &MeshShader)) { + A_ERROR("Error when loading the mesh compute shader: {0}", MeshShaderPath); + Axiom::Log::Flush(); + abort(); + } + const VkPipelineShaderStageCreateInfo MeshStageInfo = + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_COMPUTE_BIT, + MeshShader); + const VkComputePipelineCreateInfo MeshPipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .stage = MeshStageInfo, + .layout = m_MeshPipelineLayout}; + VK_CHECK(vkCreateComputePipelines(m_Device, VK_NULL_HANDLE, 1, + &MeshPipelineCreateInfo, VK_NULL_HANDLE, + &m_MeshPipeline)); + vkDestroyShaderModule(m_Device, MeshShader, VK_NULL_HANDLE); + + VkShaderModule VertexShader; + const std::string VertexShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh.vert.spv"; + if (!VkUtil::LoadShaderModule(VertexShaderPath.c_str(), m_Device, + &VertexShader)) { + A_ERROR("Error when loading the mesh vertex shader: {0}", VertexShaderPath); + Axiom::Log::Flush(); + abort(); + } + + VkShaderModule FragmentShader; + const std::string FragmentShaderPath = + std::string(AXIOM_CONTENT_DIR) + "/Shaders/mesh.frag.spv"; + if (!VkUtil::LoadShaderModule(FragmentShaderPath.c_str(), m_Device, + &FragmentShader)) { + A_ERROR("Error when loading the mesh fragment shader: {0}", + FragmentShaderPath); + Axiom::Log::Flush(); + abort(); + } + + const std::array ShaderStages = { + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_VERTEX_BIT, + VertexShader), + VkInit::PipelineShaderStageCreateInfo(VK_SHADER_STAGE_FRAGMENT_BIT, + FragmentShader)}; + + const VkVertexInputBindingDescription BindingDescription{ + .binding = 0, + .stride = sizeof(MeshVertex), + .inputRate = VK_VERTEX_INPUT_RATE_VERTEX}; + const std::array AttributeDescriptions = { + VkVertexInputAttributeDescription{0, 0, VK_FORMAT_R32G32B32_SFLOAT, + offsetof(MeshVertex, Position)}, + VkVertexInputAttributeDescription{1, 0, VK_FORMAT_R32G32B32_SFLOAT, + offsetof(MeshVertex, Normal)}, + VkVertexInputAttributeDescription{2, 0, VK_FORMAT_R32G32_SFLOAT, + offsetof(MeshVertex, TexCoord)}}; + const VkPipelineVertexInputStateCreateInfo VertexInputInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .vertexBindingDescriptionCount = 1, + .pVertexBindingDescriptions = &BindingDescription, + .vertexAttributeDescriptionCount = + static_cast(AttributeDescriptions.size()), + .pVertexAttributeDescriptions = AttributeDescriptions.data()}; + const VkPipelineInputAssemblyStateCreateInfo InputAssembly{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; + const VkPipelineViewportStateCreateInfo ViewportState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .viewportCount = 1, + .scissorCount = 1}; + const VkPipelineRasterizationStateCreateInfo Rasterizer{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_NONE, + .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, + .lineWidth = 1.0f}; + const VkPipelineMultisampleStateCreateInfo Multisampling{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT}; + const VkPipelineDepthStencilStateCreateInfo DepthStencil{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, + .depthTestEnable = VK_TRUE, + .depthWriteEnable = VK_TRUE, + .depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL}; + + VkPipelineColorBlendAttachmentState ColorBlendAttachment{}; + ColorBlendAttachment.colorWriteMask = + VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + + const VkPipelineColorBlendStateCreateInfo ColorBlending{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .attachmentCount = 1, + .pAttachments = &ColorBlendAttachment}; + const std::array DynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + const VkPipelineDynamicStateCreateInfo DynamicState{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .dynamicStateCount = static_cast(DynamicStates.size()), + .pDynamicStates = DynamicStates.data()}; + const VkPipelineRenderingCreateInfo RenderingInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + .colorAttachmentCount = 1, + .pColorAttachmentFormats = &m_DrawImageFormat, + .depthAttachmentFormat = m_RasterDepthFormat}; + const VkGraphicsPipelineCreateInfo PipelineInfo{ + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .pNext = &RenderingInfo, + .stageCount = static_cast(ShaderStages.size()), + .pStages = ShaderStages.data(), + .pVertexInputState = &VertexInputInfo, + .pInputAssemblyState = &InputAssembly, + .pViewportState = &ViewportState, + .pRasterizationState = &Rasterizer, + .pMultisampleState = &Multisampling, + .pDepthStencilState = &DepthStencil, + .pColorBlendState = &ColorBlending, + .pDynamicState = &DynamicState, + .layout = m_MeshGraphicsPipelineLayout}; + VK_CHECK(vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, &PipelineInfo, + VK_NULL_HANDLE, &m_MeshGraphicsPipeline)); + + VkPipelineColorBlendAttachmentState AlphaBlendAttachment = ColorBlendAttachment; + AlphaBlendAttachment.blendEnable = VK_TRUE; + AlphaBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + AlphaBlendAttachment.dstColorBlendFactor = + VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + AlphaBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + AlphaBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + AlphaBlendAttachment.dstAlphaBlendFactor = + VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + AlphaBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + VkPipelineColorBlendStateCreateInfo AlphaBlending = ColorBlending; + AlphaBlending.pAttachments = &AlphaBlendAttachment; + VkPipelineDepthStencilStateCreateInfo AlphaDepthStencil = DepthStencil; + AlphaDepthStencil.depthWriteEnable = VK_FALSE; + VkGraphicsPipelineCreateInfo AlphaPipelineInfo = PipelineInfo; + AlphaPipelineInfo.pColorBlendState = &AlphaBlending; + AlphaPipelineInfo.pDepthStencilState = &AlphaDepthStencil; + VK_CHECK(vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, + &AlphaPipelineInfo, VK_NULL_HANDLE, + &m_MeshGraphicsAlphaBlendPipeline)); + + VkPipelineColorBlendAttachmentState DepthOnlyColorAttachment{}; + DepthOnlyColorAttachment.colorWriteMask = 0; + VkPipelineColorBlendStateCreateInfo DepthOnlyBlending = ColorBlending; + DepthOnlyBlending.pAttachments = &DepthOnlyColorAttachment; + const VkPipelineRenderingCreateInfo DepthRenderingInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO, + .depthAttachmentFormat = m_RasterDepthFormat}; + VkGraphicsPipelineCreateInfo DepthPipelineInfo = PipelineInfo; + DepthPipelineInfo.pNext = &DepthRenderingInfo; + DepthPipelineInfo.pColorBlendState = &DepthOnlyBlending; + DepthPipelineInfo.layout = m_MeshDepthPipelineLayout; + DepthPipelineInfo.stageCount = 1; + DepthPipelineInfo.pStages = &ShaderStages[0]; + VK_CHECK(vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, + &DepthPipelineInfo, VK_NULL_HANDLE, + &m_MeshDepthPipeline)); + + VkPipelineRasterizationStateCreateInfo WireframeRasterizer = Rasterizer; + WireframeRasterizer.polygonMode = VK_POLYGON_MODE_LINE; + VkPipelineDepthStencilStateCreateInfo WireframeDepthStencil = DepthStencil; + WireframeDepthStencil.depthTestEnable = VK_FALSE; + WireframeDepthStencil.depthWriteEnable = VK_FALSE; + VkGraphicsPipelineCreateInfo WireframePipelineInfo = PipelineInfo; + WireframePipelineInfo.pRasterizationState = &WireframeRasterizer; + WireframePipelineInfo.pDepthStencilState = &WireframeDepthStencil; + VK_CHECK(vkCreateGraphicsPipelines(m_Device, VK_NULL_HANDLE, 1, + &WireframePipelineInfo, VK_NULL_HANDLE, + &m_MeshWireframePipeline)); + + vkDestroyShaderModule(m_Device, VertexShader, VK_NULL_HANDLE); + vkDestroyShaderModule(m_Device, FragmentShader, VK_NULL_HANDLE); +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.h new file mode 100644 index 00000000..6ce2893e --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanPipelineLibrary.h @@ -0,0 +1,90 @@ +#pragma once + +#include "AxiomRHI/Vulkan/VulkanTypes.h" + +namespace Axiom { +class VulkanResourceManager; + +class VulkanPipelineLibrary { +public: + struct CreateInfo { + VkDevice Device{VK_NULL_HANDLE}; + VkFormat DrawImageFormat{VK_FORMAT_UNDEFINED}; + VkFormat RasterDepthFormat{VK_FORMAT_UNDEFINED}; + VkDescriptorSetLayout DrawImageDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout HzbReduceDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout MeshGraphicsFrameDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout MeshGraphicsMaterialDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout MeshComputeFrameDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout MeshDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout HDRSkyboxDescriptorLayout{VK_NULL_HANDLE}; + }; + + void Init(const CreateInfo &CreateInfo); + void Shutdown(); + + VkPipeline GetGradientPipeline() const { return m_GradientPipeline; } + VkPipelineLayout GetGradientPipelineLayout() const { + return m_GradientPipelineLayout; + } + VkPipeline GetHDRSkyboxPipeline() const { return m_HDRSkyboxPipeline; } + VkPipelineLayout GetHDRSkyboxPipelineLayout() const { + return m_HDRSkyboxPipelineLayout; + } + VkPipeline GetHzbReducePipeline() const { return m_HzbReducePipeline; } + VkPipelineLayout GetHzbReducePipelineLayout() const { + return m_HzbReducePipelineLayout; + } + VkPipeline GetMeshProjectPipeline() const { return m_MeshProjectPipeline; } + VkPipelineLayout GetMeshProjectPipelineLayout() const { + return m_MeshProjectPipelineLayout; + } + VkPipeline GetMeshPipeline() const { return m_MeshPipeline; } + VkPipelineLayout GetMeshPipelineLayout() const { return m_MeshPipelineLayout; } + VkPipeline GetMeshGraphicsPipeline() const { return m_MeshGraphicsPipeline; } + VkPipeline GetMeshGraphicsAlphaBlendPipeline() const { + return m_MeshGraphicsAlphaBlendPipeline; + } + VkPipelineLayout GetMeshGraphicsPipelineLayout() const { + return m_MeshGraphicsPipelineLayout; + } + VkPipeline GetMeshWireframePipeline() const { return m_MeshWireframePipeline; } + VkPipeline GetMeshDepthPipeline() const { return m_MeshDepthPipeline; } + VkPipelineLayout GetMeshDepthPipelineLayout() const { + return m_MeshDepthPipelineLayout; + } + +private: + void InitBackgroundPipelines(); + void InitMeshPipelines(); + +private: + VkDevice m_Device{VK_NULL_HANDLE}; + VkFormat m_DrawImageFormat{VK_FORMAT_UNDEFINED}; + VkFormat m_RasterDepthFormat{VK_FORMAT_UNDEFINED}; + VkDescriptorSetLayout m_DrawImageDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_HzbReduceDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshGraphicsFrameDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshGraphicsMaterialDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshComputeFrameDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_HDRSkyboxDescriptorLayout{VK_NULL_HANDLE}; + + VkPipeline m_GradientPipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_GradientPipelineLayout{VK_NULL_HANDLE}; + VkPipeline m_HDRSkyboxPipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_HDRSkyboxPipelineLayout{VK_NULL_HANDLE}; + VkPipeline m_HzbReducePipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_HzbReducePipelineLayout{VK_NULL_HANDLE}; + VkPipeline m_MeshProjectPipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_MeshProjectPipelineLayout{VK_NULL_HANDLE}; + VkPipeline m_MeshPipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_MeshPipelineLayout{VK_NULL_HANDLE}; + VkPipeline m_MeshGraphicsPipeline{VK_NULL_HANDLE}; + VkPipeline m_MeshGraphicsAlphaBlendPipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_MeshGraphicsPipelineLayout{VK_NULL_HANDLE}; + VkPipeline m_MeshWireframePipeline{VK_NULL_HANDLE}; + VkPipeline m_MeshDepthPipeline{VK_NULL_HANDLE}; + VkPipelineLayout m_MeshDepthPipelineLayout{VK_NULL_HANDLE}; +}; +} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanRendererTypes.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanRendererTypes.h similarity index 89% rename from Axiom/Renderer/Vulkan/VulkanRendererTypes.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanRendererTypes.h index 3b80f0f4..0ceb02c1 100644 --- a/Axiom/Renderer/Vulkan/VulkanRendererTypes.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanRendererTypes.h @@ -1,8 +1,6 @@ #pragma once -#include "Renderer/Vulkan/VulkanTypes.h" - -#include +#include "AxiomRHI/Vulkan/VulkanTypes.h" #include #include @@ -10,7 +8,6 @@ #include namespace Axiom { -constexpr uint32_t MaxMeshSubmissionsPerFrame = 256; constexpr uint32_t TimestampQueryCount = 4; struct ComputePushConstants { @@ -63,8 +60,7 @@ struct MeshFrameResources { AllocatedBuffer CameraBuffer; AllocatedBuffer HzbReadbackBuffer; VkDescriptorSet DepthFrameDescriptorSet{VK_NULL_HANDLE}; - std::array - GraphicsFrameDescriptorSets{}; + VkDescriptorSet GraphicsFrameDescriptorSet{VK_NULL_HANDLE}; VkDescriptorSet ComputeFrameDescriptorSet{VK_NULL_HANDLE}; VkQueryPool TimestampQueryPool{VK_NULL_HANDLE}; glm::mat4 HzbViewProjection{1.0f}; diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.cpp new file mode 100644 index 00000000..ab2d4cc8 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.cpp @@ -0,0 +1,651 @@ +#include "AxiomRHI/Vulkan/VulkanResourceManager.h" + +#include "AxiomRHI/Vulkan/VulkanBuffer.h" +#include "AxiomRHI/Vulkan/VulkanImage.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" + +#include +#include + +namespace Axiom { +namespace { +uint32_t ComputeHzbMipCount(VkExtent2D BaseExtent) { + uint32_t Width = BaseExtent.width; + uint32_t Height = BaseExtent.height; + uint32_t MipCount = 0; + while (Width > 0 && Height > 0) { + ++MipCount; + Width = std::max(1u, Width / 2u); + Height = std::max(1u, Height / 2u); + if (Width == 1u && Height == 1u) { + ++MipCount; + break; + } + } + return std::max(1u, MipCount - 1u); +} + +VkExtent2D ComputeHzbMipExtent(VkExtent2D BaseExtent, uint32_t MipLevel) { + return { + std::max(1u, BaseExtent.width >> MipLevel), + std::max(1u, BaseExtent.height >> MipLevel), + }; +} + +void PopulateTextureImage(VkCommandBuffer CommandBuffer, const AllocatedImage &Image, + VkBuffer StagingBuffer) { + VkUtil::TransitionImage(CommandBuffer, Image.Image, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + VkBufferImageCopy Region{}; + Region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + Region.imageSubresource.layerCount = 1; + Region.imageExtent = Image.ImageExtent; + vkCmdCopyBufferToImage(CommandBuffer, StagingBuffer, Image.Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &Region); + + VkUtil::TransitionImage(CommandBuffer, Image.Image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +} +} // namespace + +void VulkanResourceManager::Init(const CreateInfo &CreateInfo) { + m_Context = &CreateInfo.Context; + m_Device = &CreateInfo.Device; + m_WindowExtent = CreateInfo.WindowExtent; + m_HasPresentationSurface = CreateInfo.HasPresentationSurface; + m_AttachmentRequirements = CreateInfo.AttachmentRequirements; + m_SubmitTransferUpload = CreateInfo.SubmitTransferUpload; + + InitSwapchain(); + InitHzbResources(); + InitViewportReadbackBuffers(); + InitDescriptors(); + InitMeshFrameResources(); +} + +void VulkanResourceManager::Shutdown() { + DestroyHDRSkyboxTexture(); + + for (auto &Image : m_ManagedTextureImages) { + DestroyManagedImage(Image); + } + m_ManagedTextureImages.clear(); + + for (auto &Frame : m_MeshFrames) { + if (Frame.TimestampQueryPool != VK_NULL_HANDLE) { + vkDestroyQueryPool(m_Device->Device, Frame.TimestampQueryPool, + VK_NULL_HANDLE); + Frame.TimestampQueryPool = VK_NULL_HANDLE; + } + VkBufferUtil::DestroyBuffer(m_Device->Allocator, Frame.CameraBuffer); + VkBufferUtil::DestroyBuffer(m_Device->Allocator, Frame.HzbReadbackBuffer); + } + + for (auto &CaptureFrame : m_OffscreenCaptureFrames) { + VkBufferUtil::DestroyBuffer(m_Device->Allocator, CaptureFrame.ReadbackBuffer); + CaptureFrame = {}; + } + + for (VkImageView MipView : m_HzbMipImageViews) { + if (MipView != VK_NULL_HANDLE) { + vkDestroyImageView(m_Device->Device, MipView, VK_NULL_HANDLE); + } + } + m_HzbMipImageViews.clear(); + m_HzbMipExtents.clear(); + m_HzbMipOffsets.clear(); + DestroyManagedImage(m_HzbImage); + + if (m_HDRSkyboxSampler != VK_NULL_HANDLE) { + vkDestroySampler(m_Device->Device, m_HDRSkyboxSampler, VK_NULL_HANDLE); + m_HDRSkyboxSampler = VK_NULL_HANDLE; + } + if (m_TextureSampler != VK_NULL_HANDLE) { + vkDestroySampler(m_Device->Device, m_TextureSampler, VK_NULL_HANDLE); + m_TextureSampler = VK_NULL_HANDLE; + } + if (m_LinearDepthSampler != VK_NULL_HANDLE) { + vkDestroySampler(m_Device->Device, m_LinearDepthSampler, VK_NULL_HANDLE); + m_LinearDepthSampler = VK_NULL_HANDLE; + } + + if (m_MeshDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, m_MeshDescriptorLayout, + VK_NULL_HANDLE); + m_MeshDescriptorLayout = VK_NULL_HANDLE; + } + if (m_MeshComputeFrameDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, + m_MeshComputeFrameDescriptorLayout, + VK_NULL_HANDLE); + m_MeshComputeFrameDescriptorLayout = VK_NULL_HANDLE; + } + if (m_MeshGraphicsFrameDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, + m_MeshGraphicsFrameDescriptorLayout, + VK_NULL_HANDLE); + m_MeshGraphicsFrameDescriptorLayout = VK_NULL_HANDLE; + } + if (m_MeshGraphicsMaterialDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, + m_MeshGraphicsMaterialDescriptorLayout, + VK_NULL_HANDLE); + m_MeshGraphicsMaterialDescriptorLayout = VK_NULL_HANDLE; + } + if (m_HzbReduceDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, m_HzbReduceDescriptorLayout, + VK_NULL_HANDLE); + m_HzbReduceDescriptorLayout = VK_NULL_HANDLE; + } + if (m_DrawImageDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, m_DrawImageDescriptorLayout, + VK_NULL_HANDLE); + m_DrawImageDescriptorLayout = VK_NULL_HANDLE; + } + if (m_HDRSkyboxDescriptorLayout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(m_Device->Device, m_HDRSkyboxDescriptorLayout, + VK_NULL_HANDLE); + m_HDRSkyboxDescriptorLayout = VK_NULL_HANDLE; + } + m_GlobalDescriptorAllocator.DestroyPool(m_Device->Device); + + DestroyManagedImage(m_RasterDepthImage); + DestroyManagedImage(m_DepthImage); + DestroyManagedImage(m_DrawImage); + + m_Swapchain.Shutdown(*m_Device); + m_AttachmentRequirements = {}; +} + +void VulkanResourceManager::InitSwapchain() { + if (m_HasPresentationSurface) { + m_Swapchain.Init(*m_Context, *m_Device, m_WindowExtent.width, + m_WindowExtent.height); + } else { + m_Swapchain.ImageFormat = VK_FORMAT_B8G8R8A8_UNORM; + m_Swapchain.Extent = m_WindowExtent; + } + + const VkExtent3D DrawImageExtent = { + m_WindowExtent.width, m_WindowExtent.height, 1}; + + if (m_AttachmentRequirements.NeedsGBuffer && + m_AttachmentRequirements.GBufferColorTargetCount > 0) { + // Deferred attachments are not implemented in this PR; forward continues to + // allocate its single HDR target plus depth resources. + } + + m_DrawImage.ImageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; + m_DrawImage.ImageExtent = DrawImageExtent; + + VkImageCreateInfo DrawInfo = VkInit::ImageCreateInfo( + m_DrawImage.ImageFormat, + VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, + m_DrawImage.ImageExtent); + + VmaAllocationCreateInfo DrawAllocInfo{}; + DrawAllocInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + DrawAllocInfo.requiredFlags = + VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + VK_CHECK(vmaCreateImage(m_Device->Allocator, &DrawInfo, &DrawAllocInfo, + &m_DrawImage.Image, &m_DrawImage.Allocation, + VK_NULL_HANDLE)); + VkImageViewCreateInfo DrawViewInfo = VkInit::ImageViewCreateInfo( + m_DrawImage.ImageFormat, m_DrawImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); + VK_CHECK(vkCreateImageView(m_Device->Device, &DrawViewInfo, VK_NULL_HANDLE, + &m_DrawImage.ImageView)); + + m_DepthImage.ImageFormat = VK_FORMAT_R32_UINT; + m_DepthImage.ImageExtent = DrawImageExtent; + VkImageCreateInfo DepthInfo = VkInit::ImageCreateInfo( + m_DepthImage.ImageFormat, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT, + m_DepthImage.ImageExtent); + VK_CHECK(vmaCreateImage(m_Device->Allocator, &DepthInfo, &DrawAllocInfo, + &m_DepthImage.Image, &m_DepthImage.Allocation, + VK_NULL_HANDLE)); + VkImageViewCreateInfo DepthViewInfo = VkInit::ImageViewCreateInfo( + m_DepthImage.ImageFormat, m_DepthImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); + VK_CHECK(vkCreateImageView(m_Device->Device, &DepthViewInfo, VK_NULL_HANDLE, + &m_DepthImage.ImageView)); + + m_RasterDepthImage.ImageFormat = VK_FORMAT_D32_SFLOAT; + m_RasterDepthImage.ImageExtent = DrawImageExtent; + VkImageCreateInfo RasterDepthInfo = VkInit::ImageCreateInfo( + m_RasterDepthImage.ImageFormat, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + m_RasterDepthImage.ImageExtent); + VK_CHECK(vmaCreateImage(m_Device->Allocator, &RasterDepthInfo, &DrawAllocInfo, + &m_RasterDepthImage.Image, + &m_RasterDepthImage.Allocation, VK_NULL_HANDLE)); + VkImageViewCreateInfo RasterDepthViewInfo = VkInit::ImageViewCreateInfo( + m_RasterDepthImage.ImageFormat, m_RasterDepthImage.Image, + VK_IMAGE_ASPECT_DEPTH_BIT); + VK_CHECK(vkCreateImageView(m_Device->Device, &RasterDepthViewInfo, + VK_NULL_HANDLE, &m_RasterDepthImage.ImageView)); +} + +void VulkanResourceManager::InitViewportReadbackBuffers() { + const size_t BufferSize = + static_cast(m_DrawImage.ImageExtent.width) * + static_cast(m_DrawImage.ImageExtent.height) * sizeof(uint16_t) * + 4u; + for (auto &CaptureFrame : m_OffscreenCaptureFrames) { + CaptureFrame.ReadbackBuffer = VkBufferUtil::CreateBuffer( + m_Device->Allocator, BufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT, + VMA_MEMORY_USAGE_GPU_TO_CPU, + VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT); + } +} + +void VulkanResourceManager::InitHzbResources() { + const VkExtent2D BaseExtent = {m_DrawImage.ImageExtent.width, + m_DrawImage.ImageExtent.height}; + const uint32_t MipCount = ComputeHzbMipCount(BaseExtent); + + m_HzbImage.ImageFormat = VK_FORMAT_R32_SFLOAT; + m_HzbImage.ImageExtent = {BaseExtent.width, BaseExtent.height, 1}; + + VkImageCreateInfo HzbInfo = + VkInit::ImageCreateInfo(m_HzbImage.ImageFormat, + VK_IMAGE_USAGE_STORAGE_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + m_HzbImage.ImageExtent); + HzbInfo.mipLevels = MipCount; + + VmaAllocationCreateInfo AllocationInfo{}; + AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + AllocationInfo.requiredFlags = + VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vmaCreateImage(m_Device->Allocator, &HzbInfo, &AllocationInfo, + &m_HzbImage.Image, &m_HzbImage.Allocation, + VK_NULL_HANDLE)); + + m_HzbMipImageViews.reserve(MipCount); + m_HzbMipExtents.reserve(MipCount); + m_HzbMipOffsets.reserve(MipCount); + + for (uint32_t MipLevel = 0; MipLevel < MipCount; ++MipLevel) { + const VkExtent2D MipExtent = ComputeHzbMipExtent(BaseExtent, MipLevel); + m_HzbMipExtents.push_back(MipExtent); + m_HzbMipOffsets.push_back(m_HzbReadbackBufferSize); + m_HzbReadbackBufferSize += + static_cast(MipExtent.width) * + static_cast(MipExtent.height) * sizeof(float); + + VkImageViewCreateInfo ViewInfo = VkInit::ImageViewCreateInfo( + m_HzbImage.ImageFormat, m_HzbImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); + ViewInfo.subresourceRange.baseMipLevel = MipLevel; + ViewInfo.subresourceRange.levelCount = 1; + + VkImageView MipView = VK_NULL_HANDLE; + VK_CHECK(vkCreateImageView(m_Device->Device, &ViewInfo, VK_NULL_HANDLE, + &MipView)); + m_HzbMipImageViews.push_back(MipView); + } +} + +void VulkanResourceManager::InitDescriptors() { + const uint32_t InitialSetCount = + 5 + (FRAME_OVERLAP * 3) + + static_cast(m_HzbMipImageViews.size()) + 64; + std::vector Sizes = { + {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 4.0f}, + {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6.0f}, + {VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 2.0f}, + {VK_DESCRIPTOR_TYPE_SAMPLER, 2.0f}, + {VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 4.0f}, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2.0f}}; + m_GlobalDescriptorAllocator.InitPool(m_Device->Device, InitialSetCount, Sizes); + + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); + m_DrawImageDescriptorLayout = + Builder.Build(m_Device->Device, VK_SHADER_STAGE_COMPUTE_BIT); + } + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); + Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + m_HzbReduceDescriptorLayout = + Builder.Build(m_Device->Device, VK_SHADER_STAGE_COMPUTE_BIT); + } + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + m_MeshGraphicsFrameDescriptorLayout = + Builder.Build(m_Device->Device, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + } + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE); + Builder.AddBinding(2, VK_DESCRIPTOR_TYPE_SAMPLER); + m_MeshGraphicsMaterialDescriptorLayout = + Builder.Build(m_Device->Device, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + } + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); + Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + Builder.AddBinding(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + m_MeshComputeFrameDescriptorLayout = + Builder.Build(m_Device->Device, VK_SHADER_STAGE_COMPUTE_BIT); + } + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + Builder.AddBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + Builder.AddBinding(2, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER); + m_MeshDescriptorLayout = + Builder.Build(m_Device->Device, VK_SHADER_STAGE_COMPUTE_BIT); + } + { + DescriptorLayoutBuilder Builder; + Builder.AddBinding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + m_HDRSkyboxDescriptorLayout = + Builder.Build(m_Device->Device, VK_SHADER_STAGE_COMPUTE_BIT); + } + + m_DrawImageDescriptorSet = m_GlobalDescriptorAllocator.Allocate( + m_Device->Device, m_DrawImageDescriptorLayout); + VkDescriptorImageInfo DrawImageInfo{}; + DrawImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + DrawImageInfo.imageView = m_DrawImage.ImageView; + const VkWriteDescriptorSet DrawImageWrite = VkInit::WriteDescriptorSet( + VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, m_DrawImageDescriptorSet, &DrawImageInfo, + 0); + vkUpdateDescriptorSets(m_Device->Device, 1, &DrawImageWrite, 0, + VK_NULL_HANDLE); + + VkSamplerCreateInfo SamplerInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = VK_NULL_HANDLE, + .magFilter = VK_FILTER_NEAREST, + .minFilter = VK_FILTER_NEAREST, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE, + .unnormalizedCoordinates = VK_FALSE}; + VK_CHECK(vkCreateSampler(m_Device->Device, &SamplerInfo, VK_NULL_HANDLE, + &m_LinearDepthSampler)); + + SamplerInfo.magFilter = VK_FILTER_LINEAR; + SamplerInfo.minFilter = VK_FILTER_LINEAR; + SamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + SamplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; + SamplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; + SamplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; + VK_CHECK(vkCreateSampler(m_Device->Device, &SamplerInfo, VK_NULL_HANDLE, + &m_TextureSampler)); + + VkSamplerCreateInfo HDRSamplerInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = VK_NULL_HANDLE, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT, + .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, + .borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK, + .unnormalizedCoordinates = VK_FALSE}; + VK_CHECK(vkCreateSampler(m_Device->Device, &HDRSamplerInfo, VK_NULL_HANDLE, + &m_HDRSkyboxSampler)); + + m_HzbReduceDescriptorSets.reserve(m_HzbMipImageViews.size()); + for (size_t MipLevel = 0; MipLevel < m_HzbMipImageViews.size(); ++MipLevel) { + VkDescriptorSet DescriptorSet = m_GlobalDescriptorAllocator.Allocate( + m_Device->Device, m_HzbReduceDescriptorLayout); + m_HzbReduceDescriptorSets.push_back(DescriptorSet); + + VkDescriptorImageInfo DestinationImageInfo{}; + DestinationImageInfo.imageView = m_HzbMipImageViews[MipLevel]; + DestinationImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + + VkDescriptorImageInfo SourceImageInfo{}; + SourceImageInfo.sampler = m_LinearDepthSampler; + SourceImageInfo.imageView = + (MipLevel == 0) ? m_RasterDepthImage.ImageView + : m_HzbMipImageViews[MipLevel - 1]; + SourceImageInfo.imageLayout = + (MipLevel == 0) ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_GENERAL; + + const std::array Writes = { + VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + DescriptorSet, &DestinationImageInfo, 0), + VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + DescriptorSet, &SourceImageInfo, 1)}; + vkUpdateDescriptorSets(m_Device->Device, static_cast(Writes.size()), + Writes.data(), 0, VK_NULL_HANDLE); + } +} + +void VulkanResourceManager::InitMeshFrameResources() { + for (auto &Frame : m_MeshFrames) { + Frame.CameraBuffer = VkBufferUtil::CreateBuffer( + m_Device->Allocator, sizeof(CameraFrameUniform), + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU, + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT); + Frame.HzbReadbackBuffer = VkBufferUtil::CreateBuffer( + m_Device->Allocator, static_cast(m_HzbReadbackBufferSize), + VK_BUFFER_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_TO_CPU, + VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT); + + Frame.DepthFrameDescriptorSet = m_GlobalDescriptorAllocator.Allocate( + m_Device->Device, m_MeshGraphicsFrameDescriptorLayout); + Frame.GraphicsFrameDescriptorSet = m_GlobalDescriptorAllocator.Allocate( + m_Device->Device, m_MeshGraphicsFrameDescriptorLayout); + Frame.ComputeFrameDescriptorSet = m_GlobalDescriptorAllocator.Allocate( + m_Device->Device, m_MeshComputeFrameDescriptorLayout); + + VkQueryPoolCreateInfo QueryPoolInfo{ + .sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, + .pNext = VK_NULL_HANDLE, + .queryType = VK_QUERY_TYPE_TIMESTAMP, + .queryCount = TimestampQueryCount}; + VK_CHECK(vkCreateQueryPool(m_Device->Device, &QueryPoolInfo, VK_NULL_HANDLE, + &Frame.TimestampQueryPool)); + } +} + +AllocatedImage +VulkanResourceManager::CreateManagedTextureImage(const TextureSourceData &TextureData) { + return CreateTextureImage(TextureData, true); +} + +AllocatedImage VulkanResourceManager::CreateManagedTextureImage( + const HDRTextureSourceData &TextureData) { + return CreateTextureImage(TextureData, true); +} + +AllocatedImage VulkanResourceManager::CreateTextureImage( + const TextureSourceData &TextureData, bool TrackForShutdown) { + AllocatedImage TextureImage{}; + TextureImage.ImageFormat = VK_FORMAT_R8G8B8A8_UNORM; + TextureImage.ImageExtent = {TextureData.Width, TextureData.Height, 1}; + + VkImageCreateInfo ImageInfo = VkInit::ImageCreateInfo( + TextureImage.ImageFormat, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + TextureImage.ImageExtent); + ConfigureTransferSharing(ImageInfo); + + VmaAllocationCreateInfo AllocationInfo{}; + AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + AllocationInfo.requiredFlags = + VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vmaCreateImage(m_Device->Allocator, &ImageInfo, &AllocationInfo, + &TextureImage.Image, &TextureImage.Allocation, + VK_NULL_HANDLE)); + + VkImageViewCreateInfo ViewInfo = VkInit::ImageViewCreateInfo( + TextureImage.ImageFormat, TextureImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); + VK_CHECK(vkCreateImageView(m_Device->Device, &ViewInfo, VK_NULL_HANDLE, + &TextureImage.ImageView)); + + const VkDeviceSize ByteCount = TextureData.Pixels.size(); + auto StagingBuffer = VkBufferUtil::CreateBuffer( + m_Device->Allocator, static_cast(ByteCount), + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY, + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT); + std::memcpy(StagingBuffer.Info.pMappedData, TextureData.Pixels.data(), + TextureData.Pixels.size()); + + m_SubmitTransferUpload( + [TextureImage, StagingBuffer](VkCommandBuffer CommandBuffer) mutable { + PopulateTextureImage(CommandBuffer, TextureImage, StagingBuffer.Buffer); + }, + [this, StagingBuffer]() mutable { + VkBufferUtil::DestroyBuffer(m_Device->Allocator, StagingBuffer); + }); + + if (TrackForShutdown) { + m_ManagedTextureImages.push_back(TextureImage); + } + return TextureImage; +} + +AllocatedImage VulkanResourceManager::CreateTextureImage( + const HDRTextureSourceData &TextureData, bool TrackForShutdown) { + AllocatedImage TextureImage{}; + TextureImage.ImageFormat = VK_FORMAT_R32G32B32A32_SFLOAT; + TextureImage.ImageExtent = {TextureData.Width, TextureData.Height, 1}; + + VkImageCreateInfo ImageInfo = VkInit::ImageCreateInfo( + TextureImage.ImageFormat, + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + TextureImage.ImageExtent); + ConfigureTransferSharing(ImageInfo); + + VmaAllocationCreateInfo AllocationInfo{}; + AllocationInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + AllocationInfo.requiredFlags = + VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + VK_CHECK(vmaCreateImage(m_Device->Allocator, &ImageInfo, &AllocationInfo, + &TextureImage.Image, &TextureImage.Allocation, + VK_NULL_HANDLE)); + + VkImageViewCreateInfo ViewInfo = VkInit::ImageViewCreateInfo( + TextureImage.ImageFormat, TextureImage.Image, VK_IMAGE_ASPECT_COLOR_BIT); + VK_CHECK(vkCreateImageView(m_Device->Device, &ViewInfo, VK_NULL_HANDLE, + &TextureImage.ImageView)); + + const VkDeviceSize ByteCount = + static_cast(TextureData.Pixels.size()) * sizeof(float); + auto StagingBuffer = VkBufferUtil::CreateBuffer( + m_Device->Allocator, static_cast(ByteCount), + VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY, + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | + VMA_ALLOCATION_CREATE_MAPPED_BIT); + std::memcpy(StagingBuffer.Info.pMappedData, TextureData.Pixels.data(), + static_cast(ByteCount)); + + m_SubmitTransferUpload( + [TextureImage, StagingBuffer](VkCommandBuffer CommandBuffer) mutable { + PopulateTextureImage(CommandBuffer, TextureImage, StagingBuffer.Buffer); + }, + [this, StagingBuffer]() mutable { + VkBufferUtil::DestroyBuffer(m_Device->Allocator, StagingBuffer); + }); + + if (TrackForShutdown) { + m_ManagedTextureImages.push_back(TextureImage); + } + return TextureImage; +} + +void VulkanResourceManager::SyncHDRSkyboxTexture(HDRTextureSourceDataRef Wanted, + FrameData &CurrentFrame) { + if (Wanted == m_LoadedHDRSkyboxData) { + return; + } + + if (m_HDRSkyboxImage.Image != VK_NULL_HANDLE) { + AllocatedImage OldImage = m_HDRSkyboxImage; + CurrentFrame.DeletionQueue.PushFunction([this, OldImage]() mutable { + AllocatedImage ImageCopy = OldImage; + DestroyManagedImage(ImageCopy); + }); + m_HDRSkyboxImage = {}; + m_HDRSkyboxDescriptorSet = VK_NULL_HANDLE; + } + + if (Wanted && Wanted->IsValid()) { + m_HDRSkyboxImage = CreateTextureImage(*Wanted, false); + m_HDRSkyboxDescriptorSet = m_GlobalDescriptorAllocator.Allocate( + m_Device->Device, m_HDRSkyboxDescriptorLayout); + + VkDescriptorImageInfo SamplerImage{}; + SamplerImage.sampler = m_HDRSkyboxSampler; + SamplerImage.imageView = m_HDRSkyboxImage.ImageView; + SamplerImage.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + + const VkWriteDescriptorSet Write = VkInit::WriteDescriptorSet( + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, m_HDRSkyboxDescriptorSet, + &SamplerImage, 0); + vkUpdateDescriptorSets(m_Device->Device, 1, &Write, 0, VK_NULL_HANDLE); + } + + m_LoadedHDRSkyboxData = Wanted; +} + +void VulkanResourceManager::DestroyHDRSkyboxTexture() { + DestroyManagedImage(m_HDRSkyboxImage); + m_HDRSkyboxDescriptorSet = VK_NULL_HANDLE; + m_LoadedHDRSkyboxData.reset(); +} + +void VulkanResourceManager::DestroyManagedImage(AllocatedImage &Image) { + if (Image.ImageView != VK_NULL_HANDLE) { + vkDestroyImageView(m_Device->Device, Image.ImageView, VK_NULL_HANDLE); + Image.ImageView = VK_NULL_HANDLE; + } + if (Image.Image != VK_NULL_HANDLE) { + vmaDestroyImage(m_Device->Allocator, Image.Image, Image.Allocation); + Image.Image = VK_NULL_HANDLE; + Image.Allocation = VK_NULL_HANDLE; + } +} + +void VulkanResourceManager::ConfigureTransferSharing(VkImageCreateInfo &ImageInfo) const { + if (m_Device->TransferQueueFamily == m_Device->GraphicsQueueFamily) { + return; + } + + static uint32_t QueueFamilies[2]; + QueueFamilies[0] = m_Device->GraphicsQueueFamily; + QueueFamilies[1] = m_Device->TransferQueueFamily; + ImageInfo.sharingMode = VK_SHARING_MODE_CONCURRENT; + ImageInfo.queueFamilyIndexCount = 2; + ImageInfo.pQueueFamilyIndices = QueueFamilies; +} + +void VulkanResourceManager::ConfigureTransferSharing( + VkBufferCreateInfo &BufferInfo) const { + if (m_Device->TransferQueueFamily == m_Device->GraphicsQueueFamily) { + return; + } + + static uint32_t QueueFamilies[2]; + QueueFamilies[0] = m_Device->GraphicsQueueFamily; + QueueFamilies[1] = m_Device->TransferQueueFamily; + BufferInfo.sharingMode = VK_SHARING_MODE_CONCURRENT; + BufferInfo.queueFamilyIndexCount = 2; + BufferInfo.pQueueFamilyIndices = QueueFamilies; +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.h new file mode 100644 index 00000000..d43749e1 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanResourceManager.h @@ -0,0 +1,183 @@ +#pragma once + +#include "Renderer/Material.h" +#include "Renderer/RendererTypes.h" +#include "Renderer/RenderSurface.h" +#include "AxiomRHI/Vulkan/VulkanCommandContext.h" +#include "AxiomRHI/Vulkan/VulkanDescriptors.h" +#include "AxiomRHI/Vulkan/VulkanDevice.h" +#include "AxiomRHI/Vulkan/VulkanRendererTypes.h" +#include "AxiomRHI/Vulkan/VulkanSwapchain.h" + +#include +#include +#include +#include +#include +#include + +namespace Axiom { +class VulkanContext; + +class VulkanResourceManager { +public: + struct OffscreenCaptureFrame { + AllocatedBuffer ReadbackBuffer; + bool HasPendingReadback{false}; + uint64_t SubmittedFrameNumber{0}; + SessionUserId SubmittedUser{}; + }; + + struct CreateInfo { + VulkanContext &Context; + VulkanDevice &Device; + VkExtent2D WindowExtent{}; + bool HasPresentationSurface{false}; + RendererAttachmentRequirements AttachmentRequirements{}; + std::function &&, + std::function &&)> + SubmitTransferUpload; + }; + + void Init(const CreateInfo &CreateInfo); + void Shutdown(); + + AllocatedImage CreateManagedTextureImage(const TextureSourceData &TextureData); + AllocatedImage CreateManagedTextureImage(const HDRTextureSourceData &TextureData); + void SyncHDRSkyboxTexture(HDRTextureSourceDataRef Wanted, + FrameData &CurrentFrame); + + VulkanSwapchain &GetSwapchain() { return m_Swapchain; } + const VulkanSwapchain &GetSwapchain() const { return m_Swapchain; } + + DescriptorAllocator &GetDescriptorAllocator() { return m_GlobalDescriptorAllocator; } + const DescriptorAllocator &GetDescriptorAllocator() const { + return m_GlobalDescriptorAllocator; + } + + VkDescriptorSet GetDrawImageDescriptorSet() const { return m_DrawImageDescriptorSet; } + VkDescriptorSetLayout GetDrawImageDescriptorLayout() const { + return m_DrawImageDescriptorLayout; + } + VkDescriptorSetLayout GetHzbReduceDescriptorLayout() const { + return m_HzbReduceDescriptorLayout; + } + VkDescriptorSetLayout GetMeshGraphicsFrameDescriptorLayout() const { + return m_MeshGraphicsFrameDescriptorLayout; + } + VkDescriptorSetLayout GetMeshGraphicsMaterialDescriptorLayout() const { + return m_MeshGraphicsMaterialDescriptorLayout; + } + VkDescriptorSetLayout GetMeshComputeFrameDescriptorLayout() const { + return m_MeshComputeFrameDescriptorLayout; + } + VkDescriptorSetLayout GetMeshDescriptorLayout() const { + return m_MeshDescriptorLayout; + } + + VkSampler GetLinearDepthSampler() const { return m_LinearDepthSampler; } + VkSampler GetTextureSampler() const { return m_TextureSampler; } + VkSampler GetHDRSkyboxSampler() const { return m_HDRSkyboxSampler; } + VkDescriptorSetLayout GetHDRSkyboxDescriptorLayout() const { + return m_HDRSkyboxDescriptorLayout; + } + VkDescriptorSet GetHDRSkyboxDescriptorSet() const { return m_HDRSkyboxDescriptorSet; } + bool HasHDRSkyboxTexture() const { return m_LoadedHDRSkyboxData != nullptr; } + + AllocatedImage &GetDrawImage() { return m_DrawImage; } + const AllocatedImage &GetDrawImage() const { return m_DrawImage; } + AllocatedImage &GetDepthImage() { return m_DepthImage; } + const AllocatedImage &GetDepthImage() const { return m_DepthImage; } + AllocatedImage &GetRasterDepthImage() { return m_RasterDepthImage; } + const AllocatedImage &GetRasterDepthImage() const { return m_RasterDepthImage; } + AllocatedImage &GetHzbImage() { return m_HzbImage; } + const AllocatedImage &GetHzbImage() const { return m_HzbImage; } + + VkImageLayout GetHzbImageLayout() const { return m_HzbImageLayout; } + void SetHzbImageLayout(VkImageLayout Layout) { m_HzbImageLayout = Layout; } + + const std::vector &GetHzbMipImageViews() const { + return m_HzbMipImageViews; + } + const std::vector &GetHzbReduceDescriptorSets() const { + return m_HzbReduceDescriptorSets; + } + const std::vector &GetHzbMipExtents() const { return m_HzbMipExtents; } + const std::vector &GetHzbMipOffsets() const { return m_HzbMipOffsets; } + VkDeviceSize GetHzbReadbackBufferSize() const { return m_HzbReadbackBufferSize; } + + std::array &GetMeshFrames() { + return m_MeshFrames; + } + const std::array &GetMeshFrames() const { + return m_MeshFrames; + } + MeshFrameResources &GetMeshFrame(uint64_t FrameNumber) { + return m_MeshFrames[FrameNumber % FRAME_OVERLAP]; + } + + std::array &GetOffscreenCaptureFrames() { + return m_OffscreenCaptureFrames; + } + OffscreenCaptureFrame &GetOffscreenCaptureFrame(uint64_t FrameNumber) { + return m_OffscreenCaptureFrames[FrameNumber % FRAME_OVERLAP]; + } + +private: + void InitSwapchain(); + void InitViewportReadbackBuffers(); + void InitHzbResources(); + void InitDescriptors(); + void InitMeshFrameResources(); + AllocatedImage CreateTextureImage(const TextureSourceData &TextureData, + bool TrackForShutdown); + AllocatedImage CreateTextureImage(const HDRTextureSourceData &TextureData, + bool TrackForShutdown); + void DestroyHDRSkyboxTexture(); + void DestroyManagedImage(AllocatedImage &Image); + void ConfigureTransferSharing(VkImageCreateInfo &ImageInfo) const; + void ConfigureTransferSharing(VkBufferCreateInfo &BufferInfo) const; + +private: + VulkanContext *m_Context{nullptr}; + VulkanDevice *m_Device{nullptr}; + VkExtent2D m_WindowExtent{1700, 900}; + bool m_HasPresentationSurface{false}; + RendererAttachmentRequirements m_AttachmentRequirements{}; + std::function &&, + std::function &&)> + m_SubmitTransferUpload; + + VulkanSwapchain m_Swapchain; + DescriptorAllocator m_GlobalDescriptorAllocator; + VkDescriptorSet m_DrawImageDescriptorSet{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_DrawImageDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_HzbReduceDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshGraphicsFrameDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshGraphicsMaterialDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshComputeFrameDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSetLayout m_MeshDescriptorLayout{VK_NULL_HANDLE}; + VkSampler m_LinearDepthSampler{VK_NULL_HANDLE}; + VkSampler m_TextureSampler{VK_NULL_HANDLE}; + + VkDescriptorSetLayout m_HDRSkyboxDescriptorLayout{VK_NULL_HANDLE}; + VkDescriptorSet m_HDRSkyboxDescriptorSet{VK_NULL_HANDLE}; + VkSampler m_HDRSkyboxSampler{VK_NULL_HANDLE}; + AllocatedImage m_HDRSkyboxImage{}; + HDRTextureSourceDataRef m_LoadedHDRSkyboxData{nullptr}; + + AllocatedImage m_DrawImage; + AllocatedImage m_DepthImage; + AllocatedImage m_RasterDepthImage; + AllocatedImage m_HzbImage; + std::array m_OffscreenCaptureFrames{}; + std::vector m_HzbMipImageViews; + std::vector m_HzbReduceDescriptorSets; + std::vector m_HzbMipExtents; + std::vector m_HzbMipOffsets; + VkDeviceSize m_HzbReadbackBufferSize{0}; + VkImageLayout m_HzbImageLayout{VK_IMAGE_LAYOUT_UNDEFINED}; + std::array m_MeshFrames{}; + std::vector m_ManagedTextureImages; +}; +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.cpp new file mode 100644 index 00000000..c1a613a9 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.cpp @@ -0,0 +1,274 @@ +#include "AxiomRHI/Vulkan/VulkanRhiDevice.h" + +#include "Renderer/Camera.h" +#include "Renderer/RenderScene.h" +#include "AxiomRHI/Vulkan/VulkanImage.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanMesh.h" + +#include "Core/Log.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +glm::vec3 TransformPoint(const glm::mat4 &Transform, const glm::vec3 &Point) { + return glm::vec3(Transform * glm::vec4(Point, 1.0f)); +} +} // namespace + +namespace Axiom { +void VulkanRhiDevice::Init(const RHIDeviceCreateInfo &CreateInfo) { + assert(CreateInfo.TargetSurface != nullptr); + + m_Surface = CreateInfo.TargetSurface; + m_HasPresentationSurface = m_Surface->SupportsPresentation(); + m_EnableImGui = m_HasPresentationSurface; + m_WindowExtent = {CreateInfo.Width, CreateInfo.Height}; + m_GpuResourceQueue = std::make_shared(); + + m_Context.Init(*m_Surface); + m_Device.Init(m_Context); + m_CommandContext.Init(m_Device.Device, m_Device.GraphicsQueueFamily); + m_OcclusionCulling.Init(m_Device.Device, m_Device.Allocator); + m_GraphicsQueue = std::make_unique(m_Device.GraphicsQueue, + RHIQueueType::Graphics); + m_ComputeQueue = std::make_unique(m_Device.GraphicsQueue, + RHIQueueType::Compute); + m_TransferQueue = std::make_unique(m_Device.TransferQueue, + RHIQueueType::Transfer); + + m_ResourceManager.Init({.Context = m_Context, + .Device = m_Device, + .WindowExtent = m_WindowExtent, + .HasPresentationSurface = m_HasPresentationSurface, + .AttachmentRequirements = + CreateInfo.AttachmentRequirements, + .SubmitTransferUpload = + [this](std::function &&Record, + std::function &&Cleanup) { + m_DrawSubmissionSystem.SubmitTransferUpload( + std::move(Record), std::move(Cleanup)); + }}); + + m_MaterialResources.Init( + {.Device = m_Device.Device, + .DescriptorAllocator = &m_ResourceManager.GetDescriptorAllocator(), + .MaterialDescriptorSetLayout = + m_ResourceManager.GetMeshGraphicsMaterialDescriptorLayout(), + .TextureSampler = m_ResourceManager.GetTextureSampler(), + .CreateTextureImage = [this](const TextureSourceData &TextureData) { + return m_ResourceManager.CreateManagedTextureImage(TextureData); + }}); + + m_PipelineLibrary.Init( + {.Device = m_Device.Device, + .DrawImageFormat = m_ResourceManager.GetDrawImage().ImageFormat, + .RasterDepthFormat = m_ResourceManager.GetRasterDepthImage().ImageFormat, + .DrawImageDescriptorLayout = + m_ResourceManager.GetDrawImageDescriptorLayout(), + .HzbReduceDescriptorLayout = + m_ResourceManager.GetHzbReduceDescriptorLayout(), + .MeshGraphicsFrameDescriptorLayout = + m_ResourceManager.GetMeshGraphicsFrameDescriptorLayout(), + .MeshGraphicsMaterialDescriptorLayout = + m_ResourceManager.GetMeshGraphicsMaterialDescriptorLayout(), + .MeshComputeFrameDescriptorLayout = + m_ResourceManager.GetMeshComputeFrameDescriptorLayout(), + .MeshDescriptorLayout = m_ResourceManager.GetMeshDescriptorLayout(), + .HDRSkyboxDescriptorLayout = + m_ResourceManager.GetHDRSkyboxDescriptorLayout()}); + + m_DrawSubmissionSystem.Init( + {.Surface = m_Surface.get(), + .Context = m_Context, + .Device = m_Device, + .CommandContext = m_CommandContext, + .Resources = m_ResourceManager, + .Pipelines = m_PipelineLibrary, + .MaterialResources = m_MaterialResources, + .OcclusionCulling = m_OcclusionCulling, + .EnableImGui = m_EnableImGui, + .HasPresentationSurface = m_HasPresentationSurface}); + + m_IsInitialized = true; + A_CORE_INFO("Vulkan Engine set up was successful: {0}", + m_IsInitialized ? "True" : "False"); +} + +void VulkanRhiDevice::Shutdown() { + A_CORE_INFO("Running Vulkan renderer cleanup..."); + if (!m_IsInitialized) { + return; + } + + vkDeviceWaitIdle(m_Device.Device); + m_DrawSubmissionSystem.Shutdown(); + m_MaterialResources.Shutdown(); + m_CommandContext.Shutdown(m_Device.Device); + m_TransferQueue.reset(); + m_ComputeQueue.reset(); + m_GraphicsQueue.reset(); + m_MeshesByHandle.clear(); + m_NextMeshHandleValue = 1; + m_GpuResourceQueue->Flush(); + m_GpuResourceQueue.reset(); + m_PipelineLibrary.Shutdown(); + m_ResourceManager.Shutdown(); + m_Device.Shutdown(); + m_Context.Shutdown(); + + m_IsInitialized = false; +} + +void VulkanRhiDevice::WaitIdle() { vkDeviceWaitIdle(m_Device.Device); } + +IRHIQueue *VulkanRhiDevice::GetQueue(RHIQueueType Type) { + switch (Type) { + case RHIQueueType::Graphics: + return m_GraphicsQueue.get(); + case RHIQueueType::Compute: + return m_ComputeQueue.get(); + case RHIQueueType::Transfer: + return m_TransferQueue.get(); + } + + return nullptr; +} + +std::unique_ptr +VulkanRhiDevice::CreateCommandList(RHIQueueType Type) { + uint32_t FrameIndex = static_cast(m_FrameNumber % FRAME_OVERLAP); + VkCommandPool CommandPool = m_CommandContext.GetFrame(FrameIndex).CommandPool; + VkCommandBufferAllocateInfo AllocateInfo = + VkInit::CommandBufferAllocateInfo(CommandPool, 1); + VkCommandBuffer CommandBuffer = VK_NULL_HANDLE; + VK_CHECK(vkAllocateCommandBuffers(m_Device.Device, &AllocateInfo, + &CommandBuffer)); + return std::make_unique(m_Device.Device, CommandPool, + CommandBuffer, Type, true); +} + +std::unique_ptr +VulkanRhiDevice::CreateBuffer(const RHIBufferDesc &Desc) { + return std::make_unique(Desc); +} + +std::unique_ptr +VulkanRhiDevice::CreateTexture(const RHITextureDesc &Desc) { + return std::make_unique(Desc); +} + +std::unique_ptr +VulkanRhiDevice::CreatePipeline(const RHIPipelineDesc &Desc) { + return std::make_unique(Desc.Type); +} + +std::unique_ptr VulkanRhiDevice::CreateDescriptorTable() { + return std::make_unique(); +} + +std::unique_ptr VulkanRhiDevice::CreateBindGroup() { + return std::make_unique(); +} + +std::unique_ptr VulkanRhiDevice::CreateSwapchain() { + return std::make_unique(); +} + +std::unique_ptr VulkanRhiDevice::CreateFence() { + VkFenceCreateInfo FenceCreateInfo = VkInit::FenceCreateInfo(0); + VkFence Fence = VK_NULL_HANDLE; + VK_CHECK(vkCreateFence(m_Device.Device, &FenceCreateInfo, VK_NULL_HANDLE, + &Fence)); + return std::make_unique(m_Device.Device, Fence, true); +} + +std::unique_ptr +VulkanRhiDevice::CreateSemaphore(bool Timeline) { + VkSemaphoreTypeCreateInfo TimelineInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, + .semaphoreType = + Timeline ? VK_SEMAPHORE_TYPE_TIMELINE : VK_SEMAPHORE_TYPE_BINARY, + .initialValue = 0, + }; + VkSemaphoreCreateInfo SemaphoreCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, + .pNext = Timeline ? &TimelineInfo : VK_NULL_HANDLE, + }; + VkSemaphore Semaphore = VK_NULL_HANDLE; + VK_CHECK(vkCreateSemaphore(m_Device.Device, &SemaphoreCreateInfo, + VK_NULL_HANDLE, &Semaphore)); + return std::make_unique(m_Device.Device, Semaphore, Timeline, + true); +} + +std::shared_ptr +VulkanRhiDevice::CreateMesh(const MeshData &MeshSource, + const MeshCreateOptions &Options) { + std::shared_ptr Mesh = VulkanMesh::Create( + MeshSource, m_Device.Allocator, m_Device.Device, m_Device.GraphicsQueue, + m_CommandContext.GetFrame(m_FrameNumber).CommandPool, + m_ResourceManager.GetDescriptorAllocator(), m_GpuResourceQueue, Options, + m_ResourceManager.GetMeshDescriptorLayout()); + if (Mesh == nullptr) { + return nullptr; + } + + const MeshHandle Handle = AllocateMeshHandle(); + Mesh->AssignHandle(Handle); + const auto [It, Inserted] = m_MeshesByHandle.emplace(Handle, Mesh); + assert(Inserted && "Allocated duplicate mesh handle"); + (void)It; + return Mesh; +} + +MaterialHandle +VulkanRhiDevice::CreateMaterialHandle(const MaterialInstance &Material) { + return m_MaterialResources.CreateMaterialHandle(Material); +} + +void VulkanRhiDevice::UpdateMaterialHandle( + MaterialHandle Handle, const MaterialInstance &Material) { + m_MaterialResources.UpdateMaterialHandle(Handle, Material); +} + +void VulkanRhiDevice::BeginFrame() {} + +MeshHandle VulkanRhiDevice::AllocateMeshHandle() { + MeshHandle Handle{m_NextMeshHandleValue++}; + assert(Handle.IsValid() && "Opaque mesh handle allocation overflowed"); + return Handle; +} + +VulkanMesh *VulkanRhiDevice::ResolveMeshHandle(MeshHandle Handle) const { + assert(Handle.IsValid() && "Render submission contained an invalid mesh handle"); + if (!Handle.IsValid()) { + return nullptr; + } + + const auto It = m_MeshesByHandle.find(Handle); + assert(It != m_MeshesByHandle.end() && + "Render submission referenced an unknown mesh handle"); + if (It == m_MeshesByHandle.end()) { + return nullptr; + } + + std::shared_ptr Mesh = It->second.lock(); + assert(Mesh != nullptr && + "Render submission referenced a stale mesh handle with no live mesh"); + return Mesh.get(); +} + +const MaterialInstance * +VulkanRhiDevice::ResolveMaterialHandle(MaterialHandle Handle) const { + return m_MaterialResources.ResolveMaterialHandle(Handle); +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.h new file mode 100644 index 00000000..305e5d8e --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiDevice.h @@ -0,0 +1,101 @@ +#pragma once + +#include "Renderer/RenderSurface.h" +#include "Renderer/RendererTypes.h" +#include "RHI/IRHI.h" +#include "AxiomRHI/Vulkan/GPUResourceQueue.h" +#include "AxiomRHI/Vulkan/VulkanCommandContext.h" +#include "AxiomRHI/Vulkan/VulkanContext.h" +#include "AxiomRHI/Vulkan/VulkanDevice.h" +#include "AxiomRHI/Vulkan/VulkanDrawSubmissionSystem.h" +#include "AxiomRHI/Vulkan/VulkanMaterialResources.h" +#include "AxiomRHI/Vulkan/VulkanOcclusionCulling.h" +#include "AxiomRHI/Vulkan/VulkanPipelineLibrary.h" +#include "AxiomRHI/Vulkan/VulkanResourceManager.h" +#include "AxiomRHI/Vulkan/VulkanRhiObjects.h" +#include "AxiomRHI/Vulkan/VulkanRendererTypes.h" + +#include +#include +#include +#include +#include + +namespace Axiom { +class VulkanMesh; + +class VulkanRhiDevice final : public IRHIDevice { +public: + void Init(const RHIDeviceCreateInfo &CreateInfo) override; + void Shutdown() override; + void BeginFrame() override; + void WaitIdle() override; + IRHIQueue *GetQueue(RHIQueueType Type) override; + std::unique_ptr CreateCommandList(RHIQueueType Type) override; + std::unique_ptr CreateBuffer(const RHIBufferDesc &Desc) override; + std::unique_ptr CreateTexture(const RHITextureDesc &Desc) override; + std::unique_ptr CreatePipeline(const RHIPipelineDesc &Desc) override; + std::unique_ptr CreateDescriptorTable() override; + std::unique_ptr CreateBindGroup() override; + std::unique_ptr CreateSwapchain() override; + std::unique_ptr CreateFence() override; + std::unique_ptr CreateSemaphore(bool Timeline) override; + + VulkanContext &GetContext() { return m_Context; } + VulkanDevice &GetVulkanDevice() { return m_Device; } + VulkanCommandContext &GetCommandContext() { return m_CommandContext; } + VulkanResourceManager &GetResourceManager() { return m_ResourceManager; } + VulkanPipelineLibrary &GetPipelineLibrary() { return m_PipelineLibrary; } + VulkanDrawSubmissionSystem &GetDrawSubmissionSystem() { + return m_DrawSubmissionSystem; + } + IRenderSurface &GetRenderSurface() { return *m_Surface; } + VulkanMaterialResources &GetMaterialResources() { + return m_MaterialResources; + } + VulkanOcclusionCulling &GetOcclusionCulling() { return m_OcclusionCulling; } + const std::shared_ptr &GetGpuResourceQueue() const { + return m_GpuResourceQueue; + } + bool HasPresentationSurface() const { return m_HasPresentationSurface; } + bool IsImGuiEnabled() const { return m_EnableImGui; } + VkExtent2D GetWindowExtent() const { return m_WindowExtent; } + uint64_t GetFrameNumber() const { return m_FrameNumber; } + void AdvanceFrame() { ++m_FrameNumber; } + + std::shared_ptr + CreateMesh(const MeshData &Mesh, const MeshCreateOptions &Options = {}); + MaterialHandle CreateMaterialHandle(const MaterialInstance &Material); + void UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material); + bool IsInitialized() const { return m_IsInitialized; } + MeshHandle AllocateMeshHandle(); + VulkanMesh *ResolveMeshHandle(MeshHandle Handle) const; + const MaterialInstance *ResolveMaterialHandle(MaterialHandle Handle) const; + +private: + bool m_IsInitialized{false}; + uint64_t m_FrameNumber{0}; + VkExtent2D m_WindowExtent{1700, 900}; + bool m_HasPresentationSurface{false}; + bool m_EnableImGui{true}; + + RenderSurfacePtr m_Surface; + + VulkanContext m_Context; + VulkanDevice m_Device; + VulkanCommandContext m_CommandContext; + VulkanResourceManager m_ResourceManager; + VulkanPipelineLibrary m_PipelineLibrary; + VulkanDrawSubmissionSystem m_DrawSubmissionSystem; + VulkanMaterialResources m_MaterialResources; + VulkanOcclusionCulling m_OcclusionCulling; + std::unique_ptr m_GraphicsQueue; + std::unique_ptr m_ComputeQueue; + std::unique_ptr m_TransferQueue; + std::shared_ptr m_GpuResourceQueue; + std::unordered_map, MeshHandleHash> + m_MeshesByHandle; + uint64_t m_NextMeshHandleValue{1}; +}; +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.cpp new file mode 100644 index 00000000..91f49950 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.cpp @@ -0,0 +1,135 @@ +#include "AxiomRHI/Vulkan/VulkanRhiObjects.h" + +#include +#include + +namespace Axiom { +VulkanCommandList::VulkanCommandList(VkDevice Device, VkCommandPool CommandPool, + VkCommandBuffer CommandBuffer, + RHIQueueType QueueType, + bool OwnsCommandBuffer) + : m_Device(Device), m_CommandPool(CommandPool), m_CommandBuffer(CommandBuffer), + m_QueueType(QueueType), m_OwnsCommandBuffer(OwnsCommandBuffer) {} + +VulkanCommandList::~VulkanCommandList() { + if (m_OwnsCommandBuffer && m_Device != VK_NULL_HANDLE && + m_CommandPool != VK_NULL_HANDLE && m_CommandBuffer != VK_NULL_HANDLE) { + vkFreeCommandBuffers(m_Device, m_CommandPool, 1, &m_CommandBuffer); + } +} + +void VulkanCommandList::Begin() { + VK_CHECK(vkResetCommandBuffer(m_CommandBuffer, 0)); + const VkCommandBufferBeginInfo BeginInfo = + VkInit::CommandBufferBeginInfo(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); + VK_CHECK(vkBeginCommandBuffer(m_CommandBuffer, &BeginInfo)); + m_IsRecording = true; +} + +void VulkanCommandList::End() { + VK_CHECK(vkEndCommandBuffer(m_CommandBuffer)); + m_IsRecording = false; +} + +VulkanFence::~VulkanFence() { + if (m_OwnsFence && m_Device != VK_NULL_HANDLE && m_Fence != VK_NULL_HANDLE) { + vkDestroyFence(m_Device, m_Fence, VK_NULL_HANDLE); + } +} + +VulkanSemaphore::~VulkanSemaphore() { + if (m_OwnsSemaphore && m_Device != VK_NULL_HANDLE && + m_Semaphore != VK_NULL_HANDLE) { + vkDestroySemaphore(m_Device, m_Semaphore, VK_NULL_HANDLE); + } +} + +VulkanQueue::VulkanQueue(VkQueue Queue, RHIQueueType Type) + : m_Queue(Queue), m_Type(Type) {} + +void VulkanQueue::Submit(IRHICommandList &CommandList, + std::span WaitSemaphores, + std::span SignalSemaphores, + IRHIFence *Fence) { + auto *VulkanCommand = dynamic_cast(&CommandList); + assert(VulkanCommand != nullptr && + "Vulkan queues require Vulkan command list submissions"); + if (VulkanCommand == nullptr) { + return; + } + + std::vector WaitInfos; + WaitInfos.reserve(WaitSemaphores.size()); + for (const RHIQueueWaitInfo &WaitInfo : WaitSemaphores) { + auto *Semaphore = dynamic_cast(WaitInfo.Semaphore); + assert(Semaphore != nullptr && + "Vulkan queues require Vulkan semaphore wait objects"); + if (Semaphore == nullptr) { + continue; + } + WaitInfos.push_back({ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = Semaphore->GetSemaphore(), + .value = WaitInfo.Value, + .stageMask = ToVulkanStage(WaitInfo.Stage), + }); + } + + std::vector SignalInfos; + SignalInfos.reserve(SignalSemaphores.size()); + for (const RHIQueueSignalInfo &SignalInfo : SignalSemaphores) { + auto *Semaphore = dynamic_cast(SignalInfo.Semaphore); + assert(Semaphore != nullptr && + "Vulkan queues require Vulkan semaphore signal objects"); + if (Semaphore == nullptr) { + continue; + } + SignalInfos.push_back({ + .sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, + .semaphore = Semaphore->GetSemaphore(), + .value = SignalInfo.Value, + .stageMask = ToVulkanStage(SignalInfo.Stage), + }); + } + + const VkCommandBufferSubmitInfo CommandInfo = + VkInit::CommandBufferSubmitInfo(VulkanCommand->GetCommandBuffer()); + const auto *VulkanFenceObject = + Fence != nullptr ? dynamic_cast(Fence) : nullptr; + const VkFence VulkanFenceHandle = + VulkanFenceObject != nullptr ? VulkanFenceObject->GetFence() + : VK_NULL_HANDLE; + + const VkSubmitInfo2 SubmitInfo{ + .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, + .waitSemaphoreInfoCount = static_cast(WaitInfos.size()), + .pWaitSemaphoreInfos = + WaitInfos.empty() ? VK_NULL_HANDLE : WaitInfos.data(), + .commandBufferInfoCount = 1, + .pCommandBufferInfos = &CommandInfo, + .signalSemaphoreInfoCount = static_cast(SignalInfos.size()), + .pSignalSemaphoreInfos = + SignalInfos.empty() ? VK_NULL_HANDLE : SignalInfos.data(), + }; + VK_CHECK(vkQueueSubmit2(m_Queue, 1, &SubmitInfo, VulkanFenceHandle)); +} + +VkPipelineStageFlags2 VulkanQueue::ToVulkanStage(RHICommandStage Stage) { + switch (Stage) { + case RHICommandStage::All: + return VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; + case RHICommandStage::Draw: + return VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | + VK_PIPELINE_STAGE_2_VERTEX_SHADER_BIT | + VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT; + case RHICommandStage::ColorAttachmentOutput: + return VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT; + case RHICommandStage::Compute: + return VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT; + case RHICommandStage::Transfer: + return VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT; + } + + return VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT; +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.h new file mode 100644 index 00000000..3ce837a7 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanRhiObjects.h @@ -0,0 +1,120 @@ +#pragma once + +#include "RHI/IRHI.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" + +#include + +namespace Axiom { +class VulkanCommandList final : public IRHICommandList { +public: + VulkanCommandList(VkDevice Device, VkCommandPool CommandPool, + VkCommandBuffer CommandBuffer, RHIQueueType QueueType, + bool OwnsCommandBuffer); + ~VulkanCommandList() override; + + void Begin() override; + void End() override; + bool IsRecording() const override { return m_IsRecording; } + RHIQueueType GetQueueType() const override { return m_QueueType; } + + VkCommandBuffer GetCommandBuffer() const { return m_CommandBuffer; } + +private: + VkDevice m_Device{VK_NULL_HANDLE}; + VkCommandPool m_CommandPool{VK_NULL_HANDLE}; + VkCommandBuffer m_CommandBuffer{VK_NULL_HANDLE}; + RHIQueueType m_QueueType{RHIQueueType::Graphics}; + bool m_OwnsCommandBuffer{false}; + bool m_IsRecording{false}; +}; + +class VulkanFence final : public IRHIFence { +public: + VulkanFence(VkDevice Device, VkFence Fence, bool OwnsFence) + : m_Device(Device), m_Fence(Fence), m_OwnsFence(OwnsFence) {} + ~VulkanFence() override; + + VkFence GetFence() const { return m_Fence; } + +private: + VkDevice m_Device{VK_NULL_HANDLE}; + VkFence m_Fence{VK_NULL_HANDLE}; + bool m_OwnsFence{false}; +}; + +class VulkanSemaphore final : public IRHISemaphore { +public: + VulkanSemaphore(VkDevice Device, VkSemaphore Semaphore, bool Timeline, + bool OwnsSemaphore) + : m_Device(Device), m_Semaphore(Semaphore), m_IsTimeline(Timeline), + m_OwnsSemaphore(OwnsSemaphore) {} + ~VulkanSemaphore() override; + + bool IsTimeline() const override { return m_IsTimeline; } + VkSemaphore GetSemaphore() const { return m_Semaphore; } + +private: + VkDevice m_Device{VK_NULL_HANDLE}; + VkSemaphore m_Semaphore{VK_NULL_HANDLE}; + bool m_IsTimeline{false}; + bool m_OwnsSemaphore{false}; +}; + +class VulkanQueue final : public IRHIQueue { +public: + VulkanQueue(VkQueue Queue, RHIQueueType Type); + + RHIQueueType GetType() const override { return m_Type; } + void Submit(IRHICommandList &CommandList, + std::span WaitSemaphores = {}, + std::span SignalSemaphores = {}, + IRHIFence *Fence = nullptr) override; + + VkQueue GetQueue() const { return m_Queue; } + +private: + static VkPipelineStageFlags2 ToVulkanStage(RHICommandStage Stage); + +private: + VkQueue m_Queue{VK_NULL_HANDLE}; + RHIQueueType m_Type{RHIQueueType::Graphics}; +}; + +class VulkanBufferHandle final : public IRHIBuffer { +public: + explicit VulkanBufferHandle(RHIBufferDesc Desc) : m_Desc(std::move(Desc)) {} + + const RHIBufferDesc &GetDesc() const override { return m_Desc; } + +private: + RHIBufferDesc m_Desc{}; +}; + +class VulkanTextureHandle final : public IRHITexture { +public: + explicit VulkanTextureHandle(RHITextureDesc Desc) : m_Desc(std::move(Desc)) {} + + const RHITextureDesc &GetDesc() const override { return m_Desc; } + +private: + RHITextureDesc m_Desc{}; +}; + +class VulkanPipelineHandle final : public IRHIPipeline { +public: + explicit VulkanPipelineHandle(RHIPipelineType Type) : m_Type(Type) {} + + RHIPipelineType GetType() const override { return m_Type; } + +private: + RHIPipelineType m_Type{RHIPipelineType::Graphics}; +}; + +class VulkanDescriptorTableHandle final : public IRHIDescriptorTable {}; + +class VulkanBindGroupHandle final : public IRHIBindGroup {}; + +class VulkanSwapchainHandle final : public IRHISwapchain {}; +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.cpp new file mode 100644 index 00000000..c5791aad --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.cpp @@ -0,0 +1,1032 @@ +#include "AxiomRHI/Vulkan/VulkanSceneRenderer.h" + +#include "Renderer/Camera.h" +#include "Renderer/RenderScene.h" +#include "AxiomRHI/Vulkan/VulkanImage.h" +#include "AxiomRHI/Vulkan/VulkanInitializers.h" +#include "AxiomRHI/Vulkan/VulkanMesh.h" +#include "AxiomRHI/Vulkan/VulkanRhiDevice.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { +glm::vec3 TransformPoint(const glm::mat4 &Transform, const glm::vec3 &Point) { + return glm::vec3(Transform * glm::vec4(Point, 1.0f)); +} +} // namespace + +namespace Axiom { +namespace { +VulkanRhiDevice *RequireVulkanDevice(IRHIDevice &Device) { + return dynamic_cast(&Device); +} +} // namespace + +void VulkanSceneRenderer::Init(IRHIDevice &Device, + const RendererCreateInfo &CreateInfo) { + m_Device = RequireVulkanDevice(Device); + if (m_Device != nullptr) { + m_FrameOutput = CreateInfo.FrameOutput; + m_Device->GetDrawSubmissionSystem().SetRecordPreparedScenePasses( + [this](VkCommandBuffer CommandBuffer, RenderScene &Scene, + uint64_t FrameNumber, RendererViewMode ViewMode) { + RecordPreparedScenePasses(CommandBuffer, Scene, FrameNumber, ViewMode); + }); + } +} + +void VulkanSceneRenderer::Shutdown() { + if (m_Device != nullptr) { + m_Device->GetDrawSubmissionSystem().SetRecordPreparedScenePasses({}); + } + m_Device = nullptr; + m_FrameOutput = nullptr; + m_ActiveScene = nullptr; + m_StopRendering = false; + m_RenderFallbackBackground = false; + m_PreparedSceneState = {}; + m_CandidateScratch.clear(); + m_QueuedScenePasses.clear(); + m_SceneDrawImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + m_SceneRasterDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED; +} + +void VulkanSceneRenderer::BeginFrame() { + if (m_Device == nullptr) { + return; + } + + m_StopRendering = m_Device->HasPresentationSurface() && + m_Device->GetRenderSurface().IsMinimized(); + m_RenderFallbackBackground = false; + m_ActiveScene = nullptr; + ResetPreparedSceneState(); + m_QueuedScenePasses.clear(); + m_SceneDrawImageLayout = VK_IMAGE_LAYOUT_UNDEFINED; + m_SceneRasterDepthLayout = VK_IMAGE_LAYOUT_UNDEFINED; + if (m_StopRendering) { + return; + } + + RendererFrameStats &FrameStats = AccessFrameStats(); + FrameStats.SubmittedMeshCount = 0; + FrameStats.FrustumCulledMeshCount = 0; + FrameStats.OcclusionCulledMeshCount = 0; + FrameStats.MeshSubmissionCount = 0; + FrameStats.TriangleCount = 0; +#if !defined(NDEBUG) + FrameStats.DebugGraphicsMaterialDescriptorUpdates = 0; + FrameStats.DebugOpaqueMaterialDescriptorBinds = 0; + FrameStats.DebugTranslucentMaterialDescriptorBinds = 0; + FrameStats.DebugOpaqueUniqueMaterialCount = 0; + FrameStats.DebugTranslucentUniqueMaterialCount = 0; + m_Device->GetMaterialResources().ResetDebugCounters(); +#endif + m_Device->GetDrawSubmissionSystem().BeginFrame(m_StopRendering); +} + +std::shared_ptr +VulkanSceneRenderer::CreateMesh(const MeshData &Mesh, + const MeshCreateOptions &Options) { + return m_Device != nullptr ? m_Device->CreateMesh(Mesh, Options) : nullptr; +} + +MaterialHandle +VulkanSceneRenderer::CreateMaterialHandle(const MaterialInstance &Material) { + return m_Device != nullptr ? m_Device->CreateMaterialHandle(Material) + : MaterialHandle{}; +} + +void VulkanSceneRenderer::UpdateMaterialHandle( + MaterialHandle Handle, const MaterialInstance &Material) { + if (m_Device != nullptr) { + m_Device->UpdateMaterialHandle(Handle, Material); + } +} + +void VulkanSceneRenderer::Render(RenderScene &Scene) { + if (m_Device == nullptr) { + return; + } + + if (!Scene.Submissions.empty()) { + PrepareSceneFrame(Scene); + RecordBackground(); + RecordDepthPrepass(); + BuildHzb(); + RecordComputeMeshPath(); + RecordOpaqueForward(); + RecordTranslucentForward(); + FinalizeSceneFrame(); + return; + } + + RenderFallbackBackground(Scene); +} + +void VulkanSceneRenderer::RenderImGui() { + if (m_Device != nullptr) { + m_Device->GetDrawSubmissionSystem().RenderImGui(m_StopRendering, m_ViewMode); + } +} + +void VulkanSceneRenderer::EndFrame() { + if (m_Device == nullptr || m_StopRendering) { + return; + } + + m_Device->GetDrawSubmissionSystem().DrawFrame( + {.FrameNumber = m_Device->GetFrameNumber(), + .ActiveScene = m_ActiveScene, + .ViewMode = m_ViewMode, + .ViewportFrameUser = m_ViewportFrameUser, + .FrameOutput = m_FrameOutput}); + m_Device->AdvanceFrame(); +} + +void VulkanSceneRenderer::SetViewMode(RendererViewMode ViewMode) { + m_ViewMode = ViewMode; +} + +void VulkanSceneRenderer::SetViewportFrameUser(SessionUserId User) { + m_ViewportFrameUser = User; +} + +void VulkanSceneRenderer::SetViewportFrameOutput( + IViewportFrameOutput *FrameOutput) { + m_FrameOutput = FrameOutput; +} + +std::optional VulkanSceneRenderer::ConsumeCapturedFrame() { + return m_Device != nullptr ? m_Device->GetDrawSubmissionSystem().ConsumeCapturedFrame() + : std::nullopt; +} + +RendererFrameStats &VulkanSceneRenderer::AccessFrameStats() { + return m_Device->GetDrawSubmissionSystem().AccessFrameStats(); +} + +const RendererFrameStats &VulkanSceneRenderer::GetFrameStats() const { + return m_Device->GetDrawSubmissionSystem().GetFrameStats(); +} + +void VulkanSceneRenderer::PrepareSceneFrame(RenderScene &Scene) { + m_ActiveScene = &Scene; + ResetPreparedSceneState(); + m_QueuedScenePasses.clear(); + m_PreparedSceneState.Scene = &Scene; + if (Scene.ActiveCamera == nullptr) { + return; + } + + RendererFrameStats &FrameStats = AccessFrameStats(); + const size_t SubmissionCount = Scene.Submissions.size(); + + m_PreparedSceneState.ForceWireframe = m_ViewMode == RendererViewMode::Wireframe; + m_PreparedSceneState.CameraData = BuildCameraData(Scene, m_ViewMode); + m_PreparedSceneState.HasPreparedCamera = true; + + auto &ResourceManager = m_Device->GetResourceManager(); + MeshFrameResources &Frame = ResourceManager.GetMeshFrame(m_Device->GetFrameNumber()); + std::memcpy(Frame.CameraBuffer.Info.pMappedData, &m_PreparedSceneState.CameraData, + sizeof(CameraFrameUniform)); + UpdateComputeFrameDescriptors(Frame); + UpdateDepthFrameDescriptors(Frame); + UpdateGraphicsFrameDescriptors(Frame); + + FrameStats.SubmittedMeshCount = static_cast(SubmissionCount); + FrameStats.FrustumCulledMeshCount = 0; + FrameStats.OcclusionCulledMeshCount = 0; + FrameStats.MeshSubmissionCount = 0; + FrameStats.TriangleCount = 0; + + auto &Candidates = m_CandidateScratch; + auto &VisibleSubmissions = m_PreparedSceneState.VisibleSubmissions; + Candidates.clear(); + VisibleSubmissions.Clear(); + Candidates.reserve(SubmissionCount); + VisibleSubmissions.OpaqueGraphics.reserve(SubmissionCount); + VisibleSubmissions.TranslucentGraphics.reserve(SubmissionCount); + VisibleSubmissions.Compute.reserve(SubmissionCount); + + for (size_t Index = 0; Index < SubmissionCount; ++Index) { + const auto &Submission = Scene.Submissions[Index]; + VulkanMesh *VulkanMeshRef = m_Device->ResolveMeshHandle(Submission.MeshHandle); + if (VulkanMeshRef == nullptr) { + continue; + } + + if (!m_PreparedSceneState.ForceWireframe && + !m_Device->GetOcclusionCulling().IsBoundsVisible( + m_PreparedSceneState.CameraData.ViewProjection, Submission.Transform, + VulkanMeshRef->BoundsMin, VulkanMeshRef->BoundsMax)) { + ++FrameStats.FrustumCulledMeshCount; + continue; + } + + const glm::vec3 WorldCenter = ComputeWorldCenter(Submission, *VulkanMeshRef); + const glm::vec3 Delta = WorldCenter - Scene.ActiveCamera->GetPosition(); + Candidates.push_back({.SubmissionIndex = static_cast(Index), + .MeshHandle = Submission.MeshHandle, + .Mesh = VulkanMeshRef, + .SortDepth = glm::dot(Delta, Delta)}); + } + + if (!m_PreparedSceneState.ForceWireframe) { + std::sort(Candidates.begin(), Candidates.end(), + [](const CandidateSubmission &Left, + const CandidateSubmission &Right) { + return Left.SortDepth < Right.SortDepth; + }); + } + + const MeshFrameResources *PreviousOcclusionFrame = + m_PreparedSceneState.ForceWireframe + ? nullptr + : m_Device->GetOcclusionCulling().GetPreviousOcclusionFrame( + m_Device->GetCommandContext(), ResourceManager.GetMeshFrames(), + m_Device->GetFrameNumber()); + bool UseOcclusion = false; + if (PreviousOcclusionFrame != nullptr) { + vmaInvalidateAllocation(m_Device->GetVulkanDevice().Allocator, + PreviousOcclusionFrame->HzbReadbackBuffer.Allocation, 0, + VK_WHOLE_SIZE); + UseOcclusion = m_Device->GetOcclusionCulling().ShouldUsePreviousOcclusionData( + *PreviousOcclusionFrame, m_PreparedSceneState.CameraData); + } + + for (const CandidateSubmission &Candidate : Candidates) { + if (UseOcclusion && + m_Device->GetOcclusionCulling().IsOccludedByPreviousFrame( + *PreviousOcclusionFrame, GetSubmission(Candidate.SubmissionIndex).Transform, + Candidate.Mesh->BoundsMin, Candidate.Mesh->BoundsMax, + ResourceManager.GetHzbMipExtents(), ResourceManager.GetHzbMipOffsets())) { + ++FrameStats.OcclusionCulledMeshCount; + continue; + } + + VisibleSubmission Visible{ + .SubmissionIndex = Candidate.SubmissionIndex, + .MeshHandle = Candidate.MeshHandle, + .SortDepth = Candidate.SortDepth, + }; + const RenderMeshSubmission &Submission = GetSubmission(Candidate.SubmissionIndex); + if (!m_PreparedSceneState.ForceWireframe && + Submission.RenderPath == MeshRenderPath::Compute) { + VisibleSubmissions.Compute.push_back(Visible); + } else if (Submission.Translucent) { + VisibleSubmissions.TranslucentGraphics.push_back(Visible); + } else { + VisibleSubmissions.OpaqueGraphics.push_back(Visible); + } + + ++FrameStats.MeshSubmissionCount; + FrameStats.TriangleCount += Candidate.Mesh->TriangleCount; + } + + std::sort(VisibleSubmissions.OpaqueGraphics.begin(), + VisibleSubmissions.OpaqueGraphics.end(), + [this](const VisibleSubmission &Left, const VisibleSubmission &Right) { + const MaterialInstance *LeftMaterial = + m_Device->ResolveMaterialHandle( + GetSubmission(Left.SubmissionIndex).MaterialHandle); + const MaterialInstance *RightMaterial = + m_Device->ResolveMaterialHandle( + GetSubmission(Right.SubmissionIndex).MaterialHandle); + if (LeftMaterial != RightMaterial) { + return LeftMaterial < RightMaterial; + } + return Left.SubmissionIndex < Right.SubmissionIndex; + }); + + PrepareGraphicsMaterialDescriptors(); +} + +void VulkanSceneRenderer::RecordBackground() { + QueueScenePass(ScenePassPrimitive::Background); +} + +void VulkanSceneRenderer::RecordDepthPrepass() { + QueueScenePass(ScenePassPrimitive::DepthPrepass); +} + +void VulkanSceneRenderer::RecordComputeMeshPath() { + QueueScenePass(ScenePassPrimitive::ComputeMeshPath); +} + +void VulkanSceneRenderer::BuildHzb() { QueueScenePass(ScenePassPrimitive::Hzb); } + +void VulkanSceneRenderer::RecordOpaqueForward() { + QueueScenePass(ScenePassPrimitive::OpaqueForward); +} + +void VulkanSceneRenderer::RecordTranslucentForward() { + QueueScenePass(ScenePassPrimitive::TranslucentForward); +} + +void VulkanSceneRenderer::FinalizeSceneFrame() { + m_PreparedSceneState.HasQueuedFinalize = true; +} + +void VulkanSceneRenderer::RenderFallbackBackground(RenderScene &Scene) { + m_ActiveScene = &Scene; + m_RenderFallbackBackground = true; + ResetPreparedSceneState(); + m_QueuedScenePasses.clear(); + RecordBackground(); + FinalizeSceneFrame(); +} + +void VulkanSceneRenderer::RecordPreparedScenePasses( + VkCommandBuffer CommandBuffer, RenderScene &Scene, uint64_t FrameNumber, + RendererViewMode ViewMode) { + m_ActiveScene = &Scene; + m_ViewMode = ViewMode; + m_SceneDrawImageLayout = VK_IMAGE_LAYOUT_GENERAL; + m_SceneRasterDepthLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; + + if (m_QueuedScenePasses.empty() && m_RenderFallbackBackground) { + RecordBackground(); + } + + MeshFrameResources &Frame = m_Device->GetResourceManager().GetMeshFrame(FrameNumber); + for (const ScenePassPrimitive Pass : m_QueuedScenePasses) { + switch (Pass) { + case ScenePassPrimitive::Background: + EnsureDrawImageLayout(CommandBuffer, VK_IMAGE_LAYOUT_GENERAL); + DrawBackgroundPass(CommandBuffer, m_ActiveScene); + break; + case ScenePassPrimitive::DepthPrepass: + EnsureRasterDepthLayout(CommandBuffer, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + RecordDepthPrepassPass(CommandBuffer, Frame); + break; + case ScenePassPrimitive::Hzb: + EnsureRasterDepthLayout(CommandBuffer, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + BuildHzbPass(CommandBuffer, Frame); + break; + case ScenePassPrimitive::ComputeMeshPath: + EnsureRasterDepthLayout(CommandBuffer, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL); + EnsureDrawImageLayout(CommandBuffer, VK_IMAGE_LAYOUT_GENERAL); + RecordComputeMeshPathPass(CommandBuffer, Frame); + break; + case ScenePassPrimitive::OpaqueForward: + EnsureRasterDepthLayout(CommandBuffer, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + EnsureDrawImageLayout(CommandBuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + RecordOpaqueForwardPass(CommandBuffer, Frame); + break; + case ScenePassPrimitive::TranslucentForward: + EnsureRasterDepthLayout(CommandBuffer, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + EnsureDrawImageLayout(CommandBuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + RecordTranslucentForwardPass(CommandBuffer, Frame); + break; + } + } +} + +void VulkanSceneRenderer::DrawBackgroundPass(VkCommandBuffer CommandBuffer, + RenderScene *Scene) { + auto &ResourceManager = m_Device->GetResourceManager(); + ResourceManager.SyncHDRSkyboxTexture( + Scene ? Scene->SkyboxHDRTexture : nullptr, + m_Device->GetCommandContext().GetFrame(m_Device->GetFrameNumber())); + + const VkExtent2D DrawExtent = GetDrawExtent2D(); + const bool UseHDR = + ResourceManager.HasHDRSkyboxTexture() && + ResourceManager.GetHDRSkyboxDescriptorSet() != VK_NULL_HANDLE && + Scene != nullptr && Scene->ActiveCamera != nullptr && + !Scene->ActiveCamera->IsOrthographic(); + + if (UseHDR) { + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetHDRSkyboxPipeline()); + const std::array Sets = { + ResourceManager.GetDrawImageDescriptorSet(), + ResourceManager.GetHDRSkyboxDescriptorSet()}; + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetHDRSkyboxPipelineLayout(), 0, + static_cast(Sets.size()), Sets.data(), 0, VK_NULL_HANDLE); + + const glm::mat4 InverseViewProj = + glm::inverse(Scene->ActiveCamera->GetViewProjectionMatrix()); + vkCmdPushConstants( + CommandBuffer, m_Device->GetPipelineLibrary().GetHDRSkyboxPipelineLayout(), + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(glm::mat4), + glm::value_ptr(InverseViewProj)); + vkCmdDispatch(CommandBuffer, + static_cast(std::ceil(DrawExtent.width / 16.0f)), + static_cast(std::ceil(DrawExtent.height / 16.0f)), 1); + return; + } + + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetGradientPipeline()); + const VkDescriptorSet DrawImageDescriptorSet = + ResourceManager.GetDrawImageDescriptorSet(); + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetGradientPipelineLayout(), 0, 1, + &DrawImageDescriptorSet, 0, VK_NULL_HANDLE); + + ComputePushConstants PC; + if (Scene != nullptr) { + PC.data1 = glm::vec4(Scene->SkyboxColorTop, 1.0f); + PC.data2 = glm::vec4(Scene->SkyboxColorBottom, 1.0f); + } else { + PC.data1 = glm::vec4(0.08f, 0.09f, 0.14f, 1.0f); + PC.data2 = glm::vec4(0.14f, 0.24f, 0.38f, 1.0f); + } + + vkCmdPushConstants(CommandBuffer, + m_Device->GetPipelineLibrary().GetGradientPipelineLayout(), + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PC), &PC); + vkCmdDispatch(CommandBuffer, + static_cast(std::ceil(DrawExtent.width / 16.0f)), + static_cast(std::ceil(DrawExtent.height / 16.0f)), 1); +} + +void VulkanSceneRenderer::BuildHzbPass(VkCommandBuffer CommandBuffer, + MeshFrameResources &Frame) { + if (CommandBuffer == VK_NULL_HANDLE) { + QueueScenePass(ScenePassPrimitive::Hzb); + return; + } + + m_Device->GetDrawSubmissionSystem().BuildHzb(CommandBuffer, Frame); + if (m_PreparedSceneState.HasPreparedCamera) { + Frame.HzbViewProjection = m_PreparedSceneState.CameraData.ViewProjection; + Frame.HzbViewportSize = glm::vec2(m_PreparedSceneState.CameraData.ViewportSize); + } + m_SceneRasterDepthLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; +} + +void VulkanSceneRenderer::QueueScenePass(ScenePassPrimitive Pass) { + m_QueuedScenePasses.push_back(Pass); +} + +void VulkanSceneRenderer::ResetPreparedSceneState() { + m_PreparedSceneState = {}; + m_PreparedSceneState.VisibleSubmissions.Clear(); +} + +glm::vec3 VulkanSceneRenderer::ComputeWorldCenter( + const RenderMeshSubmission &Submission, const VulkanMesh &Mesh) const { + const glm::vec3 LocalCenter = (Mesh.BoundsMin + Mesh.BoundsMax) * 0.5f; + return TransformPoint(Submission.Transform, LocalCenter); +} + +CameraFrameUniform +VulkanSceneRenderer::BuildCameraData(const RenderScene &Scene, + RendererViewMode ViewMode) const { + auto &Camera = *Scene.ActiveCamera; + + CameraFrameUniform CameraData{}; + CameraData.View = Camera.GetViewMatrix(); + CameraData.Projection = Camera.GetProjectionMatrix(); + CameraData.ViewProjection = Camera.GetViewProjectionMatrix(); + CameraData.CameraPosition = glm::vec4(Camera.GetPosition(), 1.0f); + const VkExtent2D DrawExtent = GetDrawExtent2D(); + CameraData.ViewportSize = glm::vec4(static_cast(DrawExtent.width), + static_cast(DrawExtent.height), 0.0f, + 0.0f); + CameraData.RenderOptions.x = static_cast(ViewMode); + + if (Scene.Sun.has_value()) { + const auto &Sun = *Scene.Sun; + const glm::vec3 Dir = glm::normalize(Sun.Direction); + CameraData.LightDirectionAndIntensity = glm::vec4(Dir, Sun.Intensity); + CameraData.LightColorAndEnabled = glm::vec4(Sun.Color, 1.0f); + } + + return CameraData; +} + +void VulkanSceneRenderer::UpdateComputeFrameDescriptors( + const MeshFrameResources &Frame) const { + auto &ResourceManager = m_Device->GetResourceManager(); + VkDescriptorBufferInfo CameraBufferInfo = + VkInit::BufferInfo(Frame.CameraBuffer.Buffer, 0, Frame.CameraBuffer.Size); + VkDescriptorBufferInfo MutableCameraBufferInfo = CameraBufferInfo; + VkDescriptorImageInfo ColorImageInfo{}; + ColorImageInfo.imageView = ResourceManager.GetDrawImage().ImageView; + ColorImageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + + VkDescriptorImageInfo DepthImageInfo{}; + DepthImageInfo.sampler = ResourceManager.GetLinearDepthSampler(); + DepthImageInfo.imageView = ResourceManager.GetRasterDepthImage().ImageView; + DepthImageInfo.imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL; + + std::array ComputeFrameWrites = { + VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + Frame.ComputeFrameDescriptorSet, &ColorImageInfo, 0), + VkInit::WriteDescriptorSet(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + Frame.ComputeFrameDescriptorSet, &DepthImageInfo, 1), + VkInit::WriteDescriptorBuffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + Frame.ComputeFrameDescriptorSet, + &MutableCameraBufferInfo, 2)}; + vkUpdateDescriptorSets(m_Device->GetVulkanDevice().Device, + static_cast(ComputeFrameWrites.size()), + ComputeFrameWrites.data(), 0, VK_NULL_HANDLE); +} + +void VulkanSceneRenderer::UpdateDepthFrameDescriptors( + const MeshFrameResources &Frame) const { + VkDescriptorBufferInfo CameraBufferInfo = + VkInit::BufferInfo(Frame.CameraBuffer.Buffer, 0, Frame.CameraBuffer.Size); + VkDescriptorBufferInfo MutableCameraBufferInfo = CameraBufferInfo; + std::array DepthFrameWrites = { + VkInit::WriteDescriptorBuffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + Frame.DepthFrameDescriptorSet, + &MutableCameraBufferInfo, 0)}; + vkUpdateDescriptorSets(m_Device->GetVulkanDevice().Device, + static_cast(DepthFrameWrites.size()), + DepthFrameWrites.data(), 0, VK_NULL_HANDLE); +} + +void VulkanSceneRenderer::UpdateGraphicsFrameDescriptors( + const MeshFrameResources &Frame) const { + VkDescriptorBufferInfo CameraBufferInfo = + VkInit::BufferInfo(Frame.CameraBuffer.Buffer, 0, Frame.CameraBuffer.Size); + VkDescriptorBufferInfo MutableCameraBufferInfo = CameraBufferInfo; + std::array GraphicsFrameWrites = { + VkInit::WriteDescriptorBuffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + Frame.GraphicsFrameDescriptorSet, + &MutableCameraBufferInfo, 0)}; + vkUpdateDescriptorSets(m_Device->GetVulkanDevice().Device, + static_cast(GraphicsFrameWrites.size()), + GraphicsFrameWrites.data(), 0, VK_NULL_HANDLE); +} + +void VulkanSceneRenderer::PrepareGraphicsMaterialDescriptors() { +#if !defined(NDEBUG) + auto &FrameStats = AccessFrameStats(); + std::unordered_set PreparedMaterials; + std::unordered_set OpaqueMaterials; + std::unordered_set TranslucentMaterials; + PreparedMaterials.reserve( + m_PreparedSceneState.VisibleSubmissions.OpaqueGraphics.size() + + m_PreparedSceneState.VisibleSubmissions.TranslucentGraphics.size()); + OpaqueMaterials.reserve( + m_PreparedSceneState.VisibleSubmissions.OpaqueGraphics.size()); + TranslucentMaterials.reserve( + m_PreparedSceneState.VisibleSubmissions.TranslucentGraphics.size()); + + auto PrepareSubmissionMaterial = [this, &PreparedMaterials]( + const VisibleSubmission &Visible) { + const MaterialInstance *Material = m_Device->ResolveMaterialHandle( + GetSubmission(Visible.SubmissionIndex).MaterialHandle); + if (!PreparedMaterials.insert(Material).second) { + return; + } + m_Device->GetMaterialResources().ResolveMaterialDescriptorSet(Material); + }; + + auto CountMaterialRuns = [this](const auto &Submissions) { + uint32_t Runs = 0; + const MaterialInstance *PreviousMaterial = nullptr; + for (const VisibleSubmission &Visible : Submissions) { + const MaterialInstance *CurrentMaterial = m_Device->ResolveMaterialHandle( + GetSubmission(Visible.SubmissionIndex).MaterialHandle); + if (CurrentMaterial != PreviousMaterial) { + ++Runs; + PreviousMaterial = CurrentMaterial; + } + } + return Runs; + }; + + for (const VisibleSubmission &Visible : + m_PreparedSceneState.VisibleSubmissions.OpaqueGraphics) { + const MaterialInstance *Material = m_Device->ResolveMaterialHandle( + GetSubmission(Visible.SubmissionIndex).MaterialHandle); + OpaqueMaterials.insert(Material); + PrepareSubmissionMaterial(Visible); + } + + auto SortedTranslucentSubmissions = + m_PreparedSceneState.VisibleSubmissions.TranslucentGraphics; + std::sort(SortedTranslucentSubmissions.begin(), + SortedTranslucentSubmissions.end(), + [](const VisibleSubmission &Left, const VisibleSubmission &Right) { + return Left.SortDepth > Right.SortDepth; + }); + for (const VisibleSubmission &Visible : SortedTranslucentSubmissions) { + TranslucentMaterials.insert(m_Device->ResolveMaterialHandle( + GetSubmission(Visible.SubmissionIndex).MaterialHandle)); + PrepareSubmissionMaterial(Visible); + } + + FrameStats.DebugGraphicsMaterialDescriptorUpdates = + m_Device->GetMaterialResources().GetDebugGraphicsMaterialDescriptorUpdates(); + FrameStats.DebugOpaqueMaterialDescriptorBinds = + CountMaterialRuns(m_PreparedSceneState.VisibleSubmissions.OpaqueGraphics); + FrameStats.DebugTranslucentMaterialDescriptorBinds = + CountMaterialRuns(SortedTranslucentSubmissions); + FrameStats.DebugOpaqueUniqueMaterialCount = + static_cast(OpaqueMaterials.size()); + FrameStats.DebugTranslucentUniqueMaterialCount = + static_cast(TranslucentMaterials.size()); +#endif +} + +void VulkanSceneRenderer::RecordDepthPrepassPass( + VkCommandBuffer CommandBuffer, const MeshFrameResources &Frame) const { + const auto &OpaqueGraphicsSubmissions = + m_PreparedSceneState.VisibleSubmissions.OpaqueGraphics; + const auto &ComputeSubmissions = m_PreparedSceneState.VisibleSubmissions.Compute; + if (OpaqueGraphicsSubmissions.empty() && ComputeSubmissions.empty()) { + return; + } + + const VkExtent2D DrawExtent = GetDrawExtent2D(); + VkViewport Viewport{0.0f, 0.0f, static_cast(DrawExtent.width), + static_cast(DrawExtent.height), 0.0f, 1.0f}; + VkRect2D Scissor{{0, 0}, DrawExtent}; + + VkRenderingAttachmentInfo DepthOnlyAttachment = VkInit::DepthAttachmentInfo( + m_Device->GetResourceManager().GetRasterDepthImage().ImageView, + VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL); + VkRenderingInfo DepthOnlyRenderingInfo{ + .sType = VK_STRUCTURE_TYPE_RENDERING_INFO, + .pNext = VK_NULL_HANDLE, + .renderArea = VkRect2D{VkOffset2D{0, 0}, DrawExtent}, + .layerCount = 1, + .colorAttachmentCount = 0, + .pColorAttachments = VK_NULL_HANDLE, + .pDepthAttachment = &DepthOnlyAttachment, + .pStencilAttachment = VK_NULL_HANDLE}; + + vkCmdBeginRendering(CommandBuffer, &DepthOnlyRenderingInfo); + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_Device->GetPipelineLibrary().GetMeshDepthPipeline()); + vkCmdSetViewport(CommandBuffer, 0, 1, &Viewport); + vkCmdSetScissor(CommandBuffer, 0, 1, &Scissor); + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_Device->GetPipelineLibrary().GetMeshDepthPipelineLayout(), 0, 1, + &Frame.DepthFrameDescriptorSet, 0, VK_NULL_HANDLE); + + auto RecordSubmission = [&](const VisibleSubmission &Visible) { + const RenderMeshSubmission &Submission = GetSubmission(Visible.SubmissionIndex); + VulkanMesh *Mesh = ResolveVisibleMesh(Visible); + if (Mesh == nullptr) { + return; + } + + MeshGraphicsPushConstants PushConstants{}; + PushConstants.Model = Submission.Transform; + vkCmdPushConstants( + CommandBuffer, m_Device->GetPipelineLibrary().GetMeshDepthPipelineLayout(), + VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(MeshGraphicsPushConstants), + &PushConstants); + BindMeshBuffers(CommandBuffer, *Mesh); + vkCmdDrawIndexed(CommandBuffer, Mesh->IndexCount, 1, 0, 0, 0); + }; + + for (const VisibleSubmission &Visible : OpaqueGraphicsSubmissions) { + RecordSubmission(Visible); + } + for (const VisibleSubmission &Visible : ComputeSubmissions) { + RecordSubmission(Visible); + } + + vkCmdEndRendering(CommandBuffer); +} + +void VulkanSceneRenderer::RecordComputeMeshPathPass( + VkCommandBuffer CommandBuffer, const MeshFrameResources &Frame) const { + for (const VisibleSubmission &Visible : + m_PreparedSceneState.VisibleSubmissions.Compute) { + VulkanMesh *Mesh = ResolveVisibleMesh(Visible); + if (Mesh == nullptr) { + continue; + } + + std::array DescriptorSets = { + Frame.ComputeFrameDescriptorSet, Mesh->DescriptorSet}; + + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetMeshProjectPipeline()); + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetMeshProjectPipelineLayout(), 0, + static_cast(DescriptorSets.size()), DescriptorSets.data(), 0, + VK_NULL_HANDLE); + + MeshProjectPushConstants ProjectPushConstants{}; + ProjectPushConstants.Model = GetSubmission(Visible.SubmissionIndex).Transform; + ProjectPushConstants.Counts.x = Mesh->VertexCount; + vkCmdPushConstants( + CommandBuffer, m_Device->GetPipelineLibrary().GetMeshProjectPipelineLayout(), + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(MeshProjectPushConstants), + &ProjectPushConstants); + + const uint32_t VertexGroupCount = std::max(1u, (Mesh->VertexCount + 63u) / 64u); + vkCmdDispatch(CommandBuffer, VertexGroupCount, 1, 1); + + VkBufferMemoryBarrier2 ProjectedVertexBarrier{ + .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, + .pNext = VK_NULL_HANDLE, + .srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .buffer = Mesh->ProjectedVertexBuffer.Buffer, + .offset = 0, + .size = Mesh->ProjectedVertexBuffer.Size}; + VkDependencyInfo ProjectDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .pNext = VK_NULL_HANDLE, + .bufferMemoryBarrierCount = 1, + .pBufferMemoryBarriers = &ProjectedVertexBarrier}; + vkCmdPipelineBarrier2(CommandBuffer, &ProjectDependencyInfo); + + vkCmdBindPipeline(CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetMeshPipeline()); + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, + m_Device->GetPipelineLibrary().GetMeshPipelineLayout(), 0, + static_cast(DescriptorSets.size()), DescriptorSets.data(), 0, + VK_NULL_HANDLE); + + MeshRasterPushConstants RasterPushConstants{}; + RasterPushConstants.Counts.x = Mesh->TriangleCount; + vkCmdPushConstants( + CommandBuffer, m_Device->GetPipelineLibrary().GetMeshPipelineLayout(), + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(MeshRasterPushConstants), + &RasterPushConstants); + + const uint32_t GroupCount = std::max(1u, (Mesh->TriangleCount + 63u) / 64u); + vkCmdDispatch(CommandBuffer, GroupCount, 1, 1); + + VkImageMemoryBarrier2 DrawImageBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .pNext = VK_NULL_HANDLE, + .srcStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .srcAccessMask = VK_ACCESS_2_SHADER_WRITE_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, + .dstAccessMask = + VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = m_Device->GetResourceManager().GetDrawImage().Image, + .subresourceRange = + VkInit::ImageSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT)}; + VkDependencyInfo ComputeDependencyInfo{ + .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, + .pNext = VK_NULL_HANDLE, + .imageMemoryBarrierCount = 1, + .pImageMemoryBarriers = &DrawImageBarrier}; + vkCmdPipelineBarrier2(CommandBuffer, &ComputeDependencyInfo); + } +} + +void VulkanSceneRenderer::RecordOpaqueForwardPass( + VkCommandBuffer CommandBuffer, const MeshFrameResources &Frame) { + const auto &GraphicsSubmissions = + m_PreparedSceneState.VisibleSubmissions.OpaqueGraphics; + if (GraphicsSubmissions.empty()) { + return; + } + + const VkExtent2D DrawExtent = GetDrawExtent2D(); + VkViewport Viewport{0.0f, 0.0f, static_cast(DrawExtent.width), + static_cast(DrawExtent.height), 0.0f, 1.0f}; + VkRect2D Scissor{{0, 0}, DrawExtent}; + + VkRenderingAttachmentInfo ColorAttachment = VkInit::AttachmentInfo( + m_Device->GetResourceManager().GetDrawImage().ImageView, VK_NULL_HANDLE, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + VkRenderingAttachmentInfo DepthAttachment{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .pNext = VK_NULL_HANDLE, + .imageView = m_Device->GetResourceManager().GetRasterDepthImage().ImageView, + .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE}; + VkRenderingInfo RenderingInfo = + VkInit::RenderingInfo(DrawExtent, &ColorAttachment, &DepthAttachment); + + vkCmdBeginRendering(CommandBuffer, &RenderingInfo); + vkCmdBindPipeline( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_PreparedSceneState.ForceWireframe + ? m_Device->GetPipelineLibrary().GetMeshWireframePipeline() + : m_Device->GetPipelineLibrary().GetMeshGraphicsPipeline()); + vkCmdSetViewport(CommandBuffer, 0, 1, &Viewport); + vkCmdSetScissor(CommandBuffer, 0, 1, &Scissor); + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_Device->GetPipelineLibrary().GetMeshGraphicsPipelineLayout(), 0, 1, + &Frame.GraphicsFrameDescriptorSet, 0, VK_NULL_HANDLE); + + VkDescriptorSet BoundMaterialDescriptorSet = VK_NULL_HANDLE; +#if !defined(NDEBUG) + uint32_t MaterialDescriptorBindCount = 0; +#endif + for (const VisibleSubmission &Visible : GraphicsSubmissions) { + const RenderMeshSubmission &Submission = GetSubmission(Visible.SubmissionIndex); + VulkanMesh *Mesh = ResolveVisibleMesh(Visible); + if (Mesh == nullptr) { + continue; + } + + const VkDescriptorSet MaterialDescriptorSet = + m_Device->GetMaterialResources().ResolveMaterialDescriptorSet( + m_Device->ResolveMaterialHandle(Submission.MaterialHandle)); + if (MaterialDescriptorSet != BoundMaterialDescriptorSet) { + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_Device->GetPipelineLibrary().GetMeshGraphicsPipelineLayout(), 1, 1, + &MaterialDescriptorSet, 0, VK_NULL_HANDLE); + BoundMaterialDescriptorSet = MaterialDescriptorSet; +#if !defined(NDEBUG) + ++MaterialDescriptorBindCount; +#endif + } + MeshGraphicsPushConstants PushConstants{}; + PushConstants.Model = Submission.Transform; + if (const MaterialInstance *Material = + m_Device->ResolveMaterialHandle(Submission.MaterialHandle); + Material != nullptr) { + PushConstants.BaseColorFactor = Material->BaseColorFactor; + PushConstants.Metallic = Material->Metallic; + PushConstants.Roughness = Material->Roughness; + } + vkCmdPushConstants( + CommandBuffer, m_Device->GetPipelineLibrary().GetMeshGraphicsPipelineLayout(), + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(MeshGraphicsPushConstants), &PushConstants); + BindMeshBuffers(CommandBuffer, *Mesh); + vkCmdDrawIndexed(CommandBuffer, Mesh->IndexCount, 1, 0, 0, 0); + } + + vkCmdEndRendering(CommandBuffer); + +#if !defined(NDEBUG) + AccessFrameStats().DebugGraphicsMaterialDescriptorUpdates = + m_Device->GetMaterialResources().GetDebugGraphicsMaterialDescriptorUpdates(); + AccessFrameStats().DebugOpaqueMaterialDescriptorBinds = + MaterialDescriptorBindCount; +#endif +} + +void VulkanSceneRenderer::RecordTranslucentForwardPass( + VkCommandBuffer CommandBuffer, const MeshFrameResources &Frame) { + auto &GraphicsSubmissions = + m_PreparedSceneState.VisibleSubmissions.TranslucentGraphics; + if (GraphicsSubmissions.empty()) { + return; + } + + std::sort(GraphicsSubmissions.begin(), GraphicsSubmissions.end(), + [](const VisibleSubmission &Left, const VisibleSubmission &Right) { + return Left.SortDepth > Right.SortDepth; + }); + + const VkExtent2D DrawExtent = GetDrawExtent2D(); + VkViewport Viewport{0.0f, 0.0f, static_cast(DrawExtent.width), + static_cast(DrawExtent.height), 0.0f, 1.0f}; + VkRect2D Scissor{{0, 0}, DrawExtent}; + + VkRenderingAttachmentInfo ColorAttachment = VkInit::AttachmentInfo( + m_Device->GetResourceManager().GetDrawImage().ImageView, VK_NULL_HANDLE, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + VkRenderingAttachmentInfo DepthAttachment{ + .sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, + .pNext = VK_NULL_HANDLE, + .imageView = m_Device->GetResourceManager().GetRasterDepthImage().ImageView, + .imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, + .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE}; + VkRenderingInfo RenderingInfo = + VkInit::RenderingInfo(DrawExtent, &ColorAttachment, &DepthAttachment); + + vkCmdBeginRendering(CommandBuffer, &RenderingInfo); + vkCmdBindPipeline( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_PreparedSceneState.ForceWireframe + ? m_Device->GetPipelineLibrary().GetMeshWireframePipeline() + : m_Device->GetPipelineLibrary().GetMeshGraphicsAlphaBlendPipeline()); + vkCmdSetViewport(CommandBuffer, 0, 1, &Viewport); + vkCmdSetScissor(CommandBuffer, 0, 1, &Scissor); + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_Device->GetPipelineLibrary().GetMeshGraphicsPipelineLayout(), 0, 1, + &Frame.GraphicsFrameDescriptorSet, 0, VK_NULL_HANDLE); + + VkDescriptorSet BoundMaterialDescriptorSet = VK_NULL_HANDLE; +#if !defined(NDEBUG) + uint32_t MaterialDescriptorBindCount = 0; +#endif + for (const VisibleSubmission &Visible : GraphicsSubmissions) { + const RenderMeshSubmission &Submission = GetSubmission(Visible.SubmissionIndex); + VulkanMesh *Mesh = ResolveVisibleMesh(Visible); + if (Mesh == nullptr) { + continue; + } + + const VkDescriptorSet MaterialDescriptorSet = + m_Device->GetMaterialResources().ResolveMaterialDescriptorSet( + m_Device->ResolveMaterialHandle(Submission.MaterialHandle)); + if (MaterialDescriptorSet != BoundMaterialDescriptorSet) { + vkCmdBindDescriptorSets( + CommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_Device->GetPipelineLibrary().GetMeshGraphicsPipelineLayout(), 1, 1, + &MaterialDescriptorSet, 0, VK_NULL_HANDLE); + BoundMaterialDescriptorSet = MaterialDescriptorSet; +#if !defined(NDEBUG) + ++MaterialDescriptorBindCount; +#endif + } + MeshGraphicsPushConstants PushConstants{}; + PushConstants.Model = Submission.Transform; + if (const MaterialInstance *Material = + m_Device->ResolveMaterialHandle(Submission.MaterialHandle); + Material != nullptr) { + PushConstants.BaseColorFactor = Material->BaseColorFactor; + PushConstants.Metallic = Material->Metallic; + PushConstants.Roughness = Material->Roughness; + } + vkCmdPushConstants( + CommandBuffer, m_Device->GetPipelineLibrary().GetMeshGraphicsPipelineLayout(), + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, + sizeof(MeshGraphicsPushConstants), &PushConstants); + BindMeshBuffers(CommandBuffer, *Mesh); + vkCmdDrawIndexed(CommandBuffer, Mesh->IndexCount, 1, 0, 0, 0); + } + + vkCmdEndRendering(CommandBuffer); + +#if !defined(NDEBUG) + AccessFrameStats().DebugGraphicsMaterialDescriptorUpdates = + m_Device->GetMaterialResources().GetDebugGraphicsMaterialDescriptorUpdates(); + AccessFrameStats().DebugTranslucentMaterialDescriptorBinds = + MaterialDescriptorBindCount; +#endif +} + +void VulkanSceneRenderer::EnsureDrawImageLayout( + VkCommandBuffer CommandBuffer, VkImageLayout DesiredLayout) { + if (m_SceneDrawImageLayout == DesiredLayout) { + return; + } + + VkUtil::TransitionImage(CommandBuffer, + m_Device->GetResourceManager().GetDrawImage().Image, + m_SceneDrawImageLayout, DesiredLayout); + m_SceneDrawImageLayout = DesiredLayout; +} + +void VulkanSceneRenderer::EnsureRasterDepthLayout( + VkCommandBuffer CommandBuffer, VkImageLayout DesiredLayout) { + if (m_SceneRasterDepthLayout == DesiredLayout) { + return; + } + + VkUtil::TransitionImage(CommandBuffer, + m_Device->GetResourceManager().GetRasterDepthImage().Image, + m_SceneRasterDepthLayout, DesiredLayout); + m_SceneRasterDepthLayout = DesiredLayout; +} + +void VulkanSceneRenderer::BindMeshBuffers(VkCommandBuffer CommandBuffer, + const VulkanMesh &Mesh) const { + VkDeviceSize VertexOffset = 0; + vkCmdBindVertexBuffers(CommandBuffer, 0, 1, &Mesh.VertexBuffer.Buffer, + &VertexOffset); + vkCmdBindIndexBuffer(CommandBuffer, Mesh.IndexBuffer.Buffer, 0, + VK_INDEX_TYPE_UINT32); +} + +const RenderMeshSubmission & +VulkanSceneRenderer::GetSubmission(uint32_t SubmissionIndex) const { + assert(m_PreparedSceneState.Scene != nullptr); + return m_PreparedSceneState.Scene->Submissions[SubmissionIndex]; +} + +VulkanMesh * +VulkanSceneRenderer::ResolveVisibleMesh(const VisibleSubmission &Visible) const { + return m_Device->ResolveMeshHandle(Visible.MeshHandle); +} + +VkExtent2D VulkanSceneRenderer::GetDrawExtent2D() const { + const VkExtent3D Extent3D = + m_Device->GetResourceManager().GetDrawImage().ImageExtent; + return {.width = Extent3D.width, .height = Extent3D.height}; +} +} // namespace Axiom diff --git a/AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.h new file mode 100644 index 00000000..735aa1d5 --- /dev/null +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanSceneRenderer.h @@ -0,0 +1,114 @@ +#pragma once + +#include "AxiomRHI/SceneRendererBackendFactory.h" +#include "AxiomRHI/Vulkan/VulkanRendererTypes.h" + +#include +#include + +namespace Axiom { +class VulkanRhiDevice; +class VulkanMesh; + +class VulkanSceneRenderer final : public ISceneRendererBackend { +public: + void Init(IRHIDevice &Device, const RendererCreateInfo &CreateInfo) override; + void Shutdown() override; + void BeginFrame() override; + std::shared_ptr + CreateMesh(const MeshData &Mesh, const MeshCreateOptions &Options) override; + MaterialHandle CreateMaterialHandle(const MaterialInstance &Material) override; + void UpdateMaterialHandle(MaterialHandle Handle, + const MaterialInstance &Material) override; + void Render(RenderScene &Scene) override; + void RenderImGui() override; + void EndFrame() override; + void SetViewMode(RendererViewMode ViewMode) override; + void SetViewportFrameUser(SessionUserId User) override; + void SetViewportFrameOutput(IViewportFrameOutput *FrameOutput) override; + std::optional ConsumeCapturedFrame() override; + RendererFrameStats &AccessFrameStats() override; + const RendererFrameStats &GetFrameStats() const override; + void RecordPreparedScenePasses(VkCommandBuffer CommandBuffer, RenderScene &Scene, + uint64_t FrameNumber, + RendererViewMode ViewMode); + +private: + enum class ScenePassPrimitive { + Background, + DepthPrepass, + Hzb, + ComputeMeshPath, + OpaqueForward, + TranslucentForward, + }; + + struct CandidateSubmission { + uint32_t SubmissionIndex{0}; + MeshHandle MeshHandle{}; + VulkanMesh *Mesh{nullptr}; + float SortDepth{0.0f}; + }; + + struct PreparedSceneState { + RenderScene *Scene{nullptr}; + CameraFrameUniform CameraData{}; + VisibleSubmissionList VisibleSubmissions; + bool ForceWireframe{false}; + bool HasPreparedCamera{false}; + bool HasQueuedFinalize{false}; + }; + + void PrepareSceneFrame(RenderScene &Scene); + void RecordBackground(); + void RecordDepthPrepass(); + void BuildHzb(); + void RecordComputeMeshPath(); + void RecordOpaqueForward(); + void RecordTranslucentForward(); + void FinalizeSceneFrame(); + void RenderFallbackBackground(RenderScene &Scene); + void DrawBackgroundPass(VkCommandBuffer CommandBuffer, RenderScene *Scene); + void BuildHzbPass(VkCommandBuffer CommandBuffer, MeshFrameResources &Frame); + void QueueScenePass(ScenePassPrimitive Pass); + void ResetPreparedSceneState(); + glm::vec3 ComputeWorldCenter(const RenderMeshSubmission &Submission, + const VulkanMesh &Mesh) const; + CameraFrameUniform BuildCameraData(const RenderScene &Scene, + RendererViewMode ViewMode) const; + void UpdateComputeFrameDescriptors(const MeshFrameResources &Frame) const; + void UpdateDepthFrameDescriptors(const MeshFrameResources &Frame) const; + void UpdateGraphicsFrameDescriptors(const MeshFrameResources &Frame) const; + void PrepareGraphicsMaterialDescriptors(); + void RecordDepthPrepassPass(VkCommandBuffer CommandBuffer, + const MeshFrameResources &Frame) const; + void RecordComputeMeshPathPass(VkCommandBuffer CommandBuffer, + const MeshFrameResources &Frame) const; + void RecordOpaqueForwardPass(VkCommandBuffer CommandBuffer, + const MeshFrameResources &Frame); + void RecordTranslucentForwardPass(VkCommandBuffer CommandBuffer, + const MeshFrameResources &Frame); + void EnsureDrawImageLayout(VkCommandBuffer CommandBuffer, + VkImageLayout DesiredLayout); + void EnsureRasterDepthLayout(VkCommandBuffer CommandBuffer, + VkImageLayout DesiredLayout); + void BindMeshBuffers(VkCommandBuffer CommandBuffer, const VulkanMesh &Mesh) const; + const RenderMeshSubmission &GetSubmission(uint32_t SubmissionIndex) const; + VulkanMesh *ResolveVisibleMesh(const VisibleSubmission &Visible) const; + VkExtent2D GetDrawExtent2D() const; + +private: + VulkanRhiDevice *m_Device{nullptr}; + bool m_StopRendering{false}; + bool m_RenderFallbackBackground{false}; + RenderScene *m_ActiveScene{nullptr}; + RendererViewMode m_ViewMode{RendererViewMode::Lit}; + SessionUserId m_ViewportFrameUser{}; + IViewportFrameOutput *m_FrameOutput{nullptr}; + PreparedSceneState m_PreparedSceneState{}; + std::vector m_CandidateScratch; + std::vector m_QueuedScenePasses; + VkImageLayout m_SceneDrawImageLayout{VK_IMAGE_LAYOUT_UNDEFINED}; + VkImageLayout m_SceneRasterDepthLayout{VK_IMAGE_LAYOUT_UNDEFINED}; +}; +} // namespace Axiom diff --git a/Axiom/Renderer/Vulkan/VulkanStringUtils.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanStringUtils.h similarity index 100% rename from Axiom/Renderer/Vulkan/VulkanStringUtils.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanStringUtils.h diff --git a/Axiom/Renderer/Vulkan/VulkanSwapchain.cpp b/AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.cpp similarity index 94% rename from Axiom/Renderer/Vulkan/VulkanSwapchain.cpp rename to AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.cpp index ff277cf6..f547f1de 100644 --- a/Axiom/Renderer/Vulkan/VulkanSwapchain.cpp +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.cpp @@ -1,7 +1,7 @@ -#include "Renderer/Vulkan/VulkanSwapchain.h" +#include "AxiomRHI/Vulkan/VulkanSwapchain.h" -#include "Renderer/Vulkan/VulkanContext.h" -#include "Renderer/Vulkan/VulkanDevice.h" +#include "AxiomRHI/Vulkan/VulkanContext.h" +#include "AxiomRHI/Vulkan/VulkanDevice.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanSwapchain.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.h similarity index 93% rename from Axiom/Renderer/Vulkan/VulkanSwapchain.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.h index 6527a1ea..cb976c3c 100644 --- a/Axiom/Renderer/Vulkan/VulkanSwapchain.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanSwapchain.h @@ -1,6 +1,6 @@ #pragma once -#include "Renderer/Vulkan/VulkanTypes.h" +#include "AxiomRHI/Vulkan/VulkanTypes.h" #include diff --git a/Axiom/Renderer/Vulkan/VulkanTypes.h b/AxiomInternal/AxiomRHI/Vulkan/VulkanTypes.h similarity index 96% rename from Axiom/Renderer/Vulkan/VulkanTypes.h rename to AxiomInternal/AxiomRHI/Vulkan/VulkanTypes.h index c100e269..3d728e9b 100644 --- a/Axiom/Renderer/Vulkan/VulkanTypes.h +++ b/AxiomInternal/AxiomRHI/Vulkan/VulkanTypes.h @@ -16,7 +16,7 @@ #include #include "Core/Log.h" -#include "Renderer/Vulkan/VulkanStringUtils.h" +#include "AxiomRHI/Vulkan/VulkanStringUtils.h" #define VK_CHECK(x) \ do { \ diff --git a/CMakeLists.txt b/CMakeLists.txt index 285c5957..b2e02f93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,10 @@ option(AXIOM_ENABLE_WEBRTC "Enable the macOS WebRTC transport integration seam" OFF) option(AXIOM_ENABLE_PHYSICS "Enable the JoltPhysics runtime simulation seam" ON) +option(AXIOM_THREADED_RENDER + "Enable the experimental threaded renderer and worker job system" OFF) +option(AXIOM_ENABLE_TSAN + "Build with ThreadSanitizer instrumentation" OFF) set(AXIOM_WEBRTC_FRAMEWORK_PATH "" CACHE PATH "Optional path to a locally built WebRTC.framework for macOS WebRTC integration") set(AXIOM_WEBRTC_LIBRARY_PATH "" CACHE FILEPATH @@ -30,6 +34,16 @@ add_subdirectory(ThirdParty/fastgltf) add_subdirectory(ThirdParty/glm/glm) include(FetchContent) +FetchContent_Declare( + rapidjson + URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.zip + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_GetProperties(rapidjson) +if(NOT rapidjson_POPULATED) + FetchContent_Populate(rapidjson) +endif() + FetchContent_Declare( assimp URL https://github.com/assimp/assimp/archive/refs/tags/v5.4.3.zip diff --git a/Content/Cooked/AssetCookManifest.json b/Content/Cooked/AssetCookManifest.json index 0b48e9c1..a33eb60c 100644 --- a/Content/Cooked/AssetCookManifest.json +++ b/Content/Cooked/AssetCookManifest.json @@ -1,211 +1,412 @@ { "entries": [ - {"assetId":15229175004894892839,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__101","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__101.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12140336635836207652,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__101","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__101.wmat","formatVersion":1,"sourceHash":3586948860202070916}, - {"assetId":16670649961235633850,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__102","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__102.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":5457201860838636531,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__102","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__102.wmat","formatVersion":1,"sourceHash":6145656221699350966}, - {"assetId":18212675864156437195,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__81","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__81.wmat","formatVersion":1,"sourceHash":10338313927153405416}, - {"assetId":16222585198370233502,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__82","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__82.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6341542026414383172,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__82","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__82.wmat","formatVersion":1,"sourceHash":10985747164797825642}, - {"assetId":6276292986914166204,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__83","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__83.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":13913950202207721753,"kind":"mesh","relativePath":"sponza_atrium_3.glb","cookedPath":"Cooked/sponza_atrium_3.wmesh","formatVersion":2,"sourceHash":13152113367551948137}, - {"assetId":3524126900194724051,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__84","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__84.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1368142407168599961,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__84","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__84.wmat","formatVersion":1,"sourceHash":1573590478991570099}, - {"assetId":8379936892125882364,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__85","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__85.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17615425434727149779,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__85","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__85.wmat","formatVersion":1,"sourceHash":15021837972009510944}, - {"assetId":5370529907106783966,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__86","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__86.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":3307378166964972138,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__86","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__86.wmat","formatVersion":1,"sourceHash":16414512736408610493}, - {"assetId":11435685726260663774,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__87","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__87.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1601912636468888724,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__87","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__87.wmat","formatVersion":1,"sourceHash":2836131640602896348}, - {"assetId":1914623039437072610,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__88","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__88.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6896494829003407116,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__88","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__88.wmat","formatVersion":1,"sourceHash":3677809703873982172}, - {"assetId":4113860169957569099,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__89","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__89.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6410752746425559445,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__89","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__89.wmat","formatVersion":1,"sourceHash":794871690607076599}, - {"assetId":16292226204569430461,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__90","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__90.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12546584266307313070,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__90","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__90.wmat","formatVersion":1,"sourceHash":17304015300532786011}, - {"assetId":1161113871088196371,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__91","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__91.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":13140657335267704776,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__91","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__91.wmat","formatVersion":1,"sourceHash":10993084337399955276}, - {"assetId":2153231978189338291,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__92","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__92.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":7574761956857896106,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__92","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__92.wmat","formatVersion":1,"sourceHash":15347192330358828181}, - {"assetId":14593766337526508589,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__93","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__93.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12588103244075146374,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__93","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__93.wmat","formatVersion":1,"sourceHash":11879179696935072141}, - {"assetId":6530629928095013992,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__94","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__94.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":15112736952776741626,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__94","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__94.wmat","formatVersion":1,"sourceHash":7690582110283420096}, - {"assetId":18081150503426162446,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__95","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__95.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1994779334733726980,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__95","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__95.wmat","formatVersion":1,"sourceHash":3660925423700951973}, - {"assetId":10480157717321104301,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__96","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__96.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":4087356689680254175,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__96","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__96.wmat","formatVersion":1,"sourceHash":4973025491254526861}, - {"assetId":3901345578842322117,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__97","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__97.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":2419053644206384395,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__97","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__97.wmat","formatVersion":1,"sourceHash":15030832607098163724}, - {"assetId":5743276401868917762,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__98","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__98.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":10793103097143920867,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__98","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__98.wmat","formatVersion":1,"sourceHash":11605956222055713613}, - {"assetId":17840664353554127551,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__99","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__99.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6429394278380673583,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__99","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__99.wmat","formatVersion":1,"sourceHash":521088494192120068}, - {"assetId":3968184785818211594,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__100","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__100.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":15000008075878416251,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__100","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__100.wmat","formatVersion":1,"sourceHash":14785741697486435488}, - {"assetId":8949008766911302915,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__0","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__0.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6406691723554709426,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__0","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__0.wmat","formatVersion":1,"sourceHash":12105348316115371814}, - {"assetId":2047440963454411317,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__1","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__1.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":338408498823845738,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__1","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__1.wmat","formatVersion":1,"sourceHash":11937650793961544448}, - {"assetId":2609070077096360103,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__2","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__2.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":2082388146543210735,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__2","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__2.wmat","formatVersion":1,"sourceHash":16410775011811095777}, - {"assetId":12144441902023958388,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__3","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__3.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17527947539543232506,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__3","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__3.wmat","formatVersion":1,"sourceHash":3416732856032483136}, - {"assetId":14935625733198014204,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__4","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__4.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":2917609430187082044,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__4","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__4.wmat","formatVersion":1,"sourceHash":2122964563376366127}, - {"assetId":8000510243179873760,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__5","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__5.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1833647933708458171,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__5","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__5.wmat","formatVersion":1,"sourceHash":431772422916233084}, - {"assetId":15108022691734276197,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__6","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__6.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":13270350426872169045,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__6","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__6.wmat","formatVersion":1,"sourceHash":1613934549528476018}, - {"assetId":15426782677142257150,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__7","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__7.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":11133723940075140816,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__7","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__7.wmat","formatVersion":1,"sourceHash":1920503734990514352}, - {"assetId":935055086319286039,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__8","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__8.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9878873497959639324,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__8","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__8.wmat","formatVersion":1,"sourceHash":17514446031319777689}, - {"assetId":6989572634075200210,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__9","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__9.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":15772495388093143928,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__9","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__9.wmat","formatVersion":1,"sourceHash":14637193827494394025}, - {"assetId":9599674849738750772,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__10","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__10.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":2896645638468082733,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__10","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__10.wmat","formatVersion":1,"sourceHash":17362250718079553547}, - {"assetId":14616201166244300419,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__11","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__11.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":5182902489092553603,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__11","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__11.wmat","formatVersion":1,"sourceHash":14699062389568748940}, - {"assetId":13318269828404021151,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__12","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__12.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1381354933644706323,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__12","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__12.wmat","formatVersion":1,"sourceHash":2067503673473807703}, - {"assetId":418041667898929635,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__13","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__13.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":7335716352758067924,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__13","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__13.wmat","formatVersion":1,"sourceHash":3443891746728836002}, - {"assetId":11460860277751759961,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__14","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__14.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":10303325347291496521,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__14","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__14.wmat","formatVersion":1,"sourceHash":11468274840462287699}, - {"assetId":57118869757405117,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__15","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__15.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":4318461462746769796,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__15","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__15.wmat","formatVersion":1,"sourceHash":1988328698587556133}, - {"assetId":5070985410898534162,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__16","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__16.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":13980165142181086485,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__16","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__16.wmat","formatVersion":1,"sourceHash":4937683144631245473}, - {"assetId":536335246703651284,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__17","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__17.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":7387032219278094511,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__17","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__17.wmat","formatVersion":1,"sourceHash":2852633945460346414}, - {"assetId":15686379171003262801,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__18","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__18.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":8720650190583489158,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__18","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__18.wmat","formatVersion":1,"sourceHash":15463314947407387625}, - {"assetId":1412843259362631608,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__19","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__19.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":5685564747107029388,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__19","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__19.wmat","formatVersion":1,"sourceHash":9445146940921157142}, - {"assetId":9192472894437067434,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__20","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__20.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6894865326422565929,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__20","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__20.wmat","formatVersion":1,"sourceHash":14062335024188840592}, - {"assetId":6639126766486840204,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__21","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__21.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9141098151863153504,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__21","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__21.wmat","formatVersion":1,"sourceHash":7625158974550489513}, - {"assetId":16584590773156486040,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__22","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__22.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":11986908085456801312,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__22","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__22.wmat","formatVersion":1,"sourceHash":9746980511802920792}, - {"assetId":16912923655458696876,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__23","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__23.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":75998186172291003,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__23","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__23.wmat","formatVersion":1,"sourceHash":3761010161796409630}, - {"assetId":16916866212321513280,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__24","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__24.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12353471030465543246,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__24","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__24.wmat","formatVersion":1,"sourceHash":9327000674952958699}, - {"assetId":2374162152453047656,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__25","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__25.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":10315655477725643010,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__25","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__25.wmat","formatVersion":1,"sourceHash":13800637962277353069}, - {"assetId":6231828774228086012,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__26","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__26.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1489882246957632078,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__26","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__26.wmat","formatVersion":1,"sourceHash":1914889437746598852}, - {"assetId":14835781405174517290,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__27","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__27.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":15777561656369557976,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__27","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__27.wmat","formatVersion":1,"sourceHash":3741971524521780286}, - {"assetId":12365111798863810594,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__28","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__28.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1487016160038041592,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__28","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__28.wmat","formatVersion":1,"sourceHash":16847151654528284207}, - {"assetId":4816640343485796455,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__29","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__29.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":5736582587947684968,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__29","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__29.wmat","formatVersion":1,"sourceHash":3110976458674466213}, - {"assetId":2335489473499343915,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__30","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__30.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9309213505028262097,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__30","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__30.wmat","formatVersion":1,"sourceHash":6855991421574206476}, - {"assetId":14294178196105835203,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__31","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__31.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17061688190034129669,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__31","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__31.wmat","formatVersion":1,"sourceHash":11800299738552449015}, - {"assetId":11849761624884880228,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__32","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__32.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":4251901682895947253,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__32","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__32.wmat","formatVersion":1,"sourceHash":926952403814070686}, - {"assetId":11968440148746543500,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__33","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__33.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12421151130816866460,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__33","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__33.wmat","formatVersion":1,"sourceHash":13849620051997181063}, - {"assetId":9413999610432788887,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__34","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__34.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":13377664948097222989,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__34","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__34.wmat","formatVersion":1,"sourceHash":6745908713292237972}, - {"assetId":3206050910767936484,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__35","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__35.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":884509795584781669,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__35","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__35.wmat","formatVersion":1,"sourceHash":13770668752275295155}, - {"assetId":6560012162369303788,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__36","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__36.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":4560151726280134650,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__36","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__36.wmat","formatVersion":1,"sourceHash":8466891692858494164}, - {"assetId":639270548772163740,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__37","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__37.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17922609879183352666,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__37","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__37.wmat","formatVersion":1,"sourceHash":4184171774617991775}, - {"assetId":17555800433981341095,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__38","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__38.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":10100035789257866383,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__38","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__38.wmat","formatVersion":1,"sourceHash":16606266445749377162}, - {"assetId":11420437435638758274,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__39","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__39.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":4050580276738303960,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__39","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__39.wmat","formatVersion":1,"sourceHash":15102528717865983540}, - {"assetId":11306607703883254382,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__40","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__40.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":8858843042684904766,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__40","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__40.wmat","formatVersion":1,"sourceHash":13741552318892389879}, - {"assetId":12460855100054038388,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__41","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__41.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9631852698186271261,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__41","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__41.wmat","formatVersion":1,"sourceHash":588546811845668699}, - {"assetId":3254376880908665827,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__42","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__42.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":16203075113385007226,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__42","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__42.wmat","formatVersion":1,"sourceHash":6413442546387661798}, - {"assetId":9013002783490148122,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__43","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__43.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6530180347153082594,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__43","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__43.wmat","formatVersion":1,"sourceHash":10492985162459293085}, - {"assetId":3548548975956987037,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__44","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__44.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":929704761341936123,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__44","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__44.wmat","formatVersion":1,"sourceHash":775071552424960229}, - {"assetId":12949756596101087740,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__45","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__45.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12493647735195381543,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__45","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__45.wmat","formatVersion":1,"sourceHash":9139502168056263364}, - {"assetId":7668103232490655480,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__46","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__46.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":3516490016846387337,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__46","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__46.wmat","formatVersion":1,"sourceHash":7930211813925214123}, - {"assetId":7760219078526813804,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__47","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__47.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9233051403786368865,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__47","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__47.wmat","formatVersion":1,"sourceHash":13377665289041716221}, - {"assetId":952881265951366713,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__48","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__48.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17259416390395138510,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__48","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__48.wmat","formatVersion":1,"sourceHash":10128547069958724922}, - {"assetId":9958318104475448220,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__49","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__49.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":11108836736635882916,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__49","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__49.wmat","formatVersion":1,"sourceHash":14622359694189615963}, - {"assetId":15478161406022928727,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__50","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__50.wtex","formatVersion":2,"sourceHash":13834509895381492195}, - {"assetId":9089107162248191790,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__50","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__50.wmat","formatVersion":1,"sourceHash":13483825194474164463}, - {"assetId":3072345739054193766,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__51","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__51.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17465508359896384865,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__51","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__51.wmat","formatVersion":1,"sourceHash":16978121369089822570}, - {"assetId":7268203633730872137,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__52","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__52.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":3244402830141628456,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__52","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__52.wmat","formatVersion":1,"sourceHash":5353180470586327978}, - {"assetId":12748113197778075258,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__53","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__53.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":16306434343215284634,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__53","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__53.wmat","formatVersion":1,"sourceHash":7447515478019148808}, - {"assetId":4519313381700649372,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__54","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__54.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":998295677570452009,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__54","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__54.wmat","formatVersion":1,"sourceHash":12355589836418080625}, - {"assetId":157030763324087685,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__55","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__55.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":5789132488689622967,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__55","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__55.wmat","formatVersion":1,"sourceHash":16892589328850450973}, - {"assetId":9166456867916392735,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__56","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__56.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":18054864403564059647,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__56","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__56.wmat","formatVersion":1,"sourceHash":5829632433907177469}, - {"assetId":11006535976202166325,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__57","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__57.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":5730670853493317779,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__57","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__57.wmat","formatVersion":1,"sourceHash":13505299453285165148}, - {"assetId":12129955814755625773,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__58","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__58.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1732882884677843463,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__58","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__58.wmat","formatVersion":1,"sourceHash":538972629376551351}, - {"assetId":8327526667649131927,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__59","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__59.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":16064301680766859049,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__59","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__59.wmat","formatVersion":1,"sourceHash":528110843448922099}, - {"assetId":1904484066334468700,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__60","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__60.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":3045438415191816439,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__60","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__60.wmat","formatVersion":1,"sourceHash":9108640508261734035}, - {"assetId":7736993864853341099,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__61","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__61.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":16314920960340782341,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__61","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__61.wmat","formatVersion":1,"sourceHash":915934276284787436}, - {"assetId":5383388308933643833,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__62","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__62.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6923143004171991324,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__62","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__62.wmat","formatVersion":1,"sourceHash":3482299694309499429}, - {"assetId":8428097613701717203,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__63","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__63.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":11883505219019110422,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__63","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__63.wmat","formatVersion":1,"sourceHash":6832124729437712248}, - {"assetId":16919978094734787135,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__64","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__64.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":7493961902870723016,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__64","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__64.wmat","formatVersion":1,"sourceHash":17964375059061294257}, - {"assetId":11978365854394189157,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__65","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__65.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9703569224552357746,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__65","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__65.wmat","formatVersion":1,"sourceHash":5915094414860838624}, - {"assetId":18402512700998817142,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__66","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__66.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":17913129048379707209,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__66","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__66.wmat","formatVersion":1,"sourceHash":8409928813853797007}, - {"assetId":13094775716748457230,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__67","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__67.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":16052493459987036767,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__67","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__67.wmat","formatVersion":1,"sourceHash":13838582871554607396}, - {"assetId":9420110106818975442,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__68","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__68.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":14249256068688134670,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__68","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__68.wmat","formatVersion":1,"sourceHash":5251816322602559214}, - {"assetId":11665770550954512744,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__69","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__69.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":15892041888065753741,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__69","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__69.wmat","formatVersion":1,"sourceHash":4805949336775480751}, - {"assetId":9436205051216976025,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__70","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__70.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":14942587649695695571,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__70","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__70.wmat","formatVersion":1,"sourceHash":3404472943720160873}, - {"assetId":4021034824484642372,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__71","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__71.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":16133487766062081595,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__71","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__71.wmat","formatVersion":1,"sourceHash":4901093357586403455}, - {"assetId":1162139202324819048,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__72","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__72.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":18400739809462618767,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__72","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__72.wmat","formatVersion":1,"sourceHash":11202574233412256705}, - {"assetId":2419795551060346878,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__73","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__73.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":12004174778239393697,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__73","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__73.wmat","formatVersion":1,"sourceHash":11085349085811421204}, - {"assetId":17658336077506810347,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__74","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__74.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6830953916348626798,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__74","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__74.wmat","formatVersion":1,"sourceHash":16191336209829419681}, - {"assetId":11270128962196775149,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__75","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__75.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":1438750279709290240,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__75","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__75.wmat","formatVersion":1,"sourceHash":5122982235709189413}, - {"assetId":4246832261388970769,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__76","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__76.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":10585462442248717742,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__76","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__76.wmat","formatVersion":1,"sourceHash":6651055802267303058}, - {"assetId":11236147002407908713,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__77","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__77.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6278905015285581392,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__77","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__77.wmat","formatVersion":1,"sourceHash":12058946091763468861}, - {"assetId":6385561491663638508,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__78","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__78.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":11833017377497455644,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__78","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__78.wmat","formatVersion":1,"sourceHash":15746208370710709652}, - {"assetId":5774460084544343272,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__79","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__79.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":6449228353247814594,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__79","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__79.wmat","formatVersion":1,"sourceHash":5581722757693394212}, - {"assetId":12363407856602167113,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__80","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__80.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":7825203658809297879,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__80","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__80.wmat","formatVersion":1,"sourceHash":3883641405705167589}, - {"assetId":10411425554735263391,"kind":"texture","relativePath":"Generated/MeshTextures/sponza_atrium_3__81","cookedPath":"Cooked/Generated/MeshTextures/sponza_atrium_3__81.wtex","formatVersion":2,"sourceHash":1136906439044749114}, - {"assetId":9297787003665117867,"kind":"material","relativePath":"Generated/MeshMaterials/sponza_atrium_3__83","cookedPath":"Cooked/Generated/MeshMaterials/sponza_atrium_3__83.wmat","formatVersion":1,"sourceHash":987993751301634973} + { + "assetId": 5874958416375166249, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_22", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_22.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 18368983865493355979, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_22", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_22.wmat", + "formatVersion": 1, + "sourceHash": 2499649355613197980 + }, + { + "assetId": 1471271566526309177, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_23", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_23.wmat", + "formatVersion": 1, + "sourceHash": 14405652211656311485 + }, + { + "assetId": 15623778468829846945, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_24", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_24.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 17364695528653627007, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_24", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_24.wmat", + "formatVersion": 1, + "sourceHash": 10201332737404521061 + }, + { + "assetId": 11964435080788203141, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_23", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_23.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 13913950202207721753, + "kind": "mesh", + "relativePath": "sponza_atrium_3.glb", + "cookedPath": "Cooked/sponza_atrium_3.wmesh", + "formatVersion": 3, + "sourceHash": 13152113367551948137 + }, + { + "assetId": 12095794822955869250, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_0", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_0.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 11463434478890720569, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_0", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_0.wmat", + "formatVersion": 1, + "sourceHash": 4151507458737491729 + }, + { + "assetId": 15507219847355056982, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_1", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_1.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 4766821222485132194, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_1", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_1.wmat", + "formatVersion": 1, + "sourceHash": 851604858293817024 + }, + { + "assetId": 815853016346939371, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_2", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_2.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 3525531968300511773, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_2", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_2.wmat", + "formatVersion": 1, + "sourceHash": 16924987618147102985 + }, + { + "assetId": 18054945187621528064, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_3", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_3.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 8607470849403086088, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_3", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_3.wmat", + "formatVersion": 1, + "sourceHash": 4859792653537051262 + }, + { + "assetId": 17475871112495913969, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_4", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_4.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 6566714637266923561, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_4", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_4.wmat", + "formatVersion": 1, + "sourceHash": 17385784984262903789 + }, + { + "assetId": 8360206298536381676, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_5", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_5.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 258479273388716458, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_5", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_5.wmat", + "formatVersion": 1, + "sourceHash": 12369277255082436023 + }, + { + "assetId": 6061821343567341184, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_6", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_6.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 18357066439617767883, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_6", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_6.wmat", + "formatVersion": 1, + "sourceHash": 15296315176305413764 + }, + { + "assetId": 3097236797834889310, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_7", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_7.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 3990839876037181663, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_7", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_7.wmat", + "formatVersion": 1, + "sourceHash": 4813458475082590939 + }, + { + "assetId": 18236586418421669034, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_8", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_8.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 1013511930689990028, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_8", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_8.wmat", + "formatVersion": 1, + "sourceHash": 17552538437644665814 + }, + { + "assetId": 2355814595322717398, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_9", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_9.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 13719278246398746676, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_9", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_9.wmat", + "formatVersion": 1, + "sourceHash": 2435843369152962752 + }, + { + "assetId": 10420483041868897471, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_10", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_10.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 4686620954401484950, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_10", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_10.wmat", + "formatVersion": 1, + "sourceHash": 13670450735059639575 + }, + { + "assetId": 3482185794204254174, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_11", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_11.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 3600304580076067718, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_11", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_11.wmat", + "formatVersion": 1, + "sourceHash": 875959168468682624 + }, + { + "assetId": 7118742746551308105, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_12", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_12.wtex", + "formatVersion": 2, + "sourceHash": 13834509895381492195 + }, + { + "assetId": 5624130574239054390, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_12", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_12.wmat", + "formatVersion": 1, + "sourceHash": 2087501798860557200 + }, + { + "assetId": 16639437455641564074, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_13", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_13.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 11652605912420992422, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_13", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_13.wmat", + "formatVersion": 1, + "sourceHash": 6891810056084308115 + }, + { + "assetId": 4756632044765826066, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_14", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_14.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 13991375210611018107, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_14", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_14.wmat", + "formatVersion": 1, + "sourceHash": 2682483665576978034 + }, + { + "assetId": 8225381685074179513, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_15", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_15.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 13457548976043167027, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_15", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_15.wmat", + "formatVersion": 1, + "sourceHash": 1166917312362300911 + }, + { + "assetId": 4048015879855409049, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_16", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_16.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 2358919811855562025, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_16", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_16.wmat", + "formatVersion": 1, + "sourceHash": 3895798813244501456 + }, + { + "assetId": 14655125979242647878, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_17", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_17.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 5438110662311396084, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_17", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_17.wmat", + "formatVersion": 1, + "sourceHash": 7098601986122915318 + }, + { + "assetId": 16595015124617109697, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_18", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_18.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 2356852059834577069, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_18", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_18.wmat", + "formatVersion": 1, + "sourceHash": 15211163213654784592 + }, + { + "assetId": 10116488612668460552, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_19", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_19.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 14066926934971471472, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_19", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_19.wmat", + "formatVersion": 1, + "sourceHash": 18137511848333088768 + }, + { + "assetId": 8606755084970727485, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_20", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_20.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 12490383226945625237, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_20", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_20.wmat", + "formatVersion": 1, + "sourceHash": 16230331080883235507 + }, + { + "assetId": 12936064864078827244, + "kind": "texture", + "relativePath": "Generated/MeshTextures/sponza_atrium_3__shared_21", + "cookedPath": "Cooked/Generated/MeshTextures/sponza_atrium_3__shared_21.wtex", + "formatVersion": 2, + "sourceHash": 1136906439044749114 + }, + { + "assetId": 13524454951415366529, + "kind": "material", + "relativePath": "Generated/MeshMaterials/sponza_atrium_3__shared_21", + "cookedPath": "Cooked/Generated/MeshMaterials/sponza_atrium_3__shared_21.wmat", + "formatVersion": 1, + "sourceHash": 17139728439266277423 + } ] } diff --git a/Content/Cooked/sponza_atrium_3.wmesh b/Content/Cooked/sponza_atrium_3.wmesh index 37339303..295f5566 100644 Binary files a/Content/Cooked/sponza_atrium_3.wmesh and b/Content/Cooked/sponza_atrium_3.wmesh differ diff --git a/Content/Shaders/mesh.frag b/Content/Shaders/mesh.frag index 888b072c..04086785 100644 --- a/Content/Shaders/mesh.frag +++ b/Content/Shaders/mesh.frag @@ -5,8 +5,8 @@ layout(location = 1) in vec2 inTexCoord; layout(location = 2) in vec3 inWorldPos; layout(location = 0) out vec4 outColor; -layout(set = 0, binding = 1) uniform texture2D baseColorImage; -layout(set = 0, binding = 2) uniform sampler baseColorSampler; +layout(set = 1, binding = 1) uniform texture2D baseColorImage; +layout(set = 1, binding = 2) uniform sampler baseColorSampler; layout(push_constant) uniform MeshGraphicsPushConstants { mat4 model; diff --git a/Content/Shaders/mesh.frag.spv b/Content/Shaders/mesh.frag.spv index ff982079..7c82c684 100644 Binary files a/Content/Shaders/mesh.frag.spv and b/Content/Shaders/mesh.frag.spv differ diff --git a/Content/Shaders/mesh_project.comp b/Content/Shaders/mesh_project.comp index 921e8bb2..d6853f00 100644 --- a/Content/Shaders/mesh_project.comp +++ b/Content/Shaders/mesh_project.comp @@ -2,11 +2,6 @@ layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; -struct MeshVertex { - vec4 position; - vec4 normal; -}; - struct ProjectedVertex { vec4 pixelAndDepth; vec4 normalAndValid; @@ -14,7 +9,7 @@ struct ProjectedVertex { }; layout(std430, set = 1, binding = 0) readonly buffer VertexBuffer { - MeshVertex vertices[]; + float vertexData[]; }; layout(std430, set = 1, binding = 2) writeonly buffer ProjectedVertexBuffer { ProjectedVertex projectedVertices[]; @@ -39,13 +34,23 @@ void main() { return; } - MeshVertex vertex = vertices[vertexIndex]; - vec4 worldPosition = pushConstants.model * vec4(vertex.position.xyz, 1.0); + uint vertexBase = vertexIndex * 8; + vec3 position = vec3( + vertexData[vertexBase + 0], + vertexData[vertexBase + 1], + vertexData[vertexBase + 2] + ); + vec3 normal = vec3( + vertexData[vertexBase + 3], + vertexData[vertexBase + 4], + vertexData[vertexBase + 5] + ); + vec4 worldPosition = pushConstants.model * vec4(position, 1.0); vec4 clipPosition = cameraFrame.viewProjection * worldPosition; ProjectedVertex result; result.clipPosition = clipPosition; - result.normalAndValid.xyz = normalize(mat3(pushConstants.model) * vertex.normal.xyz); + result.normalAndValid.xyz = normalize(mat3(pushConstants.model) * normal); result.normalAndValid.w = clipPosition.w > 0.0001 ? 1.0 : 0.0; if (result.normalAndValid.w > 0.5) { diff --git a/Content/Shaders/mesh_project.comp.spv b/Content/Shaders/mesh_project.comp.spv index a3deab37..34cd2cf4 100644 Binary files a/Content/Shaders/mesh_project.comp.spv and b/Content/Shaders/mesh_project.comp.spv differ diff --git a/Docs/DistributedWraithEngineDesign.md b/Docs/DistributedWraithEngineDesign.md index ecea6481..f8404113 100644 --- a/Docs/DistributedWraithEngineDesign.md +++ b/Docs/DistributedWraithEngineDesign.md @@ -2,7 +2,7 @@ ## Document Status - Status: Draft -- Date: 2026-05-11 +- Date: 2026-05-25 - Audience: Engine, tools, networking, web, and infrastructure contributors - Intended outcome: Establish the target architecture for evolving WraithEngine into a distributed game engine and browser-based collaborative editor @@ -16,9 +16,12 @@ - Added engine-facing `ISessionTransport` and made `AxiomSessionEndpoint` the first in-process transport implementation - Added `AxiomRemoteViewportDevClient` as a transport-subscriber harness that receives authoritative events and writes client-received frames to disk - Added `AxiomRemoteViewportServer` as the first real browser-facing remote viewport prototype, evolving from HTTP image polling to WebSocket/JPEG bring-up and now to a native macOS WebRTC plus H.264 browser path +- Refactored browser-facing transport into a standalone `WraithNetworking` engine module registered through `ModuleManager` +- Replaced the custom headless HTTP/WebSocket server path with vendored `uWebSockets` while preserving the existing WebRTC communication layer inside the module boundary - Added engine-owned encoded-video packet types plus `IVideoEncoder` and extended `AxiomSessionEndpoint` so encoded packets can flow beside raw viewport frames - Added a macOS-first `VideoToolbox` H.264 encoder path for headless remote-viewport bring-up - `AxiomRemoteViewportServer` now treats WebRTC as the only supported remote viewport media path +- `WraithNetworking` now exposes initialization state and connection metrics for future CVAR/config integration - Removed the largest remote-viewport performance bottlenecks by unthrottling the headless server loop and tuning the encoder/input path for latency - The remote viewport now runs at acceptable frame rate, but still has noticeable residual input latency that likely requires deeper WebRTC sender/playout tuning - A root-level `EditorFrontend` workspace now serves as the longer-lived browser editor shell using Next.js, React, and Tailwind CSS @@ -62,7 +65,7 @@ - `ListAssets` command returns `{"type":"asset_list","assets":[{id,name,kind,path}...]}`; the content browser triggers it on connection and renders real assets in grid/list views with mesh/texture filter tabs and a Refresh button - `GetSchema` command returns `{"type":"object_schema","objectId":"...","className":"...","properties":[{name,type,readOnly}...]}`; the details panel fetches schema on selection change and shows a `className` badge in the panel header - `SetProperty` command dispatches to `RenameObjectCommand`, `SetObjectVisibilityCommand`, or `SetTransformCommand` based on property name; vec3 fields (location, rotationDegrees, scale) read the current transform from `ObjectDetailsById` and patch only the changed component -- `SceneFile` (`Axiom/Assets/SceneFile.h/.cpp`) provides `SaveSceneToFile` / `LoadSceneFromFile`; serialization uses a manual `ostringstream` JSON emitter in flat-node format with `parentId` links; deserialization uses a purpose-built recursive descent parser (no external JSON library) +- `SceneFile` (`Axiom/Assets/SceneFile.h/.cpp`) provides `SaveSceneToFile` / `LoadSceneFromFile`; it now serializes and parses the flat-node `parentId`-linked scene format through `rapidjson`, with in-place parse where the current file-loading path allows it - `LoadStartupScene` now checks for `Content/scene.json` first and falls back to the hardcoded default scene; scene state persists across server restarts automatically - `SaveScene` command writes `Content/scene.json` and replies with `{"type":"scene_saved"}` or `{"type":"scene_save_failed"}`; the toolbar Save button animates to a green checkmark for 2.5 s on success or a red X on failure - Phase 7 (Asset Pipeline) is now implemented: mesh asset assignment, dynamic directional lighting, material property editing, texture thumbnail previews, texture assignment, FBX/OBJ import via assimp, and OS-level file import into the content browser @@ -81,12 +84,24 @@ - Scripts now sit behind the runtime boundary: they instantiate and tick only while playing, freeze while paused, and tear down on stop - Jolt physics is now integrated as a runtime-only service that is created on play, stepped only while playing, frozen while paused, and destroyed on stop - Physics authoring is now available through authoritative scene details: body type, collider type, box half extents, sphere radius, mass, friction, and restitution persist through save/load +- `PhysicsWorld` no longer depends on `EditorSceneState`; `EditorSession` now extracts a lightweight `RuntimeSceneState` containing only runtime-facing transforms, collider shapes, masses, and material indices before booting physics - Imported mesh assets now default to static box collision that covers the authored mesh bounds; older scenes are migrated to that default on load only when the mesh had no authored physics yet - Generated mesh children from multi-mesh imports remain read-only and inherit physics authoring from their imported root mesh object; the browser inspector now surfaces that inheritance instead of hiding physics entirely - A configurable sky background is now wired end-to-end: `RenderScene::SkyboxColorTop/Bottom` plus an optional `HDRTextureSourceDataRef SkyboxHDRTexture` feed two `DrawBackground` paths in the Vulkan backend — the existing `gradient_color.comp` produces a vertical blend, and a new `skybox_hdr.comp` unprojects each pixel through `inverse(proj * view)` and equirectangularly samples a `VK_FORMAT_R32G32B32A32_SFLOAT` image so the HDR is preserved at full float precision and rotates with the camera; `SetWorldSettingsCommand` carries both the colors and a content-relative `SkyboxHDRPath`, `EditorSession` loads HDR data on path change (with previous-path caching to avoid redundant disk hits), and `HeadlessSessionLayer` republishes the scene's HDR ref each frame via `RenderCommand::SetSkyboxHDR`; HDR uploads/swaps are deferred onto the per-frame `DeletionQueue` so in-flight command buffers can't reference a freed image - The cooked-texture format (`.wtex`) was extended to v2 with a `PixelFormat` field appended after the existing v1 header; legacy v1 LDR files continue to load unchanged, while HDR textures cook as v2 with full-float (`RGBA32F`) pixel data through new `SaveCookedHDRTextureAsset` / `LoadCookedHDRTextureAsset` / `CookHDRTextureAsset` paths, so the editor's authored HDR keeps its full dynamic range for future image-based lighting reuse - `.hdr` is now a first-class asset extension: `LocalAssetSource` indexes it as `AssetKind::Texture`, `RemoteViewportServer` accepts it through the upload endpoint, and the content browser picker filters it natively - A new `WorldDetailsPanel` (docked alongside the Details inspector) hosts a `Sky Gradient` section with linked color pickers and an `HDR Sky` section that combines a typed content-relative path input, drag-drop from the content browser (consuming the existing `axiom/asset-path` / `axiom/asset-kind` dataTransfer payload), and a folder-icon `AssetPickerButton` popover that lists every `.hdr` in the project searchably; the same `AssetPickerButton` is also wired into the script-class field on the Details panel so script attachment no longer requires typing a fully-qualified class name; the panel reuses the canonical Details-style section/input chrome and forces the shadcn popover surfaces into dark theme so they match the rest of the editor +- Added a foundational engine module/plugin system: `IModule` defines standardized lifecycle hooks (`GetName`, `Initialize`, `Update`, `Shutdown`), and `ModuleManager` owns registration, initialization order, per-module active state, and query APIs intended for future CVAR/config integration +- `Application` now owns a `ModuleManager` and drives the frame through module phases instead of hardcoded polling/update/render branches; default runtime responsibilities were extracted into engine modules for window events, layer update, layer render, and renderer frame orchestration +- Window/presentation ownership has been pushed further out of the renderer: `Window` now exposes minimization state and Vulkan-surface creation hooks, `RenderSurface` forwards them, and the Vulkan backend no longer includes GLFW headers or stores a GLFW window pointer just to manage presentation details +- Headless runtime hosts now register focused host modules instead of wiring everything inline: transport/session bootstrap moved into `HeadlessSessionTransportModule`, and script-host lifecycle moved into `SessionScriptHostModule` +- Editor viewport responsibilities were split out of `GlfwEditorLayer` into `EditorViewportInputModule`, `EditorViewportSelectionModule`, and `EditorSceneRenderModule` +- Headless overlay and gizmo rendering responsibilities were split out of `HeadlessSessionLayer` into `HeadlessOverlayModule`, which now owns light billboards, collider overlays, presence markers, material caches, and per-user gizmo overlay state +- `EditorSession` refactoring has advanced: `EditorSession` is now a coordinator that delegates command routing to `EditorCommandDispatcher`, scene/tree/transform ownership to `EditorSceneStateManager`, physics lifecycle to `EditorPhysicsController`, and command validation to `EditorSessionValidationModule` +- Vulkan mesh lifetime is now less singleton-coupled: `VulkanMesh` destruction no longer calls back into `VulkanRendererBackend::TryGet()`, and deferred GPU cleanup is routed through a standalone shared `GPUResourceQueue` +- The old 1,789-line `VulkanRendererBackend` renderer monolith has now been decomposed into `VulkanResourceManager` (swapchain, images, buffers, descriptor-backed lifetime), `VulkanPipelineLibrary` (raw pipeline/layout caching), and `VulkanDrawSubmissionSystem` (command recording, graphics submission, offscreen capture publication, and async transfer-queue synchronization), with `VulkanRendererBackend` reduced to a coordinator +- Synchronous texture/HDR upload paths no longer stall the main thread through `ImmediateSubmit`; uploads now flow through a transfer queue with semaphore synchronization into graphics work +- Headless parity validation now includes a clean pre-refactor baseline comparison: the first true captured startup-scene frame from baseline and refactor matched byte-for-byte, while a later mismatch investigation confirmed the remaining difference was stale baseline offscreen readback behavior rather than a rendering regression ## 1. Executive Summary WraithEngine will evolve from a single-process native editor into a distributed platform with one shared C++ engine runtime that supports two execution styles: @@ -96,7 +111,7 @@ WraithEngine will evolve from a single-process native editor into a distributed The first major milestone is a `remote browser editor`. The browser will own most editor UI using React, Next.js, and Tailwind CSS, while a native engine session remains authoritative for rendering, world state, asset loading, and validation of edits. The viewport will be rendered server-side and streamed to the browser via WebRTC using H.264 first. -The current repository is still a native Vulkan/GLFW/ImGui engine with a simple `Application -> Renderer -> RenderScene` flow, file-based mesh loading, and a native editor executable. That is a good base for the next phase because the rendering and runtime already exist, but the codebase needs new seams for headless execution, transport, collaboration state, asset reflection, scripting isolation, and deployment. +The current repository has moved beyond the original single-process Vulkan/GLFW/ImGui-only shape: it now includes a headless runtime path, authoritative `EditorSession` state, remote viewport transport/server layers, browser-editor integration, and an engine module runtime seam. That module seam now includes a concrete `WraithNetworking` implementation that owns browser-facing HTTP/WebSocket transport while preserving the existing WebRTC session logic. That is a good base for the next phase because the rendering and runtime already exist, but the codebase still needs continued decomposition around session dispatch, remote server responsibilities, and packaging/deployment boundaries. This document describes the target architecture, public concepts, service boundaries, trust model, rollout order, and acceptance criteria for that transition. @@ -152,20 +167,24 @@ At the time of writing, the repository contains: The active architecture is roughly: -1. `Application` owns a GLFW window and a renderer. -2. `Layer`s update every frame and can render from authoritative state in a separate pass. -3. A local `EditorSession` owns viewport camera/look state and drains queued commands deterministically each frame. -4. `RenderCommand` writes authoritative frame data into a frame-local `RenderScene`. -5. `Renderer` passes that scene into a Vulkan backend. -6. The backend renders to a window-presented swapchain image. +1. `Application` owns a window, renderer-facing runtime state, and a `ModuleManager`. +2. Engine-owned modules now drive the core loop phases for window polling, layer update, layer render, and renderer frame orchestration. +3. Local editor and headless hosts layer their own feature modules on top of that runtime seam instead of expanding `Application` or the host constructors directly. +4. A local or headless `EditorSession` owns authoritative scene/editor state and drains queued commands deterministically each frame. +5. `RenderCommand` writes authoritative frame data into a frame-local `RenderScene`. +6. `Renderer` passes that scene into a Vulkan backend. +7. The backend renders to either a window-presented swapchain image or an offscreen headless target. +8. Runtime-only services such as physics now boot from stripped scene snapshots instead of reading editor-state types directly. This is enough to establish: - there is already a separation between scene submission and rendering +- there is now a first-class runtime module seam for enabling, disabling, and eventually configuring major engine features without re-hardcoding the app loop - there is now an initial engine-owned command/event authority seam for editor viewport state - the renderer can evolve into windowed and headless targets -- the current authoritative domain is still narrow and local to viewport/input state rather than full world editing -- there is not yet a reflection, network, collaboration, headless runtime, or scripting host layer +- OS-specific presentation details can continue moving outward because the renderer now queries minimization and surface creation through abstractions instead of hardcoded GLFW calls +- the current architecture already includes reflection, networking, collaboration, headless runtime, and scripting-host layers, but some of those areas still need cleaner boundaries and further hardening +- `EditorSession` remains one of the main concentration points for authoritative world-editing logic even after the first extraction steps ## 5. Architectural Principles - One core runtime, multiple adapters @@ -204,7 +223,7 @@ Current implementation direction: - use the existing root-level `EditorFrontend` folder as the home for the browser editor shell - keep `EditorFrontend/components/engine/viewport.tsx` as the canonical browser WebRTC client -- keep `AxiomRemoteViewportServer` focused on session, signaling, command, and diagnostics endpoints +- keep `AxiomRemoteViewportServer` focused on session ownership and module registration, with `WraithNetworking` owning browser-facing HTTP/WebSocket transport and preserving the existing WebRTC signaling/media path ### 6.2 Conceptual Topology ```text @@ -220,6 +239,9 @@ Control Service Engine Session ├─ Authoritative editor state + ├─ `WraithNetworking` module + │ ├─ `uWebSockets` HTTP/WebSocket transport + │ └─ protected WebRTC signaling/media bridge ├─ Rendering ├─ Frame capture + encode ├─ Asset system @@ -430,6 +452,8 @@ Current implementation note: - the current slice covers per-user viewport camera state, look/cursor-capture state, last cursor position bookkeeping, presence state, startup-scene logical mesh instances, selection state, and object transform authority for the startup scene - renderer-owned `RenderMeshSubmission` objects are no longer authoritative session state; they are now rebuilt from logical session scene data through an adapter at render time +- submission-time mesh resolution now stamps `RenderMeshSubmission` with a typed `VulkanMesh*`, so the Vulkan render loop no longer performs RTTI checks (`dynamic_pointer_cast`) per submitted mesh +- diagnostic submission names were moved out of `RenderMeshSubmission` and into a separate debug-data registry so the steady-state renderer path does not carry per-submission heap-backed strings - all scene objects are now backed by an Instance hierarchy rooted at `DataModel`; `EditorSession` owns a `std::unique_ptr` and keeps `EditorSceneState::Items` synchronized as a projection of the live tree - `SetSceneState` and `SetSceneItems` rebuild the Instance tree from the provided snapshot, enabling rehydration and round-trip restore - entity/component/object registries, locks, presence, and asset editing state remain future work @@ -1047,7 +1071,7 @@ Progress update: - `IAssetSource` / `LocalAssetSource` VFS abstraction introduced in `Axiom/Assets/IAssetSource.h/.cpp`; `LocalAssetSource` scans a root directory and derives stable `AssetId` values from hashed relative paths; `ResolveRelative` provides typed engine-internal path lookup - `ListAssets`, `GetSchema`, `SetProperty`, and `SaveScene` command types added to `HeadlessCommandType` and `ParseRemoteViewportCommand`; all four are handled in both the WebSocket and WebRTC dispatch paths in `RemoteViewportServer` - `SerializeAssetList`, `SerializeObjectSchema`, and `SerializeSaveResult` serializers added to `HeadlessCommandProtocol` -- `SceneFile` serializer/parser implemented in `Axiom/Assets/SceneFile.h/.cpp` using a manual ostringstream JSON emitter and a purpose-built recursive descent parser; flat-node format with `parentId` links avoids any external JSON library dependency +- `SceneFile` serializer/parser implemented in `Axiom/Assets/SceneFile.h/.cpp` using `rapidjson`; the flat-node format with `parentId` links is preserved, but the handwritten emitter/parser has been retired - `LoadStartupScene` now checks for `Content/scene.json` first and falls back to the hardcoded default startup scene, making scene state persistent across restarts - `SaveScene` command writes `Content/scene.json` at runtime and returns `scene_saved` or `scene_save_failed`; the pre-existing toolbar Save button is now wired end-to-end and animates to a green checkmark (success) or red X (failure) for 2.5 s - content browser replaced with a live server-driven implementation: `listAssets` is dispatched on connection, results populate grid/list views with mesh/texture filter tabs and a Refresh button @@ -1068,13 +1092,28 @@ Progress update: - `InternalCalls` binds C++ `EditorSession` methods to the managed surface via Coral's unmanaged function pointer fields; `RegisterInternalCalls` uploads them to the engine ALC - `ScriptHost` implements `IEditorEventSubscriber`; `ObjectCreatedEvent` instantiates scripts, `ObjectDeletedEvent` destroys them, `ScriptClassChangedEvent` handles attach/detach - `ScriptHost::Tick(dt)` drives `OnTick` on all live instances; `ScriptHost::ReloadUserAssembly` tears down and recreates the `UserScripts` ALC and re-instantiates all scripts -- macOS `kqueue`-based file watcher auto-triggers reload when the assembly on disk changes (behind `AXIOM_SCRIPTING_WATCH` flag) +- HAL-managed file watcher auto-triggers reload when the assembly on disk changes (current macOS backend uses `kqueue`, behind `AXIOM_SCRIPTING_WATCH`) - Two trust tiers: `Restricted` (default for hosted — blocks `System.Net.*`, `System.Reflection.Emit`, `System.Diagnostics.Process`; assembly manifest validated via `PEReader` before loading) and `Trusted` (local dev — full BCL) - `RestrictedAssemblyLoadContext` and `TrustedAssemblyLoadContext` enforce the policy in managed code; `ScriptSecurity` bridges the `IsRestricted` flag via an internal call - Browser: details panel shows a `ScriptClass` text field; toolbar Reload Scripts button sends `reload_scripts`; unhandled script exceptions surface as dismissible toasts - Five Google Test cases in `Tests/ScriptingTests.cpp`: `ScriptHostLifecycle`, `InternalCallRoundTrip`, `ScriptLifecycle`, `HotReload`, `RestrictedProfileBlocks` - Coral patched: cross-ALC assembly sharing in `AssemblyLoader.ResolveAssembly` (prevents duplicate `WraithEngine.Managed` load with null function pointers) and `ManagedAssembly::RefreshTypeCache` (repopulates `s_CachedTypes` after `UnloadAssemblyLoadContext` clears it globally) +### Platform Foundation Update + +- Added a top-level `HAL/` directory as the engine's platform bedrock, built as `AxiomHAL` +- `AxiomHAL` is now the base dependency beneath `AxiomCore`, `ModuleManager`, and engine plugins/modules that need platform services +- first-party OS-specific code is now isolated under `HAL/`, including: + - platform detection and environment helpers + - Vulkan loader fallback and dynamic-library access + - socket helpers used by the remote transport stack + - file watching used by script hot reload + - SVG rasterization + - VideoToolbox H.264 encoder creation + - macOS WebRTC session creation +- higher-level modules such as `ScriptHost`, `RemoteViewportServer`, `VideoEncoderFactory`, `SvgTexture`, and `VulkanLoader` now route through HAL interfaces/factories rather than including OS headers directly +- this keeps the engine-facing call sites platform-agnostic and leaves the architecture prepared for future Windows/Linux implementations inside `HAL/` + ### Phase 7: Asset Pipeline Scope: Get real asset data flowing — mesh assignment, directional lighting, material @@ -1125,7 +1164,7 @@ editor-only metadata included. **Mesh binary (`.wmesh`)** - Header: magic `WMSH`, `uint32` version, `AssetId` (8 bytes), vertex count, index count, submesh count, flags -- Vertex buffer: interleaved `{vec3 position, vec3 normal, vec2 uv, vec4 tangent}` — layout fixed so the Vulkan pipeline never needs to inspect the source format +- Vertex buffer: interleaved `{vec3 position, vec3 normal, vec2 uv}` in a fixed 32-byte stride. The runtime and compute projection shader both consume that packed layout directly, so no source-format inspection or CPU-side vertex expansion is needed at load time - Index buffer: `uint32[]` indices - Submesh table: `{uint32 indexOffset, uint32 indexCount, AssetId materialId}[]` - Bounding sphere and AABB stored after submesh table for culling; no re-parsing needed at runtime @@ -1157,16 +1196,22 @@ Progress update: - `.wmesh` stores versioned mesh instance payloads plus stable `AssetId` - `.wtex` stores versioned raw RGBA texture payloads keyed by `AssetId` - `.wmat` stores versioned material factors plus texture asset reference +- the cooked mesh format is now at version 3 so new cooks can persist the packed `{vec3 position, vec3 normal, vec2 uv}` vertex layout while the loader still accepts older mesh payloads and converts them forward at load time - `AssetCookManifest` now lives at `Content/Cooked/AssetCookManifest.json` and is populated for mesh, texture, and material cooks - `CookedAssetSource` is implemented parallel to `LocalAssetSource` and resolves cooked payloads by `AssetId` through the manifest - runtime load paths now prefer cooked assets for mesh and texture loads, with source-file fallback preserved during the transition - startup scene load, scene reload, `SetMeshAsset`, and `SetMaterialTexture` all exercise best-effort cook-first flows so normal editor usage continuously validates the cooked path - scene persistence now emits and restores `materialAssetPath` entries backed by cooked `.wmat` files +- `VulkanMesh` now uploads directly to GPU buffers and drops CPU-side vertex/index storage immediately unless the caller explicitly opts into retention with `MeshCreateOptions::KeepCpuData` +- `VulkanSceneRenderer::RenderScenePasses()` now reuses persistent scratch buffers for candidate, opaque, translucent, and compute submission lists, eliminating those per-frame vector allocations from the main mesh loop - focused regression coverage now exists for: - cooked mesh / texture / material binary round-trips - manifest-backed cooked lookup resolution - `SetMeshAsset` and `SetMaterialTexture` command-path cooking - scene save/load round-trip of cooked material state +- JSON infrastructure migration is now complete across the current packaging/runtime/protocol surface: `AssetCookManifest`, `CookedAssetRuntime`, `SceneFile`, `ProjectSystem`, `HeadlessCommandProtocol`, and the JSON-producing/JSON-consuming endpoints in `RemoteViewportServer` all use `rapidjson` internally rather than the earlier handwritten serializers/parsers +- `rapidjson` is now fetched in CMake as a project dependency; current file-loading paths prefer in-place parse (`ParseInsitu`) where mutable buffers already exist, while writer-based serialization preserves the existing wire/file schemas +- verification for the migration is covered by the focused `AxiomTests` JSON slice: `HeadlessProtocolTests.*`, `ProjectSystemTests.*`, `ProjectSystemStandaloneTests.*`, `SceneLifecycleTests.SceneFile_*`, `SceneLifecycleTests.CookedSceneFile_*`, and `CookedAssetTests.*` - remaining Phase 8 work is mostly hardening and packaging-facing: - importer-driven cook orchestration rather than today’s best-effort command/load triggers - richer material/texture reference tables and GPU-oriented texture layout if needed @@ -1215,12 +1260,20 @@ Scope: Several classes have grown to 1,500 – 2,500 lines and now own multiple responsibilities. This phase extracts those responsibilities into focused units before the codebase becomes materially harder to extend. +Current progress: +- The first engine-wide extraction step is done: a reusable `IModule` / `ModuleManager` foundation now exists and the `Application` loop runs through it. +- `Application` runtime responsibilities have already been split into `ApplicationModules`. +- Headless host bootstrap responsibilities have already been split into `HostModules`. +- `GlfwEditorLayer` responsibilities have already been split into `EditorFeatureModules`. +- `HeadlessSessionLayer` overlay responsibilities have already been split into `HeadlessOverlayModule`. +- `EditorSession` has now completed the first major decomposition step: command handling lives in `EditorCommandDispatcher`, scene/tree/transform logic lives in `EditorSceneStateManager`, physics runtime lifecycle lives in `EditorPhysicsController`, and validation remains in `EditorSessionValidationModule`. + #### 10.1 Candidates for refactoring (audit before Phase 10 begins) Likely targets based on current trajectory: | File | Approximate size | Concerns to extract | |------|-----------------|---------------------| -| `Axiom/Session/EditorSession.cpp` | ~2,000 lines | Command dispatch, event publication, lock management, presence logic, schema generation | +| `Axiom/Session/EditorSession.cpp` | ~2,500 lines before the current split; now reduced to a small coordinator | Presence/lock helpers, subsystem wiring, authoritative state ownership | | `Headless/RemoteViewportServer.cpp` | ~1,500 lines | HTTP routing, WebSocket framing, WebRTC signaling, command parsing, client lifecycle | | `Headless/HeadlessCommandProtocol.cpp` | ~800 lines | Growing with every new command; serialization/deserialization should be generated or table-driven | | viewport interaction / gizmo hit-testing path | multi-file | mode-specific hit testing, drag math, and interaction branching are starting to duplicate patterns and should move toward reusable primitives or strategies | @@ -1229,7 +1282,10 @@ Likely targets based on current trajectory: **`EditorSession`** → split into: - `EditorSession` — thin coordinator; owns state, wires subsystems -- `CommandDispatcher` — validates and routes incoming commands +- `EditorSessionValidationModule` — validates incoming commands +- `EditorSceneStateManager` — owns scene snapshots, instance-tree rebuilding, generated asset expansion, transform recomputation, and selection pruning +- `EditorCommandDispatcher` — routes incoming commands and executes authoritative edits/runtime transitions +- `EditorPhysicsController` — owns physics-world startup, shutdown, and runtime stepping - `EventBroadcaster` — serializes and fans out authoritative events - `LockManager` — manages object/asset lock lifecycle - `PresenceTracker` — heartbeat, idle detection, state transitions @@ -1351,6 +1407,7 @@ Progress update: - scripting authoring is now project-local: each project gets a generated `Scripts/` workspace plus `.sln`/`.csproj`, the browser has a script editor with file CRUD and syntax highlighting, scripts can be attached to actors, and inspector `Open Script` jumps into the editor - Phase 7 (Asset Pipeline) is complete: `SetMeshAssetCommand` wires any discovered `.glb`/`.gltf`/`.fbx`/`.obj` to mesh objects and actor roots with scene-file persistence; `SetLightPropertiesCommand` drives a Blinn-Phong directional light from `SceneLight` world position; `SetMaterialPropertiesCommand` exposes `BaseColorFactor`/`Metallic`/`Roughness` push constants end-to-end through the inspector; `SetMaterialTextureCommand` assigns PNG/JPG textures to mesh base-color slots with persistence, inspector display, and drag-drop from both the content browser and outliner; FBX/OBJ import is implemented via assimp with embedded and external texture handling; the content browser accepts OS file drag-drop and a file picker Import button that upload to `POST /assets/upload`; texture thumbnail previews are served by the remote viewport server; the content browser navigates folders non-recursively; regression coverage now includes the `CreateObject`→`SetMeshAsset` runtime-creation path and actor mesh assignment - Phase 8 (Binary Asset Formats) and the first packaging foundation are now implemented: `.wmesh`, `.wtex`, and `.wmat` cooked formats exist; `AssetCookManifest` and `CookedAssetSource` resolve cooked content by stable `AssetId`; startup, scene reload, and mesh/texture editing flows all prefer cooked payloads while preserving source fallback during editor use; scene persistence now round-trips cooked material state through `materialAssetPath`; projects now cook into per-project `Content/Cooked/` and stage packaged outputs under per-project `Package/` directories +- the supporting JSON layer under those systems has now been standardized on `rapidjson`: scene files, cook manifests, packaged project manifests, cooked-runtime validation, headless command/event serialization, and remote project/script HTTP payloads all moved off the earlier handwritten JSON code while preserving their public schemas - packaged runtime cutover is now implemented for the first desktop slice: staged packages include `AxiomPackagedRuntime`, cooked project content, `scene.wscene`, the asset cook manifest, shared engine content, and a package manifest; packaged content roots are treated as cooked-only at runtime and now fail fast on invalid package layout, missing cooked scene/cook manifest, or unresolved cooked asset references - the next step is now a targeted runtime/editor architecture refactor before further large feature work: the current codebase has enough cross-cutting packaging/editor/runtime/session state that cameras, gameplay input, and possession should not be layered on top of the existing seams without first tightening the boundaries - that refactor should stay narrow and gameplay-motivated rather than turning into general cleanup; the goal is to separate editor authoring state from runtime gameplay state, editor viewport controls from in-game input, and editor camera movement from runtime camera ownership/activation diff --git a/Docs/HeadlessAxiomSessionPrototype.md b/Docs/HeadlessAxiomSessionPrototype.md index 5175dc9b..e1048281 100644 --- a/Docs/HeadlessAxiomSessionPrototype.md +++ b/Docs/HeadlessAxiomSessionPrototype.md @@ -4,9 +4,11 @@ `AxiomRemoteViewportDevClient` is a companion dev executable that exercises the same authoritative session through the new transport seam and writes the frames it receives as a transport subscriber. -`AxiomRemoteViewportServer` is now the primary remote viewport prototype backend. It runs the authoritative headless session and exposes the HTTP/WebRTC endpoints used by the browser editor. +`AxiomRemoteViewportServer` is now the primary remote viewport prototype backend. It runs the authoritative headless session, registers the standalone `WraithNetworking` module, and exposes the browser-facing networking surface through that module. -The current slice includes a macOS-first H.264 path that is wired into the native WebRTC sender. The browser viewport consumes the WebRTC video track as the only remote viewport media path. +The current slice includes a macOS-first H.264 path that is wired into the native WebRTC sender. The browser viewport consumes the WebRTC video track as the only remote viewport media path, and that WebRTC path remains preserved inside the networking module as a protected subsystem rather than being rewritten during transport refactors. + +That macOS-specific media path now lives behind the engine-wide `HAL/` layer, so the higher-level networking/session code stays platform-agnostic. ## Current Status @@ -21,16 +23,21 @@ The current slice includes a macOS-first H.264 path that is wired into the nativ - `AxiomSessionEndpoint` is the first in-process transport implementation - `AxiomRemoteViewportDevClient` is a dev harness for transport-delivered frames/events, not a browser client - `AxiomRemoteViewportServer` is the current browser-facing backend for remote viewport work +- `WraithNetworking` is now the toggleable engine module that owns browser-facing transport lifecycle for the headless server - the current server/browser slice has moved from SSE plus HTTP image polling to WebRTC video plus data channels +- custom socket, HTTP, and WebSocket server code has been removed from the headless server path and replaced with vendored `uWebSockets` - encoded video packet delivery now exists as an additive transport seam beside raw viewport frame delivery - `IVideoEncoder` now exists as the engine-owned video encode boundary - a macOS `VideoToolbox` H.264 encoder path now exists for headless remote-viewport bring-up - `AxiomRemoteViewportServer` now treats WebRTC as the only supported remote viewport media path +- the concrete H.264, WebRTC, socket, and related platform calls used by the remote viewport stack now route through `AxiomHAL` +- `WraithNetworking` exposes initialization state and connection metrics so the runtime can later surface them through CVAR/config tooling - the old frame-ownership splitter and round-robin render-target path have been removed - headless remote rendering now uses one authoritative `EditorSession`, one shared GPU resource world, and one render view per connected remote client - the authoritative session scene is now renderer-agnostic and no longer stores renderer-owned mesh submissions as its source of truth - startup scene loading now populates logical scene mesh instances from the current `basicmesh.glb` asset mapping instead of calling directly into the renderer singleton - local windowed rendering and headless rendering both rebuild render submissions through a shared `EditorSceneRendererAdapter` +- render submissions now cache their typed `VulkanMesh*` at build time and keep debug names in sidecar metadata, so the per-frame renderer hot loop avoids both RTTI casts and embedded submission-name strings - the headless host now performs one render pass per active remote render view during a single engine tick - remote `set_view_mode` is now per-client state, not a global remote-server toggle - the main-loop throttle in the headless remote viewport server has been removed so the runtime can tick at full cadence @@ -39,6 +46,7 @@ The current slice includes a macOS-first H.264 path that is wired into the nativ - the browser client now pumps camera/input updates on `requestAnimationFrame` and flushes pointer-lock look input immediately instead of batching on a fixed timer - the current stream no longer has the severe FPS collapse seen in the older prototype, but there is still roughly half a second of residual input latency to investigate later - a multi-client frame-routing bug was fixed by stamping each offscreen capture with the submitting `SessionUserId` at render time instead of inferring ownership later from mutable active-pass state +- the headless command/protocol layer and the remote project/script HTTP JSON helpers now use `rapidjson` internally; command/event/session payload schemas are unchanged, but the earlier handwritten JSON serializer/parser code on those paths has been removed - a root-level `EditorFrontend` workspace now hosts the primary browser editor shell using Next.js, React, and Tailwind CSS - `EditorFrontend` includes the docked editor layout, menu bar, toolbar, outliner, details panel, content browser, and the active WebRTC viewport client in `components/engine/viewport.tsx` - the old inline localhost:8080 page has been retired; the server now focuses on backend/session and diagnostics endpoints @@ -50,12 +58,17 @@ The current remote viewport stack is organized as: - one authoritative `EditorSession` - one shared renderer and GPU resource cache +- one `WraithNetworking` module registered with `ModuleManager` +- one `uWebSockets` HTTP/WebSocket listener owned by that module - one headless render view per connected remote client - one WebRTC session and one encoder per connected remote client - one multi-pass headless engine tick that renders all active remote views sequentially Important implementation notes: +- transport startup/shutdown now flows through `IModule` lifecycle hooks instead of direct server bootstrap calls +- the browser-facing HTTP/WebSocket layer is replaceable at the module boundary without changing the WebRTC session implementation +- first-party OS and hardware calls used by the remote viewport stack now live under `HAL/` rather than in `Headless/` or other higher-level engine modules - render-view state is per client and currently includes at least `SessionUserId`, client id, and view mode - per-client camera state remains authoritative in `EditorSession` - presence overlays are assembled per rendered user so a viewer sees other participants, not their own marker @@ -98,6 +111,8 @@ Remote viewport server example: ./AxiomRemoteViewportServer --host 127.0.0.1 --port 8080 --width 1280 --height 720 ``` +On startup, the process registers `WraithNetworking` with `ModuleManager`; the module initializes the `uWebSockets` transport, reports whether networking initialized successfully, and keeps per-connection metrics available for future runtime introspection. + Then start the browser editor: ```sh @@ -229,6 +244,7 @@ This prototype proves that: - remote-style command submission can reuse the same session authority path as the local adapter - a browser can connect to the headless authoritative session over a real network boundary and drive the existing viewport camera commands - the browser client can now receive the authoritative viewport over a WebRTC H.264 video track while using data channels for control/input traffic +- the browser-facing transport can now be enabled, initialized, and queried as a standalone engine module instead of being hardwired into the executable bootstrap path - the major FPS bottlenecks in the remote viewport prototype have been removed through server-loop, duplicate-work, encoder, and input-pump latency fixes - multiple connected browser clients now have distinct render views instead of sharing a split single-frame ownership path @@ -258,6 +274,7 @@ The authoritative scene-authoring loop has advanced on the `scene-editing` branc - `ComputeWorldTransformMatrix` walks the instance parent chain and multiplies each ancestor's local matrix to produce the object's world matrix - `DecomposeMatrix` extracts position, YXZ Euler angles (matching `BuildTransformMatrix` rotation order), and scale from a world matrix - `RecomputeSubtreeWorldTransforms` recomputes `WorldTransform` and updates the renderer mesh instance matrix for a moved object and all its descendants in one pass +- that scene/tree/transform responsibility now lives in `EditorSceneStateManager`, with `EditorSession` acting as the coordinator - reparenting a child object preserves its stored local `Transform` values unchanged; the rendered world position shifts to be relative to the new parent's world transform - `SetTransformCommand` (gizmo drag output) arrives in world space; the handler inverts the parent's world matrix to derive local-space storage, then propagates new world transforms to any children - `SerializeObjectDetails` serializes `WorldTransform` to the browser so the properties panel and gizmo always work in world space regardless of hierarchy depth @@ -325,7 +342,7 @@ Object locking, selection/lock visibility, presence indicators, and heartbeat-dr - `ObjectLockChangedEvent` is added to the `EditorEventPayload` variant and serialized as `{ "type": "object_lock_changed", "objectId": ..., "lockState": "locked"|"unlocked", "lockOwnerUserId": n|null }` - `EditorSession` exposes three public methods: `AcquireLock(ObjectId, UserId)`, `ReleaseLock(ObjectId, UserId)`, and `ReleaseAllLocksForUser(UserId)`; each publishes an `ObjectLockChangedEvent` broadcast to all clients - `RemoteViewportServer` calls `AcquireLock` at gizmo drag start and `ReleaseLock` at gizmo drag end, so the dragging user holds an exclusive transform lock for the duration of the interaction -- `ValidateCommand` in `EditorSession` rejects `SetTransform`, `Rename`, `SetObjectVisibility`, `Delete`, and `Reparent` commands on an object locked by a different user; the command is dropped with a `CommandRejected` event +- `EditorSessionValidationModule` rejects `SetTransform`, `Rename`, `SetObjectVisibility`, `Delete`, and `Reparent` commands on an object locked by a different user; the command is dropped with a `CommandRejected` event ### Selection and Lock Visibility in the Outliner diff --git a/Docs/HeadlessPhase1ImplementationNote.md b/Docs/HeadlessPhase1ImplementationNote.md new file mode 100644 index 00000000..7789dd1a --- /dev/null +++ b/Docs/HeadlessPhase1ImplementationNote.md @@ -0,0 +1,23 @@ +# Headless Phase 1 Implementation Note + +## Offscreen synchronization + +- Headless offscreen rendering no longer waits on the submitted render fence immediately after `vkQueueSubmit2`. +- Each offscreen capture slot still records the submitted frame number and render user at submit time. +- Completed readbacks are polled from `PublishCompletedOffscreenFrames()` on later ticks. +- Ready capture slots are published in ascending submitted-frame order before the slot is reused, which preserves frame ordering even when multiple readbacks complete together. +- Reuse stays bounded by `FRAME_OVERLAP`: `PrepareFrame()` still waits before a command/fence slot is recycled, and the offscreen path asserts that the matching capture slot has been drained before recording over it. + +## Frame publication + +- Completed offscreen frames continue to flow through the existing viewport frame-output seam. +- The published `ViewportFrame` is tagged from the stored submit-time user, so attribution does not depend on whichever client happens to be active when the fence later signals. +- `ConsumeCapturedFrame()` now drains a small FIFO of completed captures instead of exposing only the most recent completion, which keeps multi-frame completion behavior deterministic for debugging and tests. + +## Headless render scheduling + +- Remote views now carry lightweight scheduling state: `NeedsRender` plus a short recent-activity burst window. +- New or mutated views are marked dirty and receive a brief full-rate burst. +- Shared scene activity marks all remote views dirty so collaborators get a prompt refresh after edits. +- Idle remote views are throttled to a round-robin cadence instead of forcing one render pass per client per engine tick by default. +- When no remote views exist, the local headless view remains the fallback render target as before. diff --git a/Docs/HeadlessScalabilityBaseline.md b/Docs/HeadlessScalabilityBaseline.md new file mode 100644 index 00000000..57eb63e0 --- /dev/null +++ b/Docs/HeadlessScalabilityBaseline.md @@ -0,0 +1,38 @@ +# Headless Scalability Baseline + +This note captures where to read the new Phase 0 / Phase 1 headless scalability counters before changing scheduling or asynchronous readback behavior. + +## Where The Counters Live + +- `Axiom::HeadlessRuntimeInstrumentation` in [Axiom/Core/HeadlessRuntimeInstrumentation.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Core/HeadlessRuntimeInstrumentation.h) +- Headless render-pass scheduling hook in [Headless/HeadlessSessionHost.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionHost.cpp) +- Offscreen readback hook in [Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp) + +## What To Capture Before Refactors + +- `LastTickRenderPassCount`: current render passes scheduled for one engine tick. +- `TotalRenderPasses`: cumulative render-pass work over a benchmark window. +- `ActiveRemoteClientCount`: connected remote views currently driving scheduling. +- `PendingOffscreenReadbacks`: queued offscreen readbacks still waiting on fences. +- `TotalOffscreenReadbacksSubmitted` and `TotalOffscreenReadbacksCompleted`: whether readbacks are piling up or draining. +- `ClientCadence[*].RenderPassCount`, `LastTicksSincePreviousRender`, and `MaxTicksBetweenRenders`: per-client cadence and whether any client is already naturally throttled. + +## How To Read Them + +- In debug builds, `HeadlessSessionHost` emits periodic `HeadlessRuntime:` log lines when tick pass-count, remote client count, or pending readbacks change. +- In code, tests, or a debugger, call `Axiom::HeadlessRuntimeInstrumentation::GetSnapshot()` to read the full snapshot. +- Remote client/server totals are still available through `RemoteViewportServer::GetMetrics()`, but the render-pass and readback counters now live in the shared instrumentation snapshot. + +## Recommended Baseline Runs + +1. Single remote client connected and interacting with the viewport. +2. Multiple remote clients connected to the same scene. +3. One active client plus one mostly idle client. + +For each run, record the snapshot after a fixed tick window and compare: + +- render passes per tick against remote client count +- pending readbacks during steady state +- per-client cadence symmetry between active and idle clients + +The current baseline is expected to show roughly one render pass per connected remote client per engine tick, with no scheduler distinction yet between active and idle remote clients. diff --git a/Docs/Phase4HandleMigrationNote.md b/Docs/Phase4HandleMigrationNote.md new file mode 100644 index 00000000..7d6122b7 --- /dev/null +++ b/Docs/Phase4HandleMigrationNote.md @@ -0,0 +1,40 @@ +# Phase 4 Handle Migration Note + +Date: 2026-05-25 + +## What moved to internal handles in this slice + +- `SceneObjectHandle` is now the internal stable identity for scene objects. +- `EditorSession` maintains handle-to-string and string-to-handle translation tables. +- Selection authority now stores handles internally and projects string ids back into `EditorSessionState::SelectedObjectIds`. +- Collaboration lock authority now stores handle-keyed state internally and projects string ids back into `EditorSceneState::CollaborationByObjectId`. +- `EditorSceneMeshInstance` now carries `ObjectHandle` so mesh ownership is no longer tied only to string id lookups. +- Physics runtime bodies and physics transform updates now carry `SceneObjectHandle`, so the editor runtime binding no longer depends on string ids to route simulation updates back into scene authority. +- Scene save/load now persists object handles on nodes and object records so handles survive round trips. + +## Compatibility layers kept intentionally + +- Browser protocol commands and events still use string `objectId` values. +- `EditorSceneItem::Id`, `EditorObjectDetails::ObjectId`, collaboration event payloads, and render-facing mesh debug ids still remain string-based for migration compatibility. +- Scene serialization still treats string ids as the public/object graph identifier and now adds handle metadata alongside them rather than replacing them. + +## What still depends on string ids + +- Command payloads, event payloads, and headless protocol parse/serialize paths. +- `EditorSceneState::ObjectDetailsById`. +- Generated asset child naming and asset-root linkage (`GeneratedFromAssetRootId`). +- Scripting lifecycle and internal calls still key script instances by string object id. +- Some physics logging/debug strings still report object ids even though runtime ownership is handle-backed. + +## What still depends on `Instance` + +- Authoritative hierarchy mutation still uses the live `Instance` tree for duplicate, delete, reparent, and tree rebuild flows. +- World transform recomputation still walks `Instance` parent chains. +- Validation for reparent cycle checks still traverses the `Instance` hierarchy. +- Mesh asset expansion/removal still mutates through `Instance` parenting before syncing the projected `Items` tree. + +## Recommended next slices + +- Move scripting instance binding from string object ids to `SceneObjectHandle`. +- Replace handle-era hierarchy operations with handle/array-backed parent-child storage so duplicate, delete, reparent, and transform propagation stop depending on `Instance` as an authoritative mutation structure. +- Collapse `ObjectDetailsById` and other string-keyed authority maps behind handle-keyed storage with string ids retained only as protocol and persistence projections. diff --git a/Docs/WraithEngineRefactorPlan.md b/Docs/WraithEngineRefactorPlan.md new file mode 100644 index 00000000..55f0a555 --- /dev/null +++ b/Docs/WraithEngineRefactorPlan.md @@ -0,0 +1,386 @@ +# WraithEngine Refactor Plan + +## Document Status +- Status: Draft +- Date: 2026-05-25 +- Audience: Engine, rendering, headless runtime, and editor contributors +- Intended outcome: Turn the current engineering audit into an executable refactor roadmap ordered by dependency, risk, and team size + +## Executive Summary + +The current audit is still directionally correct, but the repo has evolved enough that the plan should target today's actual seams instead of the older architecture framing. + +The most important current facts are: + +- Scene authority already lives in editor-owned structs in `EditorSession`, but that data is mirrored into a recursive heap-owned `Instance` tree for hierarchy operations and projection. +- Render submission still carries `shared_ptr` ownership and still recovers backend-specific Vulkan types through `dynamic_cast` in the submission build path. +- Headless offscreen rendering still blocks on `vkWaitForFences` immediately after submit, which defeats frames-in-flight for the headless path. +- Multi-client headless rendering still performs one render pass per remote client per engine tick. +- `RemoteViewportServer` still mixes transport, WebRTC, project lifecycle, script workspace, asset upload, presence, input routing, and frame delivery in one class. +- String-keyed maps remain widespread in editor, headless, scripting, physics, and scene serialization paths even where stable integer handles would make the authority layer simpler and cheaper. + +## Audit Validation + +### 1. Scene and object model + +Current implementation shape: + +- Authoritative editor scene state lives in `EditorSessionState::Scene` in [Axiom/Session/EditorSession.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSession.h:130). +- Scene identity is string-based in `EditorSceneItem::Id`, `EditorObjectDetails::ObjectId`, selection state, collaboration state, and mesh instance ownership in [Axiom/Session/EditorSession.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSession.h:50). +- A parallel `Instance` tree still exists under `m_SceneRoot`, with raw parent and child pointers plus recursive ownership in [Axiom/CoreInstance/Instance.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/CoreInstance/Instance.h:42). +- Snapshot and edit operations rebuild or mutate that tree through `EditorSceneStateManager` in [Axiom/Session/EditorSceneStateManager.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSceneStateManager.cpp:173) and command handlers in [Axiom/Session/EditorCommandDispatcher.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorCommandDispatcher.cpp:236). + +Validation: + +- The old audit is still accurate about recursive ownership, heap chasing, and RTTI in the object model. +- The more precise problem today is not "scene state is only pointers"; it is "scene authority is duplicated across string-keyed value state and a pointer tree that still participates in core mutations." + +### 2. Render submission + +Current implementation shape: + +- `EditorSceneRendererAdapter` rebuilds frame submissions from logical mesh instances and caches meshes by string object id in [Axiom/Session/EditorSceneRendererAdapter.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSceneRendererAdapter.cpp:8). +- `RenderMeshSubmission` still carries `MeshRef` plus a `VulkanMesh *TypedMesh` in [Axiom/Renderer/Mesh.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Mesh.h:81). +- `ResolveVulkanMesh` still uses `dynamic_cast` in [Axiom/Renderer/Mesh.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Mesh.cpp:41). +- Mesh creation still returns `std::shared_ptr` from renderer and backend interfaces in [Axiom/Renderer/Renderer.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Renderer.h:34) and [Axiom/Renderer/RendererBackend.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/RendererBackend.h:67). + +Validation: + +- This finding is fully accurate. +- The current build path is cleaner than earlier direct session-owned submissions, but the ownership model and backend recovery are still in the hot path. + +### 3. Headless rendering and fences + +Current implementation shape: + +- Headless uses an offscreen render surface and publishes captured frames through the renderer frame-output seam in [Axiom/Core/Application.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Core/Application.cpp:152), [Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp:150), and [Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp:633). +- In the offscreen path, the draw submission system submits graphics work, marks the capture pending, then immediately waits on `CurrentFrame.RenderFence` before publishing the frame in [Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp:770). + +Validation: + +- This finding is fully accurate. +- The current implementation preserves frame attribution correctness, but it serializes headless rendering at the point where frames-in-flight should be helping. + +### 4. Multi-client rendering + +Current implementation shape: + +- The application core supports multiple render passes per tick via `BeginRenderPasses()` in [Axiom/Core/Application.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Core/Application.cpp:152). +- `HeadlessSessionHost` builds one pass per active remote render view in [Headless/HeadlessSessionHost.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionHost.cpp:134). +- Each pass resolves a specific render user and repopulates the frame through `HeadlessSessionLayer::OnRender()` in [Headless/HeadlessSessionLayer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionLayer.cpp:31). + +Validation: + +- This finding is fully accurate. +- The engine currently re-renders once per active remote client per tick, even when the scene is shared and only camera/view overlays differ. + +### 5. `RemoteViewportServer` + +Current implementation shape: + +- `RemoteViewportServer` owns the browser-facing server plus session transport subscriber behavior in [Headless/RemoteViewportServer.h](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.h:56). +- The same class handles HTTP routes, WebSocket messages, WebRTC offer and ICE endpoints, project create/open/cook/package, script CRUD, asset upload, presence tracking, per-client session creation, drag-and-drop commands, and frame routing in [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:1266), [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:1657), [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:2055), and [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:2268). + +Validation: + +- This finding is fully accurate. +- The recent module/runtime cleanup improved host seams around the server, but the server itself is still a major concentration point. + +### 6. String-keyed state + +Current implementation shape: + +- Editor scene authority uses `unordered_map` for details and collaboration in [Axiom/Session/EditorSession.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSession.h:133). +- Headless remote view registry and remote clients are keyed by string client id in [Headless/HeadlessRenderView.h](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessRenderView.h:177) and [Headless/RemoteViewportServer.h](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.h:228). +- Script instances are keyed by string object id in [Axiom/Scripting/ScriptHost.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Scripting/ScriptHost.h:122). +- Serialization and runtime systems also keep string-keyed object maps in [Axiom/Assets/SceneFile.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Assets/SceneFile.cpp:704) and [Axiom/Physics/PhysicsWorld.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Physics/PhysicsWorld.cpp:172). + +Validation: + +- This finding is accurate, but not all string maps are equal. +- Client ids should remain externally meaningful strings. +- Scene authority and runtime-facing object identity are the stronger candidates for stable integer handles. + +## Recommended Refactor Order + +### Phase 1: Headless scalability slice + +Why first: + +- Highest current scalability payoff. +- Lowest semantic blast radius compared with scene storage rewrites. +- Directly addresses the clearest N-client cost center. + +Target architecture: + +- Offscreen rendering uses true asynchronous readback. +- Completed frames are published when fences signal on a later tick rather than by waiting immediately after submit. +- Headless render scheduling becomes policy-driven per view, with at least dirty-state or cadence-based throttling for inactive clients. + +Migration strategy: + +1. Remove the immediate fence wait from the headless offscreen path. +2. Let pending readbacks complete in later frames through the existing `PublishCompletedOffscreenFrames()` path. +3. Add headless counters for render-pass count, pending readbacks, and per-client frame cadence. +4. Add render scheduling policy in `HeadlessSessionHost` so all connected clients do not force full-rate rendering by default. + +Files and systems affected: + +- [Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp:633) +- [Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanRendererBackend.cpp:150) +- [Headless/HeadlessSessionHost.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionHost.cpp:134) +- [Headless/HeadlessViewportFrameBridge.h](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessViewportFrameBridge.h:11) + +Risks: + +- Stale or misattributed frames if readback ownership regresses. +- Increased capture latency if pending-frame queues are not bounded. +- Hidden coupling with encoder timing and WebRTC sender expectations. + +Test strategy: + +- Extend frame attribution tests around [Tests/LayerTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/LayerTests.cpp:940). +- Add a renderer-level regression test or harness for two queued offscreen frames with distinct users. +- Add instrumentation assertions in headless integration tests for render-pass count versus active-client count. + +Incremental or staged: + +- Incremental. + +### Phase 2: Render submission cleanup + +Why second: + +- It compounds the Phase 1 gains. +- It reduces CPU overhead in the repeated per-client submission build path. +- It is more contained than scene-storage replacement. + +Target architecture: + +- Renderer submissions are plain data carrying opaque mesh/resource handles. +- Backend-specific type recovery happens inside the backend, not in upstream submission builders. +- Debug metadata is stable per resource or per object instead of being re-registered per frame. + +Migration strategy: + +1. Introduce an engine-owned mesh handle type beside `MeshRef`. +2. Teach renderer and backend interfaces to create and resolve handles without exposing Vulkan types. +3. Change `RenderMeshSubmission` to carry handle plus material/transform only. +4. Remove `TypedMesh` and retire `ResolveVulkanMesh` from the frame build path. + +Files and systems affected: + +- [Axiom/Renderer/Mesh.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Mesh.h:59) +- [Axiom/Renderer/Mesh.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Mesh.cpp:15) +- [Axiom/Renderer/Renderer.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Renderer.h:34) +- [Axiom/Renderer/RendererBackend.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/RendererBackend.h:67) +- [Axiom/Session/EditorSceneRendererAdapter.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSceneRendererAdapter.cpp:25) + +Risks: + +- Resource lifetime bugs if handle ownership is underspecified. +- Mesh cache invalidation bugs during asset reassignment. +- Render debug tooling regressions if debug ids stop matching submissions. + +Test strategy: + +- Keep visibility and transform-driven render behavior covered by [Tests/LayerTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/LayerTests.cpp:594). +- Add adapter cache tests for asset swap and deletion. +- Add a submission-shape test that no upstream code depends on `VulkanMesh *`. + +Incremental or staged: + +- Incremental. + +### Phase 3: `RemoteViewportServer` decomposition + +Why third: + +- The class is already too broad for safe continued feature work. +- Splitting it after the first scalability slice avoids changing server boundaries while the rendering path is still shifting. + +Target architecture: + +- Thin server shell for HTTP and transport wiring. +- Extracted services for client registry, WebRTC/video streaming, project lifecycle, script workspace, asset service, and session command routing. +- Narrower lock scopes around client transport state versus project/session state. + +Migration strategy: + +1. Extract pure helper services without changing routes or wire format. +2. Introduce an internal command router so all browser command paths stop branching in one file. +3. Move project/script/asset HTTP behavior behind dedicated services. +4. Collapse repeated `ClientId` and `SessionUserId` resolution flows into one registry boundary. + +Files and systems affected: + +- [Headless/RemoteViewportServer.h](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.h:78) +- [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:1164) +- [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:1698) +- [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:2607) + +Risks: + +- Route regressions in browser workflows. +- Concurrency bugs if extracted services still share mutable state incorrectly. +- WebRTC session lifetime regressions during reconnect and disconnect flows. + +Test strategy: + +- Preserve protocol parse/serialize coverage in [Tests/HeadlessProtocolTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/HeadlessProtocolTests.cpp:83). +- Expand networking module tests in [Tests/WraithNetworkingModuleTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/WraithNetworkingModuleTests.cpp:73). +- Add reconnect/resume tests around session connect and WebRTC close semantics. + +Incremental or staged: + +- Incremental, but should be done as a series of service extractions rather than a single rewrite branch. + +### Phase 4: Scene identity and storage redesign + +Why fourth: + +- This is the right long-term fix for scene authority, but it is not the cheapest way to buy scalability today. +- It has the broadest cross-cutting impact across editor, serialization, physics, scripting, and collaboration. + +Target architecture: + +- Stable integer handles for scene objects and hierarchy nodes inside the authority layer. +- Parent-child hierarchy stored as arrays or stable vectors rather than recursive heap objects. +- String ids preserved only for persistence, browser protocol compatibility, and user-facing labels where needed. +- `Instance` becomes an adapter-only construct or is removed from authoritative mutations entirely. + +Migration strategy: + +1. Introduce internal `SceneObjectHandle` alongside existing string object ids. +2. Build fast handle lookup tables in `EditorSession` and `EditorSceneStateManager`. +3. Migrate runtime-facing systems first: selections, collaboration state, mesh instance ownership, physics object binding, and script instance binding. +4. Move hierarchy operations to handle-based storage. +5. Reduce the `Instance` tree to a projection layer, then decide whether it still justifies its cost. + +Files and systems affected: + +- [Axiom/Session/EditorSession.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSession.h:121) +- [Axiom/Session/EditorSceneStateManager.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSceneStateManager.h:24) +- [Axiom/CoreInstance/Instance.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/CoreInstance/Instance.h:14) +- [Axiom/Session/EditorCommandDispatcher.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorCommandDispatcher.cpp:335) +- [Axiom/Assets/SceneFile.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Assets/SceneFile.cpp:704) +- [Axiom/Scripting/ScriptHost.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Scripting/ScriptHost.h:122) + +Risks: + +- Highest regression risk in the repo. +- Save/load, selection, duplicate, delete, reparent, physics, and scripting all depend on identity stability. +- Browser-facing protocols still expect string ids today, so translation layers must stay correct during migration. + +Test strategy: + +- Use current lifecycle coverage in [Tests/SceneLifecycleTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/SceneLifecycleTests.cpp:1300) as the baseline. +- Add handle-stability tests for duplicate, delete, reparent, and save/load round-trip. +- Add scripting and physics integration tests that prove handle remapping does not orphan runtime objects. + +Incremental or staged: + +- Staged rewrite. + +## Prioritized Checklist + +### Phase 0: Measurement and guardrails + +- Add profiling counters for headless render passes, pending readbacks, and per-client frame cadence. +- Add a short benchmark harness for one client versus N clients in headless mode. +- Document baseline numbers before major refactors. + +### Phase 1: Headless throughput + +- Remove immediate offscreen `vkWaitForFences` from the render submit path. +- Publish completed offscreen captures on later ticks. +- Add per-client render scheduling policy so inactive clients do not all render at full cadence. +- Validate frame attribution and ordering after async readback. + +### Phase 2: Render submission + +- Introduce opaque mesh/resource handles. +- Remove `TypedMesh` from `RenderMeshSubmission`. +- Move backend-specific mesh resolution into the Vulkan backend. +- Stop creating render debug metadata on every submission build. + +### Phase 3: Remote viewport server decomposition + +- Extract client/session registry service. +- Extract project and script workspace services. +- Extract asset upload and listing service. +- Extract browser command router and reduce mutex scope. + +### Phase 4: Scene identity and storage + +- Add internal stable integer object handles. +- Move authority lookups and runtime bindings off string ids. +- Replace hierarchy mutations that depend on recursive `Instance *` traversal. +- Demote or remove the `Instance` tree from authoritative scene mutation paths. + +## Incremental vs Staged + +Changes that can be incremental: + +- Headless async readback and render scheduling. +- Render submission handle cleanup. +- `RemoteViewportServer` service extraction. +- Narrow handle introduction under existing string ids. + +Changes that require a staged rewrite: + +- Replacing authoritative scene hierarchy storage. +- Removing `Instance` from core mutation paths. +- Converting all runtime-facing scene identity from strings to handles. + +## Recommended First Slice + +Start with headless scalability, not scene storage. + +Minimal first slice: + +1. Make offscreen frame capture truly asynchronous by removing the immediate fence wait in [Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp:782). +2. Keep current per-client render passes, but add simple render scheduling in [Headless/HeadlessSessionHost.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionHost.cpp:134) so unchanged or background clients do not all render every tick. +3. Instrument the runtime so the team can measure improvement before taking on bigger structural work. + +Why this slice: + +- It directly targets the two most obvious scalability bottlenecks. +- It avoids destabilizing scene editing, serialization, scripting, and collaboration semantics. +- It preserves the current browser/session protocol while improving the host runtime beneath it. + +## Go / No-Go Guidance + +Go: + +- Start with headless multi-client scalability. +- Follow with render submission cleanup once the headless path is no longer serialized. + +Conditional go: + +- Begin `RemoteViewportServer` decomposition after the first scalability slice, especially if more browser-facing features are planned. + +No-go: + +- Do not start with scene storage as the first refactor. +- Do not start with a full multi-client redesign that assumes shared image reuse or compositor-level fanout before removing the current fence stall and measuring the real remaining cost. + +## Evidence Sources + +- [Axiom/Session/EditorSession.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSession.h:130) +- [Axiom/Session/EditorSceneStateManager.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSceneStateManager.cpp:173) +- [Axiom/CoreInstance/Instance.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/CoreInstance/Instance.h:42) +- [Axiom/Session/EditorCommandDispatcher.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorCommandDispatcher.cpp:335) +- [Axiom/Session/EditorSceneRendererAdapter.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Session/EditorSceneRendererAdapter.cpp:25) +- [Axiom/Renderer/Mesh.h](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Mesh.h:81) +- [Axiom/Renderer/Mesh.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Mesh.cpp:41) +- [Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Renderer/Vulkan/VulkanDrawSubmissionSystem.cpp:770) +- [Axiom/Core/Application.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Axiom/Core/Application.cpp:152) +- [Headless/HeadlessSessionHost.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionHost.cpp:134) +- [Headless/HeadlessSessionLayer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/HeadlessSessionLayer.cpp:31) +- [Headless/RemoteViewportServer.h](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.h:56) +- [Headless/RemoteViewportServer.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Headless/RemoteViewportServer.cpp:1266) +- [Tests/LayerTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/LayerTests.cpp:594) +- [Tests/LayerTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/LayerTests.cpp:940) +- [Tests/SceneLifecycleTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/SceneLifecycleTests.cpp:1300) +- [Tests/HeadlessProtocolTests.cpp](/Users/joshua/Documents/GitHub/WraithEngine/Tests/HeadlessProtocolTests.cpp:512) diff --git a/Editor/CMakeLists.txt b/Editor/CMakeLists.txt index 197032d1..8bc705b2 100644 --- a/Editor/CMakeLists.txt +++ b/Editor/CMakeLists.txt @@ -1,6 +1,19 @@ add_executable(AxiomEditor + EditorFeatureModules.cpp EditorApplication.cpp - GlfwEditorLayer.cpp + GlfwEditorModule.cpp ) target_link_libraries(AxiomEditor PRIVATE AxiomCore) +target_link_libraries(AxiomEditor PRIVATE + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet +) +if(TARGET AxiomPhysics) + target_link_libraries(AxiomEditor PRIVATE AxiomPhysics) +endif() +if(TARGET AxiomScripting) + target_link_libraries(AxiomEditor PRIVATE AxiomScripting) +endif() diff --git a/Editor/EditorApplication.cpp b/Editor/EditorApplication.cpp index 8eaf2318..ece4a23e 100644 --- a/Editor/EditorApplication.cpp +++ b/Editor/EditorApplication.cpp @@ -1,8 +1,10 @@ #include +#include #include #include +#include -#include "GlfwEditorLayer.h" +#include "GlfwEditorModule.h" class EditorApplication : public Axiom::Application { public: @@ -11,8 +13,11 @@ class EditorApplication : public Axiom::Application { .Width = 1600, .Height = 900, .Mode = Axiom::RuntimeMode::LocalWindowedEditor}, - Args) { - PushLayer(new Axiom::GlfwEditorLayer()); + Args, + {.RegisterDefaultModules = false}) { + GetModuleManager().RegisterModule(std::make_unique()); + GetModuleManager().RegisterModule(std::make_unique()); + GetModuleManager().RegisterModule(std::make_unique()); } }; diff --git a/Editor/EditorFeatureModules.cpp b/Editor/EditorFeatureModules.cpp new file mode 100644 index 00000000..7a91017b --- /dev/null +++ b/Editor/EditorFeatureModules.cpp @@ -0,0 +1,106 @@ +#include "EditorFeatureModules.h" + +#include +#include +#include +#include +#include +#include + +#include + +#define GLFW_INCLUDE_NONE +#include + +namespace Axiom { +void EditorViewportInputModule::Initialize(Window &Window, float MoveSpeed) { + m_WindowInputPlatform = std::make_unique(Window); + m_InputSource = + std::make_unique(*m_WindowInputPlatform, MoveSpeed); +} + +void EditorViewportInputModule::Shutdown() { + if (m_WindowInputPlatform != nullptr) { + m_WindowInputPlatform->SetCursorMode(CursorMode::Normal); + } + m_InputSource.reset(); + m_WindowInputPlatform.reset(); +} + +void EditorViewportInputModule::Tick(EditorSession &Session, + SessionId SessionHandle, + SessionUserId LocalUserId) { + const EditorViewportState *Viewport = Session.FindViewport(LocalUserId); + if (m_InputSource != nullptr) { + m_InputSource->Tick({ + .Session = SessionHandle, + .User = LocalUserId, + .FrameIndex = Application::Get().GetFrameIndex(), + .DeltaTimeSeconds = Application::Get().GetDeltaTime(), + .Viewport = Viewport, + .CommandSink = &Session, + }); + } + + if (m_InputSource != nullptr) { + m_InputSource->SyncViewport(Session.FindViewport(LocalUserId)); + } +} + +void EditorViewportSelectionModule::Tick(EditorSession &Session, + SessionId SessionHandle, + SessionUserId LocalUserId, + IInputPlatform *InputPlatform, + const Window *Window, + bool &LastLeftMouseDown) { + const EditorViewportState *Viewport = Session.FindViewport(LocalUserId); + const bool IsLeftDown = InputPlatform != nullptr && + InputPlatform->IsMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT); + const bool ClickedNow = IsLeftDown && !LastLeftMouseDown; + LastLeftMouseDown = IsLeftDown; + + if (!ClickedNow || Viewport == nullptr || Viewport->IsLooking || + InputPlatform == nullptr || Window == nullptr || + ImGui::GetIO().WantCaptureMouse) { + return; + } + + const glm::dvec2 CursorPos = InputPlatform->GetCursorPosition(); + const std::string HitId = + HitTestMeshes(Viewport->Camera, Window->GetWidth(), Window->GetHeight(), + glm::vec2(CursorPos), Session.GetState().Scene.MeshInstances); + if (HitId.empty()) { + return; + } + + const CommandContext Context{ + .Session = SessionHandle, + .User = LocalUserId, + .FrameIndex = Application::Get().GetFrameIndex(), + .DeltaTimeSeconds = Application::Get().GetDeltaTime(), + }; + Session.Submit(Context, EditorCommand{SelectObjectCommand{.ObjectId = HitId}}); +} + +void EditorSceneRenderModule::Render(EditorSession &Session, + SessionUserId LocalUserId, + EditorSceneRendererAdapter &RendererAdapter) { + const EditorViewportState *Viewport = Session.FindViewport(LocalUserId); + if (Viewport == nullptr) { + return; + } + + RenderCommand::SetCamera(Viewport->Camera); + for (const auto &Submission : RendererAdapter.BuildRenderSubmissions(Session)) { + RenderCommand::Submit(Submission); + } + + const EditorObjectDetails *Selected = + Session.FindSelectedObjectDetails(LocalUserId); + if (Selected != nullptr && Selected->SupportsTransform && + Selected->Transform.has_value()) { + RenderCommand::SetGizmoOverlay( + {.WorldPosition = Selected->Transform->Location, .Scale = 0.5f}); + } +} +} // namespace Axiom diff --git a/Editor/EditorFeatureModules.h b/Editor/EditorFeatureModules.h new file mode 100644 index 00000000..2229c1aa --- /dev/null +++ b/Editor/EditorFeatureModules.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace Axiom { +class Window; + +class EditorViewportInputModule { +public: + void Initialize(Window &Window, float MoveSpeed); + void Shutdown(); + void Tick(EditorSession &Session, SessionId SessionHandle, + SessionUserId LocalUserId); + + IInputPlatform *GetInputPlatform() const { return m_WindowInputPlatform.get(); } + +private: + std::unique_ptr m_WindowInputPlatform; + std::unique_ptr m_InputSource; +}; + +class EditorViewportSelectionModule { +public: + void Tick(EditorSession &Session, SessionId SessionHandle, + SessionUserId LocalUserId, IInputPlatform *InputPlatform, + const Window *Window, bool &LastLeftMouseDown); +}; + +class EditorSceneRenderModule { +public: + void Render(EditorSession &Session, SessionUserId LocalUserId, + EditorSceneRendererAdapter &RendererAdapter); +}; +} // namespace Axiom diff --git a/Editor/GlfwEditorLayer.cpp b/Editor/GlfwEditorLayer.cpp deleted file mode 100644 index 9fcf7199..00000000 --- a/Editor/GlfwEditorLayer.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "GlfwEditorLayer.h" - -#include -#include -#include -#include - -#define GLFW_INCLUDE_NONE -#include - -#include -#include -#include - -namespace Axiom { -GlfwEditorLayer::GlfwEditorLayer() - : Layer("GlfwEditorLayer"), m_Session(m_SessionId) {} - -void GlfwEditorLayer::OnAttach() { - m_Session.EnsureViewportState(m_LocalUserId); - Window *Window = Application::Get().GetWindow(); - if (Window != nullptr) { - m_WindowInputPlatform = std::make_unique(*Window); - m_InputSource = - std::make_unique(*m_WindowInputPlatform, - m_MoveSpeed); - } - LoadStartupScene(m_Session); -} - -void GlfwEditorLayer::OnDetach() { - if (m_WindowInputPlatform != nullptr) { - m_WindowInputPlatform->SetCursorMode(CursorMode::Normal); - } -} - -void GlfwEditorLayer::OnUpdate() { - const EditorViewportState *Viewport = m_Session.FindViewport(m_LocalUserId); - if (m_InputSource != nullptr) { - m_InputSource->Tick({ - .Session = m_SessionId, - .User = m_LocalUserId, - .FrameIndex = Application::Get().GetFrameIndex(), - .DeltaTimeSeconds = Application::Get().GetDeltaTime(), - .Viewport = Viewport, - .CommandSink = &m_Session, - }); - } - m_Session.Tick(); - - // Left-click mesh picking — detect rising edge to select on click, not hold. - { - const bool IsLeftDown = m_WindowInputPlatform != nullptr && - m_WindowInputPlatform->IsMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT); - const bool ClickedNow = IsLeftDown && !m_LastLeftMouseDown; - m_LastLeftMouseDown = IsLeftDown; - - if (ClickedNow && Viewport != nullptr && !Viewport->IsLooking) { - const Window *Win = Application::Get().GetWindow(); - if (Win != nullptr) { - const glm::dvec2 CursorPos = m_WindowInputPlatform->GetCursorPosition(); - const std::string HitId = HitTestMeshes( - Viewport->Camera, Win->GetWidth(), Win->GetHeight(), - glm::vec2(CursorPos), m_Session.GetState().Scene.MeshInstances); - if (!HitId.empty()) { - const CommandContext Ctx{ - .Session = m_SessionId, - .User = m_LocalUserId, - .FrameIndex = Application::Get().GetFrameIndex(), - .DeltaTimeSeconds = Application::Get().GetDeltaTime(), - }; - m_Session.Submit(Ctx, EditorCommand{SelectObjectCommand{.ObjectId = HitId}}); - } - } - } - } - - if (m_InputSource != nullptr) { - m_InputSource->SyncViewport(m_Session.FindViewport(m_LocalUserId)); - } -} - -void GlfwEditorLayer::OnRender() { - const EditorViewportState *Viewport = m_Session.FindViewport(m_LocalUserId); - if (Viewport == nullptr) { - return; - } - - RenderCommand::SetCamera(Viewport->Camera); - for (const auto &Submission : m_RendererAdapter.BuildRenderSubmissions(m_Session)) { - RenderCommand::Submit(Submission); - } - - const EditorObjectDetails *Selected = - m_Session.FindSelectedObjectDetails(m_LocalUserId); - if (Selected != nullptr && Selected->SupportsTransform && - Selected->Transform.has_value()) { - RenderCommand::SetGizmoOverlay( - {.WorldPosition = Selected->Transform->Location, .Scale = 0.5f}); - } -} -} // namespace Axiom diff --git a/Editor/GlfwEditorLayer.h b/Editor/GlfwEditorLayer.h deleted file mode 100644 index 09e5984d..00000000 --- a/Editor/GlfwEditorLayer.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include -#include -#include - -namespace Axiom { -class GlfwEditorLayer final : public Layer { -public: - GlfwEditorLayer(); - - void OnAttach() override; - void OnDetach() override; - void OnUpdate() override; - void OnRender() override; - -private: - SessionId m_SessionId{1}; - SessionUserId m_LocalUserId{1}; - EditorSession m_Session; - EditorSceneRendererAdapter m_RendererAdapter; - std::unique_ptr m_WindowInputPlatform; - std::unique_ptr m_InputSource; - float m_MoveSpeed{3.5f}; - bool m_LastLeftMouseDown{false}; -}; -} // namespace Axiom diff --git a/Editor/GlfwEditorModule.cpp b/Editor/GlfwEditorModule.cpp new file mode 100644 index 00000000..e14a9c2c --- /dev/null +++ b/Editor/GlfwEditorModule.cpp @@ -0,0 +1,87 @@ +#include "GlfwEditorModule.h" + +#include +#include +#include +#include + +#include + +#ifndef AXIOM_CONTENT_DIR +#define AXIOM_CONTENT_DIR "Content" +#endif + +namespace Axiom { +namespace { +bool LooksLikeContentRoot(const std::filesystem::path &Path) { + if (Path.empty()) { + return false; + } + + std::error_code Error; + if (!std::filesystem::exists(Path, Error) || Error) { + return false; + } + + return std::filesystem::exists(Path / "scene.json") || + std::filesystem::exists(Path / "Cooked" / "AssetCookManifest.json") || + Assets::IsCookedOnlyContentPath(Path); +} + +std::filesystem::path ResolveEditorContentRoot() { + std::error_code Error; + const std::filesystem::path CurrentPath = std::filesystem::current_path(Error); + if (!Error) { + if (LooksLikeContentRoot(CurrentPath)) { + return CurrentPath; + } + if (LooksLikeContentRoot(CurrentPath / "Content")) { + return CurrentPath / "Content"; + } + } + + return std::filesystem::path(AXIOM_CONTENT_DIR); +} +} // namespace + +GlfwEditorModule::GlfwEditorModule() : m_Session(m_SessionId) {} + +std::string_view GlfwEditorModule::GetName() const { + return "Editor.GlfwEditor"; +} + +bool GlfwEditorModule::Initialize(Application &App) { + m_Session.EnsureViewportState(m_LocalUserId); + m_Session.SetContentDir(ResolveEditorContentRoot()); + m_Session.SetEngineContentDir(std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine"); + Window *Window = App.GetWindow(); + if (Window != nullptr) { + m_InputModule.Initialize(*Window, m_MoveSpeed); + } + return LoadStartupScene(m_Session); +} + +void GlfwEditorModule::Update(const ModuleUpdateContext &Context) { + switch (Context.Phase) { + case ModuleUpdatePhase::FrameStart: + m_InputModule.Tick(m_Session, m_SessionId, m_LocalUserId); + m_Session.Tick(); + m_SelectionModule.Tick(m_Session, m_SessionId, m_LocalUserId, + m_InputModule.GetInputPlatform(), + Context.App.GetWindow(), m_LastLeftMouseDown); + break; + case ModuleUpdatePhase::Render: + m_RenderModule.Render(m_Session, m_LocalUserId, m_RendererAdapter); + break; + case ModuleUpdatePhase::RenderBegin: + case ModuleUpdatePhase::ImGuiRender: + case ModuleUpdatePhase::RenderEnd: + break; + } +} + +void GlfwEditorModule::Shutdown(Application &App) { + (void)App; + m_InputModule.Shutdown(); +} +} // namespace Axiom diff --git a/Editor/GlfwEditorModule.h b/Editor/GlfwEditorModule.h new file mode 100644 index 00000000..cdff8cbf --- /dev/null +++ b/Editor/GlfwEditorModule.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "EditorFeatureModules.h" + +#include +#include + +namespace Axiom { +class GlfwEditorModule final : public IModule { +public: + GlfwEditorModule(); + + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; + +private: + SessionId m_SessionId{1}; + SessionUserId m_LocalUserId{1}; + EditorSession m_Session; + EditorSceneRendererAdapter m_RendererAdapter; + EditorViewportInputModule m_InputModule; + EditorViewportSelectionModule m_SelectionModule; + EditorSceneRenderModule m_RenderModule; + float m_MoveSpeed{3.5f}; + bool m_LastLeftMouseDown{false}; +}; +} // namespace Axiom diff --git a/EditorFrontend/components/engine/viewport-movement.js b/EditorFrontend/components/engine/viewport-movement.js new file mode 100644 index 00000000..48815c33 --- /dev/null +++ b/EditorFrontend/components/engine/viewport-movement.js @@ -0,0 +1,38 @@ +export function buildViewportWorldMovement({ + pressedKeys, + yawDegrees, + pitchDegrees, + step = 0.08, +}) { + const forwardInput = + (pressedKeys.has("KeyW") ? 1 : 0) - (pressedKeys.has("KeyS") ? 1 : 0) + const strafe = + (pressedKeys.has("KeyD") ? 1 : 0) - (pressedKeys.has("KeyA") ? 1 : 0) + const lift = + (pressedKeys.has("Space") ? 1 : 0) - + ((pressedKeys.has("ShiftLeft") || pressedKeys.has("ShiftRight")) ? 1 : 0) + + const yawRadians = (yawDegrees * Math.PI) / 180 + const pitchRadians = (pitchDegrees * Math.PI) / 180 + + let forwardX = Math.cos(yawRadians) * Math.cos(pitchRadians) + let forwardZ = Math.sin(yawRadians) * Math.cos(pitchRadians) + const horizontalLength = Math.hypot(forwardX, forwardZ) + + if (horizontalLength > 0) { + forwardX /= horizontalLength + forwardZ /= horizontalLength + } else { + forwardX = 0 + forwardZ = -1 + } + + const rightX = -forwardZ + const rightZ = forwardX + + return [ + (rightX * strafe + forwardX * forwardInput) * step, + lift * step, + (rightZ * strafe + forwardZ * forwardInput) * step, + ] +} diff --git a/EditorFrontend/components/engine/viewport-movement.test.mjs b/EditorFrontend/components/engine/viewport-movement.test.mjs new file mode 100644 index 00000000..c1a46b1b --- /dev/null +++ b/EditorFrontend/components/engine/viewport-movement.test.mjs @@ -0,0 +1,54 @@ +import test from "node:test" +import assert from "node:assert/strict" + +import { buildViewportWorldMovement } from "./viewport-movement.js" + +function nearlyEqual(actual, expected, epsilon = 1e-6) { + assert.equal(actual.length, expected.length) + for (let index = 0; index < actual.length; index += 1) { + assert.ok( + Math.abs(actual[index] - expected[index]) <= epsilon, + `component ${index} expected ${expected[index]}, got ${actual[index]}` + ) + } +} + +test("W moves forward relative to default editor camera yaw", () => { + const movement = buildViewportWorldMovement({ + pressedKeys: new Set(["KeyW"]), + yawDegrees: -90, + pitchDegrees: 0, + }) + + nearlyEqual(movement, [0, 0, -0.08]) +}) + +test("S moves backward relative to default editor camera yaw", () => { + const movement = buildViewportWorldMovement({ + pressedKeys: new Set(["KeyS"]), + yawDegrees: -90, + pitchDegrees: 0, + }) + + nearlyEqual(movement, [0, 0, 0.08]) +}) + +test("movement rotates with camera yaw instead of staying on world axes", () => { + const movement = buildViewportWorldMovement({ + pressedKeys: new Set(["KeyW"]), + yawDegrees: 0, + pitchDegrees: 0, + }) + + nearlyEqual(movement, [0.08, 0, 0]) +}) + +test("strafing uses the camera-relative right vector", () => { + const movement = buildViewportWorldMovement({ + pressedKeys: new Set(["KeyD"]), + yawDegrees: -90, + pitchDegrees: 0, + }) + + nearlyEqual(movement, [0.08, 0, 0]) +}) diff --git a/EditorFrontend/components/engine/viewport.tsx b/EditorFrontend/components/engine/viewport.tsx index 9576acbe..3463ade0 100644 --- a/EditorFrontend/components/engine/viewport.tsx +++ b/EditorFrontend/components/engine/viewport.tsx @@ -28,6 +28,7 @@ import { type SessionSceneItem, type SessionSelection, } from "./remote-viewport-context" +import { buildViewportWorldMovement } from "./viewport-movement" import { getServerOrigin } from "./server-origin" const CLIENT_ID_STORAGE_KEY = "axiom-remote-client-id" const CLIENT_ID_CLAIM_CHANNEL = "axiom-remote-client-claims" @@ -301,6 +302,7 @@ export function Viewport() { const sendCommandRef = useRef< (command: RemoteViewportCommand, preferredChannel?: ChannelPreference) => Promise >(async () => false) + const currentUserIdRef = useRef(null) const { connectionState, statusText, @@ -337,6 +339,7 @@ export function Viewport() { sessionStatusText, setSessionStatusText, setSessionDetailText, + currentUserId, setGizmoMode: setGizmoModeCtx, setProjectionType: setProjectionTypeCtx, runtimeState, @@ -364,6 +367,10 @@ export function Viewport() { participantsRef.current = participants }, [participants]) + useEffect(() => { + currentUserIdRef.current = currentUserId + }, [currentUserId]) + useEffect(() => { function syncFullscreenState() { setIsFullscreen(document.fullscreenElement === viewportShellRef.current) @@ -1043,13 +1050,18 @@ export function Viewport() { sendCommandRef.current = sendCommand function movementVector(): [number, number, number] { - const keys = keysRef.current - const forward = (keys.has("KeyW") ? 1 : 0) - (keys.has("KeyS") ? 1 : 0) - const strafe = (keys.has("KeyD") ? 1 : 0) - (keys.has("KeyA") ? 1 : 0) - const lift = - (keys.has("Space") ? 1 : 0) - - ((keys.has("ShiftLeft") || keys.has("ShiftRight")) ? 1 : 0) - return [strafe * 0.08, lift * 0.08, forward * 0.08] + const localParticipant = + currentUserIdRef.current !== null + ? participantsRef.current.find( + (participant) => participant.userId === currentUserIdRef.current + ) + : null + + return buildViewportWorldMovement({ + pressedKeys: keysRef.current, + yawDegrees: localParticipant?.camera?.yawDegrees ?? -90, + pitchDegrees: localParticipant?.camera?.pitchDegrees ?? 0, + }) as [number, number, number] } function applyPendingLook() { diff --git a/EditorFrontend/package.json b/EditorFrontend/package.json index 0ca9ea12..2bf2f066 100644 --- a/EditorFrontend/package.json +++ b/EditorFrontend/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint ." + "lint": "eslint .", + "test": "node --test components/engine/viewport-movement.test.mjs" }, "dependencies": { "@hookform/resolvers": "^3.9.1", @@ -76,4 +77,4 @@ "sharp" ] } -} \ No newline at end of file +} diff --git a/HAL/CMakeLists.txt b/HAL/CMakeLists.txt new file mode 100644 index 00000000..f4fb0d09 --- /dev/null +++ b/HAL/CMakeLists.txt @@ -0,0 +1,93 @@ +add_library(AxiomHAL STATIC + DynamicLibrary.cpp + FileWatcher.cpp + Platform.cpp + PlatformMedia.cpp + Socket.cpp + SvgRasterizer.cpp +) + +if(APPLE) + target_sources(AxiomHAL PRIVATE + MacOS/MacOSFileWatcher.cpp + MacOS/MacOSSvgRasterizer.cpp + MacOS/MacOSVideoToolboxH264Encoder.mm + MacOS/MacOSWebRtcSession.mm + ) +endif() + +target_include_directories(AxiomHAL PUBLIC + "${CMAKE_SOURCE_DIR}" + "${CMAKE_SOURCE_DIR}/Axiom" + "${CMAKE_SOURCE_DIR}/Axiom/Scene" + "${CMAKE_SOURCE_DIR}/Headless" + "${CMAKE_SOURCE_DIR}/ThirdParty/fastgltf/include" + "${CMAKE_SOURCE_DIR}/ThirdParty/glfw/include" + "${CMAKE_SOURCE_DIR}/ThirdParty/glm" + "${CMAKE_SOURCE_DIR}/ThirdParty/spdlog/include" + "${CMAKE_SOURCE_DIR}/ThirdParty/stb_image" + "${rapidjson_SOURCE_DIR}/include" + "$<$:${AXIOM_JOLT_SOURCE_DIR}>" +) + +target_include_directories(AxiomHAL PRIVATE + "${CMAKE_SOURCE_DIR}/ThirdParty/vkbootstrap" + "${CMAKE_SOURCE_DIR}/ThirdParty/vma" + "${CMAKE_SOURCE_DIR}/ThirdParty/volk" + "${Vulkan_INCLUDE_DIRS}" +) + +target_compile_definitions(AxiomHAL PUBLIC + AXIOM_WEBRTC_LINKED=${AXIOM_WEBRTC_LINKED} +) + +if(AXIOM_ENABLE_WEBRTC) + target_compile_definitions(AxiomHAL PUBLIC AXIOM_ENABLE_WEBRTC=1) +else() + target_compile_definitions(AxiomHAL PUBLIC AXIOM_ENABLE_WEBRTC=0) +endif() + +if(AXIOM_SCRIPTING_WATCH AND APPLE) + target_compile_definitions(AxiomHAL PUBLIC AXIOM_SCRIPTING_WATCH=1) +else() + target_compile_definitions(AxiomHAL PUBLIC AXIOM_SCRIPTING_WATCH=0) +endif() + +if(AXIOM_VULKAN_LOADER_PATH) + string(REPLACE "\\" "\\\\" AXIOM_VULKAN_LOADER_PATH_ESCAPED + "${AXIOM_VULKAN_LOADER_PATH}") + target_compile_definitions(AxiomHAL PUBLIC + AXIOM_VULKAN_LOADER_PATH="${AXIOM_VULKAN_LOADER_PATH_ESCAPED}" + ) +endif() + +if(AXIOM_WEBRTC_COMPILE_OPTIONS) + target_compile_options(AxiomHAL PUBLIC ${AXIOM_WEBRTC_COMPILE_OPTIONS}) +endif() + +if(AXIOM_WEBRTC_LINK_OPTIONS) + target_link_options(AxiomHAL PUBLIC ${AXIOM_WEBRTC_LINK_OPTIONS}) +endif() + +if(AXIOM_WEBRTC_LIBRARIES) + target_link_libraries(AxiomHAL PUBLIC ${AXIOM_WEBRTC_LIBRARIES}) +endif() + +if(AXIOM_ENABLE_WEBRTC AND TARGET AxiomWebRTC) + target_link_libraries(AxiomHAL PUBLIC AxiomWebRTC) +endif() + +if(UNIX AND NOT APPLE) + target_link_libraries(AxiomHAL PUBLIC ${CMAKE_DL_LIBS}) +endif() + +if(APPLE) + target_link_libraries(AxiomHAL PUBLIC + "-framework CoreFoundation" + "-framework CoreGraphics" + "-framework CoreMedia" + "-framework CoreVideo" + "-framework Foundation" + "-framework VideoToolbox" + ) +endif() diff --git a/HAL/DynamicLibrary.cpp b/HAL/DynamicLibrary.cpp new file mode 100644 index 00000000..22c6d75f --- /dev/null +++ b/HAL/DynamicLibrary.cpp @@ -0,0 +1,97 @@ +#include "HAL/DynamicLibrary.h" + +#include "HAL/Platform.h" + +#if AXIOM_PLATFORM_WINDOWS +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + +namespace Axiom::HAL { +DynamicLibrary::~DynamicLibrary() { Reset(); } + +DynamicLibrary::DynamicLibrary(DynamicLibrary &&Other) noexcept + : m_Handle(Other.m_Handle) { + Other.m_Handle = nullptr; +} + +DynamicLibrary &DynamicLibrary::operator=(DynamicLibrary &&Other) noexcept { + if (this == &Other) { + return *this; + } + + Reset(); + m_Handle = Other.m_Handle; + Other.m_Handle = nullptr; + return *this; +} + +bool DynamicLibrary::Open(const char *Path) { + Reset(); + +#if AXIOM_PLATFORM_WINDOWS + m_Handle = reinterpret_cast(LoadLibraryA(Path)); +#else + m_Handle = dlopen(Path, RTLD_NOW | RTLD_LOCAL); +#endif + + return m_Handle != nullptr; +} + +void DynamicLibrary::Reset() { + if (m_Handle == nullptr) { + return; + } + +#if AXIOM_PLATFORM_WINDOWS + FreeLibrary(reinterpret_cast(m_Handle)); +#else + dlclose(m_Handle); +#endif + m_Handle = nullptr; +} + +void *DynamicLibrary::FindSymbol(const char *Name) const { + if (m_Handle == nullptr) { + return nullptr; + } + +#if AXIOM_PLATFORM_WINDOWS + return reinterpret_cast( + GetProcAddress(reinterpret_cast(m_Handle), Name)); +#else + return dlsym(m_Handle, Name); +#endif +} + +std::string DynamicLibrary::GetLastError() { +#if AXIOM_PLATFORM_WINDOWS + const DWORD ErrorCode = ::GetLastError(); + if (ErrorCode == 0) { + return "unknown Windows loader error"; + } + + LPSTR MessageBuffer = nullptr; + const DWORD MessageLength = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, ErrorCode, 0, reinterpret_cast(&MessageBuffer), 0, + nullptr); + std::string Message = + MessageLength > 0 && MessageBuffer != nullptr + ? MessageBuffer + : "unknown Windows loader error"; + if (MessageBuffer != nullptr) { + LocalFree(MessageBuffer); + } + return Message; +#else + if (const char *Error = dlerror()) { + return Error; + } + return "unknown POSIX loader error"; +#endif +} +} // namespace Axiom::HAL diff --git a/HAL/DynamicLibrary.h b/HAL/DynamicLibrary.h new file mode 100644 index 00000000..20b95b0e --- /dev/null +++ b/HAL/DynamicLibrary.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace Axiom::HAL { +class DynamicLibrary final { +public: + DynamicLibrary() = default; + ~DynamicLibrary(); + + DynamicLibrary(const DynamicLibrary &) = delete; + DynamicLibrary &operator=(const DynamicLibrary &) = delete; + + DynamicLibrary(DynamicLibrary &&Other) noexcept; + DynamicLibrary &operator=(DynamicLibrary &&Other) noexcept; + + [[nodiscard]] bool Open(const char *Path); + void Reset(); + + [[nodiscard]] bool IsValid() const { return m_Handle != nullptr; } + [[nodiscard]] void *FindSymbol(const char *Name) const; + + [[nodiscard]] static std::string GetLastError(); + +private: + void *m_Handle{nullptr}; +}; +} // namespace Axiom::HAL diff --git a/HAL/FileWatcher.cpp b/HAL/FileWatcher.cpp new file mode 100644 index 00000000..f22823f3 --- /dev/null +++ b/HAL/FileWatcher.cpp @@ -0,0 +1,35 @@ +#include "HAL/FileWatcher.h" + +#include "HAL/Platform.h" + +namespace Axiom::HAL { +namespace { +class NullFileWatcher final : public IFileWatcher { +public: + bool StartWatching(const std::filesystem::path &Path, + std::function OnChanged, + std::string &Error) override { + (void)Path; + (void)OnChanged; + Error = "file watching is not implemented for this platform"; + return false; + } + + void StopWatching() override {} + + [[nodiscard]] bool IsWatching() const override { return false; } +}; +} // namespace + +#if AXIOM_PLATFORM_MACOS && AXIOM_SCRIPTING_WATCH +std::unique_ptr CreateMacOSFileWatcher(); +#endif + +std::unique_ptr CreateFileWatcher() { +#if AXIOM_PLATFORM_MACOS && AXIOM_SCRIPTING_WATCH + return CreateMacOSFileWatcher(); +#else + return std::make_unique(); +#endif +} +} // namespace Axiom::HAL diff --git a/HAL/FileWatcher.h b/HAL/FileWatcher.h new file mode 100644 index 00000000..c6d021f6 --- /dev/null +++ b/HAL/FileWatcher.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +namespace Axiom::HAL { +class IFileWatcher { +public: + virtual ~IFileWatcher() = default; + + virtual bool StartWatching(const std::filesystem::path &Path, + std::function OnChanged, + std::string &Error) = 0; + virtual void StopWatching() = 0; + [[nodiscard]] virtual bool IsWatching() const = 0; +}; + +std::unique_ptr CreateFileWatcher(); +} // namespace Axiom::HAL diff --git a/HAL/MacOS/MacOSFileWatcher.cpp b/HAL/MacOS/MacOSFileWatcher.cpp new file mode 100644 index 00000000..85f3e251 --- /dev/null +++ b/HAL/MacOS/MacOSFileWatcher.cpp @@ -0,0 +1,100 @@ +#include "HAL/FileWatcher.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace Axiom::HAL { +namespace { +class MacOSFileWatcher final : public IFileWatcher { +public: + ~MacOSFileWatcher() override { StopWatching(); } + + bool StartWatching(const std::filesystem::path &Path, + std::function OnChanged, + std::string &Error) override { + StopWatching(); + + if (Path.empty()) { + Error = "watch path is empty"; + return false; + } + + m_Running.store(true); + m_Thread = std::thread([this, WatchPath = Path, Callback = std::move(OnChanged)] { + const std::filesystem::path WatchDirectory = WatchPath.parent_path(); + + const int KqueueHandle = kqueue(); + if (KqueueHandle < 0) { + m_Running.store(false); + return; + } + + const int DirectoryHandle = open(WatchDirectory.c_str(), O_RDONLY | O_EVTONLY); + if (DirectoryHandle < 0) { + close(KqueueHandle); + m_Running.store(false); + return; + } + + struct kevent Change; + EV_SET(&Change, DirectoryHandle, EVFILT_VNODE, EV_ADD | EV_CLEAR, + NOTE_WRITE | NOTE_EXTEND | NOTE_RENAME, 0, nullptr); + kevent(KqueueHandle, &Change, 1, nullptr, 0, nullptr); + + std::filesystem::file_time_type LastWriteTime{}; + if (std::filesystem::exists(WatchPath)) { + LastWriteTime = std::filesystem::last_write_time(WatchPath); + } + + while (m_Running.load()) { + struct kevent Event; + struct timespec Timeout{1, 0}; + const int EventCount = kevent(KqueueHandle, nullptr, 0, &Event, 1, &Timeout); + if (EventCount <= 0 || !std::filesystem::exists(WatchPath)) { + continue; + } + + const auto NewWriteTime = std::filesystem::last_write_time(WatchPath); + if (NewWriteTime != LastWriteTime) { + LastWriteTime = NewWriteTime; + Callback(); + } + } + + close(DirectoryHandle); + close(KqueueHandle); + }); + + if (!m_Thread.joinable()) { + m_Running.store(false); + Error = "failed to start watcher thread"; + return false; + } + + return true; + } + + void StopWatching() override { + if (m_Running.exchange(false) && m_Thread.joinable()) { + m_Thread.join(); + } + } + + [[nodiscard]] bool IsWatching() const override { return m_Running.load(); } + +private: + std::thread m_Thread; + std::atomic m_Running{false}; +}; +} // namespace + +std::unique_ptr CreateMacOSFileWatcher() { + return std::make_unique(); +} +} // namespace Axiom::HAL diff --git a/HAL/MacOS/MacOSSvgRasterizer.cpp b/HAL/MacOS/MacOSSvgRasterizer.cpp new file mode 100644 index 00000000..712bd01b --- /dev/null +++ b/HAL/MacOS/MacOSSvgRasterizer.cpp @@ -0,0 +1,302 @@ +#include "HAL/SvgRasterizer.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace Axiom::HAL { +namespace { + +struct SvgViewBox { + float MinX{0.0f}; + float MinY{0.0f}; + float Width{0.0f}; + float Height{0.0f}; +}; + +std::optional FindAttributeValue(std::string_view Text, + std::string_view Name) { + const std::string Key = std::string(Name) + "=\""; + const size_t Start = Text.find(Key); + if (Start == std::string_view::npos) { + return std::nullopt; + } + const size_t ValueStart = Start + Key.size(); + const size_t ValueEnd = Text.find('"', ValueStart); + if (ValueEnd == std::string_view::npos) { + return std::nullopt; + } + return Text.substr(ValueStart, ValueEnd - ValueStart); +} + +bool ParseFloatToken(const char *&Cursor, const char *End, float &OutValue) { + while (Cursor < End && + (std::isspace(static_cast(*Cursor)) || *Cursor == ',')) { + ++Cursor; + } + if (Cursor >= End) { + return false; + } + + char *ParseEnd = nullptr; + OutValue = std::strtof(Cursor, &ParseEnd); + if (ParseEnd == Cursor) { + return false; + } + Cursor = ParseEnd; + return true; +} + +std::optional ParseViewBox(std::string_view Value) { + SvgViewBox Result{}; + const char *Cursor = Value.data(); + const char *End = Value.data() + Value.size(); + if (!ParseFloatToken(Cursor, End, Result.MinX) || + !ParseFloatToken(Cursor, End, Result.MinY) || + !ParseFloatToken(Cursor, End, Result.Width) || + !ParseFloatToken(Cursor, End, Result.Height)) { + return std::nullopt; + } + if (Result.Width <= 0.0f || Result.Height <= 0.0f) { + return std::nullopt; + } + return Result; +} + +class SvgPathParser { +public: + explicit SvgPathParser(std::string_view PathData) + : m_Cursor(PathData.data()), m_End(PathData.data() + PathData.size()) {} + + bool Parse(CGMutablePathRef Path) { + char Command = 0; + while (SkipSeparators()) { + if (std::isalpha(static_cast(*m_Cursor)) != 0) { + Command = *m_Cursor++; + } else if (Command == 0) { + return false; + } + + switch (Command) { + case 'M': + case 'm': + if (!ParseMoveTo(Path, Command == 'm')) { + return false; + } + break; + case 'L': + case 'l': + if (!ParseLineTo(Path, Command == 'l')) { + return false; + } + break; + case 'H': + case 'h': + if (!ParseHorizontalTo(Path, Command == 'h')) { + return false; + } + break; + case 'V': + case 'v': + if (!ParseVerticalTo(Path, Command == 'v')) { + return false; + } + break; + case 'C': + case 'c': + if (!ParseCurveTo(Path, Command == 'c')) { + return false; + } + break; + case 'Z': + case 'z': + CGPathCloseSubpath(Path); + m_Current = m_SubpathStart; + break; + default: + return false; + } + } + return true; + } + +private: + bool SkipSeparators() { + while (m_Cursor < m_End && + (std::isspace(static_cast(*m_Cursor)) || + *m_Cursor == ',')) { + ++m_Cursor; + } + return m_Cursor < m_End; + } + + bool HasNumberAhead() const { + const char *Probe = m_Cursor; + while (Probe < m_End && + (std::isspace(static_cast(*Probe)) || *Probe == ',')) { + ++Probe; + } + return Probe < m_End && + (std::isdigit(static_cast(*Probe)) != 0 || + *Probe == '-' || *Probe == '+' || *Probe == '.'); + } + + bool ReadNumber(float &OutValue) { + return ParseFloatToken(m_Cursor, m_End, OutValue); + } + + bool ReadPoint(CGPoint &OutPoint, bool Relative) { + float X = 0.0f; + float Y = 0.0f; + if (!ReadNumber(X) || !ReadNumber(Y)) { + return false; + } + OutPoint = CGPointMake(Relative ? m_Current.x + X : X, + Relative ? m_Current.y + Y : Y); + return true; + } + + bool ParseMoveTo(CGMutablePathRef Path, bool Relative) { + CGPoint Point{}; + if (!ReadPoint(Point, Relative)) { + return false; + } + CGPathMoveToPoint(Path, nullptr, Point.x, Point.y); + m_Current = Point; + m_SubpathStart = Point; + + while (HasNumberAhead()) { + if (!ReadPoint(Point, Relative)) { + return false; + } + CGPathAddLineToPoint(Path, nullptr, Point.x, Point.y); + m_Current = Point; + } + return true; + } + + bool ParseLineTo(CGMutablePathRef Path, bool Relative) { + while (HasNumberAhead()) { + CGPoint Point{}; + if (!ReadPoint(Point, Relative)) { + return false; + } + CGPathAddLineToPoint(Path, nullptr, Point.x, Point.y); + m_Current = Point; + } + return true; + } + + bool ParseHorizontalTo(CGMutablePathRef Path, bool Relative) { + while (HasNumberAhead()) { + float X = 0.0f; + if (!ReadNumber(X)) { + return false; + } + m_Current.x = Relative ? (m_Current.x + X) : X; + CGPathAddLineToPoint(Path, nullptr, m_Current.x, m_Current.y); + } + return true; + } + + bool ParseVerticalTo(CGMutablePathRef Path, bool Relative) { + while (HasNumberAhead()) { + float Y = 0.0f; + if (!ReadNumber(Y)) { + return false; + } + m_Current.y = Relative ? (m_Current.y + Y) : Y; + CGPathAddLineToPoint(Path, nullptr, m_Current.x, m_Current.y); + } + return true; + } + + bool ParseCurveTo(CGMutablePathRef Path, bool Relative) { + while (HasNumberAhead()) { + CGPoint C1{}; + CGPoint C2{}; + CGPoint End{}; + if (!ReadPoint(C1, Relative) || !ReadPoint(C2, Relative) || + !ReadPoint(End, Relative)) { + return false; + } + CGPathAddCurveToPoint(Path, nullptr, C1.x, C1.y, C2.x, C2.y, End.x, End.y); + m_Current = End; + } + return true; + } + + const char *m_Cursor{nullptr}; + const char *m_End{nullptr}; + CGPoint m_Current{0.0, 0.0}; + CGPoint m_SubpathStart{0.0, 0.0}; +}; + +} // namespace + +TextureSourceDataRef RasterizeSvgMacOS(std::string_view SvgText, + uint32_t TargetSize) { + const auto ViewBoxText = FindAttributeValue(SvgText, "viewBox"); + const auto PathText = FindAttributeValue(SvgText, "d"); + if (!ViewBoxText.has_value() || !PathText.has_value()) { + return nullptr; + } + + const auto ViewBox = ParseViewBox(*ViewBoxText); + if (!ViewBox.has_value()) { + return nullptr; + } + + CGMutablePathRef Path = CGPathCreateMutable(); + SvgPathParser Parser(*PathText); + const bool Parsed = Parser.Parse(Path); + if (!Parsed) { + CGPathRelease(Path); + return nullptr; + } + + const float LongestSide = std::max(ViewBox->Width, ViewBox->Height); + const float Scale = static_cast(TargetSize) / LongestSide; + const uint32_t Width = + std::max(1u, static_cast(std::ceil(ViewBox->Width * Scale))); + const uint32_t Height = + std::max(1u, static_cast(std::ceil(ViewBox->Height * Scale))); + + auto Texture = std::make_shared(); + Texture->Width = Width; + Texture->Height = Height; + Texture->Pixels.resize(static_cast(Width) * static_cast(Height) * 4u, 0u); + + CGColorSpaceRef ColorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef Context = CGBitmapContextCreate( + Texture->Pixels.data(), Width, Height, 8, Width * 4, ColorSpace, + static_cast(kCGImageAlphaPremultipliedLast | + kCGBitmapByteOrder32Big)); + CGColorSpaceRelease(ColorSpace); + + if (Context == nullptr) { + CGPathRelease(Path); + return nullptr; + } + + CGContextSetAllowsAntialiasing(Context, true); + CGContextSetShouldAntialias(Context, true); + CGContextTranslateCTM(Context, 0.0f, static_cast(Height)); + CGContextScaleCTM(Context, Scale, -Scale); + CGContextTranslateCTM(Context, -ViewBox->MinX, -ViewBox->MinY); + CGContextAddPath(Context, Path); + CGContextSetRGBFillColor(Context, 1.0, 1.0, 1.0, 1.0); + CGContextFillPath(Context); + + CGContextRelease(Context); + CGPathRelease(Path); + return Texture->IsValid() ? Texture : nullptr; +} +} // namespace Axiom::HAL diff --git a/Axiom/Renderer/MacOSVideoToolboxH264Encoder.mm b/HAL/MacOS/MacOSVideoToolboxH264Encoder.mm similarity index 100% rename from Axiom/Renderer/MacOSVideoToolboxH264Encoder.mm rename to HAL/MacOS/MacOSVideoToolboxH264Encoder.mm diff --git a/Headless/MacOSWebRtcSession.mm b/HAL/MacOS/MacOSWebRtcSession.mm similarity index 99% rename from Headless/MacOSWebRtcSession.mm rename to HAL/MacOS/MacOSWebRtcSession.mm index 9e70ff04..31b98730 100644 --- a/Headless/MacOSWebRtcSession.mm +++ b/HAL/MacOS/MacOSWebRtcSession.mm @@ -1,4 +1,4 @@ -#include "WebRtcSession.h" +#include "Headless/WebRtcSession.h" #include diff --git a/HAL/Platform.cpp b/HAL/Platform.cpp new file mode 100644 index 00000000..734abe0b --- /dev/null +++ b/HAL/Platform.cpp @@ -0,0 +1,70 @@ +#include "HAL/Platform.h" + +#include + +namespace { +constexpr std::string_view kVulkanLoaderEnvironmentVariable = + "AXIOM_VULKAN_LOADER_PATH"; + +#ifdef AXIOM_VULKAN_LOADER_PATH +constexpr const char *kConfiguredVulkanLoaderPath = AXIOM_VULKAN_LOADER_PATH; +#else +constexpr const char *kConfiguredVulkanLoaderPath = nullptr; +#endif +} // namespace + +namespace Axiom::HAL { +const char *GetPlatformName() { +#if AXIOM_PLATFORM_WINDOWS + return "Windows"; +#elif AXIOM_PLATFORM_MACOS + return "macOS"; +#elif AXIOM_PLATFORM_LINUX + return "Linux"; +#else + return "Unknown"; +#endif +} + +std::string GetEnvironmentVariable(std::string_view VariableName) { + const std::string Name(VariableName); + if (const char *Value = std::getenv(Name.c_str())) { + return Value; + } + return {}; +} + +std::vector GetDefaultVulkanLoaderCandidatePaths() { + std::vector Candidates; + + if (kConfiguredVulkanLoaderPath != nullptr && + kConfiguredVulkanLoaderPath[0] != '\0') { + Candidates.emplace_back(kConfiguredVulkanLoaderPath); + } + + std::string EnvironmentPath = + GetEnvironmentVariable(kVulkanLoaderEnvironmentVariable); + if (!EnvironmentPath.empty() && + (Candidates.empty() || Candidates.front() != EnvironmentPath)) { + Candidates.emplace_back(std::move(EnvironmentPath)); + } + +#if AXIOM_PLATFORM_MACOS + const std::string VulkanSdk = GetEnvironmentVariable("VULKAN_SDK"); + if (!VulkanSdk.empty()) { + Candidates.emplace_back(VulkanSdk + "/macOS/lib/libvulkan.1.dylib"); + Candidates.emplace_back(VulkanSdk + "/macOS/lib/libvulkan.dylib"); + Candidates.emplace_back(VulkanSdk + "/macOS/lib/libMoltenVK.dylib"); + } + + Candidates.emplace_back("/usr/local/lib/libvulkan.1.dylib"); + Candidates.emplace_back("/usr/local/lib/libvulkan.dylib"); + Candidates.emplace_back("/usr/local/lib/libMoltenVK.dylib"); + Candidates.emplace_back("/opt/homebrew/lib/libvulkan.1.dylib"); + Candidates.emplace_back("/opt/homebrew/lib/libvulkan.dylib"); + Candidates.emplace_back("/opt/homebrew/lib/libMoltenVK.dylib"); +#endif + + return Candidates; +} +} // namespace Axiom::HAL diff --git a/HAL/Platform.h b/HAL/Platform.h new file mode 100644 index 00000000..b2153123 --- /dev/null +++ b/HAL/Platform.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#if defined(_WIN32) +#define AXIOM_PLATFORM_WINDOWS 1 +#elif defined(__APPLE__) && defined(__MACH__) +#define AXIOM_PLATFORM_MACOS 1 +#elif defined(__linux__) +#define AXIOM_PLATFORM_LINUX 1 +#else +#error "Axiom does not support this platform yet." +#endif + +#ifndef AXIOM_PLATFORM_WINDOWS +#define AXIOM_PLATFORM_WINDOWS 0 +#endif + +#ifndef AXIOM_PLATFORM_MACOS +#define AXIOM_PLATFORM_MACOS 0 +#endif + +#ifndef AXIOM_PLATFORM_LINUX +#define AXIOM_PLATFORM_LINUX 0 +#endif + +#if (AXIOM_PLATFORM_WINDOWS + AXIOM_PLATFORM_MACOS + AXIOM_PLATFORM_LINUX) != 1 +#error "Axiom platform detection must resolve to exactly one target platform." +#endif + +namespace Axiom::HAL { +[[nodiscard]] const char *GetPlatformName(); +[[nodiscard]] std::string GetEnvironmentVariable(std::string_view VariableName); +[[nodiscard]] std::vector GetDefaultVulkanLoaderCandidatePaths(); +} // namespace Axiom::HAL diff --git a/HAL/PlatformMedia.cpp b/HAL/PlatformMedia.cpp new file mode 100644 index 00000000..3e2f1172 --- /dev/null +++ b/HAL/PlatformMedia.cpp @@ -0,0 +1,52 @@ +#include "HAL/PlatformMedia.h" + +#include "HAL/Platform.h" + +#include "Headless/WebRtcSession.h" +#include "Renderer/VideoEncoding.h" + +namespace Axiom { +#if AXIOM_PLATFORM_MACOS +std::unique_ptr CreateMacOSVideoToolboxH264Encoder(); +#endif + +#if AXIOM_PLATFORM_MACOS && defined(AXIOM_ENABLE_WEBRTC) && AXIOM_ENABLE_WEBRTC && \ + defined(AXIOM_WEBRTC_LINKED) && AXIOM_WEBRTC_LINKED +std::unique_ptr CreateMacOSWebRtcSession(); +#endif +} // namespace Axiom + +namespace Axiom::HAL { +std::unique_ptr CreatePlatformVideoEncoder() { +#if AXIOM_PLATFORM_MACOS + return CreateMacOSVideoToolboxH264Encoder(); +#else + return nullptr; +#endif +} + +std::unique_ptr CreatePlatformWebRtcSession() { +#if AXIOM_PLATFORM_MACOS && defined(AXIOM_ENABLE_WEBRTC) && AXIOM_ENABLE_WEBRTC && \ + defined(AXIOM_WEBRTC_LINKED) && AXIOM_WEBRTC_LINKED + return CreateMacOSWebRtcSession(); +#else + return nullptr; +#endif +} + +std::string DescribeWebRtcSupport() { +#if AXIOM_PLATFORM_MACOS +#if defined(AXIOM_ENABLE_WEBRTC) && AXIOM_ENABLE_WEBRTC + #if defined(AXIOM_WEBRTC_LINKED) && AXIOM_WEBRTC_LINKED + return "This build links an external native WebRTC binary and exposes the sender/signaling seam, but the concrete peer connection backend is not implemented yet."; + #else + return "This build reserves the WebRTC integration seam, but no external native WebRTC binary was linked."; + #endif +#else + return "This build was compiled without WebRTC support. Enable the AXIOM_ENABLE_WEBRTC CMake option for the macOS libwebrtc path."; +#endif +#else + return "The first WebRTC transport slice is macOS-only. This platform keeps the signaling seam compiled, but no native WebRTC backend is available."; +#endif +} +} // namespace Axiom::HAL diff --git a/HAL/PlatformMedia.h b/HAL/PlatformMedia.h new file mode 100644 index 00000000..b52d0b0e --- /dev/null +++ b/HAL/PlatformMedia.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace Axiom { +class IVideoEncoder; +class IWebRtcSession; +} + +namespace Axiom::HAL { +std::unique_ptr CreatePlatformVideoEncoder(); +std::unique_ptr CreatePlatformWebRtcSession(); +[[nodiscard]] std::string DescribeWebRtcSupport(); +} // namespace Axiom::HAL diff --git a/HAL/Socket.cpp b/HAL/Socket.cpp new file mode 100644 index 00000000..3bb2277c --- /dev/null +++ b/HAL/Socket.cpp @@ -0,0 +1,90 @@ +#include "HAL/Socket.h" + +#include "HAL/Platform.h" + +#include + +#if AXIOM_PLATFORM_WINDOWS +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#else +#include +#include +#endif + +namespace { +#if AXIOM_PLATFORM_WINDOWS +using NativeSocketHandle = SOCKET; +constexpr NativeSocketHandle kInvalidSocket = INVALID_SOCKET; +#else +using NativeSocketHandle = int; +constexpr NativeSocketHandle kInvalidSocket = -1; +#endif + +NativeSocketHandle ToNativeSocket(Axiom::HAL::SocketHandle Socket) { + return static_cast(Socket); +} +} // namespace + +namespace Axiom::HAL { +void InitializeSockets() { +#if AXIOM_PLATFORM_WINDOWS + static std::once_flag Flag; + std::call_once(Flag, []() { + WSADATA Data{}; + WSAStartup(MAKEWORD(2, 2), &Data); + }); +#endif +} + +void CloseSocket(SocketHandle Socket) { + const NativeSocketHandle NativeSocket = ToNativeSocket(Socket); + if (NativeSocket == kInvalidSocket) { + return; + } + +#if AXIOM_PLATFORM_WINDOWS + closesocket(NativeSocket); +#else + close(NativeSocket); +#endif +} + +void SetReuseAddress(SocketHandle Socket) { + const NativeSocketHandle NativeSocket = ToNativeSocket(Socket); + constexpr int Reuse = 1; + +#if AXIOM_PLATFORM_WINDOWS + setsockopt(NativeSocket, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&Reuse), sizeof(Reuse)); +#else + setsockopt(NativeSocket, SOL_SOCKET, SO_REUSEADDR, &Reuse, sizeof(Reuse)); +#endif +} + +bool SendSocketBytes(SocketHandle Socket, const void *Data, size_t Size) { + const NativeSocketHandle NativeSocket = ToNativeSocket(Socket); + const auto *Bytes = static_cast(Data); + size_t Offset = 0; + while (Offset < Size) { +#if AXIOM_PLATFORM_WINDOWS + const int Sent = + send(NativeSocket, Bytes + Offset, static_cast(Size - Offset), 0); + if (Sent == SOCKET_ERROR || Sent == 0) { +#else + const ssize_t Sent = send(NativeSocket, Bytes + Offset, Size - Offset, 0); + if (Sent <= 0) { +#endif + return false; + } + + Offset += static_cast(Sent); + } + + return true; +} +} // namespace Axiom::HAL diff --git a/HAL/Socket.h b/HAL/Socket.h new file mode 100644 index 00000000..de3e36ab --- /dev/null +++ b/HAL/Socket.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace Axiom::HAL { +using SocketHandle = uintptr_t; + +void InitializeSockets(); +void CloseSocket(SocketHandle Socket); +void SetReuseAddress(SocketHandle Socket); +[[nodiscard]] bool SendSocketBytes(SocketHandle Socket, const void *Data, + size_t Size); +} // namespace Axiom::HAL diff --git a/HAL/SvgRasterizer.cpp b/HAL/SvgRasterizer.cpp new file mode 100644 index 00000000..9595277f --- /dev/null +++ b/HAL/SvgRasterizer.cpp @@ -0,0 +1,21 @@ +#include "HAL/SvgRasterizer.h" + +#include "HAL/Platform.h" + +namespace Axiom::HAL { +#if AXIOM_PLATFORM_MACOS +TextureSourceDataRef RasterizeSvgMacOS(std::string_view SvgText, + uint32_t TargetSize); +#endif + +TextureSourceDataRef RasterizeSvg(std::string_view SvgText, + uint32_t TargetSize) { +#if AXIOM_PLATFORM_MACOS + return RasterizeSvgMacOS(SvgText, TargetSize); +#else + (void)SvgText; + (void)TargetSize; + return nullptr; +#endif +} +} // namespace Axiom::HAL diff --git a/HAL/SvgRasterizer.h b/HAL/SvgRasterizer.h new file mode 100644 index 00000000..2dd4cfac --- /dev/null +++ b/HAL/SvgRasterizer.h @@ -0,0 +1,11 @@ +#pragma once + +#include "Renderer/Material.h" + +#include +#include + +namespace Axiom::HAL { +TextureSourceDataRef RasterizeSvg(std::string_view SvgText, + uint32_t TargetSize); +} // namespace Axiom::HAL diff --git a/Headless/AxiomRemoteViewportServer.cpp b/Headless/AxiomRemoteViewportServer.cpp index 70691490..d5e436e6 100644 --- a/Headless/AxiomRemoteViewportServer.cpp +++ b/Headless/AxiomRemoteViewportServer.cpp @@ -1,8 +1,10 @@ #include "RemoteViewportServer.h" +#include "WraithNetworkingModule.h" #include "HeadlessCommandProtocol.h" #include +#include int main(int argc, char **argv) { std::string Error; @@ -19,18 +21,30 @@ int main(int argc, char **argv) { return 1; } - Axiom::RemoteViewportServer Server(Host, Options); - if (!Server.Start(Error)) { - std::cerr << Axiom::SerializeError(Error) << std::endl; + auto NetworkingModule = + std::make_unique(Host, Options); + Axiom::WraithNetworkingModule *NetworkingModulePtr = NetworkingModule.get(); + if (!Host.GetModuleManager().RegisterModule(std::move(NetworkingModule))) { + std::cerr + << Axiom::SerializeError("Failed to register the WraithNetworking module.") + << std::endl; + return 1; + } + const auto NetworkingState = NetworkingModulePtr->GetStateSnapshot(); + if (!NetworkingModulePtr->IsInitialized()) { + std::cerr << Axiom::SerializeError( + NetworkingState.LastError.empty() + ? "Failed to initialize the WraithNetworking module." + : NetworkingState.LastError) + << std::endl; return 1; } std::cout << Axiom::SerializeReady(Options.Width, Options.Height) << std::endl; - while (!Server.ShouldStop() && Host.Step()) { + while (!NetworkingModulePtr->ShouldStop() && Host.Step()) { } - Server.Stop(); std::cout << Axiom::SerializeShutdown() << std::endl; return 0; } diff --git a/Headless/CMakeLists.txt b/Headless/CMakeLists.txt index 6c2920a6..43d983c7 100644 --- a/Headless/CMakeLists.txt +++ b/Headless/CMakeLists.txt @@ -1,16 +1,78 @@ +add_library(uSockets STATIC + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src/bsd.c" + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src/context.c" + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src/loop.c" + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src/socket.c" + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src/udp.c" + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src/eventing/epoll_kqueue.c" +) + +target_include_directories(uSockets PUBLIC + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src" +) + +target_compile_definitions(uSockets PUBLIC LIBUS_NO_SSL) + +add_library(uWebSocketsVendor INTERFACE) +target_include_directories(uWebSocketsVendor INTERFACE + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/src" + "${CMAKE_SOURCE_DIR}/ThirdParty/uWebSockets/uSockets/src" +) +target_link_libraries(uWebSocketsVendor INTERFACE uSockets) + +add_library(WraithNetworking STATIC + RemoteViewportGizmoController.cpp + RemoteViewportGridSnap.cpp + RemoteViewportHttpRouter.cpp + RemoteViewportPresence.cpp + RemoteViewportServer.cpp + RemoteViewportWebRtcSessionManager.cpp + RemoteViewportWebSocketDispatch.cpp + WraithNetworkingModule.cpp +) + +target_include_directories(WraithNetworking PUBLIC + "${CMAKE_SOURCE_DIR}/Headless" + "${CMAKE_SOURCE_DIR}/ThirdParty/glfw/deps" +) + +target_link_libraries(WraithNetworking PUBLIC AxiomCore uWebSocketsVendor) +target_link_libraries(WraithNetworking PRIVATE AxiomHAL) +target_link_libraries(WraithNetworking PUBLIC + AxiomNet + AxiomScene + AxiomRenderer +) +if(TARGET AxiomScripting) + target_link_libraries(WraithNetworking PUBLIC AxiomScripting) +endif() + +target_compile_definitions(WraithNetworking PRIVATE + AXIOM_WITH_PHYSICS=$,1,0> + AXIOM_WITH_SCRIPTING=$,1,0> +) + +if(WIN32) + target_link_libraries(WraithNetworking PUBLIC ws2_32 advapi32) +endif() + add_executable(AxiomHeadless AxiomHeadless.cpp HeadlessCommandProtocol.cpp + HeadlessOverlayModule.cpp + HostModules.cpp HeadlessSessionHost.cpp - HeadlessSessionLayer.cpp + HeadlessSessionModule.cpp WebRtcSession.cpp ) add_executable(AxiomRemoteViewportDevClient DevRemoteViewportClient.cpp HeadlessCommandProtocol.cpp + HeadlessOverlayModule.cpp + HostModules.cpp HeadlessSessionHost.cpp - HeadlessSessionLayer.cpp + HeadlessSessionModule.cpp WebRtcSession.cpp ) @@ -22,52 +84,98 @@ target_include_directories(AxiomRemoteViewportDevClient PRIVATE "${CMAKE_SOURCE_DIR}/ThirdParty/glfw/deps" ) -target_link_libraries(AxiomHeadless PRIVATE AxiomCore) -target_link_libraries(AxiomRemoteViewportDevClient PRIVATE AxiomCore) +target_link_libraries(AxiomHeadless PRIVATE + AxiomCore + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet +) +if(TARGET AxiomPhysics) + target_link_libraries(AxiomHeadless PRIVATE AxiomPhysics) +endif() +if(TARGET AxiomScripting) + target_link_libraries(AxiomHeadless PRIVATE AxiomScripting) +endif() +target_link_libraries(AxiomRemoteViewportDevClient PRIVATE + AxiomCore + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet +) +if(TARGET AxiomPhysics) + target_link_libraries(AxiomRemoteViewportDevClient PRIVATE AxiomPhysics) +endif() +if(TARGET AxiomScripting) + target_link_libraries(AxiomRemoteViewportDevClient PRIVATE AxiomScripting) +endif() add_executable(AxiomRemoteViewportServer AxiomRemoteViewportServer.cpp - RemoteViewportServer.cpp HeadlessCommandProtocol.cpp + HeadlessOverlayModule.cpp + HostModules.cpp HeadlessSessionHost.cpp - HeadlessSessionLayer.cpp + HeadlessSessionModule.cpp WebRtcSession.cpp ) add_executable(AxiomPackagedRuntime AxiomPackagedRuntime.cpp + HeadlessOverlayModule.cpp + HostModules.cpp PackagedRuntimeHost.cpp - HeadlessSessionLayer.cpp + HeadlessSessionModule.cpp ) -if(APPLE) - target_sources(AxiomHeadless PRIVATE - MacOSWebRtcSession.mm - ) - target_sources(AxiomRemoteViewportDevClient PRIVATE - MacOSWebRtcSession.mm - ) - target_sources(AxiomRemoteViewportServer PRIVATE - MacOSWebRtcSession.mm - ) -endif() - target_include_directories(AxiomRemoteViewportServer PRIVATE "${CMAKE_SOURCE_DIR}/ThirdParty/glfw/deps" ) -target_link_libraries(AxiomRemoteViewportServer PRIVATE AxiomCore) -target_link_libraries(AxiomPackagedRuntime PRIVATE AxiomCore) +target_link_libraries(AxiomRemoteViewportServer PRIVATE + AxiomCore + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet + WraithNetworking +) +if(TARGET AxiomPhysics) + target_link_libraries(AxiomRemoteViewportServer PRIVATE AxiomPhysics) +endif() +if(TARGET AxiomScripting) + target_link_libraries(AxiomRemoteViewportServer PRIVATE AxiomScripting) +endif() +target_link_libraries(AxiomPackagedRuntime PRIVATE + AxiomCore + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet +) +if(TARGET AxiomPhysics) + target_link_libraries(AxiomPackagedRuntime PRIVATE AxiomPhysics) +endif() +if(TARGET AxiomScripting) + target_link_libraries(AxiomPackagedRuntime PRIVATE AxiomScripting) +endif() + +foreach(AXIOM_HEADLESS_TARGET IN ITEMS + AxiomHeadless + AxiomRemoteViewportDevClient + AxiomRemoteViewportServer + AxiomPackagedRuntime) + target_compile_definitions(${AXIOM_HEADLESS_TARGET} PRIVATE + AXIOM_WITH_PHYSICS=$,1,0> + AXIOM_WITH_SCRIPTING=$,1,0> + ) +endforeach() set_target_properties(AxiomPackagedRuntime PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Headless" ) -target_compile_definitions(AxiomCore PUBLIC +target_compile_definitions(AxiomScene PUBLIC AXIOM_PACKAGED_RUNTIME_BINARY_PATH="$" ) - -if(WIN32) - target_link_libraries(AxiomRemoteViewportServer PRIVATE ws2_32) - target_link_libraries(AxiomRemoteViewportServer PRIVATE advapi32) -endif() diff --git a/Headless/HeadlessCommandProtocol.cpp b/Headless/HeadlessCommandProtocol.cpp index 9cbc957a..30a3b080 100644 --- a/Headless/HeadlessCommandProtocol.cpp +++ b/Headless/HeadlessCommandProtocol.cpp @@ -1,27 +1,21 @@ #include "HeadlessCommandProtocol.h" -#include -#include +#include +#include +#include #include -#include +#include +#include #include +#include namespace Axiom { namespace { -std::optional MatchString(std::string_view Text, - const std::regex &Pattern, - size_t Group = 1) { - std::match_results Match; - if (!std::regex_search(Text.begin(), Text.end(), Match, Pattern) || - Match.size() <= Group) { - return std::nullopt; - } - return std::string(Match[Group].first, Match[Group].second); -} + +using JsonWriter = rapidjson::Writer; std::optional ParseDouble(std::string_view Value) { - // std::from_chars for floating-point requires macOS 13.3+; use strtod. char *End = nullptr; const double Result = std::strtod(Value.data(), &End); if (End != Value.data() + Value.size()) { @@ -30,50 +24,6 @@ std::optional ParseDouble(std::string_view Value) { return Result; } -std::string UnescapeJsonString(std::string_view Value) { - std::string Unescaped; - Unescaped.reserve(Value.size()); - for (size_t Index = 0; Index < Value.size(); ++Index) { - const char Character = Value[Index]; - if (Character != '\\' || Index + 1 >= Value.size()) { - Unescaped.push_back(Character); - continue; - } - - const char Escape = Value[++Index]; - switch (Escape) { - case '\\': - Unescaped.push_back('\\'); - break; - case '"': - Unescaped.push_back('"'); - break; - case '/': - Unescaped.push_back('/'); - break; - case 'b': - Unescaped.push_back('\b'); - break; - case 'f': - Unescaped.push_back('\f'); - break; - case 'n': - Unescaped.push_back('\n'); - break; - case 'r': - Unescaped.push_back('\r'); - break; - case 't': - Unescaped.push_back('\t'); - break; - default: - Unescaped.push_back(Escape); - break; - } - } - return Unescaped; -} - std::optional ParseUnsigned16(std::string_view Value) { uint16_t Result = 0; const auto [Ptr, Ec] = @@ -84,41 +34,169 @@ std::optional ParseUnsigned16(std::string_view Value) { return Result; } -template -std::string_view MatchView(const MatchType &Match, size_t Index) { - return std::string_view(&*Match[Index].first, Match[Index].length()); +void WriteString(JsonWriter &Writer, std::string_view Value) { + Writer.String(Value.data(), static_cast(Value.size())); +} + +template void WriteNumber(JsonWriter &Writer, Number Value) { + const double DoubleValue = static_cast(Value); + const double RoundedValue = std::nearbyint(DoubleValue); + if (std::isfinite(DoubleValue) && DoubleValue == RoundedValue) { + if (RoundedValue >= 0.0 && + RoundedValue <= + static_cast(std::numeric_limits::max())) { + Writer.Uint64(static_cast(RoundedValue)); + return; + } + if (RoundedValue >= + static_cast(std::numeric_limits::min()) && + RoundedValue <= + static_cast(std::numeric_limits::max())) { + Writer.Int64(static_cast(RoundedValue)); + return; + } + } + + if constexpr (std::is_floating_point_v>) { + std::ostringstream Stream; + Stream << Value; + const std::string Text = Stream.str(); + Writer.RawValue(Text.c_str(), Text.size(), rapidjson::kNumberType); + return; + } + + Writer.Double(DoubleValue); +} + +template std::string BuildJson(Fn &&FnWriter) { + rapidjson::StringBuffer Buffer; + JsonWriter Writer(Buffer); + FnWriter(Writer); + return std::string(Buffer.GetString(), Buffer.GetSize()); +} + +std::optional ParseVec2(const rapidjson::Value &Value) { + if (!Value.IsArray() || Value.Size() != 2 || !Value[0].IsNumber() || + !Value[1].IsNumber()) { + return std::nullopt; + } + return glm::dvec2(Value[0].GetDouble(), Value[1].GetDouble()); +} + +std::optional ParseVec3(const rapidjson::Value &Value) { + if (!Value.IsArray() || Value.Size() != 3 || !Value[0].IsNumber() || + !Value[1].IsNumber() || !Value[2].IsNumber()) { + return std::nullopt; + } + return glm::vec3(Value[0].GetFloat(), Value[1].GetFloat(), + Value[2].GetFloat()); +} + +std::optional ParseVec4(const rapidjson::Value &Value) { + if (!Value.IsArray() || Value.Size() != 4 || !Value[0].IsNumber() || + !Value[1].IsNumber() || !Value[2].IsNumber() || !Value[3].IsNumber()) { + return std::nullopt; + } + return glm::vec4(Value[0].GetFloat(), Value[1].GetFloat(), + Value[2].GetFloat(), Value[3].GetFloat()); +} + +const rapidjson::Value *FindMemberValue(const rapidjson::Value &Object, + const char *Name) { + if (!Object.IsObject()) { + return nullptr; + } + const auto It = Object.FindMember(Name); + if (It == Object.MemberEnd()) { + return nullptr; + } + return &It->value; +} + +std::optional GetStringView(const rapidjson::Value &Object, + const char *Name) { + const rapidjson::Value *Value = FindMemberValue(Object, Name); + if (Value == nullptr || !Value->IsString()) { + return std::nullopt; + } + return std::string_view(Value->GetString(), Value->GetStringLength()); +} + +std::optional GetBoolValue(const rapidjson::Value &Object, + const char *Name) { + const rapidjson::Value *Value = FindMemberValue(Object, Name); + if (Value == nullptr || !Value->IsBool()) { + return std::nullopt; + } + return Value->GetBool(); } -std::optional MatchVec2(std::string_view Text, - const std::regex &Pattern) { - std::match_results Match; - if (!std::regex_search(Text.begin(), Text.end(), Match, Pattern) || - Match.size() < 3) { +std::optional GetFloatValue(const rapidjson::Value &Object, + const char *Name) { + const rapidjson::Value *Value = FindMemberValue(Object, Name); + if (Value == nullptr || !Value->IsNumber()) { return std::nullopt; } - const auto X = ParseDouble(MatchView(Match, 1)); - const auto Y = ParseDouble(MatchView(Match, 2)); - if (!X.has_value() || !Y.has_value()) { + return Value->GetFloat(); +} + +std::optional GetVec2Value(const rapidjson::Value &Object, + const char *Name) { + const rapidjson::Value *Value = FindMemberValue(Object, Name); + if (Value == nullptr) { return std::nullopt; } - return glm::dvec2(*X, *Y); + return ParseVec2(*Value); } -std::optional MatchVec3(std::string_view Text, - const std::regex &Pattern) { - std::match_results Match; - if (!std::regex_search(Text.begin(), Text.end(), Match, Pattern) || - Match.size() < 4) { +std::optional GetVec3Value(const rapidjson::Value &Object, + const char *Name) { + const rapidjson::Value *Value = FindMemberValue(Object, Name); + if (Value == nullptr) { return std::nullopt; } - const auto X = ParseDouble(MatchView(Match, 1)); - const auto Y = ParseDouble(MatchView(Match, 2)); - const auto Z = ParseDouble(MatchView(Match, 3)); - if (!X.has_value() || !Y.has_value() || !Z.has_value()) { + return ParseVec3(*Value); +} + +std::optional GetVec4Value(const rapidjson::Value &Object, + const char *Name) { + const rapidjson::Value *Value = FindMemberValue(Object, Name); + if (Value == nullptr) { return std::nullopt; } - return glm::vec3(static_cast(*X), static_cast(*Y), - static_cast(*Z)); + return ParseVec4(*Value); +} + +void WriteVec2(JsonWriter &Writer, const glm::dvec2 &Value) { + Writer.StartArray(); + WriteNumber(Writer, Value.x); + WriteNumber(Writer, Value.y); + Writer.EndArray(); +} + +void WriteVec3(JsonWriter &Writer, const glm::vec3 &Value) { + Writer.StartArray(); + WriteNumber(Writer, Value.x); + WriteNumber(Writer, Value.y); + WriteNumber(Writer, Value.z); + Writer.EndArray(); +} + +void WriteVec4(JsonWriter &Writer, const glm::vec4 &Value) { + Writer.StartArray(); + WriteNumber(Writer, Value.r); + WriteNumber(Writer, Value.g); + WriteNumber(Writer, Value.b); + WriteNumber(Writer, Value.a); + Writer.EndArray(); +} + +void WriteOptionalUint64(JsonWriter &Writer, std::optional Value) { + if (Value.has_value()) { + Writer.Uint64(*Value); + } else { + Writer.Null(); + } } std::string EventPayloadType(const EditorEventPayload &Payload) { @@ -324,152 +402,215 @@ std::vector BuildParticipants(const EditorSessionState &State return Participants; } -void SerializeSceneItem(std::ostringstream &Stream, const EditorSceneItem &Item) { - Stream << "{\"id\":\"" << EscapeJson(Item.Id) << "\",\"displayName\":\"" - << EscapeJson(Item.DisplayName) << "\",\"kind\":\"" - << SceneItemKindToString(Item.Kind) << "\",\"visible\":" - << (Item.Visible ? "true" : "false") << ",\"children\":["; - for (size_t Index = 0; Index < Item.Children.size(); ++Index) { - if (Index != 0) { - Stream << ","; - } - SerializeSceneItem(Stream, Item.Children[Index]); - } - Stream << "]}"; +void WriteSceneItem(JsonWriter &Writer, const EditorSceneItem &Item) { + Writer.StartObject(); + Writer.Key("id"); + WriteString(Writer, Item.Id); + Writer.Key("displayName"); + WriteString(Writer, Item.DisplayName); + Writer.Key("kind"); + WriteString(Writer, SceneItemKindToString(Item.Kind)); + Writer.Key("visible"); + Writer.Bool(Item.Visible); + Writer.Key("children"); + Writer.StartArray(); + for (const auto &Child : Item.Children) { + WriteSceneItem(Writer, Child); + } + Writer.EndArray(); + Writer.EndObject(); } -void SerializeObjectDetails(std::ostringstream &Stream, - const EditorSessionState &State, - const EditorObjectDetails &Details) { - Stream << "{\"objectId\":\"" << EscapeJson(Details.ObjectId) - << "\",\"displayName\":\"" << EscapeJson(Details.DisplayName) - << "\",\"kind\":\"" << SceneItemKindToString(Details.Kind) - << "\",\"visible\":" << (Details.Visible ? "true" : "false") - << ",\"isGeneratedAssetChild\":" - << (Details.IsGeneratedAssetChild ? "true" : "false"); +void WriteObjectDetails(JsonWriter &Writer, const EditorSessionState &State, + const EditorObjectDetails &Details) { + Writer.StartObject(); + Writer.Key("objectId"); + WriteString(Writer, Details.ObjectId); + Writer.Key("displayName"); + WriteString(Writer, Details.DisplayName); + Writer.Key("kind"); + WriteString(Writer, SceneItemKindToString(Details.Kind)); + Writer.Key("visible"); + Writer.Bool(Details.Visible); + Writer.Key("isGeneratedAssetChild"); + Writer.Bool(Details.IsGeneratedAssetChild); + Writer.Key("generatedFromAssetRootId"); if (Details.GeneratedFromAssetRootId.has_value()) { - Stream << ",\"generatedFromAssetRootId\":\"" - << EscapeJson(*Details.GeneratedFromAssetRootId) << "\""; + WriteString(Writer, *Details.GeneratedFromAssetRootId); } else { - Stream << ",\"generatedFromAssetRootId\":null"; - } - Stream - << ",\"capabilities\":{\"supportsTransform\":" - << (Details.SupportsTransform ? "true" : "false") - << ",\"transformReadOnly\":" - << (Details.TransformReadOnly ? "true" : "false") << "},\"transform\":"; - // Serialize WorldTransform (world-space) so the frontend works in world space. - // Fall back to Transform for objects that predate world-transform computation. - const auto &T = Details.WorldTransform.has_value() ? Details.WorldTransform - : Details.Transform; - if (T.has_value()) { - Stream << "{\"location\":[" << T->Location.x << "," - << T->Location.y << "," << T->Location.z - << "],\"rotationDegrees\":[" << T->RotationDegrees.x - << "," << T->RotationDegrees.y << "," - << T->RotationDegrees.z << "],\"scale\":[" - << T->Scale.x << "," << T->Scale.y - << "," << T->Scale.z << "]}"; + Writer.Null(); + } + + Writer.Key("capabilities"); + Writer.StartObject(); + Writer.Key("supportsTransform"); + Writer.Bool(Details.SupportsTransform); + Writer.Key("transformReadOnly"); + Writer.Bool(Details.TransformReadOnly); + Writer.EndObject(); + + Writer.Key("transform"); + const auto &Transform = + Details.WorldTransform.has_value() ? Details.WorldTransform + : Details.Transform; + if (Transform.has_value()) { + Writer.StartObject(); + Writer.Key("location"); + WriteVec3(Writer, Transform->Location); + Writer.Key("rotationDegrees"); + WriteVec3(Writer, Transform->RotationDegrees); + Writer.Key("scale"); + WriteVec3(Writer, Transform->Scale); + Writer.EndObject(); } else { - Stream << "null"; + Writer.Null(); } + + Writer.Key("light"); if (Details.Light.has_value()) { - Stream << ",\"light\":{\"color\":[" << Details.Light->Color.r << "," - << Details.Light->Color.g << "," << Details.Light->Color.b - << "],\"intensity\":" << Details.Light->Intensity << "}"; + Writer.StartObject(); + Writer.Key("color"); + WriteVec3(Writer, Details.Light->Color); + Writer.Key("intensity"); + WriteNumber(Writer, Details.Light->Intensity); + Writer.EndObject(); } else { - Stream << ",\"light\":null"; + Writer.Null(); } + + Writer.Key("material"); if (Details.Material.has_value()) { - Stream << ",\"material\":{\"baseColorFactor\":[" - << Details.Material->BaseColorFactor.r << "," - << Details.Material->BaseColorFactor.g << "," - << Details.Material->BaseColorFactor.b << "," - << Details.Material->BaseColorFactor.a - << "],\"metallic\":" << Details.Material->Metallic - << ",\"roughness\":" << Details.Material->Roughness; + Writer.StartObject(); + Writer.Key("baseColorFactor"); + WriteVec4(Writer, Details.Material->BaseColorFactor); + Writer.Key("metallic"); + WriteNumber(Writer, Details.Material->Metallic); + Writer.Key("roughness"); + WriteNumber(Writer, Details.Material->Roughness); + Writer.Key("textureAssetPath"); if (Details.Material->TextureAssetPath.has_value()) { - Stream << ",\"textureAssetPath\":\"" - << EscapeJson(*Details.Material->TextureAssetPath) << "\""; + WriteString(Writer, *Details.Material->TextureAssetPath); } else { - Stream << ",\"textureAssetPath\":null"; + Writer.Null(); } - Stream << "}"; + Writer.EndObject(); } else { - Stream << ",\"material\":null"; + Writer.Null(); } + + Writer.Key("physics"); if (Details.Physics.has_value()) { - Stream << ",\"physics\":{\"bodyType\":\"" - << PhysicsBodyTypeToString(Details.Physics->BodyType) - << "\",\"colliderType\":\"" - << PhysicsColliderTypeToString(Details.Physics->ColliderType) - << "\",\"boxHalfExtents\":[" - << Details.Physics->BoxHalfExtents.x << "," - << Details.Physics->BoxHalfExtents.y << "," - << Details.Physics->BoxHalfExtents.z - << "],\"sphereRadius\":" << Details.Physics->SphereRadius - << ",\"mass\":" << Details.Physics->Mass - << ",\"friction\":" << Details.Physics->Friction - << ",\"restitution\":" << Details.Physics->Restitution << "}"; + Writer.StartObject(); + Writer.Key("bodyType"); + WriteString(Writer, PhysicsBodyTypeToString(Details.Physics->BodyType)); + Writer.Key("colliderType"); + WriteString(Writer, + PhysicsColliderTypeToString(Details.Physics->ColliderType)); + Writer.Key("boxHalfExtents"); + WriteVec3(Writer, Details.Physics->BoxHalfExtents); + Writer.Key("sphereRadius"); + WriteNumber(Writer, Details.Physics->SphereRadius); + Writer.Key("mass"); + WriteNumber(Writer, Details.Physics->Mass); + Writer.Key("friction"); + WriteNumber(Writer, Details.Physics->Friction); + Writer.Key("restitution"); + WriteNumber(Writer, Details.Physics->Restitution); + Writer.EndObject(); } else { - Stream << ",\"physics\":null"; + Writer.Null(); } - Stream << ",\"collaboration\":{\"selectedByUserIds\":["; - bool FirstSelectionOwner = true; + + Writer.Key("collaboration"); + Writer.StartObject(); + Writer.Key("selectedByUserIds"); + Writer.StartArray(); for (const auto &Participant : BuildParticipants(State, SessionUserId{0})) { if (!Participant.SelectedObjectId.has_value() || *Participant.SelectedObjectId != Details.ObjectId) { continue; } - if (!FirstSelectionOwner) { - Stream << ","; - } - FirstSelectionOwner = false; - Stream << Participant.User.Value; + Writer.Uint64(Participant.User.Value); } - Stream << "],\"lockState\":\""; + Writer.EndArray(); + Writer.Key("lockState"); const auto CollaborationIt = State.Scene.CollaborationByObjectId.find(Details.ObjectId); if (CollaborationIt != State.Scene.CollaborationByObjectId.end()) { - Stream << LockStateToString(CollaborationIt->second.LockState) - << "\",\"lockOwnerUserId\":"; + WriteString(Writer, + LockStateToString(CollaborationIt->second.LockState)); + Writer.Key("lockOwnerUserId"); if (CollaborationIt->second.LockOwner.has_value()) { - Stream << CollaborationIt->second.LockOwner->Value; + Writer.Uint64(CollaborationIt->second.LockOwner->Value); } else { - Stream << "null"; + Writer.Null(); } } else { - Stream << "unlocked\",\"lockOwnerUserId\":null"; + WriteString(Writer, "unlocked"); + Writer.Key("lockOwnerUserId"); + Writer.Null(); } - Stream << "}}"; + Writer.EndObject(); + + Writer.EndObject(); } -void SerializeParticipant(std::ostringstream &Stream, - const EditorParticipant &Participant) { - Stream << "{\"userId\":" << Participant.User.Value << ",\"displayName\":\"" - << EscapeJson(Participant.DisplayName) << "\",\"presenceState\":\"" - << PresenceStateToString(Participant.State) << "\",\"isLocal\":" - << (Participant.IsLocal ? "true" : "false") - << ",\"currentTool\":\"" << EscapeJson(Participant.CurrentTool) - << "\",\"presentationColor\":\"" - << EscapeJson(Participant.PresentationColor) - << "\",\"selectionObjectId\":"; + +void WriteParticipant(JsonWriter &Writer, const EditorParticipant &Participant) { + Writer.StartObject(); + Writer.Key("userId"); + Writer.Uint64(Participant.User.Value); + Writer.Key("displayName"); + WriteString(Writer, Participant.DisplayName); + Writer.Key("presenceState"); + WriteString(Writer, PresenceStateToString(Participant.State)); + Writer.Key("isLocal"); + Writer.Bool(Participant.IsLocal); + Writer.Key("currentTool"); + WriteString(Writer, Participant.CurrentTool); + Writer.Key("presentationColor"); + WriteString(Writer, Participant.PresentationColor); + Writer.Key("selectionObjectId"); if (Participant.SelectedObjectId.has_value()) { - Stream << "\"" << EscapeJson(*Participant.SelectedObjectId) << "\""; + WriteString(Writer, *Participant.SelectedObjectId); } else { - Stream << "null"; + Writer.Null(); } - Stream << ",\"camera\":"; + Writer.Key("camera"); if (Participant.Camera.has_value()) { - Stream << "{\"position\":[" << Participant.Camera->Position.x << "," - << Participant.Camera->Position.y << "," - << Participant.Camera->Position.z << "],\"yawDegrees\":" - << Participant.Camera->YawDegrees << ",\"pitchDegrees\":" - << Participant.Camera->PitchDegrees << "}"; + Writer.StartObject(); + Writer.Key("position"); + WriteVec3(Writer, Participant.Camera->Position); + Writer.Key("yawDegrees"); + WriteNumber(Writer, Participant.Camera->YawDegrees); + Writer.Key("pitchDegrees"); + WriteNumber(Writer, Participant.Camera->PitchDegrees); + Writer.EndObject(); } else { - Stream << "null"; + Writer.Null(); } - Stream << "}"; + Writer.EndObject(); } + +std::optional +ParseJson(std::string_view JsonLine, std::string &MutableJson, std::string &Error) { + MutableJson.assign(JsonLine.begin(), JsonLine.end()); + rapidjson::Document Document; + Document.ParseInsitu(MutableJson.data()); + if (Document.HasParseError() || !Document.IsObject()) { + Error = "Command is not valid JSON."; + return std::nullopt; + } + return Document; +} + +template +HeadlessCommand WrapCommand(HeadlessCommandType Type, TCommand Payload) { + return HeadlessCommand{ + .Type = Type, + .EditorPayload = {.Payload = std::move(Payload)}, + }; +} + } // namespace std::optional ParseHeadlessOptions(int argc, char **argv, @@ -509,30 +650,14 @@ std::optional ParseHeadlessOptions(int argc, char **argv, std::optional ParseHeadlessCommand(std::string_view JsonLine, std::string &Error) { - static const std::regex TypePattern(R"json("type"\s*:\s*"([^"]+)")json"); - static const std::regex ViewModePattern(R"json("viewMode"\s*:\s*"([^"]+)")json"); - static const std::regex ShowCollidersPattern( - R"json("showColliders"\s*:\s*(true|false))json"); - static const std::regex BoolPattern( - R"json("isLooking"\s*:\s*(true|false))json"); - static const std::regex CursorPattern( - R"json("cursorPosition"\s*:\s*\[\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*\])json"); - static const std::regex MovementPattern( - R"json("worldMovement"\s*:\s*\[\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*\])json"); - static const std::regex PositionPattern( - R"json("position"\s*:\s*\[\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*\])json"); - static const std::regex YawPattern( - R"json("yawDegrees"\s*:\s*([-+0-9.eE]+))json"); - static const std::regex PitchPattern( - R"json("pitchDegrees"\s*:\s*([-+0-9.eE]+))json"); - static const std::regex MouseXPattern( - R"json("mouseX"\s*:\s*([-+0-9.eE]+))json"); - static const std::regex MouseYPattern( - R"json("mouseY"\s*:\s*([-+0-9.eE]+))json"); - static const std::regex ProjectionTypePattern( - R"json("projectionType"\s*:\s*"([^"]+)")json"); - - const auto Type = MatchString(JsonLine, TypePattern); + std::string MutableJson; + auto ParsedDocument = ParseJson(JsonLine, MutableJson, Error); + if (!ParsedDocument.has_value()) { + return std::nullopt; + } + rapidjson::Document &Document = *ParsedDocument; + + const auto Type = GetStringView(Document, "type"); if (!Type.has_value()) { Error = "Command is missing a string `type` field."; return std::nullopt; @@ -549,7 +674,7 @@ std::optional ParseHeadlessCommand(std::string_view JsonLine, .EditorPayload = {}}; } if (*Type == "set_view_mode") { - const auto ViewMode = MatchString(JsonLine, ViewModePattern); + const auto ViewMode = GetStringView(Document, "viewMode"); if (!ViewMode.has_value()) { Error = "`set_view_mode` requires `viewMode`."; return std::nullopt; @@ -563,7 +688,7 @@ std::optional ParseHeadlessCommand(std::string_view JsonLine, } else if (*ViewMode == "wireframe") { ParsedMode = RendererViewMode::Wireframe; } else { - Error = "Unsupported view mode: " + *ViewMode; + Error = "Unsupported view mode: " + std::string(*ViewMode); return std::nullopt; } @@ -572,29 +697,31 @@ std::optional ParseHeadlessCommand(std::string_view JsonLine, .ViewMode = ParsedMode}; } if (*Type == "set_camera_projection") { - const auto ProjType = MatchString(JsonLine, ProjectionTypePattern); - if (!ProjType.has_value()) { + const auto ProjectionType = GetStringView(Document, "projectionType"); + if (!ProjectionType.has_value()) { Error = "`set_camera_projection` requires `projectionType`."; return std::nullopt; } CameraProjectionType Parsed{}; - if (*ProjType == "perspective") { + if (*ProjectionType == "perspective") { Parsed = CameraProjectionType::Perspective; - } else if (*ProjType == "orthographic") { + } else if (*ProjectionType == "orthographic") { Parsed = CameraProjectionType::Orthographic; } else { - Error = "Unsupported projectionType: " + *ProjType; + Error = "Unsupported projectionType: " + std::string(*ProjectionType); return std::nullopt; } return HeadlessCommand{ .Type = HeadlessCommandType::SetCameraProjection, - .EditorPayload = {SetCameraProjectionCommand{.ProjectionType = Parsed}}, - .ProjectionType = Parsed}; + .EditorPayload = {.Payload = SetCameraProjectionCommand{ + .ProjectionType = Parsed}}, + .ProjectionType = Parsed, + }; } if (*Type == "set_show_colliders") { - const auto ShowColliders = MatchString(JsonLine, ShowCollidersPattern); + const auto ShowColliders = GetBoolValue(Document, "showColliders"); if (!ShowColliders.has_value()) { Error = "`set_show_colliders` requires `showColliders`."; return std::nullopt; @@ -602,343 +729,226 @@ std::optional ParseHeadlessCommand(std::string_view JsonLine, return HeadlessCommand{.Type = HeadlessCommandType::SetShowColliders, .EditorPayload = {}, - .ShowColliders = *ShowColliders == "true"}; + .ShowColliders = *ShowColliders}; } if (*Type == "quit") { return HeadlessCommand{.Type = HeadlessCommandType::Quit, .EditorPayload = {}}; } if (*Type == "play_session") { - return HeadlessCommand{ - .Type = HeadlessCommandType::PlaySession, - .EditorPayload = {.Payload = PlaySessionCommand{}}, - }; + return WrapCommand(HeadlessCommandType::PlaySession, PlaySessionCommand{}); } if (*Type == "pause_session") { - return HeadlessCommand{ - .Type = HeadlessCommandType::PauseSession, - .EditorPayload = {.Payload = PauseSessionCommand{}}, - }; + return WrapCommand(HeadlessCommandType::PauseSession, PauseSessionCommand{}); } if (*Type == "resume_session") { - return HeadlessCommand{ - .Type = HeadlessCommandType::ResumeSession, - .EditorPayload = {.Payload = ResumeSessionCommand{}}, - }; + return WrapCommand(HeadlessCommandType::ResumeSession, ResumeSessionCommand{}); } if (*Type == "stop_session") { - return HeadlessCommand{ - .Type = HeadlessCommandType::StopSession, - .EditorPayload = {.Payload = StopSessionCommand{}}, - }; + return WrapCommand(HeadlessCommandType::StopSession, StopSessionCommand{}); } if (*Type == "set_look_active") { - const auto BoolValue = MatchString(JsonLine, BoolPattern); - if (!BoolValue.has_value()) { + const auto IsLooking = GetBoolValue(Document, "isLooking"); + if (!IsLooking.has_value()) { Error = "`set_look_active` requires `isLooking`."; return std::nullopt; } - const auto Cursor = MatchVec2(JsonLine, CursorPattern); return HeadlessCommand{ .Type = HeadlessCommandType::SetLookActive, .EditorPayload = {.Payload = SetLookActiveCommand{ - .IsLooking = *BoolValue == "true", - .CursorPosition = Cursor, + .IsLooking = *IsLooking, + .CursorPosition = GetVec2Value(Document, "cursorPosition"), }}, }; } if (*Type == "set_viewport_camera_pose") { - const auto Position = MatchVec3(JsonLine, PositionPattern); - const auto Yaw = MatchString(JsonLine, YawPattern); - const auto Pitch = MatchString(JsonLine, PitchPattern); - if (!Position.has_value() || !Yaw.has_value() || !Pitch.has_value()) { + const auto Position = GetVec3Value(Document, "position"); + const auto YawDegrees = GetFloatValue(Document, "yawDegrees"); + const auto PitchDegrees = GetFloatValue(Document, "pitchDegrees"); + if (!Position.has_value() || !YawDegrees.has_value() || + !PitchDegrees.has_value()) { Error = "`set_viewport_camera_pose` requires `position`, `yawDegrees`, and `pitchDegrees`."; return std::nullopt; } - const auto ParsedYaw = ParseDouble(*Yaw); - const auto ParsedPitch = ParseDouble(*Pitch); - if (!ParsedYaw.has_value() || !ParsedPitch.has_value()) { - Error = "`set_viewport_camera_pose` requires numeric `yawDegrees` and `pitchDegrees`."; - return std::nullopt; - } return HeadlessCommand{ .Type = HeadlessCommandType::SetViewportCameraPose, .EditorPayload = {.Payload = SetViewportCameraPoseCommand{ .Position = *Position, - .YawDegrees = static_cast(*ParsedYaw), - .PitchDegrees = static_cast(*ParsedPitch), + .YawDegrees = *YawDegrees, + .PitchDegrees = *PitchDegrees, }}, }; } if (*Type == "select_object") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); + const auto ObjectId = GetStringView(Document, "objectId"); if (!ObjectId.has_value()) { Error = "`select_object` requires `objectId`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::SelectObject, - .EditorPayload = - {.Payload = SelectObjectCommand{.ObjectId = UnescapeJsonString(*ObjectId)}}, - }; + return WrapCommand(HeadlessCommandType::SelectObject, + SelectObjectCommand{.ObjectId = std::string(*ObjectId)}); } if (*Type == "rename_object") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - static const std::regex DisplayNamePattern( - R"json("displayName"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto DisplayName = MatchString(JsonLine, DisplayNamePattern); + const auto ObjectId = GetStringView(Document, "objectId"); + const auto DisplayName = GetStringView(Document, "displayName"); if (!ObjectId.has_value() || !DisplayName.has_value()) { Error = "`rename_object` requires `objectId` and `displayName`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::RenameObject, - .EditorPayload = - {.Payload = RenameObjectCommand{ - .ObjectId = UnescapeJsonString(*ObjectId), - .DisplayName = UnescapeJsonString(*DisplayName), - }}, - }; + return WrapCommand( + HeadlessCommandType::RenameObject, + RenameObjectCommand{.ObjectId = std::string(*ObjectId), + .DisplayName = std::string(*DisplayName)}); } if (*Type == "set_object_visibility") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - static const std::regex VisiblePattern( - R"json("visible"\s*:\s*(true|false))json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto Visible = MatchString(JsonLine, VisiblePattern); + const auto ObjectId = GetStringView(Document, "objectId"); + const auto Visible = GetBoolValue(Document, "visible"); if (!ObjectId.has_value() || !Visible.has_value()) { Error = "`set_object_visibility` requires `objectId` and `visible`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::SetObjectVisibility, - .EditorPayload = - {.Payload = SetObjectVisibilityCommand{ - .ObjectId = UnescapeJsonString(*ObjectId), - .Visible = *Visible == "true", - }}, - }; + return WrapCommand( + HeadlessCommandType::SetObjectVisibility, + SetObjectVisibilityCommand{.ObjectId = std::string(*ObjectId), + .Visible = *Visible}); } if (*Type == "set_transform") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - static const std::regex LocationPattern( - R"json("location"\s*:\s*\[\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*\])json"); - static const std::regex RotationPattern( - R"json("rotationDegrees"\s*:\s*\[\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*\])json"); - static const std::regex ScalePattern( - R"json("scale"\s*:\s*\[\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*,\s*([-+0-9.eE]+)\s*\])json"); - - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto Location = MatchVec3(JsonLine, LocationPattern); - const auto Rotation = MatchVec3(JsonLine, RotationPattern); - const auto Scale = MatchVec3(JsonLine, ScalePattern); - if (!ObjectId.has_value() || !Location.has_value() || !Rotation.has_value() || - !Scale.has_value()) { + const auto ObjectId = GetStringView(Document, "objectId"); + const auto Location = GetVec3Value(Document, "location"); + const auto RotationDegrees = GetVec3Value(Document, "rotationDegrees"); + const auto Scale = GetVec3Value(Document, "scale"); + if (!ObjectId.has_value() || !Location.has_value() || + !RotationDegrees.has_value() || !Scale.has_value()) { Error = "`set_transform` requires `objectId`, `location`, `rotationDegrees`, and `scale`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::SetTransform, - .EditorPayload = - {.Payload = SetTransformCommand{ - .ObjectId = UnescapeJsonString(*ObjectId), - .Location = *Location, - .RotationDegrees = *Rotation, - .Scale = *Scale, - }}, - }; + return WrapCommand( + HeadlessCommandType::SetTransform, + SetTransformCommand{.ObjectId = std::string(*ObjectId), + .Location = *Location, + .RotationDegrees = *RotationDegrees, + .Scale = *Scale}); } if (*Type == "create_object") { - static const std::regex TemplateIdPattern( - R"json("templateId"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto TemplateId = MatchString(JsonLine, TemplateIdPattern); + const auto TemplateId = GetStringView(Document, "templateId"); if (!TemplateId.has_value()) { Error = "`create_object` requires `templateId`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::CreateObject, - .EditorPayload = - {.Payload = CreateObjectCommand{ - .TemplateId = UnescapeJsonString(*TemplateId), - }}, - }; + return WrapCommand(HeadlessCommandType::CreateObject, + CreateObjectCommand{.TemplateId = + std::string(*TemplateId)}); } if (*Type == "duplicate_object") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); + const auto ObjectId = GetStringView(Document, "objectId"); if (!ObjectId.has_value()) { Error = "`duplicate_object` requires `objectId`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::DuplicateObject, - .EditorPayload = - {.Payload = DuplicateObjectCommand{ - .ObjectId = UnescapeJsonString(*ObjectId), - }}, - }; + return WrapCommand(HeadlessCommandType::DuplicateObject, + DuplicateObjectCommand{.ObjectId = + std::string(*ObjectId)}); } if (*Type == "delete_object") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); + const auto ObjectId = GetStringView(Document, "objectId"); if (!ObjectId.has_value()) { Error = "`delete_object` requires `objectId`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::DeleteObject, - .EditorPayload = - {.Payload = DeleteObjectCommand{ - .ObjectId = UnescapeJsonString(*ObjectId), - }}, - }; + return WrapCommand(HeadlessCommandType::DeleteObject, + DeleteObjectCommand{.ObjectId = std::string(*ObjectId)}); } if (*Type == "reparent_object") { - static const std::regex ObjectIdPattern( - R"json("objectId"\s*:\s*"((?:\\.|[^"])*)")json"); - static const std::regex NewParentIdPattern( - R"json("newParentId"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto NewParentId = MatchString(JsonLine, NewParentIdPattern); + const auto ObjectId = GetStringView(Document, "objectId"); + const auto NewParentId = GetStringView(Document, "newParentId"); if (!ObjectId.has_value() || !NewParentId.has_value()) { Error = "`reparent_object` requires `objectId` and `newParentId`."; return std::nullopt; } - return HeadlessCommand{ - .Type = HeadlessCommandType::ReparentObject, - .EditorPayload = - {.Payload = ReparentObjectCommand{ - .ObjectId = UnescapeJsonString(*ObjectId), - .NewParentId = UnescapeJsonString(*NewParentId), - }}, - }; + return WrapCommand( + HeadlessCommandType::ReparentObject, + ReparentObjectCommand{.ObjectId = std::string(*ObjectId), + .NewParentId = std::string(*NewParentId)}); } if (*Type == "update_viewport_camera") { - const auto Movement = MatchVec3(JsonLine, MovementPattern); - if (!Movement.has_value()) { + const auto WorldMovement = GetVec3Value(Document, "worldMovement"); + if (!WorldMovement.has_value()) { Error = "`update_viewport_camera` requires `worldMovement`."; return std::nullopt; } - const auto Cursor = MatchVec2(JsonLine, CursorPattern); return HeadlessCommand{ .Type = HeadlessCommandType::UpdateViewportCamera, .EditorPayload = {.Payload = UpdateViewportCameraCommand{ - .WorldMovement = *Movement, - .CursorPosition = Cursor, + .WorldMovement = *WorldMovement, + .CursorPosition = GetVec2Value(Document, "cursorPosition"), }}, }; } - if (*Type == "gizmo_hover") { - const auto MX = MatchString(JsonLine, MouseXPattern); - const auto MY = MatchString(JsonLine, MouseYPattern); - float MouseX = 0.0f; - float MouseY = 0.0f; - if (MX.has_value()) { - if (const auto V = ParseDouble(*MX)) { - MouseX = static_cast(*V); - } + auto ParseMousePosition = [&](float DefaultX, float DefaultY) { + glm::vec2 MousePosition(DefaultX, DefaultY); + if (const auto MouseX = GetFloatValue(Document, "mouseX"); + MouseX.has_value()) { + MousePosition.x = *MouseX; } - if (MY.has_value()) { - if (const auto V = ParseDouble(*MY)) { - MouseY = static_cast(*V); - } + if (const auto MouseY = GetFloatValue(Document, "mouseY"); + MouseY.has_value()) { + MousePosition.y = *MouseY; } + return MousePosition; + }; + + if (*Type == "gizmo_hover") { return HeadlessCommand{ .Type = HeadlessCommandType::GizmoHover, - .MousePosition = {MouseX, MouseY}, + .MousePosition = ParseMousePosition(0.0f, 0.0f), + }; + } + if (*Type == "gizmo_drag_start") { + return HeadlessCommand{ + .Type = HeadlessCommandType::GizmoDragStart, + .MousePosition = ParseMousePosition(0.0f, 0.0f), + }; + } + if (*Type == "gizmo_drag_update") { + return HeadlessCommand{ + .Type = HeadlessCommandType::GizmoDragUpdate, + .MousePosition = ParseMousePosition(0.0f, 0.0f), + }; + } + if (*Type == "gizmo_drag_end") { + return HeadlessCommand{ + .Type = HeadlessCommandType::GizmoDragEnd, + .MousePosition = ParseMousePosition(0.0f, 0.0f), }; } - - auto ParseMouseXY = [&](HeadlessCommandType T) -> HeadlessCommand { - const auto MX = MatchString(JsonLine, MouseXPattern); - const auto MY = MatchString(JsonLine, MouseYPattern); - float MouseX = 0.0f; - float MouseY = 0.0f; - if (MX.has_value()) { - if (const auto V = ParseDouble(*MX)) MouseX = static_cast(*V); - } - if (MY.has_value()) { - if (const auto V = ParseDouble(*MY)) MouseY = static_cast(*V); - } - return HeadlessCommand{.Type = T, .MousePosition = {MouseX, MouseY}}; - }; - - if (*Type == "gizmo_drag_start") return ParseMouseXY(HeadlessCommandType::GizmoDragStart); - if (*Type == "gizmo_drag_update") return ParseMouseXY(HeadlessCommandType::GizmoDragUpdate); - if (*Type == "gizmo_drag_end") return ParseMouseXY(HeadlessCommandType::GizmoDragEnd); if (*Type == "drop_mesh") { - static const std::regex AssetPathPattern(R"json("assetPath"\s*:\s*"([^"]+)")json"); - const auto AssetPath = MatchString(JsonLine, AssetPathPattern); - const auto MX = MatchString(JsonLine, MouseXPattern); - const auto MY = MatchString(JsonLine, MouseYPattern); - float MouseX = 0.0f; - float MouseY = 0.0f; - if (MX.has_value()) { - if (const auto V = ParseDouble(*MX)) MouseX = static_cast(*V); - } - if (MY.has_value()) { - if (const auto V = ParseDouble(*MY)) MouseY = static_cast(*V); - } - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::DropMesh; - Cmd.MeshAssetPath = AssetPath.value_or(""); - Cmd.MousePosition = {MouseX, MouseY}; - return Cmd; + HeadlessCommand Command; + Command.Type = HeadlessCommandType::DropMesh; + Command.MeshAssetPath = + std::string(GetStringView(Document, "assetPath").value_or("")); + Command.MousePosition = ParseMousePosition(0.0f, 0.0f); + return Command; } if (*Type == "drop_texture") { - static const std::regex TexturePathPattern(R"json("textureAssetPath"\s*:\s*"([^"]*)")json"); - const auto TexturePath = MatchString(JsonLine, TexturePathPattern); - const auto MX = MatchString(JsonLine, MouseXPattern); - const auto MY = MatchString(JsonLine, MouseYPattern); - float MouseX = 0.0f; - float MouseY = 0.0f; - if (MX.has_value()) { - if (const auto V = ParseDouble(*MX)) MouseX = static_cast(*V); - } - if (MY.has_value()) { - if (const auto V = ParseDouble(*MY)) MouseY = static_cast(*V); - } - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::DropTexture; - Cmd.TextureAssetPath = TexturePath.value_or(""); - Cmd.MousePosition = {MouseX, MouseY}; - return Cmd; + HeadlessCommand Command; + Command.Type = HeadlessCommandType::DropTexture; + Command.TextureAssetPath = + std::string(GetStringView(Document, "textureAssetPath").value_or("")); + Command.MousePosition = ParseMousePosition(0.0f, 0.0f); + return Command; } if (*Type == "place_actor") { - static const std::regex TemplateIdPattern( - R"json("templateId"\s*:\s*"((?:\\.|[^"])*)")json"); - static const std::regex MeshAssetPathPattern( - R"json("meshAssetPath"\s*:\s*"((?:\\.|[^"])*)")json"); - const auto TemplateId = MatchString(JsonLine, TemplateIdPattern); - const auto MeshAssetPath = MatchString(JsonLine, MeshAssetPathPattern); - const auto MX = MatchString(JsonLine, MouseXPattern); - const auto MY = MatchString(JsonLine, MouseYPattern); - float MouseX = -1.0f; - float MouseY = -1.0f; - if (MX.has_value()) { - if (const auto V = ParseDouble(*MX)) MouseX = static_cast(*V); - } - if (MY.has_value()) { - if (const auto V = ParseDouble(*MY)) MouseY = static_cast(*V); - } - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::PlaceActor; - Cmd.PlaceActorTemplateId = TemplateId.has_value() ? UnescapeJsonString(*TemplateId) : ""; - Cmd.PlaceActorMeshAssetPath = MeshAssetPath.has_value() ? UnescapeJsonString(*MeshAssetPath) : ""; - Cmd.MousePosition = {MouseX, MouseY}; - return Cmd; + HeadlessCommand Command; + Command.Type = HeadlessCommandType::PlaceActor; + Command.PlaceActorTemplateId = + std::string(GetStringView(Document, "templateId").value_or("")); + Command.PlaceActorMeshAssetPath = + std::string(GetStringView(Document, "meshAssetPath").value_or("")); + Command.MousePosition = ParseMousePosition(-1.0f, -1.0f); + return Command; } if (*Type == "list_assets") { return HeadlessCommand{.Type = HeadlessCommandType::ListAssets}; @@ -950,252 +960,158 @@ std::optional ParseHeadlessCommand(std::string_view JsonLine, return HeadlessCommand{.Type = HeadlessCommandType::ReloadScripts}; } if (*Type == "attach_script") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - static const std::regex ClassPattern(R"json("scriptClass"\s*:\s*"([^"]*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto ScriptClass = MatchString(JsonLine, ClassPattern); return HeadlessCommand{ .Type = HeadlessCommandType::AttachScript, .EditorPayload = {.Payload = AttachScriptCommand{ - .ObjectId = ObjectId.value_or(""), - .ScriptClassName = ScriptClass.value_or("")}}}; + .ObjectId = std::string( + GetStringView(Document, "objectId").value_or("")), + .ScriptClassName = std::string( + GetStringView(Document, "scriptClass") + .value_or(""))}}, + }; } if (*Type == "detach_script") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); return HeadlessCommand{ .Type = HeadlessCommandType::DetachScript, .EditorPayload = {.Payload = DetachScriptCommand{ - .ObjectId = ObjectId.value_or("")}}}; + .ObjectId = std::string( + GetStringView(Document, "objectId").value_or(""))}}, + }; } if (*Type == "set_mesh_asset") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - static const std::regex AssetPathPattern(R"json("assetPath"\s*:\s*"([^"]+)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto AssetPath = MatchString(JsonLine, AssetPathPattern); + const std::string ObjectId = + std::string(GetStringView(Document, "objectId").value_or("")); + const std::string AssetPath = + std::string(GetStringView(Document, "assetPath").value_or("")); return HeadlessCommand{ .Type = HeadlessCommandType::SetMeshAsset, .EditorPayload = {.Payload = SetMeshAssetCommand{ - .ObjectId = ObjectId.value_or(""), - .AssetPath = AssetPath.value_or("")}}, - .AssetPath = AssetPath.value_or("")}; + .ObjectId = ObjectId, + .AssetPath = AssetPath}}, + .AssetPath = AssetPath, + }; } if (*Type == "set_light_properties") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - static const std::regex ColorPattern( - R"json("color"\s*:\s*\[\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*\])json"); - static const std::regex IntensityPattern(R"json("intensity"\s*:\s*(-?[0-9Ee.+-]+))json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto Color = MatchVec3(JsonLine, ColorPattern); - std::optional Intensity; - { - std::match_results M; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), M, IntensityPattern)) - Intensity = ParseDouble(std::string_view(M[1].first, M[1].second)); - } - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::SetLightProperties; - Cmd.Color = Color.value_or(glm::vec3(1.0f)); - Cmd.Intensity = static_cast(Intensity.value_or(1.0)); - Cmd.EditorPayload = {.Payload = SetLightPropertiesCommand{ - .ObjectId = ObjectId.value_or(""), - .Color = Cmd.Color, - .Intensity = Cmd.Intensity}}; - return Cmd; + HeadlessCommand Command; + Command.Type = HeadlessCommandType::SetLightProperties; + Command.Color = GetVec3Value(Document, "color").value_or(glm::vec3(1.0f)); + Command.Intensity = GetFloatValue(Document, "intensity").value_or(1.0f); + Command.EditorPayload = {.Payload = SetLightPropertiesCommand{ + .ObjectId = std::string( + GetStringView(Document, "objectId") + .value_or("")), + .Color = Command.Color, + .Intensity = Command.Intensity}}; + return Command; } if (*Type == "set_material_properties") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - static const std::regex BaseColorPattern( - R"json("baseColorFactor"\s*:\s*\[\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*\])json"); - static const std::regex MetallicPattern(R"json("metallic"\s*:\s*(-?[0-9Ee.+-]+))json"); - static const std::regex RoughnessPattern(R"json("roughness"\s*:\s*(-?[0-9Ee.+-]+))json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - std::optional BaseColorFactor; - { - std::match_results M; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), M, BaseColorPattern)) { - auto R = ParseDouble(std::string_view(M[1].first, M[1].second)); - auto G = ParseDouble(std::string_view(M[2].first, M[2].second)); - auto B = ParseDouble(std::string_view(M[3].first, M[3].second)); - auto A = ParseDouble(std::string_view(M[4].first, M[4].second)); - if (R && G && B && A) - BaseColorFactor = glm::vec4( - static_cast(*R), static_cast(*G), - static_cast(*B), static_cast(*A)); - } - } - std::optional Metallic; - std::optional Roughness; - { - std::match_results M; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), M, MetallicPattern)) - Metallic = ParseDouble(std::string_view(M[1].first, M[1].second)); - } - { - std::match_results M; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), M, RoughnessPattern)) - Roughness = ParseDouble(std::string_view(M[1].first, M[1].second)); - } - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::SetMaterialProperties; - Cmd.BaseColorFactor = BaseColorFactor.value_or(glm::vec4(1.0f)); - Cmd.Metallic = static_cast(Metallic.value_or(0.0)); - Cmd.Roughness = static_cast(Roughness.value_or(0.5)); - Cmd.EditorPayload = {.Payload = SetMaterialPropertiesCommand{ - .ObjectId = ObjectId.value_or(""), - .BaseColorFactor = Cmd.BaseColorFactor, - .Metallic = Cmd.Metallic, - .Roughness = Cmd.Roughness}}; - return Cmd; + HeadlessCommand Command; + Command.Type = HeadlessCommandType::SetMaterialProperties; + Command.BaseColorFactor = + GetVec4Value(Document, "baseColorFactor").value_or(glm::vec4(1.0f)); + Command.Metallic = GetFloatValue(Document, "metallic").value_or(0.0f); + Command.Roughness = GetFloatValue(Document, "roughness").value_or(0.5f); + Command.EditorPayload = {.Payload = SetMaterialPropertiesCommand{ + .ObjectId = std::string( + GetStringView(Document, "objectId") + .value_or("")), + .BaseColorFactor = Command.BaseColorFactor, + .Metallic = Command.Metallic, + .Roughness = Command.Roughness}}; + return Command; } if (*Type == "set_material_texture") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - static const std::regex TexturePathPattern(R"json("textureAssetPath"\s*:\s*"([^"]*)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto TexturePath = MatchString(JsonLine, TexturePathPattern); - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::SetMaterialTexture; - Cmd.TextureAssetPath = TexturePath.value_or(""); - Cmd.EditorPayload = {.Payload = SetMaterialTextureCommand{ - .ObjectId = ObjectId.value_or(""), - .TextureAssetPath = Cmd.TextureAssetPath}}; - return Cmd; + HeadlessCommand Command; + Command.Type = HeadlessCommandType::SetMaterialTexture; + Command.TextureAssetPath = + std::string(GetStringView(Document, "textureAssetPath").value_or("")); + Command.EditorPayload = {.Payload = SetMaterialTextureCommand{ + .ObjectId = std::string( + GetStringView(Document, "objectId") + .value_or("")), + .TextureAssetPath = Command.TextureAssetPath}}; + return Command; } if (*Type == "get_schema") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - return HeadlessCommand{.Type = HeadlessCommandType::GetSchema, - .ObjectId = ObjectId.value_or("")}; + return HeadlessCommand{ + .Type = HeadlessCommandType::GetSchema, + .ObjectId = + std::string(GetStringView(Document, "objectId").value_or("")), + }; } if (*Type == "set_property") { - static const std::regex ObjectIdPattern(R"json("objectId"\s*:\s*"([^"]+)")json"); - static const std::regex PropPattern(R"json("property"\s*:\s*"([^"]+)")json"); - static const std::regex StringValPattern(R"json("value"\s*:\s*"([^"]*)")json"); - static const std::regex BoolValPattern(R"json("value"\s*:\s*(true|false))json"); - static const std::regex NumberValPattern( - R"json("value"\s*:\s*(-?[0-9Ee.+-]+))json"); - static const std::regex Vec3ValPattern( - R"json("value"\s*:\s*\[\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*\])json"); - - const auto ObjectId = MatchString(JsonLine, ObjectIdPattern); - const auto PropName = MatchString(JsonLine, PropPattern); - - std::optional Val; - if (const auto StrVal = MatchString(JsonLine, StringValPattern)) { - Val = PropertyValue{*StrVal}; - } else if (const auto BoolStr = MatchString(JsonLine, BoolValPattern)) { - Val = PropertyValue{*BoolStr == "true"}; - } else if (const auto NumberStr = MatchString(JsonLine, NumberValPattern)) { - if (const auto Number = ParseDouble(*NumberStr)) { - Val = PropertyValue{static_cast(*Number)}; - } - } else { - std::match_results M; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), M, Vec3ValPattern) && - M.size() == 4) { - const auto X = ParseDouble(std::string_view(M[1].first, M[1].second)); - const auto Y = ParseDouble(std::string_view(M[2].first, M[2].second)); - const auto Z = ParseDouble(std::string_view(M[3].first, M[3].second)); - if (X && Y && Z) { - Val = PropertyValue{glm::vec3{static_cast(*X), - static_cast(*Y), - static_cast(*Z)}}; - } + HeadlessCommand Command; + Command.Type = HeadlessCommandType::SetProperty; + Command.ObjectId = + std::string(GetStringView(Document, "objectId").value_or("")); + Command.PropertyName = + std::string(GetStringView(Document, "property").value_or("")); + if (const rapidjson::Value *Value = FindMemberValue(Document, "value"); + Value != nullptr) { + if (Value->IsString()) { + Command.PropertyVal = PropertyValue{std::string( + Value->GetString(), Value->GetStringLength())}; + } else if (Value->IsBool()) { + Command.PropertyVal = PropertyValue{Value->GetBool()}; + } else if (Value->IsNumber()) { + Command.PropertyVal = PropertyValue{Value->GetFloat()}; + } else if (const auto Vec3Value = ParseVec3(*Value); + Vec3Value.has_value()) { + Command.PropertyVal = PropertyValue{*Vec3Value}; } } - - return HeadlessCommand{.Type = HeadlessCommandType::SetProperty, - .ObjectId = ObjectId.value_or(""), - .PropertyName = PropName.value_or(""), - .PropertyVal = Val}; + return Command; } if (*Type == "heartbeat") { return HeadlessCommand{.Type = HeadlessCommandType::Heartbeat}; } if (*Type == "set_world_settings") { - static const std::regex TopPattern( - R"json("skyboxColorTop"\s*:\s*\[\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*\])json"); - static const std::regex BottomPattern( - R"json("skyboxColorBottom"\s*:\s*\[\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*,\s*(-?[0-9Ee.+-]+)\s*\])json"); - static const std::regex HDRPattern( - R"json("skyboxHDRPath"\s*:\s*"([^"]*)")json"); - - HeadlessCommand Cmd; - Cmd.Type = HeadlessCommandType::SetWorldSettings; - - std::match_results Match; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), Match, TopPattern) && - Match.size() == 4) { - if (const auto R = ParseDouble(std::string_view(Match[1].first, Match[1].second))) - Cmd.SkyboxColorTop.r = static_cast(*R); - if (const auto G = ParseDouble(std::string_view(Match[2].first, Match[2].second))) - Cmd.SkyboxColorTop.g = static_cast(*G); - if (const auto B = ParseDouble(std::string_view(Match[3].first, Match[3].second))) - Cmd.SkyboxColorTop.b = static_cast(*B); + HeadlessCommand Command; + Command.Type = HeadlessCommandType::SetWorldSettings; + if (const auto Top = GetVec3Value(Document, "skyboxColorTop"); + Top.has_value()) { + Command.SkyboxColorTop = *Top; } - if (std::regex_search(JsonLine.begin(), JsonLine.end(), Match, BottomPattern) && - Match.size() == 4) { - if (const auto R = ParseDouble(std::string_view(Match[1].first, Match[1].second))) - Cmd.SkyboxColorBottom.r = static_cast(*R); - if (const auto G = ParseDouble(std::string_view(Match[2].first, Match[2].second))) - Cmd.SkyboxColorBottom.g = static_cast(*G); - if (const auto B = ParseDouble(std::string_view(Match[3].first, Match[3].second))) - Cmd.SkyboxColorBottom.b = static_cast(*B); - } - - if (std::regex_search(JsonLine.begin(), JsonLine.end(), Match, HDRPattern) && - Match.size() == 2) { - Cmd.SkyboxHDRPath.assign(Match[1].first, Match[1].second); + if (const auto Bottom = GetVec3Value(Document, "skyboxColorBottom"); + Bottom.has_value()) { + Command.SkyboxColorBottom = *Bottom; } - - Cmd.EditorPayload = {.Payload = SetWorldSettingsCommand{ - .Settings = EditorWorldSettings{ - .SkyboxColorTop = Cmd.SkyboxColorTop, - .SkyboxColorBottom = Cmd.SkyboxColorBottom, - .SkyboxHDRPath = Cmd.SkyboxHDRPath}}}; - return Cmd; + Command.SkyboxHDRPath = + std::string(GetStringView(Document, "skyboxHDRPath").value_or("")); + Command.EditorPayload = {.Payload = SetWorldSettingsCommand{ + .Settings = EditorWorldSettings{ + .SkyboxColorTop = Command.SkyboxColorTop, + .SkyboxColorBottom = + Command.SkyboxColorBottom, + .SkyboxHDRPath = Command.SkyboxHDRPath}}}; + return Command; } if (*Type == "set_gizmo_mode") { - static const std::regex ModePattern(R"json("mode"\s*:\s*"([^"]+)")json"); - const auto ModeStr = MatchString(JsonLine, ModePattern); GizmoMode Mode = GizmoMode::Translate; - if (ModeStr.has_value()) { + if (const auto ModeStr = GetStringView(Document, "mode"); + ModeStr.has_value()) { if (*ModeStr == "scale") { Mode = GizmoMode::Scale; } else if (*ModeStr == "rotate") { Mode = GizmoMode::Rotate; } } - return HeadlessCommand{.Type = HeadlessCommandType::SetGizmoMode, .Mode = Mode}; + return HeadlessCommand{.Type = HeadlessCommandType::SetGizmoMode, + .Mode = Mode}; } if (*Type == "set_grid_snap") { - static const std::regex EnabledPattern(R"json("enabled"\s*:\s*(true|false))json"); - static const std::regex TranslationStepPattern( - R"json("translationStep"\s*:\s*(-?[0-9Ee.+-]+))json"); - static const std::regex RotationStepPattern( - R"json("rotationStepDegrees"\s*:\s*(-?[0-9Ee.+-]+))json"); - static const std::regex ScaleStepPattern( - R"json("scaleStep"\s*:\s*(-?[0-9Ee.+-]+))json"); - const auto EnabledStr = MatchString(JsonLine, EnabledPattern); - auto ParseScalar = [&](const std::regex &pattern, double fallback) { - std::match_results match; - if (std::regex_search(JsonLine.begin(), JsonLine.end(), match, pattern)) { - if (const auto value = - ParseDouble(std::string_view(match[1].first, match[1].second))) { - return static_cast(*value); - } - } - return static_cast(fallback); - }; return HeadlessCommand{ .Type = HeadlessCommandType::SetGridSnap, - .Enabled = EnabledStr.value_or("false") == "true", - .TranslationStep = ParseScalar(TranslationStepPattern, 1.0), - .RotationStepDegrees = ParseScalar(RotationStepPattern, 15.0), - .ScaleStep = ParseScalar(ScaleStepPattern, 0.1)}; + .Enabled = GetBoolValue(Document, "enabled").value_or(false), + .TranslationStep = + GetFloatValue(Document, "translationStep").value_or(1.0f), + .RotationStepDegrees = + GetFloatValue(Document, "rotationStepDegrees").value_or(15.0f), + .ScaleStep = GetFloatValue(Document, "scaleStep").value_or(0.1f), + }; } - Error = "Unsupported command type: " + *Type; + Error = "Unsupported command type: " + std::string(*Type); return std::nullopt; } @@ -1259,282 +1175,386 @@ ParseRemoteViewportCommand(std::string_view JsonLine, std::string &Error) { return std::nullopt; } -std::string EscapeJson(std::string_view Value) { - std::string Escaped; - Escaped.reserve(Value.size()); - for (const char Character : Value) { - switch (Character) { - case '\\': - Escaped += "\\\\"; - break; - case '"': - Escaped += "\\\""; - break; - case '\n': - Escaped += "\\n"; - break; - case '\r': - Escaped += "\\r"; - break; - case '\t': - Escaped += "\\t"; - break; - default: - Escaped.push_back(Character); - break; - } - } - return Escaped; -} - std::string SerializeEvent(const PublishedEditorEvent &Event) { - std::ostringstream Stream; - Stream << "{\"type\":\"event\",\"eventId\":" << Event.Id.Value - << ",\"payloadType\":\"" << EventPayloadType(Event.Event.Payload) << "\""; - if (const auto *Camera = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Camera->User.Value << ",\"position\":[" - << Camera->Position.x << "," << Camera->Position.y << "," - << Camera->Position.z << "],\"yawDegrees\":" << Camera->YawDegrees - << ",\"pitchDegrees\":" << Camera->PitchDegrees; - } else if (const auto *Look = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Look->User.Value << ",\"isLooking\":" - << (Look->IsLooking ? "true" : "false"); - } else if (const auto *Acknowledged = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Acknowledged->User.Value - << ",\"acknowledgedCommandId\":" - << Acknowledged->AcknowledgedCommand.Value - << ",\"commandType\":\"" << EscapeJson(Acknowledged->CommandType) - << "\""; - } else if (const auto *Rejected = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Rejected->User.Value - << ",\"rejectedCommandId\":" << Rejected->RejectedCommand.Value - << ",\"reason\":\"" << EscapeJson(Rejected->Reason) << "\""; - } else if (const auto *Presence = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Presence->User.Value - << ",\"displayName\":\"" << EscapeJson(Presence->DisplayName) - << "\",\"isLocal\":" << (Presence->IsLocal ? "true" : "false") - << ",\"presenceState\":\"" << EscapeJson(Presence->PresenceState) - << "\",\"selectionObjectId\":"; - if (Presence->SelectedObjectId.has_value()) { - Stream << "\"" << EscapeJson(*Presence->SelectedObjectId) << "\""; - } else { - Stream << "null"; - } - } else if (const auto *Selection = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Selection->User.Value << ",\"objectId\":"; - if (Selection->ObjectId.has_value()) { - Stream << "\"" << EscapeJson(*Selection->ObjectId) << "\""; - } else { - Stream << "null"; - } - } else if (const auto *Rename = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Rename->User.Value << ",\"objectId\":\"" - << EscapeJson(Rename->ObjectId) << "\",\"displayName\":\"" - << EscapeJson(Rename->DisplayName) << "\""; - } else if (const auto *Visibility = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Visibility->User.Value << ",\"objectId\":\"" - << EscapeJson(Visibility->ObjectId) << "\",\"visible\":" - << (Visibility->Visible ? "true" : "false"); - } else if (const auto *Created = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Created->User.Value << ",\"objectId\":\"" - << EscapeJson(Created->ObjectId) << "\",\"displayName\":\"" - << EscapeJson(Created->DisplayName) << "\""; - } else if (const auto *Deleted = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Deleted->User.Value << ",\"objectId\":\"" - << EscapeJson(Deleted->ObjectId) << "\""; - } else if (const auto *Reparented = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Reparented->User.Value << ",\"objectId\":\"" - << EscapeJson(Reparented->ObjectId) << "\",\"newParentId\":\"" - << EscapeJson(Reparented->NewParentId) << "\""; - } else if (const auto *Transform = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << Transform->User.Value << ",\"objectId\":\"" - << EscapeJson(Transform->ObjectId) << "\",\"location\":[" - << Transform->Location.x << "," << Transform->Location.y << "," - << Transform->Location.z << "],\"rotationDegrees\":[" - << Transform->RotationDegrees.x << "," - << Transform->RotationDegrees.y << "," - << Transform->RotationDegrees.z << "],\"scale\":[" - << Transform->Scale.x << "," << Transform->Scale.y << "," - << Transform->Scale.z << "]"; - } else if (const auto *LockChanged = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(LockChanged->ObjectId) - << "\",\"lockState\":\"" << LockStateToString(LockChanged->LockState) - << "\",\"lockOwnerUserId\":"; - if (LockChanged->LockOwner.has_value()) { - Stream << LockChanged->LockOwner->Value; - } else { - Stream << "null"; - } - } else if (const auto *ScriptChanged = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(ScriptChanged->ObjectId) - << "\",\"scriptClass\":"; - if (ScriptChanged->ScriptClass.has_value()) { - Stream << "\"" << EscapeJson(*ScriptChanged->ScriptClass) << "\""; - } else { - Stream << "null"; + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("event"); + Writer.Key("eventId"); + Writer.Uint64(Event.Id.Value); + Writer.Key("payloadType"); + WriteString(Writer, EventPayloadType(Event.Event.Payload)); + + if (const auto *Camera = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Camera->User.Value); + Writer.Key("position"); + WriteVec3(Writer, Camera->Position); + Writer.Key("yawDegrees"); + WriteNumber(Writer, Camera->YawDegrees); + Writer.Key("pitchDegrees"); + WriteNumber(Writer, Camera->PitchDegrees); + } else if (const auto *Look = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Look->User.Value); + Writer.Key("isLooking"); + Writer.Bool(Look->IsLooking); + } else if (const auto *Acknowledged = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Acknowledged->User.Value); + Writer.Key("acknowledgedCommandId"); + Writer.Uint64(Acknowledged->AcknowledgedCommand.Value); + Writer.Key("commandType"); + WriteString(Writer, Acknowledged->CommandType); + } else if (const auto *Rejected = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Rejected->User.Value); + Writer.Key("rejectedCommandId"); + Writer.Uint64(Rejected->RejectedCommand.Value); + Writer.Key("reason"); + WriteString(Writer, Rejected->Reason); + } else if (const auto *Presence = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Presence->User.Value); + Writer.Key("displayName"); + WriteString(Writer, Presence->DisplayName); + Writer.Key("isLocal"); + Writer.Bool(Presence->IsLocal); + Writer.Key("presenceState"); + WriteString(Writer, Presence->PresenceState); + Writer.Key("selectionObjectId"); + if (Presence->SelectedObjectId.has_value()) { + WriteString(Writer, *Presence->SelectedObjectId); + } else { + Writer.Null(); + } + } else if (const auto *Selection = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Selection->User.Value); + Writer.Key("objectId"); + if (Selection->ObjectId.has_value()) { + WriteString(Writer, *Selection->ObjectId); + } else { + Writer.Null(); + } + } else if (const auto *Rename = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Rename->User.Value); + Writer.Key("objectId"); + WriteString(Writer, Rename->ObjectId); + Writer.Key("displayName"); + WriteString(Writer, Rename->DisplayName); + } else if (const auto *Visibility = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Visibility->User.Value); + Writer.Key("objectId"); + WriteString(Writer, Visibility->ObjectId); + Writer.Key("visible"); + Writer.Bool(Visibility->Visible); + } else if (const auto *Created = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Created->User.Value); + Writer.Key("objectId"); + WriteString(Writer, Created->ObjectId); + Writer.Key("displayName"); + WriteString(Writer, Created->DisplayName); + } else if (const auto *Deleted = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Deleted->User.Value); + Writer.Key("objectId"); + WriteString(Writer, Deleted->ObjectId); + } else if (const auto *Reparented = + std::get_if(&Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Reparented->User.Value); + Writer.Key("objectId"); + WriteString(Writer, Reparented->ObjectId); + Writer.Key("newParentId"); + WriteString(Writer, Reparented->NewParentId); + } else if (const auto *Transform = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(Transform->User.Value); + Writer.Key("objectId"); + WriteString(Writer, Transform->ObjectId); + Writer.Key("location"); + WriteVec3(Writer, Transform->Location); + Writer.Key("rotationDegrees"); + WriteVec3(Writer, Transform->RotationDegrees); + Writer.Key("scale"); + WriteVec3(Writer, Transform->Scale); + } else if (const auto *LockChanged = + std::get_if(&Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, LockChanged->ObjectId); + Writer.Key("lockState"); + WriteString(Writer, LockStateToString(LockChanged->LockState)); + Writer.Key("lockOwnerUserId"); + if (LockChanged->LockOwner.has_value()) { + Writer.Uint64(LockChanged->LockOwner->Value); + } else { + Writer.Null(); + } + } else if (const auto *ScriptChanged = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, ScriptChanged->ObjectId); + Writer.Key("scriptClass"); + if (ScriptChanged->ScriptClass.has_value()) { + WriteString(Writer, *ScriptChanged->ScriptClass); + } else { + Writer.Null(); + } + } else if (const auto *ScriptError = + std::get_if(&Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, ScriptError->ObjectId); + Writer.Key("message"); + WriteString(Writer, ScriptError->Message); + } else if (const auto *MeshAsset = + std::get_if(&Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, MeshAsset->ObjectId); + Writer.Key("assetPath"); + WriteString(Writer, MeshAsset->AssetPath); + } else if (const auto *LightProps = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, LightProps->ObjectId); + Writer.Key("color"); + WriteVec3(Writer, LightProps->Color); + Writer.Key("intensity"); + WriteNumber(Writer, LightProps->Intensity); + } else if (const auto *MaterialProps = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, MaterialProps->ObjectId); + Writer.Key("baseColorFactor"); + WriteVec4(Writer, MaterialProps->BaseColorFactor); + Writer.Key("metallic"); + WriteNumber(Writer, MaterialProps->Metallic); + Writer.Key("roughness"); + WriteNumber(Writer, MaterialProps->Roughness); + } else if (const auto *TextureEvent = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, TextureEvent->ObjectId); + Writer.Key("textureAssetPath"); + WriteString(Writer, TextureEvent->TextureAssetPath); + } else if (const auto *PhysicsProps = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("objectId"); + WriteString(Writer, PhysicsProps->ObjectId); + Writer.Key("bodyType"); + WriteString(Writer, + PhysicsBodyTypeToString(PhysicsProps->Physics.BodyType)); + Writer.Key("colliderType"); + WriteString(Writer, + PhysicsColliderTypeToString( + PhysicsProps->Physics.ColliderType)); + Writer.Key("boxHalfExtents"); + WriteVec3(Writer, PhysicsProps->Physics.BoxHalfExtents); + Writer.Key("sphereRadius"); + WriteNumber(Writer, PhysicsProps->Physics.SphereRadius); + Writer.Key("mass"); + WriteNumber(Writer, PhysicsProps->Physics.Mass); + Writer.Key("friction"); + WriteNumber(Writer, PhysicsProps->Physics.Friction); + Writer.Key("restitution"); + WriteNumber(Writer, PhysicsProps->Physics.Restitution); + } else if (const auto *RuntimeState = + std::get_if( + &Event.Event.Payload)) { + Writer.Key("user"); + Writer.Uint64(RuntimeState->User.Value); + Writer.Key("runtimeState"); + WriteString(Writer, RuntimeStateToString(RuntimeState->State)); } - } else if (const auto *ScriptError = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(ScriptError->ObjectId) - << "\",\"message\":\"" << EscapeJson(ScriptError->Message) << "\""; - } else if (const auto *MeshAsset = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(MeshAsset->ObjectId) - << "\",\"assetPath\":\"" << EscapeJson(MeshAsset->AssetPath) << "\""; - } else if (const auto *LightProps = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(LightProps->ObjectId) - << "\",\"color\":[" << LightProps->Color.r << "," << LightProps->Color.g - << "," << LightProps->Color.b << "],\"intensity\":" << LightProps->Intensity; - } else if (const auto *MatProps = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(MatProps->ObjectId) - << "\",\"baseColorFactor\":[" - << MatProps->BaseColorFactor.r << "," << MatProps->BaseColorFactor.g << "," - << MatProps->BaseColorFactor.b << "," << MatProps->BaseColorFactor.a - << "],\"metallic\":" << MatProps->Metallic - << ",\"roughness\":" << MatProps->Roughness; - } else if (const auto *TexEv = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(TexEv->ObjectId) - << "\",\"textureAssetPath\":\"" << EscapeJson(TexEv->TextureAssetPath) << "\""; - } else if (const auto *PhysicsProps = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"objectId\":\"" << EscapeJson(PhysicsProps->ObjectId) - << "\",\"bodyType\":\"" - << PhysicsBodyTypeToString(PhysicsProps->Physics.BodyType) - << "\",\"colliderType\":\"" - << PhysicsColliderTypeToString(PhysicsProps->Physics.ColliderType) - << "\",\"boxHalfExtents\":[" - << PhysicsProps->Physics.BoxHalfExtents.x << "," - << PhysicsProps->Physics.BoxHalfExtents.y << "," - << PhysicsProps->Physics.BoxHalfExtents.z - << "],\"sphereRadius\":" << PhysicsProps->Physics.SphereRadius - << ",\"mass\":" << PhysicsProps->Physics.Mass - << ",\"friction\":" << PhysicsProps->Physics.Friction - << ",\"restitution\":" << PhysicsProps->Physics.Restitution; - } else if (const auto *RuntimeState = - std::get_if(&Event.Event.Payload)) { - Stream << ",\"user\":" << RuntimeState->User.Value - << ",\"runtimeState\":\"" - << RuntimeStateToString(RuntimeState->State) << "\""; - } - Stream << "}"; - return Stream.str(); + + Writer.EndObject(); + }); } std::string SerializeReady(uint32_t Width, uint32_t Height) { - std::ostringstream Stream; - Stream << "{\"type\":\"ready\",\"width\":" << Width << ",\"height\":" << Height - << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("ready"); + Writer.Key("width"); + Writer.Uint(Width); + Writer.Key("height"); + Writer.Uint(Height); + Writer.EndObject(); + }); } -std::string SerializeConnected() { return "{\"type\":\"connected\"}"; } +std::string SerializeConnected() { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("connected"); + Writer.EndObject(); + }); +} -std::string SerializeDisconnected() { return "{\"type\":\"disconnected\"}"; } +std::string SerializeDisconnected() { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("disconnected"); + Writer.EndObject(); + }); +} std::string SerializeFrame(const std::filesystem::path &Path, const CapturedFrame &Frame) { - std::ostringstream Stream; - Stream << "{\"type\":\"frame\",\"frameIndex\":" << Frame.FrameIndex - << ",\"path\":\"" << EscapeJson(Path.string()) << "\",\"width\":" - << Frame.Width << ",\"height\":" << Frame.Height << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("frame"); + Writer.Key("frameIndex"); + Writer.Uint64(Frame.FrameIndex); + Writer.Key("path"); + WriteString(Writer, Path.string()); + Writer.Key("width"); + Writer.Uint(Frame.Width); + Writer.Key("height"); + Writer.Uint(Frame.Height); + Writer.EndObject(); + }); } std::string SerializeFrameMetadata(uint64_t FrameIndex, uint32_t Width, uint32_t Height, std::string_view FrameUrl) { - std::ostringstream Stream; - Stream << "{\"type\":\"frame\",\"frameIndex\":" << FrameIndex - << ",\"path\":\"" << EscapeJson(FrameUrl) << "\",\"width\":" << Width - << ",\"height\":" << Height << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("frame"); + Writer.Key("frameIndex"); + Writer.Uint64(FrameIndex); + Writer.Key("path"); + WriteString(Writer, FrameUrl); + Writer.Key("width"); + Writer.Uint(Width); + Writer.Key("height"); + Writer.Uint(Height); + Writer.EndObject(); + }); } std::string SerializeEncodedVideoPacketMetadata( const EncodedVideoPacket &Packet, std::string_view PacketUrl) { - std::ostringstream Stream; - Stream << "{\"type\":\"encoded_video\",\"codec\":\""; - switch (Packet.Codec) { - case EncodedVideoCodec::H264: - Stream << "h264"; - break; - } - Stream << "\",\"frameIndex\":" << Packet.FrameIndex - << ",\"path\":\"" << EscapeJson(PacketUrl) << "\",\"width\":" - << Packet.Width << ",\"height\":" << Packet.Height - << ",\"isKeyframe\":" - << (Packet.IsKeyframe ? "true" : "false") - << ",\"byteLength\":" << Packet.Bytes.size() << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("encoded_video"); + Writer.Key("codec"); + switch (Packet.Codec) { + case EncodedVideoCodec::H264: + Writer.String("h264"); + break; + } + Writer.Key("frameIndex"); + Writer.Uint64(Packet.FrameIndex); + Writer.Key("path"); + WriteString(Writer, PacketUrl); + Writer.Key("width"); + Writer.Uint(Packet.Width); + Writer.Key("height"); + Writer.Uint(Packet.Height); + Writer.Key("isKeyframe"); + Writer.Bool(Packet.IsKeyframe); + Writer.Key("byteLength"); + Writer.Uint64(Packet.Bytes.size()); + Writer.EndObject(); + }); } std::optional ParseWebRtcSessionDescription(std::string_view JsonLine, std::string &Error) { - static const std::regex TypePattern(R"json("type"\s*:\s*"([^"]+)")json"); - static const std::regex SdpPattern(R"json("sdp"\s*:\s*"((?:\\.|[^"])*)")json"); + std::string MutableJson; + auto ParsedDocument = ParseJson(JsonLine, MutableJson, Error); + if (!ParsedDocument.has_value()) { + Error = "WebRTC session description is not valid JSON."; + return std::nullopt; + } + rapidjson::Document &Document = *ParsedDocument; - const auto Type = MatchString(JsonLine, TypePattern); + const auto Type = GetStringView(Document, "type"); if (!Type.has_value()) { Error = "WebRTC session description is missing a string `type` field."; return std::nullopt; } - - const auto Sdp = MatchString(JsonLine, SdpPattern); + const auto Sdp = GetStringView(Document, "sdp"); if (!Sdp.has_value()) { Error = "WebRTC session description is missing a string `sdp` field."; return std::nullopt; } - if (*Type != "offer" && *Type != "answer") { - Error = "Unsupported WebRTC session description type: " + *Type; + Error = "Unsupported WebRTC session description type: " + + std::string(*Type); return std::nullopt; } - - return WebRtcSessionDescription{.Type = *Type, - .Sdp = UnescapeJsonString(*Sdp)}; + return WebRtcSessionDescription{ + .Type = std::string(*Type), + .Sdp = std::string(*Sdp), + }; } std::optional ParseWebRtcIceCandidate(std::string_view JsonLine, std::string &Error) { - static const std::regex CandidatePattern( - R"json("candidate"\s*:\s*"((?:\\.|[^"])*)")json"); - static const std::regex MidPattern(R"json("sdpMid"\s*:\s*"([^"]+)")json"); - static const std::regex MLinePattern( - R"json("sdpMLineIndex"\s*:\s*([0-9]+))json"); + std::string MutableJson; + auto ParsedDocument = ParseJson(JsonLine, MutableJson, Error); + if (!ParsedDocument.has_value()) { + Error = "WebRTC ICE candidate is not valid JSON."; + return std::nullopt; + } + rapidjson::Document &Document = *ParsedDocument; - const auto Candidate = MatchString(JsonLine, CandidatePattern); + const auto Candidate = GetStringView(Document, "candidate"); if (!Candidate.has_value()) { Error = "WebRTC ICE candidate is missing a string `candidate` field."; return std::nullopt; } - WebRtcIceCandidate Parsed{.Candidate = UnescapeJsonString(*Candidate)}; - if (const auto Mid = MatchString(JsonLine, MidPattern); Mid.has_value()) { - Parsed.SdpMid = UnescapeJsonString(*Mid); - } - - const auto MLineValue = MatchString(JsonLine, MLinePattern); - if (MLineValue.has_value()) { - const auto ParsedIndex = ParseUnsigned16(*MLineValue); + WebRtcIceCandidate Parsed{ + .Candidate = std::string(*Candidate), + }; + if (const auto SdpMid = GetStringView(Document, "sdpMid"); + SdpMid.has_value()) { + Parsed.SdpMid = std::string(*SdpMid); + } + if (const rapidjson::Value *MLineIndex = + FindMemberValue(Document, "sdpMLineIndex"); + MLineIndex != nullptr) { + if (!MLineIndex->IsUint()) { + Error = + "WebRTC ICE candidate `sdpMLineIndex` must be an unsigned integer."; + return std::nullopt; + } + const auto ParsedIndex = + ParseUnsigned16(std::to_string(MLineIndex->GetUint())); if (!ParsedIndex.has_value()) { - Error = "WebRTC ICE candidate `sdpMLineIndex` must be an unsigned integer."; + Error = + "WebRTC ICE candidate `sdpMLineIndex` must be an unsigned integer."; return std::nullopt; } Parsed.SdpMLineIndex = *ParsedIndex; @@ -1545,41 +1565,62 @@ ParseWebRtcIceCandidate(std::string_view JsonLine, std::string &Error) { std::string SerializeWebRtcSessionDescription( const WebRtcSessionDescription &Description, std::string_view SessionId) { - std::ostringstream Stream; - Stream << "{\"type\":\"" << EscapeJson(Description.Type) << "\",\"sdp\":\"" - << EscapeJson(Description.Sdp) << "\""; - if (!SessionId.empty()) { - Stream << ",\"sessionId\":\"" << EscapeJson(SessionId) << "\""; - } - Stream << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + WriteString(Writer, Description.Type); + Writer.Key("sdp"); + WriteString(Writer, Description.Sdp); + if (!SessionId.empty()) { + Writer.Key("sessionId"); + WriteString(Writer, SessionId); + } + Writer.EndObject(); + }); } std::string SerializeWebRtcIceCandidate(const WebRtcIceCandidate &Candidate) { - std::ostringstream Stream; - Stream << "{\"candidate\":\"" << EscapeJson(Candidate.Candidate) << "\""; - if (Candidate.SdpMid.has_value()) { - Stream << ",\"sdpMid\":\"" << EscapeJson(*Candidate.SdpMid) << "\""; - } - if (Candidate.SdpMLineIndex.has_value()) { - Stream << ",\"sdpMLineIndex\":" << *Candidate.SdpMLineIndex; - } - Stream << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("candidate"); + WriteString(Writer, Candidate.Candidate); + if (Candidate.SdpMid.has_value()) { + Writer.Key("sdpMid"); + WriteString(Writer, *Candidate.SdpMid); + } + if (Candidate.SdpMLineIndex.has_value()) { + Writer.Key("sdpMLineIndex"); + Writer.Uint(*Candidate.SdpMLineIndex); + } + Writer.EndObject(); + }); } std::string SerializeWebRtcIceCandidateList( std::span Candidates) { - std::ostringstream Stream; - Stream << "{\"type\":\"ice_candidates\",\"candidates\":["; - for (size_t Index = 0; Index < Candidates.size(); ++Index) { - if (Index != 0) { - Stream << ","; + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("ice_candidates"); + Writer.Key("candidates"); + Writer.StartArray(); + for (const auto &Candidate : Candidates) { + Writer.StartObject(); + Writer.Key("candidate"); + WriteString(Writer, Candidate.Candidate); + if (Candidate.SdpMid.has_value()) { + Writer.Key("sdpMid"); + WriteString(Writer, *Candidate.SdpMid); + } + if (Candidate.SdpMLineIndex.has_value()) { + Writer.Key("sdpMLineIndex"); + Writer.Uint(*Candidate.SdpMLineIndex); + } + Writer.EndObject(); } - Stream << SerializeWebRtcIceCandidate(Candidates[Index]); - } - Stream << "]}"; - return Stream.str(); + Writer.EndArray(); + Writer.EndObject(); + }); } std::string SerializeSessionSnapshot(const EditorSessionState &State, @@ -1590,8 +1631,7 @@ std::string SerializeSessionSnapshot(const EditorSessionState &State, std::string_view WebRtcConnectionState) { const std::vector Participants = BuildParticipants(State, CurrentUser); - const SessionUserId RuntimeControllerUser = - [&]() -> SessionUserId { + const SessionUserId RuntimeControllerUser = [&]() -> SessionUserId { if (State.RuntimeControllerUser.has_value()) { return *State.RuntimeControllerUser; } @@ -1609,73 +1649,88 @@ std::string SerializeSessionSnapshot(const EditorSessionState &State, return Candidate.value_or(SessionUserId{1}); }(); - std::ostringstream Stream; - Stream << "{\"type\":\"session_snapshot\",\"sessionId\":" << State.Session.Value - << ",\"currentUserId\":" << CurrentUser.Value - << ",\"runtimeControllerUserId\":" << RuntimeControllerUser.Value - << ",\"showColliders\":" << (ShowColliders ? "true" : "false") - << ",\"runtimeState\":\"" << RuntimeStateToString(State.RuntimeState) - << "\"" - << ",\"transport\":{\"connected\":" - << (TransportConnected ? "true" : "false") << ",\"state\":\"" - << EscapeJson(TransportState) << "\",\"webrtcConnectionState\":\"" - << EscapeJson(WebRtcConnectionState) << "\"},\"participants\":["; - - for (size_t Index = 0; Index < Participants.size(); ++Index) { - if (Index != 0) { - Stream << ","; - } - SerializeParticipant(Stream, Participants[Index]); - } - - Stream << "],\"selections\":["; - bool FirstSelection = true; - for (const auto &[User, ObjectId] : State.SelectedObjectIds) { - if (!FirstSelection) { - Stream << ","; + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("session_snapshot"); + Writer.Key("sessionId"); + Writer.Uint64(State.Session.Value); + Writer.Key("currentUserId"); + Writer.Uint64(CurrentUser.Value); + Writer.Key("runtimeControllerUserId"); + Writer.Uint64(RuntimeControllerUser.Value); + Writer.Key("showColliders"); + Writer.Bool(ShowColliders); + Writer.Key("runtimeState"); + WriteString(Writer, RuntimeStateToString(State.RuntimeState)); + + Writer.Key("transport"); + Writer.StartObject(); + Writer.Key("connected"); + Writer.Bool(TransportConnected); + Writer.Key("state"); + WriteString(Writer, TransportState); + Writer.Key("webrtcConnectionState"); + WriteString(Writer, WebRtcConnectionState); + Writer.EndObject(); + + Writer.Key("participants"); + Writer.StartArray(); + for (const auto &Participant : Participants) { + WriteParticipant(Writer, Participant); } - FirstSelection = false; - Stream << "{\"userId\":" << User.Value << ",\"objectId\":\"" - << EscapeJson(ObjectId) << "\"}"; - } + Writer.EndArray(); + + Writer.Key("selections"); + Writer.StartArray(); + for (const auto &[User, ObjectId] : State.SelectedObjectIds) { + Writer.StartObject(); + Writer.Key("userId"); + Writer.Uint64(User.Value); + Writer.Key("objectId"); + WriteString(Writer, ObjectId); + Writer.EndObject(); + } + Writer.EndArray(); - Stream << "],\"sceneTree\":["; - for (size_t Index = 0; Index < State.Scene.Items.size(); ++Index) { - if (Index != 0) { - Stream << ","; + Writer.Key("sceneTree"); + Writer.StartArray(); + for (const auto &Item : State.Scene.Items) { + WriteSceneItem(Writer, Item); } - SerializeSceneItem(Stream, State.Scene.Items[Index]); - } - Stream << "],\"worldSettings\":{\"skyboxColorTop\":[" - << State.Scene.WorldSettings.SkyboxColorTop.r << "," - << State.Scene.WorldSettings.SkyboxColorTop.g << "," - << State.Scene.WorldSettings.SkyboxColorTop.b << "],\"skyboxColorBottom\":[" - << State.Scene.WorldSettings.SkyboxColorBottom.r << "," - << State.Scene.WorldSettings.SkyboxColorBottom.g << "," - << State.Scene.WorldSettings.SkyboxColorBottom.b - << "],\"skyboxHDRPath\":\"" - << EscapeJson(State.Scene.WorldSettings.SkyboxHDRPath) << "\"}" - << ",\"selectedObjectDetails\":"; - if (const EditorObjectDetails *Details = - [&]() -> const EditorObjectDetails * { - const auto SelectionIt = State.SelectedObjectIds.find(CurrentUser); - if (SelectionIt == State.SelectedObjectIds.end()) { - return nullptr; - } - const auto DetailsIt = - State.Scene.ObjectDetailsById.find(SelectionIt->second); - return DetailsIt != State.Scene.ObjectDetailsById.end() - ? &DetailsIt->second - : nullptr; - }(); - Details != nullptr) { - SerializeObjectDetails(Stream, State, *Details); - } else { - Stream << "null"; - } - Stream << "}"; - return Stream.str(); + Writer.EndArray(); + + Writer.Key("worldSettings"); + Writer.StartObject(); + Writer.Key("skyboxColorTop"); + WriteVec3(Writer, State.Scene.WorldSettings.SkyboxColorTop); + Writer.Key("skyboxColorBottom"); + WriteVec3(Writer, State.Scene.WorldSettings.SkyboxColorBottom); + Writer.Key("skyboxHDRPath"); + WriteString(Writer, State.Scene.WorldSettings.SkyboxHDRPath); + Writer.EndObject(); + + Writer.Key("selectedObjectDetails"); + const EditorObjectDetails *Details = [&]() -> const EditorObjectDetails * { + const auto SelectionIt = State.SelectedObjectIds.find(CurrentUser); + if (SelectionIt == State.SelectedObjectIds.end()) { + return nullptr; + } + const auto DetailsIt = + State.Scene.ObjectDetailsById.find(SelectionIt->second); + return DetailsIt != State.Scene.ObjectDetailsById.end() + ? &DetailsIt->second + : nullptr; + }(); + if (Details != nullptr) { + WriteObjectDetails(Writer, State, *Details); + } else { + Writer.Null(); + } + + Writer.EndObject(); + }); } std::string SerializeSessionConnectResponse( @@ -1683,13 +1738,23 @@ std::string SerializeSessionConnectResponse( SessionUserId CurrentUser, bool ShowColliders, bool TransportConnected, std::string_view TransportState, std::string_view WebRtcConnectionState) { - std::ostringstream Stream; - Stream << "{\"type\":\"session_connect\",\"clientId\":\"" - << EscapeJson(ClientId) << "\",\"snapshot\":" - << SerializeSessionSnapshot(State, CurrentUser, ShowColliders, TransportConnected, - TransportState, WebRtcConnectionState) - << "}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("session_connect"); + Writer.Key("clientId"); + WriteString(Writer, ClientId); + Writer.Key("snapshot"); + + const std::string Snapshot = SerializeSessionSnapshot( + State, CurrentUser, ShowColliders, TransportConnected, TransportState, + WebRtcConnectionState); + rapidjson::Document SnapshotDocument; + SnapshotDocument.Parse(Snapshot.c_str()); + SnapshotDocument.Accept(Writer); + + Writer.EndObject(); + }); } std::string SerializeWebRtcStatus(bool Enabled, bool Available, @@ -1699,159 +1764,230 @@ std::string SerializeWebRtcStatus(bool Enabled, bool Available, std::string_view SessionId, size_t PendingLocalIceCandidateCount, const WebRtcVideoStatus &VideoStatus) { - std::ostringstream Stream; - Stream << "{\"type\":\"webrtc_status\",\"enabled\":" - << (Enabled ? "true" : "false") << ",\"available\":" - << (Available ? "true" : "false") << ",\"signalingState\":\"" - << EscapeJson(SignalingState) << "\",\"connectionState\":\"" - << EscapeJson(ConnectionState) << "\",\"detail\":\"" - << EscapeJson(Detail) << "\",\"sessionId\":\"" - << EscapeJson(SessionId) << "\",\"pendingLocalIceCandidateCount\":" - << PendingLocalIceCandidateCount << ",\"video\":{\"codec\":\"" - << EscapeJson(VideoStatus.Codec) << "\",\"senderBound\":" - << (VideoStatus.SenderBound ? "true" : "false") - << ",\"waitingForKeyframe\":" - << (VideoStatus.WaitingForKeyframe ? "true" : "false") - << ",\"hasOutstandingSendRequest\":" - << (VideoStatus.HasOutstandingSendRequest ? "true" : "false") - << ",\"pendingPacketCount\":" << VideoStatus.PendingPacketCount - << ",\"droppedPacketCount\":" << VideoStatus.DroppedPacketCount - << ",\"droppedStaleRequestCount\":" - << VideoStatus.DroppedStaleRequestCount - << ",\"droppedStalePacketCount\":" - << VideoStatus.DroppedStalePacketCount - << ",\"lastFrameIndex\":"; - if (VideoStatus.LastFrameIndex.has_value()) { - Stream << *VideoStatus.LastFrameIndex; - } else { - Stream << "null"; - } - Stream << ",\"latestRequestedFrameIndex\":"; - if (VideoStatus.LatestRequestedFrameIndex.has_value()) { - Stream << *VideoStatus.LatestRequestedFrameIndex; - } else { - Stream << "null"; - } - Stream << ",\"latestEncodedFrameIndex\":"; - if (VideoStatus.LatestEncodedFrameIndex.has_value()) { - Stream << *VideoStatus.LatestEncodedFrameIndex; - } else { - Stream << "null"; - } - Stream << ",\"lastKeyframeFrameIndex\":"; - if (VideoStatus.LastKeyframeFrameIndex.has_value()) { - Stream << *VideoStatus.LastKeyframeFrameIndex; - } else { - Stream << "null"; - } - Stream << "},\"dataChannels\":[" - << "{\"label\":\"editor-events\",\"ordered\":true," - "\"maxRetransmits\":null}," - << "{\"label\":\"viewport-input\",\"ordered\":false," - "\"maxRetransmits\":0}]}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("webrtc_status"); + Writer.Key("enabled"); + Writer.Bool(Enabled); + Writer.Key("available"); + Writer.Bool(Available); + Writer.Key("signalingState"); + WriteString(Writer, SignalingState); + Writer.Key("connectionState"); + WriteString(Writer, ConnectionState); + Writer.Key("detail"); + WriteString(Writer, Detail); + Writer.Key("sessionId"); + WriteString(Writer, SessionId); + Writer.Key("pendingLocalIceCandidateCount"); + Writer.Uint64(PendingLocalIceCandidateCount); + + Writer.Key("video"); + Writer.StartObject(); + Writer.Key("codec"); + WriteString(Writer, VideoStatus.Codec); + Writer.Key("senderBound"); + Writer.Bool(VideoStatus.SenderBound); + Writer.Key("waitingForKeyframe"); + Writer.Bool(VideoStatus.WaitingForKeyframe); + Writer.Key("hasOutstandingSendRequest"); + Writer.Bool(VideoStatus.HasOutstandingSendRequest); + Writer.Key("pendingPacketCount"); + Writer.Uint64(VideoStatus.PendingPacketCount); + Writer.Key("droppedPacketCount"); + Writer.Uint64(VideoStatus.DroppedPacketCount); + Writer.Key("droppedStaleRequestCount"); + Writer.Uint64(VideoStatus.DroppedStaleRequestCount); + Writer.Key("droppedStalePacketCount"); + Writer.Uint64(VideoStatus.DroppedStalePacketCount); + Writer.Key("lastFrameIndex"); + WriteOptionalUint64(Writer, VideoStatus.LastFrameIndex); + Writer.Key("latestRequestedFrameIndex"); + WriteOptionalUint64(Writer, VideoStatus.LatestRequestedFrameIndex); + Writer.Key("latestEncodedFrameIndex"); + WriteOptionalUint64(Writer, VideoStatus.LatestEncodedFrameIndex); + Writer.Key("lastKeyframeFrameIndex"); + WriteOptionalUint64(Writer, VideoStatus.LastKeyframeFrameIndex); + Writer.EndObject(); + + Writer.Key("dataChannels"); + Writer.StartArray(); + Writer.StartObject(); + Writer.Key("label"); + Writer.String("editor-events"); + Writer.Key("ordered"); + Writer.Bool(true); + Writer.Key("maxRetransmits"); + Writer.Null(); + Writer.EndObject(); + Writer.StartObject(); + Writer.Key("label"); + Writer.String("viewport-input"); + Writer.Key("ordered"); + Writer.Bool(false); + Writer.Key("maxRetransmits"); + Writer.Uint(0); + Writer.EndObject(); + Writer.EndArray(); + + Writer.EndObject(); + }); } std::string SerializeAssetList(const std::vector &Assets) { - std::ostringstream Stream; - Stream << "{\"type\":\"asset_list\",\"assets\":["; - for (size_t I = 0; I < Assets.size(); ++I) { - const auto &Desc = Assets[I]; - if (I > 0) - Stream << ","; - const std::string_view Kind = - Desc.Kind == Assets::AssetKind::Mesh ? "mesh" : "texture"; - Stream << "{\"id\":" << Desc.Id.Value << ",\"name\":\"" - << EscapeJson(Desc.Name) << "\",\"kind\":\"" << Kind - << "\",\"path\":\"" << EscapeJson(Desc.RelativePath) << "\"}"; - } - Stream << "]}"; - return Stream.str(); + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("asset_list"); + Writer.Key("assets"); + Writer.StartArray(); + for (const auto &Asset : Assets) { + Writer.StartObject(); + Writer.Key("id"); + Writer.Uint64(Asset.Id.Value); + Writer.Key("name"); + WriteString(Writer, Asset.Name); + Writer.Key("kind"); + WriteString(Writer, + Asset.Kind == Assets::AssetKind::Mesh ? "mesh" : "texture"); + Writer.Key("path"); + WriteString(Writer, Asset.RelativePath); + Writer.EndObject(); + } + Writer.EndArray(); + Writer.EndObject(); + }); } std::string SerializeObjectSchema(const EditorObjectDetails &Details) { - std::ostringstream Stream; - - const char *ClassName = "Unknown"; - switch (Details.Kind) { - case EditorSceneItemKind::Folder: ClassName = "Folder"; break; - case EditorSceneItemKind::Mesh: ClassName = "Mesh"; break; - case EditorSceneItemKind::Light: ClassName = "Light"; break; - case EditorSceneItemKind::Camera: ClassName = "Camera"; break; - case EditorSceneItemKind::Actor: ClassName = "Actor"; break; - } - - Stream << "{\"type\":\"object_schema\",\"objectId\":\"" - << EscapeJson(Details.ObjectId) << "\",\"className\":\"" << ClassName - << "\",\"properties\":["; - - bool First = true; - // Appends a property descriptor; Value (if non-empty) is the current value. - auto AppendProp = [&](std::string_view Name, std::string_view Type, bool ReadOnly, - std::string_view Value = {}) { - if (!First) Stream << ","; - First = false; - Stream << "{\"name\":\"" << Name << "\",\"type\":\"" << Type - << "\",\"readOnly\":" << (ReadOnly ? "true" : "false"); - if (!Value.empty()) Stream << ",\"value\":\"" << EscapeJson(Value) << "\""; - Stream << "}"; - }; + return BuildJson([&](JsonWriter &Writer) { + const char *ClassName = "Unknown"; + switch (Details.Kind) { + case EditorSceneItemKind::Folder: + ClassName = "Folder"; + break; + case EditorSceneItemKind::Mesh: + ClassName = "Mesh"; + break; + case EditorSceneItemKind::Light: + ClassName = "Light"; + break; + case EditorSceneItemKind::Camera: + ClassName = "Camera"; + break; + case EditorSceneItemKind::Actor: + ClassName = "Actor"; + break; + } + + Writer.StartObject(); + Writer.Key("type"); + Writer.String("object_schema"); + Writer.Key("objectId"); + WriteString(Writer, Details.ObjectId); + Writer.Key("className"); + Writer.String(ClassName); + Writer.Key("properties"); + Writer.StartArray(); + + auto AppendProperty = [&](std::string_view Name, std::string_view Type, + bool ReadOnly, + std::optional Value = + std::nullopt) { + Writer.StartObject(); + Writer.Key("name"); + WriteString(Writer, Name); + Writer.Key("type"); + WriteString(Writer, Type); + Writer.Key("readOnly"); + Writer.Bool(ReadOnly); + if (Value.has_value() && !Value->empty()) { + Writer.Key("value"); + WriteString(Writer, *Value); + } + Writer.EndObject(); + }; + + AppendProperty("displayName", "string", false, Details.DisplayName); + AppendProperty("visible", "bool", false); + + if (Details.SupportsTransform) { + const bool ReadOnly = Details.TransformReadOnly; + AppendProperty("location", "vec3", ReadOnly); + AppendProperty("rotationDegrees", "vec3", ReadOnly); + AppendProperty("scale", "vec3", ReadOnly); + } - AppendProp("displayName", "string", false, Details.DisplayName); - AppendProp("visible", "bool", false); - - if (Details.SupportsTransform) { - const bool RO = Details.TransformReadOnly; - AppendProp("location", "vec3", RO); - AppendProp("rotationDegrees", "vec3", RO); - AppendProp("scale", "vec3", RO); - } - - if (Details.Kind == EditorSceneItemKind::Actor) { - AppendProp("scriptClass", "string", false, - Details.ScriptClass.value_or("")); - } - - if (Details.Kind == EditorSceneItemKind::Mesh) { - const std::string_view TexPath = - (Details.Material.has_value() && Details.Material->TextureAssetPath.has_value()) - ? *Details.Material->TextureAssetPath - : std::string_view{}; - AppendProp("baseColorTexture", "texture_ref", false, TexPath); - } - - if (Details.SupportsTransform) { - const EditorPhysicsProperties Physics = - Details.Physics.value_or(EditorPhysicsProperties{}); - AppendProp("physicsBodyType", "enum", Details.TransformReadOnly, - PhysicsBodyTypeToString(Physics.BodyType)); - AppendProp("physicsColliderType", "enum", Details.TransformReadOnly, - PhysicsColliderTypeToString(Physics.ColliderType)); - AppendProp("physicsBoxHalfExtents", "vec3", Details.TransformReadOnly); - AppendProp("physicsSphereRadius", "number", Details.TransformReadOnly, - std::to_string(Physics.SphereRadius)); - AppendProp("physicsMass", "number", Details.TransformReadOnly, - std::to_string(Physics.Mass)); - AppendProp("physicsFriction", "number", Details.TransformReadOnly, - std::to_string(Physics.Friction)); - AppendProp("physicsRestitution", "number", Details.TransformReadOnly, - std::to_string(Physics.Restitution)); - } - - Stream << "]}"; - return Stream.str(); + if (Details.Kind == EditorSceneItemKind::Actor) { + AppendProperty("scriptClass", "string", false, + Details.ScriptClass.value_or("")); + } + + if (Details.Kind == EditorSceneItemKind::Mesh) { + const std::string_view TexturePath = + (Details.Material.has_value() && + Details.Material->TextureAssetPath.has_value()) + ? *Details.Material->TextureAssetPath + : std::string_view{}; + AppendProperty("baseColorTexture", "texture_ref", false, TexturePath); + } + + if (Details.SupportsTransform) { + const EditorPhysicsProperties Physics = + Details.Physics.value_or(EditorPhysicsProperties{}); + AppendProperty("physicsBodyType", "enum", Details.TransformReadOnly, + PhysicsBodyTypeToString(Physics.BodyType)); + AppendProperty("physicsColliderType", "enum", Details.TransformReadOnly, + PhysicsColliderTypeToString(Physics.ColliderType)); + AppendProperty("physicsBoxHalfExtents", "vec3", + Details.TransformReadOnly); + const auto SphereRadius = std::to_string(Physics.SphereRadius); + const auto Mass = std::to_string(Physics.Mass); + const auto Friction = std::to_string(Physics.Friction); + const auto Restitution = std::to_string(Physics.Restitution); + AppendProperty("physicsSphereRadius", "number", + Details.TransformReadOnly, SphereRadius); + AppendProperty("physicsMass", "number", Details.TransformReadOnly, Mass); + AppendProperty("physicsFriction", "number", Details.TransformReadOnly, + Friction); + AppendProperty("physicsRestitution", "number", + Details.TransformReadOnly, Restitution); + } + + Writer.EndArray(); + Writer.EndObject(); + }); } std::string SerializeSaveResult(bool Success) { - return Success ? "{\"type\":\"scene_saved\"}" - : "{\"type\":\"scene_save_failed\"}"; + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String(Success ? "scene_saved" : "scene_save_failed"); + Writer.EndObject(); + }); } std::string SerializeError(std::string_view Message) { - return std::string("{\"type\":\"error\",\"message\":\"") + - EscapeJson(Message) + "\"}"; + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("error"); + Writer.Key("message"); + WriteString(Writer, Message); + Writer.EndObject(); + }); +} + +std::string SerializeShutdown() { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("shutdown"); + Writer.EndObject(); + }); } -std::string SerializeShutdown() { return "{\"type\":\"shutdown\"}"; } } // namespace Axiom diff --git a/Headless/HeadlessCommandProtocol.h b/Headless/HeadlessCommandProtocol.h index 91a5b69d..d351ae88 100644 --- a/Headless/HeadlessCommandProtocol.h +++ b/Headless/HeadlessCommandProtocol.h @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -136,7 +136,6 @@ std::optional ParseHeadlessOptions(int argc, char **argv, std::string &Error); std::optional ParseHeadlessCommand(std::string_view JsonLine, std::string &Error); -std::string EscapeJson(std::string_view Value); std::string SerializeEvent(const PublishedEditorEvent &Event); std::string SerializeReady(uint32_t Width, uint32_t Height); std::string SerializeConnected(); diff --git a/Headless/HeadlessOverlayModule.cpp b/Headless/HeadlessOverlayModule.cpp new file mode 100644 index 00000000..98fc241d --- /dev/null +++ b/Headless/HeadlessOverlayModule.cpp @@ -0,0 +1,399 @@ +#include "HeadlessOverlayModule.h" + +#include +#include +#include + +#include +#include + +#include +#include + +namespace Axiom { +namespace { +constexpr float ColliderOverlayScale = 1.01f; +constexpr float ColliderCornerScale = 0.085f; + +MeshData BuildPresenceMarkerMeshData() { + MeshData Mesh{}; + Mesh.Vertices = { + {.Position = {-0.35f, -0.2f, 0.5f}}, + {.Position = {0.35f, -0.2f, 0.5f}}, + {.Position = {0.35f, 0.2f, 0.5f}}, + {.Position = {-0.35f, 0.2f, 0.5f}}, + {.Position = {0.0f, 0.0f, -0.9f}}, + }; + Mesh.Indices = {0, 1, 2, 0, 2, 3, 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4}; + Mesh.BoundsMin = {-0.35f, -0.2f, -0.9f}; + Mesh.BoundsMax = {0.35f, 0.2f, 0.5f}; + return Mesh; +} + +MeshData BuildUnitBoxMeshData() { + MeshData Mesh{}; + Mesh.Vertices = { + {.Position = {-1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f}}, + {.Position = {1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f}}, + {.Position = {1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f}}, + {.Position = {-1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f}}, + {.Position = {-1.0f, -1.0f, -1.0f}, .Normal = {0.0f, 0.0f, -1.0f}}, + {.Position = {1.0f, -1.0f, -1.0f}, .Normal = {0.0f, 0.0f, -1.0f}}, + {.Position = {1.0f, 1.0f, -1.0f}, .Normal = {0.0f, 0.0f, -1.0f}}, + {.Position = {-1.0f, 1.0f, -1.0f}, .Normal = {0.0f, 0.0f, -1.0f}}, + {.Position = {-1.0f, -1.0f, -1.0f}, .Normal = {-1.0f, 0.0f, 0.0f}}, + {.Position = {-1.0f, -1.0f, 1.0f}, .Normal = {-1.0f, 0.0f, 0.0f}}, + {.Position = {-1.0f, 1.0f, 1.0f}, .Normal = {-1.0f, 0.0f, 0.0f}}, + {.Position = {-1.0f, 1.0f, -1.0f}, .Normal = {-1.0f, 0.0f, 0.0f}}, + {.Position = {1.0f, -1.0f, -1.0f}, .Normal = {1.0f, 0.0f, 0.0f}}, + {.Position = {1.0f, -1.0f, 1.0f}, .Normal = {1.0f, 0.0f, 0.0f}}, + {.Position = {1.0f, 1.0f, 1.0f}, .Normal = {1.0f, 0.0f, 0.0f}}, + {.Position = {1.0f, 1.0f, -1.0f}, .Normal = {1.0f, 0.0f, 0.0f}}, + {.Position = {-1.0f, 1.0f, -1.0f}, .Normal = {0.0f, 1.0f, 0.0f}}, + {.Position = {-1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 1.0f, 0.0f}}, + {.Position = {1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 1.0f, 0.0f}}, + {.Position = {1.0f, 1.0f, -1.0f}, .Normal = {0.0f, 1.0f, 0.0f}}, + {.Position = {-1.0f, -1.0f, -1.0f}, .Normal = {0.0f, -1.0f, 0.0f}}, + {.Position = {-1.0f, -1.0f, 1.0f}, .Normal = {0.0f, -1.0f, 0.0f}}, + {.Position = {1.0f, -1.0f, 1.0f}, .Normal = {0.0f, -1.0f, 0.0f}}, + {.Position = {1.0f, -1.0f, -1.0f}, .Normal = {0.0f, -1.0f, 0.0f}}, + }; + Mesh.Indices = { + 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 8, 9, 10, 8, 10, 11, + 12, 14, 13, 12, 15, 14, 16, 17, 18, 16, 18, 19, 20, 22, 21, 20, 23, 22, + }; + Mesh.BoundsMin = {-1.0f, -1.0f, -1.0f}; + Mesh.BoundsMax = {1.0f, 1.0f, 1.0f}; + return Mesh; +} + +MeshData BuildUnitSphereMeshData(uint32_t LongitudeSegments = 16, + uint32_t LatitudeSegments = 10) { + MeshData Mesh{}; + Mesh.Vertices.reserve(static_cast(LongitudeSegments + 1) * + static_cast(LatitudeSegments + 1)); + for (uint32_t Lat = 0; Lat <= LatitudeSegments; ++Lat) { + const float V = static_cast(Lat) / static_cast(LatitudeSegments); + const float Theta = V * std::numbers::pi_v; + const float SinTheta = std::sin(Theta); + const float CosTheta = std::cos(Theta); + for (uint32_t Lon = 0; Lon <= LongitudeSegments; ++Lon) { + const float U = static_cast(Lon) / static_cast(LongitudeSegments); + const float Phi = U * std::numbers::pi_v * 2.0f; + const float SinPhi = std::sin(Phi); + const float CosPhi = std::cos(Phi); + const glm::vec3 Normal{SinTheta * CosPhi, CosTheta, SinTheta * SinPhi}; + Mesh.Vertices.push_back({ + .Position = Normal, + .Normal = glm::normalize(Normal), + .TexCoord = {U, V}, + }); + } + } + const uint32_t Stride = LongitudeSegments + 1; + for (uint32_t Lat = 0; Lat < LatitudeSegments; ++Lat) { + for (uint32_t Lon = 0; Lon < LongitudeSegments; ++Lon) { + const uint32_t A = Lat * Stride + Lon; + const uint32_t B = A + Stride; + const uint32_t C = A + 1; + const uint32_t D = B + 1; + Mesh.Indices.insert(Mesh.Indices.end(), {A, B, C, C, B, D}); + } + } + Mesh.BoundsMin = {-1.0f, -1.0f, -1.0f}; + Mesh.BoundsMax = {1.0f, 1.0f, 1.0f}; + return Mesh; +} + +glm::vec4 ParseHexColor(std::string_view Hex) { + if (Hex.size() != 7 || Hex.front() != '#') { + return {1.0f, 1.0f, 1.0f, 1.0f}; + } + auto ParseChannel = [](char High, char Low) -> uint8_t { + auto ToNibble = [](char Value) -> uint8_t { + if (Value >= '0' && Value <= '9') return static_cast(Value - '0'); + if (Value >= 'a' && Value <= 'f') return static_cast(10 + Value - 'a'); + if (Value >= 'A' && Value <= 'F') return static_cast(10 + Value - 'A'); + return 0; + }; + return static_cast((ToNibble(High) << 4u) | ToNibble(Low)); + }; + return {ParseChannel(Hex[1], Hex[2]) / 255.0f, + ParseChannel(Hex[3], Hex[4]) / 255.0f, + ParseChannel(Hex[5], Hex[6]) / 255.0f, 1.0f}; +} + +glm::mat4 BuildPresenceTransform(const EditorParticipant::CameraState &Camera) { + Axiom::Camera OrientedCamera; + OrientedCamera.SetRotation(Camera.YawDegrees, Camera.PitchDegrees); + glm::mat4 Transform(1.0f); + Transform[0] = glm::vec4(OrientedCamera.GetRight(), 0.0f); + Transform[1] = glm::vec4(OrientedCamera.GetUp(), 0.0f); + Transform[2] = glm::vec4(OrientedCamera.GetForward(), 0.0f); + Transform[3] = glm::vec4(Camera.Position, 1.0f); + return Transform * glm::scale(glm::mat4(1.0f), glm::vec3(0.35f)); +} + +glm::mat4 BuildTransformMatrix(const EditorTransformDetails &Transform) { + glm::mat4 Matrix = glm::translate(glm::mat4(1.0f), Transform.Location); + Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.y), + glm::vec3(0.0f, 1.0f, 0.0f)); + Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.x), + glm::vec3(1.0f, 0.0f, 0.0f)); + Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.z), + glm::vec3(0.0f, 0.0f, 1.0f)); + return glm::scale(Matrix, Transform.Scale); +} + +const EditorTransformDetails *GetEffectiveTransform(const EditorObjectDetails &Details) { + if (Details.WorldTransform.has_value()) return &*Details.WorldTransform; + if (Details.Transform.has_value()) return &*Details.Transform; + return nullptr; +} + +bool HasRenderableCollider(const EditorObjectDetails &Details) { + return Details.Visible && Details.Physics.has_value() && + Details.Physics->BodyType != EditorPhysicsBodyType::None && + Details.Physics->ColliderType != EditorPhysicsColliderType::None && + GetEffectiveTransform(Details) != nullptr; +} +} // namespace + +HeadlessOverlayModule::HeadlessOverlayModule(EditorSession &Session) + : m_Session(Session) {} + +void HeadlessOverlayModule::Initialize() { + Application *App = Application::TryGet(); + Renderer *Renderer = App != nullptr ? App->TryGetRenderer() : nullptr; + if (Renderer == nullptr) { + return; + } + + m_PresenceMarkerMesh = Renderer->CreateMesh(BuildPresenceMarkerMeshData()); + m_ColliderBoxMesh = Renderer->CreateMesh(BuildUnitBoxMeshData()); + m_ColliderSphereMesh = Renderer->CreateMesh(BuildUnitSphereMeshData()); +} + +void HeadlessOverlayModule::SetPresenceMarkerMeshForTesting(MeshRef Mesh) { + m_PresenceMarkerMesh = std::move(Mesh); +} + +void HeadlessOverlayModule::SetColliderMeshesForTesting(MeshRef BoxMesh, + MeshRef SphereMesh) { + m_ColliderBoxMesh = std::move(BoxMesh); + m_ColliderSphereMesh = std::move(SphereMesh); +} + +void HeadlessOverlayModule::SetGizmoHoveredAxis(SessionUserId User, int Axis) { + std::lock_guard Lock(m_GizmoHoverMutex); + m_GizmoHoveredAxisByUser[User.Value] = Axis; +} + +int HeadlessOverlayModule::GetGizmoHoveredAxis(SessionUserId User) const { + std::lock_guard Lock(m_GizmoHoverMutex); + const auto It = m_GizmoHoveredAxisByUser.find(User.Value); + return It != m_GizmoHoveredAxisByUser.end() ? It->second : -1; +} + +void HeadlessOverlayModule::SetGizmoMode(SessionUserId User, GizmoMode Mode) { + std::lock_guard Lock(m_GizmoModeMutex); + m_GizmoModeByUser[User.Value] = Mode; +} + +GizmoMode HeadlessOverlayModule::GetGizmoMode(SessionUserId User) const { + std::lock_guard Lock(m_GizmoModeMutex); + const auto It = m_GizmoModeByUser.find(User.Value); + return It != m_GizmoModeByUser.end() ? It->second : GizmoMode::Translate; +} + +std::vector HeadlessOverlayModule::BuildLightBillboards() const { + std::vector Result; + for (const auto &[Id, Details] : m_Session.GetState().Scene.ObjectDetailsById) { + (void)Id; + if (Details.Kind != EditorSceneItemKind::Light || !Details.Visible || + !Details.Light.has_value()) { + continue; + } + const EditorTransformDetails *EffectiveTransform = + Details.WorldTransform.has_value() ? &*Details.WorldTransform + : Details.Transform.has_value() ? &*Details.Transform + : nullptr; + Result.push_back({ + .ObjectId = Details.ObjectId, + .WorldPosition = EffectiveTransform != nullptr + ? EffectiveTransform->Location + : glm::vec3(0.0f), + .Color = glm::vec4(Details.Light->Color, 1.0f), + .PixelSize = 48.0f, + }); + } + return Result; +} + +std::vector +HeadlessOverlayModule::BuildColliderOverlaySubmissions() const { + std::vector Result; + for (const auto &[Id, Details] : m_Session.GetState().Scene.ObjectDetailsById) { + (void)Id; + if (!HasRenderableCollider(Details)) { + continue; + } + const EditorTransformDetails &Transform = *GetEffectiveTransform(Details); + const EditorPhysicsProperties &Physics = *Details.Physics; + MeshRef ColliderMesh; + glm::mat4 ColliderTransform = BuildTransformMatrix(Transform); + if (Physics.ColliderType == EditorPhysicsColliderType::Box) { + if (m_ColliderBoxMesh == nullptr) continue; + ColliderMesh = m_ColliderBoxMesh; + ColliderTransform *= glm::scale(glm::mat4(1.0f), + Physics.BoxHalfExtents * ColliderOverlayScale); + } else if (Physics.ColliderType == EditorPhysicsColliderType::Sphere) { + if (m_ColliderSphereMesh == nullptr) continue; + ColliderMesh = m_ColliderSphereMesh; + ColliderTransform *= glm::scale( + glm::mat4(1.0f), glm::vec3(Physics.SphereRadius * ColliderOverlayScale)); + } else { + continue; + } + Result.push_back({ + .MeshHandle = GetMeshHandle(ColliderMesh), + .MaterialHandle = GetOrCreateColliderMaterial(Physics.BodyType), + .DebugDataId = RegisterRenderMeshSubmissionDebugData( + {.Name = Details.ObjectId + "-collider"}), + .RenderPath = MeshRenderPath::Graphics, + .Transform = ColliderTransform, + .Translucent = true, + }); + if (m_ColliderBoxMesh == nullptr) continue; + const glm::vec3 HalfExtents = + Physics.ColliderType == EditorPhysicsColliderType::Box + ? Physics.BoxHalfExtents * ColliderOverlayScale + : glm::vec3(Physics.SphereRadius * ColliderOverlayScale); + for (int X = -1; X <= 1; X += 2) { + for (int Y = -1; Y <= 1; Y += 2) { + for (int Z = -1; Z <= 1; Z += 2) { + const glm::vec3 LocalOffset = + glm::vec3(static_cast(X), static_cast(Y), + static_cast(Z)) * HalfExtents; + glm::mat4 CornerTransform = + BuildTransformMatrix(Transform) * + glm::translate(glm::mat4(1.0f), LocalOffset) * + glm::scale(glm::mat4(1.0f), glm::vec3(std::max( + 0.03f, + std::max(HalfExtents.x, + std::max(HalfExtents.y, + HalfExtents.z)) * + ColliderCornerScale))); + Result.push_back({ + .MeshHandle = GetMeshHandle(m_ColliderBoxMesh), + .MaterialHandle = GetOrCreateColliderMaterial(Physics.BodyType), + .DebugDataId = RegisterRenderMeshSubmissionDebugData( + {.Name = Details.ObjectId + "-collider-corner"}), + .RenderPath = MeshRenderPath::Graphics, + .Transform = CornerTransform, + .Translucent = false, + }); + } + } + } + } + return Result; +} + +std::vector +HeadlessOverlayModule::BuildPresenceOverlaySubmissions(SessionUserId RenderUser) const { + std::vector Result; + if (m_PresenceMarkerMesh == nullptr) { + return Result; + } + const std::vector Participants = + m_Session.BuildParticipants(RenderUser); + for (const EditorParticipant &Participant : Participants) { + if (Participant.User.Value == 1 || Participant.User.Value == RenderUser.Value || + Participant.State != EditorUserPresenceState::Connected || + !Participant.Camera.has_value()) { + continue; + } + Result.push_back({ + .MeshHandle = GetMeshHandle(m_PresenceMarkerMesh), + .MaterialHandle = GetOrCreatePresenceMaterial(Participant.User), + .DebugDataId = RegisterRenderMeshSubmissionDebugData( + {.Name = "participant-camera-" + + std::to_string(Participant.User.Value)}), + .RenderPath = MeshRenderPath::Graphics, + .Transform = BuildPresenceTransform(*Participant.Camera), + }); + } + return Result; +} + +const MaterialInstance * +HeadlessOverlayModule::GetPresenceMaterialForTesting(SessionUserId User) const { + const auto It = m_PresenceMaterials.find(User.Value); + return It != m_PresenceMaterials.end() ? &It->second.Material : nullptr; +} + +const MaterialInstance *HeadlessOverlayModule::GetColliderMaterialForTesting( + EditorPhysicsBodyType BodyType) const { + const auto It = m_ColliderMaterials.find(static_cast(BodyType)); + return It != m_ColliderMaterials.end() ? &It->second.Material : nullptr; +} + +MaterialHandle +HeadlessOverlayModule::AllocateMaterialHandle(const MaterialInstance &Material) const { + Application *App = Application::TryGet(); + if (App != nullptr) { + if (Renderer *Renderer = App->TryGetRenderer(); Renderer != nullptr) { + return Renderer->CreateMaterialHandle(Material); + } + } + + return MaterialHandle{m_NextFallbackMaterialHandleValue++}; +} + +MaterialHandle +HeadlessOverlayModule::GetOrCreatePresenceMaterial(SessionUserId User) const { + const auto Existing = m_PresenceMaterials.find(User.Value); + if (Existing != m_PresenceMaterials.end()) { + return Existing->second.Handle; + } + const EditorParticipant Participant = m_Session.BuildParticipant(User); + const glm::vec4 Color = ParseHexColor(Participant.PresentationColor); + auto Texture = std::make_shared(); + Texture->Width = 1; + Texture->Height = 1; + Texture->Pixels = {static_cast(Color.r * 255.0f), + static_cast(Color.g * 255.0f), + static_cast(Color.b * 255.0f), + static_cast(Color.a * 255.0f)}; + MaterialInstance Material{}; + Material.BaseColorTexture = Texture; + const MaterialHandle Handle = AllocateMaterialHandle(Material); + m_PresenceMaterials.emplace( + User.Value, CachedMaterialEntry{.Handle = Handle, .Material = Material}); + return Handle; +} + +MaterialHandle HeadlessOverlayModule::GetOrCreateColliderMaterial( + EditorPhysicsBodyType BodyType) const { + const int Key = static_cast(BodyType); + const auto Existing = m_ColliderMaterials.find(Key); + if (Existing != m_ColliderMaterials.end()) { + return Existing->second.Handle; + } + MaterialInstance Material{}; + if (BodyType == EditorPhysicsBodyType::Dynamic) { + Material.BaseColorFactor = {1.0f, 0.55f, 0.2f, 0.22f}; + } else if (BodyType == EditorPhysicsBodyType::Static) { + Material.BaseColorFactor = {0.2f, 0.9f, 1.0f, 0.18f}; + } else { + Material.BaseColorFactor = {0.8f, 0.8f, 0.8f, 0.18f}; + } + Material.Metallic = 0.0f; + Material.Roughness = 0.15f; + const MaterialHandle Handle = AllocateMaterialHandle(Material); + m_ColliderMaterials.emplace( + Key, CachedMaterialEntry{.Handle = Handle, .Material = Material}); + return Handle; +} +} // namespace Axiom diff --git a/Headless/HeadlessOverlayModule.h b/Headless/HeadlessOverlayModule.h new file mode 100644 index 00000000..014b4f87 --- /dev/null +++ b/Headless/HeadlessOverlayModule.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace Axiom { +class HeadlessOverlayModule { +public: + explicit HeadlessOverlayModule(EditorSession &Session); + + void Initialize(); + void SetPresenceMarkerMeshForTesting(MeshRef Mesh); + void SetColliderMeshesForTesting(MeshRef BoxMesh, MeshRef SphereMesh); + + void SetGizmoHoveredAxis(SessionUserId User, int Axis); + int GetGizmoHoveredAxis(SessionUserId User) const; + void SetGizmoMode(SessionUserId User, GizmoMode Mode); + GizmoMode GetGizmoMode(SessionUserId User) const; + + std::vector BuildLightBillboards() const; + std::vector BuildColliderOverlaySubmissions() const; + std::vector + BuildPresenceOverlaySubmissions(SessionUserId RenderUser) const; + const MaterialInstance * + GetPresenceMaterialForTesting(SessionUserId User) const; + const MaterialInstance * + GetColliderMaterialForTesting(EditorPhysicsBodyType BodyType) const; + +private: + struct CachedMaterialEntry { + MaterialHandle Handle{}; + MaterialInstance Material; + }; + + MaterialHandle AllocateMaterialHandle(const MaterialInstance &Material) const; + MaterialHandle GetOrCreatePresenceMaterial(SessionUserId User) const; + MaterialHandle GetOrCreateColliderMaterial(EditorPhysicsBodyType BodyType) const; + + EditorSession &m_Session; + MeshRef m_PresenceMarkerMesh; + MeshRef m_ColliderBoxMesh; + MeshRef m_ColliderSphereMesh; + mutable std::unordered_map m_PresenceMaterials; + mutable std::unordered_map m_ColliderMaterials; + mutable uint32_t m_NextFallbackMaterialHandleValue{1}; + mutable std::mutex m_GizmoHoverMutex; + std::unordered_map m_GizmoHoveredAxisByUser; + mutable std::mutex m_GizmoModeMutex; + std::unordered_map m_GizmoModeByUser; +}; +} // namespace Axiom diff --git a/Headless/HeadlessRenderView.h b/Headless/HeadlessRenderView.h index 79857f3e..cb39791c 100644 --- a/Headless/HeadlessRenderView.h +++ b/Headless/HeadlessRenderView.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -16,10 +16,15 @@ struct HeadlessRenderViewState { RendererViewMode ViewMode{RendererViewMode::Lit}; bool ShowColliders{true}; bool IsLocal{false}; + bool NeedsRender{true}; + uint32_t ActiveBurstTicksRemaining{0}; }; class HeadlessRenderViewRegistry { public: + static constexpr uint32_t RecentlyActiveBurstTicks = 3u; + static constexpr uint32_t IdleRenderIntervalTicks = 4u; + explicit HeadlessRenderViewRegistry(SessionUserId LocalUser = SessionUserId{1}) { EnsureLocalView(LocalUser); } @@ -27,6 +32,8 @@ class HeadlessRenderViewRegistry { HeadlessRenderViewState &EnsureLocalView(SessionUserId LocalUser) { m_LocalView.User = LocalUser; m_LocalView.IsLocal = true; + m_LocalView.NeedsRender = true; + m_LocalView.ActiveBurstTicksRemaining = RecentlyActiveBurstTicks; if (m_FocusedClientId.has_value() && !m_RemoteViewsByClientId.contains(*m_FocusedClientId)) { m_FocusedClientId.reset(); @@ -45,6 +52,7 @@ class HeadlessRenderViewRegistry { } View.User = User; View.IsLocal = false; + MarkViewActive(View, true); return View; } @@ -62,19 +70,25 @@ class HeadlessRenderViewRegistry { } bool FocusRemoteView(std::string_view ClientId) { - if (!m_RemoteViewsByClientId.contains(std::string(ClientId))) { + auto It = m_RemoteViewsByClientId.find(std::string(ClientId)); + if (It == m_RemoteViewsByClientId.end()) { return false; } m_FocusedClientId = std::string(ClientId); + MarkViewActive(It->second, false); return true; } - void FocusLocalView() { m_FocusedClientId.reset(); } + void FocusLocalView() { + m_FocusedClientId.reset(); + MarkViewActive(m_LocalView, false); + } bool SetViewMode(SessionUserId User, RendererViewMode ViewMode) { if (m_LocalView.User.Value == User.Value) { m_LocalView.ViewMode = ViewMode; + MarkViewActive(m_LocalView, true); return true; } @@ -82,6 +96,7 @@ class HeadlessRenderViewRegistry { (void)ClientId; if (View.User.Value == User.Value) { View.ViewMode = ViewMode; + MarkViewActive(View, true); return true; } } @@ -95,12 +110,14 @@ class HeadlessRenderViewRegistry { } It->second.ViewMode = ViewMode; + MarkViewActive(It->second, true); return true; } bool SetShowColliders(SessionUserId User, bool ShowColliders) { if (m_LocalView.User.Value == User.Value) { m_LocalView.ShowColliders = ShowColliders; + MarkViewActive(m_LocalView, true); return true; } @@ -108,6 +125,7 @@ class HeadlessRenderViewRegistry { (void)ClientId; if (View.User.Value == User.Value) { View.ShowColliders = ShowColliders; + MarkViewActive(View, true); return true; } } @@ -121,9 +139,43 @@ class HeadlessRenderViewRegistry { } It->second.ShowColliders = ShowColliders; + MarkViewActive(It->second, true); return true; } + void MarkAllRemoteViewsDirty() { + for (auto &[ClientId, View] : m_RemoteViewsByClientId) { + (void)ClientId; + MarkViewActive(View, true); + } + } + + bool MarkRemoteViewActive(std::string_view ClientId, bool NeedsRender = true) { + auto It = m_RemoteViewsByClientId.find(std::string(ClientId)); + if (It == m_RemoteViewsByClientId.end()) { + return false; + } + + MarkViewActive(It->second, NeedsRender); + return true; + } + + bool MarkViewActive(SessionUserId User, bool NeedsRender = true) { + if (m_LocalView.User.Value == User.Value) { + MarkViewActive(m_LocalView, NeedsRender); + return true; + } + + for (auto &[ClientId, View] : m_RemoteViewsByClientId) { + (void)ClientId; + if (View.User.Value == User.Value) { + MarkViewActive(View, NeedsRender); + return true; + } + } + return false; + } + const HeadlessRenderViewState *FindRemoteView( std::string_view ClientId) const { const auto It = m_RemoteViewsByClientId.find(std::string(ClientId)); @@ -166,15 +218,54 @@ class HeadlessRenderViewRegistry { return Result; } + void AdvanceRenderSchedulingTick() { + ++m_SchedulingTick; + for (auto &[ClientId, View] : m_RemoteViewsByClientId) { + (void)ClientId; + if (View.ActiveBurstTicksRemaining > 0u) { + --View.ActiveBurstTicksRemaining; + } + } + if (m_LocalView.ActiveBurstTicksRemaining > 0u) { + --m_LocalView.ActiveBurstTicksRemaining; + } + } + + uint64_t GetSchedulingTick() const { return m_SchedulingTick; } + + void MarkViewRendered(SessionUserId User) { + if (m_LocalView.User.Value == User.Value) { + m_LocalView.NeedsRender = false; + return; + } + + for (auto &[ClientId, View] : m_RemoteViewsByClientId) { + (void)ClientId; + if (View.User.Value == User.Value) { + View.NeedsRender = false; + return; + } + } + } + private: + static void MarkViewActive(HeadlessRenderViewState &View, bool NeedsRender) { + View.NeedsRender = View.NeedsRender || NeedsRender; + View.ActiveBurstTicksRemaining = + std::max(View.ActiveBurstTicksRemaining, RecentlyActiveBurstTicks); + } + HeadlessRenderViewState m_LocalView{ .ClientId = "", .User = SessionUserId{1}, .ViewMode = RendererViewMode::Lit, .ShowColliders = true, .IsLocal = true, + .NeedsRender = true, + .ActiveBurstTicksRemaining = RecentlyActiveBurstTicks, }; std::unordered_map m_RemoteViewsByClientId; std::optional m_FocusedClientId; + uint64_t m_SchedulingTick{0}; }; } // namespace Axiom diff --git a/Headless/HeadlessSessionHost.cpp b/Headless/HeadlessSessionHost.cpp index 3101215a..f2dd5bd6 100644 --- a/Headless/HeadlessSessionHost.cpp +++ b/Headless/HeadlessSessionHost.cpp @@ -1,8 +1,10 @@ #include "HeadlessSessionHost.h" -#include +#include +#include +#include -#include +#include namespace Axiom { HeadlessSessionHost::HeadlessSessionHost(const ApplicationArgs &Args, @@ -11,10 +13,14 @@ HeadlessSessionHost::HeadlessSessionHost(const ApplicationArgs &Args, .Width = Width, .Height = Height, .Mode = RuntimeMode::HeadlessEditorSession}, - Args) { - m_Layer = new HeadlessSessionLayer(); - m_Layer->SetSharedRendererAdapter(&m_SharedRendererAdapter); - m_Layer->SetRenderViewResolver( + Args, + {.RegisterDefaultModules = false}) { + GetModuleManager().RegisterModule(std::make_unique()); + + auto SessionModule = std::make_unique(); + m_SessionModule = SessionModule.get(); + m_SessionModule->SetSharedRendererAdapter(&m_SharedRendererAdapter); + m_SessionModule->SetRenderViewResolver( [this]() -> std::optional { if (const HeadlessRenderViewState *View = GetActiveRenderView(); View != nullptr) { @@ -22,72 +28,141 @@ HeadlessSessionHost::HeadlessSessionHost(const ApplicationArgs &Args, } return std::nullopt; }); - PushLayer(m_Layer); - m_Endpoint = std::make_unique(m_Layer->GetSession()); - m_Endpoint->SetVideoEncoder(CreateDefaultVideoEncoder()); - m_RenderViews.EnsureLocalView(m_Layer->GetLocalUserId()); - m_FrameBridge = std::make_unique( - *m_Endpoint, [this]() -> std::optional { + GetModuleManager().RegisterModule(std::move(SessionModule)); + GetModuleManager().RegisterModule(std::make_unique()); + + m_RenderViews.EnsureLocalView(m_SessionModule->GetLocalUserId()); + auto TransportModule = std::make_unique( + m_SessionModule->GetSession(), + [this]() -> std::optional { if (const HeadlessRenderViewState *View = GetActiveRenderView(); View != nullptr) { return *View; } return std::nullopt; }); - SetViewportFrameOutput(m_FrameBridge.get()); - m_ScriptHost.Initialize( - AXIOM_CORAL_MANAGED_DIR, - AXIOM_SCRIPTING_TRUST_RESTRICTED ? ScriptTrustProfile::Restricted - : ScriptTrustProfile::Trusted); - m_ScriptHost.LoadEngineAssembly(AXIOM_MANAGED_DIR); - m_ScriptHost.RegisterInternalCalls(m_Layer->GetSession(), - SessionId{1}, - m_Layer->GetLocalUserId()); - m_Layer->GetSession().Subscribe(&m_ScriptHost); - m_Layer->SetScriptHost(&m_ScriptHost); + m_TransportModule = TransportModule.get(); + GetModuleManager().RegisterModule(std::move(TransportModule)); + +#if AXIOM_WITH_SCRIPTING + auto ScriptingModule = std::make_unique( + "Headless.SessionScriptHost", m_SessionModule->GetSession(), + SessionId{1}, m_SessionModule->GetLocalUserId()); + m_ScriptingModule = ScriptingModule.get(); + GetModuleManager().RegisterModule(std::move(ScriptingModule)); +#endif } bool HeadlessSessionHost::Step() { return Application::Step(); } +std::vector +HeadlessSessionHost::BuildScheduledRenderPassViews( + HeadlessRenderViewRegistry &RenderViews, SessionUserId LocalUserId) { + RenderViews.AdvanceRenderSchedulingTick(); + + std::vector RenderPassViews = + RenderViews.BuildRemoteViewSnapshot(); + std::sort(RenderPassViews.begin(), RenderPassViews.end(), + [](const HeadlessRenderViewState &Left, + const HeadlessRenderViewState &Right) { + return Left.User.Value < Right.User.Value; + }); + + if (RenderPassViews.empty()) { + if (const HeadlessRenderViewState *LocalView = + RenderViews.FindView(LocalUserId); + LocalView != nullptr) { + RenderPassViews.push_back(*LocalView); + } + return RenderPassViews; + } + + std::vector ScheduledViews; + ScheduledViews.reserve(RenderPassViews.size()); + std::vector IdleCandidateIndices; + IdleCandidateIndices.reserve(RenderPassViews.size()); + for (size_t Index = 0; Index < RenderPassViews.size(); ++Index) { + const auto &View = RenderPassViews[Index]; + if (View.NeedsRender || View.ActiveBurstTicksRemaining > 0u) { + ScheduledViews.push_back(View); + continue; + } + IdleCandidateIndices.push_back(Index); + } + + if (!IdleCandidateIndices.empty()) { + const uint64_t IdleInterval = + std::max(1u, HeadlessRenderViewRegistry::IdleRenderIntervalTicks); + const bool ShouldServiceIdleClient = + ScheduledViews.empty() || + ((RenderViews.GetSchedulingTick() % IdleInterval) == 0u); + if (ShouldServiceIdleClient) { + const uint64_t IdleStep = + ScheduledViews.empty() + ? (RenderViews.GetSchedulingTick() - 1u) + : (RenderViews.GetSchedulingTick() / IdleInterval); + const size_t CandidateIndex = + static_cast(IdleStep % IdleCandidateIndices.size()); + ScheduledViews.push_back(RenderPassViews[IdleCandidateIndices[CandidateIndex]]); + } + } + + for (const auto &View : ScheduledViews) { + RenderViews.MarkViewRendered(View.User); + } + + return ScheduledViews; +} + void HeadlessSessionHost::LoadUserScripts( const std::filesystem::path &AssemblyPath) { - m_ScriptHost.LoadUserAssembly(AssemblyPath); - m_ScriptHost.StartFileWatcher(); +#if AXIOM_WITH_SCRIPTING + GetScriptingModule().GetScriptHost().LoadUserAssembly(AssemblyPath); + GetScriptingModule().GetScriptHost().StartFileWatcher(); +#else + (void)AssemblyPath; +#endif } void HeadlessSessionHost::ReloadUserScripts() { - m_ScriptHost.ReloadUserAssembly(); +#if AXIOM_WITH_SCRIPTING + GetScriptingModule().GetScriptHost().ReloadUserAssembly(); +#endif } bool HeadlessSessionHost::LoadStartupSceneIntoSession() { - return m_Layer->LoadStartupSceneIntoSession(); + return m_SessionModule->LoadStartupSceneIntoSession(); } bool HeadlessSessionHost::LoadStartupSceneIntoSession( const std::filesystem::path &ContentDir) { - return m_Layer->LoadStartupSceneIntoSession(ContentDir); + return m_SessionModule->LoadStartupSceneIntoSession(ContentDir); } void HeadlessSessionHost::SubmitLocalCommand(const EditorCommand &Command) { - m_Layer->Submit(Command); + m_RenderViews.MarkAllRemoteViewsDirty(); + m_SessionModule->Submit(Command); } void HeadlessSessionHost::SubmitRemoteCommand(const EditorCommand &Command) { - m_Layer->SubmitToTransport(*m_Endpoint, Command); + m_RenderViews.MarkAllRemoteViewsDirty(); + m_SessionModule->SubmitToTransport(GetTransport(), Command); } void HeadlessSessionHost::SubmitRemoteCommand(SessionUserId User, const EditorCommand &Command) { - m_Layer->SubmitToTransport(*m_Endpoint, User, Command); + m_RenderViews.MarkAllRemoteViewsDirty(); + m_RenderViews.MarkViewActive(User); + m_SessionModule->SubmitToTransport(GetTransport(), User, Command); } void HeadlessSessionHost::SetTransportVideoEncoder( std::unique_ptr Encoder) { - m_Endpoint->SetVideoEncoder(std::move(Encoder)); + m_TransportModule->SetVideoEncoder(std::move(Encoder)); } void HeadlessSessionHost::SetRemoteViewMode(RendererViewMode ViewMode) { - m_RenderViews.SetViewMode(m_Layer->GetLocalUserId(), ViewMode); + m_RenderViews.SetViewMode(m_SessionModule->GetLocalUserId(), ViewMode); } void HeadlessSessionHost::SetRemoteViewMode(SessionUserId User, @@ -96,7 +171,8 @@ void HeadlessSessionHost::SetRemoteViewMode(SessionUserId User, } void HeadlessSessionHost::SetRemoteShowColliders(bool ShowColliders) { - m_RenderViews.SetShowColliders(m_Layer->GetLocalUserId(), ShowColliders); + m_RenderViews.SetShowColliders(m_SessionModule->GetLocalUserId(), + ShowColliders); } void HeadlessSessionHost::SetRemoteShowColliders(SessionUserId User, @@ -142,26 +218,22 @@ size_t HeadlessSessionHost::BeginRenderPasses() { m_ActiveRenderPassViews.clear(); m_CurrentRenderPassIndex = 0; - m_ActiveRenderPassViews = m_RenderViews.BuildRemoteViewSnapshot(); - std::sort(m_ActiveRenderPassViews.begin(), m_ActiveRenderPassViews.end(), - [](const HeadlessRenderViewState &Left, - const HeadlessRenderViewState &Right) { - return Left.User.Value < Right.User.Value; - }); - - if (m_ActiveRenderPassViews.empty()) { - if (const HeadlessRenderViewState *LocalView = m_RenderViews.FindView( - m_Layer->GetLocalUserId()); - LocalView != nullptr) { - m_ActiveRenderPassViews.push_back(*LocalView); - } - } - + m_ActiveRenderPassViews = + BuildScheduledRenderPassViews(m_RenderViews, + m_SessionModule->GetLocalUserId()); + HeadlessRuntimeInstrumentation::RecordHeadlessTick( + GetFrameIndex(), m_ActiveRenderPassViews.size(), + m_RenderViews.GetRemoteViewCount()); return m_ActiveRenderPassViews.size(); } void HeadlessSessionHost::PrepareRenderPass(size_t PassIndex) { m_CurrentRenderPassIndex = PassIndex; + if (PassIndex < m_ActiveRenderPassViews.size()) { + const auto &View = m_ActiveRenderPassViews[PassIndex]; + HeadlessRuntimeInstrumentation::RecordHeadlessRenderPass( + GetFrameIndex(), PassIndex, View.ClientId, View.User, View.IsLocal); + } } bool HeadlessSessionHost::ShouldRenderImGuiForPass(size_t PassIndex, diff --git a/Headless/HeadlessSessionHost.h b/Headless/HeadlessSessionHost.h index b4ec50ec..97923282 100644 --- a/Headless/HeadlessSessionHost.h +++ b/Headless/HeadlessSessionHost.h @@ -3,14 +3,12 @@ #include #include -#include #include -#include #include +#include "HostModules.h" #include "HeadlessRenderView.h" -#include "HeadlessSessionLayer.h" -#include "HeadlessViewportFrameBridge.h" +#include "HeadlessSessionModule.h" #include #include @@ -21,6 +19,9 @@ class HeadlessSessionHost final : public Application { HeadlessSessionHost(const ApplicationArgs &Args, uint32_t Width, uint32_t Height); bool Step(); + static std::vector + BuildScheduledRenderPassViews(HeadlessRenderViewRegistry &RenderViews, + SessionUserId LocalUserId); bool LoadStartupSceneIntoSession(); bool LoadStartupSceneIntoSession(const std::filesystem::path &ContentDir); @@ -42,12 +43,17 @@ class HeadlessSessionHost final : public Application { const HeadlessRenderViewState *FindRenderView(SessionUserId User) const; void LoadUserScripts(const std::filesystem::path &AssemblyPath); void ReloadUserScripts(); - ISessionTransport &GetTransport() { return *m_Endpoint; } - HeadlessSessionLayer &GetHeadlessLayer() { return *m_Layer; } - ScriptHost &GetScriptHost() { return m_ScriptHost; } + ISessionTransport &GetTransport() { return m_TransportModule->GetTransport(); } + HeadlessSessionModule &GetSessionModule() { return *m_SessionModule; } + const HeadlessSessionModule &GetSessionModule() const { + return *m_SessionModule; + } const HeadlessRenderViewRegistry &GetRenderViews() const { return m_RenderViews; } +#if AXIOM_WITH_SCRIPTING + ScriptHost &GetScriptHost() { return m_ScriptingModule->GetScriptHost(); } +#endif private: size_t BeginRenderPasses() override; @@ -55,13 +61,20 @@ class HeadlessSessionHost final : public Application { bool ShouldRenderImGuiForPass(size_t PassIndex, size_t PassCount) const override; - HeadlessSessionLayer *m_Layer{nullptr}; - std::unique_ptr m_Endpoint; - std::unique_ptr m_FrameBridge; +#if AXIOM_WITH_SCRIPTING + SessionScriptHostModule &GetScriptingModule() const { + return *m_ScriptingModule; + } +#endif + + HeadlessSessionModule *m_SessionModule{nullptr}; + HeadlessSessionTransportModule *m_TransportModule{nullptr}; +#if AXIOM_WITH_SCRIPTING + SessionScriptHostModule *m_ScriptingModule{nullptr}; +#endif EditorSceneRendererAdapter m_SharedRendererAdapter; HeadlessRenderViewRegistry m_RenderViews; std::vector m_ActiveRenderPassViews; size_t m_CurrentRenderPassIndex{0}; - ScriptHost m_ScriptHost; }; } // namespace Axiom diff --git a/Headless/HeadlessSessionLayer.cpp b/Headless/HeadlessSessionLayer.cpp deleted file mode 100644 index 3f45e883..00000000 --- a/Headless/HeadlessSessionLayer.cpp +++ /dev/null @@ -1,571 +0,0 @@ -#include "HeadlessSessionLayer.h" - -#include "HeadlessRenderView.h" - -#include -#include - -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include - -namespace Axiom { -namespace { -constexpr float ColliderOverlayScale = 1.01f; -constexpr float ColliderCornerScale = 0.085f; - -MeshData BuildPresenceMarkerMeshData() { - MeshData Mesh{}; - Mesh.Vertices = { - {.Position = {-0.35f, -0.2f, 0.5f, 1.0f}}, - {.Position = {0.35f, -0.2f, 0.5f, 1.0f}}, - {.Position = {0.35f, 0.2f, 0.5f, 1.0f}}, - {.Position = {-0.35f, 0.2f, 0.5f, 1.0f}}, - {.Position = {0.0f, 0.0f, -0.9f, 1.0f}}, - }; - Mesh.Indices = { - 0, 1, 2, 0, 2, 3, - 0, 1, 4, - 1, 2, 4, - 2, 3, 4, - 3, 0, 4, - }; - Mesh.BoundsMin = {-0.35f, -0.2f, -0.9f}; - Mesh.BoundsMax = {0.35f, 0.2f, 0.5f}; - return Mesh; -} - -MeshData BuildUnitBoxMeshData() { - MeshData Mesh{}; - Mesh.Vertices = { - {.Position = {-1.0f, -1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f, 0.0f}}, - {.Position = {1.0f, -1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f, 0.0f}}, - {.Position = {1.0f, 1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f, 0.0f}}, - {.Position = {-1.0f, 1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 0.0f, 1.0f, 0.0f}}, - {.Position = {-1.0f, -1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 0.0f, -1.0f, 0.0f}}, - {.Position = {1.0f, -1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 0.0f, -1.0f, 0.0f}}, - {.Position = {1.0f, 1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 0.0f, -1.0f, 0.0f}}, - {.Position = {-1.0f, 1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 0.0f, -1.0f, 0.0f}}, - {.Position = {-1.0f, -1.0f, -1.0f, 1.0f}, .Normal = {-1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, -1.0f, 1.0f, 1.0f}, .Normal = {-1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, 1.0f, 1.0f, 1.0f}, .Normal = {-1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, 1.0f, -1.0f, 1.0f}, .Normal = {-1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, -1.0f, -1.0f, 1.0f}, .Normal = {1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, -1.0f, 1.0f, 1.0f}, .Normal = {1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, 1.0f, 1.0f, 1.0f}, .Normal = {1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, 1.0f, -1.0f, 1.0f}, .Normal = {1.0f, 0.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, 1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 1.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, 1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 1.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, 1.0f, 1.0f, 1.0f}, .Normal = {0.0f, 1.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, 1.0f, -1.0f, 1.0f}, .Normal = {0.0f, 1.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, -1.0f, -1.0f, 1.0f}, .Normal = {0.0f, -1.0f, 0.0f, 0.0f}}, - {.Position = {-1.0f, -1.0f, 1.0f, 1.0f}, .Normal = {0.0f, -1.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, -1.0f, 1.0f, 1.0f}, .Normal = {0.0f, -1.0f, 0.0f, 0.0f}}, - {.Position = {1.0f, -1.0f, -1.0f, 1.0f}, .Normal = {0.0f, -1.0f, 0.0f, 0.0f}}, - }; - Mesh.Indices = { - 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 8, 9, 10, 8, 10, 11, - 12, 14, 13, 12, 15, 14, 16, 17, 18, 16, 18, 19, 20, 22, 21, 20, 23, 22, - }; - Mesh.BoundsMin = {-1.0f, -1.0f, -1.0f}; - Mesh.BoundsMax = {1.0f, 1.0f, 1.0f}; - return Mesh; -} - -MeshData BuildUnitSphereMeshData(uint32_t LongitudeSegments = 16, - uint32_t LatitudeSegments = 10) { - MeshData Mesh{}; - Mesh.Vertices.reserve(static_cast(LongitudeSegments + 1) * - static_cast(LatitudeSegments + 1)); - for (uint32_t Lat = 0; Lat <= LatitudeSegments; ++Lat) { - const float V = static_cast(Lat) / - static_cast(LatitudeSegments); - const float Theta = V * std::numbers::pi_v; - const float SinTheta = std::sin(Theta); - const float CosTheta = std::cos(Theta); - for (uint32_t Lon = 0; Lon <= LongitudeSegments; ++Lon) { - const float U = static_cast(Lon) / - static_cast(LongitudeSegments); - const float Phi = U * std::numbers::pi_v * 2.0f; - const float SinPhi = std::sin(Phi); - const float CosPhi = std::cos(Phi); - const glm::vec3 Normal{SinTheta * CosPhi, CosTheta, SinTheta * SinPhi}; - Mesh.Vertices.push_back({ - .Position = glm::vec4(Normal, 1.0f), - .Normal = glm::vec4(glm::normalize(Normal), 0.0f), - .TexCoord = {U, V}, - }); - } - } - - Mesh.Indices.reserve(static_cast(LongitudeSegments) * - static_cast(LatitudeSegments) * 6u); - const uint32_t Stride = LongitudeSegments + 1; - for (uint32_t Lat = 0; Lat < LatitudeSegments; ++Lat) { - for (uint32_t Lon = 0; Lon < LongitudeSegments; ++Lon) { - const uint32_t A = Lat * Stride + Lon; - const uint32_t B = A + Stride; - const uint32_t C = A + 1; - const uint32_t D = B + 1; - Mesh.Indices.insert(Mesh.Indices.end(), {A, B, C, C, B, D}); - } - } - Mesh.BoundsMin = {-1.0f, -1.0f, -1.0f}; - Mesh.BoundsMax = {1.0f, 1.0f, 1.0f}; - return Mesh; -} - -MeshData BuildUnitCornerMarkerMeshData() { - MeshData Mesh{}; - Mesh.Vertices = { - {.Position = {-1.0f, -1.0f, 1.0f, 1.0f}}, {.Position = {1.0f, -1.0f, 1.0f, 1.0f}}, - {.Position = {1.0f, 1.0f, 1.0f, 1.0f}}, {.Position = {-1.0f, 1.0f, 1.0f, 1.0f}}, - {.Position = {-1.0f, -1.0f, -1.0f, 1.0f}}, {.Position = {1.0f, -1.0f, -1.0f, 1.0f}}, - {.Position = {1.0f, 1.0f, -1.0f, 1.0f}}, {.Position = {-1.0f, 1.0f, -1.0f, 1.0f}}, - }; - Mesh.Indices = { - 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, - 4, 0, 3, 4, 3, 7, 1, 5, 6, 1, 6, 2, - 3, 2, 6, 3, 6, 7, 4, 5, 1, 4, 1, 0, - }; - Mesh.BoundsMin = {-1.0f, -1.0f, -1.0f}; - Mesh.BoundsMax = {1.0f, 1.0f, 1.0f}; - return Mesh; -} - -glm::vec4 ParseHexColor(std::string_view Hex) { - if (Hex.size() != 7 || Hex.front() != '#') { - return {1.0f, 1.0f, 1.0f, 1.0f}; - } - - auto ParseChannel = [](char High, char Low) -> uint8_t { - auto ToNibble = [](char Value) -> uint8_t { - if (Value >= '0' && Value <= '9') { - return static_cast(Value - '0'); - } - if (Value >= 'a' && Value <= 'f') { - return static_cast(10 + Value - 'a'); - } - if (Value >= 'A' && Value <= 'F') { - return static_cast(10 + Value - 'A'); - } - return 0; - }; - return static_cast((ToNibble(High) << 4u) | ToNibble(Low)); - }; - - return { - ParseChannel(Hex[1], Hex[2]) / 255.0f, - ParseChannel(Hex[3], Hex[4]) / 255.0f, - ParseChannel(Hex[5], Hex[6]) / 255.0f, - 1.0f, - }; -} - -glm::mat4 BuildPresenceTransform(const EditorParticipant::CameraState &Camera) { - Axiom::Camera OrientedCamera; - OrientedCamera.SetRotation(Camera.YawDegrees, Camera.PitchDegrees); - - glm::mat4 Transform(1.0f); - Transform[0] = glm::vec4(OrientedCamera.GetRight(), 0.0f); - Transform[1] = glm::vec4(OrientedCamera.GetUp(), 0.0f); - Transform[2] = glm::vec4(OrientedCamera.GetForward(), 0.0f); - Transform[3] = glm::vec4(Camera.Position, 1.0f); - - return Transform * glm::scale(glm::mat4(1.0f), glm::vec3(0.35f)); -} - -glm::mat4 BuildTransformMatrix(const EditorTransformDetails &Transform) { - glm::mat4 Matrix = glm::translate(glm::mat4(1.0f), Transform.Location); - Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.y), - glm::vec3(0.0f, 1.0f, 0.0f)); - Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.x), - glm::vec3(1.0f, 0.0f, 0.0f)); - Matrix = glm::rotate(Matrix, glm::radians(Transform.RotationDegrees.z), - glm::vec3(0.0f, 0.0f, 1.0f)); - return glm::scale(Matrix, Transform.Scale); -} - -const EditorTransformDetails *GetEffectiveTransform(const EditorObjectDetails &Details) { - if (Details.WorldTransform.has_value()) { - return &*Details.WorldTransform; - } - if (Details.Transform.has_value()) { - return &*Details.Transform; - } - return nullptr; -} - -bool HasRenderableCollider(const EditorObjectDetails &Details) { - return Details.Visible && Details.Physics.has_value() && - Details.Physics->BodyType != EditorPhysicsBodyType::None && - Details.Physics->ColliderType != EditorPhysicsColliderType::None && - GetEffectiveTransform(Details) != nullptr; -} -} // namespace - -HeadlessSessionLayer::HeadlessSessionLayer() - : Layer("HeadlessSessionLayer"), m_Session(m_SessionId) {} - -void HeadlessSessionLayer::OnAttach() { - m_Session.EnsureViewportState(m_LocalUserId); - m_PresenceMarkerMesh = Renderer::Get().CreateMesh(BuildPresenceMarkerMeshData()); - m_ColliderBoxMesh = Renderer::Get().CreateMesh(BuildUnitBoxMeshData()); - m_ColliderSphereMesh = Renderer::Get().CreateMesh(BuildUnitSphereMeshData()); -} - -void HeadlessSessionLayer::OnUpdate() { - m_Session.Tick(Application::Get().GetDeltaTime()); - if (m_ScriptHost != nullptr) { - m_ScriptHost->Tick(Application::Get().GetDeltaTime()); - } -} - -void HeadlessSessionLayer::OnRender() { - HeadlessRenderViewState RenderView{ - .ClientId = "", - .User = m_LocalUserId, - .ViewMode = RendererViewMode::Lit, - .IsLocal = true, - }; - if (m_RenderViewResolver) { - if (const auto ResolvedView = m_RenderViewResolver(); - ResolvedView.has_value()) { - RenderView = *ResolvedView; - } - } - - SessionUserId RenderUser = RenderView.User; - const EditorViewportState *Viewport = m_Session.FindViewport(RenderUser); - if (Viewport == nullptr && RenderUser.Value != m_LocalUserId.Value) { - RenderUser = m_LocalUserId; - Viewport = m_Session.FindViewport(RenderUser); - } - if (Viewport == nullptr) { - return; - } - - if (m_RendererAdapter == nullptr) { - return; - } - - Application::Get().SetRendererViewMode(RenderView.ViewMode); - Application::Get().SetViewportFrameUser(RenderUser); - RenderCommand::SetCamera(Viewport->Camera); - - // Pick the first visible Light that has LightProperties configured. - for (const auto &[Id, Details] : m_Session.GetState().Scene.ObjectDetailsById) { - if (Details.Kind == EditorSceneItemKind::Light && Details.Visible && - Details.Light.has_value()) { - // Derive direction from the light's world-space position so that moving - // the object in the editor has an immediate effect on the sun direction. - glm::vec3 Dir = Details.Light->Direction; - const EditorTransformDetails *EffTransform = - Details.WorldTransform.has_value() ? &*Details.WorldTransform - : Details.Transform.has_value() ? &*Details.Transform - : nullptr; - if (EffTransform != nullptr && - glm::length(EffTransform->Location) > 0.001f) { - Dir = EffTransform->Location; - } - RenderCommand::SetSun({ - .Color = Details.Light->Color, - .Intensity = Details.Light->Intensity, - .Direction = Dir, - }); - break; - } - } - - RenderCommand::SetSkyboxColors(m_Session.GetState().Scene.WorldSettings.SkyboxColorTop, - m_Session.GetState().Scene.WorldSettings.SkyboxColorBottom); - RenderCommand::SetSkyboxHDR(m_Session.GetState().Scene.WorldSettings.SkyboxHDRData); - - for (const auto &Submission : m_RendererAdapter->BuildRenderSubmissions(m_Session)) { - RenderCommand::Submit(Submission); - } - for (const auto &Billboard : BuildLightBillboards()) { - RenderCommand::SubmitLightBillboard(Billboard); - } - if (RenderView.ShowColliders) { - for (const auto &Submission : BuildColliderOverlaySubmissions()) { - RenderCommand::Submit(Submission); - } - } - for (const auto &Submission : BuildPresenceOverlaySubmissions(RenderUser)) { - RenderCommand::Submit(Submission); - } - - const EditorObjectDetails *Selected = - m_Session.FindSelectedObjectDetails(RenderUser); - const auto *EffTransform = Selected != nullptr - ? (Selected->WorldTransform.has_value() ? &*Selected->WorldTransform - : (Selected->Transform.has_value() - ? &*Selected->Transform - : nullptr)) - : nullptr; - if (EffTransform != nullptr && Selected->SupportsTransform) { - RenderCommand::SetGizmoOverlay({ - .WorldPosition = EffTransform->Location, - .Scale = 0.5f, - .HoveredAxis = GetGizmoHoveredAxis(RenderUser), - .Mode = GetGizmoMode(RenderUser), - }); - } -} - -void HeadlessSessionLayer::SetGizmoHoveredAxis(SessionUserId User, int Axis) { - std::lock_guard Lock(m_GizmoHoverMutex); - m_GizmoHoveredAxisByUser[User.Value] = Axis; -} - -int HeadlessSessionLayer::GetGizmoHoveredAxis(SessionUserId User) const { - std::lock_guard Lock(m_GizmoHoverMutex); - const auto It = m_GizmoHoveredAxisByUser.find(User.Value); - return It != m_GizmoHoveredAxisByUser.end() ? It->second : -1; -} - -void HeadlessSessionLayer::SetGizmoMode(SessionUserId User, GizmoMode Mode) { - std::lock_guard Lock(m_GizmoModeMutex); - m_GizmoModeByUser[User.Value] = Mode; -} - -GizmoMode HeadlessSessionLayer::GetGizmoMode(SessionUserId User) const { - std::lock_guard Lock(m_GizmoModeMutex); - const auto It = m_GizmoModeByUser.find(User.Value); - return It != m_GizmoModeByUser.end() ? It->second : GizmoMode::Translate; -} - -std::vector HeadlessSessionLayer::BuildLightBillboards() - const { - std::vector Result; - for (const auto &[Id, Details] : m_Session.GetState().Scene.ObjectDetailsById) { - (void)Id; - if (Details.Kind != EditorSceneItemKind::Light || !Details.Visible || - !Details.Light.has_value()) { - continue; - } - - const EditorTransformDetails *EffectiveTransform = - Details.WorldTransform.has_value() ? &*Details.WorldTransform - : Details.Transform.has_value() ? &*Details.Transform - : nullptr; - Result.push_back({ - .ObjectId = Details.ObjectId, - .WorldPosition = EffectiveTransform != nullptr - ? EffectiveTransform->Location - : glm::vec3(0.0f), - .Color = glm::vec4(Details.Light->Color, 1.0f), - .PixelSize = 48.0f, - }); - } - return Result; -} - -std::vector -HeadlessSessionLayer::BuildColliderOverlaySubmissions() const { - std::vector Result; - for (const auto &[Id, Details] : m_Session.GetState().Scene.ObjectDetailsById) { - (void)Id; - if (!HasRenderableCollider(Details)) { - continue; - } - - const EditorTransformDetails &Transform = *GetEffectiveTransform(Details); - const EditorPhysicsProperties &Physics = *Details.Physics; - MeshRef ColliderMesh; - glm::mat4 ColliderTransform = BuildTransformMatrix(Transform); - if (Physics.ColliderType == EditorPhysicsColliderType::Box) { - if (m_ColliderBoxMesh == nullptr) { - continue; - } - ColliderMesh = m_ColliderBoxMesh; - ColliderTransform *= glm::scale(glm::mat4(1.0f), - Physics.BoxHalfExtents * ColliderOverlayScale); - } else if (Physics.ColliderType == EditorPhysicsColliderType::Sphere) { - if (m_ColliderSphereMesh == nullptr) { - continue; - } - ColliderMesh = m_ColliderSphereMesh; - ColliderTransform *= glm::scale( - glm::mat4(1.0f), - glm::vec3(Physics.SphereRadius * ColliderOverlayScale)); - } else { - continue; - } - - Result.push_back({ - .Mesh = ColliderMesh, - .Material = GetOrCreateColliderMaterial(Physics.BodyType), - .Name = Details.ObjectId + "-collider", - .RenderPath = MeshRenderPath::Graphics, - .Transform = ColliderTransform, - .Translucent = true, - }); - - if (m_ColliderBoxMesh == nullptr) { - continue; - } - - const glm::vec3 HalfExtents = - Physics.ColliderType == EditorPhysicsColliderType::Box - ? Physics.BoxHalfExtents * ColliderOverlayScale - : glm::vec3(Physics.SphereRadius * ColliderOverlayScale); - for (int X = -1; X <= 1; X += 2) { - for (int Y = -1; Y <= 1; Y += 2) { - for (int Z = -1; Z <= 1; Z += 2) { - const glm::vec3 LocalOffset = - glm::vec3(static_cast(X), static_cast(Y), - static_cast(Z)) * - HalfExtents; - glm::mat4 CornerTransform = - BuildTransformMatrix(Transform) * - glm::translate(glm::mat4(1.0f), LocalOffset) * - glm::scale(glm::mat4(1.0f), glm::vec3(std::max( - 0.03f, - std::max(HalfExtents.x, - std::max(HalfExtents.y, - HalfExtents.z)) * - ColliderCornerScale))); - Result.push_back({ - .Mesh = m_ColliderBoxMesh, - .Material = GetOrCreateColliderMaterial(Physics.BodyType), - .Name = Details.ObjectId + "-collider-corner", - .RenderPath = MeshRenderPath::Graphics, - .Transform = CornerTransform, - .Translucent = false, - }); - } - } - } - } - return Result; -} - -bool HeadlessSessionLayer::LoadStartupSceneIntoSession() { - return LoadStartupSceneIntoSession(std::filesystem::path(AXIOM_CONTENT_DIR)); -} - -bool HeadlessSessionLayer::LoadStartupSceneIntoSession( - const std::filesystem::path &ContentDir) { -#ifndef AXIOM_CONTENT_DIR -#define AXIOM_CONTENT_DIR "Content" -#endif - m_Session.SetContentDir(ContentDir); - m_Session.SetEngineContentDir(std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine"); - return LoadStartupScene(m_Session); -} - -void HeadlessSessionLayer::Submit(const EditorCommand &Command) { - m_Session.Submit(MakeContext(), Command); -} - -void HeadlessSessionLayer::SubmitToTransport(ISessionTransport &Transport, - const EditorCommand &Command) { - Transport.Submit(MakeContext(), Command); -} - -void HeadlessSessionLayer::SubmitToTransport(ISessionTransport &Transport, - SessionUserId User, - const EditorCommand &Command) { - Transport.Submit(MakeContext(User), Command); -} - -std::vector -HeadlessSessionLayer::BuildPresenceOverlaySubmissions( - SessionUserId RenderUser) const { - std::vector Result; - if (m_PresenceMarkerMesh == nullptr) { - return Result; - } - - const std::vector Participants = - m_Session.BuildParticipants(RenderUser); - for (const EditorParticipant &Participant : Participants) { - if (Participant.User.Value == 1 || - Participant.User.Value == RenderUser.Value || - Participant.State != EditorUserPresenceState::Connected || - !Participant.Camera.has_value()) { - continue; - } - - Result.push_back({ - .Mesh = m_PresenceMarkerMesh, - .Material = GetOrCreatePresenceMaterial(Participant.User), - .Name = "participant-camera-" + std::to_string(Participant.User.Value), - .RenderPath = MeshRenderPath::Graphics, - .Transform = BuildPresenceTransform(*Participant.Camera), - }); - } - return Result; -} - -MaterialInstanceRef -HeadlessSessionLayer::GetOrCreatePresenceMaterial(SessionUserId User) const { - const auto Existing = m_PresenceMaterials.find(User.Value); - if (Existing != m_PresenceMaterials.end()) { - return Existing->second; - } - - const EditorParticipant Participant = m_Session.BuildParticipant(User); - const glm::vec4 Color = ParseHexColor(Participant.PresentationColor); - auto Texture = std::make_shared(); - Texture->Width = 1; - Texture->Height = 1; - Texture->Pixels = { - static_cast(Color.r * 255.0f), - static_cast(Color.g * 255.0f), - static_cast(Color.b * 255.0f), - static_cast(Color.a * 255.0f), - }; - - auto Material = std::make_shared(); - Material->BaseColorTexture = Texture; - m_PresenceMaterials.emplace(User.Value, Material); - return Material; -} - -MaterialInstanceRef HeadlessSessionLayer::GetOrCreateColliderMaterial( - EditorPhysicsBodyType BodyType) const { - const int Key = static_cast(BodyType); - const auto Existing = m_ColliderMaterials.find(Key); - if (Existing != m_ColliderMaterials.end()) { - return Existing->second; - } - - auto Material = std::make_shared(); - if (BodyType == EditorPhysicsBodyType::Dynamic) { - Material->BaseColorFactor = {1.0f, 0.55f, 0.2f, 0.22f}; - } else if (BodyType == EditorPhysicsBodyType::Static) { - Material->BaseColorFactor = {0.2f, 0.9f, 1.0f, 0.18f}; - } else { - Material->BaseColorFactor = {0.8f, 0.8f, 0.8f, 0.18f}; - } - Material->Metallic = 0.0f; - Material->Roughness = 0.15f; - m_ColliderMaterials.emplace(Key, Material); - return Material; -} - -CommandContext HeadlessSessionLayer::MakeContext() const { - return MakeContext(m_LocalUserId); -} - -CommandContext HeadlessSessionLayer::MakeContext(SessionUserId User) const { - return { - .Session = m_SessionId, - .User = User, - .FrameIndex = Application::Get().GetFrameIndex(), - .DeltaTimeSeconds = Application::Get().GetDeltaTime(), - }; -} -} // namespace Axiom diff --git a/Headless/HeadlessSessionModule.cpp b/Headless/HeadlessSessionModule.cpp new file mode 100644 index 00000000..b0e063de --- /dev/null +++ b/Headless/HeadlessSessionModule.cpp @@ -0,0 +1,190 @@ +#include "HeadlessSessionModule.h" + +#include "HeadlessRenderView.h" + +#include + +#include +#include +#if AXIOM_WITH_PHYSICS +#include +#endif +#include + +#include + +namespace Axiom { +namespace { +} // namespace + +HeadlessSessionModule::HeadlessSessionModule() + : m_Session(m_SessionId), + m_OverlayModule(m_Session) { +#if AXIOM_WITH_PHYSICS + AttachEditorPhysicsController(m_Session); +#endif +} + +std::string_view HeadlessSessionModule::GetName() const { + return "Headless.Session"; +} + +bool HeadlessSessionModule::Initialize(Application &App) { + (void)App; + m_Session.EnsureViewportState(m_LocalUserId); + m_OverlayModule.Initialize(); + return true; +} + +void HeadlessSessionModule::Update(const ModuleUpdateContext &Context) { + if (Context.Phase == ModuleUpdatePhase::FrameStart) { + m_Session.Tick(Context.DeltaTimeSeconds); + return; + } + + if (Context.Phase != ModuleUpdatePhase::Render) { + return; + } + + Application &App = Context.App; + HeadlessRenderViewState RenderView{ + .ClientId = "", + .User = m_LocalUserId, + .ViewMode = RendererViewMode::Lit, + .IsLocal = true, + }; + if (m_RenderViewResolver) { + if (const auto ResolvedView = m_RenderViewResolver(); + ResolvedView.has_value()) { + RenderView = *ResolvedView; + } + } + + SessionUserId RenderUser = RenderView.User; + const EditorViewportState *Viewport = m_Session.FindViewport(RenderUser); + if (Viewport == nullptr && RenderUser.Value != m_LocalUserId.Value) { + RenderUser = m_LocalUserId; + Viewport = m_Session.FindViewport(RenderUser); + } + if (Viewport == nullptr || m_RendererAdapter == nullptr) { + return; + } + + App.SetRendererViewMode(RenderView.ViewMode); + App.SetViewportFrameUser(RenderUser); + RenderCommand::SetCamera(Viewport->Camera); + + // Pick the first visible Light that has LightProperties configured. + for (const auto &[Id, Details] : m_Session.GetState().Scene.ObjectDetailsById) { + if (Details.Kind == EditorSceneItemKind::Light && Details.Visible && + Details.Light.has_value()) { + // Derive direction from the light's world-space position so that moving + // the object in the editor has an immediate effect on the sun direction. + glm::vec3 Dir = Details.Light->Direction; + const EditorTransformDetails *EffTransform = + Details.WorldTransform.has_value() ? &*Details.WorldTransform + : Details.Transform.has_value() ? &*Details.Transform + : nullptr; + if (EffTransform != nullptr && + glm::length(EffTransform->Location) > 0.001f) { + Dir = EffTransform->Location; + } + RenderCommand::SetSun({ + .Color = Details.Light->Color, + .Intensity = Details.Light->Intensity, + .Direction = Dir, + }); + break; + } + } + + RenderCommand::SetSkyboxColors( + m_Session.GetState().Scene.WorldSettings.SkyboxColorTop, + m_Session.GetState().Scene.WorldSettings.SkyboxColorBottom); + RenderCommand::SetSkyboxHDR( + m_Session.GetState().Scene.WorldSettings.SkyboxHDRData); + + for (const auto &Submission : + m_RendererAdapter->BuildRenderSubmissions(m_Session)) { + RenderCommand::Submit(Submission); + } + for (const auto &Billboard : m_OverlayModule.BuildLightBillboards()) { + RenderCommand::SubmitLightBillboard(Billboard); + } + if (RenderView.ShowColliders) { + for (const auto &Submission : + m_OverlayModule.BuildColliderOverlaySubmissions()) { + RenderCommand::Submit(Submission); + } + } + for (const auto &Submission : + m_OverlayModule.BuildPresenceOverlaySubmissions(RenderUser)) { + RenderCommand::Submit(Submission); + } + + const EditorObjectDetails *Selected = + m_Session.FindSelectedObjectDetails(RenderUser); + const auto *EffTransform = + Selected != nullptr + ? (Selected->WorldTransform.has_value() + ? &*Selected->WorldTransform + : (Selected->Transform.has_value() ? &*Selected->Transform + : nullptr)) + : nullptr; + if (EffTransform != nullptr && Selected->SupportsTransform) { + RenderCommand::SetGizmoOverlay({ + .WorldPosition = EffTransform->Location, + .Scale = 0.5f, + .HoveredAxis = m_OverlayModule.GetGizmoHoveredAxis(RenderUser), + .Mode = m_OverlayModule.GetGizmoMode(RenderUser), + }); + } +} + +void HeadlessSessionModule::Shutdown(Application &App) { + (void)App; +} + +bool HeadlessSessionModule::LoadStartupSceneIntoSession() { + return LoadStartupSceneIntoSession(std::filesystem::path(AXIOM_CONTENT_DIR)); +} + +bool HeadlessSessionModule::LoadStartupSceneIntoSession( + const std::filesystem::path &ContentDir) { +#ifndef AXIOM_CONTENT_DIR +#define AXIOM_CONTENT_DIR "Content" +#endif + m_Session.SetContentDir(ContentDir); + m_Session.SetEngineContentDir(std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine"); + return LoadStartupScene(m_Session); +} + +void HeadlessSessionModule::Submit(const EditorCommand &Command) { + m_Session.Submit(MakeContext(), Command); +} + +void HeadlessSessionModule::SubmitToTransport(ISessionTransport &Transport, + const EditorCommand &Command) { + Transport.Submit(MakeContext(), Command); +} + +void HeadlessSessionModule::SubmitToTransport(ISessionTransport &Transport, + SessionUserId User, + const EditorCommand &Command) { + Transport.Submit(MakeContext(User), Command); +} + + +CommandContext HeadlessSessionModule::MakeContext() const { + return MakeContext(m_LocalUserId); +} + +CommandContext HeadlessSessionModule::MakeContext(SessionUserId User) const { + return { + .Session = m_SessionId, + .User = User, + .FrameIndex = Application::Get().GetFrameIndex(), + .DeltaTimeSeconds = Application::Get().GetDeltaTime(), + }; +} +} // namespace Axiom diff --git a/Headless/HeadlessSessionLayer.h b/Headless/HeadlessSessionModule.h similarity index 51% rename from Headless/HeadlessSessionLayer.h rename to Headless/HeadlessSessionModule.h index 3772edc7..e8c20786 100644 --- a/Headless/HeadlessSessionLayer.h +++ b/Headless/HeadlessSessionModule.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -9,25 +9,26 @@ #include #include +#include "HeadlessOverlayModule.h" + +#include #include -#include #include -#include namespace Axiom { struct HeadlessRenderViewState; -class ScriptHost; -class HeadlessSessionLayer final : public Layer { +class HeadlessSessionModule final : public IModule { public: using RenderViewResolver = std::function()>; - HeadlessSessionLayer(); + HeadlessSessionModule(); - void OnAttach() override; - void OnUpdate() override; - void OnRender() override; + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; bool LoadStartupSceneIntoSession(); bool LoadStartupSceneIntoSession(const std::filesystem::path &ContentDir); @@ -42,45 +43,56 @@ class HeadlessSessionLayer final : public Layer { void SetRenderViewResolver(RenderViewResolver Resolver) { m_RenderViewResolver = std::move(Resolver); } - void SetPresenceMarkerMeshForTesting(MeshRef Mesh) { m_PresenceMarkerMesh = std::move(Mesh); } + void SetPresenceMarkerMeshForTesting(MeshRef Mesh) { + m_OverlayModule.SetPresenceMarkerMeshForTesting(std::move(Mesh)); + } void SetColliderMeshesForTesting(MeshRef BoxMesh, MeshRef SphereMesh) { - m_ColliderBoxMesh = std::move(BoxMesh); - m_ColliderSphereMesh = std::move(SphereMesh); + m_OverlayModule.SetColliderMeshesForTesting(std::move(BoxMesh), + std::move(SphereMesh)); } - void SetScriptHost(ScriptHost *Host) { m_ScriptHost = Host; } EditorSession &GetSession() { return m_Session; } + const EditorSession &GetSession() const { return m_Session; } SessionUserId GetLocalUserId() const { return m_LocalUserId; } - void SetGizmoHoveredAxis(SessionUserId User, int Axis); - int GetGizmoHoveredAxis(SessionUserId User) const; - void SetGizmoMode(SessionUserId User, GizmoMode Mode); - GizmoMode GetGizmoMode(SessionUserId User) const; - std::vector BuildLightBillboards() const; - std::vector BuildColliderOverlaySubmissions() const; + void SetGizmoHoveredAxis(SessionUserId User, int Axis) { + m_OverlayModule.SetGizmoHoveredAxis(User, Axis); + } + int GetGizmoHoveredAxis(SessionUserId User) const { + return m_OverlayModule.GetGizmoHoveredAxis(User); + } + void SetGizmoMode(SessionUserId User, GizmoMode Mode) { + m_OverlayModule.SetGizmoMode(User, Mode); + } + GizmoMode GetGizmoMode(SessionUserId User) const { + return m_OverlayModule.GetGizmoMode(User); + } + std::vector BuildLightBillboards() const { + return m_OverlayModule.BuildLightBillboards(); + } + std::vector BuildColliderOverlaySubmissions() const { + return m_OverlayModule.BuildColliderOverlaySubmissions(); + } std::vector - BuildPresenceOverlaySubmissions(SessionUserId RenderUser) const; + BuildPresenceOverlaySubmissions(SessionUserId RenderUser) const { + return m_OverlayModule.BuildPresenceOverlaySubmissions(RenderUser); + } + const MaterialInstance *GetPresenceMaterialForTesting(SessionUserId User) const { + return m_OverlayModule.GetPresenceMaterialForTesting(User); + } + const MaterialInstance * + GetColliderMaterialForTesting(EditorPhysicsBodyType BodyType) const { + return m_OverlayModule.GetColliderMaterialForTesting(BodyType); + } private: - MaterialInstanceRef GetOrCreatePresenceMaterial(SessionUserId User) const; - MaterialInstanceRef - GetOrCreateColliderMaterial(EditorPhysicsBodyType BodyType) const; CommandContext MakeContext() const; CommandContext MakeContext(SessionUserId User) const; SessionId m_SessionId{1}; SessionUserId m_LocalUserId{1}; EditorSession m_Session; - ScriptHost *m_ScriptHost{nullptr}; + HeadlessOverlayModule m_OverlayModule; EditorSceneRendererAdapter *m_RendererAdapter{nullptr}; - MeshRef m_PresenceMarkerMesh; - MeshRef m_ColliderBoxMesh; - MeshRef m_ColliderSphereMesh; RenderViewResolver m_RenderViewResolver; - mutable std::unordered_map m_PresenceMaterials; - mutable std::unordered_map m_ColliderMaterials; - mutable std::mutex m_GizmoHoverMutex; - std::unordered_map m_GizmoHoveredAxisByUser; - mutable std::mutex m_GizmoModeMutex; - std::unordered_map m_GizmoModeByUser; }; } // namespace Axiom diff --git a/Headless/HostModules.cpp b/Headless/HostModules.cpp new file mode 100644 index 00000000..34a2adb2 --- /dev/null +++ b/Headless/HostModules.cpp @@ -0,0 +1,92 @@ +#include "HostModules.h" + +#include + +#include + +namespace Axiom { +HeadlessSessionTransportModule::HeadlessSessionTransportModule( + EditorSession &Session, + std::function()> ActiveViewResolver) + : m_Session(Session), + m_ActiveViewResolver(std::move(ActiveViewResolver)) {} + +std::string_view HeadlessSessionTransportModule::GetName() const { + return "Headless.SessionTransport"; +} + +bool HeadlessSessionTransportModule::Initialize(Application &App) { + m_Endpoint = std::make_unique(m_Session); + m_Endpoint->SetVideoEncoder(CreateDefaultVideoEncoder()); + m_FrameBridge = std::make_unique( + *m_Endpoint, [this]() { return m_ActiveViewResolver(); }); + App.SetViewportFrameOutput(m_FrameBridge.get()); + return true; +} + +void HeadlessSessionTransportModule::Update(const ModuleUpdateContext &Context) { + (void)Context; +} + +void HeadlessSessionTransportModule::Shutdown(Application &App) { + App.SetViewportFrameOutput(nullptr); + m_FrameBridge.reset(); + m_Endpoint.reset(); +} + +ISessionTransport &HeadlessSessionTransportModule::GetTransport() const { + return *m_Endpoint; +} + +void HeadlessSessionTransportModule::SetVideoEncoder( + std::unique_ptr Encoder) { + if (m_Endpoint != nullptr) { + m_Endpoint->SetVideoEncoder(std::move(Encoder)); + } +} + +#if AXIOM_WITH_SCRIPTING +SessionScriptHostModule::SessionScriptHostModule(std::string_view ModuleName, + EditorSession &Session, + SessionId SessionHandle, + SessionUserId LocalUserId) + : m_ModuleName(ModuleName), + m_Session(Session), + m_SessionId(SessionHandle), + m_LocalUserId(LocalUserId) {} + +std::string_view SessionScriptHostModule::GetName() const { + return m_ModuleName; +} + +bool SessionScriptHostModule::Initialize(Application &App) { + (void)App; + m_ScriptHost.Initialize( + AXIOM_CORAL_MANAGED_DIR, + AXIOM_SCRIPTING_TRUST_RESTRICTED ? ScriptTrustProfile::Restricted + : ScriptTrustProfile::Trusted); + m_ScriptHost.LoadEngineAssembly(AXIOM_MANAGED_DIR); + m_ScriptHost.RegisterInternalCalls(m_Session, m_SessionId, m_LocalUserId); + m_Session.Subscribe(&m_ScriptHost); + m_IsSubscribed = true; + return true; +} + +void SessionScriptHostModule::Update(const ModuleUpdateContext &Context) { + if (Context.Phase != ModuleUpdatePhase::FrameStart) { + return; + } + + m_ScriptHost.Tick(Context.DeltaTimeSeconds); +} + +void SessionScriptHostModule::Shutdown(Application &App) { + (void)App; + if (m_IsSubscribed) { + m_Session.Unsubscribe(&m_ScriptHost); + m_IsSubscribed = false; + } + m_ScriptHost.Shutdown(); +} +#endif +} // namespace Axiom diff --git a/Headless/HostModules.h b/Headless/HostModules.h new file mode 100644 index 00000000..000a55bc --- /dev/null +++ b/Headless/HostModules.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#if AXIOM_WITH_SCRIPTING +#include +#endif +#include + +#include "HeadlessRenderView.h" +#include "HeadlessViewportFrameBridge.h" + +#include +#include +#include +#include +#include + +namespace Axiom { +class HeadlessSessionTransportModule final : public IModule { +public: + HeadlessSessionTransportModule( + EditorSession &Session, + std::function()> ActiveViewResolver); + + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; + + ISessionTransport &GetTransport() const; + void SetVideoEncoder(std::unique_ptr Encoder); + +private: + EditorSession &m_Session; + std::function()> m_ActiveViewResolver; + std::unique_ptr m_Endpoint; + std::unique_ptr m_FrameBridge; +}; + +#if AXIOM_WITH_SCRIPTING +class SessionScriptHostModule final : public IModule { +public: + SessionScriptHostModule(std::string_view ModuleName, EditorSession &Session, + SessionId SessionHandle, SessionUserId LocalUserId); + + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; + + ScriptHost &GetScriptHost() { return m_ScriptHost; } + +private: + std::string m_ModuleName; + EditorSession &m_Session; + SessionId m_SessionId{}; + SessionUserId m_LocalUserId{}; + ScriptHost m_ScriptHost; + bool m_IsSubscribed{false}; +}; +#endif +} // namespace Axiom diff --git a/Headless/PackagedRuntimeHost.cpp b/Headless/PackagedRuntimeHost.cpp index 5da2d5e4..027af6bc 100644 --- a/Headless/PackagedRuntimeHost.cpp +++ b/Headless/PackagedRuntimeHost.cpp @@ -1,6 +1,8 @@ #include "PackagedRuntimeHost.h" +#include #include +#include #include namespace Axiom { @@ -11,34 +13,37 @@ PackagedRuntimeHost::PackagedRuntimeHost(const ApplicationArgs &Args, .Width = Width, .Height = Height, .Mode = RuntimeMode::LocalPackagedGame}, - Args) { - m_Layer = new HeadlessSessionLayer(); - m_Layer->SetSharedRendererAdapter(&m_RendererAdapter); - PushLayer(m_Layer); - - m_ScriptHost.Initialize( - AXIOM_CORAL_MANAGED_DIR, - AXIOM_SCRIPTING_TRUST_RESTRICTED ? ScriptTrustProfile::Restricted - : ScriptTrustProfile::Trusted); - m_ScriptHost.LoadEngineAssembly(AXIOM_MANAGED_DIR); - m_ScriptHost.RegisterInternalCalls(m_Layer->GetSession(), SessionId{1}, - m_Layer->GetLocalUserId()); - m_Layer->GetSession().Subscribe(&m_ScriptHost); - m_Layer->SetScriptHost(&m_ScriptHost); + Args, + {.RegisterDefaultModules = false}) { + GetModuleManager().RegisterModule(std::make_unique()); + + auto SessionModule = std::make_unique(); + m_SessionModule = SessionModule.get(); + m_SessionModule->SetSharedRendererAdapter(&m_RendererAdapter); + GetModuleManager().RegisterModule(std::move(SessionModule)); + GetModuleManager().RegisterModule(std::make_unique()); + +#if AXIOM_WITH_SCRIPTING + auto ScriptingModule = std::make_unique( + "PackagedRuntime.SessionScriptHost", m_SessionModule->GetSession(), + SessionId{1}, m_SessionModule->GetLocalUserId()); + m_ScriptingModule = ScriptingModule.get(); + GetModuleManager().RegisterModule(std::move(ScriptingModule)); +#endif } bool PackagedRuntimeHost::LoadPackagedProject(const std::filesystem::path &ContentDir, std::string *FailureReason) { - m_Layer->GetSession().SetContentDir(ContentDir); - m_Layer->GetSession().SetEngineContentDir(ContentDir / "Engine"); - if (!LoadStartupScene(m_Layer->GetSession())) { + m_SessionModule->GetSession().SetContentDir(ContentDir); + m_SessionModule->GetSession().SetEngineContentDir(ContentDir / "Engine"); + if (!LoadStartupScene(m_SessionModule->GetSession())) { if (FailureReason != nullptr) { *FailureReason = "Failed to load the packaged startup scene."; } return false; } - m_Layer->Submit({.Payload = PlaySessionCommand{}}); + m_SessionModule->Submit({.Payload = PlaySessionCommand{}}); return true; } diff --git a/Headless/PackagedRuntimeHost.h b/Headless/PackagedRuntimeHost.h index 86d9bd58..76a21b0f 100644 --- a/Headless/PackagedRuntimeHost.h +++ b/Headless/PackagedRuntimeHost.h @@ -3,9 +3,9 @@ #include #include -#include -#include "HeadlessSessionLayer.h" +#include "HostModules.h" +#include "HeadlessSessionModule.h" namespace Axiom { @@ -18,9 +18,11 @@ class PackagedRuntimeHost final : public Application { std::string *FailureReason = nullptr); private: - HeadlessSessionLayer *m_Layer{nullptr}; + HeadlessSessionModule *m_SessionModule{nullptr}; +#if AXIOM_WITH_SCRIPTING + SessionScriptHostModule *m_ScriptingModule{nullptr}; +#endif EditorSceneRendererAdapter m_RendererAdapter; - ScriptHost m_ScriptHost; }; } // namespace Axiom diff --git a/Headless/RemoteViewportGizmoController.cpp b/Headless/RemoteViewportGizmoController.cpp new file mode 100644 index 00000000..c0d50930 --- /dev/null +++ b/Headless/RemoteViewportGizmoController.cpp @@ -0,0 +1,304 @@ +#include "RemoteViewportGizmoController.h" + +#include "RemoteViewportGridSnap.h" +#include "RemoteViewportServer.h" +#include "RemoteViewportWebRtcSessionManager.h" + +#include + +#include + +namespace Axiom { +RemoteViewportGizmoController::RemoteViewportGizmoController( + RemoteViewportServer &Server) + : m_Server(Server) {} + +void RemoteViewportGizmoController::HandleTextureDropCommand( + SessionUserId User, const HeadlessCommand &Command) { + if (Command.TextureAssetPath.empty()) { + return; + } + + const EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const EditorViewportState *Viewport = Session.FindViewport(User); + if (Viewport == nullptr) { + return; + } + + const std::string HitId = HitTestMeshes( + Viewport->Camera, m_Server.m_Options.Width, m_Server.m_Options.Height, + Command.MousePosition, Session.GetState().Scene.MeshInstances); + if (HitId.empty()) { + return; + } + + m_Server.m_Host.SubmitRemoteCommand( + User, EditorCommand{SetMaterialTextureCommand{ + .ObjectId = HitId, + .TextureAssetPath = Command.TextureAssetPath, + }}); +} + +void RemoteViewportGizmoController::HandleMeshDropCommand( + SessionUserId User, const HeadlessCommand &Command) { + if (Command.MeshAssetPath.empty()) { + return; + } + + const EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const EditorViewportState *Viewport = Session.FindViewport(User); + if (Viewport == nullptr) { + return; + } + + const glm::vec3 SpawnLocation = ResolveViewportDropPosition( + Viewport->Camera, m_Server.m_Options.Width, m_Server.m_Options.Height, + Command.MousePosition, Session.GetState().Scene.MeshInstances); + + m_Server.m_Host.SubmitRemoteCommand( + User, EditorCommand{CreateMeshObjectCommand{ + .AssetPath = Command.MeshAssetPath, + .Location = SpawnLocation, + .RotationDegrees = glm::vec3(0.0f), + .Scale = glm::vec3(1.0f), + }}); +} + +void RemoteViewportGizmoController::HandlePlaceActorCommand( + SessionUserId User, const HeadlessCommand &Command) { + const EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const EditorViewportState *Viewport = Session.FindViewport(User); + if (Viewport == nullptr) { + return; + } + + glm::vec2 MousePos = Command.MousePosition; + if (MousePos.x < 0.0f || MousePos.y < 0.0f) { + MousePos = {static_cast(m_Server.m_Options.Width) * 0.5f, + static_cast(m_Server.m_Options.Height) * 0.5f}; + } + + const glm::vec3 SpawnLocation = ResolveViewportDropPosition( + Viewport->Camera, m_Server.m_Options.Width, m_Server.m_Options.Height, + MousePos, Session.GetState().Scene.MeshInstances); + + m_Server.m_Host.SubmitRemoteCommand( + User, EditorCommand{PlaceActorCommand{ + .ChildTemplateId = Command.PlaceActorTemplateId, + .ChildMeshAssetPath = Command.PlaceActorMeshAssetPath, + .Location = SpawnLocation, + }}); +} + +bool RemoteViewportGizmoController::HandleRemoteClientCommand( + RemoteClientSession &Client, const HeadlessCommand &Command) { + switch (Command.Type) { + case HeadlessCommandType::SetGizmoMode: + Client.CurrentGizmoMode = Command.Mode; + m_Server.m_Host.GetSessionModule().SetGizmoMode(Client.User, Command.Mode); + return true; + case HeadlessCommandType::SetGridSnap: + Client.GridSnap.Enabled = Command.Enabled; + Client.GridSnap.TranslationStep = Command.TranslationStep; + Client.GridSnap.RotationStepDegrees = Command.RotationStepDegrees; + Client.GridSnap.ScaleStep = Command.ScaleStep; + m_Server.m_GridSnap->Sanitize(Client.GridSnap); + return true; + case HeadlessCommandType::GizmoHover: { + if (m_Server.m_Host.GetSessionModule().GetSession().GetRuntimeState() != + EditorRuntimeState::Edit) { + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis(Client.User, -1); + return true; + } + if (Client.GizmoDrag.has_value()) { + return true; + } + const EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const EditorViewportState *Viewport = Session.FindViewport(Client.User); + const EditorObjectDetails *Selected = + Session.FindSelectedObjectDetails(Client.User); + const auto *HoverTD = (Selected != nullptr && Selected->SupportsTransform) + ? (Selected->WorldTransform.has_value() + ? &*Selected->WorldTransform + : (Selected->Transform.has_value() + ? &*Selected->Transform + : nullptr)) + : nullptr; + if (Viewport != nullptr && HoverTD != nullptr) { + const float GizmoScale = ComputeGizmoScale( + Viewport->Camera, HoverTD->Location, m_Server.m_Options.Width, + m_Server.m_Options.Height); + const int Axis = + (Client.CurrentGizmoMode == GizmoMode::Rotate) + ? HitTestGizmoRings(Viewport->Camera, m_Server.m_Options.Width, + m_Server.m_Options.Height, + Command.MousePosition, HoverTD->Location, + GizmoScale) + : HitTestGizmoAxes(Viewport->Camera, m_Server.m_Options.Width, + m_Server.m_Options.Height, + Command.MousePosition, HoverTD->Location, + GizmoScale); + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis(Client.User, Axis); + } else { + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis(Client.User, -1); + } + return true; + } + case HeadlessCommandType::GizmoDragStart: { + if (m_Server.m_Host.GetSessionModule().GetSession().GetRuntimeState() != + EditorRuntimeState::Edit) { + return true; + } + if (Client.GizmoDrag.has_value()) { + return true; + } + + EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const EditorViewportState *Viewport = Session.FindViewport(Client.User); + if (Viewport == nullptr) { + return true; + } + const EditorObjectDetails *Selected = + Session.FindSelectedObjectDetails(Client.User); + const auto *DragTD = + (Selected != nullptr && Selected->SupportsTransform && + !Selected->TransformReadOnly) + ? (Selected->WorldTransform.has_value() + ? &*Selected->WorldTransform + : (Selected->Transform.has_value() ? &*Selected->Transform + : nullptr)) + : nullptr; + + if (DragTD != nullptr) { + const glm::vec3 &ObjPos = DragTD->Location; + const float GizmoScale = ComputeGizmoScale( + Viewport->Camera, ObjPos, m_Server.m_Options.Width, + m_Server.m_Options.Height); + if (Client.CurrentGizmoMode == GizmoMode::Rotate) { + auto DragState = BeginGizmoRotateDrag( + Viewport->Camera, m_Server.m_Options.Width, m_Server.m_Options.Height, + Command.MousePosition, ObjPos, GizmoScale, ObjPos); + if (DragState.has_value()) { + Client.GizmoDrag = ActiveGizmoDrag{ + .Math = *DragState, + .ObjectId = Selected->ObjectId, + .StartRotDeg = DragTD->RotationDegrees, + .StartScale = DragTD->Scale, + .Mode = GizmoMode::Rotate, + .GizmoScaleAtDragStart = GizmoScale, + }; + Session.AcquireLock(Selected->ObjectId, Client.User); + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis( + Client.User, DragState->Axis); + return true; + } + } else { + auto DragState = BeginGizmoDrag( + Viewport->Camera, m_Server.m_Options.Width, m_Server.m_Options.Height, + Command.MousePosition, ObjPos, GizmoScale, ObjPos); + if (DragState.has_value()) { + Client.GizmoDrag = ActiveGizmoDrag{ + .Math = *DragState, + .ObjectId = Selected->ObjectId, + .StartRotDeg = DragTD->RotationDegrees, + .StartScale = DragTD->Scale, + .Mode = Client.CurrentGizmoMode, + .GizmoScaleAtDragStart = GizmoScale, + }; + Session.AcquireLock(Selected->ObjectId, Client.User); + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis( + Client.User, DragState->Axis); + return true; + } + } + } + + const auto Hit = ResolveViewportSelectionHit( + Viewport->Camera, m_Server.m_Options.Width, m_Server.m_Options.Height, + Command.MousePosition, Session.GetState().Scene.MeshInstances, + m_Server.m_Host.GetSessionModule().BuildLightBillboards()); + if (Hit.has_value() && !Hit->ObjectId.empty()) { + std::string SelectId = Hit->ObjectId; + if (const EditorObjectDetails *Picked = Session.FindObjectDetails(SelectId); + Picked != nullptr && Picked->IsGeneratedAssetChild && + Picked->GeneratedFromAssetRootId.has_value()) { + SelectId = *Picked->GeneratedFromAssetRootId; + } + m_Server.m_Host.SubmitRemoteCommand( + Client.User, EditorCommand{SelectObjectCommand{.ObjectId = SelectId}}); + } + return true; + } + case HeadlessCommandType::GizmoDragUpdate: + case HeadlessCommandType::GizmoDragEnd: { + const bool IsEnd = Command.Type == HeadlessCommandType::GizmoDragEnd; + if (m_Server.m_Host.GetSessionModule().GetSession().GetRuntimeState() != + EditorRuntimeState::Edit) { + if (Client.GizmoDrag.has_value()) { + EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const std::string DragObjectId = Client.GizmoDrag->ObjectId; + Client.GizmoDrag.reset(); + Session.ReleaseLock(DragObjectId, Client.User); + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis(Client.User, -1); + } + return true; + } + if (!Client.GizmoDrag.has_value()) { + return true; + } + + EditorSession &Session = m_Server.m_Host.GetSessionModule().GetSession(); + const EditorViewportState *Viewport = Session.FindViewport(Client.User); + if (Viewport != nullptr) { + const ActiveGizmoDrag &Drag = *Client.GizmoDrag; + glm::vec3 Location = Drag.Math.ObjectStartPos; + glm::vec3 RotDeg = Drag.StartRotDeg; + glm::vec3 Scale = Drag.StartScale; + if (Drag.Mode == GizmoMode::Translate) { + Location = UpdateGizmoDrag( + Drag.Math, Viewport->Camera, m_Server.m_Options.Width, + m_Server.m_Options.Height, Command.MousePosition.x, + Command.MousePosition.y); + } else if (Drag.Mode == GizmoMode::Scale) { + const glm::vec3 NewPosTmp = + UpdateGizmoDrag(Drag.Math, Viewport->Camera, m_Server.m_Options.Width, + m_Server.m_Options.Height, Command.MousePosition.x, + Command.MousePosition.y); + const float DeltaT = + glm::dot(NewPosTmp - Drag.Math.ObjectStartPos, Drag.Math.AxisDir); + const float Factor = + std::max(0.001f, 1.0f + DeltaT / + std::max(0.001f, Drag.GizmoScaleAtDragStart)); + Scale[Drag.Math.Axis] = Drag.StartScale[Drag.Math.Axis] * Factor; + } else { + const float DeltaDeg = UpdateGizmoRotateDrag( + Drag.Math, Viewport->Camera, m_Server.m_Options.Width, + m_Server.m_Options.Height, Command.MousePosition.x, + Command.MousePosition.y); + RotDeg[Drag.Math.Axis] = Drag.StartRotDeg[Drag.Math.Axis] + DeltaDeg; + } + m_Server.m_GridSnap->Apply(Client.GridSnap, Drag.Mode, Drag.Math.Axis, + Location, RotDeg, Scale); + EditorCommand Cmd; + Cmd.Payload = SetTransformCommand{ + .ObjectId = Drag.ObjectId, + .Location = Location, + .RotationDegrees = RotDeg, + .Scale = Scale, + }; + m_Server.m_Host.SubmitRemoteCommand(Client.User, Cmd); + } + + if (IsEnd) { + const std::string DragObjectId = Client.GizmoDrag->ObjectId; + Client.GizmoDrag.reset(); + Session.ReleaseLock(DragObjectId, Client.User); + m_Server.m_Host.GetSessionModule().SetGizmoHoveredAxis(Client.User, -1); + } + return true; + } + default: + return false; + } +} +} // namespace Axiom diff --git a/Headless/RemoteViewportGizmoController.h b/Headless/RemoteViewportGizmoController.h new file mode 100644 index 00000000..235b584e --- /dev/null +++ b/Headless/RemoteViewportGizmoController.h @@ -0,0 +1,39 @@ +#pragma once + +#include "HeadlessCommandProtocol.h" + +#include +#include + +#include + +namespace Axiom { +class RemoteViewportServer; +struct RemoteClientSession; + +struct ActiveGizmoDrag { + GizmoDragState Math; + std::string ObjectId; + glm::vec3 StartRotDeg{0.0f}; + glm::vec3 StartScale{1.0f}; + GizmoMode Mode{GizmoMode::Translate}; + float GizmoScaleAtDragStart{1.0f}; +}; + +class RemoteViewportGizmoController { +public: + explicit RemoteViewportGizmoController(RemoteViewportServer &Server); + + void HandleTextureDropCommand(SessionUserId User, + const HeadlessCommand &Command); + void HandleMeshDropCommand(SessionUserId User, + const HeadlessCommand &Command); + void HandlePlaceActorCommand(SessionUserId User, + const HeadlessCommand &Command); + bool HandleRemoteClientCommand(RemoteClientSession &Client, + const HeadlessCommand &Command); + +private: + RemoteViewportServer &m_Server; +}; +} // namespace Axiom diff --git a/Headless/RemoteViewportGridSnap.cpp b/Headless/RemoteViewportGridSnap.cpp new file mode 100644 index 00000000..54c19605 --- /dev/null +++ b/Headless/RemoteViewportGridSnap.cpp @@ -0,0 +1,45 @@ +#include "RemoteViewportGridSnap.h" + +#include +#include + +namespace Axiom { +namespace { +float SnapToStep(float Value, float Step) { + if (Step <= 0.0f) { + return Value; + } + return std::round(Value / Step) * Step; +} +} // namespace + +void RemoteViewportGridSnap::Sanitize(GridSnapSettings &Settings) const { + Settings.TranslationStep = std::max(MinimumScale, Settings.TranslationStep); + Settings.RotationStepDegrees = std::max(0.001f, Settings.RotationStepDegrees); + Settings.ScaleStep = std::max(MinimumScale, Settings.ScaleStep); +} + +void RemoteViewportGridSnap::Apply(const GridSnapSettings &Settings, + GizmoMode Mode, int Axis, + glm::vec3 &Location, + glm::vec3 &RotationDegrees, + glm::vec3 &Scale) const { + if (!Settings.Enabled || Axis < 0 || Axis > 2) { + return; + } + + switch (Mode) { + case GizmoMode::Translate: + Location[Axis] = SnapToStep(Location[Axis], Settings.TranslationStep); + break; + case GizmoMode::Rotate: + RotationDegrees[Axis] = + SnapToStep(RotationDegrees[Axis], Settings.RotationStepDegrees); + break; + case GizmoMode::Scale: + Scale[Axis] = + std::max(MinimumScale, SnapToStep(Scale[Axis], Settings.ScaleStep)); + break; + } +} +} // namespace Axiom diff --git a/Headless/RemoteViewportGridSnap.h b/Headless/RemoteViewportGridSnap.h new file mode 100644 index 00000000..c3b14009 --- /dev/null +++ b/Headless/RemoteViewportGridSnap.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include + +namespace Axiom { +struct GridSnapSettings { + bool Enabled{true}; + float TranslationStep{1.0f}; + float RotationStepDegrees{15.0f}; + float ScaleStep{0.1f}; +}; + +class RemoteViewportGridSnap { +public: + static constexpr float MinimumScale = 0.001f; + + void Sanitize(GridSnapSettings &Settings) const; + void Apply(const GridSnapSettings &Settings, GizmoMode Mode, int Axis, + glm::vec3 &Location, glm::vec3 &RotationDegrees, + glm::vec3 &Scale) const; +}; +} // namespace Axiom diff --git a/Headless/RemoteViewportHttpRouter.cpp b/Headless/RemoteViewportHttpRouter.cpp new file mode 100644 index 00000000..f139a517 --- /dev/null +++ b/Headless/RemoteViewportHttpRouter.cpp @@ -0,0 +1,561 @@ +#include "RemoteViewportHttpRouter.h" + +#include "HeadlessCommandProtocol.h" +#include "RemoteViewportServer.h" +#include "RemoteViewportWebRtcSessionManager.h" +#include "RemoteViewportWebSocketDispatch.h" + +#ifndef AXIOM_CONTENT_DIR +#define AXIOM_CONTENT_DIR "Content" +#endif + +#include +#include +#include +#include +#include +#include +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Axiom { +#include "RemoteViewportHttpRouterHelpers.inl" + +RemoteViewportHttpRouter::RemoteViewportHttpRouter(RemoteViewportServer &Server) + : m_Server(Server), m_ProjectsRoot(Project::GetDefaultProjectsRoot()) {} + +uint64_t RemoteViewportHttpRouter::GetTotalHttpRequests() const { + return m_TotalHttpRequests.load(); +} + +uintptr_t RemoteViewportHttpRouter::AllocateConnectionId() { + return m_NextClientConnectionId.fetch_add(1); +} + +void RemoteViewportHttpRouter::IncrementRequestCount() { + m_TotalHttpRequests.fetch_add(1); +} + +void RemoteViewportHttpRouter::RegisterPendingResponse( + uintptr_t ClientSocketValue, void *Response) { + std::scoped_lock Lock(m_HttpResponseMutex); + m_PendingHttpResponses[ClientSocketValue] = PendingHttpResponse{ + .Response = Response, + .Aborted = false, + }; +} + +void RemoteViewportHttpRouter::MarkPendingResponseAborted( + uintptr_t ClientSocketValue) { + std::scoped_lock Lock(m_HttpResponseMutex); + auto It = m_PendingHttpResponses.find(ClientSocketValue); + if (It != m_PendingHttpResponses.end()) { + It->second.Aborted = true; + m_PendingHttpResponses.erase(It); + } +} + +bool RemoteViewportHttpRouter::SendHttpResponse(uintptr_t ClientSocketValue, + std::string_view Response) { + PendingHttpResponse Pending{}; + { + std::scoped_lock Lock(m_HttpResponseMutex); + const auto It = m_PendingHttpResponses.find(ClientSocketValue); + if (It == m_PendingHttpResponses.end() || It->second.Aborted) { + return false; + } + Pending = It->second; + m_PendingHttpResponses.erase(It); + } + + auto Parsed = ParseHttpResponseText(Response); + if (!Parsed.has_value()) { + return false; + } + + auto *HttpResponse = static_cast *>(Pending.Response); + HttpResponse->writeStatus(Parsed->Status); + for (const auto &[HeaderName, HeaderValue] : Parsed->Headers) { + if (EqualsCaseInsensitive(HeaderName, "Content-Length") || + EqualsCaseInsensitive(HeaderName, "Connection")) { + continue; + } + HttpResponse->writeHeader(HeaderName, HeaderValue); + } + HttpResponse->end(Parsed->Body); + return true; +} + +bool RemoteViewportHttpRouter::SendJsonResponse(uintptr_t ClientSocketValue, + std::string_view Status, + std::string_view Payload) { + return SendHttpResponse(ClientSocketValue, JsonResponse(Status, Payload)); +} + +bool RemoteViewportHttpRouter::SendJsonError(uintptr_t ClientSocketValue, + std::string_view Status, + std::string_view ErrorMessage) { + return SendJsonResponse(ClientSocketValue, Status, SerializeError(ErrorMessage)); +} + +std::vector RemoteViewportHttpRouter::ListProjects() const { + return Project::DiscoverProjects(m_ProjectsRoot); +} + +std::optional +RemoteViewportHttpRouter::GetActiveProject() const { + std::scoped_lock Lock(m_ProjectMutex); + return m_ActiveProject; +} + +std::optional +RemoteViewportHttpRouter::SetActiveProjectBySlug(std::string_view ProjectSlug) { + const auto Opened = Project::OpenProjectBySlug(m_ProjectsRoot, ProjectSlug); + if (!Opened.has_value()) { + return std::nullopt; + } + + std::scoped_lock Lock(m_ProjectMutex); + m_ActiveProject = *Opened; + return Opened; +} + +void RemoteViewportHttpRouter::SetActiveProject( + const Project::ProjectDescriptor &Project) { + std::scoped_lock Lock(m_ProjectMutex); + m_ActiveProject = Project; +} + +std::filesystem::path RemoteViewportHttpRouter::GetActiveContentDir() const { + if (const auto ActiveProject = GetActiveProject(); ActiveProject.has_value()) { + return ActiveProject->Root.ContentDir; + } + return std::filesystem::path(AXIOM_CONTENT_DIR); +} + +std::filesystem::path RemoteViewportHttpRouter::GetActiveScriptsDir() const { + if (const auto ActiveProject = GetActiveProject(); ActiveProject.has_value()) { + return ActiveProject->ScriptWorkspace.ScriptsDir; + } + return std::filesystem::path(AXIOM_PROJECTS_DIR) / "__default__" / "Scripts"; +} + +std::filesystem::path RemoteViewportHttpRouter::GetEngineContentDir() const { + return std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine"; +} + +bool RemoteViewportHttpRouter::LoadActiveProjectIntoSession( + std::string *FailureReason) { + if (m_Server.m_Host.LoadStartupSceneIntoSession(GetActiveContentDir())) { + return true; + } + if (FailureReason != nullptr) { + *FailureReason = + "Failed to load the active project's startup scene into the session."; + } + return false; +} + +std::vector RemoteViewportHttpRouter::ListScriptFiles() const { + std::vector Results; + const auto ScriptsDir = GetActiveScriptsDir(); + if (!std::filesystem::exists(ScriptsDir)) { + return Results; + } + for (const auto &Entry : + std::filesystem::recursive_directory_iterator(ScriptsDir)) { + if (!Entry.is_regular_file() || Entry.path().extension() != ".cs") { + continue; + } + std::error_code Error; + const auto Relative = + std::filesystem::relative(Entry.path(), ScriptsDir, Error); + if (!Error) { + Results.push_back(Relative.generic_string()); + } + } + std::sort(Results.begin(), Results.end()); + return Results; +} + +std::vector> +RemoteViewportHttpRouter::ListScriptClasses() const { + std::vector> Results; + const auto ScriptFiles = ListScriptFiles(); + const auto ActiveProject = GetActiveProject(); + const std::string DefaultNamespace = + ActiveProject.has_value() ? ActiveProject->ScriptWorkspace.RootNamespace + : "Project.Scripts"; + const std::regex NamespacePattern( + R"(namespace\s+([A-Za-z_][A-Za-z0-9_\.]*)\s*[;{])"); + const std::regex ClassPattern( + R"(public\s+class\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*Script\b)"); + + for (const auto &RelativePath : ScriptFiles) { + const auto AbsolutePath = ResolveActiveScriptPath(RelativePath); + if (!AbsolutePath.has_value()) { + continue; + } + std::ifstream File(*AbsolutePath); + if (!File.is_open()) { + continue; + } + const std::string Content((std::istreambuf_iterator(File)), + std::istreambuf_iterator()); + std::string Namespace = DefaultNamespace; + if (std::smatch NamespaceMatch; + std::regex_search(Content, NamespaceMatch, NamespacePattern) && + NamespaceMatch.size() > 1) { + Namespace = NamespaceMatch[1].str(); + } + + auto ClassBegin = + std::sregex_iterator(Content.begin(), Content.end(), ClassPattern); + const auto ClassEnd = std::sregex_iterator(); + for (auto It = ClassBegin; It != ClassEnd; ++It) { + Results.emplace_back(Namespace + "." + (*It)[1].str(), RelativePath); + } + } + + std::sort(Results.begin(), Results.end(), + [](const auto &Left, const auto &Right) { + return Left.first < Right.first; + }); + return Results; +} + +std::optional +RemoteViewportHttpRouter::ResolveActiveScriptPath(std::string_view RelativePath, + bool AllowMissingLeaf) const { + const std::filesystem::path Relative = + std::filesystem::path(RelativePath).lexically_normal(); + if (!IsValidScriptRelativePath(Relative)) { + return std::nullopt; + } + const auto ScriptsDir = GetActiveScriptsDir(); + const auto Candidate = (ScriptsDir / Relative).lexically_normal(); + const auto ValidationPath = AllowMissingLeaf ? Candidate.parent_path() : Candidate; + if (!Project::IsPathWithinRoot(ScriptsDir, ValidationPath)) { + return std::nullopt; + } + return Candidate; +} + +std::vector +RemoteViewportHttpRouter::CollectVisibleAssets() const { + std::vector Assets; + const Assets::LocalAssetSource ProjectSource{GetActiveContentDir()}; + Assets = ProjectSource.List(); + + const Assets::LocalAssetSource EngineSource{GetEngineContentDir()}; + for (auto EngineAsset : EngineSource.List()) { + EngineAsset.RelativePath = + (std::filesystem::path("Engine") / EngineAsset.RelativePath) + .generic_string(); + EngineAsset.Name = + std::filesystem::path(EngineAsset.RelativePath).stem().string(); + EngineAsset.Id = Assets::AssetIdFromRelativePath(EngineAsset.RelativePath); + Assets.push_back(std::move(EngineAsset)); + } + + std::sort(Assets.begin(), Assets.end(), + [](const Assets::AssetDescriptor &Left, + const Assets::AssetDescriptor &Right) { + return Left.RelativePath < Right.RelativePath; + }); + return Assets; +} + +std::optional +RemoteViewportHttpRouter::ResolveVisibleAssetPath( + std::string_view RelativePath) const { + if (RelativePath.empty()) { + return std::nullopt; + } + const std::filesystem::path Relative{std::string(RelativePath)}; + for (const auto &Part : Relative) { + if (Part == "..") { + return std::nullopt; + } + } + if (!Relative.empty() && *Relative.begin() == "Engine") { + std::filesystem::path EngineRelative; + auto It = Relative.begin(); + ++It; + for (; It != Relative.end(); ++It) { + EngineRelative /= *It; + } + return GetEngineContentDir() / EngineRelative; + } + return GetActiveContentDir() / Relative; +} + +bool RemoteViewportHttpRouter::HandlePostRequest(uintptr_t ClientSocketValue, + std::string_view Path, + std::string_view HeaderBlock, + std::string_view Body) { + const std::string_view Route = StripQuery(Path); + if (Route == "/projects/create") { + return HandleCreateProjectRequest(ClientSocketValue, Body); + } + if (Route == "/projects/open") { + return HandleOpenProjectRequest(ClientSocketValue, Body); + } + if (Route == "/projects/cook") { + return HandleCookProjectRequest(ClientSocketValue); + } + if (Route == "/projects/package") { + return HandlePackageProjectRequest(ClientSocketValue); + } + if (Route == "/scripts/create") { + return HandleCreateScriptFileRequest(ClientSocketValue, Body); + } + if (Route == "/scripts/save") { + return HandleSaveScriptFileRequest(ClientSocketValue, Body); + } + if (Route == "/scripts/rename") { + return HandleRenameScriptFileRequest(ClientSocketValue, Body); + } + if (Route == "/scripts/delete") { + return HandleDeleteScriptFileRequest(ClientSocketValue, Body); + } + if (Route == "/session/connect") { + return m_Server.m_WebRtcSessions->HandleSessionConnectRequest( + ClientSocketValue, HeaderBlock, Body); + } + if (Route == "/webrtc/offer") { + return m_Server.m_WebRtcSessions->HandleWebRtcOfferRequest( + ClientSocketValue, HeaderBlock, Body); + } + if (Route == "/webrtc/ice-candidate") { + return m_Server.m_WebRtcSessions->HandleWebRtcIceCandidateRequest( + ClientSocketValue, HeaderBlock, Body); + } + if (Route == "/webrtc/close") { + return m_Server.m_WebRtcSessions->HandleWebRtcCloseRequest( + ClientSocketValue, HeaderBlock, Body); + } + if (Route == "/assets/upload") { + return HandleAssetUploadRequest(ClientSocketValue, Path, HeaderBlock, Body); + } + if (Route != "/command") { + SendJsonError(ClientSocketValue, "404 Not Found", "Unknown POST endpoint."); + return false; + } + + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + const auto User = ClientId.has_value() + ? m_Server.m_WebRtcSessions->ResolveClientUser(*ClientId) + : std::nullopt; + if (!User.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing or unknown X-Axiom-Client-Id."); + return false; + } + if (ClientId.has_value()) { + m_Server.m_WebRtcSessions->TouchClientSession(*ClientId); + } + + std::string Error; + const auto Command = ParseRemoteViewportCommand(Body, Error); + if (!Command.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", Error); + return false; + } + + switch (Command->Type) { + case HeadlessCommandType::SetViewMode: + m_Server.m_Host.SetRemoteViewMode(*User, Command->ViewMode); + break; + case HeadlessCommandType::SetShowColliders: + m_Server.m_Host.SetRemoteShowColliders(*User, Command->ShowColliders); + break; + case HeadlessCommandType::SetLookActive: + case HeadlessCommandType::SetViewportCameraPose: + case HeadlessCommandType::SetCameraProjection: + case HeadlessCommandType::UpdateViewportCamera: + case HeadlessCommandType::SelectObject: + case HeadlessCommandType::RenameObject: + case HeadlessCommandType::SetObjectVisibility: + case HeadlessCommandType::CreateObject: + case HeadlessCommandType::DuplicateObject: + case HeadlessCommandType::DeleteObject: + case HeadlessCommandType::ReparentObject: + case HeadlessCommandType::SetTransform: + case HeadlessCommandType::AttachScript: + case HeadlessCommandType::DetachScript: + case HeadlessCommandType::PlaySession: + case HeadlessCommandType::PauseSession: + case HeadlessCommandType::ResumeSession: + case HeadlessCommandType::StopSession: + case HeadlessCommandType::SetMeshAsset: + case HeadlessCommandType::SetLightProperties: + case HeadlessCommandType::SetMaterialProperties: + case HeadlessCommandType::SetMaterialTexture: + case HeadlessCommandType::SetWorldSettings: + m_Server.m_Host.SubmitRemoteCommand(*User, Command->EditorPayload); + break; + case HeadlessCommandType::DropMesh: + m_Server.m_GizmoController->HandleMeshDropCommand(*User, *Command); + break; + case HeadlessCommandType::DropTexture: + m_Server.m_GizmoController->HandleTextureDropCommand(*User, *Command); + break; + case HeadlessCommandType::PlaceActor: + m_Server.m_GizmoController->HandlePlaceActorCommand(*User, *Command); + break; + case HeadlessCommandType::Quit: + m_Server.m_StopRequested.store(true); + m_Server.m_Host.RequestClose(); + m_Server.m_WebSocketDispatch->BroadcastTextMessage(SerializeShutdown()); + break; + default: + break; + } + + SendJsonResponse(ClientSocketValue, "202 Accepted", + SerializeTypeOnlyJson("accepted")); + return false; +} + +bool RemoteViewportHttpRouter::HandleGetRequest(uintptr_t ClientSocketValue, + std::string_view Path, + std::string_view HeaderBlock) { + const std::string_view Route = StripQuery(Path); + if (Route == "/projects") { + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeProjectList(ListProjects(), GetActiveProject())); + return false; + } + if (Route == "/projects/current") { + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeCurrentProject(GetActiveProject())); + return false; + } + if (Route == "/scripts") { + return HandleListScriptsRequest(ClientSocketValue); + } + if (Route == "/scripts/classes") { + return HandleListScriptClassesRequest(ClientSocketValue); + } + if (Route == "/scripts/file") { + return HandleReadScriptFileRequest(ClientSocketValue, Path); + } + if (Route == "/health") { + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeReady(m_Server.m_Options.Width, + m_Server.m_Options.Height)); + return false; + } + if (Route == "/webrtc") { + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + const WebRtcSessionStatus Status = + ClientId.has_value() + ? m_Server.m_WebRtcSessions->GetClientWebRtcStatus(*ClientId) + : WebRtcSessionStatus{}; + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeWebRtcStatus( + Status.Enabled, Status.Available, Status.SignalingState, + Status.ConnectionState, Status.Detail, Status.SessionId, + Status.PendingLocalIceCandidateCount, Status.Video)); + return false; + } + if (Route == "/webrtc/ice-candidates") { + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + std::vector Candidates; + if (ClientId.has_value()) { + if (auto Client = m_Server.m_WebRtcSessions->FindClientSession(*ClientId); + Client != nullptr && Client->WebRtcSession != nullptr) { + Candidates = Client->WebRtcSession->TakePendingLocalIceCandidates(); + } + } + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeWebRtcIceCandidateList(Candidates)); + return false; + } + if (Route == "/session") { + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + const auto User = ClientId.has_value() + ? m_Server.m_WebRtcSessions->ResolveClientUser(*ClientId) + : std::nullopt; + if (!User.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing or unknown X-Axiom-Client-Id."); + return false; + } + if (ClientId.has_value()) { + m_Server.m_WebRtcSessions->TouchClientSession(*ClientId); + } + const WebRtcSessionStatus Status = + ClientId.has_value() + ? m_Server.m_WebRtcSessions->GetClientWebRtcStatus(*ClientId) + : WebRtcSessionStatus{}; + const bool ShowColliders = [&]() -> bool { + if (ClientId.has_value()) { + if (const HeadlessRenderViewState *View = + m_Server.m_Host.FindRemoteRenderView(*ClientId); + View != nullptr) { + return View->ShowColliders; + } + } + if (const HeadlessRenderViewState *View = + m_Server.m_Host.FindRenderView(*User); + View != nullptr) { + return View->ShowColliders; + } + return true; + }(); + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeSessionSnapshot( + m_Server.m_Host.GetSessionModule().GetSession().GetState(), + *User, ShowColliders, + m_Server.m_TransportConnected.load(), + m_Server.m_TransportConnected.load() ? "connected" + : "disconnected", + Status.ConnectionState)); + return false; + } + if (Route == "/assets/thumbnail") { + const auto RelPath = GetQueryParam(Path, "path"); + if (!RelPath.has_value() || RelPath->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing 'path' query parameter."); + return false; + } + if (RelPath->find("..") != std::string::npos) { + SendJsonError(ClientSocketValue, "400 Bad Request", "Invalid path."); + return false; + } + const auto FullPath = ResolveVisibleAssetPath(*RelPath); + if (!FullPath.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", "Invalid path."); + return false; + } + const std::vector Jpeg = MakeThumbnailJpeg(*FullPath); + if (Jpeg.empty()) { + SendJsonError(ClientSocketValue, "404 Not Found", + "Could not load thumbnail."); + return false; + } + const std::string_view JpegView(reinterpret_cast(Jpeg.data()), + Jpeg.size()); + SendHttpResponse(ClientSocketValue, + BuildHttpResponse("200 OK", "image/jpeg", JpegView)); + return false; + } + + SendJsonError(ClientSocketValue, "404 Not Found", "Unknown GET endpoint."); + return false; +} + +#include "RemoteViewportHttpRouterMutations.inl" +} // namespace Axiom diff --git a/Headless/RemoteViewportHttpRouter.h b/Headless/RemoteViewportHttpRouter.h new file mode 100644 index 00000000..e8ab0d7d --- /dev/null +++ b/Headless/RemoteViewportHttpRouter.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Axiom { +class RemoteViewportServer; + +class RemoteViewportHttpRouter { +public: + explicit RemoteViewportHttpRouter(RemoteViewportServer &Server); + + uint64_t GetTotalHttpRequests() const; + uintptr_t AllocateConnectionId(); + void IncrementRequestCount(); + void RegisterPendingResponse(uintptr_t ClientSocketValue, void *Response); + void MarkPendingResponseAborted(uintptr_t ClientSocketValue); + bool SendHttpResponse(uintptr_t ClientSocketValue, std::string_view Response); + bool SendJsonResponse(uintptr_t ClientSocketValue, std::string_view Status, + std::string_view Payload); + bool SendJsonError(uintptr_t ClientSocketValue, std::string_view Status, + std::string_view ErrorMessage); + + bool HandleGetRequest(uintptr_t ClientSocketValue, std::string_view Path, + std::string_view HeaderBlock); + bool HandlePostRequest(uintptr_t ClientSocketValue, std::string_view Path, + std::string_view HeaderBlock, + std::string_view Body); + + std::vector ListProjects() const; + std::optional GetActiveProject() const; + std::optional + SetActiveProjectBySlug(std::string_view ProjectSlug); + void SetActiveProject(const Project::ProjectDescriptor &Project); + std::filesystem::path GetActiveContentDir() const; + std::filesystem::path GetActiveScriptsDir() const; + std::filesystem::path GetEngineContentDir() const; + bool LoadActiveProjectIntoSession(std::string *FailureReason = nullptr); + std::vector ListScriptFiles() const; + std::vector> ListScriptClasses() const; + std::optional + ResolveActiveScriptPath(std::string_view RelativePath, + bool AllowMissingLeaf = false) const; + std::vector CollectVisibleAssets() const; + std::optional + ResolveVisibleAssetPath(std::string_view RelativePath) const; + +private: + struct PendingHttpResponse { + void *Response{nullptr}; + bool Aborted{false}; + }; + + bool HandleCreateProjectRequest(uintptr_t ClientSocketValue, + std::string_view Body); + bool HandleOpenProjectRequest(uintptr_t ClientSocketValue, + std::string_view Body); + bool HandleCookProjectRequest(uintptr_t ClientSocketValue); + bool HandlePackageProjectRequest(uintptr_t ClientSocketValue); + bool HandleListScriptsRequest(uintptr_t ClientSocketValue); + bool HandleListScriptClassesRequest(uintptr_t ClientSocketValue); + bool HandleReadScriptFileRequest(uintptr_t ClientSocketValue, + std::string_view Path); + bool HandleCreateScriptFileRequest(uintptr_t ClientSocketValue, + std::string_view Body); + bool HandleSaveScriptFileRequest(uintptr_t ClientSocketValue, + std::string_view Body); + bool HandleRenameScriptFileRequest(uintptr_t ClientSocketValue, + std::string_view Body); + bool HandleDeleteScriptFileRequest(uintptr_t ClientSocketValue, + std::string_view Body); + bool HandleAssetUploadRequest(uintptr_t ClientSocketValue, + std::string_view Path, + std::string_view HeaderBlock, + std::string_view Body); + + RemoteViewportServer &m_Server; + const std::filesystem::path m_ProjectsRoot; + mutable std::mutex m_ProjectMutex; + std::optional m_ActiveProject; + std::atomic m_NextClientConnectionId{1}; + mutable std::mutex m_HttpResponseMutex; + std::unordered_map m_PendingHttpResponses; + std::atomic m_TotalHttpRequests{0}; +}; +} // namespace Axiom diff --git a/Headless/RemoteViewportHttpRouterHelpers.inl b/Headless/RemoteViewportHttpRouterHelpers.inl new file mode 100644 index 00000000..eec0c3cc --- /dev/null +++ b/Headless/RemoteViewportHttpRouterHelpers.inl @@ -0,0 +1,486 @@ +namespace { +constexpr std::string_view ClientIdHeaderName = "X-Axiom-Client-Id"; + +std::string BuildHttpResponse(std::string_view Status, + std::string_view ContentType, + std::string_view Body, + std::string_view ExtraHeaders = {}) { + std::ostringstream Stream; + Stream << "HTTP/1.1 " << Status << "\r\n" + << "Content-Type: " << ContentType << "\r\n" + << "Content-Length: " << Body.size() << "\r\n" + << "Cache-Control: no-store\r\n" + << "Connection: close\r\n" + << "Access-Control-Allow-Origin: *\r\n"; + if (!ExtraHeaders.empty()) { + Stream << ExtraHeaders; + } + Stream << "\r\n" << Body; + return Stream.str(); +} + +std::string Trim(std::string_view Value) { + while (!Value.empty() && + std::isspace(static_cast(Value.front())) != 0) { + Value.remove_prefix(1); + } + while (!Value.empty() && + std::isspace(static_cast(Value.back())) != 0) { + Value.remove_suffix(1); + } + return std::string(Value); +} + +bool EqualsCaseInsensitive(std::string_view Left, std::string_view Right) { + if (Left.size() != Right.size()) { + return false; + } + for (size_t Index = 0; Index < Left.size(); ++Index) { + if (std::tolower(static_cast(Left[Index])) != + std::tolower(static_cast(Right[Index]))) { + return false; + } + } + return true; +} + +struct ParsedHttpResponse { + std::string Status; + std::vector> Headers; + std::string Body; +}; + +std::optional ParseHttpResponseText(std::string_view Response) { + const size_t HeaderEnd = Response.find("\r\n\r\n"); + if (HeaderEnd == std::string_view::npos) { + return std::nullopt; + } + const std::string_view HeaderBlock = Response.substr(0, HeaderEnd); + const size_t StatusLineEnd = HeaderBlock.find("\r\n"); + if (StatusLineEnd == std::string_view::npos) { + return std::nullopt; + } + const std::string_view StatusLine = HeaderBlock.substr(0, StatusLineEnd); + const size_t FirstSpace = StatusLine.find(' '); + if (FirstSpace == std::string_view::npos || FirstSpace + 1 >= StatusLine.size()) { + return std::nullopt; + } + + ParsedHttpResponse Parsed{}; + Parsed.Status = std::string(StatusLine.substr(FirstSpace + 1)); + Parsed.Body = std::string(Response.substr(HeaderEnd + 4)); + + size_t LineStart = StatusLineEnd + 2; + while (LineStart < HeaderBlock.size()) { + const size_t LineEnd = HeaderBlock.find("\r\n", LineStart); + const std::string_view Line = HeaderBlock.substr( + LineStart, (LineEnd == std::string_view::npos ? HeaderBlock.size() : LineEnd) - + LineStart); + const size_t Colon = Line.find(':'); + if (Colon != std::string_view::npos) { + Parsed.Headers.emplace_back(std::string(Trim(Line.substr(0, Colon))), + std::string(Trim(Line.substr(Colon + 1)))); + } + if (LineEnd == std::string_view::npos) { + break; + } + LineStart = LineEnd + 2; + } + + return Parsed; +} + +std::string_view StripQuery(std::string_view Path) { + const size_t Query = Path.find('?'); + return Query == std::string_view::npos ? Path : Path.substr(0, Query); +} + +std::string UrlDecode(std::string_view Input) { + std::string Out; + Out.reserve(Input.size()); + for (size_t I = 0; I < Input.size(); ++I) { + if (Input[I] == '%' && I + 2 < Input.size()) { + const char Hi = Input[I + 1]; + const char Lo = Input[I + 2]; + auto HexVal = [](char C) -> int { + if (C >= '0' && C <= '9') return C - '0'; + if (C >= 'a' && C <= 'f') return C - 'a' + 10; + if (C >= 'A' && C <= 'F') return C - 'A' + 10; + return -1; + }; + const int H = HexVal(Hi); + const int L = HexVal(Lo); + if (H >= 0 && L >= 0) { + Out += static_cast(H * 16 + L); + I += 2; + continue; + } + } else if (Input[I] == '+') { + Out += ' '; + continue; + } + Out += Input[I]; + } + return Out; +} + +std::optional GetQueryParam(std::string_view Path, + std::string_view Key) { + const size_t Q = Path.find('?'); + if (Q == std::string_view::npos) { + return std::nullopt; + } + std::string_view Query = Path.substr(Q + 1); + while (!Query.empty()) { + const size_t Amp = Query.find('&'); + std::string_view Pair = + Amp == std::string_view::npos ? Query : Query.substr(0, Amp); + const size_t Eq = Pair.find('='); + if (Eq != std::string_view::npos && Pair.substr(0, Eq) == Key) { + return UrlDecode(Pair.substr(Eq + 1)); + } + if (Amp == std::string_view::npos) { + break; + } + Query.remove_prefix(Amp + 1); + } + return std::nullopt; +} + +std::optional FindHeaderValue(std::string_view HeaderBlock, + std::string_view HeaderName) { + size_t LineStart = 0; + while (LineStart < HeaderBlock.size()) { + const size_t LineEnd = HeaderBlock.find("\r\n", LineStart); + const std::string_view Line = + HeaderBlock.substr(LineStart, LineEnd == std::string_view::npos + ? std::string_view::npos + : LineEnd - LineStart); + const size_t Colon = Line.find(':'); + if (Colon != std::string_view::npos && + EqualsCaseInsensitive(Trim(Line.substr(0, Colon)), HeaderName)) { + return Trim(Line.substr(Colon + 1)); + } + if (LineEnd == std::string_view::npos) { + break; + } + LineStart = LineEnd + 2; + } + return std::nullopt; +} + +std::string JsonResponse(std::string_view Status, std::string_view Payload) { + return BuildHttpResponse(Status, "application/json; charset=utf-8", Payload); +} + +using JsonWriter = rapidjson::Writer; + +void WriteJsonString(JsonWriter &Writer, std::string_view Value) { + Writer.String(Value.data(), static_cast(Value.size())); +} + +template std::string BuildJson(Fn &&FnWriter) { + rapidjson::StringBuffer Buffer; + JsonWriter Writer(Buffer); + FnWriter(Writer); + return std::string(Buffer.GetString(), Buffer.GetSize()); +} + +std::string SerializeTypeOnlyJson(std::string_view Type) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + WriteJsonString(Writer, Type); + Writer.EndObject(); + }); +} + +std::optional ExtractJsonStringField(std::string_view Body, + std::string_view FieldName) { + std::string MutableBody(Body); + rapidjson::Document Document; + Document.ParseInsitu(MutableBody.data()); + if (Document.HasParseError() || !Document.IsObject()) { + return std::nullopt; + } + const auto It = Document.FindMember(std::string(FieldName).c_str()); + if (It == Document.MemberEnd() || !It->value.IsString()) { + return std::nullopt; + } + return std::string(It->value.GetString(), It->value.GetStringLength()); +} + +void WriteProjectJson(JsonWriter &Writer, + const Project::ProjectDescriptor &Project) { + Writer.StartObject(); + Writer.Key("projectId"); + WriteJsonString(Writer, Project.Manifest.ProjectId); + Writer.Key("name"); + WriteJsonString(Writer, Project.Manifest.Name); + Writer.Key("slug"); + WriteJsonString(Writer, Project.Manifest.Slug); + Writer.Key("rootPath"); + WriteJsonString(Writer, Project.Root.RootPath.string()); + Writer.Key("contentDir"); + WriteJsonString(Writer, Project.Root.ContentDir.string()); + Writer.Key("scriptsDir"); + WriteJsonString(Writer, Project.ScriptWorkspace.ScriptsDir.string()); + Writer.Key("scriptProjectPath"); + WriteJsonString(Writer, Project.ScriptWorkspace.ScriptProjectPath.string()); + Writer.Key("scriptSolutionPath"); + WriteJsonString(Writer, Project.ScriptWorkspace.ScriptSolutionPath.string()); + Writer.Key("scriptAssemblyName"); + WriteJsonString(Writer, Project.ScriptWorkspace.AssemblyName); + Writer.Key("scriptRootNamespace"); + WriteJsonString(Writer, Project.ScriptWorkspace.RootNamespace); + Writer.Key("starterScriptPath"); + WriteJsonString(Writer, Project.ScriptWorkspace.StarterScriptPath.string()); + Writer.Key("starterScriptClassName"); + WriteJsonString(Writer, Project.ScriptWorkspace.StarterScriptClassName); + Writer.Key("starterScriptQualifiedClassName"); + WriteJsonString(Writer, + Project.ScriptWorkspace.StarterScriptQualifiedClassName); + Writer.Key("cookedDir"); + WriteJsonString(Writer, Project.Output.CookedDir.string()); + Writer.Key("cookManifestPath"); + WriteJsonString(Writer, Project.Output.CookManifestPath.string()); + Writer.Key("buildDir"); + WriteJsonString(Writer, Project.Output.BuildDir.string()); + Writer.Key("packageDir"); + WriteJsonString(Writer, Project.Output.PackageDir.string()); + Writer.Key("packagedContentDir"); + WriteJsonString(Writer, Project.Output.PackagedContentDir.string()); + Writer.Key("packagedCookedDir"); + WriteJsonString(Writer, Project.Output.PackagedCookedDir.string()); + Writer.Key("packagedSceneAssetPath"); + WriteJsonString(Writer, Project.Output.PackagedSceneAssetPath.string()); + Writer.Key("stagedRuntimeBinaryPath"); + WriteJsonString(Writer, Project.Output.StagedRuntimeBinaryPath.string()); + Writer.Key("packageManifestPath"); + WriteJsonString(Writer, Project.Output.PackageManifestPath.string()); + Writer.Key("engineContentDir"); + WriteJsonString( + Writer, (std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine").string()); + Writer.Key("sceneFilePath"); + WriteJsonString(Writer, Project.Root.SceneFilePath.string()); + Writer.EndObject(); +} + +std::string SerializeProjectList( + const std::vector &Projects, + const std::optional &ActiveProject) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("projects"); + Writer.Key("activeProjectSlug"); + if (ActiveProject.has_value()) { + WriteJsonString(Writer, ActiveProject->Manifest.Slug); + } else { + Writer.Null(); + } + Writer.Key("projects"); + Writer.StartArray(); + for (const auto &Project : Projects) { + WriteProjectJson(Writer, Project); + } + Writer.EndArray(); + Writer.EndObject(); + }); +} + +std::string SerializeCurrentProject( + const std::optional &ActiveProject) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("current_project"); + Writer.Key("project"); + if (ActiveProject.has_value()) { + WriteProjectJson(Writer, *ActiveProject); + } else { + Writer.Null(); + } + Writer.EndObject(); + }); +} + +std::string SerializeProjectCookResult( + const Project::ProjectDescriptor &Project, + const Project::ProjectCookResult &Result) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("project_cooked"); + Writer.Key("project"); + WriteProjectJson(Writer, Project); + Writer.Key("cookedSourceAssetCount"); + Writer.Uint64(Result.CookedSourceAssetCount); + Writer.Key("manifestEntryCount"); + Writer.Uint64(Result.ManifestEntryCount); + Writer.Key("cookManifestPath"); + WriteJsonString(Writer, Result.Output.CookManifestPath.string()); + Writer.EndObject(); + }); +} + +std::string SerializeProjectPackageResult( + const Project::ProjectDescriptor &Project, + const Project::ProjectPackageResult &Result) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("project_packaged"); + Writer.Key("project"); + WriteProjectJson(Writer, Project); + Writer.Key("cookedSourceAssetCount"); + Writer.Uint64(Result.Cook.CookedSourceAssetCount); + Writer.Key("manifestEntryCount"); + Writer.Uint64(Result.Cook.ManifestEntryCount); + Writer.Key("packagedFileCount"); + Writer.Uint64(Result.PackagedFileCount); + Writer.Key("includedSceneAsset"); + Writer.Bool(Result.IncludedSceneAsset); + Writer.Key("includedEngineContent"); + Writer.Bool(Result.IncludedEngineContent); + Writer.Key("includedRuntimeBinary"); + Writer.Bool(Result.IncludedRuntimeBinary); + Writer.Key("sceneAssetPath"); + WriteJsonString(Writer, Result.SceneAssetPath.string()); + Writer.Key("runtimeBinaryPath"); + WriteJsonString(Writer, Result.RuntimeBinaryPath.string()); + Writer.Key("packagedContentPath"); + WriteJsonString(Writer, Result.Cook.Output.PackagedContentDir.string()); + Writer.Key("packageDir"); + WriteJsonString(Writer, Result.Cook.Output.PackageDir.string()); + Writer.Key("packageManifestPath"); + WriteJsonString(Writer, Result.Cook.Output.PackageManifestPath.string()); + Writer.EndObject(); + }); +} + +bool IsValidScriptRelativePath(std::filesystem::path RelativePath) { + RelativePath = RelativePath.lexically_normal(); + if (RelativePath.empty() || RelativePath.is_absolute()) { + return false; + } + if (RelativePath.filename().empty() || RelativePath.extension() != ".cs") { + return false; + } + for (const auto &Part : RelativePath) { + const auto Token = Part.string(); + if (Token.empty() || Token == "." || Token == "..") { + return false; + } + } + return true; +} + +std::string SerializeScriptListJson(const std::vector &Files) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("scripts_list"); + Writer.Key("files"); + Writer.StartArray(); + for (const auto &File : Files) { + WriteJsonString(Writer, File); + } + Writer.EndArray(); + Writer.EndObject(); + }); +} + +std::string SerializeScriptFileJson(std::string_view RelativePath, + std::string_view Content) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("script_file"); + Writer.Key("path"); + WriteJsonString(Writer, RelativePath); + Writer.Key("content"); + WriteJsonString(Writer, Content); + Writer.EndObject(); + }); +} + +std::string SerializeScriptMutationJson(std::string_view MutationType, + std::string_view RelativePath) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + WriteJsonString(Writer, MutationType); + Writer.Key("path"); + WriteJsonString(Writer, RelativePath); + Writer.EndObject(); + }); +} + +std::string SerializeScriptClassesJson( + const std::vector> &Classes) { + return BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("script_classes"); + Writer.Key("classes"); + Writer.StartArray(); + for (const auto &Entry : Classes) { + Writer.StartObject(); + Writer.Key("className"); + WriteJsonString(Writer, Entry.first); + Writer.Key("path"); + WriteJsonString(Writer, Entry.second); + Writer.EndObject(); + } + Writer.EndArray(); + Writer.EndObject(); + }); +} + +std::vector MakeThumbnailJpeg(const std::filesystem::path &Path, + int MaxDim = 128) { + int W = 0, H = 0, Channels = 0; + stbi_uc *Pixels = + stbi_load(Path.string().c_str(), &W, &H, &Channels, STBI_rgb); + if (!Pixels || W <= 0 || H <= 0) { + return {}; + } + + int ThumbW = W; + int ThumbH = H; + if (W > MaxDim || H > MaxDim) { + if (W >= H) { + ThumbW = MaxDim; + ThumbH = std::max(1, H * MaxDim / W); + } else { + ThumbH = MaxDim; + ThumbW = std::max(1, W * MaxDim / H); + } + } + + std::vector Scaled(static_cast(ThumbW * ThumbH * 3)); + for (int Y = 0; Y < ThumbH; ++Y) { + for (int X = 0; X < ThumbW; ++X) { + const int SrcX = X * W / ThumbW; + const int SrcY = Y * H / ThumbH; + const stbi_uc *Src = Pixels + (SrcY * W + SrcX) * 3; + uint8_t *Dst = Scaled.data() + (Y * ThumbW + X) * 3; + Dst[0] = Src[0]; + Dst[1] = Src[1]; + Dst[2] = Src[2]; + } + } + stbi_image_free(Pixels); + + std::vector JpegBytes; + stbi_write_jpg_to_func( + [](void *Ctx, void *Data, int Size) { + auto *Out = static_cast *>(Ctx); + const uint8_t *Bytes = static_cast(Data); + Out->insert(Out->end(), Bytes, Bytes + Size); + }, + &JpegBytes, ThumbW, ThumbH, 3, Scaled.data(), 85); + return JpegBytes; +} +} // namespace diff --git a/Headless/RemoteViewportHttpRouterMutations.inl b/Headless/RemoteViewportHttpRouterMutations.inl new file mode 100644 index 00000000..38e5e4ef --- /dev/null +++ b/Headless/RemoteViewportHttpRouterMutations.inl @@ -0,0 +1,440 @@ +bool RemoteViewportHttpRouter::HandleCreateProjectRequest( + uintptr_t ClientSocketValue, std::string_view Body) { + const auto ProjectName = ExtractJsonStringField(Body, "name"); + if (!ProjectName.has_value() || ProjectName->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'name' field."); + return false; + } + std::string FailureReason; + const auto Created = Project::CreateProjectScaffold( + Project::GetDefaultProjectsRoot(), *ProjectName, &FailureReason); + if (!Created.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", FailureReason); + return false; + } + SetActiveProject(*Created); + FailureReason.clear(); + if (!LoadActiveProjectIntoSession(&FailureReason)) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", FailureReason); + return false; + } + SendJsonResponse(ClientSocketValue, "201 Created", + SerializeCurrentProject(Created)); + return false; +} + +bool RemoteViewportHttpRouter::HandleOpenProjectRequest( + uintptr_t ClientSocketValue, std::string_view Body) { + const auto ProjectSlug = ExtractJsonStringField(Body, "slug"); + if (!ProjectSlug.has_value() || ProjectSlug->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'slug' field."); + return false; + } + const auto Opened = SetActiveProjectBySlug(*ProjectSlug); + if (!Opened.has_value()) { + SendJsonError(ClientSocketValue, "404 Not Found", + "Project was not found in the managed projects directory."); + return false; + } + std::string FailureReason; + if (!LoadActiveProjectIntoSession(&FailureReason)) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", FailureReason); + return false; + } + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeCurrentProject(Opened)); + return false; +} + +bool RemoteViewportHttpRouter::HandleCookProjectRequest( + uintptr_t ClientSocketValue) { + const auto ActiveProject = GetActiveProject(); + if (!ActiveProject.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "No active project is selected."); + return false; + } + std::string FailureReason; + const auto Result = Project::CookProjectContent(*ActiveProject, &FailureReason); + if (!Result.has_value()) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", FailureReason); + return false; + } + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeProjectCookResult(*ActiveProject, *Result)); + return false; +} + +bool RemoteViewportHttpRouter::HandlePackageProjectRequest( + uintptr_t ClientSocketValue) { + const auto ActiveProject = GetActiveProject(); + if (!ActiveProject.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "No active project is selected."); + return false; + } + std::string FailureReason; + const auto Result = + Project::PackageProjectContent(*ActiveProject, &FailureReason); + if (!Result.has_value()) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", FailureReason); + return false; + } + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeProjectPackageResult(*ActiveProject, *Result)); + return false; +} + +bool RemoteViewportHttpRouter::HandleListScriptsRequest( + uintptr_t ClientSocketValue) { + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeScriptListJson(ListScriptFiles())); + return false; +} + +bool RemoteViewportHttpRouter::HandleListScriptClassesRequest( + uintptr_t ClientSocketValue) { + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeScriptClassesJson(ListScriptClasses())); + return false; +} + +bool RemoteViewportHttpRouter::HandleReadScriptFileRequest( + uintptr_t ClientSocketValue, std::string_view Path) { + const auto RelativePath = GetQueryParam(Path, "path"); + if (!RelativePath.has_value() || RelativePath->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'path' query parameter."); + return false; + } + const auto FilePath = ResolveActiveScriptPath(*RelativePath); + if (!FilePath.has_value() || !std::filesystem::exists(*FilePath)) { + SendJsonError(ClientSocketValue, "404 Not Found", + "Script file was not found."); + return false; + } + std::ifstream File(*FilePath); + if (!File.is_open()) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to open script file."); + return false; + } + const std::string Content((std::istreambuf_iterator(File)), + std::istreambuf_iterator()); + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeScriptFileJson(*RelativePath, Content)); + return false; +} + +bool RemoteViewportHttpRouter::HandleCreateScriptFileRequest( + uintptr_t ClientSocketValue, std::string_view Body) { + const auto RelativePath = ExtractJsonStringField(Body, "path"); + if (!RelativePath.has_value() || RelativePath->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'path' field."); + return false; + } + const auto FilePath = ResolveActiveScriptPath(*RelativePath, true); + if (!FilePath.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Script path must stay inside the active project's Scripts directory and end in .cs."); + return false; + } + if (std::filesystem::exists(*FilePath)) { + SendJsonError(ClientSocketValue, "409 Conflict", + "A script file with that path already exists."); + return false; + } + std::error_code Error; + std::filesystem::create_directories(FilePath->parent_path(), Error); + if (Error) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to create the script directory."); + return false; + } + std::ofstream File(*FilePath); + if (!File.is_open()) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to create script file."); + return false; + } + + const auto ActiveProject = GetActiveProject(); + const std::string Namespace = ActiveProject.has_value() + ? ActiveProject->ScriptWorkspace.RootNamespace + : "Project.Scripts"; + const std::string ClassName = FilePath->stem().string(); + std::ostringstream Template; + Template << "using WraithEngine;\n\n" + << "namespace " << Namespace << ";\n\n" + << "public class " << ClassName << " : Script\n" + << "{\n" + << " public override void OnCreate()\n" + << " {\n" + << " }\n\n" + << " public override void OnTick(float dt)\n" + << " {\n" + << " }\n" + << "}\n"; + File << Template.str(); + SendJsonResponse(ClientSocketValue, "201 Created", + SerializeScriptMutationJson("script_created", *RelativePath)); + return false; +} + +bool RemoteViewportHttpRouter::HandleSaveScriptFileRequest( + uintptr_t ClientSocketValue, std::string_view Body) { + const auto RelativePath = ExtractJsonStringField(Body, "path"); + const auto Content = ExtractJsonStringField(Body, "content"); + if (!RelativePath.has_value() || !Content.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'path' or 'content' field."); + return false; + } + const auto FilePath = ResolveActiveScriptPath(*RelativePath, true); + if (!FilePath.has_value()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Script path must stay inside the active project's Scripts directory and end in .cs."); + return false; + } + std::error_code Error; + std::filesystem::create_directories(FilePath->parent_path(), Error); + if (Error) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to create the script directory."); + return false; + } + std::ofstream File(*FilePath); + if (!File.is_open()) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to save script file."); + return false; + } + File << *Content; + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeScriptMutationJson("script_saved", *RelativePath)); + return false; +} + +bool RemoteViewportHttpRouter::HandleRenameScriptFileRequest( + uintptr_t ClientSocketValue, std::string_view Body) { + const auto RelativePath = ExtractJsonStringField(Body, "path"); + const auto NewRelativePath = ExtractJsonStringField(Body, "newPath"); + if (!RelativePath.has_value() || !NewRelativePath.has_value() || + RelativePath->empty() || NewRelativePath->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'path' or 'newPath' field."); + return false; + } + const auto OldPath = ResolveActiveScriptPath(*RelativePath); + const auto NewPath = ResolveActiveScriptPath(*NewRelativePath, true); + if (!OldPath.has_value() || !NewPath.has_value() || + !std::filesystem::exists(*OldPath)) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Script rename must stay inside the active project's Scripts directory and target an existing .cs file."); + return false; + } + if (std::filesystem::exists(*NewPath)) { + SendJsonError(ClientSocketValue, "409 Conflict", + "A script file with the destination path already exists."); + return false; + } + std::error_code Error; + std::filesystem::create_directories(NewPath->parent_path(), Error); + if (Error) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to create the destination script directory."); + return false; + } + std::filesystem::rename(*OldPath, *NewPath, Error); + if (Error) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to rename script file."); + return false; + } + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeScriptMutationJson("script_renamed", + *NewRelativePath)); + return false; +} + +bool RemoteViewportHttpRouter::HandleDeleteScriptFileRequest( + uintptr_t ClientSocketValue, std::string_view Body) { + const auto RelativePath = ExtractJsonStringField(Body, "path"); + if (!RelativePath.has_value() || RelativePath->empty()) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Missing required 'path' field."); + return false; + } + const auto FilePath = ResolveActiveScriptPath(*RelativePath); + if (!FilePath.has_value() || !std::filesystem::exists(*FilePath)) { + SendJsonError(ClientSocketValue, "404 Not Found", + "Script file was not found."); + return false; + } + std::error_code Error; + const bool Removed = std::filesystem::remove(*FilePath, Error); + if (Error || !Removed) { + SendJsonError(ClientSocketValue, "500 Internal Server Error", + "Failed to delete script file."); + return false; + } + SendJsonResponse(ClientSocketValue, "200 OK", + SerializeScriptMutationJson("script_deleted", *RelativePath)); + return false; +} + +bool RemoteViewportHttpRouter::HandleAssetUploadRequest( + uintptr_t ClientSocketValue, std::string_view Path, + std::string_view HeaderBlock, std::string_view Body) { + const auto ContentType = FindHeaderValue(HeaderBlock, "Content-Type"); + if (!ContentType.has_value() || + ContentType->find("multipart/form-data") == std::string::npos) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Expected multipart/form-data Content-Type."); + return false; + } + const auto BoundaryPos = ContentType->find("boundary="); + if (BoundaryPos == std::string::npos) { + SendJsonError(ClientSocketValue, "400 Bad Request", "Missing boundary."); + return false; + } + const std::string Boundary = "--" + ContentType->substr(BoundaryPos + 9); + const std::string TargetDir = GetQueryParam(Path, "dir").value_or(""); + const auto ContentRoot = GetActiveContentDir(); + + std::filesystem::path DestDir; + if (TargetDir.empty()) { + DestDir = ContentRoot; + } else { + const std::filesystem::path TargetDirPath{TargetDir}; + bool Unsafe = false; + for (const auto &Part : TargetDirPath) { + if (Part.string().find("..") != std::string::npos) { + Unsafe = true; + break; + } + } + if (Unsafe) { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Invalid target directory."); + return false; + } + if (TargetDirPath.begin() != TargetDirPath.end() && + (*TargetDirPath.begin()).string() == "Engine") { + SendJsonError(ClientSocketValue, "400 Bad Request", + "Engine content is read-only and cannot be modified by project uploads."); + return false; + } + DestDir = ContentRoot / TargetDirPath; + } + + static constexpr std::string_view kContentDisposition = "Content-Disposition:"; + std::vector Saved; + std::string_view Remaining{Body}; + while (true) { + const auto BPos = Remaining.find(Boundary); + if (BPos == std::string_view::npos) { + break; + } + Remaining.remove_prefix(BPos + Boundary.size()); + if (Remaining.starts_with("--")) { + break; + } + if (Remaining.starts_with("\r\n")) { + Remaining.remove_prefix(2); + } + const auto HeaderEnd = Remaining.find("\r\n\r\n"); + if (HeaderEnd == std::string_view::npos) { + break; + } + const std::string_view PartHeaders = Remaining.substr(0, HeaderEnd); + Remaining.remove_prefix(HeaderEnd + 4); + + const auto CDPos = PartHeaders.find(kContentDisposition); + if (CDPos == std::string_view::npos) { + continue; + } + const auto FnPos = PartHeaders.find("filename=\"", CDPos); + if (FnPos == std::string_view::npos) { + continue; + } + const auto FnStart = FnPos + 10; + const auto FnEnd = PartHeaders.find('"', FnStart); + if (FnEnd == std::string_view::npos) { + continue; + } + const std::string Filename{PartHeaders.substr(FnStart, FnEnd - FnStart)}; + if (Filename.empty()) { + continue; + } + + const auto BodyEnd = Remaining.find(Boundary); + if (BodyEnd == std::string_view::npos) { + break; + } + const size_t PartBodyLen = BodyEnd >= 2 ? BodyEnd - 2 : BodyEnd; + const std::string_view PartBody = Remaining.substr(0, PartBodyLen); + + const std::filesystem::path FilePath{Filename}; + const std::string Ext = [&] { + auto E = FilePath.extension().string(); + for (auto &C : E) { + C = static_cast(std::tolower(static_cast(C))); + } + return E; + }(); + static constexpr std::string_view kAllowed[] = { + ".glb", ".gltf", ".fbx", ".obj", ".png", ".jpg", ".jpeg", ".hdr"}; + bool Allowed = false; + for (const auto &A : kAllowed) { + if (Ext == A) { + Allowed = true; + break; + } + } + if (!Allowed) { + std::cerr << "[AssetUpload] rejected '" << Filename + << "': unsupported extension\n"; + continue; + } + + std::error_code Ec; + std::filesystem::create_directories(DestDir, Ec); + const std::filesystem::path OutPath = DestDir / FilePath.filename(); + std::ofstream OutFile(OutPath, std::ios::binary); + if (!OutFile.is_open()) { + std::cerr << "[AssetUpload] could not open '" << OutPath.string() + << "' for writing\n"; + continue; + } + OutFile.write(PartBody.data(), static_cast(PartBody.size())); + OutFile.close(); + std::cerr << "[AssetUpload] saved '" << OutPath.string() << "'\n"; + + const auto Rel = std::filesystem::relative(OutPath, ContentRoot, Ec); + if (!Ec) { + Saved.push_back(Rel.string()); + } + } + + m_Server.m_WebSocketDispatch->BroadcastTextMessage( + SerializeAssetList(CollectVisibleAssets())); + + const std::string Payload = BuildJson([&](JsonWriter &Writer) { + Writer.StartObject(); + Writer.Key("type"); + Writer.String("assets_uploaded"); + Writer.Key("files"); + Writer.StartArray(); + for (const auto &SavedPath : Saved) { + WriteJsonString(Writer, SavedPath); + } + Writer.EndArray(); + Writer.EndObject(); + }); + SendJsonResponse(ClientSocketValue, "200 OK", Payload); + return false; +} diff --git a/Headless/RemoteViewportPresence.cpp b/Headless/RemoteViewportPresence.cpp new file mode 100644 index 00000000..4fb7e0af --- /dev/null +++ b/Headless/RemoteViewportPresence.cpp @@ -0,0 +1,61 @@ +#include "RemoteViewportPresence.h" + +#include "RemoteViewportServer.h" +#include "RemoteViewportWebRtcSessionManager.h" + +#include + +#include +#include +#include + +namespace Axiom { +namespace { +constexpr int AwayThresholdSeconds = 10; +constexpr int DisconnectThresholdSeconds = 30; +constexpr int PresenceCheckIntervalMs = 2000; +} // namespace + +RemoteViewportPresence::RemoteViewportPresence(RemoteViewportServer &Server) + : m_Server(Server) {} + +void RemoteViewportPresence::RunLoop() { + while (!m_Server.m_StopRequested.load()) { + std::this_thread::sleep_for( + std::chrono::milliseconds(PresenceCheckIntervalMs)); + if (m_Server.m_StopRequested.load()) { + break; + } + + const auto Now = std::chrono::steady_clock::now(); + std::vector> Transitions; + for (const auto &[User, LastActivity] : + m_Server.m_WebRtcSessions->CollectPresenceEntries()) { + const auto Elapsed = + std::chrono::duration_cast(Now - LastActivity) + .count(); + const EditorUserPresence *Presence = + m_Server.m_Host.GetSessionModule().GetSession().FindPresence(User); + if (Presence == nullptr) { + continue; + } + if (Elapsed >= DisconnectThresholdSeconds && + Presence->State == EditorUserPresenceState::Away) { + Transitions.emplace_back(User, EditorUserPresenceState::Disconnected); + } else if (Elapsed >= AwayThresholdSeconds && + Presence->State == EditorUserPresenceState::Connected) { + Transitions.emplace_back(User, EditorUserPresenceState::Away); + } + } + + for (const auto &[User, State] : Transitions) { + m_Server.m_Host.GetSessionModule().GetSession().SetPresenceState(User, + State); + if (State == EditorUserPresenceState::Disconnected) { + m_Server.m_Host.GetSessionModule().GetSession().ReleaseAllLocksForUser( + User); + } + } + } +} +} // namespace Axiom diff --git a/Headless/RemoteViewportPresence.h b/Headless/RemoteViewportPresence.h new file mode 100644 index 00000000..109a3b45 --- /dev/null +++ b/Headless/RemoteViewportPresence.h @@ -0,0 +1,15 @@ +#pragma once + +namespace Axiom { +class RemoteViewportServer; + +class RemoteViewportPresence { +public: + explicit RemoteViewportPresence(RemoteViewportServer &Server); + + void RunLoop(); + +private: + RemoteViewportServer &m_Server; +}; +} // namespace Axiom diff --git a/Headless/RemoteViewportServer.cpp b/Headless/RemoteViewportServer.cpp index f076f0fa..b8c901d3 100644 --- a/Headless/RemoteViewportServer.cpp +++ b/Headless/RemoteViewportServer.cpp @@ -1,969 +1,254 @@ #include "RemoteViewportServer.h" -#include -#include +#include "HeadlessCommandProtocol.h" +#include "RemoteViewportGizmoController.h" +#include "RemoteViewportGridSnap.h" +#include "RemoteViewportHttpRouter.h" +#include "RemoteViewportPresence.h" +#include "RemoteViewportWebRtcSessionManager.h" +#include "RemoteViewportWebSocketDispatch.h" -#ifndef AXIOM_CONTENT_DIR -#define AXIOM_CONTENT_DIR "Content" -#endif +#include +#include -#include "GizmoHitTest.h" -#include "HeadlessCommandProtocol.h" -#include -#include -#include -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include -#include -#include #include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include +#include #include -#include - -#if AXIOM_PLATFORM_WINDOWS -# ifndef NOMINMAX -# define NOMINMAX -# endif -#include -#include -#include -#else -#include -#include -#include -#include -#include -#endif namespace Axiom { -namespace { -constexpr std::string_view WebSocketGuid = - "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; -constexpr std::string_view ClientIdHeaderName = "X-Axiom-Client-Id"; - -#if AXIOM_PLATFORM_WINDOWS -using SocketHandle = SOCKET; -constexpr SocketHandle InvalidSocket = INVALID_SOCKET; - -class WinsockRuntime final { -public: - WinsockRuntime() { - static std::once_flag Flag; - std::call_once(Flag, []() { - WSADATA Data{}; - WSAStartup(MAKEWORD(2, 2), &Data); - }); - } +struct RemoteViewportServerUwsState { + std::mutex StartupMutex; + std::condition_variable StartupCondition; + bool StartupCompleted{false}; + std::string StartupError; + uWS::Loop *Loop{nullptr}; + std::unique_ptr App; + us_listen_socket_t *ListenSocket{nullptr}; }; -#else -using SocketHandle = int; -constexpr SocketHandle InvalidSocket = -1; -#endif - -SocketHandle ToSocket(uintptr_t Value) { - return static_cast(Value); -} - -uintptr_t ToValue(SocketHandle Socket) { return static_cast(Socket); } - -void CloseSocket(SocketHandle Socket) { - if (Socket != InvalidSocket) { -#if AXIOM_PLATFORM_WINDOWS - closesocket(Socket); -#else - close(Socket); -#endif - } -} - -void SetReuseAddress(SocketHandle Socket) { - constexpr int Reuse = 1; -#if AXIOM_PLATFORM_WINDOWS - setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR, - reinterpret_cast(&Reuse), sizeof(Reuse)); -#else - setsockopt(Socket, SOL_SOCKET, SO_REUSEADDR, &Reuse, sizeof(Reuse)); -#endif -} -struct HttpRequest { - std::string Method; - std::string Path; - std::string HeaderBlock; - std::string Body; - size_t ContentLength{0}; +namespace { +struct RemoteViewportWebSocketUserData { + uintptr_t ConnectionId{0}; }; -std::string BuildHttpResponse(std::string_view Status, - std::string_view ContentType, - std::string_view Body, - std::string_view ExtraHeaders = {}) { +using UwsHttpRequest = uWS::HttpRequest; +using UwsHttpResponse = uWS::HttpResponse; +using UwsWebSocket = + uWS::WebSocket; + +std::string BuildHeaderBlock(std::string_view Method, UwsHttpRequest &Request) { std::ostringstream Stream; - Stream << "HTTP/1.1 " << Status << "\r\n" - << "Content-Type: " << ContentType << "\r\n" - << "Content-Length: " << Body.size() << "\r\n" - << "Cache-Control: no-store\r\n" - << "Connection: close\r\n" - << "Access-Control-Allow-Origin: *\r\n"; - if (!ExtraHeaders.empty()) { - Stream << ExtraHeaders; + Stream << Method << ' ' << Request.getFullUrl() << " HTTP/1.1\r\n"; + for (const auto &[Key, Value] : Request) { + Stream << Key << ": " << Value << "\r\n"; } - Stream << "\r\n" << Body; + Stream << "\r\n"; return Stream.str(); } +} // namespace -bool SendAll(SocketHandle Socket, const void *Data, size_t Size) { - const auto *Bytes = static_cast(Data); - size_t Offset = 0; - while (Offset < Size) { -#if AXIOM_PLATFORM_WINDOWS - const int Sent = - send(Socket, Bytes + Offset, static_cast(Size - Offset), 0); - if (Sent == SOCKET_ERROR || Sent == 0) { -#else - const ssize_t Sent = send(Socket, Bytes + Offset, Size - Offset, 0); - if (Sent <= 0) { -#endif - return false; - } - Offset += static_cast(Sent); - } - return true; -} - -bool RecvExact(SocketHandle Socket, void *Data, size_t Size) { - auto *Bytes = static_cast(Data); - size_t Offset = 0; - while (Offset < Size) { -#if AXIOM_PLATFORM_WINDOWS - const int Received = - recv(Socket, reinterpret_cast(Bytes + Offset), - static_cast(Size - Offset), 0); -#else - const ssize_t Received = recv( - Socket, reinterpret_cast(Bytes + Offset), Size - Offset, 0); -#endif - if (Received <= 0) { - return false; - } - Offset += static_cast(Received); - } - return true; -} - -bool RecvChunk(SocketHandle Socket, char *Data, size_t Size, size_t &ReceivedOut) { -#if AXIOM_PLATFORM_WINDOWS - const int Received = recv(Socket, Data, static_cast(Size), 0); -#else - const ssize_t Received = recv(Socket, Data, Size, 0); -#endif - if (Received <= 0) { - ReceivedOut = 0; - return false; - } - - ReceivedOut = static_cast(Received); - return true; -} - -std::string_view StripQuery(std::string_view Path) { - const size_t Query = Path.find('?'); - return Query == std::string_view::npos ? Path : Path.substr(0, Query); -} - -std::string UrlDecode(std::string_view Input) { - std::string Out; - Out.reserve(Input.size()); - for (size_t I = 0; I < Input.size(); ++I) { - if (Input[I] == '%' && I + 2 < Input.size()) { - const char Hi = Input[I + 1]; - const char Lo = Input[I + 2]; - auto HexVal = [](char C) -> int { - if (C >= '0' && C <= '9') return C - '0'; - if (C >= 'a' && C <= 'f') return C - 'a' + 10; - if (C >= 'A' && C <= 'F') return C - 'A' + 10; - return -1; - }; - const int H = HexVal(Hi), L = HexVal(Lo); - if (H >= 0 && L >= 0) { - Out += static_cast(H * 16 + L); - I += 2; - continue; - } - } else if (Input[I] == '+') { - Out += ' '; - continue; - } - Out += Input[I]; - } - return Out; -} - -// Returns the URL-decoded value of a query parameter, or nullopt if not present. -std::optional GetQueryParam(std::string_view Path, - std::string_view Key) { - const size_t Q = Path.find('?'); - if (Q == std::string_view::npos) return std::nullopt; - std::string_view Query = Path.substr(Q + 1); - while (!Query.empty()) { - const size_t Amp = Query.find('&'); - std::string_view Pair = Amp == std::string_view::npos ? Query : Query.substr(0, Amp); - const size_t Eq = Pair.find('='); - if (Eq != std::string_view::npos && Pair.substr(0, Eq) == Key) { - return UrlDecode(Pair.substr(Eq + 1)); - } - if (Amp == std::string_view::npos) break; - Query.remove_prefix(Amp + 1); - } - return std::nullopt; -} - -constexpr float kMinimumScale = 0.001f; - -float SnapToStep(float value, float step) { - if (step <= 0.0f) { - return value; - } - return std::round(value / step) * step; -} - -void ApplyGridSnap(bool enabled, float translationStep, - float rotationStepDegrees, float scaleStep, GizmoMode mode, int axis, - glm::vec3 &location, glm::vec3 &rotationDegrees, - glm::vec3 &scale) { - if (!enabled || axis < 0 || axis > 2) { - return; - } - - switch (mode) { - case GizmoMode::Translate: - location[axis] = SnapToStep(location[axis], translationStep); - break; - case GizmoMode::Rotate: - rotationDegrees[axis] = SnapToStep(rotationDegrees[axis], rotationStepDegrees); - break; - case GizmoMode::Scale: - scale[axis] = std::max(kMinimumScale, SnapToStep(scale[axis], scaleStep)); - break; - } -} - -std::string Trim(std::string_view Value) { - while (!Value.empty() && - std::isspace(static_cast(Value.front())) != 0) { - Value.remove_prefix(1); - } - while (!Value.empty() && - std::isspace(static_cast(Value.back())) != 0) { - Value.remove_suffix(1); - } - return std::string(Value); -} - -bool EqualsCaseInsensitive(std::string_view Left, std::string_view Right) { - if (Left.size() != Right.size()) { - return false; - } - - for (size_t Index = 0; Index < Left.size(); ++Index) { - if (std::tolower(static_cast(Left[Index])) != - std::tolower(static_cast(Right[Index]))) { - return false; - } - } - return true; -} - -std::optional ParseContentLength(std::string_view Headers) { - const std::string_view Key = "Content-Length:"; - size_t Position = Headers.find(Key); - if (Position == std::string_view::npos) { - return 0u; - } - - Position += Key.size(); - const size_t End = Headers.find("\r\n", Position); - const std::string Value = - Trim(Headers.substr(Position, End == std::string_view::npos - ? std::string_view::npos - : End - Position)); - size_t Result = 0; - const auto [Ptr, Ec] = - std::from_chars(Value.data(), Value.data() + Value.size(), Result); - if (Ec != std::errc{} || Ptr != Value.data() + Value.size()) { - return std::nullopt; - } - return Result; -} - -std::optional FindHeaderValue(std::string_view HeaderBlock, - std::string_view HeaderName) { - size_t LineStart = 0; - while (LineStart < HeaderBlock.size()) { - const size_t LineEnd = HeaderBlock.find("\r\n", LineStart); - const std::string_view Line = - HeaderBlock.substr(LineStart, LineEnd == std::string_view::npos - ? std::string_view::npos - : LineEnd - LineStart); - const size_t Colon = Line.find(':'); - if (Colon != std::string_view::npos && - EqualsCaseInsensitive(Trim(Line.substr(0, Colon)), HeaderName)) { - return Trim(Line.substr(Colon + 1)); - } - - if (LineEnd == std::string_view::npos) { - break; - } - LineStart = LineEnd + 2; - } - return std::nullopt; +RemoteViewportServer::RemoteViewportServer( + HeadlessSessionHost &Host, const RemoteViewportServerOptions &Options) + : m_Host(Host), m_Options(Options) { + m_GridSnap = std::make_unique(); + m_GizmoController = std::make_unique(*this); + m_HttpRouter = std::make_unique(*this); + m_Presence = std::make_unique(*this); + m_WebRtcSessions = std::make_unique(*this); + m_WebSocketDispatch = + std::make_unique(*this); + m_Host.SetTransportVideoEncoder(nullptr); } -bool ReadHttpRequest(SocketHandle Socket, HttpRequest &Request, - std::string &Error) { - std::string Buffer; - std::array Chunk{}; - size_t HeaderEnd = std::string::npos; - - while (HeaderEnd == std::string::npos) { - size_t Received = 0; - if (!RecvChunk(Socket, Chunk.data(), Chunk.size(), Received)) { - Error = "Failed to read HTTP request headers."; - return false; - } - Buffer.append(Chunk.data(), Received); - HeaderEnd = Buffer.find("\r\n\r\n"); - } - - const std::string_view HeaderView(Buffer.data(), HeaderEnd + 4); - const size_t RequestLineEnd = HeaderView.find("\r\n"); - if (RequestLineEnd == std::string_view::npos) { - Error = "Malformed HTTP request line."; - return false; - } - - const std::string_view RequestLine = HeaderView.substr(0, RequestLineEnd); - const size_t MethodEnd = RequestLine.find(' '); - const size_t PathEnd = - MethodEnd == std::string_view::npos ? std::string_view::npos - : RequestLine.find(' ', MethodEnd + 1); - if (MethodEnd == std::string_view::npos || PathEnd == std::string_view::npos) { - Error = "Malformed HTTP request line."; - return false; - } - - Request.Method = std::string(RequestLine.substr(0, MethodEnd)); - Request.Path = - std::string(RequestLine.substr(MethodEnd + 1, PathEnd - MethodEnd - 1)); - Request.HeaderBlock = std::string(HeaderView); - - const auto ContentLength = ParseContentLength(HeaderView); - if (!ContentLength.has_value()) { - Error = "Invalid Content-Length header."; - return false; - } - Request.ContentLength = *ContentLength; - - const size_t BodyOffset = HeaderEnd + 4; - while (Buffer.size() < BodyOffset + Request.ContentLength) { - size_t Received = 0; - if (!RecvChunk(Socket, Chunk.data(), Chunk.size(), Received)) { - Error = "Failed to read HTTP request body."; - return false; - } - Buffer.append(Chunk.data(), Received); - } +RemoteViewportServer::~RemoteViewportServer() { Stop(); } - Request.Body.assign(Buffer.data() + BodyOffset, Request.ContentLength); - return true; +RemoteViewportServerMetrics RemoteViewportServer::GetMetrics() const { + RemoteViewportServerMetrics Metrics{}; + Metrics.TransportConnected = m_TransportConnected.load(); + Metrics.ListenPort = m_Options.Port; + Metrics.ActiveWebSocketClients = m_WebSocketDispatch->GetActiveClientCount(); + Metrics.ActiveRemoteClients = m_WebRtcSessions->GetRemoteClientCount(); + Metrics.ActiveWebRtcSessions = m_WebRtcSessions->GetActiveWebRtcSessionCount(); + Metrics.TotalHttpRequests = m_HttpRouter->GetTotalHttpRequests(); + Metrics.TotalWebSocketMessages = + m_WebSocketDispatch->GetTotalWebSocketMessages(); + return Metrics; } -std::string JsonResponse(std::string_view Status, std::string_view Payload) { - return BuildHttpResponse(Status, "application/json; charset=utf-8", Payload); -} +bool RemoteViewportServer::Start(std::string &Error) { + m_StopRequested.store(false); + m_UwsState = std::make_unique(); + m_Host.GetTransport().Connect(this); + m_ServerThread = std::thread([this]() { + RemoteViewportServerUwsState *State = m_UwsState.get(); + if (State == nullptr) { + return; + } + + State->Loop = uWS::Loop::get(); + State->App = std::make_unique(); + + auto RegisterGetHandler = [this](UwsHttpResponse *Response, + UwsHttpRequest *Request) { + const uintptr_t ConnectionId = m_HttpRouter->AllocateConnectionId(); + m_HttpRouter->RegisterPendingResponse(ConnectionId, Response); + Response->onAborted([this, ConnectionId]() { + m_HttpRouter->MarkPendingResponseAborted(ConnectionId); + }); + + const std::string HeaderBlock = BuildHeaderBlock("GET", *Request); + m_HttpRouter->IncrementRequestCount(); + m_HttpRouter->HandleGetRequest(ConnectionId, + std::string(Request->getFullUrl()), + HeaderBlock); + }; -std::optional ExtractJsonStringField(std::string_view Body, - std::string_view FieldName) { - const std::string Needle = "\"" + std::string(FieldName) + "\""; - const size_t KeyPos = Body.find(Needle); - if (KeyPos == std::string_view::npos) { - return std::nullopt; - } + auto RegisterOptionsHandler = [this](UwsHttpResponse *Response, + UwsHttpRequest *Request) { + (void)Request; + const uintptr_t ConnectionId = m_HttpRouter->AllocateConnectionId(); + m_HttpRouter->RegisterPendingResponse(ConnectionId, Response); + Response->onAborted([this, ConnectionId]() { + m_HttpRouter->MarkPendingResponseAborted(ConnectionId); + }); + + m_HttpRouter->IncrementRequestCount(); + m_HttpRouter->SendHttpResponse( + ConnectionId, + "HTTP/1.1 204 No Content\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" + "Content-Length: 0\r\n" + "Cache-Control: no-store\r\n" + "Connection: close\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type, X-Axiom-Client-Id\r\n" + "\r\n"); + }; - const size_t ColonPos = Body.find(':', KeyPos + Needle.size()); - if (ColonPos == std::string_view::npos) { - return std::nullopt; - } + auto RegisterPostHandler = [this](UwsHttpResponse *Response, + UwsHttpRequest *Request) { + const uintptr_t ConnectionId = m_HttpRouter->AllocateConnectionId(); + m_HttpRouter->RegisterPendingResponse(ConnectionId, Response); + Response->onAborted([this, ConnectionId]() { + m_HttpRouter->MarkPendingResponseAborted(ConnectionId); + }); + + auto HeaderBlock = + std::make_shared(BuildHeaderBlock("POST", *Request)); + auto Path = std::make_shared(Request->getFullUrl()); + auto Body = std::make_shared(); + m_HttpRouter->IncrementRequestCount(); + + const std::string_view ContentLength = Request->getHeader("content-length"); + if (ContentLength.empty() || ContentLength == "0") { + m_HttpRouter->HandlePostRequest(ConnectionId, *Path, *HeaderBlock, ""); + return; + } - size_t ValuePos = ColonPos + 1; - while (ValuePos < Body.size() && - std::isspace(static_cast(Body[ValuePos])) != 0) { - ++ValuePos; - } - if (ValuePos >= Body.size() || Body[ValuePos] != '"') { - return std::nullopt; - } - ++ValuePos; + Response->onData([this, ConnectionId, HeaderBlock, Path, Body]( + std::string_view Chunk, bool IsLast) { + Body->append(Chunk.data(), Chunk.size()); + if (IsLast) { + m_HttpRouter->HandlePostRequest(ConnectionId, *Path, *HeaderBlock, + *Body); + } + }); + }; - std::string Result; - while (ValuePos < Body.size()) { - const char Character = Body[ValuePos++]; - if (Character == '"') { - return Result; - } - if (Character == '\\') { - if (ValuePos >= Body.size()) { - return std::nullopt; + uWS::App::WebSocketBehavior Behavior{}; + Behavior.compression = uWS::DISABLED; + Behavior.maxPayloadLength = 256 * 1024; + Behavior.upgrade = + [this](UwsHttpResponse *Response, UwsHttpRequest *Request, + us_socket_context_t *Context) { + const uintptr_t ConnectionId = m_HttpRouter->AllocateConnectionId(); + Response->template upgrade( + {.ConnectionId = ConnectionId}, + Request->getHeader("sec-websocket-key"), + Request->getHeader("sec-websocket-protocol"), + Request->getHeader("sec-websocket-extensions"), Context); + }; + Behavior.open = [this](UwsWebSocket *Socket) { + const uintptr_t ConnectionId = Socket->getUserData()->ConnectionId; + m_WebSocketDispatch->OnClientOpen(ConnectionId, Socket); + }; + Behavior.message = [this](UwsWebSocket *Socket, std::string_view Message, + uWS::OpCode OpCode) { + if (OpCode != uWS::OpCode::TEXT) { + return; } - const char Escaped = Body[ValuePos++]; - switch (Escaped) { - case '\\': - case '"': - case '/': - Result.push_back(Escaped); - break; - case 'n': - Result.push_back('\n'); - break; - case 'r': - Result.push_back('\r'); - break; - case 't': - Result.push_back('\t'); - break; - default: - return std::nullopt; + const uintptr_t ConnectionId = Socket->getUserData()->ConnectionId; + if (!m_WebSocketDispatch->HandleWebSocketMessage(ConnectionId, Message)) { + m_WebSocketDispatch->SendTextMessage( + ConnectionId, + SerializeError("Invalid WebSocket command payload.")); } - continue; - } - Result.push_back(Character); - } - - return std::nullopt; -} - -std::string EscapeJsonString(std::string_view Value) { - std::string Result; - Result.reserve(Value.size() + 4); - for (const char Character : Value) { - switch (Character) { - case '\\': - Result += "\\\\"; - break; - case '"': - Result += "\\\""; - break; - case '\n': - Result += "\\n"; - break; - case '\r': - Result += "\\r"; - break; - case '\t': - Result += "\\t"; - break; - default: - Result.push_back(Character); - break; - } - } - return Result; -} - -std::string SerializeProjectJson(const Project::ProjectDescriptor &Project) { - std::ostringstream Stream; - Stream << "{" - << "\"projectId\":\"" << EscapeJsonString(Project.Manifest.ProjectId) - << "\",\"name\":\"" << EscapeJsonString(Project.Manifest.Name) - << "\",\"slug\":\"" << EscapeJsonString(Project.Manifest.Slug) - << "\",\"rootPath\":\"" - << EscapeJsonString(Project.Root.RootPath.string()) - << "\",\"contentDir\":\"" - << EscapeJsonString(Project.Root.ContentDir.string()) - << "\",\"scriptsDir\":\"" - << EscapeJsonString(Project.ScriptWorkspace.ScriptsDir.string()) - << "\",\"scriptProjectPath\":\"" - << EscapeJsonString(Project.ScriptWorkspace.ScriptProjectPath.string()) - << "\",\"scriptSolutionPath\":\"" - << EscapeJsonString(Project.ScriptWorkspace.ScriptSolutionPath.string()) - << "\",\"scriptAssemblyName\":\"" - << EscapeJsonString(Project.ScriptWorkspace.AssemblyName) - << "\",\"scriptRootNamespace\":\"" - << EscapeJsonString(Project.ScriptWorkspace.RootNamespace) - << "\",\"starterScriptPath\":\"" - << EscapeJsonString(Project.ScriptWorkspace.StarterScriptPath.string()) - << "\",\"starterScriptClassName\":\"" - << EscapeJsonString(Project.ScriptWorkspace.StarterScriptClassName) - << "\",\"starterScriptQualifiedClassName\":\"" - << EscapeJsonString( - Project.ScriptWorkspace.StarterScriptQualifiedClassName) - << "\",\"cookedDir\":\"" - << EscapeJsonString(Project.Output.CookedDir.string()) - << "\",\"cookManifestPath\":\"" - << EscapeJsonString(Project.Output.CookManifestPath.string()) - << "\",\"buildDir\":\"" - << EscapeJsonString(Project.Output.BuildDir.string()) - << "\",\"packageDir\":\"" - << EscapeJsonString(Project.Output.PackageDir.string()) - << "\",\"packagedContentDir\":\"" - << EscapeJsonString(Project.Output.PackagedContentDir.string()) - << "\",\"packagedCookedDir\":\"" - << EscapeJsonString(Project.Output.PackagedCookedDir.string()) - << "\",\"packagedSceneAssetPath\":\"" - << EscapeJsonString(Project.Output.PackagedSceneAssetPath.string()) - << "\",\"stagedRuntimeBinaryPath\":\"" - << EscapeJsonString(Project.Output.StagedRuntimeBinaryPath.string()) - << "\",\"packageManifestPath\":\"" - << EscapeJsonString(Project.Output.PackageManifestPath.string()) - << "\",\"engineContentDir\":\"" - << EscapeJsonString((std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine").string()) - << "\",\"sceneFilePath\":\"" - << EscapeJsonString(Project.Root.SceneFilePath.string()) - << "\"}"; - return Stream.str(); -} - -std::string SerializeProjectList( - const std::vector &Projects, - const std::optional &ActiveProject) { - std::ostringstream Stream; - Stream << "{\"type\":\"projects\",\"activeProjectSlug\":"; - if (ActiveProject.has_value()) { - Stream << "\"" << EscapeJsonString(ActiveProject->Manifest.Slug) << "\""; - } else { - Stream << "null"; - } - Stream << ",\"projects\":["; - for (size_t Index = 0; Index < Projects.size(); ++Index) { - if (Index > 0) { - Stream << ","; - } - Stream << SerializeProjectJson(Projects[Index]); - } - Stream << "]}"; - return Stream.str(); -} - -std::string SerializeCurrentProject( - const std::optional &ActiveProject) { - std::ostringstream Stream; - Stream << "{\"type\":\"current_project\",\"project\":"; - if (ActiveProject.has_value()) { - Stream << SerializeProjectJson(*ActiveProject); - } else { - Stream << "null"; - } - Stream << "}"; - return Stream.str(); -} - -std::string SerializeProjectCookResult( - const Project::ProjectDescriptor &Project, - const Project::ProjectCookResult &Result) { - std::ostringstream Stream; - Stream << "{" - << "\"type\":\"project_cooked\"" - << ",\"project\":" << SerializeProjectJson(Project) - << ",\"cookedSourceAssetCount\":" << Result.CookedSourceAssetCount - << ",\"manifestEntryCount\":" << Result.ManifestEntryCount - << ",\"cookManifestPath\":\"" - << EscapeJsonString(Result.Output.CookManifestPath.string()) - << "\"}"; - return Stream.str(); -} - -std::string SerializeProjectPackageResult( - const Project::ProjectDescriptor &Project, - const Project::ProjectPackageResult &Result) { - std::ostringstream Stream; - Stream << "{" - << "\"type\":\"project_packaged\"" - << ",\"project\":" << SerializeProjectJson(Project) - << ",\"cookedSourceAssetCount\":" << Result.Cook.CookedSourceAssetCount - << ",\"manifestEntryCount\":" << Result.Cook.ManifestEntryCount - << ",\"packagedFileCount\":" << Result.PackagedFileCount - << ",\"includedSceneAsset\":" - << (Result.IncludedSceneAsset ? "true" : "false") - << ",\"includedEngineContent\":" - << (Result.IncludedEngineContent ? "true" : "false") - << ",\"includedRuntimeBinary\":" - << (Result.IncludedRuntimeBinary ? "true" : "false") - << ",\"sceneAssetPath\":\"" - << EscapeJsonString(Result.SceneAssetPath.string()) - << "\",\"runtimeBinaryPath\":\"" - << EscapeJsonString(Result.RuntimeBinaryPath.string()) - << "\"" - << ",\"packagedContentPath\":\"" - << EscapeJsonString(Result.Cook.Output.PackagedContentDir.string()) - << "\"" - << ",\"packageDir\":\"" - << EscapeJsonString(Result.Cook.Output.PackageDir.string()) - << "\",\"packageManifestPath\":\"" - << EscapeJsonString(Result.Cook.Output.PackageManifestPath.string()) - << "\"}"; - return Stream.str(); -} - -bool IsValidScriptRelativePath(std::filesystem::path RelativePath) { - RelativePath = RelativePath.lexically_normal(); - if (RelativePath.empty() || RelativePath.is_absolute()) { - return false; - } - if (RelativePath.filename().empty() || RelativePath.extension() != ".cs") { - return false; - } - - for (const auto &Part : RelativePath) { - const auto Token = Part.string(); - if (Token.empty() || Token == "." || Token == "..") { - return false; - } - } - return true; -} - -std::string SerializeScriptListJson(const std::vector &Files) { - std::ostringstream Stream; - Stream << "{\"type\":\"scripts_list\",\"files\":["; - for (size_t Index = 0; Index < Files.size(); ++Index) { - if (Index > 0) { - Stream << ","; - } - Stream << "\"" << EscapeJsonString(Files[Index]) << "\""; - } - Stream << "]}"; - return Stream.str(); -} - -std::string SerializeScriptFileJson(std::string_view RelativePath, - std::string_view Content) { - std::ostringstream Stream; - Stream << "{\"type\":\"script_file\",\"path\":\"" - << EscapeJsonString(RelativePath) << "\",\"content\":\"" - << EscapeJsonString(Content) << "\"}"; - return Stream.str(); -} - -std::string SerializeScriptMutationJson(std::string_view MutationType, - std::string_view RelativePath) { - std::ostringstream Stream; - Stream << "{\"type\":\"" << MutationType << "\",\"path\":\"" - << EscapeJsonString(RelativePath) << "\"}"; - return Stream.str(); -} - -std::string SerializeScriptClassesJson( - const std::vector> &Classes) { - std::ostringstream Stream; - Stream << "{\"type\":\"script_classes\",\"classes\":["; - for (size_t Index = 0; Index < Classes.size(); ++Index) { - if (Index > 0) { - Stream << ","; - } - Stream << "{\"className\":\"" << EscapeJsonString(Classes[Index].first) - << "\",\"path\":\"" << EscapeJsonString(Classes[Index].second) - << "\"}"; - } - Stream << "]}"; - return Stream.str(); -} - -// Loads an image file, scales it to fit within MaxDim x MaxDim (preserving -// aspect ratio), and encodes as JPEG. Returns empty vector on any failure. -std::vector MakeThumbnailJpeg(const std::filesystem::path &Path, - int MaxDim = 128) { - int W = 0, H = 0, Channels = 0; - stbi_uc *Pixels = - stbi_load(Path.string().c_str(), &W, &H, &Channels, STBI_rgb); - if (!Pixels || W <= 0 || H <= 0) return {}; - - // Compute thumbnail dimensions preserving aspect ratio. - int ThumbW = W, ThumbH = H; - if (W > MaxDim || H > MaxDim) { - if (W >= H) { - ThumbW = MaxDim; - ThumbH = std::max(1, H * MaxDim / W); - } else { - ThumbH = MaxDim; - ThumbW = std::max(1, W * MaxDim / H); - } - } - - // Nearest-neighbour downsample (fast, acceptable for small thumbnails). - std::vector Scaled(static_cast(ThumbW * ThumbH * 3)); - for (int Y = 0; Y < ThumbH; ++Y) { - for (int X = 0; X < ThumbW; ++X) { - int SrcX = X * W / ThumbW; - int SrcY = Y * H / ThumbH; - const stbi_uc *Src = Pixels + (SrcY * W + SrcX) * 3; - uint8_t *Dst = Scaled.data() + (Y * ThumbW + X) * 3; - Dst[0] = Src[0]; - Dst[1] = Src[1]; - Dst[2] = Src[2]; - } - } - stbi_image_free(Pixels); - - std::vector JpegBytes; - stbi_write_jpg_to_func( - [](void *Ctx, void *Data, int Size) { - auto *Out = static_cast *>(Ctx); - const uint8_t *Bytes = static_cast(Data); - Out->insert(Out->end(), Bytes, Bytes + Size); - }, - &JpegBytes, ThumbW, ThumbH, 3, Scaled.data(), 85); - return JpegBytes; -} - -std::string Base64Encode(const unsigned char *Data, size_t Size) { - static constexpr char Alphabet[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - std::string Encoded; - Encoded.reserve(((Size + 2u) / 3u) * 4u); - for (size_t Index = 0; Index < Size; Index += 3u) { - const uint32_t Byte0 = Data[Index]; - const uint32_t Byte1 = Index + 1u < Size ? Data[Index + 1u] : 0u; - const uint32_t Byte2 = Index + 2u < Size ? Data[Index + 2u] : 0u; - const uint32_t Triple = (Byte0 << 16u) | (Byte1 << 8u) | Byte2; - - Encoded.push_back(Alphabet[(Triple >> 18u) & 0x3Fu]); - Encoded.push_back(Alphabet[(Triple >> 12u) & 0x3Fu]); - Encoded.push_back(Index + 1u < Size ? Alphabet[(Triple >> 6u) & 0x3Fu] - : '='); - Encoded.push_back(Index + 2u < Size ? Alphabet[Triple & 0x3Fu] : '='); - } - return Encoded; -} - -std::optional> -ComputeSha1(std::string_view Input) { - std::array Digest{}; - uint64_t BitLength = static_cast(Input.size()) * 8u; - std::vector Buffer(Input.begin(), Input.end()); - Buffer.push_back(0x80u); - while ((Buffer.size() % 64u) != 56u) { - Buffer.push_back(0u); - } - for (int Shift = 56; Shift >= 0; Shift -= 8) { - Buffer.push_back( - static_cast((BitLength >> Shift) & 0xFFu)); - } - - uint32_t H0 = 0x67452301u; - uint32_t H1 = 0xEFCDAB89u; - uint32_t H2 = 0x98BADCFEu; - uint32_t H3 = 0x10325476u; - uint32_t H4 = 0xC3D2E1F0u; - - auto LeftRotate = [](uint32_t Value, int Shift) { - return static_cast((Value << Shift) | (Value >> (32 - Shift))); - }; - - for (size_t ChunkOffset = 0; ChunkOffset < Buffer.size(); - ChunkOffset += 64u) { - std::array Words{}; - for (size_t Index = 0; Index < 16u; ++Index) { - const size_t Base = ChunkOffset + (Index * 4u); - Words[Index] = (static_cast(Buffer[Base]) << 24u) | - (static_cast(Buffer[Base + 1u]) << 16u) | - (static_cast(Buffer[Base + 2u]) << 8u) | - static_cast(Buffer[Base + 3u]); - } - for (size_t Index = 16u; Index < Words.size(); ++Index) { - Words[Index] = LeftRotate( - Words[Index - 3u] ^ Words[Index - 8u] ^ Words[Index - 14u] ^ - Words[Index - 16u], - 1); - } + }; + Behavior.close = [this](UwsWebSocket *Socket, int Code, + std::string_view Message) { + (void)Code; + (void)Message; + m_WebSocketDispatch->OnClientClose(Socket->getUserData()->ConnectionId); + }; - uint32_t A = H0; - uint32_t B = H1; - uint32_t C = H2; - uint32_t D = H3; - uint32_t E = H4; + State->App->ws("/ws", std::move(Behavior)) + .get("/*", std::move(RegisterGetHandler)) + .post("/*", std::move(RegisterPostHandler)) + .options("/*", std::move(RegisterOptionsHandler)) + .listen(m_Options.Host, static_cast(m_Options.Port), + [State](us_listen_socket_t *ListenSocket) { + std::scoped_lock Lock(State->StartupMutex); + State->ListenSocket = ListenSocket; + State->StartupCompleted = true; + if (ListenSocket == nullptr) { + State->StartupError = + "Failed to bind the remote viewport server socket."; + } + State->StartupCondition.notify_all(); + }); - for (size_t Index = 0; Index < Words.size(); ++Index) { - uint32_t F = 0; - uint32_t K = 0; - if (Index < 20u) { - F = (B & C) | ((~B) & D); - K = 0x5A827999u; - } else if (Index < 40u) { - F = B ^ C ^ D; - K = 0x6ED9EBA1u; - } else if (Index < 60u) { - F = (B & C) | (B & D) | (C & D); - K = 0x8F1BBCDCu; - } else { - F = B ^ C ^ D; - K = 0xCA62C1D6u; + { + std::scoped_lock Lock(State->StartupMutex); + if (!State->StartupCompleted) { + State->StartupCompleted = true; + State->StartupError = + "uWebSockets did not complete remote viewport startup."; + State->StartupCondition.notify_all(); } - - const uint32_t Temp = - LeftRotate(A, 5) + F + E + K + Words[Index]; - E = D; - D = C; - C = LeftRotate(B, 30); - B = A; - A = Temp; - } - - H0 += A; - H1 += B; - H2 += C; - H3 += D; - H4 += E; - } - - const std::array State{H0, H1, H2, H3, H4}; - for (size_t Index = 0; Index < State.size(); ++Index) { - const uint32_t Word = State[Index]; - Digest[Index * 4u] = static_cast((Word >> 24u) & 0xFFu); - Digest[Index * 4u + 1u] = - static_cast((Word >> 16u) & 0xFFu); - Digest[Index * 4u + 2u] = - static_cast((Word >> 8u) & 0xFFu); - Digest[Index * 4u + 3u] = static_cast(Word & 0xFFu); - } - - return Digest; -} - -bool IsWebSocketUpgradeRequest(std::string_view HeaderBlock) { - const auto Upgrade = FindHeaderValue(HeaderBlock, "Upgrade"); - const auto Connection = FindHeaderValue(HeaderBlock, "Connection"); - std::string LowerConnection = - Connection.has_value() ? *Connection : std::string(); - std::transform(LowerConnection.begin(), LowerConnection.end(), - LowerConnection.begin(), - [](unsigned char Character) { - return static_cast(std::tolower(Character)); - }); - return Upgrade.has_value() && Connection.has_value() && - EqualsCaseInsensitive(*Upgrade, "websocket") && - LowerConnection.find("upgrade") != std::string::npos; -} - -std::vector BuildWebSocketFrame(uint8_t Opcode, const void *Data, - size_t Size) { - std::vector Frame; - Frame.reserve(Size + 10u); - Frame.push_back(static_cast(0x80u | (Opcode & 0x0Fu))); - if (Size <= 125u) { - Frame.push_back(static_cast(Size)); - } else if (Size <= 0xFFFFu) { - Frame.push_back(126u); - Frame.push_back(static_cast((Size >> 8u) & 0xFFu)); - Frame.push_back(static_cast(Size & 0xFFu)); - } else { - Frame.push_back(127u); - for (int Shift = 56; Shift >= 0; Shift -= 8) { - Frame.push_back( - static_cast((static_cast(Size) >> Shift) & - 0xFFu)); } - } - - const auto *Bytes = static_cast(Data); - Frame.insert(Frame.end(), Bytes, Bytes + Size); - return Frame; -} - -std::string GenerateClientId() { - static std::atomic Counter{1}; - const uint64_t Value = Counter.fetch_add(1); - std::ostringstream Stream; - Stream << "client-" << Value; - return Stream.str(); -} -} // namespace - -struct RemoteViewportServer::RemoteClientSession::PacketOutput final - : IEncodedVideoPacketOutput { - PacketOutput(RemoteViewportServer &ServerIn, std::string ClientIdIn) - : Server(ServerIn), ClientId(std::move(ClientIdIn)) {} - - RemoteViewportServer &Server; - std::string ClientId; - - void OnEncodedVideoPacket(const EncodedVideoPacket &Packet) override { - Server.HandleClientEncodedVideoPacket(ClientId, Packet); - } -}; - -RemoteViewportServer::RemoteViewportServer( - HeadlessSessionHost &Host, const RemoteViewportServerOptions &Options) - : m_Host(Host), - m_Options(Options), - m_ProjectsRoot(Project::GetDefaultProjectsRoot()) { - m_Host.SetTransportVideoEncoder(nullptr); -} - -RemoteViewportServer::~RemoteViewportServer() { Stop(); } - -bool RemoteViewportServer::Start(std::string &Error) { -#if AXIOM_PLATFORM_WINDOWS - WinsockRuntime Winsock; - (void)Winsock; -#endif - - addrinfo Hint{}; - Hint.ai_family = AF_INET; - Hint.ai_socktype = SOCK_STREAM; - Hint.ai_protocol = IPPROTO_TCP; - Hint.ai_flags = AI_PASSIVE; - - addrinfo *AddressInfo = nullptr; - const std::string PortString = std::to_string(m_Options.Port); - if (getaddrinfo(m_Options.Host.c_str(), PortString.c_str(), &Hint, - &AddressInfo) != 0) { - Error = "Failed to resolve the remote viewport server address."; - return false; - } - SocketHandle ListenSocket = InvalidSocket; - for (addrinfo *Current = AddressInfo; Current != nullptr; - Current = Current->ai_next) { - ListenSocket = - socket(Current->ai_family, Current->ai_socktype, Current->ai_protocol); - if (ListenSocket == InvalidSocket) { - continue; + if (State->ListenSocket != nullptr) { + State->Loop->run(); } - SetReuseAddress(ListenSocket); - - if (bind(ListenSocket, Current->ai_addr, Current->ai_addrlen) == 0 && - listen(ListenSocket, SOMAXCONN) == 0) { - break; + State->App.reset(); + if (State->Loop != nullptr) { + State->Loop->free(); + State->Loop = nullptr; } + }); - CloseSocket(ListenSocket); - ListenSocket = InvalidSocket; + { + std::unique_lock Lock(m_UwsState->StartupMutex); + m_UwsState->StartupCondition.wait( + Lock, [this]() { return m_UwsState->StartupCompleted; }); + Error = m_UwsState->StartupError; } - freeaddrinfo(AddressInfo); - - if (ListenSocket == InvalidSocket) { - Error = "Failed to bind the remote viewport server socket."; + if (!Error.empty()) { + m_StopRequested.store(true); + if (m_ServerThread.joinable()) { + m_ServerThread.join(); + } + m_Host.GetTransport().Disconnect(this); + m_UwsState.reset(); return false; } - m_ListenSocket = ToValue(ListenSocket); - m_StopRequested.store(false); - m_Host.GetTransport().Connect(this); - m_AcceptThread = std::thread([this]() { AcceptLoop(); }); - m_PresenceThread = std::thread([this]() { PresenceLoop(); }); + m_PresenceThread = std::thread([this]() { m_Presence->RunLoop(); }); return true; } @@ -973,21 +258,31 @@ void RemoteViewportServer::Stop() { return; } - const SocketHandle ListenSocket = ToSocket(m_ListenSocket); - m_ListenSocket = ToValue(InvalidSocket); - CloseSocket(ListenSocket); - CloseAllClients(); - if (m_AcceptThread.joinable()) { - m_AcceptThread.join(); + m_WebSocketDispatch->CloseAllClients(); + if (m_UwsState != nullptr && m_UwsState->Loop != nullptr) { + RemoteViewportServerUwsState *State = m_UwsState.get(); + m_UwsState->Loop->defer([State]() { + if (State->ListenSocket != nullptr) { + us_listen_socket_close(0, State->ListenSocket); + State->ListenSocket = nullptr; + } + if (State->App != nullptr) { + State->App->close(); + } + }); + } + if (m_ServerThread.joinable()) { + m_ServerThread.join(); } if (m_PresenceThread.joinable()) { m_PresenceThread.join(); } m_Host.GetTransport().Disconnect(this); - for (IWebRtcSession *Session : CollectClientWebRtcSessions()) { + for (IWebRtcSession *Session : m_WebRtcSessions->CollectClientWebRtcSessions()) { Session->ResetPeer("server_stopped"); } + m_UwsState.reset(); } void RemoteViewportServer::OnSessionTransportConnected() { @@ -1003,2415 +298,15 @@ void RemoteViewportServer::OnSessionTransportDisconnected() { void RemoteViewportServer::OnSessionTransportEditorEvent( const PublishedEditorEvent &Event) { const std::string Message = SerializeEvent(Event); - BroadcastTextMessage(Message); - for (IWebRtcSession *Session : CollectClientWebRtcSessions()) { + m_WebSocketDispatch->BroadcastTextMessage(Message); + for (IWebRtcSession *Session : m_WebRtcSessions->CollectClientWebRtcSessions()) { Session->SendReliableMessage(Message); } } void RemoteViewportServer::OnSessionTransportViewportFrame( const ViewportFrame &Frame) { - if (Frame.User.Value != 0u) { - if (const HeadlessRenderViewState *RenderView = - m_Host.FindRenderView(Frame.User); - RenderView != nullptr && !RenderView->IsLocal) { - if (RemoteClientSession *Client = FindClientSession(RenderView->ClientId); - Client != nullptr) { - if (Client->WebRtcSession != nullptr) { - Client->WebRtcSession->OnViewportFrame(Frame); - } - if (Client->VideoEncoder != nullptr) { - Client->VideoEncoder->EncodeFrame({ - .FrameIndex = Frame.FrameIndex, - .Width = Frame.Width, - .Height = Frame.Height, - .Format = Frame.Format, - .Pixels = Frame.Pixels, - }); - } - } - } - } -} - -static constexpr int AwayThresholdSeconds = 10; -static constexpr int DisconnectThresholdSeconds = 30; -static constexpr int PresenceCheckIntervalMs = 2000; - -void RemoteViewportServer::PresenceLoop() { - while (!m_StopRequested.load()) { - std::this_thread::sleep_for(std::chrono::milliseconds(PresenceCheckIntervalMs)); - if (m_StopRequested.load()) { - break; - } - - const auto Now = std::chrono::steady_clock::now(); - std::vector> Transitions; - - { - std::scoped_lock Lock(m_ClientMutex); - for (const auto &[ClientId, Client] : m_RemoteClientsById) { - const auto Elapsed = - std::chrono::duration_cast(Now - Client.LastActivity) - .count(); - const EditorUserPresence *Presence = - m_Host.GetHeadlessLayer().GetSession().FindPresence(Client.User); - if (Presence == nullptr) { - continue; - } - if (Elapsed >= DisconnectThresholdSeconds && - Presence->State == EditorUserPresenceState::Away) { - Transitions.emplace_back(Client.User, EditorUserPresenceState::Disconnected); - } else if (Elapsed >= AwayThresholdSeconds && - Presence->State == EditorUserPresenceState::Connected) { - Transitions.emplace_back(Client.User, EditorUserPresenceState::Away); - } - } - } - - for (const auto &[User, State] : Transitions) { - m_Host.GetHeadlessLayer().GetSession().SetPresenceState(User, State); - if (State == EditorUserPresenceState::Disconnected) { - m_Host.GetHeadlessLayer().GetSession().ReleaseAllLocksForUser(User); - } - } - } -} - -void RemoteViewportServer::AcceptLoop() { - const SocketHandle ListenSocket = ToSocket(m_ListenSocket); - while (!m_StopRequested.load()) { - sockaddr_in ClientAddress{}; - socklen_t ClientAddressLength = sizeof(ClientAddress); - const SocketHandle ClientSocket = - accept(ListenSocket, reinterpret_cast(&ClientAddress), - &ClientAddressLength); - if (ClientSocket == InvalidSocket) { - if (!m_StopRequested.load()) { - std::this_thread::sleep_for(std::chrono::milliseconds(25)); - } - continue; - } - - std::thread(&RemoteViewportServer::HandleClient, this, ToValue(ClientSocket)) - .detach(); - } -} - -void RemoteViewportServer::HandleClient(uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - if (HandleHttpRequest(ClientSocketValue)) { - return; - } - CloseSocket(ClientSocket); -} - -void RemoteViewportServer::BroadcastTextMessage(std::string Message) { - std::vector Clients; - { - std::scoped_lock Lock(m_ClientMutex); - for (const auto &Client : m_WebSocketClients) { - if (Client.IsOpen) { - Clients.push_back(Client.SocketValue); - } - } - } - - std::vector FailedClients; - for (const uintptr_t ClientSocketValue : Clients) { - if (!SendTextMessage(ClientSocketValue, Message)) { - FailedClients.push_back(ClientSocketValue); - } - } - - for (const uintptr_t FailedClient : FailedClients) { - RemoveWebSocketClient(FailedClient); - } -} - -void RemoteViewportServer::CloseAllClients() { - std::vector ClientSockets; - { - std::scoped_lock Lock(m_ClientMutex); - for (auto &Client : m_WebSocketClients) { - Client.IsOpen = false; - ClientSockets.push_back(Client.SocketValue); - } - m_WebSocketClients.clear(); - } - - for (const uintptr_t ClientSocketValue : ClientSockets) { - CloseSocket(ToSocket(ClientSocketValue)); - } -} - -void RemoteViewportServer::RemoveWebSocketClient(uintptr_t ClientSocketValue) { - bool Removed = false; - { - std::scoped_lock Lock(m_ClientMutex); - auto It = std::find_if(m_WebSocketClients.begin(), m_WebSocketClients.end(), - [ClientSocketValue](const WebSocketClient &Client) { - return Client.SocketValue == ClientSocketValue; - }); - if (It != m_WebSocketClients.end()) { - It->IsOpen = false; - m_WebSocketClients.erase(It); - Removed = true; - } - } - - if (Removed) { - CloseSocket(ToSocket(ClientSocketValue)); - std::cout << SerializeDisconnected() << std::endl; - } -} - -bool RemoteViewportServer::SendTextMessage(uintptr_t ClientSocketValue, - std::string_view Message) { - const auto Frame = BuildWebSocketFrame(0x1u, Message.data(), Message.size()); - std::scoped_lock Lock(m_SendMutex); - return SendAll(ToSocket(ClientSocketValue), Frame.data(), Frame.size()); -} - -bool RemoteViewportServer::SendBinaryMessage(uintptr_t ClientSocketValue, - const void *Data, size_t Size) { - const auto Frame = BuildWebSocketFrame(0x2u, Data, Size); - std::scoped_lock Lock(m_SendMutex); - return SendAll(ToSocket(ClientSocketValue), Frame.data(), Frame.size()); -} - -bool RemoteViewportServer::HandleHttpRequest(uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - HttpRequest Request{}; - std::string Error; - if (!ReadHttpRequest(ClientSocket, Request, Error)) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError(Error)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - if (Request.Method == "GET" && - HandleWebSocketUpgrade(ClientSocketValue, Request.HeaderBlock, - Request.Path)) { - return true; - } - if (Request.Method == "GET") { - return HandleGetRequest(ClientSocketValue, Request.Path, Request.HeaderBlock); - } - if (Request.Method == "POST") { - return HandlePostRequest(ClientSocketValue, Request.Path, Request.HeaderBlock, - Request.Body); - } - if (Request.Method == "OPTIONS") { - const std::string Response = BuildHttpResponse( - "204 No Content", "text/plain; charset=utf-8", "", - "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" - "Access-Control-Allow-Headers: Content-Type, X-Axiom-Client-Id\r\n"); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = JsonResponse( - "405 Method Not Allowed", - SerializeError("Only GET, POST, and OPTIONS are supported.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandlePostRequest(uintptr_t ClientSocketValue, - std::string_view Path, - std::string_view HeaderBlock, - std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const std::string_view Route = StripQuery(Path); - if (Route == "/projects/create") { - return HandleCreateProjectRequest(ClientSocketValue, Body); - } - if (Route == "/projects/open") { - return HandleOpenProjectRequest(ClientSocketValue, Body); - } - if (Route == "/projects/cook") { - return HandleCookProjectRequest(ClientSocketValue); - } - if (Route == "/projects/package") { - return HandlePackageProjectRequest(ClientSocketValue); - } - if (Route == "/scripts/create") { - return HandleCreateScriptFileRequest(ClientSocketValue, Body); - } - if (Route == "/scripts/save") { - return HandleSaveScriptFileRequest(ClientSocketValue, Body); - } - if (Route == "/scripts/rename") { - return HandleRenameScriptFileRequest(ClientSocketValue, Body); - } - if (Route == "/scripts/delete") { - return HandleDeleteScriptFileRequest(ClientSocketValue, Body); - } - if (Route == "/session/connect") { - return HandleSessionConnectRequest(ClientSocketValue, HeaderBlock, Body); - } - if (Route == "/webrtc/offer") { - return HandleWebRtcOfferRequest(ClientSocketValue, HeaderBlock, Body); - } - if (Route == "/webrtc/ice-candidate") { - return HandleWebRtcIceCandidateRequest(ClientSocketValue, HeaderBlock, Body); - } - if (Route == "/webrtc/close") { - return HandleWebRtcCloseRequest(ClientSocketValue, HeaderBlock, Body); - } - if (Route == "/assets/upload") { - return HandleAssetUploadRequest(ClientSocketValue, Path, HeaderBlock, Body); - } - if (Route != "/command") { - const std::string Response = - JsonResponse("404 Not Found", SerializeError("Unknown POST endpoint.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto User = ResolveClientUser(HeaderBlock); - if (!User.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing or unknown X-Axiom-Client-Id.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - if (const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - ClientId.has_value()) { - TouchClientSession(*ClientId); - } - - std::string Error; - const auto Command = ParseRemoteViewportCommand(Body, Error); - if (!Command.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError(Error)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - switch (Command->Type) { - case HeadlessCommandType::SetViewMode: - m_Host.SetRemoteViewMode(*User, Command->ViewMode); - break; - case HeadlessCommandType::SetShowColliders: - m_Host.SetRemoteShowColliders(*User, Command->ShowColliders); - break; - case HeadlessCommandType::SetLookActive: - case HeadlessCommandType::SetViewportCameraPose: - case HeadlessCommandType::SetCameraProjection: - case HeadlessCommandType::UpdateViewportCamera: - case HeadlessCommandType::SelectObject: - case HeadlessCommandType::RenameObject: - case HeadlessCommandType::SetObjectVisibility: - case HeadlessCommandType::CreateObject: - case HeadlessCommandType::DuplicateObject: - case HeadlessCommandType::DeleteObject: - case HeadlessCommandType::ReparentObject: - case HeadlessCommandType::SetTransform: - case HeadlessCommandType::AttachScript: - case HeadlessCommandType::DetachScript: - case HeadlessCommandType::PlaySession: - case HeadlessCommandType::PauseSession: - case HeadlessCommandType::ResumeSession: - case HeadlessCommandType::StopSession: - case HeadlessCommandType::SetMeshAsset: - case HeadlessCommandType::SetLightProperties: - case HeadlessCommandType::SetMaterialProperties: - case HeadlessCommandType::SetMaterialTexture: - case HeadlessCommandType::SetWorldSettings: - m_Host.SubmitRemoteCommand(*User, Command->EditorPayload); - break; - case HeadlessCommandType::DropMesh: - HandleMeshDropCommand(*User, *Command); - break; - case HeadlessCommandType::DropTexture: { - HandleTextureDropCommand(*User, *Command); - break; - } - case HeadlessCommandType::PlaceActor: - HandlePlaceActorCommand(*User, *Command); - break; - case HeadlessCommandType::GizmoHover: - case HeadlessCommandType::GizmoDragStart: - case HeadlessCommandType::GizmoDragUpdate: - case HeadlessCommandType::GizmoDragEnd: - case HeadlessCommandType::SetGizmoMode: - case HeadlessCommandType::SetGridSnap: - break; - case HeadlessCommandType::Heartbeat: - case HeadlessCommandType::ListAssets: - case HeadlessCommandType::GetSchema: - case HeadlessCommandType::SetProperty: - case HeadlessCommandType::SaveScene: - case HeadlessCommandType::ReloadScripts: - break; - case HeadlessCommandType::Quit: - m_StopRequested.store(true); - m_Host.RequestClose(); - BroadcastTextMessage(SerializeShutdown()); - break; - case HeadlessCommandType::LoadStartupScene: - case HeadlessCommandType::RenderFrame: - break; - } - - const std::string Response = - JsonResponse("202 Accepted", "{\"type\":\"accepted\"}"); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleCreateProjectRequest( - uintptr_t ClientSocketValue, std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto ProjectName = ExtractJsonStringField(Body, "name"); - if (!ProjectName.has_value() || ProjectName->empty()) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Missing required 'name' field.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::string FailureReason; - const auto Created = Project::CreateProjectScaffold(m_ProjectsRoot, *ProjectName, - &FailureReason); - if (!Created.has_value()) { - const std::string Response = JsonResponse( - "400 Bad Request", SerializeError(FailureReason)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - { - std::scoped_lock Lock(m_ProjectMutex); - m_ActiveProject = *Created; - } - - FailureReason.clear(); - if (!LoadActiveProjectIntoSession(&FailureReason)) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError(FailureReason)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("201 Created", SerializeCurrentProject(Created)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleOpenProjectRequest(uintptr_t ClientSocketValue, - std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto ProjectSlug = ExtractJsonStringField(Body, "slug"); - if (!ProjectSlug.has_value() || ProjectSlug->empty()) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Missing required 'slug' field.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto Opened = SetActiveProjectBySlug(*ProjectSlug); - if (!Opened.has_value()) { - const std::string Response = JsonResponse( - "404 Not Found", - SerializeError("Project was not found in the managed projects directory.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::string FailureReason; - if (!LoadActiveProjectIntoSession(&FailureReason)) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError(FailureReason)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("200 OK", SerializeCurrentProject(Opened)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleCookProjectRequest(uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto ActiveProject = GetActiveProject(); - if (!ActiveProject.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("No active project is selected.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::string FailureReason; - const auto Result = - Project::CookProjectContent(*ActiveProject, &FailureReason); - if (!Result.has_value()) { - const std::string Response = - JsonResponse("500 Internal Server Error", SerializeError(FailureReason)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("200 OK", SerializeProjectCookResult(*ActiveProject, *Result)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandlePackageProjectRequest( - uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto ActiveProject = GetActiveProject(); - if (!ActiveProject.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("No active project is selected.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::string FailureReason; - const auto Result = - Project::PackageProjectContent(*ActiveProject, &FailureReason); - if (!Result.has_value()) { - const std::string Response = - JsonResponse("500 Internal Server Error", SerializeError(FailureReason)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = JsonResponse( - "200 OK", SerializeProjectPackageResult(*ActiveProject, *Result)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleListScriptsRequest(uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const std::string Response = - JsonResponse("200 OK", SerializeScriptListJson(ListScriptFiles())); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleListScriptClassesRequest( - uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const std::string Response = - JsonResponse("200 OK", SerializeScriptClassesJson(ListScriptClasses())); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleReadScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Path) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto RelativePath = GetQueryParam(Path, "path"); - if (!RelativePath.has_value() || RelativePath->empty()) { - const std::string Response = JsonResponse( - "400 Bad Request", SerializeError("Missing required 'path' query parameter.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto FilePath = ResolveActiveScriptPath(*RelativePath); - if (!FilePath.has_value() || !std::filesystem::exists(*FilePath)) { - const std::string Response = JsonResponse( - "404 Not Found", SerializeError("Script file was not found.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::ifstream File(*FilePath); - if (!File.is_open()) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError("Failed to open script file.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Content((std::istreambuf_iterator(File)), - std::istreambuf_iterator()); - const std::string Response = - JsonResponse("200 OK", SerializeScriptFileJson(*RelativePath, Content)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleCreateScriptFileRequest( - uintptr_t ClientSocketValue, std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto RelativePath = ExtractJsonStringField(Body, "path"); - if (!RelativePath.has_value() || RelativePath->empty()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing required 'path' field.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto FilePath = ResolveActiveScriptPath(*RelativePath, true); - if (!FilePath.has_value()) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Script path must stay inside the active project's Scripts directory and end in .cs.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (std::filesystem::exists(*FilePath)) { - const std::string Response = JsonResponse( - "409 Conflict", SerializeError("A script file with that path already exists.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::error_code Error; - std::filesystem::create_directories(FilePath->parent_path(), Error); - if (Error) { - const std::string Response = JsonResponse( - "500 Internal Server Error", - SerializeError("Failed to create the script directory.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::ofstream File(*FilePath); - if (!File.is_open()) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError("Failed to create script file.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto ActiveProject = GetActiveProject(); - const std::string Namespace = ActiveProject.has_value() - ? ActiveProject->ScriptWorkspace.RootNamespace - : "Project.Scripts"; - const std::string ClassName = FilePath->stem().string(); - std::ostringstream Template; - Template << "using WraithEngine;\n\n" - << "namespace " << Namespace << ";\n\n" - << "public class " << ClassName << " : Script\n" - << "{\n" - << " public override void OnCreate()\n" - << " {\n" - << " }\n\n" - << " public override void OnTick(float dt)\n" - << " {\n" - << " }\n" - << "}\n"; - File << Template.str(); - File.close(); - - const std::string Response = - JsonResponse("201 Created", SerializeScriptMutationJson("script_created", *RelativePath)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleSaveScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto RelativePath = ExtractJsonStringField(Body, "path"); - const auto Content = ExtractJsonStringField(Body, "content"); - if (!RelativePath.has_value() || !Content.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing required 'path' or 'content' field.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto FilePath = ResolveActiveScriptPath(*RelativePath, true); - if (!FilePath.has_value()) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Script path must stay inside the active project's Scripts directory and end in .cs.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::error_code Error; - std::filesystem::create_directories(FilePath->parent_path(), Error); - if (Error) { - const std::string Response = JsonResponse( - "500 Internal Server Error", - SerializeError("Failed to create the script directory.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::ofstream File(*FilePath); - if (!File.is_open()) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError("Failed to save script file.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - File << *Content; - File.close(); - - const std::string Response = - JsonResponse("200 OK", SerializeScriptMutationJson("script_saved", *RelativePath)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleRenameScriptFileRequest( - uintptr_t ClientSocketValue, std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto RelativePath = ExtractJsonStringField(Body, "path"); - const auto NewRelativePath = ExtractJsonStringField(Body, "newPath"); - if (!RelativePath.has_value() || !NewRelativePath.has_value() || - RelativePath->empty() || NewRelativePath->empty()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing required 'path' or 'newPath' field.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto OldPath = ResolveActiveScriptPath(*RelativePath); - const auto NewPath = ResolveActiveScriptPath(*NewRelativePath, true); - if (!OldPath.has_value() || !NewPath.has_value() || - !std::filesystem::exists(*OldPath)) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Script rename must stay inside the active project's Scripts directory and target an existing .cs file.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (std::filesystem::exists(*NewPath)) { - const std::string Response = JsonResponse( - "409 Conflict", SerializeError("A script file with the destination path already exists.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::error_code Error; - std::filesystem::create_directories(NewPath->parent_path(), Error); - if (Error) { - const std::string Response = JsonResponse( - "500 Internal Server Error", - SerializeError("Failed to create the destination script directory.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - std::filesystem::rename(*OldPath, *NewPath, Error); - if (Error) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError("Failed to rename script file.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("200 OK", SerializeScriptMutationJson("script_renamed", *NewRelativePath)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleDeleteScriptFileRequest( - uintptr_t ClientSocketValue, std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto RelativePath = ExtractJsonStringField(Body, "path"); - if (!RelativePath.has_value() || RelativePath->empty()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing required 'path' field.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto FilePath = ResolveActiveScriptPath(*RelativePath); - if (!FilePath.has_value() || !std::filesystem::exists(*FilePath)) { - const std::string Response = JsonResponse( - "404 Not Found", SerializeError("Script file was not found.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::error_code Error; - const bool Removed = std::filesystem::remove(*FilePath, Error); - if (Error || !Removed) { - const std::string Response = JsonResponse( - "500 Internal Server Error", SerializeError("Failed to delete script file.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("200 OK", SerializeScriptMutationJson("script_deleted", *RelativePath)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleSessionConnectRequest( - uintptr_t ClientSocketValue, std::string_view HeaderBlock, - std::string_view Body) { - (void)Body; - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto ClientIdHint = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - const ClientSessionResolution Resolution = - CreateOrResumeClientSession(ClientIdHint); - RemoteClientSession &Client = *Resolution.Session; - TouchClientSession(Client.ClientId); - - if (Resolution.ResumedExisting && Client.WebRtcSession != nullptr) { - const WebRtcSessionStatus CurrentStatus = Client.WebRtcSession->GetStatus(); - if (CurrentStatus.ConnectionState != "new" && - CurrentStatus.ConnectionState != "closed") { - Client.WebRtcSession->ResetPeer("client_session_resumed"); - } - } - - const WebRtcSessionStatus Status = - Client.WebRtcSession != nullptr ? Client.WebRtcSession->GetStatus() - : WebRtcSessionStatus{}; - const bool ShowColliders = - [&]() -> bool { - if (const HeadlessRenderViewState *View = - m_Host.FindRemoteRenderView(Client.ClientId); - View != nullptr) { - return View->ShowColliders; - } - return true; - }(); - const std::string Payload = SerializeSessionConnectResponse( - Client.ClientId, m_Host.GetHeadlessLayer().GetSession().GetState(), - Client.User, ShowColliders, m_TransportConnected.load(), - m_TransportConnected.load() ? "connected" : "disconnected", - Status.ConnectionState); - const std::string Response = JsonResponse("200 OK", Payload); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleGetRequest(uintptr_t ClientSocketValue, - std::string_view Path, - std::string_view HeaderBlock) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const std::string_view Route = StripQuery(Path); - if (Route == "/projects") { - const auto Projects = ListProjects(); - const auto ActiveProject = GetActiveProject(); - const std::string Response = - JsonResponse("200 OK", SerializeProjectList(Projects, ActiveProject)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (Route == "/projects/current") { - const auto ActiveProject = GetActiveProject(); - const std::string Response = - JsonResponse("200 OK", SerializeCurrentProject(ActiveProject)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (Route == "/scripts") { - return HandleListScriptsRequest(ClientSocketValue); - } - if (Route == "/scripts/classes") { - return HandleListScriptClassesRequest(ClientSocketValue); - } - if (Route == "/scripts/file") { - return HandleReadScriptFileRequest(ClientSocketValue, Path); - } - if (Route == "/health") { - const std::string Body = SerializeReady(m_Options.Width, m_Options.Height); - const std::string Response = JsonResponse("200 OK", Body); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (Route == "/webrtc") { - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - const WebRtcSessionStatus Status = - ClientId.has_value() ? GetClientWebRtcStatus(*ClientId) - : WebRtcSessionStatus{}; - const std::string Body = - SerializeWebRtcStatus(Status.Enabled, Status.Available, - Status.SignalingState, Status.ConnectionState, - Status.Detail, Status.SessionId, - Status.PendingLocalIceCandidateCount, - Status.Video); - const std::string Response = JsonResponse("200 OK", Body); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (Route == "/webrtc/ice-candidates") { - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - std::vector Candidates; - if (ClientId.has_value()) { - if (RemoteClientSession *Client = FindClientSession(*ClientId); - Client != nullptr && Client->WebRtcSession != nullptr) { - Candidates = Client->WebRtcSession->TakePendingLocalIceCandidates(); - } - } - const std::string Body = SerializeWebRtcIceCandidateList(Candidates); - const std::string Response = JsonResponse("200 OK", Body); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (Route == "/session") { - const auto User = ResolveClientUser(HeaderBlock); - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - if (!User.has_value()) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Missing or unknown X-Axiom-Client-Id.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (ClientId.has_value()) { - TouchClientSession(*ClientId); - } - const WebRtcSessionStatus Status = - ClientId.has_value() ? GetClientWebRtcStatus(*ClientId) - : WebRtcSessionStatus{}; - const bool ShowColliders = - [&]() -> bool { - if (ClientId.has_value()) { - if (const HeadlessRenderViewState *View = - m_Host.FindRemoteRenderView(*ClientId); - View != nullptr) { - return View->ShowColliders; - } - } - if (const HeadlessRenderViewState *View = m_Host.FindRenderView(*User); - View != nullptr) { - return View->ShowColliders; - } - return true; - }(); - const std::string Body = SerializeSessionSnapshot( - m_Host.GetHeadlessLayer().GetSession().GetState(), *User, - ShowColliders, - m_TransportConnected.load(), - m_TransportConnected.load() ? "connected" : "disconnected", - Status.ConnectionState); - const std::string Response = JsonResponse("200 OK", Body); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (Route == "/assets/thumbnail") { - const auto RelPath = GetQueryParam(Path, "path"); - if (!RelPath.has_value() || RelPath->empty()) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError("Missing 'path' query parameter.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - // Reject any path that tries to escape the content directory. - if (RelPath->find("..") != std::string::npos) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError("Invalid path.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - const auto FullPath = ResolveVisibleAssetPath(*RelPath); - if (!FullPath.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError("Invalid path.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - const std::vector Jpeg = MakeThumbnailJpeg(*FullPath); - if (Jpeg.empty()) { - const std::string Response = - JsonResponse("404 Not Found", SerializeError("Could not load thumbnail.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - const std::string_view JpegView(reinterpret_cast(Jpeg.data()), - Jpeg.size()); - const std::string Response = - BuildHttpResponse("200 OK", "image/jpeg", JpegView); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - const std::string Response = - JsonResponse("404 Not Found", SerializeError("Unknown GET endpoint.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleWebRtcOfferRequest(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto User = ResolveClientUser(HeaderBlock); - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - if (!User.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing or unknown X-Axiom-Client-Id.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (ClientId.has_value()) { - TouchClientSession(*ClientId); - } - - std::string Error; - const auto Offer = ParseWebRtcSessionDescription(Body, Error); - if (!Offer.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError(Error)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - if (Offer->Type != "offer") { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("WebRTC offer endpoint requires `type` to be `offer`.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - if (!ClientId.has_value()) { - const std::string Response = JsonResponse( - "503 Service Unavailable", - SerializeError("Missing X-Axiom-Client-Id for WebRTC session.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - RemoteClientSession *Client = FindClientSession(*ClientId); - if (Client == nullptr || Client->WebRtcSession == nullptr) { - const std::string Response = JsonResponse( - "503 Service Unavailable", - SerializeError("WebRTC session support is unavailable.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - WebRtcSessionDescription Answer{}; - if (!Client->WebRtcSession->HandleOffer(*Offer, Answer, Error)) { - const WebRtcSessionStatus Status = Client->WebRtcSession->GetStatus(); - const std::string Payload = SerializeWebRtcStatus( - Status.Enabled, Status.Available, Status.SignalingState, - Status.ConnectionState, Error.empty() ? Status.Detail : Error, - Status.SessionId, Status.PendingLocalIceCandidateCount, - Status.Video); - const std::string Response = - JsonResponse("503 Service Unavailable", Payload); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("200 OK", SerializeWebRtcSessionDescription( - Answer, Client->WebRtcSession->GetStatus().SessionId)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleWebRtcIceCandidateRequest( - uintptr_t ClientSocketValue, std::string_view HeaderBlock, - std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto User = ResolveClientUser(HeaderBlock); - if (!User.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing or unknown X-Axiom-Client-Id.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - ClientId.has_value()) { - TouchClientSession(*ClientId); - } - - std::string Error; - const auto Candidate = ParseWebRtcIceCandidate(Body, Error); - if (!Candidate.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError(Error)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - if (!ClientId.has_value()) { - const std::string Response = JsonResponse( - "503 Service Unavailable", - SerializeError("Missing X-Axiom-Client-Id for WebRTC session.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - RemoteClientSession *Client = FindClientSession(*ClientId); - if (Client == nullptr || Client->WebRtcSession == nullptr) { - const std::string Response = JsonResponse( - "503 Service Unavailable", - SerializeError("WebRTC session support is unavailable.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - if (!Client->WebRtcSession->AddRemoteIceCandidate(*Candidate, Error)) { - const WebRtcSessionStatus Status = Client->WebRtcSession->GetStatus(); - const std::string Payload = SerializeWebRtcStatus( - Status.Enabled, Status.Available, Status.SignalingState, - Status.ConnectionState, Error.empty() ? Status.Detail : Error, - Status.SessionId, Status.PendingLocalIceCandidateCount, - Status.Video); - const std::string Response = - JsonResponse("503 Service Unavailable", Payload); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Response = - JsonResponse("202 Accepted", "{\"type\":\"accepted\"}"); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleWebRtcCloseRequest( - uintptr_t ClientSocketValue, std::string_view HeaderBlock, - std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - const auto User = ResolveClientUser(HeaderBlock); - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - if (!User.has_value()) { - const std::string Response = - JsonResponse("400 Bad Request", - SerializeError("Missing or unknown X-Axiom-Client-Id.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (ClientId.has_value()) { - TouchClientSession(*ClientId); - } - - std::string Reason = "browser_requested_close"; - if (!Body.empty()) { - Reason = std::string(Body); - } - - if (!ClientId.has_value()) { - const std::string Response = JsonResponse( - "503 Service Unavailable", - SerializeError("Missing X-Axiom-Client-Id for WebRTC session.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - RemoteClientSession *Client = FindClientSession(*ClientId); - if (Client == nullptr || Client->WebRtcSession == nullptr) { - const std::string Response = JsonResponse( - "503 Service Unavailable", - SerializeError("WebRTC session support is unavailable.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - std::string Error; - if (!Client->WebRtcSession->CloseSession(Reason, Error)) { - const std::string Response = - JsonResponse("500 Internal Server Error", - SerializeError(Error.empty() - ? "Failed to close WebRTC session." - : Error)); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - if (ClientId.has_value()) { - std::optional DisconnectedUser; - { - std::scoped_lock Lock(m_ClientMutex); - const auto It = m_RemoteClientsById.find(*ClientId); - if (It != m_RemoteClientsById.end()) { - DisconnectedUser = It->second.User; - } - } - if (DisconnectedUser.has_value()) { - EditorSession &DisconnectSession = m_Host.GetHeadlessLayer().GetSession(); - DisconnectSession.ReleaseAllLocksForUser(*DisconnectedUser); - DisconnectSession.SetPresenceState(*DisconnectedUser, - EditorUserPresenceState::Disconnected); - } - m_Host.RemoveRemoteRenderView(*ClientId); - } - m_Host.FocusLocalRenderView(); - const WebRtcSessionStatus Status = Client->WebRtcSession->GetStatus(); - const std::string Payload = SerializeWebRtcStatus( - Status.Enabled, Status.Available, Status.SignalingState, - Status.ConnectionState, Status.Detail, Status.SessionId, - Status.PendingLocalIceCandidateCount, Status.Video); - const std::string Response = JsonResponse("200 OK", Payload); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -bool RemoteViewportServer::HandleAssetUploadRequest( - uintptr_t ClientSocketValue, std::string_view Path, - std::string_view HeaderBlock, std::string_view Body) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - - // Parse boundary from Content-Type header. - const auto ContentType = FindHeaderValue(HeaderBlock, "Content-Type"); - if (!ContentType.has_value() || - ContentType->find("multipart/form-data") == std::string::npos) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Expected multipart/form-data Content-Type.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - const auto BoundaryPos = ContentType->find("boundary="); - if (BoundaryPos == std::string::npos) { - const std::string Response = - JsonResponse("400 Bad Request", SerializeError("Missing boundary.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - const std::string Boundary = - "--" + ContentType->substr(BoundaryPos + 9 /* len("boundary=") */); - - // Optional target subdirectory from ?dir= query parameter. - const std::string TargetDir = GetQueryParam(Path, "dir").value_or(""); - - const auto ContentRoot = GetActiveContentDir(); - const Assets::LocalAssetSource ContentSource{ContentRoot}; - - // Resolve and validate destination directory. - std::filesystem::path DestDir; - if (TargetDir.empty()) { - DestDir = ContentRoot; - } else { - const std::filesystem::path TargetDirPath{TargetDir}; - // Prevent path traversal: reject any component that starts with ".." - bool Unsafe = false; - for (const auto &Part : TargetDirPath) { - if (Part.string().find("..") != std::string::npos) { - Unsafe = true; - break; - } - } - if (Unsafe) { - const std::string Response = JsonResponse( - "400 Bad Request", SerializeError("Invalid target directory.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - if (TargetDirPath.begin() != TargetDirPath.end() && - (*TargetDirPath.begin()).string() == "Engine") { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Engine content is read-only and cannot be modified by project uploads.")); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - DestDir = ContentRoot / TargetDirPath; - } - - // Split multipart body into parts. - static constexpr std::string_view kContentDisposition = "Content-Disposition:"; - std::vector Saved; - std::string_view Remaining{Body}; - - while (true) { - // Find boundary - const auto BPos = Remaining.find(Boundary); - if (BPos == std::string_view::npos) break; - Remaining.remove_prefix(BPos + Boundary.size()); - // Skip "\r\n" after boundary, or stop on "--" (final boundary) - if (Remaining.starts_with("--")) break; - if (Remaining.starts_with("\r\n")) Remaining.remove_prefix(2); - - // Find end of part headers (blank line) - const auto HeaderEnd = Remaining.find("\r\n\r\n"); - if (HeaderEnd == std::string_view::npos) break; - const std::string_view PartHeaders = Remaining.substr(0, HeaderEnd); - Remaining.remove_prefix(HeaderEnd + 4); - - // Find filename in Content-Disposition - const auto CDPos = PartHeaders.find(kContentDisposition); - if (CDPos == std::string_view::npos) continue; - const auto FnPos = PartHeaders.find("filename=\"", CDPos); - if (FnPos == std::string_view::npos) continue; - const auto FnStart = FnPos + 10; - const auto FnEnd = PartHeaders.find('"', FnStart); - if (FnEnd == std::string_view::npos) continue; - const std::string Filename{PartHeaders.substr(FnStart, FnEnd - FnStart)}; - if (Filename.empty()) continue; - - // Find part body (ends at next boundary) - const auto BodyEnd = Remaining.find(Boundary); - if (BodyEnd == std::string_view::npos) break; - // Strip trailing \r\n before boundary - const size_t PartBodyLen = BodyEnd >= 2 ? BodyEnd - 2 : BodyEnd; - const std::string_view PartBody = Remaining.substr(0, PartBodyLen); - - // Validate extension - const std::filesystem::path FilePath{Filename}; - const std::string Ext = [&] { - auto E = FilePath.extension().string(); - for (auto &C : E) C = static_cast(std::tolower(static_cast(C))); - return E; - }(); - static constexpr std::string_view kAllowed[] = { - ".glb", ".gltf", ".fbx", ".obj", ".png", ".jpg", ".jpeg", ".hdr"}; - bool Allowed = false; - for (const auto &A : kAllowed) { - if (Ext == A) { Allowed = true; break; } - } - if (!Allowed) { - std::cerr << "[AssetUpload] rejected '" << Filename - << "': unsupported extension\n"; - continue; - } - - // Write file to destination, creating directories as needed. - std::error_code Ec; - std::filesystem::create_directories(DestDir, Ec); - const std::filesystem::path OutPath = DestDir / FilePath.filename(); - std::ofstream OutFile(OutPath, std::ios::binary); - if (!OutFile.is_open()) { - std::cerr << "[AssetUpload] could not open '" << OutPath.string() - << "' for writing\n"; - continue; - } - OutFile.write(PartBody.data(), static_cast(PartBody.size())); - OutFile.close(); - std::cerr << "[AssetUpload] saved '" << OutPath.string() << "'\n"; - - // Build content-relative path for the response. - const auto Rel = std::filesystem::relative(OutPath, - ContentRoot, Ec); - if (!Ec) Saved.push_back(Rel.string()); - } - - // Broadcast updated asset list to all WebSocket clients. - { - BroadcastTextMessage(SerializeAssetList(CollectVisibleAssets())); - } - - // Build JSON response with saved paths. - std::ostringstream Out; - Out << "{\"type\":\"assets_uploaded\",\"files\":["; - for (size_t i = 0; i < Saved.size(); ++i) { - if (i > 0) Out << ","; - Out << "\"" << EscapeJson(Saved[i]) << "\""; - } - Out << "]}"; - const std::string Response = JsonResponse("200 OK", Out.str()); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; -} - -std::optional RemoteViewportServer::ResolveClientUser( - std::string_view HeaderBlock) const { - const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); - if (!ClientId.has_value()) { - return std::nullopt; - } - - std::scoped_lock Lock(m_ClientMutex); - const auto It = m_RemoteClientsById.find(*ClientId); - if (It == m_RemoteClientsById.end()) { - return std::nullopt; - } - return It->second.User; -} - -RemoteViewportServer::RemoteClientSession * -RemoteViewportServer::FindClientSession(std::string_view ClientId) { - std::scoped_lock Lock(m_ClientMutex); - const auto It = m_RemoteClientsById.find(std::string(ClientId)); - return It != m_RemoteClientsById.end() ? &It->second : nullptr; -} - -const RemoteViewportServer::RemoteClientSession * -RemoteViewportServer::FindClientSession(std::string_view ClientId) const { - std::scoped_lock Lock(m_ClientMutex); - const auto It = m_RemoteClientsById.find(std::string(ClientId)); - return It != m_RemoteClientsById.end() ? &It->second : nullptr; -} - -WebRtcSessionStatus -RemoteViewportServer::GetClientWebRtcStatus(std::string_view ClientId) const { - std::scoped_lock Lock(m_ClientMutex); - const auto It = m_RemoteClientsById.find(std::string(ClientId)); - if (It == m_RemoteClientsById.end() || It->second.WebRtcSession == nullptr) { - return {}; - } - return It->second.WebRtcSession->GetStatus(); -} - -std::vector RemoteViewportServer::CollectClientWebRtcSessions() const { - std::vector Sessions; - std::scoped_lock Lock(m_ClientMutex); - Sessions.reserve(m_RemoteClientsById.size()); - for (const auto &[ClientId, Session] : m_RemoteClientsById) { - (void)ClientId; - if (Session.WebRtcSession != nullptr) { - Sessions.push_back(Session.WebRtcSession.get()); - } - } - return Sessions; -} - -RemoteViewportServer::ClientSessionResolution -RemoteViewportServer::CreateOrResumeClientSession( - const std::optional &ClientIdHint) { - RemoteClientSession *ResolvedSession = nullptr; - bool ResumedExisting = false; - { - std::scoped_lock Lock(m_ClientMutex); - if (ClientIdHint.has_value()) { - const auto Existing = m_RemoteClientsById.find(*ClientIdHint); - if (Existing != m_RemoteClientsById.end()) { - Existing->second.LastActivity = std::chrono::steady_clock::now(); - ResolvedSession = &Existing->second; - ResumedExisting = true; - } - } - if (ResolvedSession == nullptr) { - RemoteClientSession Session{}; - Session.ClientId = GenerateClientId(); - Session.User = SessionUserId{m_NextRemoteUserId++}; - Session.LastActivity = std::chrono::steady_clock::now(); - Session.WebRtcSession = CreateWebRtcSession(); - Session.VideoEncoder = CreateDefaultVideoEncoder(); - Session.VideoPacketOutput = std::make_unique( - *this, Session.ClientId); - if (Session.VideoEncoder != nullptr && Session.VideoPacketOutput != nullptr) { - Session.VideoEncoder->SetOutput(Session.VideoPacketOutput.get()); - } - if (Session.WebRtcSession != nullptr) { - const std::string ClientId = Session.ClientId; - Session.WebRtcSession->SetCommandMessageHandler( - [this, ClientId](std::string_view Payload) { - HandleClientWebRtcMessage(ClientId, Payload); - }); - } - auto [It, Inserted] = - m_RemoteClientsById.emplace(Session.ClientId, std::move(Session)); - (void)Inserted; - ResolvedSession = &It->second; - } - } - - m_Host.GetHeadlessLayer().GetSession().EnsureViewportState(ResolvedSession->User); - m_Host.GetHeadlessLayer().GetSession().SetPresenceState( - ResolvedSession->User, EditorUserPresenceState::Connected); - m_Host.EnsureRemoteRenderView(ResolvedSession->ClientId, ResolvedSession->User); - return {.Session = ResolvedSession, .ResumedExisting = ResumedExisting}; -} - -void RemoteViewportServer::TouchClientSession(const std::string &ClientId) { - std::scoped_lock Lock(m_ClientMutex); - const auto It = m_RemoteClientsById.find(ClientId); - if (It != m_RemoteClientsById.end()) { - It->second.LastActivity = std::chrono::steady_clock::now(); - } - m_Host.FocusRemoteRenderView(ClientId); -} - -std::vector RemoteViewportServer::ListProjects() const { - return Project::DiscoverProjects(m_ProjectsRoot); -} - -std::optional -RemoteViewportServer::GetActiveProject() const { - std::scoped_lock Lock(m_ProjectMutex); - return m_ActiveProject; -} - -std::optional -RemoteViewportServer::SetActiveProjectBySlug(std::string_view ProjectSlug) { - const auto Opened = Project::OpenProjectBySlug(m_ProjectsRoot, ProjectSlug); - if (!Opened.has_value()) { - return std::nullopt; - } - - { - std::scoped_lock Lock(m_ProjectMutex); - m_ActiveProject = *Opened; - } - return Opened; -} - -std::filesystem::path RemoteViewportServer::GetActiveContentDir() const { - if (const auto ActiveProject = GetActiveProject(); ActiveProject.has_value()) { - return ActiveProject->Root.ContentDir; - } - return std::filesystem::path(AXIOM_CONTENT_DIR); -} - -std::filesystem::path RemoteViewportServer::GetActiveScriptsDir() const { - if (const auto ActiveProject = GetActiveProject(); ActiveProject.has_value()) { - return ActiveProject->ScriptWorkspace.ScriptsDir; - } - return std::filesystem::path(AXIOM_PROJECTS_DIR) / "__default__" / "Scripts"; -} - -std::filesystem::path RemoteViewportServer::GetEngineContentDir() const { - return std::filesystem::path(AXIOM_CONTENT_DIR) / "Engine"; -} - -bool RemoteViewportServer::LoadActiveProjectIntoSession( - std::string *FailureReason) { - if (m_Host.LoadStartupSceneIntoSession(GetActiveContentDir())) { - return true; - } - - if (FailureReason != nullptr) { - *FailureReason = - "Failed to load the active project's startup scene into the session."; - } - return false; -} - -std::vector RemoteViewportServer::ListScriptFiles() const { - std::vector Results; - const auto ScriptsDir = GetActiveScriptsDir(); - if (!std::filesystem::exists(ScriptsDir)) { - return Results; - } - - for (const auto &Entry : - std::filesystem::recursive_directory_iterator(ScriptsDir)) { - if (!Entry.is_regular_file() || Entry.path().extension() != ".cs") { - continue; - } - - std::error_code Error; - const auto Relative = - std::filesystem::relative(Entry.path(), ScriptsDir, Error); - if (Error) { - continue; - } - Results.push_back(Relative.generic_string()); - } - - std::sort(Results.begin(), Results.end()); - return Results; -} - -std::vector> -RemoteViewportServer::ListScriptClasses() const { - std::vector> Results; - const auto ScriptFiles = ListScriptFiles(); - const auto ActiveProject = GetActiveProject(); - const std::string DefaultNamespace = - ActiveProject.has_value() ? ActiveProject->ScriptWorkspace.RootNamespace - : "Project.Scripts"; - const std::regex NamespacePattern( - R"(namespace\s+([A-Za-z_][A-Za-z0-9_\.]*)\s*[;{])"); - const std::regex ClassPattern( - R"(public\s+class\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*Script\b)"); - - for (const auto &RelativePath : ScriptFiles) { - const auto AbsolutePath = ResolveActiveScriptPath(RelativePath); - if (!AbsolutePath.has_value()) { - continue; - } - - std::ifstream File(*AbsolutePath); - if (!File.is_open()) { - continue; - } - - const std::string Content((std::istreambuf_iterator(File)), - std::istreambuf_iterator()); - std::string Namespace = DefaultNamespace; - if (std::smatch NamespaceMatch; - std::regex_search(Content, NamespaceMatch, NamespacePattern) && - NamespaceMatch.size() > 1) { - Namespace = NamespaceMatch[1].str(); - } - - auto ClassBegin = std::sregex_iterator(Content.begin(), Content.end(), ClassPattern); - const auto ClassEnd = std::sregex_iterator(); - for (auto It = ClassBegin; It != ClassEnd; ++It) { - const std::string ClassName = (*It)[1].str(); - Results.emplace_back(Namespace + "." + ClassName, RelativePath); - } - } - - std::sort(Results.begin(), Results.end(), - [](const auto &Left, const auto &Right) { - return Left.first < Right.first; - }); - return Results; -} - -std::optional -RemoteViewportServer::ResolveActiveScriptPath(std::string_view RelativePath, - bool AllowMissingLeaf) const { - const std::filesystem::path Relative = - std::filesystem::path(RelativePath).lexically_normal(); - if (!IsValidScriptRelativePath(Relative)) { - return std::nullopt; - } - - const auto ScriptsDir = GetActiveScriptsDir(); - const auto Candidate = (ScriptsDir / Relative).lexically_normal(); - const auto ValidationPath = - AllowMissingLeaf ? Candidate.parent_path() : Candidate; - if (!Project::IsPathWithinRoot(ScriptsDir, ValidationPath)) { - return std::nullopt; - } - return Candidate; -} - -std::vector -RemoteViewportServer::CollectVisibleAssets() const { - std::vector Assets; - - const Assets::LocalAssetSource ProjectSource{GetActiveContentDir()}; - Assets = ProjectSource.List(); - - const Assets::LocalAssetSource EngineSource{GetEngineContentDir()}; - for (auto EngineAsset : EngineSource.List()) { - EngineAsset.RelativePath = - (std::filesystem::path("Engine") / EngineAsset.RelativePath) - .generic_string(); - EngineAsset.Name = std::filesystem::path(EngineAsset.RelativePath).stem().string(); - EngineAsset.Id = Assets::AssetIdFromRelativePath(EngineAsset.RelativePath); - Assets.push_back(std::move(EngineAsset)); - } - - std::sort(Assets.begin(), Assets.end(), - [](const Assets::AssetDescriptor &Left, - const Assets::AssetDescriptor &Right) { - return Left.RelativePath < Right.RelativePath; - }); - return Assets; -} - -std::optional -RemoteViewportServer::ResolveVisibleAssetPath(std::string_view RelativePath) const { - if (RelativePath.empty()) { - return std::nullopt; - } - - const std::filesystem::path Relative{std::string(RelativePath)}; - for (const auto &Part : Relative) { - if (Part == "..") { - return std::nullopt; - } - } - - if (!Relative.empty() && *Relative.begin() == "Engine") { - std::filesystem::path EngineRelative; - auto It = Relative.begin(); - ++It; - for (; It != Relative.end(); ++It) { - EngineRelative /= *It; - } - return GetEngineContentDir() / EngineRelative; - } - - return GetActiveContentDir() / Relative; -} - -void RemoteViewportServer::HandleClientEncodedVideoPacket( - std::string_view ClientId, const EncodedVideoPacket &Packet) { - if (RemoteClientSession *Client = FindClientSession(ClientId); - Client != nullptr && Client->WebRtcSession != nullptr) { - Client->WebRtcSession->OnEncodedVideoPacket(Packet); - } -} - -void RemoteViewportServer::HandleTextureDropCommand( - SessionUserId User, const HeadlessCommand &Command) { - if (Command.TextureAssetPath.empty()) { - return; - } - - const EditorSession &Session = m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = Session.FindViewport(User); - if (Viewport == nullptr) { - return; - } - - const std::string HitId = HitTestMeshes( - Viewport->Camera, m_Options.Width, m_Options.Height, - Command.MousePosition, Session.GetState().Scene.MeshInstances); - if (HitId.empty()) { - return; - } - - m_Host.SubmitRemoteCommand(User, EditorCommand{SetMaterialTextureCommand{ - .ObjectId = HitId, - .TextureAssetPath = Command.TextureAssetPath, - }}); -} - -void RemoteViewportServer::HandleMeshDropCommand(SessionUserId User, - const HeadlessCommand &Command) { - if (Command.MeshAssetPath.empty()) { - return; - } - - const EditorSession &Session = m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = Session.FindViewport(User); - if (Viewport == nullptr) { - return; - } - - const glm::vec3 SpawnLocation = ResolveViewportDropPosition( - Viewport->Camera, m_Options.Width, m_Options.Height, Command.MousePosition, - Session.GetState().Scene.MeshInstances); - - m_Host.SubmitRemoteCommand(User, EditorCommand{CreateMeshObjectCommand{ - .AssetPath = Command.MeshAssetPath, - .Location = SpawnLocation, - .RotationDegrees = glm::vec3(0.0f), - .Scale = glm::vec3(1.0f), - }}); -} - -void RemoteViewportServer::HandlePlaceActorCommand(SessionUserId User, - const HeadlessCommand &Command) { - const EditorSession &Session = m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = Session.FindViewport(User); - if (Viewport == nullptr) { - return; - } - - glm::vec2 MousePos = Command.MousePosition; - if (MousePos.x < 0.0f || MousePos.y < 0.0f) { - MousePos = {static_cast(m_Options.Width) * 0.5f, - static_cast(m_Options.Height) * 0.5f}; - } - - const glm::vec3 SpawnLocation = ResolveViewportDropPosition( - Viewport->Camera, m_Options.Width, m_Options.Height, MousePos, - Session.GetState().Scene.MeshInstances); - - m_Host.SubmitRemoteCommand( - User, EditorCommand{PlaceActorCommand{ - .ChildTemplateId = Command.PlaceActorTemplateId, - .ChildMeshAssetPath = Command.PlaceActorMeshAssetPath, - .Location = SpawnLocation, - }}); -} - -bool RemoteViewportServer::HandleWebSocketUpgrade(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Path) { - const std::string_view Route = StripQuery(Path); - if (Route != "/ws" || !IsWebSocketUpgradeRequest(HeaderBlock)) { - return false; - } - - const auto Key = FindHeaderValue(HeaderBlock, "Sec-WebSocket-Key"); - if (!Key.has_value()) { - const std::string Response = JsonResponse( - "400 Bad Request", - SerializeError("Missing Sec-WebSocket-Key for WebSocket upgrade.")); - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const auto Digest = ComputeSha1(*Key + std::string(WebSocketGuid)); - if (!Digest.has_value()) { - const std::string Response = JsonResponse( - "500 Internal Server Error", - SerializeError("Failed to compute WebSocket handshake.")); - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - SendAll(ClientSocket, Response.data(), Response.size()); - return false; - } - - const std::string Accept = Base64Encode(Digest->data(), Digest->size()); - std::ostringstream Response; - Response << "HTTP/1.1 101 Switching Protocols\r\n" - << "Upgrade: websocket\r\n" - << "Connection: Upgrade\r\n" - << "Sec-WebSocket-Accept: " << Accept << "\r\n\r\n"; - const std::string ResponseText = Response.str(); - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - if (!SendAll(ClientSocket, ResponseText.data(), ResponseText.size())) { - return false; - } - - { - std::scoped_lock Lock(m_ClientMutex); - m_WebSocketClients.push_back({.SocketValue = ClientSocketValue}); - } - - std::cout << SerializeConnected() << std::endl; - SendTextMessage(ClientSocketValue, - SerializeReady(m_Options.Width, m_Options.Height)); - SendTextMessage(ClientSocketValue, SerializeConnected()); - - RunWebSocketSession(ClientSocketValue); - return true; -} - -void RemoteViewportServer::RunWebSocketSession(uintptr_t ClientSocketValue) { - const SocketHandle ClientSocket = ToSocket(ClientSocketValue); - while (!m_StopRequested.load()) { - std::array Header{}; - if (!RecvExact(ClientSocket, Header.data(), Header.size())) { - break; - } - - const bool IsFinal = (Header[0] & 0x80u) != 0u; - const uint8_t Opcode = static_cast(Header[0] & 0x0Fu); - const bool IsMasked = (Header[1] & 0x80u) != 0u; - uint64_t PayloadLength = static_cast(Header[1] & 0x7Fu); - if (!IsFinal || !IsMasked) { - break; - } - - if (PayloadLength == 126u) { - std::array ExtendedLength{}; - if (!RecvExact(ClientSocket, ExtendedLength.data(), - ExtendedLength.size())) { - break; - } - PayloadLength = (static_cast(ExtendedLength[0]) << 8u) | - static_cast(ExtendedLength[1]); - } else if (PayloadLength == 127u) { - std::array ExtendedLength{}; - if (!RecvExact(ClientSocket, ExtendedLength.data(), - ExtendedLength.size())) { - break; - } - PayloadLength = 0; - for (const unsigned char Byte : ExtendedLength) { - PayloadLength = (PayloadLength << 8u) | static_cast(Byte); - } - } - - std::array MaskingKey{}; - if (!RecvExact(ClientSocket, MaskingKey.data(), MaskingKey.size())) { - break; - } - - std::vector Payload(PayloadLength); - if (PayloadLength > 0u && - !RecvExact(ClientSocket, Payload.data(), Payload.size())) { - break; - } - for (size_t Index = 0; Index < Payload.size(); ++Index) { - Payload[Index] ^= MaskingKey[Index % MaskingKey.size()]; - } - - if (Opcode == 0x8u) { - break; - } - if (Opcode == 0x9u) { - const auto Pong = BuildWebSocketFrame(0xAu, Payload.data(), Payload.size()); - std::scoped_lock Lock(m_SendMutex); - if (!SendAll(ClientSocket, Pong.data(), Pong.size())) { - break; - } - continue; - } - if (Opcode != 0x1u) { - continue; - } - - const std::string TextPayload(Payload.begin(), Payload.end()); - if (!HandleWebSocketMessage(ClientSocketValue, TextPayload)) { - SendTextMessage(ClientSocketValue, - SerializeError("Invalid WebSocket command payload.")); - } - } - - RemoveWebSocketClient(ClientSocketValue); -} - -bool RemoteViewportServer::HandleWebSocketMessage(uintptr_t ClientSocketValue, - std::string_view Payload) { - std::string Error; - const auto Command = ParseRemoteViewportCommand(Payload, Error); - if (!Command.has_value()) { - return false; - } - - switch (Command->Type) { - case HeadlessCommandType::SetViewMode: - m_Host.SetRemoteViewMode(Command->ViewMode); - return true; - case HeadlessCommandType::SetShowColliders: - m_Host.SetRemoteShowColliders(Command->ShowColliders); - return true; - case HeadlessCommandType::DropMesh: - HandleMeshDropCommand(m_Host.GetHeadlessLayer().GetLocalUserId(), *Command); - return true; - case HeadlessCommandType::DropTexture: - HandleTextureDropCommand(m_Host.GetHeadlessLayer().GetLocalUserId(), *Command); - return true; - case HeadlessCommandType::PlaceActor: - HandlePlaceActorCommand(m_Host.GetHeadlessLayer().GetLocalUserId(), *Command); - return true; - case HeadlessCommandType::SetLookActive: - case HeadlessCommandType::SetViewportCameraPose: - case HeadlessCommandType::SetCameraProjection: - case HeadlessCommandType::SelectObject: - case HeadlessCommandType::RenameObject: - case HeadlessCommandType::SetObjectVisibility: - case HeadlessCommandType::CreateObject: - case HeadlessCommandType::DuplicateObject: - case HeadlessCommandType::DeleteObject: - case HeadlessCommandType::ReparentObject: - case HeadlessCommandType::SetTransform: - case HeadlessCommandType::AttachScript: - case HeadlessCommandType::DetachScript: - case HeadlessCommandType::PlaySession: - case HeadlessCommandType::PauseSession: - case HeadlessCommandType::ResumeSession: - case HeadlessCommandType::StopSession: - case HeadlessCommandType::SetMeshAsset: - case HeadlessCommandType::SetLightProperties: - case HeadlessCommandType::SetMaterialProperties: - case HeadlessCommandType::SetMaterialTexture: - case HeadlessCommandType::SetWorldSettings: - case HeadlessCommandType::ReloadScripts: - case HeadlessCommandType::UpdateViewportCamera: - case HeadlessCommandType::GizmoHover: - case HeadlessCommandType::GizmoDragStart: - case HeadlessCommandType::GizmoDragUpdate: - case HeadlessCommandType::GizmoDragEnd: - case HeadlessCommandType::SetGizmoMode: - case HeadlessCommandType::SetGridSnap: - case HeadlessCommandType::Heartbeat: - return false; - case HeadlessCommandType::ListAssets: { - SendTextMessage(ClientSocketValue, SerializeAssetList(CollectVisibleAssets())); - return true; - } - case HeadlessCommandType::GetSchema: { - const auto &DetailsById = - m_Host.GetHeadlessLayer().GetSession().GetState().Scene.ObjectDetailsById; - const auto It = DetailsById.find(Command->ObjectId); - if (It != DetailsById.end()) { - SendTextMessage(ClientSocketValue, SerializeObjectSchema(It->second)); - } - return true; - } - case HeadlessCommandType::SetProperty: - return false; - case HeadlessCommandType::SaveScene: { - const Assets::LocalAssetSource ContentDir{GetActiveContentDir()}; - const auto ScenePath = ContentDir.ResolveRelative("scene.json"); - const bool Ok = Assets::SaveSceneToFile( - ScenePath, - m_Host.GetHeadlessLayer().GetSession().GetState().Scene); - SendTextMessage(ClientSocketValue, SerializeSaveResult(Ok)); - return true; - } - case HeadlessCommandType::Quit: - m_StopRequested.store(true); - m_Host.RequestClose(); - BroadcastTextMessage(SerializeShutdown()); - return true; - case HeadlessCommandType::LoadStartupScene: - case HeadlessCommandType::RenderFrame: - return false; - } - - return false; -} - -bool RemoteViewportServer::HandleClientWebRtcMessage(std::string_view ClientId, - std::string_view Payload) { - std::string Error; - const auto Command = ParseRemoteViewportCommand(Payload, Error); - if (!Command.has_value()) { - return false; - } - - RemoteClientSession *Client = FindClientSession(ClientId); - if (Client == nullptr) { - return false; - } - TouchClientSession(Client->ClientId); - - switch (Command->Type) { - case HeadlessCommandType::SetViewMode: - m_Host.SetRemoteViewMode(Client->User, Command->ViewMode); - return true; - case HeadlessCommandType::SetShowColliders: - m_Host.SetRemoteShowColliders(Client->User, Command->ShowColliders); - return true; - case HeadlessCommandType::SetLookActive: - case HeadlessCommandType::SetViewportCameraPose: - case HeadlessCommandType::SetCameraProjection: - case HeadlessCommandType::UpdateViewportCamera: - case HeadlessCommandType::SelectObject: - case HeadlessCommandType::RenameObject: - case HeadlessCommandType::SetObjectVisibility: - case HeadlessCommandType::CreateObject: - case HeadlessCommandType::DuplicateObject: - case HeadlessCommandType::DeleteObject: - case HeadlessCommandType::ReparentObject: - case HeadlessCommandType::SetTransform: - case HeadlessCommandType::AttachScript: - case HeadlessCommandType::DetachScript: - case HeadlessCommandType::PlaySession: - case HeadlessCommandType::PauseSession: - case HeadlessCommandType::ResumeSession: - case HeadlessCommandType::StopSession: - case HeadlessCommandType::SetMeshAsset: - case HeadlessCommandType::SetLightProperties: - case HeadlessCommandType::SetMaterialProperties: - case HeadlessCommandType::SetMaterialTexture: - case HeadlessCommandType::SetWorldSettings: - m_Host.SubmitRemoteCommand(Client->User, Command->EditorPayload); - return true; - case HeadlessCommandType::DropMesh: - HandleMeshDropCommand(Client->User, *Command); - return true; - case HeadlessCommandType::PlaceActor: - HandlePlaceActorCommand(Client->User, *Command); - return true; - - case HeadlessCommandType::ReloadScripts: { - m_Host.ReloadUserScripts(); - if (Client->WebRtcSession != nullptr) { - Client->WebRtcSession->SendReliableMessage( - "{\"type\":\"scripts_reloaded\"}"); - } - return true; - } - case HeadlessCommandType::Heartbeat: { - const EditorUserPresence *Presence = - m_Host.GetHeadlessLayer().GetSession().FindPresence(Client->User); - if (Presence != nullptr && Presence->State == EditorUserPresenceState::Away) { - m_Host.GetHeadlessLayer().GetSession().SetPresenceState( - Client->User, EditorUserPresenceState::Connected); - } - return true; - } - case HeadlessCommandType::ListAssets: { - if (Client->WebRtcSession != nullptr) { - Client->WebRtcSession->SendReliableMessage( - SerializeAssetList(CollectVisibleAssets())); - } - return true; - } - case HeadlessCommandType::GetSchema: { - const auto &DetailsById = - m_Host.GetHeadlessLayer().GetSession().GetState().Scene.ObjectDetailsById; - const auto It = DetailsById.find(Command->ObjectId); - if (It != DetailsById.end() && Client->WebRtcSession != nullptr) { - Client->WebRtcSession->SendReliableMessage( - SerializeObjectSchema(It->second)); - } - return true; - } - case HeadlessCommandType::SaveScene: { - const Assets::LocalAssetSource ContentDir{GetActiveContentDir()}; - const auto ScenePath = ContentDir.ResolveRelative("scene.json"); - const bool Ok = Assets::SaveSceneToFile( - ScenePath, - m_Host.GetHeadlessLayer().GetSession().GetState().Scene); - if (Client->WebRtcSession != nullptr) { - Client->WebRtcSession->SendReliableMessage(SerializeSaveResult(Ok)); - } - return true; - } - case HeadlessCommandType::SetProperty: { - if (!Command->PropertyVal.has_value()) { - return false; - } - const auto &Name = Command->PropertyName; - const auto &Val = *Command->PropertyVal; - const auto &ObjId = Command->ObjectId; - - if (Name == "displayName") { - if (const auto *S = std::get_if(&Val)) { - m_Host.SubmitRemoteCommand( - Client->User, - EditorCommand{RenameObjectCommand{.ObjectId = ObjId, .DisplayName = *S}}); - return true; - } - } else if (Name == "visible") { - if (const auto *B = std::get_if(&Val)) { - m_Host.SubmitRemoteCommand( - Client->User, - EditorCommand{SetObjectVisibilityCommand{.ObjectId = ObjId, .Visible = *B}}); - return true; - } - } else if (Name == "scriptClass") { - if (const auto *S = std::get_if(&Val)) { - if (S->empty()) { - m_Host.SubmitRemoteCommand( - Client->User, - EditorCommand{DetachScriptCommand{.ObjectId = ObjId}}); - } else { - m_Host.SubmitRemoteCommand( - Client->User, - EditorCommand{AttachScriptCommand{.ObjectId = ObjId, - .ScriptClassName = *S}}); - } - return true; - } - } else if (Name == "physicsBodyType" || Name == "physicsColliderType" || - Name == "physicsBoxHalfExtents" || Name == "physicsSphereRadius" || - Name == "physicsMass" || Name == "physicsFriction" || - Name == "physicsRestitution") { - const auto &DetailsById = - m_Host.GetHeadlessLayer().GetSession().GetState().Scene.ObjectDetailsById; - const auto It = DetailsById.find(ObjId); - if (It == DetailsById.end() || !It->second.SupportsTransform) { - return false; - } - - Axiom::EditorPhysicsProperties Physics = - It->second.Physics.value_or(Axiom::EditorPhysicsProperties{}); - if (Name == "physicsBodyType") { - const auto *S = std::get_if(&Val); - if (S == nullptr) { - return false; - } - if (*S == "none") { - Physics.BodyType = Axiom::EditorPhysicsBodyType::None; - } else if (*S == "static") { - Physics.BodyType = Axiom::EditorPhysicsBodyType::Static; - } else if (*S == "dynamic") { - Physics.BodyType = Axiom::EditorPhysicsBodyType::Dynamic; - } else { - return false; - } - } else if (Name == "physicsColliderType") { - const auto *S = std::get_if(&Val); - if (S == nullptr) { - return false; - } - if (*S == "none") { - Physics.ColliderType = Axiom::EditorPhysicsColliderType::None; - } else if (*S == "box") { - Physics.ColliderType = Axiom::EditorPhysicsColliderType::Box; - } else if (*S == "sphere") { - Physics.ColliderType = Axiom::EditorPhysicsColliderType::Sphere; - } else { - return false; - } - } else if (Name == "physicsBoxHalfExtents") { - const auto *V = std::get_if(&Val); - if (V == nullptr) { - return false; - } - Physics.BoxHalfExtents = *V; - } else if (Name == "physicsSphereRadius") { - const auto *Number = std::get_if(&Val); - if (Number == nullptr) { - return false; - } - Physics.SphereRadius = *Number; - } else if (Name == "physicsMass") { - const auto *Number = std::get_if(&Val); - if (Number == nullptr) { - return false; - } - Physics.Mass = *Number; - } else if (Name == "physicsFriction") { - const auto *Number = std::get_if(&Val); - if (Number == nullptr) { - return false; - } - Physics.Friction = *Number; - } else if (Name == "physicsRestitution") { - const auto *Number = std::get_if(&Val); - if (Number == nullptr) { - return false; - } - Physics.Restitution = *Number; - } - - m_Host.SubmitRemoteCommand( - Client->User, - EditorCommand{SetPhysicsPropertiesCommand{ - .ObjectId = ObjId, - .Physics = Physics, - }}); - return true; - } else if (Name == "location" || Name == "rotationDegrees" || Name == "scale") { - if (const auto *V = std::get_if(&Val)) { - const auto &DetailsById = - m_Host.GetHeadlessLayer().GetSession().GetState().Scene.ObjectDetailsById; - const auto It = DetailsById.find(ObjId); - if (It == DetailsById.end() || !It->second.Transform.has_value()) { - return false; - } - const EditorTransformDetails &Current = *It->second.Transform; - SetTransformCommand Cmd{ - .ObjectId = ObjId, - .Location = Current.Location, - .RotationDegrees = Current.RotationDegrees, - .Scale = Current.Scale, - }; - if (Name == "location") Cmd.Location = *V; - else if (Name == "rotationDegrees") Cmd.RotationDegrees = *V; - else Cmd.Scale = *V; - m_Host.SubmitRemoteCommand(Client->User, EditorCommand{Cmd}); - return true; - } - } - return false; - } - case HeadlessCommandType::SetGizmoMode: - Client->CurrentGizmoMode = Command->Mode; - m_Host.GetHeadlessLayer().SetGizmoMode(Client->User, Command->Mode); - return true; - case HeadlessCommandType::SetGridSnap: { - Client->GridSnap.Enabled = Command->Enabled; - Client->GridSnap.TranslationStep = - std::max(kMinimumScale, Command->TranslationStep); - Client->GridSnap.RotationStepDegrees = - std::max(0.001f, Command->RotationStepDegrees); - Client->GridSnap.ScaleStep = std::max(kMinimumScale, Command->ScaleStep); - return true; - } - case HeadlessCommandType::GizmoHover: { - if (m_Host.GetHeadlessLayer().GetSession().GetRuntimeState() != - EditorRuntimeState::Edit) { - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, -1); - return true; - } - if (Client->GizmoDrag.has_value()) { - return true; - } - const EditorSession &Session = - m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = - Session.FindViewport(Client->User); - const EditorObjectDetails *Selected = - Session.FindSelectedObjectDetails(Client->User); - const auto *HoverTD = (Selected != nullptr && Selected->SupportsTransform) - ? (Selected->WorldTransform.has_value() ? &*Selected->WorldTransform - : (Selected->Transform.has_value() ? &*Selected->Transform : nullptr)) - : nullptr; - if (Viewport != nullptr && HoverTD != nullptr) { - const float GizmoScale = ComputeGizmoScale( - Viewport->Camera, HoverTD->Location, - m_Options.Width, m_Options.Height); - const int Axis = - (Client->CurrentGizmoMode == GizmoMode::Rotate) - ? HitTestGizmoRings(Viewport->Camera, m_Options.Width, - m_Options.Height, Command->MousePosition, - HoverTD->Location, GizmoScale) - : HitTestGizmoAxes(Viewport->Camera, m_Options.Width, - m_Options.Height, Command->MousePosition, - HoverTD->Location, GizmoScale); - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, Axis); - } else { - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, -1); - } - return true; - } - case HeadlessCommandType::GizmoDragStart: { - if (m_Host.GetHeadlessLayer().GetSession().GetRuntimeState() != - EditorRuntimeState::Edit) { - return true; - } - if (Client->GizmoDrag.has_value()) { - return true; - } - EditorSession &Session = - m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = - Session.FindViewport(Client->User); - if (Viewport == nullptr) { - return true; - } - const EditorObjectDetails *Selected = - Session.FindSelectedObjectDetails(Client->User); - const auto *DragTD = (Selected != nullptr && Selected->SupportsTransform) - ? (Selected->WorldTransform.has_value() ? &*Selected->WorldTransform - : (Selected->Transform.has_value() ? &*Selected->Transform : nullptr)) - : nullptr; - - if (DragTD != nullptr) { - const glm::vec3 &ObjPos = DragTD->Location; - const float GizmoScale = ComputeGizmoScale( - Viewport->Camera, ObjPos, m_Options.Width, m_Options.Height); - if (Client->CurrentGizmoMode == GizmoMode::Rotate) { - auto DragState = BeginGizmoRotateDrag( - Viewport->Camera, m_Options.Width, m_Options.Height, - Command->MousePosition, ObjPos, GizmoScale, ObjPos); - if (DragState.has_value()) { - Client->GizmoDrag = ActiveGizmoDrag{ - .Math = *DragState, - .ObjectId = Selected->ObjectId, - .StartRotDeg = DragTD->RotationDegrees, - .StartScale = DragTD->Scale, - .Mode = GizmoMode::Rotate, - .GizmoScaleAtDragStart = GizmoScale, - }; - Session.AcquireLock(Selected->ObjectId, Client->User); - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, DragState->Axis); - return true; - } - } else { - auto DragState = BeginGizmoDrag( - Viewport->Camera, m_Options.Width, m_Options.Height, - Command->MousePosition, ObjPos, GizmoScale, ObjPos); - if (DragState.has_value()) { - Client->GizmoDrag = ActiveGizmoDrag{ - .Math = *DragState, - .ObjectId = Selected->ObjectId, - .StartRotDeg = DragTD->RotationDegrees, - .StartScale = DragTD->Scale, - .Mode = Client->CurrentGizmoMode, - .GizmoScaleAtDragStart = GizmoScale, - }; - Session.AcquireLock(Selected->ObjectId, Client->User); - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, DragState->Axis); - return true; - } - } - } - - // No gizmo hit — fall back to viewport object picking. - const auto Hit = ResolveViewportSelectionHit( - Viewport->Camera, m_Options.Width, m_Options.Height, - Command->MousePosition, Session.GetState().Scene.MeshInstances, - m_Host.GetHeadlessLayer().BuildLightBillboards()); - if (Hit.has_value() && !Hit->ObjectId.empty()) { - m_Host.SubmitRemoteCommand(Client->User, - EditorCommand{SelectObjectCommand{.ObjectId = Hit->ObjectId}}); - } - return true; - } - case HeadlessCommandType::DropTexture: { - HandleTextureDropCommand(Client->User, *Command); - return true; - } - case HeadlessCommandType::GizmoDragUpdate: { - if (m_Host.GetHeadlessLayer().GetSession().GetRuntimeState() != - EditorRuntimeState::Edit) { - if (Client->GizmoDrag.has_value()) { - EditorSession &Session = m_Host.GetHeadlessLayer().GetSession(); - const std::string DragObjectId = Client->GizmoDrag->ObjectId; - Client->GizmoDrag.reset(); - Session.ReleaseLock(DragObjectId, Client->User); - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, -1); - } - return true; - } - if (!Client->GizmoDrag.has_value()) { - return true; - } - const EditorSession &Session = - m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = - Session.FindViewport(Client->User); - if (Viewport == nullptr) { - return true; - } - const ActiveGizmoDrag &Drag = *Client->GizmoDrag; - glm::vec3 Location = Drag.Math.ObjectStartPos; - glm::vec3 RotDeg = Drag.StartRotDeg; - glm::vec3 Scale = Drag.StartScale; - if (Drag.Mode == GizmoMode::Translate) { - Location = UpdateGizmoDrag(Drag.Math, Viewport->Camera, - m_Options.Width, m_Options.Height, - Command->MousePosition.x, Command->MousePosition.y); - } else if (Drag.Mode == GizmoMode::Scale) { - const glm::vec3 NewPosTmp = - UpdateGizmoDrag(Drag.Math, Viewport->Camera, m_Options.Width, - m_Options.Height, Command->MousePosition.x, - Command->MousePosition.y); - const float DeltaT = - glm::dot(NewPosTmp - Drag.Math.ObjectStartPos, Drag.Math.AxisDir); - const float Factor = std::max( - 0.001f, 1.0f + DeltaT / std::max(0.001f, Drag.GizmoScaleAtDragStart)); - Scale[Drag.Math.Axis] = Drag.StartScale[Drag.Math.Axis] * Factor; - } else { - const float DeltaDeg = UpdateGizmoRotateDrag( - Drag.Math, Viewport->Camera, m_Options.Width, m_Options.Height, - Command->MousePosition.x, Command->MousePosition.y); - RotDeg[Drag.Math.Axis] = Drag.StartRotDeg[Drag.Math.Axis] + DeltaDeg; - } - ApplyGridSnap(Client->GridSnap.Enabled, Client->GridSnap.TranslationStep, - Client->GridSnap.RotationStepDegrees, Client->GridSnap.ScaleStep, - Drag.Mode, Drag.Math.Axis, Location, RotDeg, Scale); - EditorCommand Cmd; - Cmd.Payload = SetTransformCommand{ - .ObjectId = Drag.ObjectId, - .Location = Location, - .RotationDegrees = RotDeg, - .Scale = Scale, - }; - m_Host.SubmitRemoteCommand(Client->User, Cmd); - return true; - } - case HeadlessCommandType::GizmoDragEnd: { - if (!Client->GizmoDrag.has_value()) { - return true; - } - EditorSession &Session = - m_Host.GetHeadlessLayer().GetSession(); - const EditorViewportState *Viewport = - Session.FindViewport(Client->User); - if (Viewport != nullptr) { - const ActiveGizmoDrag &Drag = *Client->GizmoDrag; - glm::vec3 Location = Drag.Math.ObjectStartPos; - glm::vec3 RotDeg = Drag.StartRotDeg; - glm::vec3 Scale = Drag.StartScale; - if (Drag.Mode == GizmoMode::Translate) { - Location = UpdateGizmoDrag(Drag.Math, Viewport->Camera, - m_Options.Width, m_Options.Height, - Command->MousePosition.x, Command->MousePosition.y); - } else if (Drag.Mode == GizmoMode::Scale) { - const glm::vec3 NewPosTmp = - UpdateGizmoDrag(Drag.Math, Viewport->Camera, m_Options.Width, - m_Options.Height, Command->MousePosition.x, - Command->MousePosition.y); - const float DeltaT = - glm::dot(NewPosTmp - Drag.Math.ObjectStartPos, Drag.Math.AxisDir); - const float Factor = std::max( - 0.001f, 1.0f + DeltaT / std::max(0.001f, Drag.GizmoScaleAtDragStart)); - Scale[Drag.Math.Axis] = Drag.StartScale[Drag.Math.Axis] * Factor; - } else { - const float DeltaDeg = UpdateGizmoRotateDrag( - Drag.Math, Viewport->Camera, m_Options.Width, m_Options.Height, - Command->MousePosition.x, Command->MousePosition.y); - RotDeg[Drag.Math.Axis] = Drag.StartRotDeg[Drag.Math.Axis] + DeltaDeg; - } - ApplyGridSnap(Client->GridSnap.Enabled, Client->GridSnap.TranslationStep, - Client->GridSnap.RotationStepDegrees, Client->GridSnap.ScaleStep, - Drag.Mode, Drag.Math.Axis, Location, RotDeg, Scale); - EditorCommand Cmd; - Cmd.Payload = SetTransformCommand{ - .ObjectId = Drag.ObjectId, - .Location = Location, - .RotationDegrees = RotDeg, - .Scale = Scale, - }; - m_Host.SubmitRemoteCommand(Client->User, Cmd); - } - const std::string DragObjectId = Client->GizmoDrag->ObjectId; - Client->GizmoDrag.reset(); - Session.ReleaseLock(DragObjectId, Client->User); - m_Host.GetHeadlessLayer().SetGizmoHoveredAxis(Client->User, -1); - return true; - } - case HeadlessCommandType::Quit: - m_StopRequested.store(true); - m_Host.RequestClose(); - BroadcastTextMessage(SerializeShutdown()); - return true; - case HeadlessCommandType::LoadStartupScene: - case HeadlessCommandType::RenderFrame: - return false; - } - - return false; + m_WebRtcSessions->HandleViewportFrame(Frame); } bool ParseRemoteViewportServerOptions(int argc, char **argv, diff --git a/Headless/RemoteViewportServer.h b/Headless/RemoteViewportServer.h index 64a43b33..c317f7bd 100644 --- a/Headless/RemoteViewportServer.h +++ b/Headless/RemoteViewportServer.h @@ -4,25 +4,22 @@ #include "HeadlessSessionHost.h" #include "WebRtcSession.h" -#include -#include #include -#include -#include -#include #include -#include -#include #include -#include -#include #include #include -#include -#include namespace Axiom { +class RemoteViewportServerUwsState; +class RemoteViewportGridSnap; +class RemoteViewportGizmoController; +class RemoteViewportHttpRouter; +class RemoteViewportPresence; +class RemoteViewportWebRtcSessionManager; +class RemoteViewportWebSocketDispatch; + struct RemoteViewportServerOptions { std::string Host{"127.0.0.1"}; uint16_t Port{8080}; @@ -30,17 +27,42 @@ struct RemoteViewportServerOptions { uint32_t Height{900}; }; -class RemoteViewportServer final : public ISessionTransportSubscriber { +struct RemoteViewportServerMetrics { + bool TransportConnected{false}; + uint16_t ListenPort{0}; + size_t ActiveWebSocketClients{0}; + size_t ActiveRemoteClients{0}; + size_t ActiveWebRtcSessions{0}; + uint64_t TotalHttpRequests{0}; + uint64_t TotalWebSocketMessages{0}; +}; + +class IRemoteViewportServer { +public: + virtual ~IRemoteViewportServer() = default; + + virtual bool Start(std::string &Error) = 0; + virtual void Stop() = 0; + [[nodiscard]] virtual bool ShouldStop() const = 0; + [[nodiscard]] virtual uint16_t GetPort() const = 0; + [[nodiscard]] virtual RemoteViewportServerMetrics GetMetrics() const = 0; +}; + +class RemoteViewportServer final : public IRemoteViewportServer, + public ISessionTransportSubscriber { public: RemoteViewportServer(HeadlessSessionHost &Host, const RemoteViewportServerOptions &Options); ~RemoteViewportServer() override; - bool Start(std::string &Error); - void Stop(); + bool Start(std::string &Error) override; + void Stop() override; - bool ShouldStop() const { return m_StopRequested.load(); } - uint16_t GetPort() const { return m_Options.Port; } + [[nodiscard]] bool ShouldStop() const override { + return m_StopRequested.load(); + } + [[nodiscard]] uint16_t GetPort() const override { return m_Options.Port; } + [[nodiscard]] RemoteViewportServerMetrics GetMetrics() const override; void OnSessionTransportConnected() override; void OnSessionTransportDisconnected() override; @@ -49,155 +71,26 @@ class RemoteViewportServer final : public ISessionTransportSubscriber { void OnSessionTransportViewportFrame(const ViewportFrame &Frame) override; private: - struct GridSnapSettings { - bool Enabled{true}; - float TranslationStep{1.0f}; - float RotationStepDegrees{15.0f}; - float ScaleStep{0.1f}; - }; - - struct WebSocketClient { - uintptr_t SocketValue{static_cast(~0ull)}; - bool IsOpen{true}; - }; - - struct ActiveGizmoDrag { - GizmoDragState Math; - std::string ObjectId; - glm::vec3 StartRotDeg{0.0f}; - glm::vec3 StartScale{1.0f}; - GizmoMode Mode{GizmoMode::Translate}; - float GizmoScaleAtDragStart{1.0f}; - }; - - struct RemoteClientSession { - struct PacketOutput; - - std::string ClientId; - SessionUserId User; - std::chrono::steady_clock::time_point LastActivity; - std::unique_ptr WebRtcSession; - std::unique_ptr VideoEncoder; - std::unique_ptr VideoPacketOutput; - std::optional GizmoDrag; - GizmoMode CurrentGizmoMode{GizmoMode::Translate}; - GridSnapSettings GridSnap; - }; - - struct ClientSessionResolution { - RemoteClientSession *Session{nullptr}; - bool ResumedExisting{false}; - }; - -private: - void AcceptLoop(); - void PresenceLoop(); - void HandleClient(uintptr_t ClientSocketValue); - void BroadcastTextMessage(std::string Message); - void CloseAllClients(); - void RemoveWebSocketClient(uintptr_t ClientSocketValue); - bool SendTextMessage(uintptr_t ClientSocketValue, std::string_view Message); - bool SendBinaryMessage(uintptr_t ClientSocketValue, const void *Data, - size_t Size); - bool HandleHttpRequest(uintptr_t ClientSocketValue); - bool HandleGetRequest(uintptr_t ClientSocketValue, std::string_view Path, - std::string_view HeaderBlock); - bool HandlePostRequest(uintptr_t ClientSocketValue, std::string_view Path, - std::string_view HeaderBlock, - std::string_view Body); - bool HandleCreateProjectRequest(uintptr_t ClientSocketValue, - std::string_view Body); - bool HandleOpenProjectRequest(uintptr_t ClientSocketValue, - std::string_view Body); - bool HandleCookProjectRequest(uintptr_t ClientSocketValue); - bool HandlePackageProjectRequest(uintptr_t ClientSocketValue); - bool HandleListScriptsRequest(uintptr_t ClientSocketValue); - bool HandleListScriptClassesRequest(uintptr_t ClientSocketValue); - bool HandleReadScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Path); - bool HandleCreateScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Body); - bool HandleSaveScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Body); - bool HandleRenameScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Body); - bool HandleDeleteScriptFileRequest(uintptr_t ClientSocketValue, - std::string_view Body); - bool HandleSessionConnectRequest(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Body); - bool HandleWebRtcOfferRequest(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Body); - bool HandleWebRtcIceCandidateRequest(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Body); - bool HandleWebRtcCloseRequest(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Body); - bool HandleAssetUploadRequest(uintptr_t ClientSocketValue, - std::string_view Path, - std::string_view HeaderBlock, - std::string_view Body); - bool HandleWebSocketUpgrade(uintptr_t ClientSocketValue, - std::string_view HeaderBlock, - std::string_view Path); - void RunWebSocketSession(uintptr_t ClientSocketValue); - bool HandleWebSocketMessage(uintptr_t ClientSocketValue, - std::string_view Payload); - bool HandleClientWebRtcMessage(std::string_view ClientId, - std::string_view Payload); - void HandleTextureDropCommand(SessionUserId User, - const HeadlessCommand &Command); - void HandleMeshDropCommand(SessionUserId User, - const HeadlessCommand &Command); - void HandlePlaceActorCommand(SessionUserId User, - const HeadlessCommand &Command); - void HandleClientEncodedVideoPacket(std::string_view ClientId, - const EncodedVideoPacket &Packet); - std::optional ResolveClientUser( - std::string_view HeaderBlock) const; - RemoteClientSession *FindClientSession(std::string_view ClientId); - const RemoteClientSession *FindClientSession(std::string_view ClientId) const; - WebRtcSessionStatus GetClientWebRtcStatus(std::string_view ClientId) const; - std::vector CollectClientWebRtcSessions() const; - ClientSessionResolution CreateOrResumeClientSession( - const std::optional &ClientIdHint); - void TouchClientSession(const std::string &ClientId); - std::vector ListProjects() const; - std::optional GetActiveProject() const; - std::optional - SetActiveProjectBySlug(std::string_view ProjectSlug); - std::filesystem::path GetActiveContentDir() const; - std::filesystem::path GetActiveScriptsDir() const; - std::filesystem::path GetEngineContentDir() const; - bool LoadActiveProjectIntoSession(std::string *FailureReason = nullptr); - std::vector ListScriptFiles() const; - std::vector> ListScriptClasses() const; - std::optional - ResolveActiveScriptPath(std::string_view RelativePath, - bool AllowMissingLeaf = false) const; - std::vector CollectVisibleAssets() const; - std::optional - ResolveVisibleAssetPath(std::string_view RelativePath) const; + friend class RemoteViewportGridSnap; + friend class RemoteViewportGizmoController; + friend class RemoteViewportHttpRouter; + friend class RemoteViewportPresence; + friend class RemoteViewportWebRtcSessionManager; + friend class RemoteViewportWebSocketDispatch; -private: HeadlessSessionHost &m_Host; RemoteViewportServerOptions m_Options; std::atomic m_StopRequested{false}; std::atomic m_TransportConnected{false}; - uintptr_t m_ListenSocket{static_cast(~0ull)}; - std::thread m_AcceptThread; + std::unique_ptr m_UwsState; + std::thread m_ServerThread; std::thread m_PresenceThread; - - mutable std::mutex m_ClientMutex; - std::vector m_WebSocketClients; - std::unordered_map m_RemoteClientsById; - uint64_t m_NextRemoteUserId{2}; - mutable std::mutex m_SendMutex; - const std::filesystem::path m_ProjectsRoot; - mutable std::mutex m_ProjectMutex; - std::optional m_ActiveProject; + std::unique_ptr m_GridSnap; + std::unique_ptr m_GizmoController; + std::unique_ptr m_HttpRouter; + std::unique_ptr m_Presence; + std::unique_ptr m_WebRtcSessions; + std::unique_ptr m_WebSocketDispatch; }; bool ParseRemoteViewportServerOptions(int argc, char **argv, diff --git a/Headless/RemoteViewportWebRtcSessionManager.cpp b/Headless/RemoteViewportWebRtcSessionManager.cpp new file mode 100644 index 00000000..dd028247 --- /dev/null +++ b/Headless/RemoteViewportWebRtcSessionManager.cpp @@ -0,0 +1,472 @@ +#include "RemoteViewportWebRtcSessionManager.h" + +#include "HeadlessCommandProtocol.h" +#include "RemoteViewportHttpRouter.h" +#include "RemoteViewportServer.h" +#include "RemoteViewportWebSocketDispatch.h" + +#include +#include + +#include +#include +#include +#include + +namespace Axiom { +namespace { +constexpr std::string_view ClientIdHeaderName = "X-Axiom-Client-Id"; + +std::string Trim(std::string_view Value) { + while (!Value.empty() && + std::isspace(static_cast(Value.front())) != 0) { + Value.remove_prefix(1); + } + while (!Value.empty() && + std::isspace(static_cast(Value.back())) != 0) { + Value.remove_suffix(1); + } + return std::string(Value); +} + +bool EqualsCaseInsensitive(std::string_view Left, std::string_view Right) { + if (Left.size() != Right.size()) { + return false; + } + for (size_t Index = 0; Index < Left.size(); ++Index) { + if (std::tolower(static_cast(Left[Index])) != + std::tolower(static_cast(Right[Index]))) { + return false; + } + } + return true; +} + +std::optional FindHeaderValue(std::string_view HeaderBlock, + std::string_view HeaderName) { + size_t LineStart = 0; + while (LineStart < HeaderBlock.size()) { + const size_t LineEnd = HeaderBlock.find("\r\n", LineStart); + const std::string_view Line = + HeaderBlock.substr(LineStart, LineEnd == std::string_view::npos + ? std::string_view::npos + : LineEnd - LineStart); + const size_t Colon = Line.find(':'); + if (Colon != std::string_view::npos && + EqualsCaseInsensitive(Trim(Line.substr(0, Colon)), HeaderName)) { + return Trim(Line.substr(Colon + 1)); + } + if (LineEnd == std::string_view::npos) { + break; + } + LineStart = LineEnd + 2; + } + return std::nullopt; +} + +std::string GenerateClientId() { + static std::atomic Counter{1}; + const uint64_t Value = Counter.fetch_add(1); + std::ostringstream Stream; + Stream << "client-" << Value; + return Stream.str(); +} +} // namespace + +struct RemoteClientSession::PacketOutput final : IEncodedVideoPacketOutput { + PacketOutput(RemoteViewportWebRtcSessionManager &ManagerIn, + std::string ClientIdIn) + : Manager(ManagerIn), ClientId(std::move(ClientIdIn)) {} + + RemoteViewportWebRtcSessionManager &Manager; + std::string ClientId; + + void OnEncodedVideoPacket(const EncodedVideoPacket &Packet) override { + Manager.HandleClientEncodedVideoPacket(ClientId, Packet); + } +}; + +RemoteViewportWebRtcSessionManager::RemoteViewportWebRtcSessionManager( + RemoteViewportServer &Server) + : m_Server(Server) {} + +size_t RemoteViewportWebRtcSessionManager::GetRemoteClientCount() const { + std::scoped_lock Lock(m_RemoteClientMutex); + return m_RemoteClientsById.size(); +} + +size_t RemoteViewportWebRtcSessionManager::GetActiveWebRtcSessionCount() const { + std::scoped_lock Lock(m_RemoteClientMutex); + size_t Count = 0; + for (const auto &[ClientId, Client] : m_RemoteClientsById) { + (void)ClientId; + if (Client->WebRtcSession != nullptr) { + ++Count; + } + } + return Count; +} + +std::vector> +RemoteViewportWebRtcSessionManager::CollectPresenceEntries() const { + std::vector> + Entries; + std::scoped_lock Lock(m_RemoteClientMutex); + Entries.reserve(m_RemoteClientsById.size()); + for (const auto &[ClientId, Client] : m_RemoteClientsById) { + (void)ClientId; + Entries.emplace_back(Client->User, Client->LastActivity); + } + return Entries; +} + +std::optional +RemoteViewportWebRtcSessionManager::ResolveClientUser( + std::string_view ClientId) const { + std::scoped_lock Lock(m_RemoteClientMutex); + const auto It = m_RemoteClientsById.find(std::string(ClientId)); + if (It == m_RemoteClientsById.end()) { + return std::nullopt; + } + return It->second->User; +} + +std::shared_ptr +RemoteViewportWebRtcSessionManager::FindClientSession(std::string_view ClientId) { + std::scoped_lock Lock(m_RemoteClientMutex); + const auto It = m_RemoteClientsById.find(std::string(ClientId)); + return It != m_RemoteClientsById.end() ? It->second : nullptr; +} + +std::shared_ptr +RemoteViewportWebRtcSessionManager::FindClientSession( + std::string_view ClientId) const { + std::scoped_lock Lock(m_RemoteClientMutex); + const auto It = m_RemoteClientsById.find(std::string(ClientId)); + return It != m_RemoteClientsById.end() ? It->second : nullptr; +} + +WebRtcSessionStatus RemoteViewportWebRtcSessionManager::GetClientWebRtcStatus( + std::string_view ClientId) const { + const auto Client = FindClientSession(ClientId); + if (Client == nullptr || Client->WebRtcSession == nullptr) { + return {}; + } + return Client->WebRtcSession->GetStatus(); +} + +std::vector +RemoteViewportWebRtcSessionManager::CollectClientWebRtcSessions() const { + std::vector Sessions; + std::scoped_lock Lock(m_RemoteClientMutex); + Sessions.reserve(m_RemoteClientsById.size()); + for (const auto &[ClientId, Client] : m_RemoteClientsById) { + (void)ClientId; + if (Client->WebRtcSession != nullptr) { + Sessions.push_back(Client->WebRtcSession.get()); + } + } + return Sessions; +} + +ClientSessionResolution +RemoteViewportWebRtcSessionManager::CreateOrResumeClientSession( + const std::optional &ClientIdHint) { + std::shared_ptr ResolvedSession; + bool ResumedExisting = false; + { + std::scoped_lock Lock(m_RemoteClientMutex); + if (ClientIdHint.has_value()) { + const auto Existing = m_RemoteClientsById.find(*ClientIdHint); + if (Existing != m_RemoteClientsById.end()) { + Existing->second->LastActivity = std::chrono::steady_clock::now(); + ResolvedSession = Existing->second; + ResumedExisting = true; + } + } + if (ResolvedSession == nullptr) { + auto Session = std::make_shared(); + Session->ClientId = GenerateClientId(); + Session->User = SessionUserId{m_NextRemoteUserId++}; + Session->LastActivity = std::chrono::steady_clock::now(); + Session->WebRtcSession = CreateWebRtcSession(); + Session->VideoEncoder = CreateDefaultVideoEncoder(); + Session->VideoPacketOutput = + std::make_unique(*this, + Session->ClientId); + if (Session->VideoEncoder != nullptr && + Session->VideoPacketOutput != nullptr) { + Session->VideoEncoder->SetOutput(Session->VideoPacketOutput.get()); + } + if (Session->WebRtcSession != nullptr) { + const std::string ClientId = Session->ClientId; + Session->WebRtcSession->SetCommandMessageHandler( + [this, ClientId](std::string_view Payload) { + m_Server.m_WebSocketDispatch->HandleClientWebRtcMessage(ClientId, + Payload); + }); + } + m_Server.m_GridSnap->Sanitize(Session->GridSnap); + m_RemoteClientsById.emplace(Session->ClientId, Session); + ResolvedSession = std::move(Session); + } + } + + m_Server.m_Host.GetSessionModule().GetSession().EnsureViewportState( + ResolvedSession->User); + m_Server.m_Host.GetSessionModule().GetSession().SetPresenceState( + ResolvedSession->User, EditorUserPresenceState::Connected); + m_Server.m_Host.EnsureRemoteRenderView(ResolvedSession->ClientId, + ResolvedSession->User); + return {.Session = std::move(ResolvedSession), + .ResumedExisting = ResumedExisting}; +} + +void RemoteViewportWebRtcSessionManager::TouchClientSession( + const std::string &ClientId) { + { + std::scoped_lock Lock(m_RemoteClientMutex); + const auto It = m_RemoteClientsById.find(ClientId); + if (It != m_RemoteClientsById.end()) { + It->second->LastActivity = std::chrono::steady_clock::now(); + } + } + m_Server.m_Host.FocusRemoteRenderView(ClientId); +} + +bool RemoteViewportWebRtcSessionManager::HandleSessionConnectRequest( + uintptr_t ClientSocketValue, std::string_view HeaderBlock, + std::string_view Body) { + (void)Body; + const auto ClientIdHint = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + const ClientSessionResolution Resolution = + CreateOrResumeClientSession(ClientIdHint); + RemoteClientSession &Client = *Resolution.Session; + TouchClientSession(Client.ClientId); + + if (Resolution.ResumedExisting && Client.WebRtcSession != nullptr) { + const WebRtcSessionStatus CurrentStatus = Client.WebRtcSession->GetStatus(); + if (CurrentStatus.ConnectionState != "new" && + CurrentStatus.ConnectionState != "closed") { + Client.WebRtcSession->ResetPeer("client_session_resumed"); + } + } + + const WebRtcSessionStatus Status = + Client.WebRtcSession != nullptr ? Client.WebRtcSession->GetStatus() + : WebRtcSessionStatus{}; + const bool ShowColliders = [&]() -> bool { + if (const HeadlessRenderViewState *View = + m_Server.m_Host.FindRemoteRenderView(Client.ClientId); + View != nullptr) { + return View->ShowColliders; + } + return true; + }(); + const std::string Payload = SerializeSessionConnectResponse( + Client.ClientId, m_Server.m_Host.GetSessionModule().GetSession().GetState(), + Client.User, ShowColliders, m_Server.m_TransportConnected.load(), + m_Server.m_TransportConnected.load() ? "connected" : "disconnected", + Status.ConnectionState); + return m_Server.m_HttpRouter->SendJsonResponse(ClientSocketValue, "200 OK", + Payload); +} + +bool RemoteViewportWebRtcSessionManager::HandleWebRtcOfferRequest( + uintptr_t ClientSocketValue, std::string_view HeaderBlock, + std::string_view Body) { + const auto User = + [&]() -> std::optional { + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + return ClientId.has_value() ? ResolveClientUser(*ClientId) : std::nullopt; + }(); + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + if (!User.has_value()) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "400 Bad Request", + "Missing or unknown X-Axiom-Client-Id."); + } + if (ClientId.has_value()) { + TouchClientSession(*ClientId); + } + + std::string Error; + const auto Offer = ParseWebRtcSessionDescription(Body, Error); + if (!Offer.has_value()) { + return m_Server.m_HttpRouter->SendJsonError(ClientSocketValue, + "400 Bad Request", Error); + } + if (Offer->Type != "offer") { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "400 Bad Request", + "WebRTC offer endpoint requires `type` to be `offer`."); + } + if (!ClientId.has_value()) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "503 Service Unavailable", + "Missing X-Axiom-Client-Id for WebRTC session."); + } + + auto Client = FindClientSession(*ClientId); + if (Client == nullptr || Client->WebRtcSession == nullptr) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "503 Service Unavailable", + "WebRTC session support is unavailable."); + } + + WebRtcSessionDescription Answer{}; + if (!Client->WebRtcSession->HandleOffer(*Offer, Answer, Error)) { + const WebRtcSessionStatus Status = Client->WebRtcSession->GetStatus(); + const std::string Payload = SerializeWebRtcStatus( + Status.Enabled, Status.Available, Status.SignalingState, + Status.ConnectionState, Error.empty() ? Status.Detail : Error, + Status.SessionId, Status.PendingLocalIceCandidateCount, Status.Video); + return m_Server.m_HttpRouter->SendJsonResponse( + ClientSocketValue, "503 Service Unavailable", Payload); + } + + return m_Server.m_HttpRouter->SendJsonResponse( + ClientSocketValue, "200 OK", + SerializeWebRtcSessionDescription( + Answer, Client->WebRtcSession->GetStatus().SessionId)); +} + +bool RemoteViewportWebRtcSessionManager::HandleWebRtcIceCandidateRequest( + uintptr_t ClientSocketValue, std::string_view HeaderBlock, + std::string_view Body) { + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + const auto User = + ClientId.has_value() ? ResolveClientUser(*ClientId) : std::nullopt; + if (!User.has_value()) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "400 Bad Request", + "Missing or unknown X-Axiom-Client-Id."); + } + if (ClientId.has_value()) { + TouchClientSession(*ClientId); + } + + std::string Error; + const auto Candidate = ParseWebRtcIceCandidate(Body, Error); + if (!Candidate.has_value()) { + return m_Server.m_HttpRouter->SendJsonError(ClientSocketValue, + "400 Bad Request", Error); + } + if (!ClientId.has_value()) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "503 Service Unavailable", + "Missing X-Axiom-Client-Id for WebRTC session."); + } + + auto Client = FindClientSession(*ClientId); + if (Client == nullptr || Client->WebRtcSession == nullptr) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "503 Service Unavailable", + "WebRTC session support is unavailable."); + } + if (!Client->WebRtcSession->AddRemoteIceCandidate(*Candidate, Error)) { + const WebRtcSessionStatus Status = Client->WebRtcSession->GetStatus(); + const std::string Payload = SerializeWebRtcStatus( + Status.Enabled, Status.Available, Status.SignalingState, + Status.ConnectionState, Error.empty() ? Status.Detail : Error, + Status.SessionId, Status.PendingLocalIceCandidateCount, Status.Video); + return m_Server.m_HttpRouter->SendJsonResponse( + ClientSocketValue, "503 Service Unavailable", Payload); + } + + return m_Server.m_HttpRouter->SendJsonResponse( + ClientSocketValue, "202 Accepted", "{\"type\":\"accepted\"}"); +} + +bool RemoteViewportWebRtcSessionManager::HandleWebRtcCloseRequest( + uintptr_t ClientSocketValue, std::string_view HeaderBlock, + std::string_view Body) { + const auto ClientId = FindHeaderValue(HeaderBlock, ClientIdHeaderName); + const auto User = + ClientId.has_value() ? ResolveClientUser(*ClientId) : std::nullopt; + if (!User.has_value()) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "400 Bad Request", + "Missing or unknown X-Axiom-Client-Id."); + } + if (ClientId.has_value()) { + TouchClientSession(*ClientId); + } + + std::string Reason = Body.empty() ? "browser_requested_close" + : std::string(Body); + if (!ClientId.has_value()) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "503 Service Unavailable", + "Missing X-Axiom-Client-Id for WebRTC session."); + } + + auto Client = FindClientSession(*ClientId); + if (Client == nullptr || Client->WebRtcSession == nullptr) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "503 Service Unavailable", + "WebRTC session support is unavailable."); + } + + std::string Error; + if (!Client->WebRtcSession->CloseSession(Reason, Error)) { + return m_Server.m_HttpRouter->SendJsonError( + ClientSocketValue, "500 Internal Server Error", + Error.empty() ? "Failed to close WebRTC session." : Error); + } + + std::optional DisconnectedUser; + if (const auto Existing = FindClientSession(*ClientId); Existing != nullptr) { + DisconnectedUser = Existing->User; + } + if (DisconnectedUser.has_value()) { + EditorSession &DisconnectSession = m_Server.m_Host.GetSessionModule().GetSession(); + DisconnectSession.ReleaseAllLocksForUser(*DisconnectedUser); + DisconnectSession.SetPresenceState(*DisconnectedUser, + EditorUserPresenceState::Disconnected); + } + m_Server.m_Host.RemoveRemoteRenderView(*ClientId); + m_Server.m_Host.FocusLocalRenderView(); + + const WebRtcSessionStatus Status = Client->WebRtcSession->GetStatus(); + const std::string Payload = SerializeWebRtcStatus( + Status.Enabled, Status.Available, Status.SignalingState, + Status.ConnectionState, Status.Detail, Status.SessionId, + Status.PendingLocalIceCandidateCount, Status.Video); + return m_Server.m_HttpRouter->SendJsonResponse(ClientSocketValue, "200 OK", + Payload); +} + +void RemoteViewportWebRtcSessionManager::HandleViewportFrame( + const ViewportFrame &Frame) { + if (Frame.User.Value == 0u) { + return; + } + if (const HeadlessRenderViewState *RenderView = + m_Server.m_Host.FindRenderView(Frame.User); + RenderView != nullptr && !RenderView->IsLocal) { + if (auto Client = FindClientSession(RenderView->ClientId); Client != nullptr) { + if (Client->WebRtcSession != nullptr) { + Client->WebRtcSession->OnViewportFrame(Frame); + } + if (Client->VideoEncoder != nullptr) { + Client->VideoEncoder->EncodeFrame({ + .FrameIndex = Frame.FrameIndex, + .Width = Frame.Width, + .Height = Frame.Height, + .Format = Frame.Format, + .Pixels = Frame.Pixels, + }); + } + } + } +} + +void RemoteViewportWebRtcSessionManager::HandleClientEncodedVideoPacket( + std::string_view ClientId, const EncodedVideoPacket &Packet) { + if (auto Client = FindClientSession(ClientId); + Client != nullptr && Client->WebRtcSession != nullptr) { + Client->WebRtcSession->OnEncodedVideoPacket(Packet); + } +} +} // namespace Axiom diff --git a/Headless/RemoteViewportWebRtcSessionManager.h b/Headless/RemoteViewportWebRtcSessionManager.h new file mode 100644 index 00000000..c9fd39be --- /dev/null +++ b/Headless/RemoteViewportWebRtcSessionManager.h @@ -0,0 +1,82 @@ +#pragma once + +#include "RemoteViewportGizmoController.h" +#include "RemoteViewportGridSnap.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Axiom { +class RemoteViewportServer; + +struct RemoteClientSession { + struct PacketOutput; + + std::string ClientId; + SessionUserId User; + std::chrono::steady_clock::time_point LastActivity; + std::unique_ptr WebRtcSession; + std::unique_ptr VideoEncoder; + std::unique_ptr VideoPacketOutput; + std::optional GizmoDrag; + GizmoMode CurrentGizmoMode{GizmoMode::Translate}; + GridSnapSettings GridSnap; +}; + +struct ClientSessionResolution { + std::shared_ptr Session; + bool ResumedExisting{false}; +}; + +class RemoteViewportWebRtcSessionManager { +public: + explicit RemoteViewportWebRtcSessionManager(RemoteViewportServer &Server); + + size_t GetRemoteClientCount() const; + size_t GetActiveWebRtcSessionCount() const; + std::vector> + CollectPresenceEntries() const; + std::optional ResolveClientUser(std::string_view ClientId) const; + std::shared_ptr FindClientSession(std::string_view ClientId); + std::shared_ptr + FindClientSession(std::string_view ClientId) const; + WebRtcSessionStatus GetClientWebRtcStatus(std::string_view ClientId) const; + std::vector CollectClientWebRtcSessions() const; + ClientSessionResolution + CreateOrResumeClientSession(const std::optional &ClientIdHint); + void TouchClientSession(const std::string &ClientId); + + bool HandleSessionConnectRequest(uintptr_t ClientSocketValue, + std::string_view HeaderBlock, + std::string_view Body); + bool HandleWebRtcOfferRequest(uintptr_t ClientSocketValue, + std::string_view HeaderBlock, + std::string_view Body); + bool HandleWebRtcIceCandidateRequest(uintptr_t ClientSocketValue, + std::string_view HeaderBlock, + std::string_view Body); + bool HandleWebRtcCloseRequest(uintptr_t ClientSocketValue, + std::string_view HeaderBlock, + std::string_view Body); + void HandleViewportFrame(const ViewportFrame &Frame); + void HandleClientEncodedVideoPacket(std::string_view ClientId, + const EncodedVideoPacket &Packet); + +private: + RemoteViewportServer &m_Server; + mutable std::mutex m_RemoteClientMutex; + std::unordered_map> + m_RemoteClientsById; + uint64_t m_NextRemoteUserId{2}; +}; +} // namespace Axiom diff --git a/Headless/RemoteViewportWebSocketDispatch.cpp b/Headless/RemoteViewportWebSocketDispatch.cpp new file mode 100644 index 00000000..92516ceb --- /dev/null +++ b/Headless/RemoteViewportWebSocketDispatch.cpp @@ -0,0 +1,444 @@ +#include "RemoteViewportWebSocketDispatch.h" + +#include "HeadlessCommandProtocol.h" +#include "RemoteViewportGizmoController.h" +#include "RemoteViewportHttpRouter.h" +#include "RemoteViewportServer.h" +#include "RemoteViewportWebRtcSessionManager.h" + +#include +#include + +#include + +namespace Axiom { +namespace { +struct RemoteViewportWebSocketUserData { + uintptr_t ConnectionId{0}; +}; + +using UwsWebSocket = + uWS::WebSocket; + +bool HandleSetProperty(HeadlessSessionHost &Host, RemoteClientSession &Client, + const HeadlessCommand &Command) { + if (!Command.PropertyVal.has_value()) { + return false; + } + + const auto &Name = Command.PropertyName; + const auto &Val = *Command.PropertyVal; + const auto &ObjId = Command.ObjectId; + + if (Name == "displayName") { + if (const auto *S = std::get_if(&Val)) { + Host.SubmitRemoteCommand( + Client.User, + EditorCommand{RenameObjectCommand{.ObjectId = ObjId, .DisplayName = *S}}); + return true; + } + } else if (Name == "visible") { + if (const auto *B = std::get_if(&Val)) { + Host.SubmitRemoteCommand( + Client.User, + EditorCommand{SetObjectVisibilityCommand{.ObjectId = ObjId, .Visible = *B}}); + return true; + } + } else if (Name == "scriptClass") { + if (const auto *S = std::get_if(&Val)) { + if (S->empty()) { + Host.SubmitRemoteCommand( + Client.User, EditorCommand{DetachScriptCommand{.ObjectId = ObjId}}); + } else { + Host.SubmitRemoteCommand( + Client.User, EditorCommand{AttachScriptCommand{ + .ObjectId = ObjId, + .ScriptClassName = *S, + }}); + } + return true; + } + } else if (Name == "physicsBodyType" || Name == "physicsColliderType" || + Name == "physicsBoxHalfExtents" || Name == "physicsSphereRadius" || + Name == "physicsMass" || Name == "physicsFriction" || + Name == "physicsRestitution") { + const auto &DetailsById = Host.GetSessionModule() + .GetSession() + .GetState() + .Scene.ObjectDetailsById; + const auto It = DetailsById.find(ObjId); + if (It == DetailsById.end() || !It->second.SupportsTransform) { + return false; + } + + EditorPhysicsProperties Physics = + It->second.Physics.value_or(EditorPhysicsProperties{}); + if (Name == "physicsBodyType") { + const auto *S = std::get_if(&Val); + if (S == nullptr) return false; + if (*S == "none") Physics.BodyType = EditorPhysicsBodyType::None; + else if (*S == "static") Physics.BodyType = EditorPhysicsBodyType::Static; + else if (*S == "dynamic") Physics.BodyType = EditorPhysicsBodyType::Dynamic; + else return false; + } else if (Name == "physicsColliderType") { + const auto *S = std::get_if(&Val); + if (S == nullptr) return false; + if (*S == "none") Physics.ColliderType = EditorPhysicsColliderType::None; + else if (*S == "box") Physics.ColliderType = EditorPhysicsColliderType::Box; + else if (*S == "sphere") Physics.ColliderType = EditorPhysicsColliderType::Sphere; + else return false; + } else if (Name == "physicsBoxHalfExtents") { + const auto *V = std::get_if(&Val); + if (V == nullptr) return false; + Physics.BoxHalfExtents = *V; + } else if (Name == "physicsSphereRadius") { + const auto *Number = std::get_if(&Val); + if (Number == nullptr) return false; + Physics.SphereRadius = *Number; + } else if (Name == "physicsMass") { + const auto *Number = std::get_if(&Val); + if (Number == nullptr) return false; + Physics.Mass = *Number; + } else if (Name == "physicsFriction") { + const auto *Number = std::get_if(&Val); + if (Number == nullptr) return false; + Physics.Friction = *Number; + } else if (Name == "physicsRestitution") { + const auto *Number = std::get_if(&Val); + if (Number == nullptr) return false; + Physics.Restitution = *Number; + } + + Host.SubmitRemoteCommand( + Client.User, + EditorCommand{SetPhysicsPropertiesCommand{ + .ObjectId = ObjId, + .Physics = Physics, + }}); + return true; + } else if (Name == "location" || Name == "rotationDegrees" || Name == "scale") { + if (const auto *V = std::get_if(&Val)) { + const auto &DetailsById = Host.GetSessionModule() + .GetSession() + .GetState() + .Scene.ObjectDetailsById; + const auto It = DetailsById.find(ObjId); + if (It == DetailsById.end() || !It->second.Transform.has_value()) { + return false; + } + const EditorTransformDetails &Current = *It->second.Transform; + SetTransformCommand Cmd{ + .ObjectId = ObjId, + .Location = Current.Location, + .RotationDegrees = Current.RotationDegrees, + .Scale = Current.Scale, + }; + if (Name == "location") Cmd.Location = *V; + else if (Name == "rotationDegrees") Cmd.RotationDegrees = *V; + else Cmd.Scale = *V; + Host.SubmitRemoteCommand(Client.User, EditorCommand{Cmd}); + return true; + } + } + + return false; +} +} // namespace + +RemoteViewportWebSocketDispatch::RemoteViewportWebSocketDispatch( + RemoteViewportServer &Server) + : m_Server(Server) {} + +size_t RemoteViewportWebSocketDispatch::GetActiveClientCount() const { + std::scoped_lock Lock(m_WebSocketMutex); + return m_WebSocketClients.size(); +} + +uint64_t RemoteViewportWebSocketDispatch::GetTotalWebSocketMessages() const { + return m_TotalWebSocketMessages.load(); +} + +void RemoteViewportWebSocketDispatch::OnClientOpen(uintptr_t ConnectionId, + void *Socket) { + { + std::scoped_lock Lock(m_WebSocketMutex); + m_WebSocketClients.push_back( + {.ConnectionId = ConnectionId, .Socket = Socket, .IsOpen = true}); + } + std::cout << SerializeConnected() << std::endl; + SendTextMessage(ConnectionId, + SerializeReady(m_Server.m_Options.Width, m_Server.m_Options.Height)); + SendTextMessage(ConnectionId, SerializeConnected()); +} + +void RemoteViewportWebSocketDispatch::OnClientClose(uintptr_t ConnectionId) { + bool Removed = false; + { + std::scoped_lock Lock(m_WebSocketMutex); + auto It = std::find_if(m_WebSocketClients.begin(), m_WebSocketClients.end(), + [ConnectionId](const WebSocketClient &Client) { + return Client.ConnectionId == ConnectionId; + }); + if (It != m_WebSocketClients.end()) { + It->IsOpen = false; + m_WebSocketClients.erase(It); + Removed = true; + } + } + if (Removed) { + std::cout << SerializeDisconnected() << std::endl; + } +} + +void RemoteViewportWebSocketDispatch::CloseAllClients() { + std::scoped_lock Lock(m_WebSocketMutex); + for (auto &Client : m_WebSocketClients) { + Client.IsOpen = false; + } + m_WebSocketClients.clear(); +} + +void RemoteViewportWebSocketDispatch::BroadcastTextMessage(std::string Message) { + std::vector Clients; + { + std::scoped_lock Lock(m_WebSocketMutex); + for (const auto &Client : m_WebSocketClients) { + if (Client.IsOpen) { + Clients.push_back(Client.ConnectionId); + } + } + } + std::vector FailedClients; + for (const uintptr_t ClientSocketValue : Clients) { + if (!SendTextMessage(ClientSocketValue, Message)) { + FailedClients.push_back(ClientSocketValue); + } + } + for (const uintptr_t FailedClient : FailedClients) { + OnClientClose(FailedClient); + } +} + +bool RemoteViewportWebSocketDispatch::SendTextMessage( + uintptr_t ClientSocketValue, std::string_view Message) { + void *SocketHandle = nullptr; + { + std::scoped_lock Lock(m_WebSocketMutex); + const auto It = std::find_if( + m_WebSocketClients.begin(), m_WebSocketClients.end(), + [ClientSocketValue](const WebSocketClient &Client) { + return Client.ConnectionId == ClientSocketValue && Client.IsOpen; + }); + if (It == m_WebSocketClients.end()) { + return false; + } + SocketHandle = It->Socket; + } + auto *Socket = static_cast(SocketHandle); + std::scoped_lock Lock(m_SendMutex); + return Socket->send(Message, uWS::OpCode::TEXT) != UwsWebSocket::DROPPED; +} + +bool RemoteViewportWebSocketDispatch::SendBinaryMessage( + uintptr_t ClientSocketValue, const void *Data, size_t Size) { + void *SocketHandle = nullptr; + { + std::scoped_lock Lock(m_WebSocketMutex); + const auto It = std::find_if( + m_WebSocketClients.begin(), m_WebSocketClients.end(), + [ClientSocketValue](const WebSocketClient &Client) { + return Client.ConnectionId == ClientSocketValue && Client.IsOpen; + }); + if (It == m_WebSocketClients.end()) { + return false; + } + SocketHandle = It->Socket; + } + auto *Socket = static_cast(SocketHandle); + const std::string_view Payload(static_cast(Data), Size); + std::scoped_lock Lock(m_SendMutex); + return Socket->send(Payload, uWS::OpCode::BINARY) != UwsWebSocket::DROPPED; +} + +bool RemoteViewportWebSocketDispatch::HandleWebSocketMessage( + uintptr_t ClientSocketValue, std::string_view Payload) { + m_TotalWebSocketMessages.fetch_add(1); + std::string Error; + const auto Command = ParseRemoteViewportCommand(Payload, Error); + if (!Command.has_value()) { + return false; + } + + switch (Command->Type) { + case HeadlessCommandType::SetViewMode: + m_Server.m_Host.SetRemoteViewMode(Command->ViewMode); + return true; + case HeadlessCommandType::SetShowColliders: + m_Server.m_Host.SetRemoteShowColliders(Command->ShowColliders); + return true; + case HeadlessCommandType::DropMesh: + m_Server.m_GizmoController->HandleMeshDropCommand( + m_Server.m_Host.GetSessionModule().GetLocalUserId(), *Command); + return true; + case HeadlessCommandType::DropTexture: + m_Server.m_GizmoController->HandleTextureDropCommand( + m_Server.m_Host.GetSessionModule().GetLocalUserId(), *Command); + return true; + case HeadlessCommandType::PlaceActor: + m_Server.m_GizmoController->HandlePlaceActorCommand( + m_Server.m_Host.GetSessionModule().GetLocalUserId(), *Command); + return true; + case HeadlessCommandType::ListAssets: + SendTextMessage(ClientSocketValue, + SerializeAssetList(m_Server.m_HttpRouter->CollectVisibleAssets())); + return true; + case HeadlessCommandType::GetSchema: { + const auto &DetailsById = m_Server.m_Host.GetSessionModule() + .GetSession() + .GetState() + .Scene.ObjectDetailsById; + const auto It = DetailsById.find(Command->ObjectId); + if (It != DetailsById.end()) { + SendTextMessage(ClientSocketValue, SerializeObjectSchema(It->second)); + } + return true; + } + case HeadlessCommandType::SaveScene: { + const Assets::LocalAssetSource ContentDir{ + m_Server.m_HttpRouter->GetActiveContentDir()}; + const auto ScenePath = ContentDir.ResolveRelative("scene.json"); + const bool Ok = Assets::SaveSceneToFile( + ScenePath, m_Server.m_Host.GetSessionModule().GetSession().GetState().Scene); + SendTextMessage(ClientSocketValue, SerializeSaveResult(Ok)); + return true; + } + case HeadlessCommandType::Quit: + m_Server.m_StopRequested.store(true); + m_Server.m_Host.RequestClose(); + BroadcastTextMessage(SerializeShutdown()); + return true; + default: + return false; + } +} + +bool RemoteViewportWebSocketDispatch::HandleClientWebRtcMessage( + std::string_view ClientId, std::string_view Payload) { + m_TotalWebSocketMessages.fetch_add(1); + std::string Error; + const auto Command = ParseRemoteViewportCommand(Payload, Error); + if (!Command.has_value()) { + return false; + } + + auto Client = m_Server.m_WebRtcSessions->FindClientSession(ClientId); + if (Client == nullptr) { + return false; + } + m_Server.m_WebRtcSessions->TouchClientSession(Client->ClientId); + + switch (Command->Type) { + case HeadlessCommandType::SetViewMode: + m_Server.m_Host.SetRemoteViewMode(Client->User, Command->ViewMode); + return true; + case HeadlessCommandType::SetShowColliders: + m_Server.m_Host.SetRemoteShowColliders(Client->User, Command->ShowColliders); + return true; + case HeadlessCommandType::SetLookActive: + case HeadlessCommandType::SetViewportCameraPose: + case HeadlessCommandType::SetCameraProjection: + case HeadlessCommandType::UpdateViewportCamera: + case HeadlessCommandType::SelectObject: + case HeadlessCommandType::RenameObject: + case HeadlessCommandType::SetObjectVisibility: + case HeadlessCommandType::CreateObject: + case HeadlessCommandType::DuplicateObject: + case HeadlessCommandType::DeleteObject: + case HeadlessCommandType::ReparentObject: + case HeadlessCommandType::SetTransform: + case HeadlessCommandType::AttachScript: + case HeadlessCommandType::DetachScript: + case HeadlessCommandType::PlaySession: + case HeadlessCommandType::PauseSession: + case HeadlessCommandType::ResumeSession: + case HeadlessCommandType::StopSession: + case HeadlessCommandType::SetMeshAsset: + case HeadlessCommandType::SetLightProperties: + case HeadlessCommandType::SetMaterialProperties: + case HeadlessCommandType::SetMaterialTexture: + case HeadlessCommandType::SetWorldSettings: + m_Server.m_Host.SubmitRemoteCommand(Client->User, Command->EditorPayload); + return true; + case HeadlessCommandType::DropMesh: + m_Server.m_GizmoController->HandleMeshDropCommand(Client->User, *Command); + return true; + case HeadlessCommandType::DropTexture: + m_Server.m_GizmoController->HandleTextureDropCommand(Client->User, *Command); + return true; + case HeadlessCommandType::PlaceActor: + m_Server.m_GizmoController->HandlePlaceActorCommand(Client->User, *Command); + return true; + case HeadlessCommandType::ReloadScripts: + m_Server.m_Host.ReloadUserScripts(); + if (Client->WebRtcSession != nullptr) { + Client->WebRtcSession->SendReliableMessage( + "{\"type\":\"scripts_reloaded\"}"); + } + return true; + case HeadlessCommandType::Heartbeat: { + const EditorUserPresence *Presence = + m_Server.m_Host.GetSessionModule().GetSession().FindPresence(Client->User); + if (Presence != nullptr && Presence->State == EditorUserPresenceState::Away) { + m_Server.m_Host.GetSessionModule().GetSession().SetPresenceState( + Client->User, EditorUserPresenceState::Connected); + } + return true; + } + case HeadlessCommandType::ListAssets: + if (Client->WebRtcSession != nullptr) { + Client->WebRtcSession->SendReliableMessage( + SerializeAssetList(m_Server.m_HttpRouter->CollectVisibleAssets())); + } + return true; + case HeadlessCommandType::GetSchema: { + const auto &DetailsById = m_Server.m_Host.GetSessionModule() + .GetSession() + .GetState() + .Scene.ObjectDetailsById; + const auto It = DetailsById.find(Command->ObjectId); + if (It != DetailsById.end() && Client->WebRtcSession != nullptr) { + Client->WebRtcSession->SendReliableMessage(SerializeObjectSchema(It->second)); + } + return true; + } + case HeadlessCommandType::SaveScene: { + const Assets::LocalAssetSource ContentDir{ + m_Server.m_HttpRouter->GetActiveContentDir()}; + const auto ScenePath = ContentDir.ResolveRelative("scene.json"); + const bool Ok = Assets::SaveSceneToFile( + ScenePath, m_Server.m_Host.GetSessionModule().GetSession().GetState().Scene); + if (Client->WebRtcSession != nullptr) { + Client->WebRtcSession->SendReliableMessage(SerializeSaveResult(Ok)); + } + return true; + } + case HeadlessCommandType::SetProperty: + return HandleSetProperty(m_Server.m_Host, *Client, *Command); + case HeadlessCommandType::SetGizmoMode: + case HeadlessCommandType::SetGridSnap: + case HeadlessCommandType::GizmoHover: + case HeadlessCommandType::GizmoDragStart: + case HeadlessCommandType::GizmoDragUpdate: + case HeadlessCommandType::GizmoDragEnd: + return m_Server.m_GizmoController->HandleRemoteClientCommand(*Client, + *Command); + case HeadlessCommandType::Quit: + m_Server.m_StopRequested.store(true); + m_Server.m_Host.RequestClose(); + BroadcastTextMessage(SerializeShutdown()); + return true; + default: + return false; + } +} +} // namespace Axiom diff --git a/Headless/RemoteViewportWebSocketDispatch.h b/Headless/RemoteViewportWebSocketDispatch.h new file mode 100644 index 00000000..7fcd4862 --- /dev/null +++ b/Headless/RemoteViewportWebSocketDispatch.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Axiom { +class RemoteViewportServer; + +class RemoteViewportWebSocketDispatch { +public: + explicit RemoteViewportWebSocketDispatch(RemoteViewportServer &Server); + + size_t GetActiveClientCount() const; + uint64_t GetTotalWebSocketMessages() const; + + void OnClientOpen(uintptr_t ConnectionId, void *Socket); + void OnClientClose(uintptr_t ConnectionId); + void CloseAllClients(); + void BroadcastTextMessage(std::string Message); + bool SendTextMessage(uintptr_t ClientSocketValue, std::string_view Message); + bool SendBinaryMessage(uintptr_t ClientSocketValue, const void *Data, + size_t Size); + bool HandleWebSocketMessage(uintptr_t ClientSocketValue, + std::string_view Payload); + bool HandleClientWebRtcMessage(std::string_view ClientId, + std::string_view Payload); + +private: + struct WebSocketClient { + uintptr_t ConnectionId{static_cast(~0ull)}; + void *Socket{nullptr}; + bool IsOpen{true}; + }; + + RemoteViewportServer &m_Server; + mutable std::mutex m_WebSocketMutex; + std::vector m_WebSocketClients; + mutable std::mutex m_SendMutex; + std::atomic m_TotalWebSocketMessages{0}; +}; +} // namespace Axiom diff --git a/Headless/WebRtcSession.cpp b/Headless/WebRtcSession.cpp index 046561d6..6d69205a 100644 --- a/Headless/WebRtcSession.cpp +++ b/Headless/WebRtcSession.cpp @@ -1,6 +1,6 @@ #include "WebRtcSession.h" -#include +#include #include #include @@ -175,19 +175,7 @@ class StubWebRtcSession final : public IWebRtcSession { } std::string BuildDetail() const { -#if AXIOM_PLATFORM_MACOS -#if defined(AXIOM_ENABLE_WEBRTC) && AXIOM_ENABLE_WEBRTC - #if defined(AXIOM_WEBRTC_LINKED) && AXIOM_WEBRTC_LINKED - return "This build links an external native WebRTC binary and exposes the sender/signaling seam, but the concrete peer connection backend is not implemented yet."; - #else - return "This build reserves the WebRTC integration seam, but no external native WebRTC binary was linked."; - #endif -#else - return "This build was compiled without WebRTC support. Enable the AXIOM_ENABLE_WEBRTC CMake option for the macOS libwebrtc path."; -#endif -#else - return "The first WebRTC transport slice is macOS-only. This platform keeps the signaling seam compiled, but no native WebRTC backend is available."; -#endif + return HAL::DescribeWebRtcSupport(); } private: @@ -201,17 +189,10 @@ class StubWebRtcSession final : public IWebRtcSession { }; } // namespace -#if AXIOM_PLATFORM_MACOS && defined(AXIOM_ENABLE_WEBRTC) && AXIOM_ENABLE_WEBRTC && \ - defined(AXIOM_WEBRTC_LINKED) && AXIOM_WEBRTC_LINKED -std::unique_ptr CreateMacOSWebRtcSession(); -#endif - std::unique_ptr CreateWebRtcSession() { -#if AXIOM_PLATFORM_MACOS && defined(AXIOM_ENABLE_WEBRTC) && AXIOM_ENABLE_WEBRTC && \ - defined(AXIOM_WEBRTC_LINKED) && AXIOM_WEBRTC_LINKED - return CreateMacOSWebRtcSession(); -#else + if (auto Session = HAL::CreatePlatformWebRtcSession()) { + return Session; + } return std::make_unique(); -#endif } } // namespace Axiom diff --git a/Headless/WraithNetworkingModule.cpp b/Headless/WraithNetworkingModule.cpp new file mode 100644 index 00000000..d381bfe4 --- /dev/null +++ b/Headless/WraithNetworkingModule.cpp @@ -0,0 +1,99 @@ +#include "WraithNetworkingModule.h" + +#include "RemoteViewportServer.h" + +namespace Axiom { +namespace { +std::unique_ptr +MakeRemoteViewportServer(HeadlessSessionHost &Host, + const RemoteViewportServerOptions &Options) { + return std::make_unique(Host, Options); +} +} // namespace + +WraithNetworkingModule::WraithNetworkingModule( + HeadlessSessionHost &Host, const RemoteViewportServerOptions &Options, + bool Enabled) + : m_ServerFactory([&Host, Options]() { + return MakeRemoteViewportServer(Host, Options); + }), + m_Enabled(Enabled) {} + +WraithNetworkingModule::WraithNetworkingModule( + RemoteViewportServerFactory ServerFactory, bool Enabled) + : m_ServerFactory(std::move(ServerFactory)), m_Enabled(Enabled) {} + +std::string_view WraithNetworkingModule::GetName() const { return ModuleName; } + +bool WraithNetworkingModule::Initialize(Application &App) { + (void)App; + if (!m_Enabled) { + m_Server.reset(); + m_LastError.clear(); + m_InitializationState = WraithNetworkingInitializationState::Shutdown; + return true; + } + + m_InitializationState = WraithNetworkingInitializationState::Starting; + m_LastError.clear(); + m_Server = m_ServerFactory ? m_ServerFactory() : nullptr; + if (m_Server == nullptr) { + m_LastError = "No remote viewport server factory is configured."; + m_InitializationState = WraithNetworkingInitializationState::Failed; + return false; + } + + if (!m_Server->Start(m_LastError)) { + m_InitializationState = WraithNetworkingInitializationState::Failed; + return false; + } + + m_InitializationState = WraithNetworkingInitializationState::Initialized; + return true; +} + +void WraithNetworkingModule::Update(const ModuleUpdateContext &Context) { + (void)Context; +} + +void WraithNetworkingModule::Shutdown(Application &App) { + (void)App; + if (m_Server != nullptr) { + m_Server->Stop(); + m_Server.reset(); + } + m_InitializationState = WraithNetworkingInitializationState::Shutdown; +} + +bool WraithNetworkingModule::IsInitialized() const { + return m_InitializationState == WraithNetworkingInitializationState::Initialized; +} + +bool WraithNetworkingModule::ShouldStop() const { + return m_Server != nullptr && m_Server->ShouldStop(); +} + +WraithNetworkingStateSnapshot WraithNetworkingModule::GetStateSnapshot() const { + WraithNetworkingStateSnapshot Snapshot{}; + Snapshot.InitializationState = m_InitializationState; + Snapshot.Enabled = m_Enabled; + Snapshot.LastError = m_LastError; + if (m_Server != nullptr) { + Snapshot.Metrics = ConvertMetrics(m_Server->GetMetrics()); + } + return Snapshot; +} + +WraithNetworkingMetrics WraithNetworkingModule::ConvertMetrics( + const RemoteViewportServerMetrics &Metrics) { + return { + .TransportConnected = Metrics.TransportConnected, + .ListenPort = Metrics.ListenPort, + .ActiveWebSocketClients = Metrics.ActiveWebSocketClients, + .ActiveRemoteClients = Metrics.ActiveRemoteClients, + .ActiveWebRtcSessions = Metrics.ActiveWebRtcSessions, + .TotalHttpRequests = Metrics.TotalHttpRequests, + .TotalWebSocketMessages = Metrics.TotalWebSocketMessages, + }; +} +} // namespace Axiom diff --git a/Headless/WraithNetworkingModule.h b/Headless/WraithNetworkingModule.h new file mode 100644 index 00000000..f32d14ba --- /dev/null +++ b/Headless/WraithNetworkingModule.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace Axiom { +class Application; +class HeadlessSessionHost; +class IRemoteViewportServer; +struct RemoteViewportServerOptions; +struct RemoteViewportServerMetrics; + +enum class WraithNetworkingInitializationState { + Uninitialized, + Starting, + Initialized, + Failed, + Shutdown, +}; + +struct WraithNetworkingMetrics { + bool TransportConnected{false}; + uint16_t ListenPort{0}; + size_t ActiveWebSocketClients{0}; + size_t ActiveRemoteClients{0}; + size_t ActiveWebRtcSessions{0}; + uint64_t TotalHttpRequests{0}; + uint64_t TotalWebSocketMessages{0}; +}; + +struct WraithNetworkingStateSnapshot { + WraithNetworkingInitializationState InitializationState{ + WraithNetworkingInitializationState::Uninitialized}; + bool Enabled{true}; + std::string LastError; + WraithNetworkingMetrics Metrics; +}; + +using RemoteViewportServerFactory = std::function()>; + +class WraithNetworkingModule final : public IModule { +public: + static constexpr std::string_view ModuleName = "WraithNetworking"; + + WraithNetworkingModule(HeadlessSessionHost &Host, + const RemoteViewportServerOptions &Options, + bool Enabled = true); + explicit WraithNetworkingModule(RemoteViewportServerFactory ServerFactory, + bool Enabled = true); + + [[nodiscard]] std::string_view GetName() const override; + bool Initialize(Application &App) override; + void Update(const ModuleUpdateContext &Context) override; + void Shutdown(Application &App) override; + + [[nodiscard]] bool IsEnabled() const { return m_Enabled; } + [[nodiscard]] bool IsInitialized() const; + [[nodiscard]] bool ShouldStop() const; + [[nodiscard]] WraithNetworkingStateSnapshot GetStateSnapshot() const; + +private: + static WraithNetworkingMetrics + ConvertMetrics(const RemoteViewportServerMetrics &Metrics); + + RemoteViewportServerFactory m_ServerFactory; + std::unique_ptr m_Server; + bool m_Enabled{true}; + WraithNetworkingInitializationState m_InitializationState{ + WraithNetworkingInitializationState::Uninitialized}; + std::string m_LastError; +}; +} // namespace Axiom diff --git a/Projects/demo/Content/Cooked/AssetCookManifest.json b/Projects/demo/Content/Cooked/AssetCookManifest.json index e1c118b0..90b6a58b 100644 --- a/Projects/demo/Content/Cooked/AssetCookManifest.json +++ b/Projects/demo/Content/Cooked/AssetCookManifest.json @@ -1,32 +1,260 @@ { "entries": [ - {"assetId":11054845128086561483,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__0","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__0.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":1323443729801283033,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__0","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__0.wmat","formatVersion":1,"sourceHash":6585526972712796331}, - {"assetId":11068972388330914457,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__1","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__1.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":17753315465212080212,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__1","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__1.wmat","formatVersion":1,"sourceHash":18232101317138792969}, - {"assetId":6132156365919755870,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__2","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__2.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":8234543475717911528,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__2","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__2.wmat","formatVersion":1,"sourceHash":2828385059820120076}, - {"assetId":9215884269959433414,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__3","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__3.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":14998471815154488503,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__3","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__3.wmat","formatVersion":1,"sourceHash":4021005988200470709}, - {"assetId":11301533523077526917,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__4","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__4.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":11967514072175168108,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__4","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__4.wmat","formatVersion":1,"sourceHash":9824825244894527368}, - {"assetId":5696516304507048819,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__5","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__5.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":2657833261433452607,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__5","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__5.wmat","formatVersion":1,"sourceHash":2790757201892348790}, - {"assetId":15708674061080559810,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__6","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__6.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":223594801003485581,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__6","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__6.wmat","formatVersion":1,"sourceHash":6645031688116725003}, - {"assetId":11303169344647721679,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__7","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__7.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":8138644377090399950,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__7","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__7.wmat","formatVersion":1,"sourceHash":12341077000753935042}, - {"assetId":12939086642465158429,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__8","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__8.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":7571478219705300658,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__8","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__8.wmat","formatVersion":1,"sourceHash":8537203445773243884}, - {"assetId":9265197887256992175,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__9","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__9.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":14940576689237941493,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__9","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__9.wmat","formatVersion":1,"sourceHash":9489745460434497930}, - {"assetId":12838424781629803079,"kind":"texture","relativePath":"Generated/MeshTextures/warrior_cats_minecraft_firestar__10","cookedPath":"Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__10.wtex","formatVersion":2,"sourceHash":29466183609606694}, - {"assetId":6590305953151862635,"kind":"material","relativePath":"Generated/MeshMaterials/warrior_cats_minecraft_firestar__10","cookedPath":"Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__10.wmat","formatVersion":1,"sourceHash":6419014078988137037}, - {"assetId":4289732757022430848,"kind":"mesh","relativePath":"warrior_cats_minecraft_firestar.glb","cookedPath":"Cooked/warrior_cats_minecraft_firestar.wmesh","formatVersion":2,"sourceHash":9787426149011279238}, - {"assetId":1545219856950675401,"kind":"material","relativePath":"Generated/MeshMaterials/basicmesh__0","cookedPath":"Cooked/Generated/MeshMaterials/basicmesh__0.wmat","formatVersion":1,"sourceHash":17385510028530011617}, - {"assetId":15423180035666698351,"kind":"material","relativePath":"Generated/MeshMaterials/basicmesh__1","cookedPath":"Cooked/Generated/MeshMaterials/basicmesh__1.wmat","formatVersion":1,"sourceHash":5263286426569856486}, - {"assetId":2002519743011508280,"kind":"material","relativePath":"Generated/MeshMaterials/basicmesh__2","cookedPath":"Cooked/Generated/MeshMaterials/basicmesh__2.wmat","formatVersion":1,"sourceHash":5263286426569856486}, - {"assetId":6124137011624734461,"kind":"mesh","relativePath":"basicmesh.glb","cookedPath":"Cooked/basicmesh.wmesh","formatVersion":2,"sourceHash":10769525362242101033}, - {"assetId":6221181023279934479,"kind":"texture","relativePath":"sundowner_overlook_4k.hdr","cookedPath":"Cooked/sundowner_overlook_4k.wtex","formatVersion":2,"sourceHash":7609855777205962748} + { + "assetId": 11054845128086561483, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__0", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__0.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 1323443729801283033, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__0", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__0.wmat", + "formatVersion": 1, + "sourceHash": 6585526972712796331 + }, + { + "assetId": 11068972388330914457, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__1", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__1.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 17753315465212080212, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__1", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__1.wmat", + "formatVersion": 1, + "sourceHash": 18232101317138792969 + }, + { + "assetId": 6132156365919755870, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__2", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__2.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 8234543475717911528, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__2", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__2.wmat", + "formatVersion": 1, + "sourceHash": 2828385059820120076 + }, + { + "assetId": 9215884269959433414, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__3", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__3.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 14998471815154488503, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__3", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__3.wmat", + "formatVersion": 1, + "sourceHash": 4021005988200470709 + }, + { + "assetId": 11301533523077526917, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__4", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__4.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 11967514072175168108, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__4", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__4.wmat", + "formatVersion": 1, + "sourceHash": 9824825244894527368 + }, + { + "assetId": 5696516304507048819, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__5", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__5.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 2657833261433452607, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__5", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__5.wmat", + "formatVersion": 1, + "sourceHash": 2790757201892348790 + }, + { + "assetId": 15708674061080559810, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__6", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__6.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 223594801003485581, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__6", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__6.wmat", + "formatVersion": 1, + "sourceHash": 6645031688116725003 + }, + { + "assetId": 11303169344647721679, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__7", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__7.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 8138644377090399950, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__7", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__7.wmat", + "formatVersion": 1, + "sourceHash": 12341077000753935042 + }, + { + "assetId": 12939086642465158429, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__8", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__8.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 7571478219705300658, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__8", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__8.wmat", + "formatVersion": 1, + "sourceHash": 8537203445773243884 + }, + { + "assetId": 9265197887256992175, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__9", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__9.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 14940576689237941493, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__9", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__9.wmat", + "formatVersion": 1, + "sourceHash": 9489745460434497930 + }, + { + "assetId": 12838424781629803079, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__10", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__10.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 6590305953151862635, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__10", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__10.wmat", + "formatVersion": 1, + "sourceHash": 6419014078988137037 + }, + { + "assetId": 4289732757022430848, + "kind": "mesh", + "relativePath": "warrior_cats_minecraft_firestar.glb", + "cookedPath": "Cooked/warrior_cats_minecraft_firestar.wmesh", + "formatVersion": 3, + "sourceHash": 9787426149011279238 + }, + { + "assetId": 1545219856950675401, + "kind": "material", + "relativePath": "Generated/MeshMaterials/basicmesh__0", + "cookedPath": "Cooked/Generated/MeshMaterials/basicmesh__0.wmat", + "formatVersion": 1, + "sourceHash": 17385510028530011617 + }, + { + "assetId": 15423180035666698351, + "kind": "material", + "relativePath": "Generated/MeshMaterials/basicmesh__1", + "cookedPath": "Cooked/Generated/MeshMaterials/basicmesh__1.wmat", + "formatVersion": 1, + "sourceHash": 5263286426569856486 + }, + { + "assetId": 2002519743011508280, + "kind": "material", + "relativePath": "Generated/MeshMaterials/basicmesh__2", + "cookedPath": "Cooked/Generated/MeshMaterials/basicmesh__2.wmat", + "formatVersion": 1, + "sourceHash": 5263286426569856486 + }, + { + "assetId": 6124137011624734461, + "kind": "mesh", + "relativePath": "basicmesh.glb", + "cookedPath": "Cooked/basicmesh.wmesh", + "formatVersion": 3, + "sourceHash": 10769525362242101033 + }, + { + "assetId": 6221181023279934479, + "kind": "texture", + "relativePath": "sundowner_overlook_4k.hdr", + "cookedPath": "Cooked/sundowner_overlook_4k.wtex", + "formatVersion": 2, + "sourceHash": 7609855777205962748 + }, + { + "assetId": 8164181851900995658, + "kind": "texture", + "relativePath": "Generated/MeshTextures/warrior_cats_minecraft_firestar__shared_0", + "cookedPath": "Cooked/Generated/MeshTextures/warrior_cats_minecraft_firestar__shared_0.wtex", + "formatVersion": 2, + "sourceHash": 29466183609606694 + }, + { + "assetId": 8231817489589773986, + "kind": "material", + "relativePath": "Generated/MeshMaterials/warrior_cats_minecraft_firestar__shared_0", + "cookedPath": "Cooked/Generated/MeshMaterials/warrior_cats_minecraft_firestar__shared_0.wmat", + "formatVersion": 1, + "sourceHash": 13951828551931422539 + }, + { + "assetId": 7821889617419735525, + "kind": "material", + "relativePath": "Generated/MeshMaterials/basicmesh__shared_0", + "cookedPath": "Cooked/Generated/MeshMaterials/basicmesh__shared_0.wmat", + "formatVersion": 1, + "sourceHash": 17385510028530011617 + }, + { + "assetId": 3310334708335029040, + "kind": "material", + "relativePath": "Generated/MeshMaterials/basicmesh__shared_1", + "cookedPath": "Cooked/Generated/MeshMaterials/basicmesh__shared_1.wmat", + "formatVersion": 1, + "sourceHash": 5263286426569856486 + } ] } diff --git a/README.md b/README.md index b701b43e..9ec578f1 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ Wraith Engine is a C++/Vulkan game engine runtime paired with a browser-based ed Collaborative simulation is session-wide: when the simulation host presses `Play`, `Pause`, or `Stop`, every connected collaborator sees the same runtime state. In the current model, the first connected browser collaborator becomes the simulation host for that session, while the headless renderer keeps its reserved local render user. +The browser-facing transport is now encapsulated behind a dedicated `WraithNetworking` engine module. That module owns HTTP and WebSocket serving through vendored `uWebSockets`, while the existing WebRTC session path remains intact as the protected media/control layer used for streamed viewports. + +The engine's platform boundary now lives under a foundational `HAL/` layer. OS, hardware, and compiler-specific functionality routes through `AxiomHAL`, and higher-level engine modules no longer include platform headers or call platform APIs directly. + ## Architecture ``` @@ -25,6 +29,15 @@ AxiomRemoteViewportServer (C++) └─ EditorSession (authoritative scene state) ├─ Vulkan Renderer (offscreen, per-client) └─ ScriptHost (Coral .NET 9 / C# scripting) + +AxiomCore runtime flow + ├─ HAL (`AxiomHAL`: platform, dylib, sockets, file watch, SVG, media backends) + └─ ModuleManager (register / enable / disable / query modules) + ├─ Application modules (window polling, layer update, layer render, renderer frame) + ├─ Host modules (session transport, script host lifecycle, networking) + ├─ Editor feature modules (viewport input, selection, scene render) + ├─ Headless overlay module (billboards, colliders, presence, gizmo overlay state) + └─ EditorSession managers (command dispatch, scene-state coordination, physics lifecycle, validation) ``` ## Features @@ -32,7 +45,13 @@ AxiomRemoteViewportServer (C++) **Engine** - Vulkan rendering backend with MoltenVK on macOS - Headless offscreen rendering with H.264 encoding (VideoToolbox on macOS) +- Vulkan backend split into focused subsystems: `VulkanResourceManager`, `VulkanPipelineLibrary`, and `VulkanDrawSubmissionSystem`, with `VulkanRendererBackend` acting as the coordinator +- Texture uploads now use an async transfer-queue path with semaphore synchronization instead of stalling the main thread with synchronous immediate-submit work +- Hot-path mesh submission path avoids per-frame RTTI and reuses persistent scratch buffers in the Vulkan scene renderer - Authoritative command/event model for scene mutations +- Foundational engine module/plugin system with lifecycle-managed runtime modules and queryable active state +- Foundational `HAL/` platform layer with macOS implementations isolated under `HAL/MacOS/` +- `WraithNetworking` module for toggleable browser-facing transport, exposing initialization state plus connection metrics for future CVAR/config plumbing - DataModel scene hierarchy — folders, meshes, lights, cameras, actors - Transform gizmos (translate / scale / rotate) with server-side hit-testing - Multi-client rendering: each connected user gets their own viewport @@ -41,8 +60,10 @@ AxiomRemoteViewportServer (C++) - Scene persistence — `scene.json` save/load across restarts - Session-wide Play / Pause / Stop with authoritative edit-snapshot restore - Runtime-only Jolt physics stepping with pause / resume support +- Physics runtime boot now consumes a lightweight `RuntimeSceneState` snapshot instead of depending on editor-only scene headers - Default static box collision for imported mesh assets, with load-time migration for older meshes that had no authored physics yet - Configurable sky background — a two-color vertical gradient (compute-shader blended) or an equirectangular HDR (`.hdr`) sampled from a world-space ray; HDR data is preserved end-to-end as float pixels through a v2 cooked `.wtex` so future image-based lighting can reuse the same asset +- Mesh GPU upload now releases CPU-side vertex/index arrays by default; callers can opt back in with `MeshCreateOptions::KeepCpuData` for the rare CPU-readback/debug workflows that still need them **Browser editor** - Dockable panels: outliner, details/property inspector, content browser, toolbar, Place Actors, script editor, remote viewport @@ -60,6 +81,38 @@ AxiomRemoteViewportServer (C++) - Perspective / Orthographic viewport projection toggle; HDR skybox automatically falls back to gradient in orthographic mode - Place Actors panel: searchable, category-filtered panel with click-to-place and drag-to-viewport placement; shapes (Cube, Sphere, Cylinder, Cone, Plane) place a Mesh child inside an Actor wrapper; lights, cameras, and generic actors each place their appropriate type +**Runtime architecture** +- `IModule` / `ModuleManager` foundation for engine-owned feature registration, initialization, update, shutdown, active-state toggling, and future CVAR/config integration +- `AxiomHAL` now acts as the base platform dependency for `AxiomCore`, which then feeds editor and headless modules +- `Application` loop now executes module phases instead of hardcoding subsystem updates +- Core app flow, headless host flow, editor viewport flow, and headless overlay rendering are split into focused modules instead of living in monolithic layer/application classes +- Browser-facing networking is now routed through the standalone `WraithNetworking` module instead of being bootstrapped inline by `AxiomRemoteViewportServer` +- `WraithNetworking` uses vendored `uWebSockets` for HTTP/WebSocket transport while preserving the existing WebRTC communication layer inside the module boundary +- First-party OS-specific functionality is now isolated under `HAL/`: platform detection, Vulkan loader fallback/dynamic library access, socket helpers, script file watching, SVG rasterization, VideoToolbox H.264, and macOS WebRTC +- `EditorSession` is now a lightweight coordinator that delegates command routing to `EditorCommandDispatcher`, scene/tree/transform responsibilities to `EditorSceneStateManager`, physics lifecycle to `EditorPhysicsController`, and validation to `EditorSessionValidationModule` +- Physics is now isolated behind a runtime-only scene snapshot seam and controller boundary: editor-state extraction stays in the session subsystem, while `PhysicsWorld` consumes only transforms, collider shapes, and material indices +- Window minimization and Vulkan surface creation now live behind the `Window` / `RenderSurface` abstraction, so the Vulkan backend no longer owns a GLFW window pointer or directly queries GLFW iconification state +- Vulkan mesh teardown now routes through a standalone `GPUResourceQueue` instead of reaching into the renderer backend singleton during destruction +- The old 1,700+ line Vulkan renderer monolith is gone: resource lifetime, pipeline/layout caching, and draw/submission responsibilities now live in distinct renderer subsystems with narrower ownership boundaries + +## Rendering Data Path Notes + +- `MeshVertex` is now a compact 32-byte layout: `vec3 position`, `vec3 normal`, `vec2 uv`. This trims per-vertex CPU and GPU upload footprint from the previous 40-byte layout while still matching the renderer's Vulkan vertex-input stride. +- `RenderMeshSubmission` no longer stores a per-submission `std::string Name`. Diagnostic names now live in a separate debug-data registry so runtime submissions stay lean. +- The Vulkan scene renderer reuses persistent scratch vectors for candidate, opaque, translucent, and compute submission lists. `RenderScenePasses()` clears and reuses those buffers each frame instead of allocating fresh vectors in the hot loop. +- Mesh type resolution now happens when submissions are built or queued, so the per-frame render loop no longer performs `std::dynamic_pointer_cast` for every submitted mesh. +- `VulkanRendererBackend` is now GLFW-agnostic for frame begin and presentation setup: minimized-state checks and Vulkan surface creation are forwarded through runtime abstractions instead of hardcoded OS-library calls. +- `VulkanMesh` destruction no longer depends on `VulkanRendererBackend::TryGet()`. GPU buffer teardown is deferred through a shared `GPUResourceQueue`, which keeps resource lifetime independent from the backend singleton. +- Renderer responsibilities are now partitioned explicitly: `VulkanResourceManager` owns swapchain/images/buffers/descriptor-backed lifetime, `VulkanPipelineLibrary` owns raw Vulkan pipeline and layout caching, and `VulkanDrawSubmissionSystem` owns command recording, queue submission, offscreen capture publication, and async transfer synchronization. +- Async texture upload now routes through the transfer queue and a timeline-semaphore wait on graphics submission. This preserves the old rendered output while removing the main-thread stall from material and HDR texture uploads. +- Cooked mesh loading remains backward-compatible with older `.wmesh` payloads, but newly cooked assets use the current packed vertex layout. + +## Renderer Validation + +- `VulkanRendererBackend.cpp` was reduced from the previous 1,789-line monolith to a thin coordinator layer. +- A clean pre-refactor headless baseline and the refactored headless build both rendered the startup scene successfully. +- The first true captured startup-scene frame from baseline and refactor matched byte-for-byte in headless validation, confirming the subsystem split preserved rendered output. + ## Prerequisites - CMake 3.10+ @@ -122,7 +175,7 @@ cmake --build build/debug ### With scripting + automatic hot reload (macOS only) -The file watcher uses `kqueue` to detect `.dll` changes and reload without restarting the server: +The HAL file watcher uses a macOS `kqueue` backend to detect `.dll` changes and reload without restarting the server: ```bash cmake --preset debug \ @@ -148,6 +201,8 @@ cmake --preset debug \ -DAXIOM_WEBRTC_INCLUDE_DIR=/path/to/webrtc/include ``` +Note: the headless browser-facing server now uses vendored `uWebSockets` for HTTP/WebSocket handling and does not require a separate external HTTP/WebSocket library configuration step. + ### With tests ```bash @@ -186,7 +241,7 @@ cmake --build build/release |--------|------|---------|-------------| | `BUILD_TESTING` | `BOOL` | `OFF` | Build the Google Test suite | | `AXIOM_ENABLE_SCRIPTING` | `BOOL` | `OFF` | Enable the Coral C# scripting host (.NET 9) | -| `AXIOM_SCRIPTING_WATCH` | `BOOL` | `OFF` | Auto-reload user scripts on disk change (macOS kqueue). Requires `AXIOM_ENABLE_SCRIPTING=ON` | +| `AXIOM_SCRIPTING_WATCH` | `BOOL` | `OFF` | Auto-reload user scripts on disk change through the HAL file-watcher layer (current macOS backend uses `kqueue`). Requires `AXIOM_ENABLE_SCRIPTING=ON` | | `AXIOM_SCRIPTING_TRUST_DEFAULT` | `STRING` | `Restricted` | Default sandbox tier for user scripts. `Restricted` (hosted — blocks `System.Net.*`, `System.Reflection.Emit`, etc.) or `Trusted` (local dev — full BCL access) | | `AXIOM_ENABLE_WEBRTC` | `BOOL` | `OFF` | Enable the macOS WebRTC transport | | `AXIOM_ENABLE_PHYSICS` | `BOOL` | `ON` | Enable the JoltPhysics runtime simulation seam | @@ -212,6 +267,8 @@ cmake --build build/release --host 127.0.0.1 --port 8080 --width 1280 --height 720 ``` +At startup, `AxiomRemoteViewportServer` registers the toggleable `WraithNetworking` module with `ModuleManager`. That module owns transport initialization, publishes connection metrics/state snapshots, and keeps the existing WebRTC session logic active behind the same public server API. + ### Browser editor ```bash @@ -271,6 +328,7 @@ Open `http://localhost:3000` in your browser. | Path | Contents | |------|----------| | `Axiom/` | Engine library — core, session, renderer, remote transport, scripting host | +| `HAL/` | Foundational platform abstraction layer plus concrete macOS implementations | | `Editor/` | Native GLFW + ImGui editor executable | | `Headless/` | Headless runtime and `AxiomRemoteViewportServer` | | `Scripting/WraithEngine.Managed/` | C# engine API assembly (`Script`, `GameObject`, `Transform`) | diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index b98c28c0..f83ba47c 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -1,16 +1,25 @@ add_executable(AxiomTests CookedAssetTests.cpp + HeadlessScalabilityTests.cpp LayerTests.cpp HeadlessProtocolTests.cpp MeshPickingTests.cpp + ModuleManagerTests.cpp ProjectTests.cpp + RenderSubmissionTests.cpp SceneLifecycleTests.cpp - ../Headless/HeadlessSessionLayer.cpp + ThreadingTests.cpp + UWebSocketsVendorTests.cpp + WraithNetworkingModuleTests.cpp + ../Headless/HeadlessOverlayModule.cpp + ../Headless/HeadlessSessionHost.cpp + ../Headless/HeadlessSessionModule.cpp ../Headless/HeadlessCommandProtocol.cpp + ../Headless/HostModules.cpp ../Headless/WebRtcSession.cpp ) -if(APPLE) +if(APPLE AND EXISTS "${CMAKE_SOURCE_DIR}/Headless/MacOSWebRtcSession.mm") target_sources(AxiomTests PRIVATE ../Headless/MacOSWebRtcSession.mm ) @@ -18,8 +27,19 @@ endif() target_link_libraries(AxiomTests PRIVATE AxiomCore + AxiomScene + AxiomAssets + AxiomRenderer + AxiomNet + WraithNetworking GTest::gtest_main ) +if(TARGET AxiomPhysics) + target_link_libraries(AxiomTests PRIVATE AxiomPhysics) +endif() +if(TARGET AxiomScripting) + target_link_libraries(AxiomTests PRIVATE AxiomScripting) +endif() target_include_directories(AxiomTests PRIVATE "${CMAKE_SOURCE_DIR}/Headless" @@ -28,8 +48,19 @@ target_include_directories(AxiomTests PRIVATE target_compile_definitions(AxiomTests PRIVATE AXIOM_CONTENT_DIR="${CMAKE_SOURCE_DIR}/Content" AXIOM_PROJECTS_DIR="${CMAKE_SOURCE_DIR}/Projects" + AXIOM_WITH_PHYSICS=$,1,0> + AXIOM_WITH_SCRIPTING=$,1,0> + AXIOM_THREAD_SANITIZER=$,1,0> ) +if(AXIOM_ENABLE_TSAN) + target_compile_options(AxiomTests PRIVATE + -fsanitize=thread + -fno-omit-frame-pointer + ) + target_link_options(AxiomTests PRIVATE -fsanitize=thread) +endif() + # --------------------------------------------------------------------------- # Scripting tests — only compiled when the scripting subsystem is enabled # --------------------------------------------------------------------------- diff --git a/Tests/CookedAssetTests.cpp b/Tests/CookedAssetTests.cpp index 947813b1..25beb44a 100644 --- a/Tests/CookedAssetTests.cpp +++ b/Tests/CookedAssetTests.cpp @@ -7,11 +7,15 @@ #include #include #include +#include #include +#include #include #include #include +#include +#include namespace { @@ -40,6 +44,216 @@ void CopyFileChecked(const std::filesystem::path &From, ASSERT_FALSE(Ec); } +void WriteSharedMaterialValidationAsset(const std::filesystem::path &ContentRoot) { + EnsureTempDirectory(ContentRoot); + + const std::array, 24> Positions = {{ + {{-0.5f, -0.5f, 0.5f}}, + {{0.5f, -0.5f, 0.5f}}, + {{0.5f, 0.5f, 0.5f}}, + {{-0.5f, 0.5f, 0.5f}}, + {{-0.5f, -0.5f, -0.5f}}, + {{-0.5f, 0.5f, -0.5f}}, + {{0.5f, 0.5f, -0.5f}}, + {{0.5f, -0.5f, -0.5f}}, + {{-0.5f, 0.5f, -0.5f}}, + {{-0.5f, 0.5f, 0.5f}}, + {{0.5f, 0.5f, 0.5f}}, + {{0.5f, 0.5f, -0.5f}}, + {{-0.5f, -0.5f, -0.5f}}, + {{0.5f, -0.5f, -0.5f}}, + {{0.5f, -0.5f, 0.5f}}, + {{-0.5f, -0.5f, 0.5f}}, + {{0.5f, -0.5f, -0.5f}}, + {{0.5f, 0.5f, -0.5f}}, + {{0.5f, 0.5f, 0.5f}}, + {{0.5f, -0.5f, 0.5f}}, + {{-0.5f, -0.5f, -0.5f}}, + {{-0.5f, -0.5f, 0.5f}}, + {{-0.5f, 0.5f, 0.5f}}, + {{-0.5f, 0.5f, -0.5f}}, + }}; + const std::array, 24> Normals = {{ + {{0.0f, 0.0f, 1.0f}}, {{0.0f, 0.0f, 1.0f}}, {{0.0f, 0.0f, 1.0f}}, + {{0.0f, 0.0f, 1.0f}}, {{0.0f, 0.0f, -1.0f}}, {{0.0f, 0.0f, -1.0f}}, + {{0.0f, 0.0f, -1.0f}}, {{0.0f, 0.0f, -1.0f}}, {{0.0f, 1.0f, 0.0f}}, + {{0.0f, 1.0f, 0.0f}}, {{0.0f, 1.0f, 0.0f}}, {{0.0f, 1.0f, 0.0f}}, + {{0.0f, -1.0f, 0.0f}}, {{0.0f, -1.0f, 0.0f}}, {{0.0f, -1.0f, 0.0f}}, + {{0.0f, -1.0f, 0.0f}}, {{1.0f, 0.0f, 0.0f}}, {{1.0f, 0.0f, 0.0f}}, + {{1.0f, 0.0f, 0.0f}}, {{1.0f, 0.0f, 0.0f}}, {{-1.0f, 0.0f, 0.0f}}, + {{-1.0f, 0.0f, 0.0f}}, {{-1.0f, 0.0f, 0.0f}}, {{-1.0f, 0.0f, 0.0f}}, + }}; + const std::array, 24> UVs = {{ + {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}, + {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}, + {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}, + {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}, + {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}, + {{0.0f, 0.0f}}, {{1.0f, 0.0f}}, {{1.0f, 1.0f}}, {{0.0f, 1.0f}}, + }}; + const std::array Indices = { + 0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, + 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23, + }; + + std::vector Buffer; + auto AppendFloats = [&Buffer](const auto &Values) { + for (const float Value : Values) { + const auto *Bytes = reinterpret_cast(&Value); + Buffer.insert(Buffer.end(), Bytes, Bytes + sizeof(float)); + } + }; + auto AlignTo4 = [&Buffer]() { + while ((Buffer.size() % 4u) != 0u) { + Buffer.push_back(std::byte{0}); + } + }; + + const size_t PositionOffset = Buffer.size(); + for (const auto &Vertex : Positions) { + AppendFloats(Vertex); + } + AlignTo4(); + + const size_t NormalOffset = Buffer.size(); + for (const auto &Normal : Normals) { + AppendFloats(Normal); + } + AlignTo4(); + + const size_t UvOffset = Buffer.size(); + for (const auto &Uv : UVs) { + AppendFloats(Uv); + } + AlignTo4(); + + const size_t IndexOffset = Buffer.size(); + for (const uint16_t Index : Indices) { + const auto *Bytes = reinterpret_cast(&Index); + Buffer.insert(Buffer.end(), Bytes, Bytes + sizeof(uint16_t)); + } + + { + std::ofstream Bin(ContentRoot / "shared_materials_cube.bin", std::ios::binary); + ASSERT_TRUE(Bin.is_open()); + Bin.write(reinterpret_cast(Buffer.data()), + static_cast(Buffer.size())); + ASSERT_TRUE(Bin.good()); + } + + std::ostringstream Json; + Json << "{\n" + << " \"asset\": {\"version\": \"2.0\"},\n" + << " \"scene\": 0,\n" + << " \"scenes\": [{\"nodes\": ["; + for (int Index = 0; Index < 100; ++Index) { + if (Index != 0) { + Json << ", "; + } + Json << Index; + } + Json << "]}],\n" + << " \"nodes\": [\n"; + for (int Index = 0; Index < 100; ++Index) { + Json << " {\"mesh\": " << (Index % 3) << ", \"translation\": [" + << static_cast(Index % 10) * 2.0f << ", 0.0, " + << static_cast(Index / 10) * 2.0f << "]}"; + Json << (Index == 99 ? "\n" : ",\n"); + } + Json << " ],\n" + << " \"meshes\": [\n" + << " {\"primitives\": [{\"attributes\": {\"POSITION\": 0, \"NORMAL\": 1, " + "\"TEXCOORD_0\": 2}, \"indices\": 3, \"material\": 0}]},\n" + << " {\"primitives\": [{\"attributes\": {\"POSITION\": 0, \"NORMAL\": 1, " + "\"TEXCOORD_0\": 2}, \"indices\": 3, \"material\": 1}]},\n" + << " {\"primitives\": [{\"attributes\": {\"POSITION\": 0, \"NORMAL\": 1, " + "\"TEXCOORD_0\": 2}, \"indices\": 3, \"material\": 2}]}\n" + << " ],\n" + << " \"materials\": [\n" + << " {\"pbrMetallicRoughness\": {\"baseColorFactor\": [1.0, 0.2, 0.2, 1.0], " + "\"metallicFactor\": 0.0, \"roughnessFactor\": 0.8}},\n" + << " {\"pbrMetallicRoughness\": {\"baseColorFactor\": [0.2, 1.0, 0.2, 1.0], " + "\"metallicFactor\": 0.0, \"roughnessFactor\": 0.8}},\n" + << " {\"pbrMetallicRoughness\": {\"baseColorFactor\": [0.2, 0.2, 1.0, 1.0], " + "\"metallicFactor\": 0.0, \"roughnessFactor\": 0.8}}\n" + << " ],\n" + << " \"buffers\": [{\"uri\": \"shared_materials_cube.bin\", \"byteLength\": " + << Buffer.size() << "}],\n" + << " \"bufferViews\": [\n" + << " {\"buffer\": 0, \"byteOffset\": " << PositionOffset + << ", \"byteLength\": " << (Positions.size() * 3 * sizeof(float)) + << ", \"target\": 34962},\n" + << " {\"buffer\": 0, \"byteOffset\": " << NormalOffset + << ", \"byteLength\": " << (Normals.size() * 3 * sizeof(float)) + << ", \"target\": 34962},\n" + << " {\"buffer\": 0, \"byteOffset\": " << UvOffset + << ", \"byteLength\": " << (UVs.size() * 2 * sizeof(float)) + << ", \"target\": 34962},\n" + << " {\"buffer\": 0, \"byteOffset\": " << IndexOffset + << ", \"byteLength\": " << (Indices.size() * sizeof(uint16_t)) + << ", \"target\": 34963}\n" + << " ],\n" + << " \"accessors\": [\n" + << " {\"bufferView\": 0, \"componentType\": 5126, \"count\": 24, " + "\"type\": \"VEC3\", \"min\": [-0.5, -0.5, -0.5], \"max\": [0.5, 0.5, 0.5]},\n" + << " {\"bufferView\": 1, \"componentType\": 5126, \"count\": 24, " + "\"type\": \"VEC3\"},\n" + << " {\"bufferView\": 2, \"componentType\": 5126, \"count\": 24, " + "\"type\": \"VEC2\"},\n" + << " {\"bufferView\": 3, \"componentType\": 5123, \"count\": 36, " + "\"type\": \"SCALAR\", \"min\": [0], \"max\": [23]}\n" + << " ]\n" + << "}\n"; + + std::ofstream Gltf(ContentRoot / "shared_materials_cube.gltf"); + ASSERT_TRUE(Gltf.is_open()); + Gltf << Json.str(); + ASSERT_TRUE(Gltf.good()); +} + +void WriteSharedMaterialValidationScene(const std::filesystem::path &ScenePath) { + EnsureTempDirectory(ScenePath.parent_path()); + std::ofstream Scene(ScenePath); + ASSERT_TRUE(Scene.is_open()); + Scene << "{\n" + " \"version\": 1,\n" + " \"meshAsset\": \"\",\n" + " \"nodes\": [\n" + " {\"id\": \"world\", \"parentId\": null, \"displayName\": \"World\", " + "\"kind\": \"Folder\", \"visible\": true},\n" + " {\"id\": \"lighting\", \"parentId\": \"world\", \"displayName\": " + "\"Lighting\", \"kind\": \"Folder\", \"visible\": true},\n" + " {\"id\": \"directional-light\", \"parentId\": \"lighting\", " + "\"displayName\": \"DirectionalLight\", \"kind\": \"Light\", " + "\"visible\": true},\n" + " {\"id\": \"GridAsset\", \"parentId\": \"world\", \"displayName\": " + "\"GridAsset\", \"kind\": \"Mesh\", \"visible\": true}\n" + " ],\n" + " \"objects\": [\n" + " {\"id\": \"world\", \"displayName\": \"World\", \"kind\": \"Folder\", " + "\"visible\": true, \"isGeneratedAssetChild\": false, " + "\"supportsTransform\": false, \"transformReadOnly\": true},\n" + " {\"id\": \"lighting\", \"displayName\": \"Lighting\", " + "\"kind\": \"Folder\", \"visible\": true, \"isGeneratedAssetChild\": false, " + "\"supportsTransform\": false, \"transformReadOnly\": true},\n" + " {\"id\": \"directional-light\", \"displayName\": \"DirectionalLight\", " + "\"kind\": \"Light\", \"visible\": true, \"isGeneratedAssetChild\": false, " + "\"supportsTransform\": true, \"transformReadOnly\": false, " + "\"location\": [0.70909, 25.0, -8.0], \"rotationDegrees\": [-45.0, 30.0, 0.0], " + "\"scale\": [1.0, 1.0, 1.0], \"lightColor\": [1.0, 0.98, 0.92], " + "\"lightIntensity\": 4.0, \"lightDirection\": [0.35, 0.7, 0.2]},\n" + " {\"id\": \"GridAsset\", \"displayName\": \"GridAsset\", " + "\"kind\": \"Mesh\", \"visible\": true, \"isGeneratedAssetChild\": false, " + "\"supportsTransform\": true, \"transformReadOnly\": false, " + "\"location\": [-9.0, 0.0, -22.0], \"rotationDegrees\": [0.0, 0.0, 0.0], " + "\"scale\": [1.0, 1.0, 1.0], " + "\"assetRelativePath\": \"shared_materials_cube.gltf\"}\n" + " ],\n" + " \"meshNameToObjectId\": {}\n" + "}\n"; + ASSERT_TRUE(Scene.good()); +} + } // namespace TEST(CookedAssetTests, CookedMeshRoundTripsThroughBinaryFormat) { @@ -50,11 +264,11 @@ TEST(CookedAssetTests, CookedMeshRoundTripsThroughBinaryFormat) { Axiom::MeshData{ .Vertices = { - {.Position = {1.0f, 2.0f, 3.0f, 1.0f}, - .Normal = {0.0f, 1.0f, 0.0f, 0.0f}, + {.Position = {1.0f, 2.0f, 3.0f}, + .Normal = {0.0f, 1.0f, 0.0f}, .TexCoord = {0.25f, 0.75f}}, - {.Position = {4.0f, 5.0f, 6.0f, 1.0f}, - .Normal = {1.0f, 0.0f, 0.0f, 0.0f}, + {.Position = {4.0f, 5.0f, 6.0f}, + .Normal = {1.0f, 0.0f, 0.0f}, .TexCoord = {0.5f, 0.5f}}, }, .Indices = {0, 1, 0}, @@ -230,3 +444,192 @@ TEST(CookedAssetTests, CookMaterialAssetWritesManifestAndCookedLookupResolves) { EXPECT_FLOAT_EQ(Loaded->Metallic, 0.9f); EXPECT_EQ(Loaded->TextureAssetPath, "Engine/tf2 coconut.jpg"); } + +TEST(CookedAssetTests, + ToRuntimeMeshSceneDataReusesSharedCookedMaterialInstances) { + const auto TempRoot = MakeUniqueTempRoot("shared-cooked-materials"); + const auto ContentRoot = TempRoot / "Content"; + const auto CookedRoot = ContentRoot / "Cooked"; + EnsureTempDirectory(CookedRoot / "Generated/MeshMaterials"); + + const std::filesystem::path RelativeMaterialPath = + std::filesystem::path("Generated/MeshMaterials/shared-material"); + const std::filesystem::path CookedMaterialPath = + CookedRoot / "Generated/MeshMaterials/shared-material.wmat"; + + const Axiom::Assets::CookedMaterialData MaterialData{ + .BaseColorFactor = {0.2f, 0.7f, 0.4f, 1.0f}, + .Metallic = 0.1f, + .Roughness = 0.6f, + .TextureAssetPath = {}, + }; + ASSERT_TRUE(Axiom::Assets::SaveCookedMaterialAsset( + CookedMaterialPath, MaterialData, + Axiom::Assets::AssetIdFromRelativePath(RelativeMaterialPath))); + + Axiom::Assets::AssetCookManifest Manifest; + Manifest.Entries.push_back({ + .Id = Axiom::Assets::AssetIdFromRelativePath(RelativeMaterialPath), + .Kind = Axiom::Assets::AssetKind::Material, + .RelativePath = RelativeMaterialPath.generic_string(), + .CookedPath = "Cooked/Generated/MeshMaterials/shared-material.wmat", + .FormatVersion = Axiom::Assets::kCookedMaterialFormatVersion, + .SourceHash = 0, + }); + ASSERT_TRUE(Axiom::Assets::SaveAssetCookManifest( + CookedRoot / "AssetCookManifest.json", Manifest)); + + Axiom::Assets::CookedMeshSceneData Scene; + Scene.Instances.push_back({ + .Name = "A", + .MaterialAssetPath = RelativeMaterialPath.generic_string(), + .Mesh = {}, + .Transform = glm::mat4(1.0f), + }); + Scene.Instances.push_back({ + .Name = "B", + .MaterialAssetPath = RelativeMaterialPath.generic_string(), + .Mesh = {}, + .Transform = glm::mat4(1.0f), + }); + + const Axiom::MeshSceneData RuntimeScene = + Axiom::Assets::ToRuntimeMeshSceneData(Scene, ContentRoot); + ASSERT_EQ(RuntimeScene.Instances.size(), 2u); + ASSERT_NE(RuntimeScene.Instances[0].Material, nullptr); + ASSERT_NE(RuntimeScene.Instances[1].Material, nullptr); + EXPECT_EQ(RuntimeScene.Instances[0].Material, RuntimeScene.Instances[1].Material); + EXPECT_EQ(RuntimeScene.Instances[0].Material->BaseColorFactor, + MaterialData.BaseColorFactor); + EXPECT_FLOAT_EQ(RuntimeScene.Instances[0].Material->Metallic, + MaterialData.Metallic); + EXPECT_FLOAT_EQ(RuntimeScene.Instances[0].Material->Roughness, + MaterialData.Roughness); +} + +TEST(CookedAssetTests, + ToRuntimeMeshSceneDataCollapsesOneHundredInstancesToThreeSharedMaterials) { + const auto TempRoot = MakeUniqueTempRoot("hundred-shared-cooked-materials"); + const auto ContentRoot = TempRoot / "Content"; + const auto CookedRoot = ContentRoot / "Cooked"; + EnsureTempDirectory(CookedRoot / "Generated/MeshMaterials"); + + const std::array RelativeMaterialPaths = { + std::filesystem::path("Generated/MeshMaterials/shared-red"), + std::filesystem::path("Generated/MeshMaterials/shared-green"), + std::filesystem::path("Generated/MeshMaterials/shared-blue"), + }; + const std::array MaterialData = { + Axiom::Assets::CookedMaterialData{ + .BaseColorFactor = {1.0f, 0.2f, 0.2f, 1.0f}, + .Metallic = 0.0f, + .Roughness = 0.8f, + .TextureAssetPath = {}, + }, + Axiom::Assets::CookedMaterialData{ + .BaseColorFactor = {0.2f, 1.0f, 0.2f, 1.0f}, + .Metallic = 0.0f, + .Roughness = 0.8f, + .TextureAssetPath = {}, + }, + Axiom::Assets::CookedMaterialData{ + .BaseColorFactor = {0.2f, 0.2f, 1.0f, 1.0f}, + .Metallic = 0.0f, + .Roughness = 0.8f, + .TextureAssetPath = {}, + }, + }; + + Axiom::Assets::AssetCookManifest Manifest; + for (size_t MaterialIndex = 0; MaterialIndex < RelativeMaterialPaths.size(); + ++MaterialIndex) { + const std::filesystem::path CookedMaterialPath = + CookedRoot / "Generated/MeshMaterials" / + (RelativeMaterialPaths[MaterialIndex].filename().string() + ".wmat"); + ASSERT_TRUE(Axiom::Assets::SaveCookedMaterialAsset( + CookedMaterialPath, MaterialData[MaterialIndex], + Axiom::Assets::AssetIdFromRelativePath(RelativeMaterialPaths[MaterialIndex]))); + Manifest.Entries.push_back({ + .Id = Axiom::Assets::AssetIdFromRelativePath(RelativeMaterialPaths[MaterialIndex]), + .Kind = Axiom::Assets::AssetKind::Material, + .RelativePath = RelativeMaterialPaths[MaterialIndex].generic_string(), + .CookedPath = + (std::filesystem::path("Cooked/Generated/MeshMaterials") / + (RelativeMaterialPaths[MaterialIndex].filename().string() + ".wmat")) + .generic_string(), + .FormatVersion = Axiom::Assets::kCookedMaterialFormatVersion, + .SourceHash = 0, + }); + } + ASSERT_TRUE(Axiom::Assets::SaveAssetCookManifest( + CookedRoot / "AssetCookManifest.json", Manifest)); + + Axiom::Assets::CookedMeshSceneData Scene; + Scene.Instances.reserve(100); + for (size_t InstanceIndex = 0; InstanceIndex < 100; ++InstanceIndex) { + Scene.Instances.push_back({ + .Name = "Instance" + std::to_string(InstanceIndex), + .MaterialAssetPath = + RelativeMaterialPaths[InstanceIndex % RelativeMaterialPaths.size()] + .generic_string(), + .Mesh = {}, + .Transform = glm::mat4(1.0f), + }); + } + + const Axiom::MeshSceneData RuntimeScene = + Axiom::Assets::ToRuntimeMeshSceneData(Scene, ContentRoot); + ASSERT_EQ(RuntimeScene.Instances.size(), 100u); + + std::unordered_set UniqueMaterials; + for (size_t InstanceIndex = 0; InstanceIndex < RuntimeScene.Instances.size(); + ++InstanceIndex) { + const auto &Instance = RuntimeScene.Instances[InstanceIndex]; + ASSERT_NE(Instance.Material, nullptr); + UniqueMaterials.insert(Instance.Material.get()); + EXPECT_EQ(Instance.Material, + RuntimeScene.Instances[InstanceIndex % RelativeMaterialPaths.size()].Material); + } + + EXPECT_EQ(UniqueMaterials.size(), 3u); +} + +TEST(CookedAssetTests, + LoadBasicMeshAssetFromValidationSceneCollapsesOneHundredNodesToThreeMaterials) { + const auto TempRoot = MakeUniqueTempRoot("descriptor-shared-material-loader"); + const auto ContentRoot = TempRoot / "Content"; + WriteSharedMaterialValidationAsset(ContentRoot); + + const auto Loaded = + Axiom::Assets::LoadBasicMeshAsset(ContentRoot / "shared_materials_cube.gltf"); + ASSERT_TRUE(Loaded.has_value()); + ASSERT_EQ(Loaded->Instances.size(), 100u); + + std::unordered_set UniqueMaterials; + for (const auto &Instance : Loaded->Instances) { + ASSERT_NE(Instance.Material, nullptr); + UniqueMaterials.insert(Instance.Material.get()); + } + + EXPECT_EQ(UniqueMaterials.size(), 3u); +} + +TEST(CookedAssetTests, + LoadSceneFromValidationScenePreservesThreeSharedMaterialsAcrossExpandedMeshInstances) { + const auto TempRoot = MakeUniqueTempRoot("descriptor-shared-material-scene"); + const auto ContentRoot = TempRoot / "Content"; + WriteSharedMaterialValidationAsset(ContentRoot); + WriteSharedMaterialValidationScene(ContentRoot / "scene.json"); + + const auto Loaded = Axiom::Assets::LoadSceneFromFile(ContentRoot / "scene.json"); + ASSERT_TRUE(Loaded.has_value()); + ASSERT_EQ(Loaded->MeshInstances.size(), 100u); + + std::unordered_set UniqueMaterials; + for (const auto &Instance : Loaded->MeshInstances) { + ASSERT_NE(Instance.Material, nullptr); + UniqueMaterials.insert(Instance.Material.get()); + } + + EXPECT_EQ(UniqueMaterials.size(), 3u); +} diff --git a/Tests/HeadlessProtocolTests.cpp b/Tests/HeadlessProtocolTests.cpp index d9aca6f6..d9ea7eb4 100644 --- a/Tests/HeadlessProtocolTests.cpp +++ b/Tests/HeadlessProtocolTests.cpp @@ -1,10 +1,31 @@ #include +#include + #include "../Headless/HeadlessCommandProtocol.h" #include "../Headless/WebRtcSession.h" #include +namespace { + +rapidjson::Document ParseJson(const std::string &Json) { + rapidjson::Document Document; + Document.Parse(Json.c_str()); + EXPECT_FALSE(Document.HasParseError()); + EXPECT_TRUE(Document.IsObject()); + return Document; +} + +const rapidjson::Value &RequireMember(const rapidjson::Value &Object, + const char *Name) { + const auto It = Object.FindMember(Name); + EXPECT_NE(It, Object.MemberEnd()); + return It->value; +} + +} // namespace + TEST(HeadlessProtocolTests, ParsesSetLookActiveCommandWithCursorPosition) { std::string Error; const auto Command = Axiom::ParseHeadlessCommand( @@ -302,11 +323,24 @@ TEST(HeadlessProtocolTests, SerializesObjectTransformUpdatedEvent) { }}}; const std::string Json = Axiom::SerializeEvent(Event); - EXPECT_NE(Json.find("\"payloadType\":\"object_transform_updated\""), - std::string::npos); - EXPECT_NE(Json.find("\"location\":[1,2,3]"), std::string::npos); - EXPECT_NE(Json.find("\"rotationDegrees\":[4,5,6]"), std::string::npos); - EXPECT_NE(Json.find("\"scale\":[1,1.5,2]"), std::string::npos); + const rapidjson::Document Document = ParseJson(Json); + EXPECT_STREQ(RequireMember(Document, "payloadType").GetString(), + "object_transform_updated"); + const auto &Location = RequireMember(Document, "location"); + ASSERT_TRUE(Location.IsArray()); + EXPECT_FLOAT_EQ(Location[0].GetFloat(), 1.0f); + EXPECT_FLOAT_EQ(Location[1].GetFloat(), 2.0f); + EXPECT_FLOAT_EQ(Location[2].GetFloat(), 3.0f); + const auto &RotationDegrees = RequireMember(Document, "rotationDegrees"); + ASSERT_TRUE(RotationDegrees.IsArray()); + EXPECT_FLOAT_EQ(RotationDegrees[0].GetFloat(), 4.0f); + EXPECT_FLOAT_EQ(RotationDegrees[1].GetFloat(), 5.0f); + EXPECT_FLOAT_EQ(RotationDegrees[2].GetFloat(), 6.0f); + const auto &Scale = RequireMember(Document, "scale"); + ASSERT_TRUE(Scale.IsArray()); + EXPECT_FLOAT_EQ(Scale[0].GetFloat(), 1.0f); + EXPECT_FLOAT_EQ(Scale[1].GetFloat(), 1.5f); + EXPECT_FLOAT_EQ(Scale[2].GetFloat(), 2.0f); } TEST(HeadlessProtocolTests, SerializesRuntimeStateChangedEvent) { @@ -412,36 +446,52 @@ TEST(HeadlessProtocolTests, SerializesSessionSnapshot) { const std::string Json = Axiom::SerializeSessionSnapshot( State, Axiom::SessionUserId{1}, true, true, "connected", "connected"); - EXPECT_NE(Json.find("\"type\":\"session_snapshot\""), std::string::npos); - EXPECT_NE(Json.find("\"currentUserId\":1"), std::string::npos); - EXPECT_NE(Json.find("\"runtimeControllerUserId\":1"), std::string::npos); - EXPECT_NE(Json.find("\"showColliders\":true"), std::string::npos); - EXPECT_NE(Json.find("\"runtimeState\":\"edit\""), std::string::npos); - EXPECT_NE(Json.find("\"participants\""), std::string::npos); - EXPECT_NE(Json.find("\"displayName\":\"Local User\""), std::string::npos); - EXPECT_NE(Json.find("\"presenceState\":\"connected\""), std::string::npos); - EXPECT_NE(Json.find("\"selectionObjectId\":\"PlayerCharacter\""), - std::string::npos); - EXPECT_NE(Json.find("\"camera\":{\"position\":[1,2,3],\"yawDegrees\":-90"), - std::string::npos); - EXPECT_NE(Json.find("\"pitchDegrees\":0"), std::string::npos); - EXPECT_NE(Json.find("\"objectId\":\"PlayerCharacter\""), std::string::npos); - EXPECT_NE(Json.find("\"displayName\":\"World\""), std::string::npos); - EXPECT_NE(Json.find("\"kind\":\"actor\""), std::string::npos); - EXPECT_NE(Json.find("\"selectedObjectDetails\""), std::string::npos); - EXPECT_NE(Json.find("\"supportsTransform\":true"), std::string::npos); - EXPECT_NE(Json.find("\"transformReadOnly\":true"), std::string::npos); - EXPECT_NE(Json.find("\"location\":[1,2,3]"), std::string::npos); - EXPECT_NE(Json.find("\"physics\":{\"bodyType\":\"dynamic\""), - std::string::npos); - EXPECT_NE(Json.find("\"colliderType\":\"sphere\""), std::string::npos); - EXPECT_NE(Json.find("\"sphereRadius\":1.25"), std::string::npos); - EXPECT_NE(Json.find("\"mass\":3.5"), std::string::npos); - EXPECT_NE(Json.find("\"friction\":0.6"), std::string::npos); - EXPECT_NE(Json.find("\"restitution\":0.4"), std::string::npos); - EXPECT_NE(Json.find("\"selectedByUserIds\":[1]"), std::string::npos); - EXPECT_NE(Json.find("\"lockState\":\"locked\""), std::string::npos); - EXPECT_NE(Json.find("\"lockOwnerUserId\":1"), std::string::npos); + const rapidjson::Document Document = ParseJson(Json); + EXPECT_STREQ(RequireMember(Document, "type").GetString(), "session_snapshot"); + EXPECT_EQ(RequireMember(Document, "currentUserId").GetUint64(), 1u); + EXPECT_EQ(RequireMember(Document, "runtimeControllerUserId").GetUint64(), 1u); + EXPECT_TRUE(RequireMember(Document, "showColliders").GetBool()); + EXPECT_STREQ(RequireMember(Document, "runtimeState").GetString(), "edit"); + const auto &Participants = RequireMember(Document, "participants"); + ASSERT_TRUE(Participants.IsArray()); + ASSERT_FALSE(Participants.Empty()); + const auto &Participant = Participants[0]; + EXPECT_STREQ(RequireMember(Participant, "displayName").GetString(), "Local User"); + EXPECT_STREQ(RequireMember(Participant, "presenceState").GetString(), "connected"); + EXPECT_STREQ(RequireMember(Participant, "selectionObjectId").GetString(), + "PlayerCharacter"); + const auto &CameraJson = RequireMember(Participant, "camera"); + const auto &Position = RequireMember(CameraJson, "position"); + EXPECT_FLOAT_EQ(Position[0].GetFloat(), 1.0f); + EXPECT_FLOAT_EQ(Position[1].GetFloat(), 2.0f); + EXPECT_FLOAT_EQ(Position[2].GetFloat(), 3.0f); + EXPECT_FLOAT_EQ(RequireMember(CameraJson, "yawDegrees").GetFloat(), -90.0f); + EXPECT_FLOAT_EQ(RequireMember(CameraJson, "pitchDegrees").GetFloat(), 0.0f); + const auto &SelectedObjectDetails = RequireMember(Document, "selectedObjectDetails"); + EXPECT_STREQ(RequireMember(SelectedObjectDetails, "objectId").GetString(), + "PlayerCharacter"); + const auto &Capabilities = RequireMember(SelectedObjectDetails, "capabilities"); + EXPECT_TRUE(RequireMember(Capabilities, "supportsTransform").GetBool()); + EXPECT_TRUE(RequireMember(Capabilities, "transformReadOnly").GetBool()); + const auto &Transform = RequireMember(SelectedObjectDetails, "transform"); + const auto &SelectedLocation = RequireMember(Transform, "location"); + EXPECT_FLOAT_EQ(SelectedLocation[0].GetFloat(), 1.0f); + EXPECT_FLOAT_EQ(SelectedLocation[1].GetFloat(), 2.0f); + EXPECT_FLOAT_EQ(SelectedLocation[2].GetFloat(), 3.0f); + const auto &Physics = RequireMember(SelectedObjectDetails, "physics"); + EXPECT_STREQ(RequireMember(Physics, "bodyType").GetString(), "dynamic"); + EXPECT_STREQ(RequireMember(Physics, "colliderType").GetString(), "sphere"); + EXPECT_FLOAT_EQ(RequireMember(Physics, "sphereRadius").GetFloat(), 1.25f); + EXPECT_FLOAT_EQ(RequireMember(Physics, "mass").GetFloat(), 3.5f); + EXPECT_FLOAT_EQ(RequireMember(Physics, "friction").GetFloat(), 0.6f); + EXPECT_FLOAT_EQ(RequireMember(Physics, "restitution").GetFloat(), 0.4f); + const auto &Collaboration = RequireMember(SelectedObjectDetails, "collaboration"); + const auto &SelectedByUserIds = RequireMember(Collaboration, "selectedByUserIds"); + ASSERT_TRUE(SelectedByUserIds.IsArray()); + ASSERT_EQ(SelectedByUserIds.Size(), 1u); + EXPECT_EQ(SelectedByUserIds[0].GetUint64(), 1u); + EXPECT_STREQ(RequireMember(Collaboration, "lockState").GetString(), "locked"); + EXPECT_EQ(RequireMember(Collaboration, "lockOwnerUserId").GetUint64(), 1u); } TEST(HeadlessProtocolTests, SerializesSessionConnectResponse) { @@ -468,6 +518,43 @@ TEST(HeadlessProtocolTests, SerializesSessionConnectResponse) { EXPECT_NE(Json.find("\"currentUserId\":7"), std::string::npos); } +TEST(HeadlessProtocolTests, SerializesAssetListWithEngineAndProjectAssets) { + std::vector Assets; + Assets.push_back(Axiom::Assets::AssetDescriptor{ + .Id = Axiom::AssetId{1}, + .Name = "mesh", + .Kind = Axiom::Assets::AssetKind::Mesh, + .RelativePath = "Meshes/mesh.glb", + }); + Assets.push_back(Axiom::Assets::AssetDescriptor{ + .Id = Axiom::AssetId{2}, + .Name = "default", + .Kind = Axiom::Assets::AssetKind::Material, + .RelativePath = "Engine/Materials/default.mat", + }); + + const rapidjson::Document Document = ParseJson(Axiom::SerializeAssetList(Assets)); + EXPECT_EQ(RequireMember(Document, "type").GetString(), std::string("asset_list")); + const auto &SerializedAssets = RequireMember(Document, "assets"); + ASSERT_TRUE(SerializedAssets.IsArray()); + ASSERT_EQ(SerializedAssets.Size(), 2u); + EXPECT_EQ(RequireMember(SerializedAssets[0], "path").GetString(), + std::string("Meshes/mesh.glb")); + EXPECT_EQ(RequireMember(SerializedAssets[1], "path").GetString(), + std::string("Engine/Materials/default.mat")); + EXPECT_EQ(RequireMember(SerializedAssets[1], "kind").GetString(), + std::string("texture")); +} + +TEST(HeadlessProtocolTests, SerializesSaveResultSuccessAndFailure) { + const rapidjson::Document Success = ParseJson(Axiom::SerializeSaveResult(true)); + EXPECT_EQ(RequireMember(Success, "type").GetString(), std::string("scene_saved")); + + const rapidjson::Document Failure = ParseJson(Axiom::SerializeSaveResult(false)); + EXPECT_EQ(RequireMember(Failure, "type").GetString(), + std::string("scene_save_failed")); +} + TEST(HeadlessProtocolTests, SerializesEncodedVideoPacketMetadata) { const Axiom::EncodedVideoPacket Packet{ .Codec = Axiom::EncodedVideoCodec::H264, @@ -720,10 +807,12 @@ TEST(HeadlessProtocolTests, SerializesMaterialPropertiesChangedEvent) { }}}; const std::string Json = Axiom::SerializeEvent(Event); - EXPECT_NE(Json.find("\"payloadType\":\"material_properties_changed\""), std::string::npos); - EXPECT_NE(Json.find("\"objectId\":\"crate-1\""), std::string::npos); - EXPECT_NE(Json.find("\"metallic\":0.9"), std::string::npos); - EXPECT_NE(Json.find("\"roughness\":0.1"), std::string::npos); + const rapidjson::Document Document = ParseJson(Json); + EXPECT_STREQ(RequireMember(Document, "payloadType").GetString(), + "material_properties_changed"); + EXPECT_STREQ(RequireMember(Document, "objectId").GetString(), "crate-1"); + EXPECT_FLOAT_EQ(RequireMember(Document, "metallic").GetFloat(), 0.9f); + EXPECT_FLOAT_EQ(RequireMember(Document, "roughness").GetFloat(), 0.1f); } TEST(HeadlessProtocolTests, SerializesObjectDetailsWithMaterial) { @@ -745,10 +834,18 @@ TEST(HeadlessProtocolTests, SerializesObjectDetailsWithMaterial) { const std::string Json = Axiom::SerializeSessionSnapshot( State, Axiom::SessionUserId{1}, true, true, "connected", "connected"); - EXPECT_NE(Json.find("\"material\":{"), std::string::npos); - EXPECT_NE(Json.find("\"baseColorFactor\":[0.5,0.5,0.5,1]"), std::string::npos); - EXPECT_NE(Json.find("\"metallic\":0"), std::string::npos); - EXPECT_NE(Json.find("\"roughness\":0.5"), std::string::npos); + const rapidjson::Document Document = ParseJson(Json); + const auto &Material = RequireMember(RequireMember(Document, "selectedObjectDetails"), + "material"); + ASSERT_TRUE(Material.IsObject()); + const auto &BaseColorFactor = RequireMember(Material, "baseColorFactor"); + ASSERT_TRUE(BaseColorFactor.IsArray()); + EXPECT_FLOAT_EQ(BaseColorFactor[0].GetFloat(), 0.5f); + EXPECT_FLOAT_EQ(BaseColorFactor[1].GetFloat(), 0.5f); + EXPECT_FLOAT_EQ(BaseColorFactor[2].GetFloat(), 0.5f); + EXPECT_FLOAT_EQ(BaseColorFactor[3].GetFloat(), 1.0f); + EXPECT_FLOAT_EQ(RequireMember(Material, "metallic").GetFloat(), 0.0f); + EXPECT_FLOAT_EQ(RequireMember(Material, "roughness").GetFloat(), 0.5f); } TEST(HeadlessProtocolTests, SerializesObjectDetailsWithNullMaterialForLights) { diff --git a/Tests/HeadlessScalabilityTests.cpp b/Tests/HeadlessScalabilityTests.cpp new file mode 100644 index 00000000..6fc6efd9 --- /dev/null +++ b/Tests/HeadlessScalabilityTests.cpp @@ -0,0 +1,178 @@ +#include + +#include +#include "../Headless/HeadlessSessionHost.h" + +#include +#include +#include + +namespace { +struct ScenarioResult { + Axiom::HeadlessRuntimeInstrumentationSnapshot Snapshot; + std::vector LastScheduledViews; +}; + +const Axiom::HeadlessClientCadenceSnapshot *FindClient( + const Axiom::HeadlessRuntimeInstrumentationSnapshot &Snapshot, + std::string_view ClientId) { + for (const auto &Client : Snapshot.ClientCadence) { + if (Client.ClientId == ClientId) { + return &Client; + } + } + return nullptr; +} + +ScenarioResult RunSchedulingScenario( + size_t TickCount, + const std::vector> &RemoteClients, + const std::function &PerTick = + {}) { + Axiom::HeadlessRuntimeInstrumentation::Reset(); + + Axiom::HeadlessRenderViewRegistry Registry(Axiom::SessionUserId{1}); + for (const auto &[ClientId, User] : RemoteClients) { + Registry.UpsertRemoteView(ClientId, User); + } + + std::vector ScheduledViews; + for (size_t Tick = 1; Tick <= TickCount; ++Tick) { + if (PerTick) { + PerTick(Tick, Registry); + } + + ScheduledViews = Axiom::HeadlessSessionHost::BuildScheduledRenderPassViews( + Registry, Axiom::SessionUserId{1}); + Axiom::HeadlessRuntimeInstrumentation::RecordHeadlessTick( + Tick, ScheduledViews.size(), Registry.GetRemoteViewCount()); + for (size_t PassIndex = 0; PassIndex < ScheduledViews.size(); ++PassIndex) { + const auto &View = ScheduledViews[PassIndex]; + Axiom::HeadlessRuntimeInstrumentation::RecordHeadlessRenderPass( + Tick, PassIndex, View.ClientId, View.User, View.IsLocal); + } + } + + return { + .Snapshot = Axiom::HeadlessRuntimeInstrumentation::GetSnapshot(), + .LastScheduledViews = std::move(ScheduledViews), + }; +} +} // namespace + +TEST(HeadlessScalabilityTests, SingleRemoteClientUsesOneRenderPassPerTick) { + if (!Axiom::HeadlessRuntimeInstrumentation::IsEnabled()) { + GTEST_SKIP() << "Headless runtime instrumentation is compiled out in this build."; + } + + const ScenarioResult Result = RunSchedulingScenario( + 5, {{"client-a", Axiom::SessionUserId{7}}}); + + ASSERT_EQ(Result.LastScheduledViews.size(), 1u); + EXPECT_EQ(Result.Snapshot.EngineTickCount, 5u); + EXPECT_EQ(Result.Snapshot.LastTickRenderPassCount, 1u); + EXPECT_EQ(Result.Snapshot.TotalRenderPasses, 5u); + EXPECT_EQ(Result.Snapshot.ActiveRemoteClientCount, 1u); + + const auto *Client = FindClient(Result.Snapshot, "client-a"); + ASSERT_NE(Client, nullptr); + EXPECT_EQ(Client->RenderPassCount, 5u); + EXPECT_EQ(Client->LastEngineTick, 5u); + EXPECT_EQ(Client->MaxTicksBetweenRenders, 1u); +} + +TEST(HeadlessScalabilityTests, MultipleRemoteClientsDoNotForceFullRateRendering) { + if (!Axiom::HeadlessRuntimeInstrumentation::IsEnabled()) { + GTEST_SKIP() << "Headless runtime instrumentation is compiled out in this build."; + } + + const ScenarioResult Result = RunSchedulingScenario( + 4, {{"client-a", Axiom::SessionUserId{7}}, + {"client-b", Axiom::SessionUserId{8}}, + {"client-c", Axiom::SessionUserId{9}}}); + + ASSERT_EQ(Result.LastScheduledViews.size(), 1u); + EXPECT_EQ(Result.Snapshot.EngineTickCount, 4u); + EXPECT_EQ(Result.Snapshot.LastTickRenderPassCount, 1u); + EXPECT_EQ(Result.Snapshot.TotalRenderPasses, 8u); + EXPECT_EQ(Result.Snapshot.ActiveRemoteClientCount, 3u); + + for (const std::string ClientId : {"client-a", "client-b", "client-c"}) { + const auto *Client = FindClient(Result.Snapshot, ClientId); + ASSERT_NE(Client, nullptr); + EXPECT_GE(Client->RenderPassCount, 2u); + EXPECT_LE(Client->MaxTicksBetweenRenders, + Axiom::HeadlessRenderViewRegistry::IdleRenderIntervalTicks); + } +} + +TEST(HeadlessScalabilityTests, + DirtyAndRecentlyActiveRemoteClientsRenderMoreOftenThanIdleClients) { + if (!Axiom::HeadlessRuntimeInstrumentation::IsEnabled()) { + GTEST_SKIP() << "Headless runtime instrumentation is compiled out in this build."; + } + + const ScenarioResult Result = RunSchedulingScenario( + 6, {{"active-client", Axiom::SessionUserId{7}}, + {"idle-client", Axiom::SessionUserId{8}}}, + [](size_t Tick, Axiom::HeadlessRenderViewRegistry &Registry) { + Registry.SetRemoteViewMode( + "active-client", + (Tick % 2u) == 0u ? Axiom::RendererViewMode::Wireframe + : Axiom::RendererViewMode::Lit); + }); + + EXPECT_EQ(Result.Snapshot.LastTickRenderPassCount, 1u); + EXPECT_EQ(Result.Snapshot.TotalRenderPasses, 9u); + EXPECT_EQ(Result.Snapshot.ActiveRemoteClientCount, 2u); + + const auto *ActiveClient = FindClient(Result.Snapshot, "active-client"); + const auto *IdleClient = FindClient(Result.Snapshot, "idle-client"); + ASSERT_NE(ActiveClient, nullptr); + ASSERT_NE(IdleClient, nullptr); + EXPECT_GT(ActiveClient->RenderPassCount, IdleClient->RenderPassCount); + EXPECT_LE(ActiveClient->MaxTicksBetweenRenders, 2u); + EXPECT_LE(IdleClient->MaxTicksBetweenRenders, + Axiom::HeadlessRenderViewRegistry::IdleRenderIntervalTicks); +} + +TEST(HeadlessScalabilityTests, DirtySharedScenePromotesAllRemoteClients) { + if (!Axiom::HeadlessRuntimeInstrumentation::IsEnabled()) { + GTEST_SKIP() << "Headless runtime instrumentation is compiled out in this build."; + } + + const ScenarioResult Result = RunSchedulingScenario( + 3, {{"client-a", Axiom::SessionUserId{7}}, + {"client-b", Axiom::SessionUserId{8}}, + {"client-c", Axiom::SessionUserId{9}}}, + [](size_t Tick, Axiom::HeadlessRenderViewRegistry &Registry) { + if (Tick == 3u) { + Registry.MarkAllRemoteViewsDirty(); + } + }); + + EXPECT_EQ(Result.Snapshot.TotalRenderPasses, 9u); + for (const std::string ClientId : {"client-a", "client-b", "client-c"}) { + const auto *Client = FindClient(Result.Snapshot, ClientId); + ASSERT_NE(Client, nullptr); + EXPECT_EQ(Client->RenderPassCount, 3u); + } +} + +TEST(HeadlessScalabilityTests, ReadbackCountersRoundTripThroughInstrumentation) { + if (!Axiom::HeadlessRuntimeInstrumentation::IsEnabled()) { + GTEST_SKIP() << "Headless runtime instrumentation is compiled out in this build."; + } + + Axiom::HeadlessRuntimeInstrumentation::Reset(); + Axiom::HeadlessRuntimeInstrumentation::RecordPendingOffscreenReadbacks(0u); + Axiom::HeadlessRuntimeInstrumentation::RecordOffscreenReadbackSubmitted( + 10u, Axiom::SessionUserId{7}, 1u); + Axiom::HeadlessRuntimeInstrumentation::RecordOffscreenReadbackCompleted( + 10u, Axiom::SessionUserId{7}, 0u); + + const auto Snapshot = Axiom::HeadlessRuntimeInstrumentation::GetSnapshot(); + EXPECT_EQ(Snapshot.PendingOffscreenReadbacks, 0u); + EXPECT_EQ(Snapshot.TotalOffscreenReadbacksSubmitted, 1u); + EXPECT_EQ(Snapshot.TotalOffscreenReadbacksCompleted, 1u); +} diff --git a/Tests/LayerTests.cpp b/Tests/LayerTests.cpp index 5da59762..3d6c59cb 100644 --- a/Tests/LayerTests.cpp +++ b/Tests/LayerTests.cpp @@ -8,7 +8,7 @@ #include #include #include "../Headless/HeadlessRenderView.h" -#include "../Headless/HeadlessSessionLayer.h" +#include "../Headless/HeadlessSessionModule.h" #include "../Headless/HeadlessViewportFrameBridge.h" #include #include @@ -78,7 +78,13 @@ class FakeInputPlatform final : public Axiom::IInputPlatform { Axiom::CursorMode ModeSet{Axiom::CursorMode::Normal}; }; -class DummyMesh final : public Axiom::Mesh {}; +class DummyMesh final : public Axiom::Mesh { +public: + DummyMesh() { AssignHandle(Axiom::MeshHandle{s_NextHandleValue++}); } + +private: + inline static uint64_t s_NextHandleValue{1}; +}; class RecordingEndpointSubscriber final : public Axiom::ISessionTransportSubscriber { @@ -222,7 +228,7 @@ TEST(EditorSessionTests, CameraMovementUpdatesOnlySessionOwnedState) { const Axiom::Camera ExpectedBefore = Session.FindViewport(Axiom::SessionUserId{7})->Camera; Axiom::Camera Expected = ExpectedBefore; - Expected.MoveLocal(glm::vec3(1.5f, -0.25f, 0.75f)); + Expected.MoveWorld(glm::vec3(1.5f, -0.25f, 0.75f)); Session.Submit(MakeContext(), {.Payload = Axiom::UpdateViewportCameraCommand{ @@ -298,8 +304,8 @@ TEST(EditorSessionTests, CommandsDrainInFifoOrder) { Session.Subscribe(&Subscriber); Session.EnsureViewportState(Axiom::SessionUserId{7}); Axiom::Camera Expected = Session.FindViewport(Axiom::SessionUserId{7})->Camera; - Expected.MoveLocal(glm::vec3(1.0f, 0.0f, 0.0f)); - Expected.MoveLocal(glm::vec3(0.0f, 2.0f, 0.0f)); + Expected.MoveWorld(glm::vec3(1.0f, 0.0f, 0.0f)); + Expected.MoveWorld(glm::vec3(0.0f, 2.0f, 0.0f)); Session.Submit(MakeContext(1), {.Payload = Axiom::UpdateViewportCameraCommand{ @@ -819,6 +825,8 @@ TEST(EditorInputSourceTests, GlfwInputSourceTranslatesPlatformStateIntoCommands) const auto &CameraCommand = std::get( Sink.Commands[1].Command.Payload); EXPECT_GT(glm::dot(CameraCommand.WorldMovement, CameraCommand.WorldMovement), 0.0f); + EXPECT_GT(CameraCommand.WorldMovement.x, 0.0f); + EXPECT_LT(CameraCommand.WorldMovement.z, 0.0f); ASSERT_TRUE(CameraCommand.CursorPosition.has_value()); EXPECT_EQ(*CameraCommand.CursorPosition, glm::dvec2(12.0, 24.0)); } @@ -1012,6 +1020,59 @@ TEST(RemoteViewportTests, HeadlessViewportFrameBridgePreservesTaggedRenderUser) EXPECT_EQ(Output.LastUser.Value, 11u); } +TEST(RemoteViewportTests, + HeadlessViewportFrameBridgePreservesDistinctTaggedUsersAcrossFrames) { + class RecordingFrameOutput final : public Axiom::IViewportFrameOutput { + public: + void OnViewportFrame(const Axiom::ViewportFrame &Frame) override { + FrameIndices.push_back(Frame.FrameIndex); + Users.push_back(Frame.User); + } + + std::vector FrameIndices; + std::vector Users; + }; + + RecordingFrameOutput Output; + Axiom::HeadlessViewportFrameBridge Bridge( + Output, []() -> std::optional { + return Axiom::HeadlessRenderViewState{ + .ClientId = "resolver-client", + .User = Axiom::SessionUserId{99}, + .ViewMode = Axiom::RendererViewMode::Wireframe, + .IsLocal = false, + }; + }); + + std::array Bytes{std::byte{0x01}, std::byte{0x02}, + std::byte{0x03}, std::byte{0x04}, + std::byte{0x05}, std::byte{0x06}, + std::byte{0x07}, std::byte{0x08}}; + Bridge.OnViewportFrame({ + .FrameIndex = 101, + .Width = 1, + .Height = 1, + .Format = Axiom::ViewportFrameFormat::R8G8B8A8Unorm, + .Pixels = std::span(Bytes.data(), 4u), + .User = Axiom::SessionUserId{7}, + }); + Bridge.OnViewportFrame({ + .FrameIndex = 102, + .Width = 1, + .Height = 1, + .Format = Axiom::ViewportFrameFormat::R8G8B8A8Unorm, + .Pixels = std::span(Bytes.data() + 4u, 4u), + .User = Axiom::SessionUserId{8}, + }); + + ASSERT_EQ(Output.FrameIndices.size(), 2u); + ASSERT_EQ(Output.Users.size(), 2u); + EXPECT_EQ(Output.FrameIndices[0], 101u); + EXPECT_EQ(Output.Users[0].Value, 7u); + EXPECT_EQ(Output.FrameIndices[1], 102u); + EXPECT_EQ(Output.Users[1].Value, 8u); +} + TEST(RemoteViewportTests, AxiomEndpointForwardsEventsAndFrames) { Axiom::EditorSession Session(Axiom::SessionId{1}); Axiom::AxiomSessionEndpoint Endpoint(Session); @@ -1212,9 +1273,9 @@ TEST(RenderSceneTests, RenderCommandSubmitsLightBillboardOverlay) { EXPECT_FLOAT_EQ(Scene.LightBillboards.front().PixelSize, 40.0f); } -TEST(HeadlessSessionLayerTests, BuildLightBillboardsUsesVisibleLightsOnly) { - Axiom::HeadlessSessionLayer Layer; - Layer.GetSession().SetObjectDetails({ +TEST(HeadlessSessionModuleTests, BuildLightBillboardsUsesVisibleLightsOnly) { + Axiom::HeadlessSessionModule Module; + Module.GetSession().SetObjectDetails({ { .ObjectId = "light-a", .DisplayName = "Light A", @@ -1263,7 +1324,7 @@ TEST(HeadlessSessionLayerTests, BuildLightBillboardsUsesVisibleLightsOnly) { }); const std::vector Billboards = - Layer.BuildLightBillboards(); + Module.BuildLightBillboards(); ASSERT_EQ(Billboards.size(), 2u); const auto WorldTransformBillboard = std::find_if( @@ -1291,11 +1352,11 @@ TEST(HeadlessSessionLayerTests, BuildLightBillboardsUsesVisibleLightsOnly) { Billboards.end()); } -TEST(HeadlessSessionLayerTests, BuildColliderOverlaySubmissionsUsesPhysicsData) { - Axiom::HeadlessSessionLayer Layer; - Layer.SetColliderMeshesForTesting(std::make_shared(), - std::make_shared()); - Layer.GetSession().SetObjectDetails({ +TEST(HeadlessSessionModuleTests, BuildColliderOverlaySubmissionsUsesPhysicsData) { + Axiom::HeadlessSessionModule Module; + Module.SetColliderMeshesForTesting(std::make_shared(), + std::make_shared()); + Module.GetSession().SetObjectDetails({ { .ObjectId = "static-box", .DisplayName = "Static Box", @@ -1354,7 +1415,7 @@ TEST(HeadlessSessionLayerTests, BuildColliderOverlaySubmissionsUsesPhysicsData) }); const std::vector Submissions = - Layer.BuildColliderOverlaySubmissions(); + Module.BuildColliderOverlaySubmissions(); ASSERT_EQ(Submissions.size(), 18u); const size_t TranslucentCount = static_cast(std::count_if( @@ -1366,30 +1427,38 @@ TEST(HeadlessSessionLayerTests, BuildColliderOverlaySubmissionsUsesPhysicsData) const auto StaticIt = std::find_if( Submissions.begin(), Submissions.end(), [](const Axiom::RenderMeshSubmission &Submission) { - return Submission.Name == "static-box-collider"; + return Axiom::GetRenderMeshSubmissionDebugName(Submission.DebugDataId) == + "static-box-collider"; }); ASSERT_NE(StaticIt, Submissions.end()); EXPECT_TRUE(StaticIt->Translucent); EXPECT_FLOAT_EQ(StaticIt->Transform[3].x, 1.0f); EXPECT_FLOAT_EQ(StaticIt->Transform[3].y, 2.0f); EXPECT_FLOAT_EQ(StaticIt->Transform[3].z, 3.0f); - EXPECT_NE(StaticIt->Material, nullptr); - EXPECT_GT(StaticIt->Material->BaseColorFactor.g, 0.8f); + const Axiom::MaterialInstance *StaticMaterial = + Module.GetColliderMaterialForTesting(Axiom::EditorPhysicsBodyType::Static); + ASSERT_NE(StaticMaterial, nullptr); + EXPECT_TRUE(StaticIt->MaterialHandle.IsValid() || StaticMaterial != nullptr); + EXPECT_GT(StaticMaterial->BaseColorFactor.g, 0.8f); EXPECT_GT(glm::length(glm::vec3(StaticIt->Transform[0])), 3.0f); const auto DynamicIt = std::find_if( Submissions.begin(), Submissions.end(), [](const Axiom::RenderMeshSubmission &Submission) { - return Submission.Name == "dynamic-sphere-collider"; + return Axiom::GetRenderMeshSubmissionDebugName(Submission.DebugDataId) == + "dynamic-sphere-collider"; }); ASSERT_NE(DynamicIt, Submissions.end()); EXPECT_TRUE(DynamicIt->Translucent); EXPECT_FLOAT_EQ(DynamicIt->Transform[3].x, -3.0f); EXPECT_FLOAT_EQ(DynamicIt->Transform[3].y, 5.0f); EXPECT_FLOAT_EQ(DynamicIt->Transform[3].z, 8.0f); - EXPECT_NE(DynamicIt->Material, nullptr); - EXPECT_GT(DynamicIt->Material->BaseColorFactor.r, 0.9f); - EXPECT_LT(DynamicIt->Material->BaseColorFactor.a, 0.5f); + const Axiom::MaterialInstance *DynamicMaterial = + Module.GetColliderMaterialForTesting(Axiom::EditorPhysicsBodyType::Dynamic); + ASSERT_NE(DynamicMaterial, nullptr); + EXPECT_TRUE(DynamicIt->MaterialHandle.IsValid() || DynamicMaterial != nullptr); + EXPECT_GT(DynamicMaterial->BaseColorFactor.r, 0.9f); + EXPECT_LT(DynamicMaterial->BaseColorFactor.a, 0.5f); } TEST(SvgTextureTests, LightbulbSvgRasterizesToValidTexture) { diff --git a/Tests/ModuleManagerTests.cpp b/Tests/ModuleManagerTests.cpp new file mode 100644 index 00000000..3bbd9c57 --- /dev/null +++ b/Tests/ModuleManagerTests.cpp @@ -0,0 +1,241 @@ +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { +class FakeWindow final : public Axiom::Window { +public: + FakeWindow() : Window("Module Test Window", 320, 200) {} + + void PollEvents() override { ++PollCount; } + bool IsKeyPressed(int Key) const override { + (void)Key; + return false; + } + bool IsMouseButtonPressed(int Button) const override { + (void)Button; + return false; + } + void GetCursorPosition(double &X, double &Y) const override { + X = 0.0; + Y = 0.0; + } + void SetCursorMode(Axiom::CursorMode Mode) override { CursorMode = Mode; } + [[nodiscard]] Axiom::CursorMode GetCursorMode() const override { + return CursorMode; + } + [[nodiscard]] bool ShouldClose() const override { return Closed; } + [[nodiscard]] bool IsMinimized() const override { return Minimized; } + void RequestClose() override { Closed = true; } + [[nodiscard]] void *GetNativeHandle() const override { return nullptr; } + [[nodiscard]] bool + SupportsPresentationBackend(Axiom::PresentationBackendType Backend) const + override { + (void)Backend; + return false; + } + Axiom::PresentationSurfaceResult + CreatePresentationSurface(Axiom::PresentationBackendType Backend, + void *Instance, void *Surface) const override { + (void)Backend; + (void)Instance; + (void)Surface; + return Axiom::PresentationSurfaceResult::InitializationFailed; + } + + size_t PollCount{0}; + bool Closed{false}; + bool Minimized{false}; + Axiom::CursorMode CursorMode{Axiom::CursorMode::Normal}; +}; + +class RecordingModule final : public Axiom::IModule { +public: + explicit RecordingModule(std::string Name, bool InitializeResult = true) + : m_Name(std::move(Name)), m_InitializeResult(InitializeResult) {} + + [[nodiscard]] std::string_view GetName() const override { return m_Name; } + + bool Initialize(Axiom::Application &App) override { + (void)App; + ++InitializeCalls; + return m_InitializeResult; + } + + void Update(const Axiom::ModuleUpdateContext &Context) override { + ObservedPhases.push_back(Context.Phase); + ObservedPasses.push_back(Context.RenderPassIndex); + ObservedFrameIndices.push_back(Context.FrameIndex); + } + + void Shutdown(Axiom::Application &App) override { + (void)App; + ++ShutdownCalls; + } + + size_t InitializeCalls{0}; + size_t ShutdownCalls{0}; + std::vector ObservedPhases; + std::vector ObservedPasses; + std::vector ObservedFrameIndices; + +private: + std::string m_Name; + bool m_InitializeResult{true}; +}; + +class ModuleTestApplication final : public Axiom::Application { +public: + explicit ModuleTestApplication( + Axiom::RuntimeMode Mode = Axiom::RuntimeMode::HeadlessEditorSession) + : Application( + {.Title = "Module Test App", + .Width = 320, + .Height = 200, + .Mode = Mode}, + {.Arguments = nullptr, .ArgumentCount = 0}, + {.Window = std::make_unique(), + .RenderSurface = + std::make_shared(320, 200), + .Renderer = nullptr, + .InitializeRenderer = false, + .RegisterDefaultModules = false}) {} + + void SetRenderPassCount(size_t Count) { m_RenderPassCount = Count; } + void RequestTestClose() { RequestClose(); } + + FakeWindow &GetFakeWindow() { + return static_cast(*GetWindow()); + } + +protected: + size_t BeginRenderPasses() override { return m_RenderPassCount; } + +private: + size_t m_RenderPassCount{1}; +}; + +TEST(ModuleManagerTests, TracksModuleStatesAndSupportsActivationQueries) { + ModuleTestApplication App; + + auto Primary = std::make_unique("Primary"); + RecordingModule *PrimaryPtr = Primary.get(); + EXPECT_TRUE(App.GetModuleManager().RegisterModule(std::move(Primary))); + + auto Disabled = std::make_unique("Disabled"); + RecordingModule *DisabledPtr = Disabled.get(); + EXPECT_TRUE( + App.GetModuleManager().RegisterModule(std::move(Disabled), false)); + + auto Failed = std::make_unique("Failed", false); + RecordingModule *FailedPtr = Failed.get(); + EXPECT_TRUE(App.GetModuleManager().RegisterModule(std::move(Failed))); + + ASSERT_EQ(PrimaryPtr->InitializeCalls, 1u); + ASSERT_EQ(DisabledPtr->InitializeCalls, 1u); + ASSERT_EQ(FailedPtr->InitializeCalls, 1u); + + EXPECT_TRUE(App.GetModuleManager().HasModule("Primary")); + EXPECT_TRUE(App.GetModuleManager().IsModuleActive("Primary")); + EXPECT_FALSE(App.GetModuleManager().IsModuleActive("Disabled")); + EXPECT_FALSE(App.GetModuleManager().IsModuleActive("Failed")); + + const auto PrimaryState = App.GetModuleManager().GetModuleState("Primary"); + ASSERT_TRUE(PrimaryState.has_value()); + EXPECT_TRUE(PrimaryState->IsLoaded); + EXPECT_TRUE(PrimaryState->IsActive); + EXPECT_EQ(PrimaryState->Lifecycle, + Axiom::ModuleLifecycleState::Initialized); + + const auto DisabledState = App.GetModuleManager().GetModuleState("Disabled"); + ASSERT_TRUE(DisabledState.has_value()); + EXPECT_TRUE(DisabledState->IsLoaded); + EXPECT_FALSE(DisabledState->IsActive); + + const auto FailedState = App.GetModuleManager().GetModuleState("Failed"); + ASSERT_TRUE(FailedState.has_value()); + EXPECT_FALSE(FailedState->IsLoaded); + EXPECT_FALSE(FailedState->IsActive); + EXPECT_EQ(FailedState->Lifecycle, Axiom::ModuleLifecycleState::Failed); + + const std::vector States = + App.GetModuleManager().GetModuleStates(); + ASSERT_EQ(States.size(), 3u); + EXPECT_TRUE(std::ranges::any_of(States, [](const Axiom::ModuleState &State) { + return State.Name == "Primary" && State.IsLoaded && State.IsActive; + })); + + EXPECT_TRUE(App.GetModuleManager().SetModuleActive("Disabled", true)); + EXPECT_TRUE(App.GetModuleManager().IsModuleActive("Disabled")); + + EXPECT_FALSE(App.GetModuleManager().RegisterModule( + std::make_unique("Primary"))); + EXPECT_FALSE(App.GetModuleManager().SetModuleActive("Missing", true)); +} + +TEST(ModuleManagerTests, ApplicationStepRunsThroughActiveModulesOnly) { + ModuleTestApplication App; + App.SetRenderPassCount(2); + + auto Active = std::make_unique("Active"); + RecordingModule *ActivePtr = Active.get(); + ASSERT_TRUE(App.GetModuleManager().RegisterModule(std::move(Active))); + + auto Inactive = std::make_unique("Inactive"); + RecordingModule *InactivePtr = Inactive.get(); + ASSERT_TRUE(App.GetModuleManager().RegisterModule(std::move(Inactive), false)); + + EXPECT_TRUE(App.Step()); + + ASSERT_EQ(ActivePtr->ObservedPhases.size(), 8u); + EXPECT_EQ(ActivePtr->ObservedPhases[0], Axiom::ModuleUpdatePhase::FrameStart); + EXPECT_EQ(ActivePtr->ObservedPhases[1], Axiom::ModuleUpdatePhase::RenderBegin); + EXPECT_EQ(ActivePtr->ObservedPhases[2], Axiom::ModuleUpdatePhase::Render); + EXPECT_EQ(ActivePtr->ObservedPhases[3], Axiom::ModuleUpdatePhase::RenderEnd); + EXPECT_EQ(ActivePtr->ObservedPhases[4], Axiom::ModuleUpdatePhase::RenderBegin); + EXPECT_EQ(ActivePtr->ObservedPhases[5], Axiom::ModuleUpdatePhase::Render); + EXPECT_EQ(ActivePtr->ObservedPhases[6], Axiom::ModuleUpdatePhase::ImGuiRender); + EXPECT_EQ(ActivePtr->ObservedPhases[7], Axiom::ModuleUpdatePhase::RenderEnd); + EXPECT_EQ(ActivePtr->ObservedPasses, + (std::vector{0u, 0u, 0u, 0u, 1u, 1u, 1u, 1u})); + EXPECT_TRUE(std::all_of(ActivePtr->ObservedFrameIndices.begin(), + ActivePtr->ObservedFrameIndices.end(), + [](uint64_t FrameIndex) { return FrameIndex == 1u; })); + + EXPECT_TRUE(InactivePtr->ObservedPhases.empty()); + + App.RequestTestClose(); + EXPECT_FALSE(App.Step()); +} + +TEST(ModuleManagerTests, ApplicationStepSleepsOnlyForMinimizedWindowedMode) { + ModuleTestApplication WindowedApp(Axiom::RuntimeMode::LocalWindowedEditor); + WindowedApp.GetFakeWindow().Minimized = true; + + const auto WindowedStart = std::chrono::steady_clock::now(); + EXPECT_TRUE(WindowedApp.Step()); + const auto WindowedElapsed = + std::chrono::steady_clock::now() - WindowedStart; + EXPECT_GE(WindowedElapsed, std::chrono::milliseconds(10)); + + ModuleTestApplication HeadlessApp; + HeadlessApp.GetFakeWindow().Minimized = true; + + const auto HeadlessStart = std::chrono::steady_clock::now(); + EXPECT_TRUE(HeadlessApp.Step()); + const auto HeadlessElapsed = + std::chrono::steady_clock::now() - HeadlessStart; + EXPECT_LT(HeadlessElapsed, std::chrono::milliseconds(10)); +} +} // namespace diff --git a/Tests/RenderSubmissionTests.cpp b/Tests/RenderSubmissionTests.cpp new file mode 100644 index 00000000..7ab03587 --- /dev/null +++ b/Tests/RenderSubmissionTests.cpp @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include + +namespace { +class FakeMesh final : public Axiom::Mesh { +public: + explicit FakeMesh(Axiom::MeshHandle Handle) { AssignHandle(Handle); } +}; + +struct CountingMeshFactory { + Axiom::RenderMeshResource operator()(const Axiom::MeshData &) { + ++CreateCount; + Axiom::MeshHandle Handle{NextHandleValue++}; + return {.Handle = Handle, .Mesh = std::make_shared(Handle)}; + } + + int CreateCount{0}; + uint64_t NextHandleValue{1}; +}; + +template +concept HasLegacyTypedMeshMember = requires(T Submission) { + Submission.TypedMesh; +}; + +template +concept HasLegacyMeshRefMember = requires(T Submission) { + Submission.Mesh; +}; + +template +concept HasOpaqueMeshHandleMember = requires(T Submission) { + Submission.MeshHandle; +}; + +static_assert(!HasLegacyTypedMeshMember); +static_assert(!HasLegacyMeshRefMember); +static_assert(HasOpaqueMeshHandleMember); + +Axiom::EditorSceneMeshInstance MakeMeshInstance(std::string ObjectId, + std::string AssetRelativePath) { + return { + .ObjectId = std::move(ObjectId), + .Mesh = Axiom::MeshData{ + .Vertices = {{{0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f}, + {0.0f, 0.0f}}, + {{1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f}, + {1.0f, 0.0f}}, + {{0.0f, 1.0f, 0.0f}, + {0.0f, 0.0f, 1.0f}, + {0.0f, 1.0f}}}, + .Indices = {0, 1, 2}, + .BoundsMin = {0.0f, 0.0f, 0.0f}, + .BoundsMax = {1.0f, 1.0f, 0.0f}, + }, + .Material = std::make_shared(), + .RenderPath = Axiom::MeshRenderPath::Graphics, + .Transform = glm::mat4(1.0f), + .AssetRelativePath = std::move(AssetRelativePath), + }; +} + +Axiom::Camera MakeRenderCamera() { + static std::once_flag Flag; + std::call_once(Flag, []() { Axiom::Log::Init(); }); + Axiom::Camera Camera; + Camera.LookAt({0.0f, 0.0f, 6.0f}, {0.0f, 0.0f, 0.0f}); + Camera.SetPerspective(55.0f, 1280.0f / 720.0f, 0.1f, 100.0f); + return Camera; +} + +void EnsureLoggingInitialized() { + static std::once_flag Flag; + std::call_once(Flag, []() { Axiom::Log::Init(); }); +} + +Axiom::MeshData MakeTriangleMesh() { + return { + .Vertices = {{{-0.25f, -0.25f, 0.0f}, + {0.0f, 0.0f, 1.0f}, + {0.0f, 0.0f}}, + {{0.25f, -0.25f, 0.0f}, + {0.0f, 0.0f, 1.0f}, + {1.0f, 0.0f}}, + {{0.0f, 0.25f, 0.0f}, + {0.0f, 0.0f, 1.0f}, + {0.5f, 1.0f}}}, + .Indices = {0, 1, 2}, + .BoundsMin = {-0.25f, -0.25f, 0.0f}, + .BoundsMax = {0.25f, 0.25f, 0.0f}, + }; +} +} // namespace + +TEST(RenderSubmissionTests, EditorSceneRendererAdapterReusesCachedMeshUntilAssetChanges) { + auto Factory = std::make_shared(); + Axiom::EditorSceneRendererAdapter Adapter( + [Factory](const Axiom::MeshData &Mesh) { return (*Factory)(Mesh); }); + Axiom::EditorSession Session(Axiom::SessionId{1}); + + Session.SetSceneMeshInstances({MakeMeshInstance("crate", "Meshes/crate-a.glb")}); + const std::vector First = + Adapter.BuildRenderSubmissions(Session); + const std::vector Second = + Adapter.BuildRenderSubmissions(Session); + + ASSERT_EQ(First.size(), 1u); + ASSERT_EQ(Second.size(), 1u); + EXPECT_EQ(Factory->CreateCount, 1); + EXPECT_EQ(First[0].MeshHandle, Second[0].MeshHandle); + EXPECT_EQ(First[0].DebugDataId, Second[0].DebugDataId); + EXPECT_EQ(Axiom::GetRenderMeshSubmissionDebugName(First[0].DebugDataId), "crate"); + + Session.SetSceneMeshInstances({MakeMeshInstance("crate", "Meshes/crate-b.glb")}); + const std::vector Swapped = + Adapter.BuildRenderSubmissions(Session); + + ASSERT_EQ(Swapped.size(), 1u); + EXPECT_EQ(Factory->CreateCount, 2); + EXPECT_NE(Swapped[0].MeshHandle, First[0].MeshHandle); + EXPECT_EQ(Swapped[0].DebugDataId, First[0].DebugDataId); +} + +TEST(RenderSubmissionTests, EditorSceneRendererAdapterDropsDeletedObjectsFromCache) { + auto Factory = std::make_shared(); + Axiom::EditorSceneRendererAdapter Adapter( + [Factory](const Axiom::MeshData &Mesh) { return (*Factory)(Mesh); }); + Axiom::EditorSession Session(Axiom::SessionId{2}); + + Session.SetSceneMeshInstances({MakeMeshInstance("crate", "Meshes/crate.glb")}); + const std::vector First = + Adapter.BuildRenderSubmissions(Session); + ASSERT_EQ(First.size(), 1u); + EXPECT_EQ(Factory->CreateCount, 1); + + Session.SetSceneMeshInstances({}); + const std::vector Empty = + Adapter.BuildRenderSubmissions(Session); + EXPECT_TRUE(Empty.empty()); + EXPECT_EQ(Factory->CreateCount, 1); + + Session.SetSceneMeshInstances({MakeMeshInstance("crate", "Meshes/crate.glb")}); + const std::vector Recreated = + Adapter.BuildRenderSubmissions(Session); + + ASSERT_EQ(Recreated.size(), 1u); + EXPECT_EQ(Factory->CreateCount, 2); + EXPECT_NE(Recreated[0].MeshHandle, First[0].MeshHandle); +} + +TEST(RenderSubmissionTests, VulkanRendererRendersAllThousandSubmittedMeshesOffscreen) { + constexpr uint32_t Width = 1280; + constexpr uint32_t Height = 720; + constexpr size_t MeshCount = 1000; + + EnsureLoggingInitialized(); + if (!Axiom::CanInitializeHeadlessVulkan()) { + GTEST_SKIP() << "Headless Vulkan is unavailable on this host"; + } + + auto Surface = std::make_shared(Width, Height); + Axiom::Renderer Renderer; + Renderer.Init({ + .TargetSurface = Surface, + .Width = Width, + .Height = Height, + }); + Renderer.SetViewMode(Axiom::RendererViewMode::Wireframe); + + Axiom::RenderMeshResource MeshResource = + Renderer.CreateMeshResource(MakeTriangleMesh()); + ASSERT_TRUE(MeshResource.IsValid()); + const Axiom::MeshHandle MeshHandle = MeshResource.Handle; + + const Axiom::Camera Camera = MakeRenderCamera(); + const auto SubmitScene = [&]() { + Renderer.BeginFrame(); + Axiom::RenderCommand::SetCamera(Camera); + for (size_t Index = 0; Index < MeshCount; ++Index) { + const float X = static_cast(Index % 40u) * 0.12f - 2.34f; + const float Y = static_cast(Index / 40u) * 0.10f - 1.20f; + Axiom::RenderCommand::Submit({ + .MeshHandle = MeshHandle, + .DebugDataId = Axiom::RegisterRenderMeshSubmissionDebugData( + {.Name = "submission-" + std::to_string(Index)}), + .Transform = + glm::translate(glm::mat4(1.0f), glm::vec3(X, Y, 0.0f)), + }); + } + Renderer.Render(); + Renderer.EndFrame(); + }; + + SubmitScene(); + const Axiom::RendererFrameStats Stats = Renderer.GetFrameStats(); + EXPECT_EQ(Stats.SubmittedMeshCount, MeshCount); + EXPECT_EQ(Stats.MeshSubmissionCount, MeshCount); + EXPECT_EQ(Stats.FrustumCulledMeshCount, 0u); + EXPECT_EQ(Stats.OcclusionCulledMeshCount, 0u); + EXPECT_EQ(Stats.TriangleCount, MeshCount); + + SubmitScene(); + SubmitScene(); + std::optional Captured = Renderer.ConsumeCapturedFrame(); + ASSERT_TRUE(Captured.has_value()); + EXPECT_EQ(Captured->Width, Width); + EXPECT_EQ(Captured->Height, Height); + EXPECT_FALSE(Captured->Pixels.empty()); + + MeshResource.Mesh.reset(); + Renderer.Shutdown(); +} diff --git a/Tests/SceneLifecycleTests.cpp b/Tests/SceneLifecycleTests.cpp index 1eb3fb9d..94caab45 100644 --- a/Tests/SceneLifecycleTests.cpp +++ b/Tests/SceneLifecycleTests.cpp @@ -5,6 +5,9 @@ #include #include #include +#if AXIOM_WITH_PHYSICS +#include +#endif #include #include @@ -318,10 +321,11 @@ TEST(SceneLifecycleTests, AuthoringMutationsAreRejectedWhileSimulationIsActive) } TEST(SceneLifecycleTests, PhysicsStepsDynamicBodiesOnlyWhilePlaying) { -#if !AXIOM_ENABLE_PHYSICS +#if !AXIOM_WITH_PHYSICS GTEST_SKIP() << "Physics backend disabled for this build."; #else Axiom::EditorSession Session = MakeWorldSession(); + Axiom::AttachEditorPhysicsController(Session); Session.SetObjectDetails({ { .ObjectId = "world", @@ -404,10 +408,11 @@ TEST(SceneLifecycleTests, PhysicsStepsDynamicBodiesOnlyWhilePlaying) { } TEST(SceneLifecycleTests, PhysicsPauseFreezesDynamicBodies) { -#if !AXIOM_ENABLE_PHYSICS +#if !AXIOM_WITH_PHYSICS GTEST_SKIP() << "Physics backend disabled for this build."; #else Axiom::EditorSession Session = MakeWorldSession(); + Axiom::AttachEditorPhysicsController(Session); Session.SetObjectDetails({ { .ObjectId = "world", @@ -477,10 +482,11 @@ TEST(SceneLifecycleTests, PhysicsPauseFreezesDynamicBodies) { } TEST(SceneLifecycleTests, PhysicsStopRestoresPrePlayTransformState) { -#if !AXIOM_ENABLE_PHYSICS +#if !AXIOM_WITH_PHYSICS GTEST_SKIP() << "Physics backend disabled for this build."; #else Axiom::EditorSession Session = MakeWorldSession(); + Axiom::AttachEditorPhysicsController(Session); Session.SetObjectDetails({ { .ObjectId = "world", @@ -1037,6 +1043,42 @@ TEST(SceneLifecycleTests, DuplicateProducesCloneUnderSameParent) { EXPECT_EQ(World->Children.size(), 2u); } +TEST(SceneLifecycleTests, DuplicateAssignsDistinctStableHandles) { + Axiom::EditorSession Session = MakeWorldSession(); + RecordingSubscriber Subscriber; + Session.Subscribe(&Subscriber); + + Session.Submit( + MakeContext(1), + {.Payload = Axiom::CreateObjectCommand{.TemplateId = "Light"}}); + Session.Tick(); + + const auto *Created = FindEvent(Subscriber.Events); + ASSERT_NE(Created, nullptr); + const std::string OriginalId = Created->ObjectId; + const Axiom::SceneObjectHandle OriginalHandle = + Session.ResolveObjectHandle(OriginalId); + ASSERT_TRUE(static_cast(OriginalHandle)); + + Subscriber.Events.clear(); + Session.Submit(MakeContext(2), + {.Payload = Axiom::DuplicateObjectCommand{ + .ObjectId = OriginalId, + }}); + Session.Tick(); + + const auto *CloneCreated = + FindEvent(Subscriber.Events); + ASSERT_NE(CloneCreated, nullptr); + const Axiom::SceneObjectHandle CloneHandle = + Session.ResolveObjectHandle(CloneCreated->ObjectId); + ASSERT_TRUE(static_cast(CloneHandle)); + EXPECT_NE(CloneHandle, OriginalHandle); + EXPECT_EQ(Session.FindObjectDetails(OriginalHandle)->ObjectId, OriginalId); + EXPECT_EQ(Session.FindObjectDetails(CloneHandle)->ObjectId, + CloneCreated->ObjectId); +} + TEST(SceneLifecycleTests, DuplicateWithUnknownIdIsRejected) { Axiom::EditorSession Session = MakeWorldSession(); RecordingSubscriber Subscriber; @@ -1189,6 +1231,41 @@ TEST(SceneLifecycleTests, DeleteClearsSelectionForRemovedObject) { EXPECT_EQ(Session.FindSelectedObjectId(Axiom::SessionUserId{7}), nullptr); } +TEST(SceneLifecycleTests, DeleteClearsHandleBackedSelectionAndCollaborationState) { + Axiom::EditorSession Session = MakeWorldSession(); + + Session.Submit( + MakeContext(1), + {.Payload = Axiom::CreateObjectCommand{.TemplateId = "Mesh"}}); + Session.Tick(); + + std::string ObjectId; + for (const auto &[Id, Details] : Session.GetState().Scene.ObjectDetailsById) { + if (Id != "world") { + ObjectId = Id; + break; + } + } + ASSERT_FALSE(ObjectId.empty()); + + const Axiom::SceneObjectHandle Handle = Session.ResolveObjectHandle(ObjectId); + ASSERT_TRUE(static_cast(Handle)); + + Session.Submit(MakeContext(2), + {.Payload = Axiom::SelectObjectCommand{.ObjectId = ObjectId}}); + Session.Tick(); + Session.AcquireLock(ObjectId, Axiom::SessionUserId{7}); + ASSERT_NE(Session.FindSelectedObjectHandle(Axiom::SessionUserId{7}), nullptr); + ASSERT_NE(Session.FindCollaborationState(Handle), nullptr); + + Session.Submit(MakeContext(3), + {.Payload = Axiom::DeleteObjectCommand{.ObjectId = ObjectId}}); + Session.Tick(); + + EXPECT_EQ(Session.FindSelectedObjectHandle(Axiom::SessionUserId{7}), nullptr); + EXPECT_EQ(Session.FindCollaborationState(Handle), nullptr); +} + TEST(SceneLifecycleTests, DeleteWithUnknownIdIsRejected) { Axiom::EditorSession Session = MakeWorldSession(); RecordingSubscriber Subscriber; @@ -1314,16 +1391,18 @@ TEST(SceneLifecycleTests, SetSceneItemsRebuildsInstanceTree) { }}, }}); - const Axiom::DataModel *Root = Session.GetSceneRoot(); + const Axiom::InstancePool &Pool = Session.GetInstancePool(); + const Axiom::InstanceHandle RootHandle = Session.GetSceneRoot(); + const Axiom::DataModel *Root = Pool.ResolveAs(RootHandle); ASSERT_NE(Root, nullptr); ASSERT_EQ(Root->GetChildren().size(), 1u); - EXPECT_EQ(Root->GetChildren().front()->GetName(), "world"); + EXPECT_EQ(Pool.Resolve(Root->GetChildren().front())->GetName(), "world"); - const Axiom::Instance *World = - Root->FindFirstChild("world"); + const Axiom::InstanceHandle WorldHandle = Root->FindFirstChild("world"); + const Axiom::Instance *World = Pool.Resolve(WorldHandle); ASSERT_NE(World, nullptr); ASSERT_EQ(World->GetChildren().size(), 1u); - EXPECT_EQ(World->GetChildren().front()->GetName(), "light-1"); + EXPECT_EQ(Pool.Resolve(World->GetChildren().front())->GetName(), "light-1"); } TEST(SceneLifecycleTests, SnapshotRehydrationRestoresTreeAndAllowsCreate) { @@ -1350,9 +1429,11 @@ TEST(SceneLifecycleTests, SnapshotRehydrationRestoresTreeAndAllowsCreate) { Session.SetSceneState(std::move(State)); // Tree reflects snapshot - const Axiom::DataModel *Root = Session.GetSceneRoot(); + const Axiom::InstancePool &Pool = Session.GetInstancePool(); + const Axiom::DataModel *Root = + Pool.ResolveAs(Session.GetSceneRoot()); ASSERT_NE(Root, nullptr); - ASSERT_NE(Root->FindFirstChild("world"), nullptr); + ASSERT_TRUE(static_cast(Root->FindFirstChild("world"))); // Create works after rehydration RecordingSubscriber Subscriber; @@ -1366,9 +1447,10 @@ TEST(SceneLifecycleTests, SnapshotRehydrationRestoresTreeAndAllowsCreate) { ASSERT_NE(Created, nullptr); // New object is a child of "world" in the tree - const Axiom::Instance *World = Root->FindFirstChild("world"); + const Axiom::InstanceHandle WorldHandle = Root->FindFirstChild("world"); + const Axiom::Instance *World = Pool.Resolve(WorldHandle); ASSERT_NE(World, nullptr); - EXPECT_NE(World->FindFirstChild(Created->ObjectId), nullptr); + EXPECT_TRUE(static_cast(World->FindFirstChild(Created->ObjectId))); // Reload snapshot — new object should be gone Axiom::EditorSceneState State2{}; @@ -1391,7 +1473,13 @@ TEST(SceneLifecycleTests, SnapshotRehydrationRestoresTreeAndAllowsCreate) { Session.SetSceneState(std::move(State2)); EXPECT_EQ(Session.FindObjectDetails(Created->ObjectId), nullptr); - EXPECT_EQ(World->GetChildren().size(), 0u); + const Axiom::DataModel *ReloadedRoot = + Pool.ResolveAs(Session.GetSceneRoot()); + ASSERT_NE(ReloadedRoot, nullptr); + const Axiom::Instance *ReloadedWorld = + Pool.Resolve(ReloadedRoot->FindFirstChild("world")); + ASSERT_NE(ReloadedWorld, nullptr); + EXPECT_EQ(ReloadedWorld->GetChildren().size(), 0u); } // --------------------------------------------------------------------------- @@ -1474,6 +1562,71 @@ TEST(SceneLifecycleTests, ReparentMovesObjectToNewParent) { } } +TEST(SceneLifecycleTests, ReparentPreservesHandleAndMeshInstanceOwnership) { + Axiom::EditorSession Session = MakeSessionWithFolder(); + Session.SetSceneItems({{ + .Id = "world", + .DisplayName = "World", + .Kind = Axiom::EditorSceneItemKind::Folder, + .Visible = true, + .Children = {{ + .Id = "group", + .DisplayName = "Group", + .Kind = Axiom::EditorSceneItemKind::Folder, + .Visible = true, + }, { + .Id = "crate-1", + .DisplayName = "Crate", + .Kind = Axiom::EditorSceneItemKind::Mesh, + .Visible = true, + }}, + }}); + Session.SetObjectDetails({ + {.ObjectId = "world", + .DisplayName = "World", + .Kind = Axiom::EditorSceneItemKind::Folder, + .Visible = true, + .SupportsTransform = false, + .TransformReadOnly = true}, + {.ObjectId = "group", + .DisplayName = "Group", + .Kind = Axiom::EditorSceneItemKind::Folder, + .Visible = true, + .SupportsTransform = false, + .TransformReadOnly = true}, + {.ObjectId = "crate-1", + .DisplayName = "Crate", + .Kind = Axiom::EditorSceneItemKind::Mesh, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = Axiom::EditorTransformDetails{}}, + }); + Session.SetSceneMeshInstances({{ + .ObjectId = "crate-1", + .Mesh = {}, + .Material = nullptr, + .RenderPath = Axiom::MeshRenderPath::Graphics, + .Transform = glm::mat4(1.0f), + }}); + + const Axiom::SceneObjectHandle Handle = Session.ResolveObjectHandle("crate-1"); + ASSERT_TRUE(static_cast(Handle)); + ASSERT_EQ(Session.GetState().Scene.MeshInstances.size(), 1u); + EXPECT_EQ(Session.GetState().Scene.MeshInstances.front().ObjectHandle, Handle); + + Session.Submit(MakeContext(1), + {.Payload = Axiom::ReparentObjectCommand{ + .ObjectId = "crate-1", + .NewParentId = "group", + }}); + Session.Tick(); + + EXPECT_EQ(Session.ResolveObjectHandle("crate-1"), Handle); + ASSERT_EQ(Session.GetState().Scene.MeshInstances.size(), 1u); + EXPECT_EQ(Session.GetState().Scene.MeshInstances.front().ObjectHandle, Handle); +} + TEST(SceneLifecycleTests, ReparentToDescendantIsRejected) { Axiom::EditorSession Session = MakeSessionWithFolder(); RecordingSubscriber Subscriber; @@ -2587,6 +2740,58 @@ TEST(SceneLifecycleTests, SceneFile_SaveLoadRoundTripsPhysicsState) { EXPECT_FLOAT_EQ(DetailsIt->second.Physics->Restitution, 0.25f); } +TEST(SceneLifecycleTests, SceneFile_SaveLoadRoundTripsStableHandlesForPhysicsObjects) { + EnsureLogInitialized(); + + const auto TempRoot = + std::filesystem::temp_directory_path() / "wraithengine-handle-scene-test"; + std::error_code RemoveError; + std::filesystem::remove_all(TempRoot, RemoveError); + std::filesystem::create_directories(TempRoot / "Content"); + + Axiom::EditorSceneState Scene; + Scene.Items = {{ + .Handle = Axiom::SceneObjectHandle{41}, + .Id = "ball", + .DisplayName = "Ball", + .Kind = Axiom::EditorSceneItemKind::Actor, + .Visible = true, + .Children = {}, + }}; + Scene.ObjectDetailsById["ball"] = Axiom::EditorObjectDetails{ + .Handle = Axiom::SceneObjectHandle{41}, + .ObjectId = "ball", + .DisplayName = "Ball", + .Kind = Axiom::EditorSceneItemKind::Actor, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = Axiom::EditorTransformDetails{}, + .Physics = Axiom::EditorPhysicsProperties{ + .BodyType = Axiom::EditorPhysicsBodyType::Dynamic, + .ColliderType = Axiom::EditorPhysicsColliderType::Sphere, + }, + }; + + const auto ScenePath = TempRoot / "Content" / "scene.json"; + ASSERT_TRUE(Axiom::Assets::SaveSceneToFile(ScenePath, Scene)); + + const auto Loaded = Axiom::Assets::LoadSceneFromFile(ScenePath); + ASSERT_TRUE(Loaded.has_value()); + const auto DetailsIt = Loaded->ObjectDetailsById.find("ball"); + ASSERT_NE(DetailsIt, Loaded->ObjectDetailsById.end()); + EXPECT_EQ(DetailsIt->second.Handle, Axiom::SceneObjectHandle{41}); + ASSERT_EQ(Loaded->Items.size(), 1u); + EXPECT_EQ(Loaded->Items.front().Handle, Axiom::SceneObjectHandle{41}); + + Axiom::EditorSession Session(Axiom::SessionId{1}); + Session.SetSceneState(*Loaded); + EXPECT_EQ(Session.ResolveObjectHandle("ball"), Axiom::SceneObjectHandle{41}); + ASSERT_NE(Session.FindObjectDetails(Axiom::SceneObjectHandle{41}), nullptr); + EXPECT_EQ(Session.FindObjectDetails(Axiom::SceneObjectHandle{41})->ObjectId, + "ball"); +} + TEST(SceneLifecycleTests, SceneFile_LoadMigratesMissingMeshPhysicsToStaticBox) { EnsureLogInitialized(); diff --git a/Tests/ScriptingTests.cpp b/Tests/ScriptingTests.cpp index c77a5bfc..3643ce0e 100644 --- a/Tests/ScriptingTests.cpp +++ b/Tests/ScriptingTests.cpp @@ -8,7 +8,7 @@ #include -#if AXIOM_SCRIPTING_ENABLED +#if AXIOM_WITH_SCRIPTING #include #include @@ -321,4 +321,4 @@ TEST_F(ScriptingTest, PauseFreezesScriptTicks) { s_Session->Tick(); } -#endif // AXIOM_SCRIPTING_ENABLED +#endif // AXIOM_WITH_SCRIPTING diff --git a/Tests/ThreadingTests.cpp b/Tests/ThreadingTests.cpp new file mode 100644 index 00000000..793f2ce7 --- /dev/null +++ b/Tests/ThreadingTests.cpp @@ -0,0 +1,389 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#if AXIOM_WITH_PHYSICS +#include +#endif +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { +void EnsureLoggingInitialized() { + static bool Initialized = false; + if (!Initialized) { + Axiom::Log::Init(); + Initialized = true; + } +} + +Axiom::MeshData MakeTriangleMesh() { + Axiom::MeshData Mesh; + Mesh.Vertices = { + {{-0.5f, -0.5f, 0.0f}}, + {{0.5f, -0.5f, 0.0f}}, + {{0.0f, 0.5f, 0.0f}}, + }; + Mesh.Indices = {0, 1, 2}; + Mesh.BoundsMin = {-0.5f, -0.5f, 0.0f}; + Mesh.BoundsMax = {0.5f, 0.5f, 0.0f}; + return Mesh; +} + +Axiom::Camera MakeRenderCamera() { + Axiom::Camera Camera; + Camera.LookAt({0.0f, 0.0f, 6.0f}, {0.0f, 0.0f, 0.0f}); + Camera.SetPerspective(55.0f, 1280.0f / 720.0f, 0.1f, 100.0f); + return Camera; +} + +Axiom::CommandContext MakeContext(uint64_t FrameIndex = 1, + uint64_t UserId = 1) { + return { + .Session = Axiom::SessionId{1}, + .User = Axiom::SessionUserId{UserId}, + .FrameIndex = FrameIndex, + .DeltaTimeSeconds = 1.0f / 60.0f, + }; +} +} // namespace + +TEST(ThreadingTests, JobsRunWithDependenciesAndParallelFor) { + Axiom::Jobs::Startup(); + + std::atomic Value{0}; + Axiom::Jobs::JobHandle First = Axiom::Jobs::ScheduleJob([&Value]() { + Value.fetch_add(1, std::memory_order_relaxed); + }); + + std::array Dependencies = {First}; + Axiom::Jobs::JobHandle Second = Axiom::Jobs::ScheduleJobAfter( + [&Value]() { Value.fetch_add(2, std::memory_order_relaxed); }, + std::span(Dependencies)); + Axiom::Jobs::Wait(Second); + + std::atomic ParallelSum{0}; + Axiom::Jobs::ParallelFor(256, [&ParallelSum](size_t Index) { + ParallelSum.fetch_add(Index + 1, std::memory_order_relaxed); + }); + + EXPECT_EQ(Value.load(std::memory_order_relaxed), 3); + EXPECT_EQ(ParallelSum.load(std::memory_order_relaxed), (256u * 257u) / 2u); + + Axiom::Jobs::Shutdown(); +} + +TEST(ThreadingTests, ThreadedRendererRunsHeadlessForThousandFramesWithoutDeadlock) { +#if AXIOM_THREADED_RENDER == 0 + GTEST_SKIP() << "Threaded renderer is disabled in this build"; +#else + constexpr uint32_t Width = 64; + constexpr uint32_t Height = 64; +#if AXIOM_THREAD_SANITIZER + constexpr size_t FrameCount = 1000; +#else + constexpr size_t FrameCount = 128; +#endif + + EnsureLoggingInitialized(); + if (!Axiom::CanInitializeHeadlessVulkan()) { + GTEST_SKIP() << "Headless Vulkan is unavailable on this host"; + } + + auto Surface = std::make_shared(Width, Height); + Axiom::Renderer Renderer; + Renderer.Init({ + .TargetSurface = Surface, + .Width = Width, + .Height = Height, + .EnableThreadedRendering = true, + }); + + Axiom::RenderMeshResource MeshResource = + Renderer.CreateMeshResource(MakeTriangleMesh()); + ASSERT_TRUE(MeshResource.IsValid()); + + const Axiom::Camera Camera = MakeRenderCamera(); + for (size_t FrameIndex = 0; FrameIndex < FrameCount; ++FrameIndex) { + Renderer.SetCpuFrameTime(16.0f); + Renderer.BeginFrame(); + Axiom::RenderCommand::SetCamera(Camera); + Axiom::RenderCommand::Submit({ + .MeshHandle = MeshResource.Handle, + .DebugDataId = Axiom::RegisterRenderMeshSubmissionDebugData( + {.Name = "threaded-frame-" + std::to_string(FrameIndex)}), + }); + Renderer.Render(); + Renderer.EndFrame(); + } + + Renderer.WaitForIdle(); + MeshResource.Mesh.reset(); + Renderer.Shutdown(); +#endif +} + +TEST(ThreadingTests, ThreadedRendererOverlapsGameThreadRecordingWithRenderThreadWork) { +#if AXIOM_THREADED_RENDER == 0 + GTEST_SKIP() << "Threaded renderer is disabled in this build"; +#else + constexpr uint32_t Width = 64; + constexpr uint32_t Height = 64; + + EnsureLoggingInitialized(); + if (!Axiom::CanInitializeHeadlessVulkan()) { + GTEST_SKIP() << "Headless Vulkan is unavailable on this host"; + } + + std::mutex Mutex; + std::condition_variable RenderStartedCv; + bool FirstRenderStarted = false; + std::thread::id RenderThreadId; + const std::thread::id GameThreadId = std::this_thread::get_id(); + + auto Surface = std::make_shared(Width, Height); + Axiom::Renderer Renderer; + Renderer.Init({ + .TargetSurface = Surface, + .Width = Width, + .Height = Height, + .EnableThreadedRendering = true, + .ThreadedRenderSceneStartCallback = + [&](uint64_t FrameNumber) { + { + std::scoped_lock Lock(Mutex); + RenderThreadId = std::this_thread::get_id(); + } + if (FrameNumber == 1) { + { + std::scoped_lock Lock(Mutex); + FirstRenderStarted = true; + } + RenderStartedCv.notify_all(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + }, + }); + + Axiom::RenderMeshResource MeshResource = + Renderer.CreateMeshResource(MakeTriangleMesh()); + ASSERT_TRUE(MeshResource.IsValid()); + const Axiom::Camera Camera = MakeRenderCamera(); + + const auto SubmitFrame = [&](size_t FrameIndex) { + Renderer.SetCpuFrameTime(16.0f); + Renderer.BeginFrame(); + Axiom::RenderCommand::SetCamera(Camera); + Axiom::RenderCommand::Submit({ + .MeshHandle = MeshResource.Handle, + .DebugDataId = Axiom::RegisterRenderMeshSubmissionDebugData( + {.Name = "overlap-frame-" + std::to_string(FrameIndex)}), + }); + Renderer.Render(); + Renderer.EndFrame(); + }; + + SubmitFrame(1); + { + std::unique_lock Lock(Mutex); + RenderStartedCv.wait(Lock, [&]() { return FirstRenderStarted; }); + } + + const auto OverlapWindowStart = std::chrono::steady_clock::now(); + SubmitFrame(2); + SubmitFrame(3); + const auto OverlapWindowMs = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - OverlapWindowStart); + + EXPECT_LT(OverlapWindowMs.count(), 40); + EXPECT_NE(RenderThreadId, std::thread::id{}); + EXPECT_NE(RenderThreadId, GameThreadId); + + Renderer.WaitForIdle(); + MeshResource.Mesh.reset(); + Renderer.Shutdown(); +#endif +} + +TEST(ThreadingTests, + ThreadedRendererAllowsPhysicsSimulationOverlapWithRenderThreadWork) { +#if AXIOM_THREADED_RENDER == 0 + GTEST_SKIP() << "Threaded renderer is disabled in this build"; +#elif !AXIOM_WITH_PHYSICS + GTEST_SKIP() << "Physics backend disabled for this build."; +#else + constexpr uint32_t Width = 64; + constexpr uint32_t Height = 64; + + EnsureLoggingInitialized(); + if (!Axiom::CanInitializeHeadlessVulkan()) { + GTEST_SKIP() << "Headless Vulkan is unavailable on this host"; + } + + std::mutex Mutex; + std::condition_variable RenderStartedCv; + bool FirstRenderStarted = false; + bool FirstRenderCompleted = false; + + auto Surface = std::make_shared(Width, Height); + Axiom::Renderer Renderer; + Renderer.Init({ + .TargetSurface = Surface, + .Width = Width, + .Height = Height, + .EnableThreadedRendering = true, + .ThreadedRenderSceneStartCallback = + [&](uint64_t FrameNumber) { + if (FrameNumber != 1) { + return; + } + { + std::scoped_lock Lock(Mutex); + FirstRenderStarted = true; + } + RenderStartedCv.notify_all(); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + }, + .ThreadedRenderSceneCompleteCallback = + [&](uint64_t FrameNumber) { + if (FrameNumber != 1) { + return; + } + std::scoped_lock Lock(Mutex); + FirstRenderCompleted = true; + }, + }); + + Axiom::RenderMeshResource MeshResource = + Renderer.CreateMeshResource(MakeTriangleMesh()); + ASSERT_TRUE(MeshResource.IsValid()); + const Axiom::Camera Camera = MakeRenderCamera(); + + Axiom::EditorSession Session(Axiom::SessionId{1}); + Axiom::AttachEditorPhysicsController(Session); + Session.SetSceneItems({{ + .Id = "world", + .DisplayName = "World", + .Kind = Axiom::EditorSceneItemKind::Folder, + .Visible = true, + .Children = {{ + .Id = "floor", + .DisplayName = "Floor", + .Kind = Axiom::EditorSceneItemKind::Mesh, + .Visible = true, + }, + { + .Id = "ball", + .DisplayName = "Ball", + .Kind = Axiom::EditorSceneItemKind::Actor, + .Visible = true, + }}, + }}); + Session.SetObjectDetails({ + { + .ObjectId = "world", + .DisplayName = "World", + .Kind = Axiom::EditorSceneItemKind::Folder, + .Visible = true, + .SupportsTransform = false, + .TransformReadOnly = true, + }, + { + .ObjectId = "floor", + .DisplayName = "Floor", + .Kind = Axiom::EditorSceneItemKind::Mesh, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = Axiom::EditorTransformDetails{ + .Location = glm::vec3(0.0f, -1.0f, 0.0f), + .RotationDegrees = glm::vec3(0.0f), + .Scale = glm::vec3(1.0f), + }, + .Physics = Axiom::EditorPhysicsProperties{ + .BodyType = Axiom::EditorPhysicsBodyType::Static, + .ColliderType = Axiom::EditorPhysicsColliderType::Box, + .BoxHalfExtents = glm::vec3(8.0f, 0.5f, 8.0f), + }, + }, + { + .ObjectId = "ball", + .DisplayName = "Ball", + .Kind = Axiom::EditorSceneItemKind::Actor, + .Visible = true, + .SupportsTransform = true, + .TransformReadOnly = false, + .Transform = Axiom::EditorTransformDetails{ + .Location = glm::vec3(0.0f, 5.0f, 0.0f), + .RotationDegrees = glm::vec3(0.0f), + .Scale = glm::vec3(1.0f), + }, + .Physics = Axiom::EditorPhysicsProperties{ + .BodyType = Axiom::EditorPhysicsBodyType::Dynamic, + .ColliderType = Axiom::EditorPhysicsColliderType::Sphere, + .SphereRadius = 0.5f, + .Mass = 1.0f, + }, + }, + }); + Session.Submit(MakeContext(1, 1), {.Payload = Axiom::PlaySessionCommand{}}); + Session.Tick(1.0f / 60.0f); + + Renderer.SetCpuFrameTime(16.0f); + Renderer.BeginFrame(); + Axiom::RenderCommand::SetCamera(Camera); + Axiom::RenderCommand::Submit({ + .MeshHandle = MeshResource.Handle, + .DebugDataId = Axiom::RegisterRenderMeshSubmissionDebugData( + {.Name = "physics-overlap-frame"}), + }); + Renderer.Render(); + Renderer.EndFrame(); + + { + std::unique_lock Lock(Mutex); + RenderStartedCv.wait(Lock, [&]() { return FirstRenderStarted; }); + } + + const auto PhysicsStart = std::chrono::steady_clock::now(); + for (int Step = 0; Step < 30; ++Step) { + Session.Tick(1.0f / 60.0f); + } + const auto PhysicsElapsed = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - PhysicsStart); + + const Axiom::EditorObjectDetails *Ball = Session.FindObjectDetails("ball"); + ASSERT_NE(Ball, nullptr); + ASSERT_TRUE(Ball->WorldTransform.has_value() || Ball->Transform.has_value()); + const Axiom::EditorTransformDetails &Transform = + Ball->WorldTransform.has_value() ? *Ball->WorldTransform : *Ball->Transform; + + bool RenderCompletedBeforePhysicsFinished = false; + { + std::scoped_lock Lock(Mutex); + RenderCompletedBeforePhysicsFinished = FirstRenderCompleted; + } + + EXPECT_LT(PhysicsElapsed.count(), 150); + EXPECT_FALSE(RenderCompletedBeforePhysicsFinished); + EXPECT_LT(Transform.Location.y, 5.0f); + + Renderer.WaitForIdle(); + MeshResource.Mesh.reset(); + Renderer.Shutdown(); +#endif +} diff --git a/Tests/UWebSocketsVendorTests.cpp b/Tests/UWebSocketsVendorTests.cpp new file mode 100644 index 00000000..f92e960d --- /dev/null +++ b/Tests/UWebSocketsVendorTests.cpp @@ -0,0 +1,12 @@ +#include + +#include + +TEST(UWebSocketsVendorTests, VendoredHeadersAndRuntimeConstruct) { + uWS::App App = uWS::App(); + App.get("/health", [](auto *Response, auto *Request) { + (void)Request; + Response->end("ok"); + }); + SUCCEED(); +} diff --git a/Tests/WraithNetworkingModuleTests.cpp b/Tests/WraithNetworkingModuleTests.cpp new file mode 100644 index 00000000..a29e1b72 --- /dev/null +++ b/Tests/WraithNetworkingModuleTests.cpp @@ -0,0 +1,195 @@ +#include + +#include +#include +#include + +#include +#include + +#include +#include + +namespace { +class FakeWindow final : public Axiom::Window { +public: + FakeWindow() : Window("WraithNetworking Test Window", 320, 200) {} + + void PollEvents() override {} + bool IsKeyPressed(int Key) const override { + (void)Key; + return false; + } + bool IsMouseButtonPressed(int Button) const override { + (void)Button; + return false; + } + void GetCursorPosition(double &X, double &Y) const override { + X = 0.0; + Y = 0.0; + } + void SetCursorMode(Axiom::CursorMode Mode) override { Cursor = Mode; } + [[nodiscard]] Axiom::CursorMode GetCursorMode() const override { + return Cursor; + } + [[nodiscard]] bool ShouldClose() const override { return Closed; } + [[nodiscard]] bool IsMinimized() const override { return false; } + void RequestClose() override { Closed = true; } + [[nodiscard]] void *GetNativeHandle() const override { return nullptr; } + [[nodiscard]] bool + SupportsPresentationBackend(Axiom::PresentationBackendType Backend) const + override { + (void)Backend; + return false; + } + Axiom::PresentationSurfaceResult + CreatePresentationSurface(Axiom::PresentationBackendType Backend, + void *Instance, void *Surface) const override { + (void)Backend; + (void)Instance; + (void)Surface; + return Axiom::PresentationSurfaceResult::InitializationFailed; + } + +private: + bool Closed{false}; + Axiom::CursorMode Cursor{Axiom::CursorMode::Normal}; +}; + +class ModuleTestApplication final : public Axiom::Application { +public: + ModuleTestApplication() + : Application( + {.Title = "WraithNetworking Test App", + .Width = 320, + .Height = 200, + .Mode = Axiom::RuntimeMode::HeadlessEditorSession}, + {.Arguments = nullptr, .ArgumentCount = 0}, + {.Window = std::make_unique(), + .RenderSurface = + std::make_shared(320, 200), + .Renderer = nullptr, + .InitializeRenderer = false, + .RegisterDefaultModules = false}) {} +}; + +class FakeRemoteViewportServer final : public Axiom::IRemoteViewportServer { +public: + bool Start(std::string &Error) override { + ++StartCalls; + Error.clear(); + if (!StartResult) { + Error = FailureReason; + return false; + } + Started = true; + return true; + } + + void Stop() override { + ++StopCalls; + Started = false; + } + + [[nodiscard]] bool ShouldStop() const override { return StopRequested; } + [[nodiscard]] uint16_t GetPort() const override { return Metrics.ListenPort; } + [[nodiscard]] Axiom::RemoteViewportServerMetrics GetMetrics() const override { + return Metrics; + } + + bool StartResult{true}; + bool Started{false}; + bool StopRequested{false}; + std::string FailureReason{"start failed"}; + Axiom::RemoteViewportServerMetrics Metrics{ + .TransportConnected = true, + .ListenPort = 8080, + .ActiveWebSocketClients = 2, + .ActiveRemoteClients = 1, + .ActiveWebRtcSessions = 1, + .TotalHttpRequests = 4, + .TotalWebSocketMessages = 6, + }; + size_t StartCalls{0}; + size_t StopCalls{0}; +}; + +TEST(WraithNetworkingModuleTests, + RegistersThroughModuleManagerAndExposesMetrics) { + ModuleTestApplication App; + + FakeRemoteViewportServer *ServerPtr = nullptr; + auto Module = std::make_unique( + [&ServerPtr]() -> std::unique_ptr { + auto Server = std::make_unique(); + ServerPtr = Server.get(); + return Server; + }); + Axiom::WraithNetworkingModule *ModulePtr = Module.get(); + + ASSERT_TRUE(App.GetModuleManager().RegisterModule(std::move(Module))); + ASSERT_NE(ServerPtr, nullptr); + ASSERT_NE(ModulePtr, nullptr); + EXPECT_EQ(ServerPtr->StartCalls, 1u); + EXPECT_TRUE(ModulePtr->IsInitialized()); + + const auto State = ModulePtr->GetStateSnapshot(); + EXPECT_EQ(State.InitializationState, + Axiom::WraithNetworkingInitializationState::Initialized); + EXPECT_TRUE(State.Metrics.TransportConnected); + EXPECT_EQ(State.Metrics.ListenPort, 8080); + EXPECT_EQ(State.Metrics.ActiveWebSocketClients, 2u); + EXPECT_EQ(State.Metrics.ActiveRemoteClients, 1u); + EXPECT_EQ(State.Metrics.ActiveWebRtcSessions, 1u); + EXPECT_EQ(State.Metrics.TotalHttpRequests, 4u); + EXPECT_EQ(State.Metrics.TotalWebSocketMessages, 6u); + + ServerPtr->StopRequested = true; + EXPECT_TRUE(ModulePtr->ShouldStop()); +} + +TEST(WraithNetworkingModuleTests, SurfacesInitializationFailures) { + ModuleTestApplication App; + + auto Module = std::make_unique( + []() -> std::unique_ptr { + auto Server = std::make_unique(); + Server->StartResult = false; + Server->FailureReason = "simulated bind failure"; + return Server; + }); + Axiom::WraithNetworkingModule *ModulePtr = Module.get(); + + ASSERT_TRUE(ModulePtr != nullptr); + EXPECT_TRUE(App.GetModuleManager().RegisterModule(std::move(Module))); + + const auto State = ModulePtr->GetStateSnapshot(); + EXPECT_EQ(State.InitializationState, + Axiom::WraithNetworkingInitializationState::Failed); + EXPECT_EQ(State.LastError, "simulated bind failure"); +} + +TEST(WraithNetworkingModuleTests, DisabledModuleStaysShutdownWithoutStartingServer) { + ModuleTestApplication App; + + bool FactoryCalled = false; + auto Module = std::make_unique( + [&FactoryCalled]() -> std::unique_ptr { + FactoryCalled = true; + return std::make_unique(); + }, + false); + Axiom::WraithNetworkingModule *ModulePtr = Module.get(); + + ASSERT_TRUE(App.GetModuleManager().RegisterModule(std::move(Module))); + ASSERT_NE(ModulePtr, nullptr); + EXPECT_FALSE(FactoryCalled); + + const auto State = ModulePtr->GetStateSnapshot(); + EXPECT_FALSE(State.Enabled); + EXPECT_EQ(State.InitializationState, + Axiom::WraithNetworkingInitializationState::Shutdown); + EXPECT_EQ(State.Metrics.ActiveRemoteClients, 0u); + EXPECT_EQ(State.Metrics.TotalWebSocketMessages, 0u); +} +} // namespace diff --git a/ThirdParty/uWebSockets b/ThirdParty/uWebSockets new file mode 160000 index 00000000..34809c2e --- /dev/null +++ b/ThirdParty/uWebSockets @@ -0,0 +1 @@ +Subproject commit 34809c2eb8210f15369b251c4405eb2f494a334e diff --git a/Tools/generate_descriptor_bind_scene.py b/Tools/generate_descriptor_bind_scene.py new file mode 100644 index 00000000..cf9095f4 --- /dev/null +++ b/Tools/generate_descriptor_bind_scene.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 + +import json +import pathlib +import struct +import sys + + +def write_shared_material_asset(content_root: pathlib.Path) -> None: + positions = [ + (-0.5, -0.5, 0.5), + (0.5, -0.5, 0.5), + (0.5, 0.5, 0.5), + (-0.5, 0.5, 0.5), + (-0.5, -0.5, -0.5), + (-0.5, 0.5, -0.5), + (0.5, 0.5, -0.5), + (0.5, -0.5, -0.5), + (-0.5, 0.5, -0.5), + (-0.5, 0.5, 0.5), + (0.5, 0.5, 0.5), + (0.5, 0.5, -0.5), + (-0.5, -0.5, -0.5), + (0.5, -0.5, -0.5), + (0.5, -0.5, 0.5), + (-0.5, -0.5, 0.5), + (0.5, -0.5, -0.5), + (0.5, 0.5, -0.5), + (0.5, 0.5, 0.5), + (0.5, -0.5, 0.5), + (-0.5, -0.5, -0.5), + (-0.5, -0.5, 0.5), + (-0.5, 0.5, 0.5), + (-0.5, 0.5, -0.5), + ] + normals = [ + (0.0, 0.0, 1.0), + (0.0, 0.0, 1.0), + (0.0, 0.0, 1.0), + (0.0, 0.0, 1.0), + (0.0, 0.0, -1.0), + (0.0, 0.0, -1.0), + (0.0, 0.0, -1.0), + (0.0, 0.0, -1.0), + (0.0, 1.0, 0.0), + (0.0, 1.0, 0.0), + (0.0, 1.0, 0.0), + (0.0, 1.0, 0.0), + (0.0, -1.0, 0.0), + (0.0, -1.0, 0.0), + (0.0, -1.0, 0.0), + (0.0, -1.0, 0.0), + (1.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (-1.0, 0.0, 0.0), + (-1.0, 0.0, 0.0), + (-1.0, 0.0, 0.0), + (-1.0, 0.0, 0.0), + ] + uvs = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] * 6 + indices = [ + 0, 1, 2, 0, 2, 3, + 4, 5, 6, 4, 6, 7, + 8, 9, 10, 8, 10, 11, + 12, 13, 14, 12, 14, 15, + 16, 17, 18, 16, 18, 19, + 20, 21, 22, 20, 22, 23, + ] + + buffer = bytearray() + + def append_floats(values): + for value in values: + buffer.extend(struct.pack(" None: + scene = { + "version": 1, + "meshAsset": "", + "nodes": [ + { + "id": "world", + "parentId": None, + "displayName": "World", + "kind": "Folder", + "visible": True, + }, + { + "id": "lighting", + "parentId": "world", + "displayName": "Lighting", + "kind": "Folder", + "visible": True, + }, + { + "id": "directional-light", + "parentId": "lighting", + "displayName": "DirectionalLight", + "kind": "Light", + "visible": True, + }, + { + "id": "GridAsset", + "parentId": "world", + "displayName": "GridAsset", + "kind": "Mesh", + "visible": True, + }, + ], + "objects": [ + { + "id": "world", + "displayName": "World", + "kind": "Folder", + "visible": True, + "isGeneratedAssetChild": False, + "supportsTransform": False, + "transformReadOnly": True, + }, + { + "id": "lighting", + "displayName": "Lighting", + "kind": "Folder", + "visible": True, + "isGeneratedAssetChild": False, + "supportsTransform": False, + "transformReadOnly": True, + }, + { + "id": "directional-light", + "displayName": "DirectionalLight", + "kind": "Light", + "visible": True, + "isGeneratedAssetChild": False, + "supportsTransform": True, + "transformReadOnly": False, + "location": [0.70909, 25.0, -8.0], + "rotationDegrees": [-45.0, 30.0, 0.0], + "scale": [1.0, 1.0, 1.0], + "lightColor": [1.0, 0.98, 0.92], + "lightIntensity": 4.0, + "lightDirection": [0.35, 0.7, 0.2], + }, + { + "id": "GridAsset", + "displayName": "GridAsset", + "kind": "Mesh", + "visible": True, + "isGeneratedAssetChild": False, + "supportsTransform": True, + "transformReadOnly": False, + "location": [-9.0, 0.0, -22.0], + "rotationDegrees": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0], + "assetRelativePath": "shared_materials_cube.gltf", + }, + ], + "meshNameToObjectId": {}, + } + (content_root / "scene.json").write_text( + json.dumps(scene, indent=2) + "\n", encoding="utf-8" + ) + + +def ensure_engine_symlink(content_root: pathlib.Path, repo_root: pathlib.Path) -> None: + source = repo_root / "Content" / "Engine" + target = content_root / "Engine" + if target.exists() or target.is_symlink(): + return + target.symlink_to(source) + + +def main() -> int: + if len(sys.argv) != 2: + print("usage: generate_descriptor_bind_scene.py ", file=sys.stderr) + return 1 + + output_root = pathlib.Path(sys.argv[1]).resolve() + repo_root = pathlib.Path(__file__).resolve().parents[1] + content_root = output_root / "Content" + content_root.mkdir(parents=True, exist_ok=True) + + ensure_engine_symlink(content_root, repo_root) + write_shared_material_asset(content_root) + write_scene(content_root) + + print(output_root) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())