From 86de67f6f6e5be7a2eb1743cd3b013347fb258a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:24:29 +0000 Subject: [PATCH 1/6] Initial plan From b041bdf24e2b88262df8fc16fba4edf67139c024 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:36:59 +0000 Subject: [PATCH 2/6] Implement Hazel-style asset system: serializers, async systems, complete RuntimeAssetManager Agent-Logs-Url: https://github.com/starbounded-dev/LuxEngine/sessions/a7b976e8-d569-4fb6-889e-44131bcfcc6a Co-authored-by: sheazywi <73042839+sheazywi@users.noreply.github.com> --- Core/Source/Lux/Asset/AssetImporter.cpp | 71 ++++-- Core/Source/Lux/Asset/AssetImporter.h | 3 + Core/Source/Lux/Asset/AssetMetadata.h | 1 + Core/Source/Lux/Asset/AssetSerializer.h | 22 ++ Core/Source/Lux/Asset/AssimpMeshImporter.cpp | 212 ++++++++++++++++++ Core/Source/Lux/Asset/AssimpMeshImporter.h | 23 ++ Core/Source/Lux/Asset/EditorAssetManager.cpp | 45 +++- Core/Source/Lux/Asset/EditorAssetManager.h | 17 +- Core/Source/Lux/Asset/EditorAssetSystem.cpp | 81 +++++++ Core/Source/Lux/Asset/EditorAssetSystem.h | 53 +++++ Core/Source/Lux/Asset/MaterialSerializer.cpp | 87 +++++++ Core/Source/Lux/Asset/MaterialSerializer.h | 14 ++ Core/Source/Lux/Asset/MeshSerializer.cpp | 159 +++++++++++++ Core/Source/Lux/Asset/MeshSerializer.h | 32 +++ Core/Source/Lux/Asset/RuntimeAssetManager.cpp | 81 +++++++ Core/Source/Lux/Asset/RuntimeAssetManager.h | 41 ++++ Core/Source/Lux/Asset/RuntimeAssetSystem.cpp | 85 +++++++ Core/Source/Lux/Asset/RuntimeAssetSystem.h | 73 ++++++ Core/Source/Lux/Asset/TextureSerializer.cpp | 18 ++ Core/Source/Lux/Asset/TextureSerializer.h | 17 ++ 20 files changed, 1111 insertions(+), 24 deletions(-) create mode 100644 Core/Source/Lux/Asset/AssetSerializer.h create mode 100644 Core/Source/Lux/Asset/AssimpMeshImporter.cpp create mode 100644 Core/Source/Lux/Asset/AssimpMeshImporter.h create mode 100644 Core/Source/Lux/Asset/EditorAssetSystem.cpp create mode 100644 Core/Source/Lux/Asset/EditorAssetSystem.h create mode 100644 Core/Source/Lux/Asset/MaterialSerializer.cpp create mode 100644 Core/Source/Lux/Asset/MaterialSerializer.h create mode 100644 Core/Source/Lux/Asset/MeshSerializer.cpp create mode 100644 Core/Source/Lux/Asset/MeshSerializer.h create mode 100644 Core/Source/Lux/Asset/RuntimeAssetSystem.cpp create mode 100644 Core/Source/Lux/Asset/RuntimeAssetSystem.h create mode 100644 Core/Source/Lux/Asset/TextureSerializer.cpp create mode 100644 Core/Source/Lux/Asset/TextureSerializer.h diff --git a/Core/Source/Lux/Asset/AssetImporter.cpp b/Core/Source/Lux/Asset/AssetImporter.cpp index 880e65b..39826a0 100644 --- a/Core/Source/Lux/Asset/AssetImporter.cpp +++ b/Core/Source/Lux/Asset/AssetImporter.cpp @@ -4,42 +4,77 @@ #include "TextureImporter.h" #include "SceneImporter.h" #include "AudioImporter.h" +#include "TextureSerializer.h" +#include "MeshSerializer.h" +#include "MaterialSerializer.h" #include +#include namespace Lux { - using AssetImportFunction = std::function(AssetHandle, const AssetMetadata&)>; - static std::map s_AssetImportFunctions = { - {AssetType::Texture, TextureImporter::ImportTexture }, - { AssetType::Scene, SceneImporter::ImportScene }, - { AssetType::Audio, AudioImporter::ImportAudio },/* - { AssetType::ObjModel, ObjModelImporter::ImportObjModel }, - { AssetType::ScriptFile, SceneImporter::ImportScript }*/ - }; + // Serializer-based dispatch table (Hazel-style). + // Each entry owns its AssetSerializer instance. + static std::map> s_Serializers; + + static void InitSerializers() + { + // Texture + s_Serializers[AssetType::Texture] = std::make_unique(); + s_Serializers[AssetType::EnvMap] = std::make_unique(); + + // Mesh + s_Serializers[AssetType::MeshSource] = std::make_unique(); + s_Serializers[AssetType::Mesh] = std::make_unique(); + s_Serializers[AssetType::StaticMesh] = std::make_unique(); + + // Material + s_Serializers[AssetType::Material] = std::make_unique(); + } Ref AssetImporter::ImportAsset(AssetHandle handle, const AssetMetadata& metadata) { LUX_PROFILE_FUNCTION_COLOR("AssetImporter::ImportAsset", 0xF2FA8A); + // Lazy-initialise the serializer table once. + static std::once_flag s_InitFlag; + std::call_once(s_InitFlag, InitSerializers); + + // ── Serializer-based types ──────────────────────────────────────────── + auto serializerIt = s_Serializers.find(metadata.Type); + if (serializerIt != s_Serializers.end()) { - LUX_PROFILE_SCOPE_COLOR("AssetImporter::ImportAsset Scope", 0x27628A); + // Build a metadata copy that carries the handle (in case the + // metadata came in without it already set). + AssetMetadata meta = metadata; + if (meta.Handle == 0) + meta.Handle = handle; - if (s_AssetImportFunctions.find(metadata.Type) == s_AssetImportFunctions.end()) - { - LUX_CORE_ERROR("No importer available for asset type: {}", (uint16_t)metadata.Type); - return nullptr; - } + Ref asset; + if (serializerIt->second->TryLoadData(meta, asset)) + return asset; + + LUX_CORE_ERROR("AssetImporter: serializer failed for type {} (handle {})", + (uint16_t)metadata.Type, (uint64_t)handle); + return nullptr; } - auto& result = s_AssetImportFunctions.at(metadata.Type);//(metadata.Type)(handle, metadata); + // ── Legacy function-pointer importers ───────────────────────────────── + using AssetImportFunction = std::function(AssetHandle, const AssetMetadata&)>; + static const std::map s_LegacyImportFunctions = { + { AssetType::Scene, SceneImporter::ImportScene }, + { AssetType::Audio, AudioImporter::ImportAudio }, + }; + auto legacyIt = s_LegacyImportFunctions.find(metadata.Type); + if (legacyIt == s_LegacyImportFunctions.end()) { - LUX_PROFILE_SCOPE_COLOR("AssetImporter::ImportAsset 2 Scope", 0xD1C48A); - - return result(handle, metadata); + LUX_CORE_ERROR("AssetImporter: no importer for asset type {} (handle {})", + (uint16_t)metadata.Type, (uint64_t)handle); + return nullptr; } + return legacyIt->second(handle, metadata); } } diff --git a/Core/Source/Lux/Asset/AssetImporter.h b/Core/Source/Lux/Asset/AssetImporter.h index dfbf34d..eb890b2 100644 --- a/Core/Source/Lux/Asset/AssetImporter.h +++ b/Core/Source/Lux/Asset/AssetImporter.h @@ -1,9 +1,12 @@ #pragma once #include "AssetMetadata.h" +#include "AssetSerializer.h" namespace Lux { + // Routes asset load requests to the appropriate AssetSerializer (or legacy + // importer function) based on the asset type stored in the metadata. class AssetImporter { public: diff --git a/Core/Source/Lux/Asset/AssetMetadata.h b/Core/Source/Lux/Asset/AssetMetadata.h index d4a12b1..f0a0633 100644 --- a/Core/Source/Lux/Asset/AssetMetadata.h +++ b/Core/Source/Lux/Asset/AssetMetadata.h @@ -8,6 +8,7 @@ namespace Lux { struct AssetMetadata { + AssetHandle Handle = 0; AssetType Type = AssetType::None; std::filesystem::path FilePath = ""; diff --git a/Core/Source/Lux/Asset/AssetSerializer.h b/Core/Source/Lux/Asset/AssetSerializer.h new file mode 100644 index 0000000..4769ae6 --- /dev/null +++ b/Core/Source/Lux/Asset/AssetSerializer.h @@ -0,0 +1,22 @@ +#pragma once + +#include "AssetMetadata.h" + +namespace Lux +{ + // Abstract interface that every asset type's serializer must satisfy. + // Editor serializers load data from disk (e.g. via YAML / Assimp). + // Runtime serializers read from packed binary streams produced at build time. + class AssetSerializer + { + public: + virtual ~AssetSerializer() = default; + + // Serialize the asset back to its source representation (YAML, binary, etc.) + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const = 0; + + // Attempt to load the asset described by metadata. + // Returns true and sets asset on success; returns false on failure. + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const = 0; + }; +} diff --git a/Core/Source/Lux/Asset/AssimpMeshImporter.cpp b/Core/Source/Lux/Asset/AssimpMeshImporter.cpp new file mode 100644 index 0000000..7118f67 --- /dev/null +++ b/Core/Source/Lux/Asset/AssimpMeshImporter.cpp @@ -0,0 +1,212 @@ +#include "lpch.h" +#include "AssimpMeshImporter.h" + +#include "Lux/Core/Math/AABB.h" +#include "Lux/Renderer/VertexBuffer.h" +#include "Lux/Renderer/IndexBuffer.h" + +#include +#include +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +namespace Lux +{ + static glm::mat4 AssimpMat4ToGlm(const aiMatrix4x4& m) + { + glm::mat4 result; + result[0][0] = m.a1; result[1][0] = m.a2; result[2][0] = m.a3; result[3][0] = m.a4; + result[0][1] = m.b1; result[1][1] = m.b2; result[2][1] = m.b3; result[3][1] = m.b4; + result[0][2] = m.c1; result[1][2] = m.c2; result[2][2] = m.c3; result[3][2] = m.c4; + result[0][3] = m.d1; result[1][3] = m.d2; result[2][3] = m.d3; result[3][3] = m.d4; + return result; + } + + AssimpMeshImporter::AssimpMeshImporter(const std::filesystem::path& path) + : m_Path(path) + { + } + + static void TraverseNodes(Ref meshSource, + aiNode* node, + const glm::mat4& parentTransform, + uint32_t parentIndex, + uint32_t level = 0) + { + glm::mat4 localTransform = AssimpMat4ToGlm(node->mTransformation); + glm::mat4 worldTransform = parentTransform * localTransform; + + MeshNode luxNode; + luxNode.Name = node->mName.C_Str(); + luxNode.LocalTransform = localTransform; + luxNode.Parent = parentIndex; + + uint32_t nodeIndex = (uint32_t)meshSource->m_Nodes.size(); + meshSource->m_Nodes.push_back(luxNode); + + if (parentIndex != 0xffffffff) + meshSource->m_Nodes[parentIndex].Children.push_back(nodeIndex); + + auto& currentNode = meshSource->m_Nodes[nodeIndex]; + + for (uint32_t i = 0; i < node->mNumMeshes; i++) + { + uint32_t submeshIndex = node->mMeshes[i]; + currentNode.Submeshes.push_back(submeshIndex); + meshSource->m_Submeshes[submeshIndex].Transform = worldTransform; + meshSource->m_Submeshes[submeshIndex].LocalTransform = localTransform; + meshSource->m_Submeshes[submeshIndex].NodeName = node->mName.C_Str(); + } + + for (uint32_t i = 0; i < node->mNumChildren; i++) + TraverseNodes(meshSource, node->mChildren[i], worldTransform, nodeIndex, level + 1); + } + + Ref AssimpMeshImporter::ImportToMeshSource() + { + Ref meshSource = Ref::Create(); + meshSource->m_FilePath = m_Path.string(); + + Assimp::Importer importer; + importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); + + constexpr uint32_t meshImportFlags = + aiProcess_CalcTangentSpace | + aiProcess_Triangulate | + aiProcess_SortByPType | + aiProcess_GenNormals | + aiProcess_GenUVCoords | + aiProcess_OptimizeMeshes | + aiProcess_JoinIdenticalVertices | + aiProcess_LimitBoneWeights | + aiProcess_ValidateDataStructure | + aiProcess_GlobalScale; + + const aiScene* scene = importer.ReadFile(m_Path.string(), meshImportFlags); + if (!scene || !scene->HasMeshes()) + { + LUX_CORE_ERROR("AssimpMeshImporter: Failed to import mesh from '{}'", m_Path.string()); + LUX_CORE_ERROR(" Assimp error: {}", importer.GetErrorString()); + return nullptr; + } + + // ── Reserve submeshes ───────────────────────────────────────────────── + meshSource->m_Submeshes.reserve(scene->mNumMeshes); + + uint32_t vertexCount = 0; + uint32_t indexCount = 0; + + meshSource->m_BoundingBox.Min = { FLT_MAX, FLT_MAX, FLT_MAX }; + meshSource->m_BoundingBox.Max = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; + + for (uint32_t m = 0; m < scene->mNumMeshes; m++) + { + aiMesh* mesh = scene->mMeshes[m]; + + Submesh& submesh = meshSource->m_Submeshes.emplace_back(); + submesh.BaseVertex = vertexCount; + submesh.BaseIndex = indexCount; + submesh.MaterialIndex = mesh->mMaterialIndex; + submesh.IndexCount = mesh->mNumFaces * 3; + submesh.VertexCount = mesh->mNumVertices; + submesh.MeshName = mesh->mName.C_Str(); + + vertexCount += mesh->mNumVertices; + indexCount += submesh.IndexCount; + + // ── AABB per submesh ────────────────────────────────────────────── + AABB& aabb = submesh.BoundingBox; + aabb.Min = { FLT_MAX, FLT_MAX, FLT_MAX }; + aabb.Max = { -FLT_MAX, -FLT_MAX, -FLT_MAX }; + + for (uint32_t v = 0; v < mesh->mNumVertices; v++) + { + Vertex vertex; + vertex.Position = { mesh->mVertices[v].x, mesh->mVertices[v].y, mesh->mVertices[v].z }; + vertex.Normal = { mesh->mNormals[v].x, mesh->mNormals[v].y, mesh->mNormals[v].z }; + + if (mesh->HasTangentsAndBitangents()) + { + vertex.Tangent = { mesh->mTangents[v].x, mesh->mTangents[v].y, mesh->mTangents[v].z }; + vertex.Binormal = { mesh->mBitangents[v].x, mesh->mBitangents[v].y, mesh->mBitangents[v].z }; + } + + if (mesh->HasTextureCoords(0)) + vertex.Texcoord = { mesh->mTextureCoords[0][v].x, mesh->mTextureCoords[0][v].y }; + else + vertex.Texcoord = { 0.0f, 0.0f }; + + aabb.Min.x = glm::min(vertex.Position.x, aabb.Min.x); + aabb.Min.y = glm::min(vertex.Position.y, aabb.Min.y); + aabb.Min.z = glm::min(vertex.Position.z, aabb.Min.z); + aabb.Max.x = glm::max(vertex.Position.x, aabb.Max.x); + aabb.Max.y = glm::max(vertex.Position.y, aabb.Max.y); + aabb.Max.z = glm::max(vertex.Position.z, aabb.Max.z); + + meshSource->m_Vertices.push_back(vertex); + } + + meshSource->m_BoundingBox.Min.x = glm::min(aabb.Min.x, meshSource->m_BoundingBox.Min.x); + meshSource->m_BoundingBox.Min.y = glm::min(aabb.Min.y, meshSource->m_BoundingBox.Min.y); + meshSource->m_BoundingBox.Min.z = glm::min(aabb.Min.z, meshSource->m_BoundingBox.Min.z); + meshSource->m_BoundingBox.Max.x = glm::max(aabb.Max.x, meshSource->m_BoundingBox.Max.x); + meshSource->m_BoundingBox.Max.y = glm::max(aabb.Max.y, meshSource->m_BoundingBox.Max.y); + meshSource->m_BoundingBox.Max.z = glm::max(aabb.Max.z, meshSource->m_BoundingBox.Max.z); + + // ── Indices ─────────────────────────────────────────────────────── + for (uint32_t f = 0; f < mesh->mNumFaces; f++) + { + const aiFace& face = mesh->mFaces[f]; + LUX_CORE_ASSERT(face.mNumIndices == 3, "Only triangles are supported!"); + Index idx; + idx.V1 = face.mIndices[0]; + idx.V2 = face.mIndices[1]; + idx.V3 = face.mIndices[2]; + meshSource->m_Indices.push_back(idx); + } + } + + // ── Node hierarchy ──────────────────────────────────────────────────── + // Insert sentinel root so every real node has a valid parentIndex + meshSource->m_Nodes.emplace_back(); // root placeholder + TraverseNodes(meshSource, scene->mRootNode, glm::mat4(1.0f), 0xffffffff); + + // ── Materials (allocate zero-material placeholders) ─────────────────── + meshSource->m_Materials.resize(scene->mNumMaterials, 0); + + // ── Triangle cache ──────────────────────────────────────────────────── + for (uint32_t i = 0; i < (uint32_t)meshSource->m_Submeshes.size(); i++) + { + const Submesh& sm = meshSource->m_Submeshes[i]; + for (uint32_t f = 0; f < sm.IndexCount / 3; f++) + { + const Index& idx = meshSource->m_Indices[sm.BaseIndex / 3 + f]; + meshSource->m_TriangleCache[i].emplace_back( + meshSource->m_Vertices[sm.BaseVertex + idx.V1], + meshSource->m_Vertices[sm.BaseVertex + idx.V2], + meshSource->m_Vertices[sm.BaseVertex + idx.V3]); + } + } + + // ── GPU buffers ─────────────────────────────────────────────────────── + meshSource->m_VertexBuffer = VertexBuffer::Create( + Buffer(meshSource->m_Vertices.data(), + (uint32_t)(meshSource->m_Vertices.size() * sizeof(Vertex)))); + + meshSource->m_IndexBuffer = IndexBuffer::Create( + Buffer(meshSource->m_Indices.data(), + (uint32_t)(meshSource->m_Indices.size() * sizeof(Index)))); + + LUX_CORE_INFO("AssimpMeshImporter: Loaded '{}' – {} submeshes, {} vertices, {} indices", + m_Path.filename().string(), + meshSource->m_Submeshes.size(), + meshSource->m_Vertices.size(), + meshSource->m_Indices.size() * 3); + + return meshSource; + } +} diff --git a/Core/Source/Lux/Asset/AssimpMeshImporter.h b/Core/Source/Lux/Asset/AssimpMeshImporter.h new file mode 100644 index 0000000..9f83175 --- /dev/null +++ b/Core/Source/Lux/Asset/AssimpMeshImporter.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Lux/Core/Base.h" +#include "Lux/Renderer/Mesh.h" + +#include + +namespace Lux +{ + // Imports a mesh file from disk using the Assimp library and populates + // a MeshSource asset with vertices, indices, submeshes and basic material handles. + class AssimpMeshImporter + { + public: + explicit AssimpMeshImporter(const std::filesystem::path& path); + + // Load the mesh source. Returns nullptr on failure. + Ref ImportToMeshSource(); + + private: + std::filesystem::path m_Path; + }; +} diff --git a/Core/Source/Lux/Asset/EditorAssetManager.cpp b/Core/Source/Lux/Asset/EditorAssetManager.cpp index c9c82bf..870ba30 100644 --- a/Core/Source/Lux/Asset/EditorAssetManager.cpp +++ b/Core/Source/Lux/Asset/EditorAssetManager.cpp @@ -1,5 +1,6 @@ #include "lpch.h" #include "AssetManager.h" +#include "EditorAssetManager.h" #include "AssetImporter.h" @@ -17,11 +18,17 @@ namespace Lux { { ".png", AssetType::Texture }, { ".jpg", AssetType::Texture }, { ".jpeg", AssetType::Texture }, + { ".hdr", AssetType::EnvMap }, { ".mp3", AssetType::Audio }, { ".wav", AssetType::Audio }, - { ".ogg", AssetType::Audio },/* - { ".obj", AssetType::ObjModel }, - { ".cs", AssetType::ScriptFile },*/ + { ".ogg", AssetType::Audio }, + { ".fbx", AssetType::MeshSource }, + { ".gltf", AssetType::MeshSource }, + { ".glb", AssetType::MeshSource }, + { ".obj", AssetType::MeshSource }, + { ".lmesh", AssetType::Mesh }, + { ".lsmesh", AssetType::StaticMesh }, + { ".lmat", AssetType::Material }, }; static AssetType GetAssetTypeFromFileExtension(const std::filesystem::path& extension) @@ -41,6 +48,33 @@ namespace Lux { return out; } + EditorAssetManager::EditorAssetManager() = default; + + EditorAssetManager::~EditorAssetManager() + { + m_AssetSystem.Stop(); + } + + void EditorAssetManager::LoadAssetAsync(AssetHandle handle) + { + if (!IsAssetHandleValid(handle)) + { + LUX_CORE_WARN("EditorAssetManager::LoadAssetAsync – unknown handle {}", (uint64_t)handle); + return; + } + + if (IsAssetLoaded(handle)) + return; // already loaded + + const AssetMetadata& metadata = GetMetadata(handle); + m_AssetSystem.QueueAssetLoad(metadata); + } + + void EditorAssetManager::SyncLoadedAssets() + { + m_AssetSystem.SyncLoadedAssets(m_LoadedAssets); + } + bool EditorAssetManager::IsAssetHandleValid(AssetHandle handle) const { return handle != 0 && m_AssetRegistry.find(handle) != m_AssetRegistry.end(); @@ -63,6 +97,7 @@ namespace Lux { { AssetHandle handle; // generate new handle AssetMetadata metadata; + metadata.Handle = handle; metadata.FilePath = filepath; metadata.Type = GetAssetTypeFromFileExtension(filepath.extension()); LUX_CORE_ASSERT(metadata.Type != AssetType::None); @@ -78,8 +113,9 @@ namespace Lux { void EditorAssetManager::ImportScriptAsset(const std::filesystem::path& filepath, uint64_t uuid) { - AssetHandle handle = uuid; // generate new handle + AssetHandle handle = uuid; // use provided uuid as handle AssetMetadata metadata; + metadata.Handle = handle; metadata.FilePath = filepath; metadata.Type = GetAssetTypeFromFileExtension(filepath.extension()); LUX_CORE_ASSERT(metadata.Type != AssetType::None); @@ -196,6 +232,7 @@ namespace Lux { { AssetHandle handle = node["Handle"].as(); auto& metadata = m_AssetRegistry[handle]; + metadata.Handle = handle; metadata.FilePath = node["FilePath"].as(); metadata.Type = AssetTypeFromString(node["Type"].as()); } diff --git a/Core/Source/Lux/Asset/EditorAssetManager.h b/Core/Source/Lux/Asset/EditorAssetManager.h index 6577457..0175bba 100644 --- a/Core/Source/Lux/Asset/EditorAssetManager.h +++ b/Core/Source/Lux/Asset/EditorAssetManager.h @@ -2,6 +2,7 @@ #include "AssetManagerBase.h" #include "AssetMetadata.h" +#include "EditorAssetSystem.h" #include @@ -12,6 +13,9 @@ namespace Lux { class EditorAssetManager : public AssetManagerBase { public: + EditorAssetManager(); + virtual ~EditorAssetManager(); + virtual Ref GetAsset(AssetHandle handle) override; virtual bool IsAssetHandleValid(AssetHandle handle) const override; @@ -21,6 +25,14 @@ namespace Lux { void ImportAsset(const std::filesystem::path& filepath); void ImportScriptAsset(const std::filesystem::path& filepath, uint64_t uuid); + // Queue a background async load. Call SyncLoadedAssets() each frame to + // pick up finished results. + void LoadAssetAsync(AssetHandle handle); + + // Must be called every frame on the main thread to drain the finished + // load queue from EditorAssetSystem. + void SyncLoadedAssets(); + const AssetMetadata& GetMetadata(AssetHandle handle) const; const std::filesystem::path& GetFilePath(AssetHandle handle) const; @@ -30,8 +42,9 @@ namespace Lux { bool DeserializeAssetRegistry(); private: - AssetRegistry m_AssetRegistry; - AssetMap m_LoadedAssets; + AssetRegistry m_AssetRegistry; + AssetMap m_LoadedAssets; + EditorAssetSystem m_AssetSystem; // TODO: memory-only assets }; diff --git a/Core/Source/Lux/Asset/EditorAssetSystem.cpp b/Core/Source/Lux/Asset/EditorAssetSystem.cpp new file mode 100644 index 0000000..6a2f5da --- /dev/null +++ b/Core/Source/Lux/Asset/EditorAssetSystem.cpp @@ -0,0 +1,81 @@ +#include "lpch.h" +#include "EditorAssetSystem.h" + +#include "AssetImporter.h" + +namespace Lux +{ + EditorAssetSystem::EditorAssetSystem() + : m_Thread("EditorAssetSystem") + { + m_Running = true; + m_Thread.Dispatch(&EditorAssetSystem::WorkerThread, this); + } + + EditorAssetSystem::~EditorAssetSystem() + { + Stop(); + } + + void EditorAssetSystem::QueueAssetLoad(const AssetMetadata& metadata) + { + { + std::scoped_lock lock(m_LoadQueueMutex); + m_LoadQueue.push(metadata); + } + m_LoadQueueCV.notify_one(); + } + + void EditorAssetSystem::SyncLoadedAssets(AssetMap& loadedAssets) + { + std::scoped_lock lock(m_FinishedQueueMutex); + while (!m_FinishedQueue.empty()) + { + auto& entry = m_FinishedQueue.front(); + if (entry.Asset) + loadedAssets[entry.Handle] = entry.Asset; + m_FinishedQueue.pop(); + } + } + + void EditorAssetSystem::Stop() + { + if (!m_Running.exchange(false)) + return; // already stopped + + m_LoadQueueCV.notify_all(); + m_Thread.Join(); + } + + void EditorAssetSystem::WorkerThread() + { + while (m_Running) + { + AssetMetadata metadata; + + { + std::unique_lock lock(m_LoadQueueMutex); + m_LoadQueueCV.wait(lock, [this] + { + return !m_LoadQueue.empty() || !m_Running; + }); + + if (!m_Running && m_LoadQueue.empty()) + break; + + metadata = m_LoadQueue.front(); + m_LoadQueue.pop(); + } + + // Load on worker thread + Ref asset = AssetImporter::ImportAsset(metadata.Handle, metadata); + if (asset) + asset->Handle = metadata.Handle; + + { + std::scoped_lock lock(m_FinishedQueueMutex); + m_FinishedQueue.push({ metadata.Handle, asset }); + } + } + } +} diff --git a/Core/Source/Lux/Asset/EditorAssetSystem.h b/Core/Source/Lux/Asset/EditorAssetSystem.h new file mode 100644 index 0000000..121ad12 --- /dev/null +++ b/Core/Source/Lux/Asset/EditorAssetSystem.h @@ -0,0 +1,53 @@ +#pragma once + +#include "AssetManagerBase.h" +#include "AssetMetadata.h" + +#include "Lux/Core/Thread.h" + +#include +#include +#include +#include + +namespace Lux +{ + // Processes asset load requests on a background thread so that the main + // thread is not blocked. Once a load completes the asset is placed in a + // "pending sync" queue that EditorAssetManager drains each frame on the + // main thread via SyncLoadedAssets(). + class EditorAssetSystem + { + public: + EditorAssetSystem(); + ~EditorAssetSystem(); + + // Push a load request. Thread-safe. + void QueueAssetLoad(const AssetMetadata& metadata); + + // Must be called from the main thread each frame. + // Drains the finished-load queue and inserts the assets into the + // provided map. + void SyncLoadedAssets(AssetMap& loadedAssets); + + // Signals the worker thread to stop and waits for it to finish. + void Stop(); + + private: + void WorkerThread(); + + private: + Thread m_Thread; + std::atomic_bool m_Running{ false }; + + // Pending load requests (main → worker) + std::queue m_LoadQueue; + std::mutex m_LoadQueueMutex; + std::condition_variable m_LoadQueueCV; + + // Finished loads (worker → main) + struct LoadedEntry { AssetHandle Handle; Ref Asset; }; + std::queue m_FinishedQueue; + std::mutex m_FinishedQueueMutex; + }; +} diff --git a/Core/Source/Lux/Asset/MaterialSerializer.cpp b/Core/Source/Lux/Asset/MaterialSerializer.cpp new file mode 100644 index 0000000..e7fd41c --- /dev/null +++ b/Core/Source/Lux/Asset/MaterialSerializer.cpp @@ -0,0 +1,87 @@ +#include "lpch.h" +#include "MaterialSerializer.h" + +#include "Lux/Renderer/MaterialAsset.h" +#include "Lux/Project/Project.h" + +#include +#include + +#include + +namespace Lux +{ + static void WriteVec3(YAML::Emitter& out, const glm::vec3& v) + { + out << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << YAML::EndSeq; + } + + void MaterialSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref materialAsset = asset.As(); + LUX_CORE_ASSERT(materialAsset); + + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "Material" << YAML::Value; + out << YAML::BeginMap; + + out << YAML::Key << "AlbedoColor" << YAML::Value; + WriteVec3(out, materialAsset->GetAlbedoColor()); + out << YAML::Key << "Metalness" << YAML::Value << materialAsset->GetMetalness(); + out << YAML::Key << "Roughness" << YAML::Value << materialAsset->GetRoughness(); + out << YAML::Key << "Emission" << YAML::Value << materialAsset->GetEmission(); + out << YAML::Key << "Transparency" << YAML::Value << materialAsset->GetTransparency(); + out << YAML::Key << "Transparent" << YAML::Value << materialAsset->IsTransparent(); + out << YAML::Key << "ShadowCasting"<< YAML::Value << materialAsset->IsShadowCasting(); + out << YAML::Key << "UseNormalMap" << YAML::Value << materialAsset->IsUsingNormalMap(); + + out << YAML::EndMap; + out << YAML::EndMap; + + std::ofstream fout(filepath); + fout << out.c_str(); + } + + bool MaterialSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + + YAML::Node data; + try + { + data = YAML::LoadFile(filepath.string()); + } + catch (const YAML::ParserException& e) + { + LUX_CORE_ERROR("MaterialSerializer: Failed to parse '{}': {}", filepath.string(), e.what()); + return false; + } + + auto matNode = data["Material"]; + if (!matNode) + return false; + + bool transparent = matNode["Transparent"].as(false); + Ref materialAsset = Ref::Create(transparent); + materialAsset->Handle = metadata.Handle; + + if (auto node = matNode["AlbedoColor"]) + { + auto seq = node.as>(); + if (seq.size() >= 3) + materialAsset->SetAlbedoColor({ seq[0], seq[1], seq[2] }); + } + materialAsset->SetMetalness(matNode["Metalness"].as(0.0f)); + materialAsset->SetRoughness(matNode["Roughness"].as(0.5f)); + materialAsset->SetEmission(matNode["Emission"].as(0.0f)); + materialAsset->SetTransparency(matNode["Transparency"].as(1.0f)); + materialAsset->SetShadowCasting(matNode["ShadowCasting"].as(true)); + materialAsset->SetUseNormalMap(matNode["UseNormalMap"].as(false)); + + asset = materialAsset; + return true; + } +} diff --git a/Core/Source/Lux/Asset/MaterialSerializer.h b/Core/Source/Lux/Asset/MaterialSerializer.h new file mode 100644 index 0000000..bc74f68 --- /dev/null +++ b/Core/Source/Lux/Asset/MaterialSerializer.h @@ -0,0 +1,14 @@ +#pragma once + +#include "AssetSerializer.h" + +namespace Lux +{ + // YAML-based serializer for MaterialAsset. + class MaterialSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + }; +} diff --git a/Core/Source/Lux/Asset/MeshSerializer.cpp b/Core/Source/Lux/Asset/MeshSerializer.cpp new file mode 100644 index 0000000..a735a15 --- /dev/null +++ b/Core/Source/Lux/Asset/MeshSerializer.cpp @@ -0,0 +1,159 @@ +#include "lpch.h" +#include "MeshSerializer.h" + +#include "AssimpMeshImporter.h" + +#include "Lux/Project/Project.h" +#include "Lux/Renderer/Mesh.h" + +#include +#include + +namespace Lux +{ + // ─── MeshSourceSerializer ──────────────────────────────────────────────── + + void MeshSourceSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + // The source file (fbx / gltf / obj …) IS the serialized form – nothing to write back. + } + + bool MeshSourceSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + AssimpMeshImporter importer(filepath); + Ref meshSource = importer.ImportToMeshSource(); + if (!meshSource) + return false; + + meshSource->Handle = metadata.Handle; + asset = meshSource; + return true; + } + + // ─── MeshSerializer ────────────────────────────────────────────────────── + + void MeshSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref mesh = asset.As(); + LUX_CORE_ASSERT(mesh); + + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "Mesh" << YAML::Value; + out << YAML::BeginMap; + out << YAML::Key << "MeshSource" << YAML::Value << (uint64_t)mesh->GetMeshSource(); + out << YAML::Key << "GenerateColliders" << YAML::Value << mesh->ShouldGenerateColliders(); + + out << YAML::Key << "Submeshes" << YAML::Value << YAML::BeginSeq; + for (uint32_t idx : mesh->GetSubmeshes()) + out << idx; + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + std::ofstream fout(filepath); + fout << out.c_str(); + } + + bool MeshSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + + YAML::Node data; + try + { + data = YAML::LoadFile(filepath.string()); + } + catch (const YAML::ParserException& e) + { + LUX_CORE_ERROR("MeshSerializer: Failed to parse '{}': {}", filepath.string(), e.what()); + return false; + } + + auto meshNode = data["Mesh"]; + if (!meshNode) + return false; + + AssetHandle meshSource = meshNode["MeshSource"].as(0); + bool generateColliders = meshNode["GenerateColliders"].as(false); + + std::vector submeshes; + if (auto submeshesNode = meshNode["Submeshes"]) + { + for (const auto& node : submeshesNode) + submeshes.push_back(node.as()); + } + + Ref mesh = Ref::Create(meshSource, submeshes, generateColliders); + mesh->Handle = metadata.Handle; + asset = mesh; + return true; + } + + // ─── StaticMeshSerializer ───────────────────────────────────────────────── + + void StaticMeshSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + Ref mesh = asset.As(); + LUX_CORE_ASSERT(mesh); + + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << "StaticMesh" << YAML::Value; + out << YAML::BeginMap; + out << YAML::Key << "MeshSource" << YAML::Value << (uint64_t)mesh->GetMeshSource(); + out << YAML::Key << "GenerateColliders" << YAML::Value << mesh->ShouldGenerateColliders(); + + out << YAML::Key << "Submeshes" << YAML::Value << YAML::BeginSeq; + for (uint32_t idx : mesh->GetSubmeshes()) + out << idx; + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + std::ofstream fout(filepath); + fout << out.c_str(); + } + + bool StaticMeshSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + auto filepath = Project::GetActiveAssetDirectory() / metadata.FilePath; + + YAML::Node data; + try + { + data = YAML::LoadFile(filepath.string()); + } + catch (const YAML::ParserException& e) + { + LUX_CORE_ERROR("StaticMeshSerializer: Failed to parse '{}': {}", filepath.string(), e.what()); + return false; + } + + auto meshNode = data["StaticMesh"]; + if (!meshNode) + return false; + + AssetHandle meshSource = meshNode["MeshSource"].as(0); + bool generateColliders = meshNode["GenerateColliders"].as(false); + + std::vector submeshes; + if (auto submeshesNode = meshNode["Submeshes"]) + { + for (const auto& node : submeshesNode) + submeshes.push_back(node.as()); + } + + Ref mesh = Ref::Create(meshSource, submeshes, generateColliders); + mesh->Handle = metadata.Handle; + asset = mesh; + return true; + } +} diff --git a/Core/Source/Lux/Asset/MeshSerializer.h b/Core/Source/Lux/Asset/MeshSerializer.h new file mode 100644 index 0000000..02062a3 --- /dev/null +++ b/Core/Source/Lux/Asset/MeshSerializer.h @@ -0,0 +1,32 @@ +#pragma once + +#include "AssetSerializer.h" + +namespace Lux +{ + // Serializes / deserializes MeshSource assets. + // Editor path – loads via AssimpMeshImporter (Assimp). + // Serialize – no-op; source files are the canonical representation. + class MeshSourceSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + }; + + // Serializes / deserializes Mesh assets (YAML – stores MeshSource handle + submesh list). + class MeshSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + }; + + // Serializes / deserializes StaticMesh assets (YAML – stores MeshSource handle + submesh list). + class StaticMeshSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + }; +} diff --git a/Core/Source/Lux/Asset/RuntimeAssetManager.cpp b/Core/Source/Lux/Asset/RuntimeAssetManager.cpp index d1e1710..081dede 100644 --- a/Core/Source/Lux/Asset/RuntimeAssetManager.cpp +++ b/Core/Source/Lux/Asset/RuntimeAssetManager.cpp @@ -1,7 +1,88 @@ #include "lpch.h" #include "AssetManager.h" +#include "RuntimeAssetManager.h" namespace Lux { + RuntimeAssetManager::RuntimeAssetManager() = default; + RuntimeAssetManager::~RuntimeAssetManager() + { + m_AssetSystem.Stop(); + } + + Ref RuntimeAssetManager::GetAsset(AssetHandle handle) + { + if (!IsAssetHandleValid(handle)) + return nullptr; + + if (IsAssetLoaded(handle)) + return m_LoadedAssets.at(handle); + + // Synchronous fallback: this should rarely be hit in a well-structured + // runtime because assets are expected to be pre-loaded asynchronously. + LUX_CORE_WARN("RuntimeAssetManager::GetAsset – synchronous load for handle {}", (uint64_t)handle); + + // Trigger an async load and wait for it (simple spin). + std::atomic_bool done{ false }; + LoadAssetAsync(handle, [&done](AssetHandle, Ref) + { + done = true; + }); + + while (!done) + { + SyncLoadedAssets(); + std::this_thread::yield(); + } + + return IsAssetLoaded(handle) ? m_LoadedAssets.at(handle) : nullptr; + } + + bool RuntimeAssetManager::IsAssetHandleValid(AssetHandle handle) const + { + return m_AssetRegistry.find(handle) != m_AssetRegistry.end(); + } + + bool RuntimeAssetManager::IsAssetLoaded(AssetHandle handle) const + { + return m_LoadedAssets.find(handle) != m_LoadedAssets.end(); + } + + AssetType RuntimeAssetManager::GetAssetType(AssetHandle handle) const + { + auto it = m_AssetRegistry.find(handle); + return it != m_AssetRegistry.end() ? it->second : AssetType::None; + } + + void RuntimeAssetManager::RegisterAsset(AssetHandle handle, AssetType type) + { + m_AssetRegistry[handle] = type; + } + + void RuntimeAssetManager::LoadAssetAsync(AssetHandle handle, + std::function)> callback) + { + if (!IsAssetHandleValid(handle)) + { + LUX_CORE_WARN("RuntimeAssetManager::LoadAssetAsync – unknown handle {}", (uint64_t)handle); + return; + } + + RuntimeAssetLoadRequest request; + request.Handle = handle; + request.Type = GetAssetType(handle); + request.Callback = std::move(callback); + m_AssetSystem.QueueAssetLoad(std::move(request)); + } + + void RuntimeAssetManager::SyncLoadedAssets() + { + m_AssetSystem.SyncLoadedAssets(m_LoadedAssets); + } + + void RuntimeAssetManager::SetRuntimeLoader(RuntimeAssetSystem::RuntimeLoader loader) + { + m_AssetSystem.SetLoader(std::move(loader)); + } } diff --git a/Core/Source/Lux/Asset/RuntimeAssetManager.h b/Core/Source/Lux/Asset/RuntimeAssetManager.h index 8eae646..0eb5042 100644 --- a/Core/Source/Lux/Asset/RuntimeAssetManager.h +++ b/Core/Source/Lux/Asset/RuntimeAssetManager.h @@ -1,11 +1,52 @@ #pragma once #include "AssetManagerBase.h" +#include "AssetMetadata.h" +#include "RuntimeAssetSystem.h" namespace Lux { + // Asset manager for the shipped runtime. Assets are loaded via a + // RuntimeAssetSystem worker thread. The registry is built programmatically + // (e.g. by scanning packed asset data) rather than from YAML on disk. class RuntimeAssetManager : public AssetManagerBase { public: + RuntimeAssetManager(); + virtual ~RuntimeAssetManager(); + + // ── AssetManagerBase interface ──────────────────────────────────────── + virtual Ref GetAsset(AssetHandle handle) override; + + virtual bool IsAssetHandleValid(AssetHandle handle) const override; + virtual bool IsAssetLoaded(AssetHandle handle) const override; + virtual AssetType GetAssetType(AssetHandle handle) const override; + + // ── Runtime-specific API ────────────────────────────────────────────── + + // Register an asset handle so the manager knows its type. + // Call this during application start-up for every asset in the pack. + void RegisterAsset(AssetHandle handle, AssetType type); + + // Enqueue an async load. The asset will be available after the next + // SyncLoadedAssets() call on the main thread. + void LoadAssetAsync(AssetHandle handle, + std::function)> callback = nullptr); + + // Must be called every frame on the main thread. + // Drains finished background loads into the loaded-asset map. + void SyncLoadedAssets(); + + // Provide the actual loader function used by the worker thread. + void SetRuntimeLoader(RuntimeAssetSystem::RuntimeLoader loader); + + private: + using AssetRegistry = std::map; + + AssetRegistry m_AssetRegistry; + AssetMap m_LoadedAssets; + + RuntimeAssetSystem m_AssetSystem; }; } + diff --git a/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp b/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp new file mode 100644 index 0000000..73f71f3 --- /dev/null +++ b/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp @@ -0,0 +1,85 @@ +#include "lpch.h" +#include "RuntimeAssetSystem.h" + +namespace Lux +{ + RuntimeAssetSystem::RuntimeAssetSystem() + : m_Thread("RuntimeAssetSystem") + { + m_Running = true; + m_Thread.Dispatch(&RuntimeAssetSystem::WorkerThread, this); + } + + RuntimeAssetSystem::~RuntimeAssetSystem() + { + Stop(); + } + + void RuntimeAssetSystem::QueueAssetLoad(RuntimeAssetLoadRequest request) + { + { + std::scoped_lock lock(m_LoadQueueMutex); + m_LoadQueue.push(std::move(request)); + } + m_LoadQueueCV.notify_one(); + } + + void RuntimeAssetSystem::SyncLoadedAssets(AssetMap& loadedAssets) + { + std::scoped_lock lock(m_FinishedQueueMutex); + while (!m_FinishedQueue.empty()) + { + auto& entry = m_FinishedQueue.front(); + if (entry.Asset) + { + loadedAssets[entry.Handle] = entry.Asset; + if (entry.Callback) + entry.Callback(entry.Handle, entry.Asset); + } + m_FinishedQueue.pop(); + } + } + + void RuntimeAssetSystem::Stop() + { + if (!m_Running.exchange(false)) + return; // already stopped + + m_LoadQueueCV.notify_all(); + m_Thread.Join(); + } + + void RuntimeAssetSystem::WorkerThread() + { + while (m_Running) + { + RuntimeAssetLoadRequest request; + + { + std::unique_lock lock(m_LoadQueueMutex); + m_LoadQueueCV.wait(lock, [this] + { + return !m_LoadQueue.empty() || !m_Running; + }); + + if (!m_Running && m_LoadQueue.empty()) + break; + + request = std::move(m_LoadQueue.front()); + m_LoadQueue.pop(); + } + + Ref asset; + if (m_Loader) + asset = m_Loader(request.Handle, request.Type); + + if (asset) + asset->Handle = request.Handle; + + { + std::scoped_lock lock(m_FinishedQueueMutex); + m_FinishedQueue.push({ request.Handle, asset, std::move(request.Callback) }); + } + } + } +} diff --git a/Core/Source/Lux/Asset/RuntimeAssetSystem.h b/Core/Source/Lux/Asset/RuntimeAssetSystem.h new file mode 100644 index 0000000..e006810 --- /dev/null +++ b/Core/Source/Lux/Asset/RuntimeAssetSystem.h @@ -0,0 +1,73 @@ +#pragma once + +#include "AssetManagerBase.h" +#include "AssetMetadata.h" + +#include "Lux/Core/Thread.h" + +#include +#include +#include +#include +#include + +namespace Lux +{ + // Request submitted to RuntimeAssetSystem for an async load. + struct RuntimeAssetLoadRequest + { + AssetHandle Handle = 0; + AssetType Type = AssetType::None; + + // Callback invoked on the main thread (via SyncLoadedAssets) once the + // asset has been loaded. May be nullptr. + std::function)> Callback; + }; + + // Background loading thread for runtime (shipped game) use. + // Unlike EditorAssetSystem it does NOT use Assimp / YAML. It expects the + // caller to provide a factory callback that can load the asset from a + // binary asset-pack stream. + class RuntimeAssetSystem + { + public: + RuntimeAssetSystem(); + ~RuntimeAssetSystem(); + + // Enqueue a load request. Thread-safe. + void QueueAssetLoad(RuntimeAssetLoadRequest request); + + // Must be called from the main thread each frame. + // Delivers finished assets to loadedAssets and fires per-asset callbacks. + void SyncLoadedAssets(AssetMap& loadedAssets); + + // Signals the worker thread to stop and waits for it to finish. + void Stop(); + + // Supply the function used to load an asset at runtime. + // Signature: Ref loader(AssetHandle handle, AssetType type) + using RuntimeLoader = std::function(AssetHandle, AssetType)>; + void SetLoader(RuntimeLoader loader) { m_Loader = std::move(loader); } + + private: + void WorkerThread(); + + private: + Thread m_Thread; + std::atomic_bool m_Running{ false }; + RuntimeLoader m_Loader; + + std::queue m_LoadQueue; + std::mutex m_LoadQueueMutex; + std::condition_variable m_LoadQueueCV; + + struct LoadedEntry + { + AssetHandle Handle; + Ref Asset; + std::function)> Callback; + }; + std::queue m_FinishedQueue; + std::mutex m_FinishedQueueMutex; + }; +} diff --git a/Core/Source/Lux/Asset/TextureSerializer.cpp b/Core/Source/Lux/Asset/TextureSerializer.cpp new file mode 100644 index 0000000..92ebbb1 --- /dev/null +++ b/Core/Source/Lux/Asset/TextureSerializer.cpp @@ -0,0 +1,18 @@ +#include "lpch.h" +#include "TextureSerializer.h" + +#include "TextureImporter.h" + +namespace Lux +{ + void TextureSerializer::Serialize(const AssetMetadata& metadata, const Ref& asset) const + { + // Source-format textures (.png/.jpg/…) are their own serialized form – nothing to write back. + } + + bool TextureSerializer::TryLoadData(const AssetMetadata& metadata, Ref& asset) const + { + asset = TextureImporter::ImportTexture(metadata.Handle, metadata); + return asset != nullptr; + } +} diff --git a/Core/Source/Lux/Asset/TextureSerializer.h b/Core/Source/Lux/Asset/TextureSerializer.h new file mode 100644 index 0000000..5881f9a --- /dev/null +++ b/Core/Source/Lux/Asset/TextureSerializer.h @@ -0,0 +1,17 @@ +#pragma once + +#include "AssetSerializer.h" + +namespace Lux +{ + // Serializes / deserializes Texture2D assets. + // Editor path: delegates to TextureImporter (stb_image). + // The serialize direction is a no-op for source-format textures + // (the source file IS the serialized form). + class TextureSerializer : public AssetSerializer + { + public: + virtual void Serialize(const AssetMetadata& metadata, const Ref& asset) const override; + virtual bool TryLoadData(const AssetMetadata& metadata, Ref& asset) const override; + }; +} From c2c5d3fc8d69f9c5196c21957d18c49dcaa85713 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 04:41:15 +0000 Subject: [PATCH 3/6] Address code review: fix busy-wait, add file-open error handling Agent-Logs-Url: https://github.com/starbounded-dev/LuxEngine/sessions/a7b976e8-d569-4fb6-889e-44131bcfcc6a Co-authored-by: sheazywi <73042839+sheazywi@users.noreply.github.com> --- Core/Source/Lux/Asset/MaterialSerializer.cpp | 5 +++++ Core/Source/Lux/Asset/MeshSerializer.cpp | 10 ++++++++++ Core/Source/Lux/Asset/RuntimeAssetManager.cpp | 6 ++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Core/Source/Lux/Asset/MaterialSerializer.cpp b/Core/Source/Lux/Asset/MaterialSerializer.cpp index e7fd41c..18b5af4 100644 --- a/Core/Source/Lux/Asset/MaterialSerializer.cpp +++ b/Core/Source/Lux/Asset/MaterialSerializer.cpp @@ -42,6 +42,11 @@ namespace Lux out << YAML::EndMap; std::ofstream fout(filepath); + if (!fout.is_open()) + { + LUX_CORE_ERROR("MaterialSerializer: Could not open '{}' for writing", filepath.string()); + return; + } fout << out.c_str(); } diff --git a/Core/Source/Lux/Asset/MeshSerializer.cpp b/Core/Source/Lux/Asset/MeshSerializer.cpp index a735a15..78928be 100644 --- a/Core/Source/Lux/Asset/MeshSerializer.cpp +++ b/Core/Source/Lux/Asset/MeshSerializer.cpp @@ -56,6 +56,11 @@ namespace Lux out << YAML::EndMap; std::ofstream fout(filepath); + if (!fout.is_open()) + { + LUX_CORE_ERROR("MeshSerializer: Could not open '{}' for writing", filepath.string()); + return; + } fout << out.c_str(); } @@ -119,6 +124,11 @@ namespace Lux out << YAML::EndMap; std::ofstream fout(filepath); + if (!fout.is_open()) + { + LUX_CORE_ERROR("StaticMeshSerializer: Could not open '{}' for writing", filepath.string()); + return; + } fout << out.c_str(); } diff --git a/Core/Source/Lux/Asset/RuntimeAssetManager.cpp b/Core/Source/Lux/Asset/RuntimeAssetManager.cpp index 081dede..cab869f 100644 --- a/Core/Source/Lux/Asset/RuntimeAssetManager.cpp +++ b/Core/Source/Lux/Asset/RuntimeAssetManager.cpp @@ -23,7 +23,8 @@ namespace Lux { // runtime because assets are expected to be pre-loaded asynchronously. LUX_CORE_WARN("RuntimeAssetManager::GetAsset – synchronous load for handle {}", (uint64_t)handle); - // Trigger an async load and wait for it (simple spin). + // Trigger an async load and sleep briefly between sync polls to avoid + // burning CPU cycles while waiting for the worker thread. std::atomic_bool done{ false }; LoadAssetAsync(handle, [&done](AssetHandle, Ref) { @@ -33,7 +34,8 @@ namespace Lux { while (!done) { SyncLoadedAssets(); - std::this_thread::yield(); + using namespace std::chrono_literals; + std::this_thread::sleep_for(1ms); } return IsAssetLoaded(handle) ? m_LoadedAssets.at(handle) : nullptr; From 6c29d95ceb96cbf2405cff7b3a16d75e1a4bd08c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:32:07 +0000 Subject: [PATCH 4/6] Fix MSVC compile errors in AssimpMeshImporter and RuntimeAssetSystem Agent-Logs-Url: https://github.com/starbounded-dev/LuxEngine/sessions/f5a1a4f3-d2fc-48ee-ade2-063e1dcfae2a Co-authored-by: sheazywi <73042839+sheazywi@users.noreply.github.com> --- Core/Source/Lux/Asset/AssimpMeshImporter.cpp | 7 +++---- Core/Source/Lux/Asset/AssimpMeshImporter.h | 3 +++ Core/Source/Lux/Asset/RuntimeAssetSystem.cpp | 14 +++++++++----- Core/Source/Lux/Asset/RuntimeAssetSystem.h | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Core/Source/Lux/Asset/AssimpMeshImporter.cpp b/Core/Source/Lux/Asset/AssimpMeshImporter.cpp index 7118f67..22fbc51 100644 --- a/Core/Source/Lux/Asset/AssimpMeshImporter.cpp +++ b/Core/Source/Lux/Asset/AssimpMeshImporter.cpp @@ -31,11 +31,10 @@ namespace Lux { } - static void TraverseNodes(Ref meshSource, + void AssimpMeshImporter::TraverseNodes(Ref meshSource, aiNode* node, const glm::mat4& parentTransform, - uint32_t parentIndex, - uint32_t level = 0) + uint32_t parentIndex) { glm::mat4 localTransform = AssimpMat4ToGlm(node->mTransformation); glm::mat4 worldTransform = parentTransform * localTransform; @@ -63,7 +62,7 @@ namespace Lux } for (uint32_t i = 0; i < node->mNumChildren; i++) - TraverseNodes(meshSource, node->mChildren[i], worldTransform, nodeIndex, level + 1); + TraverseNodes(meshSource, node->mChildren[i], worldTransform, nodeIndex); } Ref AssimpMeshImporter::ImportToMeshSource() diff --git a/Core/Source/Lux/Asset/AssimpMeshImporter.h b/Core/Source/Lux/Asset/AssimpMeshImporter.h index 9f83175..7b2b55b 100644 --- a/Core/Source/Lux/Asset/AssimpMeshImporter.h +++ b/Core/Source/Lux/Asset/AssimpMeshImporter.h @@ -3,6 +3,7 @@ #include "Lux/Core/Base.h" #include "Lux/Renderer/Mesh.h" +#include #include namespace Lux @@ -18,6 +19,8 @@ namespace Lux Ref ImportToMeshSource(); private: + void TraverseNodes(Ref meshSource, aiNode* node, const glm::mat4& parentTransform, uint32_t parentIndex); + std::filesystem::path m_Path; }; } diff --git a/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp b/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp index 73f71f3..10eef98 100644 --- a/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp +++ b/Core/Source/Lux/Asset/RuntimeAssetSystem.cpp @@ -30,11 +30,11 @@ namespace Lux while (!m_FinishedQueue.empty()) { auto& entry = m_FinishedQueue.front(); - if (entry.Asset) + if (entry.LoadedAsset) { - loadedAssets[entry.Handle] = entry.Asset; - if (entry.Callback) - entry.Callback(entry.Handle, entry.Asset); + loadedAssets[entry.Handle] = entry.LoadedAsset; + if (entry.CallbackFn != nullptr) + entry.CallbackFn(entry.Handle, entry.LoadedAsset); } m_FinishedQueue.pop(); } @@ -78,7 +78,11 @@ namespace Lux { std::scoped_lock lock(m_FinishedQueueMutex); - m_FinishedQueue.push({ request.Handle, asset, std::move(request.Callback) }); + LoadedEntry entry; + entry.Handle = request.Handle; + entry.LoadedAsset = std::move(asset); + entry.CallbackFn = std::move(request.Callback); + m_FinishedQueue.push(std::move(entry)); } } } diff --git a/Core/Source/Lux/Asset/RuntimeAssetSystem.h b/Core/Source/Lux/Asset/RuntimeAssetSystem.h index e006810..3b34a3f 100644 --- a/Core/Source/Lux/Asset/RuntimeAssetSystem.h +++ b/Core/Source/Lux/Asset/RuntimeAssetSystem.h @@ -63,9 +63,9 @@ namespace Lux struct LoadedEntry { - AssetHandle Handle; - Ref Asset; - std::function)> Callback; + AssetHandle Handle = 0; + Ref LoadedAsset; + std::function)> CallbackFn; }; std::queue m_FinishedQueue; std::mutex m_FinishedQueueMutex; From 3cd56153661b84bcb05a00f93fca03649d89789a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 05:35:32 +0000 Subject: [PATCH 5/6] Address review nits for UUID comment and invalid parent constant Agent-Logs-Url: https://github.com/starbounded-dev/LuxEngine/sessions/f5a1a4f3-d2fc-48ee-ade2-063e1dcfae2a Co-authored-by: sheazywi <73042839+sheazywi@users.noreply.github.com> --- Core/Source/Lux/Asset/AssimpMeshImporter.cpp | 6 ++++-- Core/Source/Lux/Asset/EditorAssetManager.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Core/Source/Lux/Asset/AssimpMeshImporter.cpp b/Core/Source/Lux/Asset/AssimpMeshImporter.cpp index 22fbc51..f50324f 100644 --- a/Core/Source/Lux/Asset/AssimpMeshImporter.cpp +++ b/Core/Source/Lux/Asset/AssimpMeshImporter.cpp @@ -16,6 +16,8 @@ namespace Lux { + static constexpr uint32_t s_InvalidParentIndex = 0xffffffffu; + static glm::mat4 AssimpMat4ToGlm(const aiMatrix4x4& m) { glm::mat4 result; @@ -47,7 +49,7 @@ namespace Lux uint32_t nodeIndex = (uint32_t)meshSource->m_Nodes.size(); meshSource->m_Nodes.push_back(luxNode); - if (parentIndex != 0xffffffff) + if (parentIndex != s_InvalidParentIndex) meshSource->m_Nodes[parentIndex].Children.push_back(nodeIndex); auto& currentNode = meshSource->m_Nodes[nodeIndex]; @@ -172,7 +174,7 @@ namespace Lux // ── Node hierarchy ──────────────────────────────────────────────────── // Insert sentinel root so every real node has a valid parentIndex meshSource->m_Nodes.emplace_back(); // root placeholder - TraverseNodes(meshSource, scene->mRootNode, glm::mat4(1.0f), 0xffffffff); + TraverseNodes(meshSource, scene->mRootNode, glm::mat4(1.0f), s_InvalidParentIndex); // ── Materials (allocate zero-material placeholders) ─────────────────── meshSource->m_Materials.resize(scene->mNumMaterials, 0); diff --git a/Core/Source/Lux/Asset/EditorAssetManager.cpp b/Core/Source/Lux/Asset/EditorAssetManager.cpp index 870ba30..7317ac3 100644 --- a/Core/Source/Lux/Asset/EditorAssetManager.cpp +++ b/Core/Source/Lux/Asset/EditorAssetManager.cpp @@ -113,7 +113,7 @@ namespace Lux { void EditorAssetManager::ImportScriptAsset(const std::filesystem::path& filepath, uint64_t uuid) { - AssetHandle handle = uuid; // use provided uuid as handle + AssetHandle handle = uuid; // use provided UUID as handle AssetMetadata metadata; metadata.Handle = handle; metadata.FilePath = filepath; From a76713bc972d0be5061344da13a70ba90ed32914 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:38:57 +0000 Subject: [PATCH 6/6] Fix MSVC compile errors in RuntimeAssetSystem and BeginPropertyGrid call sites Agent-Logs-Url: https://github.com/starbounded-dev/LuxEngine/sessions/7f23d118-a33a-442f-ac70-7510150891d5 Co-authored-by: sheazywi <73042839+sheazywi@users.noreply.github.com> --- Core/Source/Lux/Asset/RuntimeAssetSystem.h | 6 ++++-- Editor/Source/Panels/ConsolePanel.cpp | 16 +++++++--------- Editor/Source/Panels/ContentBrowserPanel.cpp | 10 ++++------ Editor/Source/Panels/SceneRendererPanel.cpp | 12 +++++------- Editor/Source/Panels/TextEditorPanel.cpp | 10 ++++------ 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/Core/Source/Lux/Asset/RuntimeAssetSystem.h b/Core/Source/Lux/Asset/RuntimeAssetSystem.h index 3b34a3f..f877e56 100644 --- a/Core/Source/Lux/Asset/RuntimeAssetSystem.h +++ b/Core/Source/Lux/Asset/RuntimeAssetSystem.h @@ -13,6 +13,8 @@ namespace Lux { + using RuntimeAssetCallback = std::function)>; + // Request submitted to RuntimeAssetSystem for an async load. struct RuntimeAssetLoadRequest { @@ -21,7 +23,7 @@ namespace Lux // Callback invoked on the main thread (via SyncLoadedAssets) once the // asset has been loaded. May be nullptr. - std::function)> Callback; + RuntimeAssetCallback Callback; }; // Background loading thread for runtime (shipped game) use. @@ -65,7 +67,7 @@ namespace Lux { AssetHandle Handle = 0; Ref LoadedAsset; - std::function)> CallbackFn; + RuntimeAssetCallback CallbackFn; }; std::queue m_FinishedQueue; std::mutex m_FinishedQueueMutex; diff --git a/Editor/Source/Panels/ConsolePanel.cpp b/Editor/Source/Panels/ConsolePanel.cpp index 6e11aef..c37305a 100644 --- a/Editor/Source/Panels/ConsolePanel.cpp +++ b/Editor/Source/Panels/ConsolePanel.cpp @@ -52,15 +52,13 @@ namespace Lux { if (ImGui::Button("Clear")) Clear(); - if (ImGuiEx::BeginPropertyGrid()) - { - ImGuiEx::Property("Auto-scroll", m_AutoScroll); - ImGuiEx::Property("Trace", m_ShowTrace); - ImGuiEx::Property("Info", m_ShowInfo); - ImGuiEx::Property("Warn", m_ShowWarn); - ImGuiEx::Property("Error", m_ShowError); - ImGuiEx::EndPropertyGrid(); - } + ImGuiEx::BeginPropertyGrid(); + ImGuiEx::Property("Auto-scroll", m_AutoScroll); + ImGuiEx::Property("Trace", m_ShowTrace); + ImGuiEx::Property("Info", m_ShowInfo); + ImGuiEx::Property("Warn", m_ShowWarn); + ImGuiEx::Property("Error", m_ShowError); + ImGuiEx::EndPropertyGrid(); ImGui::Separator(); // Filter input diff --git a/Editor/Source/Panels/ContentBrowserPanel.cpp b/Editor/Source/Panels/ContentBrowserPanel.cpp index 22fff34..d9e8ed4 100644 --- a/Editor/Source/Panels/ContentBrowserPanel.cpp +++ b/Editor/Source/Panels/ContentBrowserPanel.cpp @@ -259,12 +259,10 @@ namespace Lux { ImGui::Columns(1); - if (ImGuiEx::BeginPropertyGrid()) - { - ImGuiEx::PropertySlider("Thumbnail Size", thumbnailSize, 16.0f, 512.0f); - ImGuiEx::PropertySlider("Padding", padding, 0.0f, 32.0f); - ImGuiEx::EndPropertyGrid(); - } + ImGuiEx::BeginPropertyGrid(); + ImGuiEx::PropertySlider("Thumbnail Size", thumbnailSize, 16.0f, 512.0f); + ImGuiEx::PropertySlider("Padding", padding, 0.0f, 32.0f); + ImGuiEx::EndPropertyGrid(); // TODO: status bar ImGui::End(); diff --git a/Editor/Source/Panels/SceneRendererPanel.cpp b/Editor/Source/Panels/SceneRendererPanel.cpp index c2d4a64..975f7e4 100644 --- a/Editor/Source/Panels/SceneRendererPanel.cpp +++ b/Editor/Source/Panels/SceneRendererPanel.cpp @@ -27,13 +27,11 @@ namespace Lux { ImGui::Text("Ready: %s", m_Context->IsReady() ? "Yes" : "No"); ImGui::Text("Viewport: %u x %u", m_Context->GetViewportWidth(), m_Context->GetViewportHeight()); - if (ImGuiEx::BeginPropertyGrid()) - { - ImGuiEx::Property("Show Grid", options.ShowGrid); - ImGuiEx::Property("Show Selected In Wireframe", options.ShowSelectedInWireframe); - ImGuiEx::Property("Show Physics Colliders", options.ShowPhysicsColliders); - ImGuiEx::EndPropertyGrid(); - } + ImGuiEx::BeginPropertyGrid(); + ImGuiEx::Property("Show Grid", options.ShowGrid); + ImGuiEx::Property("Show Selected In Wireframe", options.ShowSelectedInWireframe); + ImGuiEx::Property("Show Physics Colliders", options.ShowPhysicsColliders); + ImGuiEx::EndPropertyGrid(); ImGui::Separator(); ImGui::Text("Draw Calls: %u", stats.DrawCalls); diff --git a/Editor/Source/Panels/TextEditorPanel.cpp b/Editor/Source/Panels/TextEditorPanel.cpp index 820aeb7..a5fba6f 100644 --- a/Editor/Source/Panels/TextEditorPanel.cpp +++ b/Editor/Source/Panels/TextEditorPanel.cpp @@ -163,14 +163,12 @@ namespace Lux } else { - if (ImGuiEx::BeginPropertyGrid()) + ImGuiEx::BeginPropertyGrid(); + if (ImGuiEx::Property("Side By Side", m_DiffSideBySide)) { - if (ImGuiEx::Property("Side By Side", m_DiffSideBySide)) - { - m_DiffEditor.SetSideBySideMode(m_DiffSideBySide); - } - ImGuiEx::EndPropertyGrid(); + m_DiffEditor.SetSideBySideMode(m_DiffSideBySide); } + ImGuiEx::EndPropertyGrid(); } ImGui::Separator();