diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f59997..012a102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -668,6 +668,21 @@ add_library(MetaCoreEditor STATIC ${METACORE_EDITOR_GENERATED_SOURCE} ) +find_program(METACORE_BASISU_EXECUTABLE + NAMES basisu + HINTS + "${CMAKE_SOURCE_DIR}/vcpkg_installed/${VCPKG_TARGET_TRIPLET}/tools/basisu" + "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/basisu" + NO_DEFAULT_PATH +) +if(METACORE_BASISU_EXECUTABLE) + target_compile_definitions(MetaCoreEditor PRIVATE + METACORE_BASISU_EXECUTABLE="${METACORE_BASISU_EXECUTABLE}" + ) +else() + message(FATAL_ERROR "Basis Universal encoder is required for the production texture pipeline") +endif() + target_include_directories(MetaCoreEditor PUBLIC Source/MetaCoreEditor/Public @@ -812,6 +827,14 @@ if(METACORE_BUILD_TESTS) add_test(NAME MetaCoreSmokeTests COMMAND MetaCoreSmokeTests) + add_executable(MetaCoreAssetPipelineTests + tests/MetaCoreAssetPipelineTests.cpp + Source/MetaCoreRender/Private/MetaCoreGlibcCompat.cpp + ) + target_link_libraries(MetaCoreAssetPipelineTests PRIVATE MetaCoreEditor) + target_compile_options(MetaCoreAssetPipelineTests PRIVATE ${METACORE_COMMON_WARNINGS}) + add_test(NAME MetaCoreAssetPipelineTests COMMAND MetaCoreAssetPipelineTests) + # Filament + ImGui 离屏渲染 Demo add_executable(FilamentImGuiDemo tests/FilamentImGuiDemo.cpp @@ -823,7 +846,6 @@ if(METACORE_BUILD_TESTS) imgui::imgui ) target_compile_options(FilamentImGuiDemo PRIVATE ${METACORE_COMMON_WARNINGS}) - target_compile_definitions(FilamentImGuiDemo PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) metacore_use_filament(FilamentImGuiDemo) metacore_stage_ui_blit_material(FilamentImGuiDemo) endif() diff --git a/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta index f8f9d13..95a5ce7 100644 --- a/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 1.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "512cb16539316502", "asset_type": "material", "guid": "fc3fd465-4f39-44af-907a-4fc0494c2502", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 1.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 4176055334418841915, "source_mtime_ns": 1781677768840654120, "source_path": "Assets/Materials/Material 1.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material 2.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 2.mcmaterial.json.mcmeta index b11f53e..517136f 100644 --- a/SandboxProject/Assets/Materials/Material 2.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 2.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "2b00372114e7b956", "asset_type": "material", "guid": "579d1aa2-baae-4b56-9b05-86f9d2e9a2ff", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 2.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 12206391660553642631, "source_mtime_ns": 1781573175016906643, "source_path": "Assets/Materials/Material 2.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material 3.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 3.mcmaterial.json.mcmeta index cb22987..7d12985 100644 --- a/SandboxProject/Assets/Materials/Material 3.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 3.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "f433edac057bcf58", "asset_type": "material", "guid": "fbe8b648-5368-4af6-b822-d2573ed5f194", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 3.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 7449433069631146521, "source_mtime_ns": 1781574603045608609, "source_path": "Assets/Materials/Material 3.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material 4.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 4.mcmaterial.json.mcmeta index 7d90701..5522fce 100644 --- a/SandboxProject/Assets/Materials/Material 4.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 4.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "96180e85664b5eb3", "asset_type": "material", "guid": "a23b9298-b98c-47ac-a2ad-3c9dac65c5af", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 4.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 12594992031322235982, "source_mtime_ns": 1781574003827012347, "source_path": "Assets/Materials/Material 4.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material 5.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 5.mcmaterial.json.mcmeta index af188b9..5316072 100644 --- a/SandboxProject/Assets/Materials/Material 5.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 5.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "47baddd806a5bdb0", "asset_type": "material", "guid": "39f65f8e-cdac-43fb-9762-e6c544547a71", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 5.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 10101379247114018828, "source_mtime_ns": 1781574163019728677, "source_path": "Assets/Materials/Material 5.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material 6.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 6.mcmaterial.json.mcmeta index 09a4e23..25fa479 100644 --- a/SandboxProject/Assets/Materials/Material 6.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 6.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "64a86163a37fabf6", "asset_type": "material", "guid": "3bd687c0-2570-4236-bd65-0f774b13d34f", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 6.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 2546970018452484703, "source_mtime_ns": 1781575611817940101, "source_path": "Assets/Materials/Material 6.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta index e7104f4..54b5e44 100644 --- a/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material 7.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "bee3ae76701cfe52", "asset_type": "material", "guid": "b13c413e-0350-4152-9f1e-41552f9823af", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material 7.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 12622570808854545607, "source_mtime_ns": 1781678340396816708, "source_path": "Assets/Materials/Material 7.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/Material.mcmaterial.json.mcmeta b/SandboxProject/Assets/Materials/Material.mcmaterial.json.mcmeta index 8b812f7..42ebd8d 100644 --- a/SandboxProject/Assets/Materials/Material.mcmaterial.json.mcmeta +++ b/SandboxProject/Assets/Materials/Material.mcmaterial.json.mcmeta @@ -1,8 +1,19 @@ { + "artifact_key": "cd72b5445ae09699", "asset_type": "material", "guid": "5edbdbc8-4a0d-4991-82a7-782de9ed4e3a", + "import_settings": { + "generated_material_policy": 1, + "preserve_alpha_mode": true, + "preserve_double_sided": true, + "shader_model": 0 + }, "importer_id": "MaterialImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/Material.mcmaterial.json", + "schema_version": 2, + "settings_hash": 6867598416662597650, "source_hash": 7326866736359587430, "source_mtime_ns": 1781572763657368309, "source_path": "Assets/Materials/Material.mcmaterial.json", diff --git a/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset b/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset index 1e5ad89..c4228ec 100644 Binary files a/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset and b/SandboxProject/Assets/Materials/default_pbr.material.json.mcasset differ diff --git a/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta b/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta index 1f5b01f..8c6c67e 100644 --- a/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta +++ b/SandboxProject/Assets/Materials/default_pbr.material.json.mcmeta @@ -1,8 +1,14 @@ { + "artifact_key": "1b8eb8422ae8a643", "asset_type": "asset", "guid": "450d9089-4ca1-4327-96c4-39046198b1be", + "import_settings": {}, "importer_id": "BinaryImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/default_pbr.material.json.mcasset", + "schema_version": 2, + "settings_hash": 11238273277574643119, "source_hash": 12650993429013095480, "source_mtime_ns": 1779698309758483890, "source_path": "Assets/Materials/default_pbr.material.json", diff --git a/SandboxProject/Assets/Models/111.glb.mcasset b/SandboxProject/Assets/Models/111.glb.mcasset index aef45e5..7396828 100644 Binary files a/SandboxProject/Assets/Models/111.glb.mcasset and b/SandboxProject/Assets/Models/111.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/111.glb.mcmeta b/SandboxProject/Assets/Models/111.glb.mcmeta index 458b850..748f231 100644 --- a/SandboxProject/Assets/Models/111.glb.mcmeta +++ b/SandboxProject/Assets/Models/111.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "1a085d56a09aac10", "asset_type": "model", "dependencies": [ "86c72638-58f3-95a7-c6a5-d6661e2df677", @@ -2099,8 +2100,26 @@ "ca00a727-1fe3-590a-5bc1-79ddec284950" ], "guid": "4073ff9f-de3a-4f85-b502-ca6ed212c5d2", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_messages": [ + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + } + ], + "last_import_result": 0, "package_path": "Assets/Models/111.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 17894180331051193223, "source_mtime_ns": 1781575411600895336, "source_path": "Assets/Models/111.glb", diff --git a/SandboxProject/Assets/Models/Angry.glb.mcasset b/SandboxProject/Assets/Models/Angry.glb.mcasset index 0dcf4a8..e4fd06b 100644 Binary files a/SandboxProject/Assets/Models/Angry.glb.mcasset and b/SandboxProject/Assets/Models/Angry.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/Angry.glb.mcmeta b/SandboxProject/Assets/Models/Angry.glb.mcmeta index a4bd249..cfc2e1f 100644 --- a/SandboxProject/Assets/Models/Angry.glb.mcmeta +++ b/SandboxProject/Assets/Models/Angry.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "af6b0ea09dddb0a4", "asset_type": "model", "dependencies": [ "e863fe4d-79a8-84ae-3102-b357ad7b4823", @@ -16,8 +17,19 @@ "4881b914-e23c-4c2c-7900-7aeefdef6ff3" ], "guid": "4c35d56e-2bba-4d35-8676-5030e502368a", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Models/Angry.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 4454436885812718669, "source_mtime_ns": 1782811201861152889, "source_path": "Assets/Models/Angry.glb", diff --git a/SandboxProject/Assets/Models/Cube.glb.mcasset b/SandboxProject/Assets/Models/Cube.glb.mcasset index a8d84fd..39e288f 100644 Binary files a/SandboxProject/Assets/Models/Cube.glb.mcasset and b/SandboxProject/Assets/Models/Cube.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/Cube.glb.mcmeta b/SandboxProject/Assets/Models/Cube.glb.mcmeta index 7d0deea..6e65ea4 100644 --- a/SandboxProject/Assets/Models/Cube.glb.mcmeta +++ b/SandboxProject/Assets/Models/Cube.glb.mcmeta @@ -1,8 +1,31 @@ { + "artifact_key": "ba190289025adbd3", "asset_type": "model", + "dependencies": [ + "9cd50d41-df30-26ff-da7e-e01bb57f7135", + "d0a9dfe6-52cb-c306-8384-085ea2996675" + ], "guid": "78fa65d0-3631-4cfc-973d-a4af2962c004", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_messages": [ + { + "code": "ambiguous_subasset_identity", + "message": "material 子资产名称为空或重复,GUID 稳定性退回序号匹配", + "severity": 1 + } + ], + "last_import_result": 1, "package_path": "Assets/Models/Cube.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 2668036311376285448, "source_mtime_ns": 1779698309758483890, "source_path": "Assets/Models/Cube.glb", @@ -12,14 +35,14 @@ "guid": "9cd50d41-df30-26ff-da7e-e01bb57f7135", "index": 0, "name": "Cube", - "stable_import_key": "", + "stable_import_key": "mesh:Cube:0", "type": "mesh" }, { - "guid": "20a8eca7-ae5b-8ed9-c7a8-b64b59567283", + "guid": "d0a9dfe6-52cb-c306-8384-085ea2996675", "index": 0, - "name": "Cube_Material", - "stable_import_key": "", + "name": "Material", + "stable_import_key": "material:Material:0", "type": "material" } ] diff --git a/SandboxProject/Assets/Models/Plane.glb.mcasset b/SandboxProject/Assets/Models/Plane.glb.mcasset index 8299c49..38e0e07 100644 Binary files a/SandboxProject/Assets/Models/Plane.glb.mcasset and b/SandboxProject/Assets/Models/Plane.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/Plane.glb.mcmeta b/SandboxProject/Assets/Models/Plane.glb.mcmeta index 9399db4..2006032 100644 --- a/SandboxProject/Assets/Models/Plane.glb.mcmeta +++ b/SandboxProject/Assets/Models/Plane.glb.mcmeta @@ -1,11 +1,23 @@ { + "artifact_key": "0f17577685a7ac42", "asset_type": "model", "dependencies": [ "a718a4d3-f5ce-7f3a-6fe6-877661f2ea55" ], "guid": "b9ff132c-4c3e-4d51-bf21-eb810482ec51", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Models/Plane.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 7189019093832532692, "source_mtime_ns": 1779698309758483890, "source_path": "Assets/Models/Plane.glb", diff --git a/SandboxProject/Assets/Models/box1.glb.mcasset b/SandboxProject/Assets/Models/box1.glb.mcasset index 744758c..c5e0c27 100644 Binary files a/SandboxProject/Assets/Models/box1.glb.mcasset and b/SandboxProject/Assets/Models/box1.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/box1.glb.mcmeta b/SandboxProject/Assets/Models/box1.glb.mcmeta index d08f8e0..36cde05 100644 --- a/SandboxProject/Assets/Models/box1.glb.mcmeta +++ b/SandboxProject/Assets/Models/box1.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "c45ac9bbe6143264", "asset_type": "model", "dependencies": [ "7998c3fb-b5f3-3fcf-444b-c84c7b86f10b", @@ -12,8 +13,19 @@ "c1db65ae-1bb6-5c96-ceed-2062cf899aeb" ], "guid": "7033a4a0-8f64-4826-8fec-22ce0fccb514", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Models/box1.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 17764949482612063346, "source_mtime_ns": 1779698309758483890, "source_path": "Assets/Models/box1.glb", diff --git a/SandboxProject/Assets/Models/defeat.glb.mcasset b/SandboxProject/Assets/Models/defeat.glb.mcasset index ed41722..0fbaa70 100644 Binary files a/SandboxProject/Assets/Models/defeat.glb.mcasset and b/SandboxProject/Assets/Models/defeat.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/defeat.glb.mcmeta b/SandboxProject/Assets/Models/defeat.glb.mcmeta index 2551b5a..9af917c 100644 --- a/SandboxProject/Assets/Models/defeat.glb.mcmeta +++ b/SandboxProject/Assets/Models/defeat.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "09c03f6703753bc7", "asset_type": "model", "dependencies": [ "691e9f45-775d-4beb-d02e-fbc5e69de025", @@ -9,8 +10,19 @@ "e1aa9ec5-14d2-8cff-369b-d9250c97951b" ], "guid": "eb4eb535-69de-4292-bf7a-b16ca66e39f1", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Models/defeat.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 11308311888863755122, "source_mtime_ns": 1782811597595352702, "source_path": "Assets/Models/defeat.glb", diff --git a/SandboxProject/Assets/Models/jxb.glb.mcasset b/SandboxProject/Assets/Models/jxb.glb.mcasset index a5d63f9..5b6d595 100644 Binary files a/SandboxProject/Assets/Models/jxb.glb.mcasset and b/SandboxProject/Assets/Models/jxb.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/jxb.glb.mcmeta b/SandboxProject/Assets/Models/jxb.glb.mcmeta index fffa355..1bcd2f2 100644 --- a/SandboxProject/Assets/Models/jxb.glb.mcmeta +++ b/SandboxProject/Assets/Models/jxb.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "cbb2e35742a787c8", "asset_type": "model", "dependencies": [ "355dbe1a-2ddd-f8d3-59ee-c527d9d0cf27", @@ -22,8 +23,19 @@ "6b1cedd6-4af2-ba3f-d22c-da6ee0281680" ], "guid": "35e7fee9-a67f-4a7f-ad9f-6479117b08b5", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Models/jxb.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 3452613311117949482, "source_mtime_ns": 1782811325693939312, "source_path": "Assets/Models/jxb.glb", diff --git a/SandboxProject/Assets/Models/jyc.glb.mcasset b/SandboxProject/Assets/Models/jyc.glb.mcasset index fbd7704..e5da293 100644 Binary files a/SandboxProject/Assets/Models/jyc.glb.mcasset and b/SandboxProject/Assets/Models/jyc.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/jyc.glb.mcmeta b/SandboxProject/Assets/Models/jyc.glb.mcmeta index 9128f80..78a4de2 100644 --- a/SandboxProject/Assets/Models/jyc.glb.mcmeta +++ b/SandboxProject/Assets/Models/jyc.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "e88aac3be20414fb", "asset_type": "model", "dependencies": [ "0db6e6bf-9d16-0d1e-af49-3d0fe46ec755", @@ -20860,8 +20861,401 @@ "d2c7e0c0-78ac-2352-8132-7e6fc838560c" ], "guid": "fa8372eb-d4ea-4e8a-93ab-768df4f04c4a", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_messages": [ + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + } + ], + "last_import_result": 0, "package_path": "Assets/Models/jyc.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 6864693034290467672, "source_mtime_ns": 1779698310946568594, "source_path": "Assets/Models/jyc.glb", diff --git a/SandboxProject/Assets/Models/超精密磨床.glb.mcasset b/SandboxProject/Assets/Models/超精密磨床.glb.mcasset index f58f492..18f6bda 100644 Binary files a/SandboxProject/Assets/Models/超精密磨床.glb.mcasset and b/SandboxProject/Assets/Models/超精密磨床.glb.mcasset differ diff --git a/SandboxProject/Assets/Models/超精密磨床.glb.mcmeta b/SandboxProject/Assets/Models/超精密磨床.glb.mcmeta index b53fa96..8ea2c2f 100644 --- a/SandboxProject/Assets/Models/超精密磨床.glb.mcmeta +++ b/SandboxProject/Assets/Models/超精密磨床.glb.mcmeta @@ -1,4 +1,5 @@ { + "artifact_key": "f4a53bf7c25ed1f8", "asset_type": "model", "dependencies": [ "f18830da-5294-9e8e-aaa5-d3537fd1d002", @@ -45,8 +46,156 @@ "474bab4f-e853-88a4-3738-590a98c9b0a8" ], "guid": "e9fd854e-2d0a-43b6-a73e-2caa1fe61f4e", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_messages": [ + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + }, + { + "code": "default_texture_binding", + "message": "材质缺少贴图引用时,已回退到首个可用贴图资源", + "severity": 0 + } + ], + "last_import_result": 0, "package_path": "Assets/Models/超精密磨床.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 4002013065180687563, "source_mtime_ns": 1781595964581304216, "source_path": "Assets/Models/超精密磨床.glb", diff --git a/SandboxProject/Assets/UI/main_menu.rcss.mcasset b/SandboxProject/Assets/UI/main_menu.rcss.mcasset index 905eddc..85ae077 100644 Binary files a/SandboxProject/Assets/UI/main_menu.rcss.mcasset and b/SandboxProject/Assets/UI/main_menu.rcss.mcasset differ diff --git a/SandboxProject/Assets/UI/main_menu.rcss.mcmeta b/SandboxProject/Assets/UI/main_menu.rcss.mcmeta index e7a3965..ca2b599 100644 --- a/SandboxProject/Assets/UI/main_menu.rcss.mcmeta +++ b/SandboxProject/Assets/UI/main_menu.rcss.mcmeta @@ -1,8 +1,14 @@ { + "artifact_key": "877767f4951cc2ab", "asset_type": "ui_stylesheet", "guid": "e0348e2f-bb62-4a8c-bc28-13c8bccb5101", + "import_settings": {}, "importer_id": "UiStylesheetImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/UI/main_menu.rcss.mcasset", + "schema_version": 2, + "settings_hash": 11238273277574643119, "source_hash": 8787559501102875347, "source_mtime_ns": 1779698310955569237, "source_path": "Assets/UI/main_menu.rcss", diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcasset b/SandboxProject/Assets/UI/main_menu.rml.mcasset index 6bebaad..04f0cbf 100644 Binary files a/SandboxProject/Assets/UI/main_menu.rml.mcasset and b/SandboxProject/Assets/UI/main_menu.rml.mcasset differ diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcmeta b/SandboxProject/Assets/UI/main_menu.rml.mcmeta index 77dc056..53f6e5c 100644 --- a/SandboxProject/Assets/UI/main_menu.rml.mcmeta +++ b/SandboxProject/Assets/UI/main_menu.rml.mcmeta @@ -1,8 +1,14 @@ { + "artifact_key": "3d142bee5d424926", "asset_type": "ui_document", "guid": "6ca33ee5-9b9c-444e-933d-ee412ca1a42d", + "import_settings": {}, "importer_id": "UiDocumentImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/UI/main_menu.rml.mcasset", + "schema_version": 2, + "settings_hash": 11238273277574643119, "source_hash": 11300316873669048992, "source_mtime_ns": 1779698310955569237, "source_path": "Assets/UI/main_menu.rml", diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 09006b3..dfba09a 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -20,7 +20,7 @@ #include #include -#include "cgltf.h" +#include "MetaCoreLocalCgltf.h" #define GLM_ENABLE_EXPERIMENTAL #include #include @@ -35,12 +35,15 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -53,6 +56,11 @@ #include #include +#if !defined(_WIN32) +#include +#include +#endif + namespace MetaCore { namespace { @@ -80,6 +88,15 @@ void MetaCoreDebugLog(const char* category, const std::string& message) { std::strcmp(value, "TRUE") == 0; } +[[nodiscard]] bool MetaCoreSynchronousAssetBootstrapEnabled() { + const char* value = std::getenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP"); + return value != nullptr && ( + std::strcmp(value, "1") == 0 || + std::strcmp(value, "true") == 0 || + std::strcmp(value, "TRUE") == 0 + ); +} + void MetaCoreDebugMaterialLog(const std::string& message) { if (MetaCoreMaterialDebugLogEnabled()) { MetaCoreDebugLog("Material", message); @@ -220,6 +237,26 @@ bool MetaCoreWriteMetaJson( json["source_hash"] = metadata.SourceHash; json["source_size_bytes"] = metadata.SourceSizeBytes; json["source_mtime_ns"] = metadata.SourceLastWriteTime; + json["schema_version"] = metadata.SchemaVersion; + json["importer_version"] = metadata.ImporterVersion; + json["import_settings"] = nlohmann::json::parse(metadata.ImportSettingsJson, nullptr, false); + if (json["import_settings"].is_discarded()) { + json["import_settings"] = nlohmann::json::object(); + } + json["settings_hash"] = metadata.SettingsHash; + json["artifact_key"] = metadata.ArtifactKey; + json["last_import_result"] = static_cast(metadata.LastImportResult); + if (!metadata.LastImportMessages.empty()) { + auto messages = nlohmann::json::array(); + for (const auto& message : metadata.LastImportMessages) { + messages.push_back({ + {"severity", static_cast(message.Severity)}, + {"code", message.Code}, + {"message", message.Message} + }); + } + json["last_import_messages"] = std::move(messages); + } // 序列化子资产列表 if (!metadata.SubAssets.empty()) { @@ -287,6 +324,29 @@ bool MetaCoreReadMetaJson( if (json.contains("source_mtime_ns") && json["source_mtime_ns"].is_number_integer()) { outMetadata.SourceLastWriteTime = json["source_mtime_ns"].get(); } + outMetadata.SchemaVersion = json.value("schema_version", 1U); + outMetadata.ImporterVersion = json.value("importer_version", 1U); + if (json.contains("import_settings") && json["import_settings"].is_object()) { + outMetadata.ImportSettingsJson = json["import_settings"].dump(); + } + outMetadata.SettingsHash = json.value("settings_hash", std::uint64_t{0}); + outMetadata.ArtifactKey = json.value("artifact_key", std::string{}); + outMetadata.LastImportResult = static_cast( + json.value("last_import_result", static_cast(MetaCoreModelImportResult::Success)) + ); + outMetadata.LastImportMessages.clear(); + if (json.contains("last_import_messages") && json["last_import_messages"].is_array()) { + for (const auto& value : json["last_import_messages"]) { + if (!value.is_object()) { + continue; + } + outMetadata.LastImportMessages.push_back(MetaCoreImportMessageDocument{ + static_cast(value.value("severity", 0)), + value.value("code", std::string{}), + value.value("message", std::string{}) + }); + } + } // 解析子资产列表 outMetadata.SubAssets.clear(); @@ -324,6 +384,250 @@ bool MetaCoreReadMetaJson( } } +constexpr std::uint32_t GMetaCoreAssetMetadataSchemaVersion = 2; +constexpr std::uint32_t GMetaCoreBuiltinImporterVersion = 2; +constexpr std::uint32_t GMetaCoreAssetGeneratorVersion = 2; + +[[nodiscard]] std::string MetaCoreHostPlatformName() { +#if defined(_WIN32) + return "Windows-x86_64"; +#else + return "Linux-x86_64"; +#endif +} + +[[nodiscard]] std::string MetaCoreCanonicalModelSettingsJson( + const MetaCoreModelImportSettingsDocument& settings +) { + nlohmann::ordered_json json; + json["scale_factor"] = settings.ScaleFactor; + json["up_axis"] = static_cast(settings.UpAxis); + json["preserve_hierarchy"] = settings.PreserveHierarchy; + json["preserve_empty_nodes"] = settings.PreserveEmptyNodes; + json["preserve_material_slots"] = settings.PreserveMaterialSlots; + return json.dump(); +} + +[[nodiscard]] MetaCoreModelImportSettingsDocument MetaCoreParseModelSettingsJson(std::string_view text) { + MetaCoreModelImportSettingsDocument settings; + const auto json = nlohmann::json::parse(text, nullptr, false); + if (!json.is_object()) { + return settings; + } + settings.ScaleFactor = json.value("scale_factor", 1.0F); + settings.UpAxis = static_cast(json.value("up_axis", 0)); + settings.PreserveHierarchy = json.value("preserve_hierarchy", true); + settings.PreserveEmptyNodes = json.value("preserve_empty_nodes", true); + settings.PreserveMaterialSlots = json.value("preserve_material_slots", true); + return settings; +} + +[[nodiscard]] std::string MetaCoreCanonicalTextureSettingsJson( + const MetaCoreTextureImportSettingsDocument& settings +) { + nlohmann::ordered_json json; + json["color_space"] = static_cast(settings.ColorSpace); + json["usage"] = static_cast(settings.Usage); + json["generate_mipmaps"] = settings.GenerateMipmaps; + json["max_size"] = settings.MaxSize; + json["compression"] = static_cast(settings.Compression); + json["compression_quality"] = settings.CompressionQuality; + json["renormalize_normal_map"] = settings.RenormalizeNormalMap; + return json.dump(); +} + +[[nodiscard]] MetaCoreTextureImportSettingsDocument MetaCoreParseTextureSettingsJson(std::string_view text) { + MetaCoreTextureImportSettingsDocument settings; + const auto json = nlohmann::json::parse(text, nullptr, false); + if (!json.is_object()) { + return settings; + } + settings.ColorSpace = static_cast(json.value("color_space", 0)); + settings.Usage = static_cast(json.value("usage", 0)); + settings.GenerateMipmaps = json.value("generate_mipmaps", true); + settings.MaxSize = std::clamp(json.value("max_size", 4096), 1, 16384); + settings.Compression = static_cast(json.value("compression", 1)); + settings.CompressionQuality = std::clamp(json.value("compression_quality", 80), 0, 100); + settings.RenormalizeNormalMap = json.value("renormalize_normal_map", true); + return settings; +} + +[[nodiscard]] std::string MetaCoreCanonicalMaterialSettingsJson( + const MetaCoreMaterialImportSettingsDocument& settings +) { + nlohmann::ordered_json json; + json["shader_model"] = static_cast(settings.ShaderModel); + json["preserve_alpha_mode"] = settings.PreserveAlphaMode; + json["preserve_double_sided"] = settings.PreserveDoubleSided; + json["generated_material_policy"] = static_cast(settings.GeneratedMaterialPolicy); + return json.dump(); +} + +[[nodiscard]] std::string MetaCoreDefaultImportSettingsJson(std::string_view assetType) { + if (assetType == "model") { + return MetaCoreCanonicalModelSettingsJson(MetaCoreModelImportSettingsDocument{}); + } + if (assetType == "texture") { + return MetaCoreCanonicalTextureSettingsJson(MetaCoreTextureImportSettingsDocument{}); + } + if (assetType == "material") { + return MetaCoreCanonicalMaterialSettingsJson(MetaCoreMaterialImportSettingsDocument{}); + } + return "{}"; +} + +[[nodiscard]] std::string MetaCoreBuildArtifactKey( + std::uint64_t sourceHash, + std::string_view importerId, + std::uint32_t importerVersion, + std::uint64_t settingsHash, + std::string_view platform +) { + std::uint64_t hash = MetaCoreHashCombine(sourceHash, MetaCoreHashString(importerId)); + hash = MetaCoreHashCombine(hash, importerVersion); + hash = MetaCoreHashCombine(hash, settingsHash); + hash = MetaCoreHashCombine(hash, GMetaCoreAssetGeneratorVersion); + hash = MetaCoreHashCombine(hash, MetaCoreHashString(platform)); + std::ostringstream stream; + stream << std::hex << std::setfill('0') << std::setw(16) << hash; + return stream.str(); +} + +void MetaCoreFinalizeMetadataContract(MetaCoreAssetMetadataDocument& metadata) { + metadata.SchemaVersion = GMetaCoreAssetMetadataSchemaVersion; + metadata.ImporterVersion = GMetaCoreBuiltinImporterVersion; + if (metadata.ImportSettingsJson.empty()) { + metadata.ImportSettingsJson = MetaCoreDefaultImportSettingsJson(metadata.AssetType); + } + metadata.SettingsHash = MetaCoreHashString(metadata.ImportSettingsJson); + metadata.ArtifactKey = MetaCoreBuildArtifactKey( + metadata.SourceHash, + metadata.ImporterId, + metadata.ImporterVersion, + metadata.SettingsHash, + MetaCoreHostPlatformName() + ); +} + +[[nodiscard]] std::optional> MetaCoreReadBinaryFile( + const std::filesystem::path& path +) { + std::ifstream input(path, std::ios::binary | std::ios::ate); + if (!input.is_open()) { + return std::nullopt; + } + const auto size = input.tellg(); + if (size < 0) { + return std::nullopt; + } + std::vector bytes(static_cast(size)); + input.seekg(0); + input.read(reinterpret_cast(bytes.data()), size); + return input.good() || input.eof() ? std::optional>(std::move(bytes)) : std::nullopt; +} + +[[nodiscard]] bool MetaCoreWriteBinaryFileAtomically( + const std::filesystem::path& path, + std::span bytes +) { + std::error_code errorCode; + std::filesystem::create_directories(path.parent_path(), errorCode); + if (errorCode) { + return false; + } + const auto temporaryPath = std::filesystem::path(path.string() + ".importing"); + { + std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!output.good()) { + return false; + } + } + std::filesystem::rename(temporaryPath, path, errorCode); + if (errorCode) { + errorCode.clear(); + std::filesystem::remove(path, errorCode); + errorCode.clear(); + std::filesystem::rename(temporaryPath, path, errorCode); + } + return !errorCode; +} + +[[nodiscard]] std::optional MetaCoreRecoverGuidFromAssetIndex( + const std::filesystem::path& projectRoot, + const std::filesystem::path& relativePath, + std::uint64_t sourceHash +) { + try { + std::ifstream input(projectRoot / "Library" / "AssetIndex.json"); + nlohmann::json json; + input >> json; + if (!json.contains("assets") || !json["assets"].is_array()) { + return std::nullopt; + } + std::optional match; + for (const auto& value : json["assets"]) { + if (!value.is_object() || value.value("path", std::string{}) != relativePath.generic_string() || + value.value("source_hash", std::uint64_t{0}) != sourceHash) { + continue; + } + const auto guid = MetaCoreAssetGuid::Parse(value.value("guid", std::string{})); + if (!guid.has_value() || !guid->IsValid() || match.has_value()) { + return std::nullopt; + } + match = *guid; + } + return match; + } catch (...) { + return std::nullopt; + } +} + +void MetaCoreWriteAssetIndex( + const MetaCoreProjectDescriptor& project, + const std::vector& previousRecords, + const std::vector& currentRecords +) { + if (project.RootPath.empty()) { + return; + } + nlohmann::json index; + index["schema_version"] = 1; + index["assets"] = nlohmann::json::array(); + for (const auto& record : currentRecords) { + if (record.Guid.IsValid()) { + index["assets"].push_back({ + {"guid", record.Guid.ToString()}, + {"path", record.RelativePath.generic_string()}, + {"source_hash", record.SourceHash}, + {"artifact_key", record.ArtifactKey} + }); + } + } + index["tombstones"] = nlohmann::json::array(); + for (const auto& previous : previousRecords) { + const bool stillPresent = std::any_of(currentRecords.begin(), currentRecords.end(), [&](const auto& current) { + return current.Guid == previous.Guid; + }); + if (!stillPresent && previous.Guid.IsValid()) { + index["tombstones"].push_back({ + {"guid", previous.Guid.ToString()}, + {"path", previous.RelativePath.generic_string()}, + {"source_hash", previous.SourceHash} + }); + } + } + const auto path = project.RootPath / "Library" / "AssetIndex.json"; + const std::string serialized = index.dump(2); + (void)MetaCoreWriteBinaryFileAtomically( + path, + std::as_bytes(std::span(serialized.data(), serialized.size())) + ); +} + std::unordered_map& MetaCoreGetComponentInspectorEditSnapshots() { static std::unordered_map snapshots; return snapshots; @@ -4510,10 +4814,6 @@ bool MetaCoreDeserializeComponentValue( } [[nodiscard]] bool MetaCoreIsPackagePath(const std::filesystem::path& path) { - const std::string filename = path.filename().string(); - if (filename.find(".json") != std::string::npos) { - return false; - } const std::string extension = path.extension().string(); return extension == ".mcscene" || extension == ".mcprefab" || extension == ".mcasset" || extension == ".mcui"; } @@ -4641,7 +4941,9 @@ bool MetaCoreDeserializeComponentValue( const std::filesystem::path& absoluteSourcePath, const std::filesystem::path& relativeSourcePath, std::uint64_t sourceHash, - std::vector& outPayloadData + std::vector& outPayloadData, + MetaCoreTextureImportSettingsDocument importSettings = {}, + const std::atomic_bool* cancelRequested = nullptr ); [[nodiscard]] std::filesystem::path MetaCoreResolveExternalModelTexturePath( @@ -5011,21 +5313,47 @@ void MetaCoreApplyStableSubAssetGuids( MetaCoreModelAssetDocument& document ) { std::unordered_map previousGuids; + std::unordered_map uniquePreviousNames; + std::unordered_map previousNameCounts; for (const MetaCoreSubAssetMetadata& subAsset : previousSubAssets) { if (subAsset.Guid.IsValid() && !subAsset.StableImportKey.empty()) { previousGuids[subAsset.Type + "|" + subAsset.StableImportKey] = subAsset.Guid; } + const std::string nameKey = subAsset.Type + "|" + subAsset.Name; + ++previousNameCounts[nameKey]; + uniquePreviousNames[nameKey] = subAsset.Guid; } std::unordered_map remappedGuids; const auto reuseGuids = [&](auto& generatedAssets, std::string_view type) { + std::unordered_map currentNameCounts; + for (const auto& generatedAsset : generatedAssets) { + ++currentNameCounts[generatedAsset.Name]; + } for (auto& generatedAsset : generatedAssets) { const auto previous = previousGuids.find(std::string(type) + "|" + generatedAsset.StableImportKey); - if (previous == previousGuids.end() || previous->second == generatedAsset.AssetGuid) { + MetaCoreAssetGuid previousGuid{}; + if (previous != previousGuids.end()) { + previousGuid = previous->second; + } else { + const std::string nameKey = std::string(type) + "|" + generatedAsset.Name; + if (!generatedAsset.Name.empty() && currentNameCounts[generatedAsset.Name] == 1 && + previousNameCounts[nameKey] == 1) { + previousGuid = uniquePreviousNames[nameKey]; + } else if (!previousSubAssets.empty()) { + MetaCoreAddImportMessage( + document.ImportReport, + MetaCoreImportMessageSeverity::Warning, + "ambiguous_subasset_identity", + std::string(type) + " 子资产名称为空或重复,GUID 稳定性退回序号匹配" + ); + } + } + if (!previousGuid.IsValid() || previousGuid == generatedAsset.AssetGuid) { continue; } - remappedGuids[generatedAsset.AssetGuid] = previous->second; - generatedAsset.AssetGuid = previous->second; + remappedGuids[generatedAsset.AssetGuid] = previousGuid; + generatedAsset.AssetGuid = previousGuid; } }; @@ -5102,6 +5430,9 @@ void MetaCoreRebuildAssetDependencyGraph(const MetaCoreProjectDescriptor& projec if (!entry.is_regular_file() || !MetaCoreIsMetaPath(entry.path())) { continue; } + if (entry.path().stem().extension() == ".mcasset") { + continue; + } MetaCoreAssetMetadataDocument metadata; if (!MetaCoreReadMetaJson(entry.path(), metadata) || !metadata.AssetGuid.IsValid()) { @@ -5225,7 +5556,9 @@ void MetaCorePublishAssetRecordChanges( const std::filesystem::path& absoluteSourcePath, const std::filesystem::path& relativeSourcePath, std::uint64_t sourceHash, - std::vector& outPayloadData + std::vector& outPayloadData, + MetaCoreTextureImportSettingsDocument importSettings, + const std::atomic_bool* cancelRequested ) { (void)sourceHash; @@ -5244,8 +5577,54 @@ void MetaCorePublishAssetRecordChanges( return {}; } - // We forced 4 channels + // We forced 4 channels. Resize before compression so the derived payload is + // governed entirely by import settings rather than the source dimensions. channels = 4; + std::vector processed( + pixels, + pixels + static_cast(width) * static_cast(height) * 4U + ); + stbi_image_free(pixels); + + const int maxSize = std::clamp(importSettings.MaxSize, 1, 16384); + if (width > maxSize || height > maxSize) { + const float scale = std::min(static_cast(maxSize) / width, static_cast(maxSize) / height); + const int targetWidth = std::max(1, static_cast(std::floor(width * scale))); + const int targetHeight = std::max(1, static_cast(std::floor(height * scale))); + std::vector resized(static_cast(targetWidth) * targetHeight * 4U); + for (int y = 0; y < targetHeight; ++y) { + const int sourceY = std::min(height - 1, y * height / targetHeight); + for (int x = 0; x < targetWidth; ++x) { + const int sourceX = std::min(width - 1, x * width / targetWidth); + std::memcpy( + resized.data() + (static_cast(y) * targetWidth + x) * 4U, + processed.data() + (static_cast(sourceY) * width + sourceX) * 4U, + 4U + ); + } + } + processed = std::move(resized); + width = targetWidth; + height = targetHeight; + } + + if (importSettings.Usage == MetaCoreTextureUsage::Normal && importSettings.RenormalizeNormalMap) { + for (std::size_t index = 0; index < processed.size(); index += 4U) { + glm::vec3 normal{ + processed[index] / 127.5F - 1.0F, + processed[index + 1U] / 127.5F - 1.0F, + processed[index + 2U] / 127.5F - 1.0F + }; + if (glm::dot(normal, normal) > 0.000001F) { + normal = glm::normalize(normal); + } else { + normal = {0.0F, 0.0F, 1.0F}; + } + processed[index] = static_cast((normal.x * 0.5F + 0.5F) * 255.0F); + processed[index + 1U] = static_cast((normal.y * 0.5F + 0.5F) * 255.0F); + processed[index + 2U] = static_cast((normal.z * 0.5F + 0.5F) * 255.0F); + } + } MetaCoreTextureAssetDocument doc; doc.AssetGuid = MetaCoreAssetGuid::Generate(); @@ -5254,41 +5633,132 @@ void MetaCorePublishAssetRecordChanges( doc.Width = width; doc.Height = height; doc.Channels = channels; + doc.ColorSpace = importSettings.ColorSpace == MetaCoreTextureColorSpace::Automatic + ? (importSettings.Usage == MetaCoreTextureUsage::Color + ? MetaCoreTextureColorSpace::Srgb + : MetaCoreTextureColorSpace::Linear) + : importSettings.ColorSpace; + doc.Compression = importSettings.Compression; + doc.UsageHint = importSettings.Usage == MetaCoreTextureUsage::Normal ? "Normal" : + (importSettings.Usage == MetaCoreTextureUsage::Data ? "Data" : "Color"); - // Check if texture dimensions are multiples of 4 (required for DXT block compression) - bool canCompress = (width % 4 == 0) && (height % 4 == 0); + const bool wantsKtx2 = importSettings.Compression != MetaCoreTextureCompression::None; + if (wantsKtx2) { +#if !defined(_WIN32) && defined(METACORE_BASISU_EXECUTABLE) + const std::uint64_t temporaryId = MetaCoreHashCombine( + sourceHash, + MetaCoreHashString(std::to_string(std::hash{}(std::this_thread::get_id()))) + ); + std::ostringstream temporaryName; + temporaryName << "metacore-basisu-" << std::hex << temporaryId; + const std::filesystem::path temporaryRoot = + std::filesystem::temp_directory_path() / temporaryName.str(); + const std::filesystem::path inputPath = temporaryRoot / "input.tga"; + const std::filesystem::path outputPath = temporaryRoot / "output.ktx2"; + std::error_code temporaryError; + std::filesystem::create_directories(temporaryRoot, temporaryError); - // Simple heuristic for alpha: if we loaded it, let's just use DXT5, else DXT1. - // Wait, DXT5 uses STB_DXT_NORMAL, we can just use DXT5 to be safe since we forced 4 channels. - // Or we could scan the alpha channel to see if it's opaque. Let's just use DXT5 if canCompress. - if (canCompress) { - doc.Format = "dxt5"; - const int numBlocksX = width / 4; - const int numBlocksY = height / 4; - const int blockSize = 16; // 16 bytes for DXT5 - const std::size_t dataSize = static_cast(numBlocksX) * numBlocksY * blockSize; + bool encoded = !temporaryError; + if (encoded) { + std::ofstream tga(inputPath, std::ios::binary | std::ios::trunc); + std::array header{}; + header[2] = 2; + header[12] = static_cast(width & 0xFF); + header[13] = static_cast((width >> 8) & 0xFF); + header[14] = static_cast(height & 0xFF); + header[15] = static_cast((height >> 8) & 0xFF); + header[16] = 32; + header[17] = 0x28; + tga.write(reinterpret_cast(header.data()), header.size()); + for (std::size_t index = 0; index < processed.size(); index += 4U) { + const std::array bgra{ + processed[index + 2U], processed[index + 1U], processed[index], processed[index + 3U] + }; + tga.write(reinterpret_cast(bgra.data()), bgra.size()); + } + encoded = tga.good(); + } - outPayloadData.resize(dataSize); - unsigned char* outPtr = reinterpret_cast(outPayloadData.data()); + if (encoded && (cancelRequested == nullptr || !cancelRequested->load())) { + std::vector arguments{ + METACORE_BASISU_EXECUTABLE, + "-file", inputPath.string(), + "-output_file", outputPath.string(), + "-ktx2", "-uastc", + "-quality", std::to_string(std::clamp(importSettings.CompressionQuality, 0, 100)), + "-effort", std::to_string(std::clamp(importSettings.CompressionQuality / 10, 0, 10)), + "-no_status_output" + }; + if (importSettings.GenerateMipmaps) { + arguments.emplace_back("-mipmap"); + } + if (doc.ColorSpace == MetaCoreTextureColorSpace::Linear) { + arguments.emplace_back("-linear"); + } + if (importSettings.Usage == MetaCoreTextureUsage::Normal) { + arguments.emplace_back("-normal_map"); + arguments.emplace_back("-mip_renorm"); + } + std::vector argv; + argv.reserve(arguments.size() + 1U); + for (std::string& argument : arguments) { + argv.push_back(argument.data()); + } + argv.push_back(nullptr); - for (int by = 0; by < numBlocksY; ++by) { - for (int bx = 0; bx < numBlocksX; ++bx) { - unsigned char block[64]; - // Extract 4x4 block - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - int px = bx * 4 + x; - int py = by * 4 + y; - int srcIdx = (py * width + px) * 4; - int dstIdx = (y * 4 + x) * 4; - std::memcpy(&block[dstIdx], &pixels[srcIdx], 4); + const pid_t child = fork(); + if (child == 0) { + execv(argv[0], argv.data()); + _exit(127); + } + int status = -1; + if (child > 0) { + for (;;) { + const pid_t result = waitpid(child, &status, WNOHANG); + if (result == child) { + break; } + if (result < 0 || (cancelRequested != nullptr && cancelRequested->load())) { + kill(child, SIGTERM); + (void)waitpid(child, &status, 0); + status = -1; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); } + } + encoded = child > 0 && WIFEXITED(status) && WEXITSTATUS(status) == 0; + } else { + encoded = false; + } - stb_compress_dxt_block(outPtr, block, 1, STB_DXT_NORMAL); // 1 = alpha - outPtr += blockSize; + if (encoded) { + std::ifstream input(outputPath, std::ios::binary | std::ios::ate); + const auto outputSize = input.tellg(); + if (input.is_open() && outputSize > 0) { + outPayloadData.resize(static_cast(outputSize)); + input.seekg(0); + input.read(reinterpret_cast(outPayloadData.data()), outputSize); + encoded = input.good(); + } else { + encoded = false; } } + std::filesystem::remove_all(temporaryRoot, temporaryError); + if (!encoded) { + return {}; + } + doc.Format = "ktx2-uastc"; + doc.Compression = MetaCoreTextureCompression::BasisUastc; + doc.MipCount = 1; + for (int levelWidth = width, levelHeight = height; + importSettings.GenerateMipmaps && (levelWidth > 1 || levelHeight > 1); + levelWidth = std::max(1, levelWidth / 2), levelHeight = std::max(1, levelHeight / 2)) { + ++doc.MipCount; + } +#else + return {}; +#endif } else { doc.Format = "raw"; const std::size_t dataSize = @@ -5296,9 +5766,43 @@ void MetaCorePublishAssetRecordChanges( static_cast(height) * static_cast(channels); outPayloadData.resize(dataSize); - std::memcpy(outPayloadData.data(), pixels, dataSize); + std::memcpy(outPayloadData.data(), processed.data(), dataSize); + if (importSettings.GenerateMipmaps) { + std::vector level = processed; + int levelWidth = width; + int levelHeight = height; + while (levelWidth > 1 || levelHeight > 1) { + const int nextWidth = std::max(1, levelWidth / 2); + const int nextHeight = std::max(1, levelHeight / 2); + std::vector next(static_cast(nextWidth) * nextHeight * 4U); + for (int y = 0; y < nextHeight; ++y) { + for (int x = 0; x < nextWidth; ++x) { + for (int channel = 0; channel < 4; ++channel) { + unsigned int sum = 0; + unsigned int samples = 0; + for (int oy = 0; oy < 2; ++oy) { + for (int ox = 0; ox < 2; ++ox) { + const int sx = std::min(levelWidth - 1, x * 2 + ox); + const int sy = std::min(levelHeight - 1, y * 2 + oy); + sum += level[(static_cast(sy) * levelWidth + sx) * 4U + channel]; + ++samples; + } + } + next[(static_cast(y) * nextWidth + x) * 4U + channel] = + static_cast(sum / samples); + } + } + } + const auto oldSize = outPayloadData.size(); + outPayloadData.resize(oldSize + next.size()); + std::memcpy(outPayloadData.data() + oldSize, next.data(), next.size()); + level = std::move(next); + levelWidth = nextWidth; + levelHeight = nextHeight; + ++doc.MipCount; + } + } } - stbi_image_free(pixels); doc.PayloadIndex = 1; doc.PayloadSize = outPayloadData.size(); @@ -5655,7 +6159,7 @@ public: [[nodiscard]] bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) override; [[nodiscard]] bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) override; [[nodiscard]] bool MovePathTo(const std::filesystem::path& relativePath, const std::filesystem::path& targetRelativePath) override; - [[nodiscard]] bool DeletePath(const std::filesystem::path& relativePath) override; + [[nodiscard]] bool DeletePath(const std::filesystem::path& relativePath, bool force = false) override; bool ApplyExternalFileChanges(const std::vector& changes) override; [[nodiscard]] const std::vector& GetAssetRegistry() const override { return AssetRecords_; } @@ -5827,6 +6331,14 @@ bool MetaCoreBuiltinAssetDatabaseService::OpenProject(const std::filesystem::pat setenv("METACORE_PROJECT_PATH", projectFilePath.parent_path().string().c_str(), 1); #endif LoadProjectDescriptor(); + if (ModuleRegistry_ != nullptr) { + if (const auto cache = ModuleRegistry_->ResolveService(); cache != nullptr) { + cache->SetProjectRoot(Project_.RootPath); + } + if (const auto thumbnails = ModuleRegistry_->ResolveService(); thumbnails != nullptr) { + thumbnails->SetProjectRoot(Project_.RootPath); + } + } const bool refreshed = Refresh(); if (ModuleRegistry_ != nullptr) { if (const auto hotReloadService = ModuleRegistry_->ResolveService(); @@ -5945,12 +6457,27 @@ bool MetaCoreBuiltinAssetDatabaseService::MovePathTo( return refreshed; } -bool MetaCoreBuiltinAssetDatabaseService::DeletePath(const std::filesystem::path& relativePath) { +bool MetaCoreBuiltinAssetDatabaseService::DeletePath(const std::filesystem::path& relativePath, bool force) { if (!HasProject() || MetaCoreIsUnsafeRelativePath(relativePath) || relativePath.empty()) { return false; } const std::filesystem::path normalizedSourcePath = relativePath.lexically_normal(); + if (!force && ModuleRegistry_ != nullptr) { + if (const auto audit = ModuleRegistry_->ResolveService(); audit != nullptr) { + std::vector candidates; + if (const auto asset = FindAssetByRelativePath(normalizedSourcePath); asset.has_value()) { + candidates.push_back(*asset); + } else { + candidates = GetAssetsUnder(normalizedSourcePath); + } + for (const auto& candidate : candidates) { + if (candidate.Guid.IsValid() && !audit->CanDelete(candidate.Guid)) { + return false; + } + } + } + } const std::filesystem::path absoluteSourcePath = Project_.RootPath / normalizedSourcePath; if (!std::filesystem::exists(absoluteSourcePath)) { return false; @@ -6146,6 +6673,16 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { } const std::filesystem::path relativePath = entry.path().lexically_relative(Project_.RootPath); + if (entry.path().extension() == ".mcasset") { + auto sourceCandidate = entry.path(); + sourceCandidate.replace_extension(); + if (std::filesystem::is_regular_file(sourceCandidate)) { + // Derived packages are already represented by their source + // record and .mcmeta. Avoid deserializing their potentially + // huge mesh payload merely to rediscover the source path. + continue; + } + } if (entry.path().extension() == ".mcasset" || entry.path().extension() == ".mcprefab" || entry.path().extension() == ".mcui") { const auto package = PackageService_->ReadPackage(entry.path()); if (!package.has_value()) { @@ -6228,7 +6765,12 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { : (MetaCoreIsJsonSourceAssetPath(relativePath) ? relativePath : MetaCoreBuildPackagePathFromSource(relativePath)), hasMetadata ? metaPath.lexically_relative(Project_.RootPath) : std::filesystem::path{}, hasMetadata ? metadata.ImporterId : MetaCoreDetectImporterId(entry.path()), - hasMetadata ? metadata.SourceHash : 0 + hasMetadata ? metadata.SourceHash : 0, + hasMetadata ? metadata.LastImportResult : MetaCoreModelImportResult::Error, + hasMetadata ? metadata.ArtifactKey : std::string{}, + hasMetadata && !metadata.LastImportMessages.empty() + ? metadata.LastImportMessages.front().Message + : std::string{} }); } } @@ -6253,7 +6795,7 @@ bool MetaCoreBuiltinAssetDatabaseService::Refresh() { if (ModuleRegistry_ != nullptr) { if (const auto importPipeline = ModuleRegistry_->ResolveService(); importPipeline != nullptr) { - (void)importPipeline->RefreshImports(); + (void)importPipeline->EnqueueAllChanged(); } } @@ -6265,23 +6807,7 @@ bool MetaCoreBuiltinAssetDatabaseService::Refresh() { MetaCoreAssetRegistry::Get().ScanDirectory(Project_.AssetsPath); MetaCorePublishAssetRecordChanges(previousRecords, AssetRecords_); MetaCoreRebuildAssetDependencyGraph(Project_); - - if (ModuleRegistry_ != nullptr) { - if (const auto cookService = ModuleRegistry_->ResolveService(); cookService != nullptr) { - std::unordered_set cookedGuids; - for (const MetaCoreAssetRecord& record : AssetRecords_) { - if (!record.Guid.IsValid()) { - continue; - } - - const std::string guidString = record.Guid.ToString(); - if (!cookedGuids.insert(guidString).second) { - continue; - } - (void)cookService->CookAsset(record.Guid); - } - } - } + MetaCoreWriteAssetIndex(Project_, previousRecords, AssetRecords_); if (ModuleRegistry_ != nullptr) { ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ @@ -6491,6 +7017,658 @@ bool MetaCoreBuiltinAssetDatabaseService::SetStartupScenePath(const std::filesys return true; } +class MetaCoreBuiltinDerivedDataCacheService final : public MetaCoreIDerivedDataCacheService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.DerivedDataCache"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + if (const auto database = moduleRegistry.ResolveService(); + database != nullptr && database->HasProject()) { + SetProjectRoot(database->GetProjectDescriptor().RootPath); + } + } + + void SetProjectRoot(const std::filesystem::path& projectRoot) override { + std::scoped_lock lock(Mutex_); + Root_ = projectRoot.empty() ? std::filesystem::path{} : projectRoot / "Library" / "DerivedData"; + if (!Root_.empty()) { + std::error_code errorCode; + std::filesystem::create_directories(Root_, errorCode); + } + } + + [[nodiscard]] std::filesystem::path ResolvePath(std::string_view artifactKey, std::string_view suffix) const override { + if (artifactKey.empty()) { + return {}; + } + std::scoped_lock lock(Mutex_); + if (Root_.empty()) { + return {}; + } + const std::string key(artifactKey); + const std::string prefix = key.size() >= 2 ? key.substr(0, 2) : "00"; + return Root_ / prefix / (key + std::string(suffix)); + } + + [[nodiscard]] bool Contains(std::string_view artifactKey, std::string_view suffix) const override { + const auto path = ResolvePath(artifactKey, suffix); + return !path.empty() && std::filesystem::is_regular_file(path); + } + + [[nodiscard]] bool Put( + std::string_view artifactKey, + std::span data, + std::string_view suffix + ) override { + const auto path = ResolvePath(artifactKey, suffix); + if (path.empty()) { + return false; + } + std::error_code errorCode; + std::filesystem::create_directories(path.parent_path(), errorCode); + if (errorCode) { + return false; + } + const auto temporaryPath = std::filesystem::path(path.string() + ".tmp"); + { + std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + return false; + } + output.write(reinterpret_cast(data.data()), static_cast(data.size())); + if (!output.good()) { + return false; + } + } + std::filesystem::rename(temporaryPath, path, errorCode); + if (errorCode) { + errorCode.clear(); + std::filesystem::remove(path, errorCode); + errorCode.clear(); + std::filesystem::rename(temporaryPath, path, errorCode); + } + return !errorCode; + } + + [[nodiscard]] std::optional> Get( + std::string_view artifactKey, + std::string_view suffix + ) const override { + const auto path = ResolvePath(artifactKey, suffix); + if (path.empty()) { + return std::nullopt; + } + std::ifstream input(path, std::ios::binary | std::ios::ate); + if (!input.is_open()) { + return std::nullopt; + } + const auto size = input.tellg(); + if (size < 0) { + return std::nullopt; + } + std::vector bytes(static_cast(size)); + input.seekg(0); + input.read(reinterpret_cast(bytes.data()), size); + return input.good() || input.eof() ? std::optional>(std::move(bytes)) : std::nullopt; + } + +private: + mutable std::mutex Mutex_{}; + std::filesystem::path Root_{}; +}; + +class MetaCoreBuiltinThumbnailService final : public MetaCoreIThumbnailService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ThumbnailService"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + Database_ = moduleRegistry.ResolveService(); + if (Database_ != nullptr && Database_->HasProject()) { + SetProjectRoot(Database_->GetProjectDescriptor().RootPath); + } + Worker_ = std::thread([this]() { WorkerLoop(); }); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + { + std::scoped_lock lock(Mutex_); + StopRequested_ = true; + } + Condition_.notify_all(); + if (Worker_.joinable()) { + Worker_.join(); + } + Database_.reset(); + } + + void SetProjectRoot(const std::filesystem::path& projectRoot) override { + std::scoped_lock lock(Mutex_); + ProjectRoot_ = projectRoot; + ThumbnailRoot_ = projectRoot.empty() ? std::filesystem::path{} : projectRoot / "Library" / "Thumbnails"; + if (!ThumbnailRoot_.empty()) { + std::error_code errorCode; + std::filesystem::create_directories(ThumbnailRoot_, errorCode); + } + } + + [[nodiscard]] std::filesystem::path RequestThumbnail( + const MetaCoreAssetRecord& asset, + std::uint32_t size + ) override { + size = std::clamp(size, 32U, 512U); + std::string sourceKey = asset.ArtifactKey.empty() + ? asset.Guid.ToString() + ":" + std::to_string(asset.SourceHash) + : asset.ArtifactKey; + MetaCoreAssetMetadataDocument metadata; + // Model metadata can contain tens of thousands of generated sub-assets + // and dependency GUIDs. Its own ArtifactKey already changes whenever the + // imported model changes, so reparsing multi-megabyte .mcmeta files on + // every Project-panel frame is unnecessary. Standalone material previews + // still include their usually small texture dependency set. + if (asset.Type != "model" && Database_ != nullptr && !asset.MetaPath.empty() && + MetaCoreReadMetaJson(ProjectRoot_ / asset.MetaPath, metadata)) { + for (const MetaCoreAssetGuid& dependency : metadata.Dependencies) { + const auto dependencyAsset = Database_->FindAssetByGuid(dependency); + sourceKey += ":" + (dependencyAsset.has_value() + ? dependencyAsset->ArtifactKey + : dependency.ToString()); + } + } + const std::uint64_t hash = MetaCoreHashCombine( + MetaCoreHashString(sourceKey), + MetaCoreHashCombine(size, 2U) + ); + std::ostringstream keyStream; + keyStream << std::hex << std::setfill('0') << std::setw(16) << hash; + const std::string key = keyStream.str(); + + std::scoped_lock lock(Mutex_); + if (ThumbnailRoot_.empty()) { + return {}; + } + const auto output = ThumbnailRoot_ / (key + ".ppm"); + if (std::filesystem::is_regular_file(output)) { + return output; + } + if (PendingKeys_.insert(key).second) { + Pending_.push_back(ThumbnailRequest{asset, size, key, output, ProjectRoot_ / asset.RelativePath}); + Condition_.notify_one(); + } + return {}; + } + + void Tick() override {} + +private: + struct ThumbnailRequest { + MetaCoreAssetRecord Asset{}; + std::uint32_t Size = 128; + std::string Key{}; + std::filesystem::path Output{}; + std::filesystem::path Source{}; + }; + + static bool WritePpm( + const std::filesystem::path& output, + std::uint32_t width, + std::uint32_t height, + std::span rgb + ) { + std::ofstream stream(output, std::ios::binary | std::ios::trunc); + if (!stream.is_open()) { + return false; + } + stream << "P6\n" << width << " " << height << "\n255\n"; + stream.write(reinterpret_cast(rgb.data()), static_cast(rgb.size())); + return stream.good(); + } + + static void Generate(const ThumbnailRequest& request) { + const std::size_t pixelCount = static_cast(request.Size) * request.Size; + std::vector rgb(pixelCount * 3U, 0); + bool usedSource = false; + if (request.Asset.Type == "texture") { + int width = 0; + int height = 0; + int channels = 0; + unsigned char* pixels = stbi_load(request.Source.string().c_str(), &width, &height, &channels, 4); + if (pixels != nullptr && width > 0 && height > 0) { + for (std::uint32_t y = 0; y < request.Size; ++y) { + const int sourceY = std::min(height - 1, static_cast((static_cast(y) * height) / request.Size)); + for (std::uint32_t x = 0; x < request.Size; ++x) { + const int sourceX = std::min(width - 1, static_cast((static_cast(x) * width) / request.Size)); + const auto* source = pixels + (static_cast(sourceY) * width + sourceX) * 4U; + auto* target = rgb.data() + (static_cast(y) * request.Size + x) * 3U; + target[0] = source[0]; + target[1] = source[1]; + target[2] = source[2]; + } + } + usedSource = true; + } + stbi_image_free(pixels); + } + if (!usedSource && request.Asset.Type == "model") { + static constexpr std::uintmax_t kMaxDetailedPreviewSourceBytes = 64ULL * 1024ULL * 1024ULL; + static constexpr std::size_t kMaxPreviewTriangles = 20000U; + std::error_code fileSizeError; + const std::uintmax_t sourceSize = std::filesystem::file_size(request.Source, fileSizeError); + cgltf_options options{}; + cgltf_data* data = nullptr; + if (!fileSizeError && sourceSize <= kMaxDetailedPreviewSourceBytes && + cgltf_parse_file(&options, request.Source.string().c_str(), &data) == cgltf_result_success && + data != nullptr && cgltf_load_buffers(&options, data, request.Source.string().c_str()) == cgltf_result_success) { + std::vector> triangles; + triangles.reserve(std::min(kMaxPreviewTriangles, 4096U)); + for (std::size_t meshIndex = 0; + data->meshes != nullptr && meshIndex < data->meshes_count && triangles.size() < kMaxPreviewTriangles; + ++meshIndex) { + const cgltf_mesh& mesh = data->meshes[meshIndex]; + for (std::size_t primitiveIndex = 0; + mesh.primitives != nullptr && primitiveIndex < mesh.primitives_count && triangles.size() < kMaxPreviewTriangles; + ++primitiveIndex) { + const cgltf_primitive& primitive = mesh.primitives[primitiveIndex]; + const cgltf_accessor* positions = nullptr; + for (std::size_t attributeIndex = 0; + primitive.attributes != nullptr && attributeIndex < primitive.attributes_count; + ++attributeIndex) { + if (primitive.attributes[attributeIndex].type == cgltf_attribute_type_position) { + positions = primitive.attributes[attributeIndex].data; + break; + } + } + if (positions == nullptr) continue; + const std::size_t indexCount = primitive.indices != nullptr + ? primitive.indices->count : positions->count; + const std::size_t triangleCount = indexCount / 3U; + const std::size_t triangleStride = std::max( + 1U, + triangleCount / kMaxPreviewTriangles + + (triangleCount % kMaxPreviewTriangles == 0U ? 0U : 1U) + ); + for (std::size_t triangleIndex = 0; + triangleIndex < triangleCount && triangles.size() < kMaxPreviewTriangles; + triangleIndex += triangleStride) { + const std::size_t index = triangleIndex * 3U; + std::array triangle{}; + bool validTriangle = true; + for (std::size_t corner = 0; corner < 3; ++corner) { + const std::size_t vertexIndex = primitive.indices != nullptr + ? cgltf_accessor_read_index(primitive.indices, index + corner) + : index + corner; + if (vertexIndex >= positions->count) { + validTriangle = false; + break; + } + float position[3]{}; + if (!cgltf_accessor_read_float(positions, vertexIndex, position, 3) || + !std::isfinite(position[0]) || !std::isfinite(position[1]) || !std::isfinite(position[2])) { + validTriangle = false; + break; + } + triangle[corner] = {position[0], position[1], position[2]}; + } + if (validTriangle) { + triangles.push_back(triangle); + } + } + } + } + if (!triangles.empty()) { + std::vector projected; + projected.reserve(triangles.size() * 3U); + glm::vec2 minimum{std::numeric_limits::max()}; + glm::vec2 maximum{-std::numeric_limits::max()}; + for (const auto& triangle : triangles) { + for (const glm::vec3& position : triangle) { + const glm::vec2 point{ + (position.x - position.z) * 0.70710678F, + position.y * 0.82F - (position.x + position.z) * 0.29F + }; + projected.push_back(point); + minimum = glm::min(minimum, point); + maximum = glm::max(maximum, point); + } + } + const glm::vec2 center = (minimum + maximum) * 0.5F; + const float extent = std::max({maximum.x - minimum.x, maximum.y - minimum.y, 0.0001F}); + for (std::uint32_t y = 0; y < request.Size; ++y) { + for (std::uint32_t x = 0; x < request.Size; ++x) { + auto* target = rgb.data() + (static_cast(y) * request.Size + x) * 3U; + const std::uint8_t shade = static_cast(24U + 22U * y / request.Size); + target[0] = shade; target[1] = shade + 3U; target[2] = shade + 9U; + } + } + const auto toPixel = [&](const glm::vec2& point) { + const glm::vec2 normalized = (point - center) / extent; + return glm::ivec2{ + static_cast((normalized.x * 0.82F + 0.5F) * request.Size), + static_cast((0.5F - normalized.y * 0.82F) * request.Size) + }; + }; + const auto drawLine = [&](glm::ivec2 start, glm::ivec2 end) { + const int dx = std::abs(end.x - start.x), sx = start.x < end.x ? 1 : -1; + const int dy = -std::abs(end.y - start.y), sy = start.y < end.y ? 1 : -1; + int error = dx + dy; + for (;;) { + if (start.x >= 0 && start.y >= 0 && start.x < static_cast(request.Size) && start.y < static_cast(request.Size)) { + auto* pixel = rgb.data() + (static_cast(start.y) * request.Size + start.x) * 3U; + pixel[0] = 118; pixel[1] = 183; pixel[2] = 255; + } + if (start == end) break; + const int twice = error * 2; + if (twice >= dy) { error += dy; start.x += sx; } + if (twice <= dx) { error += dx; start.y += sy; } + } + }; + for (std::size_t index = 0; index < projected.size(); index += 3) { + const auto a = toPixel(projected[index]); + const auto b = toPixel(projected[index + 1U]); + const auto c = toPixel(projected[index + 2U]); + drawLine(a, b); drawLine(b, c); drawLine(c, a); + } + usedSource = true; + } + } + if (data != nullptr) cgltf_free(data); + } + if (!usedSource) { + std::array color = request.Asset.Type == "model" + ? std::array{72, 112, 190} + : std::array{146, 91, 166}; + if (request.Asset.Type == "material") { + try { + std::ifstream input(request.Source); + nlohmann::json json; + input >> json; + const auto& value = json.contains("Material") ? json["Material"]["BaseColor"] : json["BaseColor"]; + if (value.is_array() && value.size() >= 3) { + for (std::size_t channel = 0; channel < 3; ++channel) { + color[channel] = static_cast(std::clamp(value[channel].get(), 0.0F, 1.0F) * 255.0F); + } + } + } catch (...) {} + } + for (std::uint32_t y = 0; y < request.Size; ++y) { + for (std::uint32_t x = 0; x < request.Size; ++x) { + const float nx = (2.0F * (x + 0.5F) / request.Size) - 1.0F; + const float ny = 1.0F - (2.0F * (y + 0.5F) / request.Size); + const float radiusSquared = nx * nx + ny * ny; + auto* target = rgb.data() + (static_cast(y) * request.Size + x) * 3U; + if (request.Asset.Type == "material" && radiusSquared <= 0.82F) { + const glm::vec3 normal = glm::normalize(glm::vec3{nx, ny, std::sqrt(0.82F - radiusSquared)}); + const float diffuse = std::max(0.0F, glm::dot(normal, glm::normalize(glm::vec3{-0.45F, 0.7F, 0.65F}))); + const float lighting = 0.18F + diffuse * 0.82F; + for (std::size_t channel = 0; channel < 3; ++channel) { + target[channel] = static_cast(std::clamp(color[channel] * lighting, 0.0F, 255.0F)); + } + } else { + const std::uint8_t background = static_cast(22U + 18U * y / request.Size); + target[0] = background; target[1] = background; target[2] = background + 5U; + } + } + } + } + (void)WritePpm(request.Output, request.Size, request.Size, rgb); + } + + void WorkerLoop() { + for (;;) { + ThumbnailRequest request; + { + std::unique_lock lock(Mutex_); + Condition_.wait(lock, [&]() { return StopRequested_ || !Pending_.empty(); }); + if (StopRequested_ && Pending_.empty()) { + return; + } + request = std::move(Pending_.front()); + Pending_.pop_front(); + } + Generate(request); + std::scoped_lock lock(Mutex_); + PendingKeys_.erase(request.Key); + } + } + + std::filesystem::path ProjectRoot_{}; + std::filesystem::path ThumbnailRoot_{}; + std::mutex Mutex_{}; + std::condition_variable Condition_{}; + std::deque Pending_{}; + std::unordered_set PendingKeys_{}; + std::thread Worker_{}; + bool StopRequested_ = false; + std::shared_ptr Database_{}; +}; + +class MetaCoreBuiltinAssetAuditService final : public MetaCoreIAssetAuditService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.AssetAudit"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabase_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabase_.reset(); + } + + [[nodiscard]] std::vector AuditAll() const override { + std::vector findings; + if (AssetDatabase_ == nullptr || !AssetDatabase_->HasProject()) { + return findings; + } + const auto& project = AssetDatabase_->GetProjectDescriptor(); + std::unordered_set known; + for (const auto& asset : AssetDatabase_->GetAssetRegistry()) { + if (asset.Guid.IsValid()) { + known.insert(asset.Guid); + } + MetaCoreAssetMetadataDocument metadata; + if (!asset.MetaPath.empty() && MetaCoreReadMetaJson(project.RootPath / asset.MetaPath, metadata)) { + for (const auto& subAsset : metadata.SubAssets) { + if (subAsset.Guid.IsValid()) { + known.insert(subAsset.Guid); + } + } + } + } + + std::unordered_set referenced; + for (const auto& asset : AssetDatabase_->GetAssetRegistry()) { + const auto absolutePath = project.RootPath / asset.RelativePath; + if (!std::filesystem::is_regular_file(absolutePath)) { + continue; + } + try { + std::ifstream input(absolutePath); + nlohmann::json json; + input >> json; + CollectJsonReferences(json, "$", asset, known, referenced, findings); + } catch (...) { + // Binary and non-JSON assets are represented by metadata dependencies below. + } + + MetaCoreAssetMetadataDocument metadata; + if (!asset.MetaPath.empty() && MetaCoreReadMetaJson(project.RootPath / asset.MetaPath, metadata)) { + for (std::size_t index = 0; index < metadata.Dependencies.size(); ++index) { + const auto target = metadata.Dependencies[index]; + referenced.insert(target); + findings.push_back(MetaCoreAssetReferenceFinding{ + asset.Guid, + asset.RelativePath, + "$.metadata.dependencies[" + std::to_string(index) + "]", + target, + known.contains(target) ? MetaCoreAssetReferenceStatus::Valid : MetaCoreAssetReferenceStatus::Missing + }); + } + } + } + + for (const auto& asset : AssetDatabase_->GetAssetRegistry()) { + if (asset.Guid.IsValid() && asset.Type != "scene" && !referenced.contains(asset.Guid)) { + findings.push_back(MetaCoreAssetReferenceFinding{ + {}, asset.RelativePath, "$", asset.Guid, MetaCoreAssetReferenceStatus::Unreferenced + }); + } + } + + std::unordered_map, MetaCoreAssetGuidHasher> adjacency; + for (const auto& finding : findings) { + if (finding.OwnerGuid.IsValid() && finding.TargetGuid.IsValid() && + finding.Status == MetaCoreAssetReferenceStatus::Valid) { + adjacency[finding.OwnerGuid].push_back(finding.TargetGuid); + } + } + std::unordered_set reachable; + std::deque worklist; + for (const auto& asset : AssetDatabase_->GetAssetRegistry()) { + if (asset.Guid.IsValid() && + (asset.RelativePath == project.StartupScenePath || + (project.StartupScenePath.empty() && asset.Type == "scene"))) { + reachable.insert(asset.Guid); + worklist.push_back(asset.Guid); + } + } + while (!worklist.empty()) { + const MetaCoreAssetGuid owner = worklist.front(); + worklist.pop_front(); + if (const auto iterator = adjacency.find(owner); iterator != adjacency.end()) { + for (const MetaCoreAssetGuid& target : iterator->second) { + if (reachable.insert(target).second) { + worklist.push_back(target); + } + } + } + } + if (!worklist.empty() || !reachable.empty() || !project.StartupScenePath.empty()) { + for (const auto& asset : AssetDatabase_->GetAssetRegistry()) { + if (asset.Guid.IsValid() && !reachable.contains(asset.Guid)) { + findings.push_back(MetaCoreAssetReferenceFinding{ + {}, asset.RelativePath, "$build_closure", asset.Guid, + MetaCoreAssetReferenceStatus::BuildUnreachable + }); + } + } + } + return findings; + } + + [[nodiscard]] std::vector FindReferencesTo( + const MetaCoreAssetGuid& assetGuid + ) const override { + auto findings = AuditAll(); + std::erase_if(findings, [&](const auto& finding) { + return finding.TargetGuid != assetGuid || + (finding.Status != MetaCoreAssetReferenceStatus::Valid && + finding.Status != MetaCoreAssetReferenceStatus::Missing); + }); + return findings; + } + + [[nodiscard]] bool CanDelete( + const MetaCoreAssetGuid& assetGuid, + std::vector* blockers + ) const override { + auto references = FindReferencesTo(assetGuid); + if (blockers != nullptr) { + *blockers = references; + } + return references.empty(); + } + + [[nodiscard]] bool ReplaceReference( + const MetaCoreAssetReferenceFinding& finding, + const MetaCoreAssetGuid& replacement + ) override { + if (AssetDatabase_ == nullptr || !AssetDatabase_->HasProject() || + finding.OwnerPath.empty() || !finding.TargetGuid.IsValid()) { + return false; + } + const auto absolutePath = AssetDatabase_->GetProjectDescriptor().RootPath / finding.OwnerPath; + try { + std::ifstream input(absolutePath); + nlohmann::json json; + input >> json; + input.close(); + bool replaced = false; + ReplaceGuid(json, finding.TargetGuid.ToString(), replacement.IsValid() ? replacement.ToString() : std::string{}, replaced); + if (!replaced) { + return false; + } + const auto temporaryPath = std::filesystem::path(absolutePath.string() + ".repairing"); + { + std::ofstream output(temporaryPath, std::ios::trunc); + output << json.dump(2); + if (!output.good()) { + return false; + } + } + std::error_code errorCode; + std::filesystem::rename(temporaryPath, absolutePath, errorCode); + if (errorCode) { + std::filesystem::remove(temporaryPath, errorCode); + return false; + } + return AssetDatabase_->Refresh(); + } catch (...) { + return false; + } + } + +private: + static void CollectJsonReferences( + const nlohmann::json& value, + const std::string& fieldPath, + const MetaCoreAssetRecord& owner, + const std::unordered_set& known, + std::unordered_set& referenced, + std::vector& findings + ) { + if (value.is_string()) { + const auto guid = MetaCoreAssetGuid::Parse(value.get()); + if (guid.has_value() && guid->IsValid() && *guid != owner.Guid) { + referenced.insert(*guid); + findings.push_back(MetaCoreAssetReferenceFinding{ + owner.Guid, + owner.RelativePath, + fieldPath, + *guid, + known.contains(*guid) ? MetaCoreAssetReferenceStatus::Valid : MetaCoreAssetReferenceStatus::Missing + }); + } + } else if (value.is_array()) { + for (std::size_t index = 0; index < value.size(); ++index) { + CollectJsonReferences(value[index], fieldPath + "[" + std::to_string(index) + "]", owner, known, referenced, findings); + } + } else if (value.is_object()) { + for (const auto& [key, child] : value.items()) { + CollectJsonReferences(child, fieldPath + "." + key, owner, known, referenced, findings); + } + } + } + + static void ReplaceGuid(nlohmann::json& value, const std::string& oldGuid, const std::string& newGuid, bool& replaced) { + if (value.is_string() && value.get() == oldGuid) { + value = newGuid; + replaced = true; + } else if (value.is_array()) { + for (auto& child : value) { + ReplaceGuid(child, oldGuid, newGuid, replaced); + } + } else if (value.is_object()) { + for (auto& [key, child] : value.items()) { + (void)key; + ReplaceGuid(child, oldGuid, newGuid, replaced); + } + } + } + + std::shared_ptr AssetDatabase_{}; +}; + class MetaCoreBuiltinCookService final : public MetaCoreICookService { public: [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.CookService"; } @@ -6535,26 +7713,111 @@ public: PackageService_ = moduleRegistry.ResolveService(); ReflectionRegistry_ = moduleRegistry.ResolveService(); CookService_ = moduleRegistry.ResolveService(); + DerivedDataCache_ = moduleRegistry.ResolveService(); + StopRequested_ = false; + const unsigned int hardwareThreads = std::max(2U, std::thread::hardware_concurrency()); + // Large imports are CPU and I/O intensive. Reserving only one hardware + // thread still starves the interactive editor and saturates storage. + const unsigned int workerCount = std::min(4U, std::max(1U, hardwareThreads - 1U)); + Workers_.reserve(workerCount); + for (unsigned int index = 0; index < workerCount; ++index) { + Workers_.emplace_back([this]() { WorkerLoop(); }); + } } void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { (void)moduleRegistry; + { + std::scoped_lock lock(JobsMutex_); + StopRequested_ = true; + for (auto& [id, job] : Jobs_) { + (void)id; + job.CancelRequested = true; + job.CancelToken->store(true); + if (job.Snapshot.State == MetaCoreImportJobState::Queued) { + job.Snapshot.State = MetaCoreImportJobState::Cancelled; + } + } + } + JobsCondition_.notify_all(); + for (std::thread& worker : Workers_) { + if (worker.joinable()) { + worker.join(); + } + } + Workers_.clear(); AssetDatabaseService_.reset(); PackageService_.reset(); ReflectionRegistry_.reset(); CookService_.reset(); + DerivedDataCache_.reset(); } [[nodiscard]] bool RefreshImports() override; [[nodiscard]] bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) override; + [[nodiscard]] MetaCoreImportJobId EnqueueAsset(const MetaCoreAssetGuid& assetGuid, bool force) override; + [[nodiscard]] std::vector EnqueueAllChanged() override; + [[nodiscard]] std::optional GetJob(MetaCoreImportJobId jobId) const override; + [[nodiscard]] std::vector GetJobs() const override; + bool CancelJob(MetaCoreImportJobId jobId) override; + [[nodiscard]] MetaCoreImportJobId RetryJob(MetaCoreImportJobId jobId) override; + void Tick() override { + if (!CommitRefreshPending_.exchange(false) || AssetDatabaseService_ == nullptr) { + return; + } + { + std::scoped_lock lock(JobsMutex_); + const bool importBatchStillRunning = std::any_of(Jobs_.begin(), Jobs_.end(), [](const auto& entry) { + const auto state = entry.second.Snapshot.State; + return state == MetaCoreImportJobState::Queued || + state == MetaCoreImportJobState::Running || + state == MetaCoreImportJobState::Committing; + }); + if (importBatchStillRunning) { + CommitRefreshPending_ = true; + return; + } + } + (void)AssetDatabaseService_->Refresh(); + } private: - [[nodiscard]] bool ImportSourceFile(const std::filesystem::path& absoluteSourcePath, bool forcePackageWrite = false); + struct ImportJob { + MetaCoreImportJobSnapshot Snapshot{}; + bool Force = false; + bool CancelRequested = false; + std::shared_ptr CancelToken = std::make_shared(false); + std::chrono::steady_clock::time_point StartedAt{}; + }; + + [[nodiscard]] MetaCoreImportJobId EnqueuePath( + const MetaCoreAssetGuid& assetGuid, + const std::filesystem::path& sourcePath, + bool force + ); + [[nodiscard]] bool WaitForJob(MetaCoreImportJobId jobId); + void WorkerLoop(); + [[nodiscard]] bool ImportSourceFile( + const std::filesystem::path& absoluteSourcePath, + bool forcePackageWrite = false, + const std::shared_ptr& cancelToken = {}, + const std::function& beginCommit = {} + ); std::shared_ptr AssetDatabaseService_{}; std::shared_ptr PackageService_{}; std::shared_ptr ReflectionRegistry_{}; std::shared_ptr CookService_{}; + std::shared_ptr DerivedDataCache_{}; + mutable std::mutex JobsMutex_{}; + std::condition_variable JobsCondition_{}; + std::unordered_map Jobs_{}; + std::unordered_map ActiveJobByPath_{}; + std::deque PendingJobs_{}; + std::vector Workers_{}; + MetaCoreImportJobId NextJobId_ = 1; + bool StopRequested_ = false; + std::atomic_bool CommitRefreshPending_ = false; }; class MetaCoreBuiltinAssetEditingService final : public MetaCoreIAssetEditingService { @@ -6599,6 +7862,16 @@ public: const MetaCoreModelImportSettingsDocument& importSettings ) override; + [[nodiscard]] bool SaveTextureImportSettings( + const MetaCoreAssetGuid& textureGuid, + const MetaCoreTextureImportSettingsDocument& importSettings + ) override; + + [[nodiscard]] bool SaveMaterialImportSettings( + const MetaCoreAssetGuid& materialGuid, + const MetaCoreMaterialImportSettingsDocument& importSettings + ) override; + [[nodiscard]] bool SaveMaterialAsset( const MetaCoreAssetGuid& materialGuid, const MetaCoreMaterialAssetDocument& document @@ -6650,7 +7923,10 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) { LoadManifest(); const std::uint64_t cookedKey = MetaCoreHashCombine( MetaCoreHashString(assetGuid.ToString()), - MetaCoreHashCombine(sourceHash, MetaCoreHashString(record->ImporterId)) + MetaCoreHashCombine( + sourceHash, + MetaCoreHashString(record->ArtifactKey.empty() ? record->ImporterId : record->ArtifactKey) + ) ); auto manifestIterator = std::find_if(Manifest_.Entries.begin(), Manifest_.Entries.end(), [&](const MetaCoreCookManifestEntry& entry) { @@ -6757,19 +8033,40 @@ bool MetaCoreBuiltinCookService::CookAsset(const MetaCoreAssetGuid& assetGuid) { return false; } + const auto dependencyClosureSet = MetaCoreAssetDependencyGraph::Get().GetDependencyClosure({assetGuid}); + std::vector dependencyClosure(dependencyClosureSet.begin(), dependencyClosureSet.end()); + std::sort(dependencyClosure.begin(), dependencyClosure.end(), [](const auto& lhs, const auto& rhs) { + return lhs.ToString() < rhs.ToString(); + }); if (manifestIterator == Manifest_.Entries.end()) { Manifest_.Entries.push_back(MetaCoreCookManifestEntry{ assetGuid, record->RelativePath, relativeCookedPath, sourceHash, - cookedKey + cookedKey, + record->ArtifactKey, +#if defined(_WIN32) + MetaCoreAssetTargetPlatform::WindowsX64, +#else + MetaCoreAssetTargetPlatform::LinuxX64, +#endif + GMetaCoreAssetGeneratorVersion, + dependencyClosure }); } else { manifestIterator->SourcePackagePath = record->RelativePath; manifestIterator->CookedPath = relativeCookedPath; manifestIterator->SourceHash = sourceHash; manifestIterator->CookedKey = cookedKey; + manifestIterator->ArtifactKey = record->ArtifactKey; +#if defined(_WIN32) + manifestIterator->TargetPlatform = MetaCoreAssetTargetPlatform::WindowsX64; +#else + manifestIterator->TargetPlatform = MetaCoreAssetTargetPlatform::LinuxX64; +#endif + manifestIterator->GeneratorVersion = GMetaCoreAssetGeneratorVersion; + manifestIterator->DependencyClosure = std::move(dependencyClosure); } SaveManifest(); @@ -7025,28 +8322,94 @@ bool MetaCoreBuiltinAssetEditingService::SaveModelImportSettings( return false; } - auto modelDocument = LoadModelAsset(modelGuid); - if (!modelDocument.has_value()) { + const std::filesystem::path absoluteSourcePath = + AssetDatabaseService_->GetProjectDescriptor().RootPath / assetRecord->SourcePath; + const std::filesystem::path absoluteMetaPath = MetaCoreBuildMetaPath(absoluteSourcePath); + MetaCoreAssetMetadataDocument metadata; + if (!MetaCoreReadMetaJson(absoluteMetaPath, metadata)) { + return false; + } + metadata.ImportSettingsJson = MetaCoreCanonicalModelSettingsJson(importSettings); + MetaCoreFinalizeMetadataContract(metadata); + + if (!MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { return false; } - modelDocument->ImportSettings = importSettings; - const bool saved = MetaCoreWriteImportedGltfAssetDocument( - *AssetDatabaseService_, - *PackageService_, - *ReflectionRegistry_, - *assetRecord, - *modelDocument - ); + bool saved = false; + if (ModuleRegistry_ != nullptr) { + if (const auto importPipeline = ModuleRegistry_->ResolveService(); + importPipeline != nullptr) { + saved = importPipeline->ReimportAsset(modelGuid); + } + } if (saved) { - ModelCache_[modelGuid] = *modelDocument; + ModelCache_.erase(modelGuid); ClearGeneratedAssetCaches(); - CacheGeneratedAssetDocuments(*assetRecord, *modelDocument); RescanHotReloadBaseline(); } return saved; } +bool MetaCoreBuiltinAssetEditingService::SaveTextureImportSettings( + const MetaCoreAssetGuid& textureGuid, + const MetaCoreTextureImportSettingsDocument& importSettings +) { + if (AssetDatabaseService_ == nullptr || ModuleRegistry_ == nullptr) { + return false; + } + const auto asset = AssetDatabaseService_->FindAssetByGuid(textureGuid); + if (!asset.has_value() || asset->Type != "texture" || asset->SourcePath.empty()) { + return false; + } + const auto metaPath = MetaCoreBuildMetaPath( + AssetDatabaseService_->GetProjectDescriptor().RootPath / asset->SourcePath + ); + MetaCoreAssetMetadataDocument metadata; + if (!MetaCoreReadMetaJson(metaPath, metadata)) { + return false; + } + metadata.ImportSettingsJson = MetaCoreCanonicalTextureSettingsJson(importSettings); + MetaCoreFinalizeMetadataContract(metadata); + if (!MetaCoreWriteMetaJson(metaPath, metadata)) { + return false; + } + const auto pipeline = ModuleRegistry_->ResolveService(); + const bool saved = pipeline != nullptr && pipeline->ReimportAsset(textureGuid); + if (saved) { + TextureCache_.erase(textureGuid); + RescanHotReloadBaseline(); + } + return saved; +} + +bool MetaCoreBuiltinAssetEditingService::SaveMaterialImportSettings( + const MetaCoreAssetGuid& materialGuid, + const MetaCoreMaterialImportSettingsDocument& importSettings +) { + if (AssetDatabaseService_ == nullptr || ModuleRegistry_ == nullptr) { + return false; + } + const auto asset = AssetDatabaseService_->FindAssetByGuid(materialGuid); + if (!asset.has_value() || asset->Type != "material" || asset->SourcePath.empty()) { + return false; + } + const auto metaPath = MetaCoreBuildMetaPath( + AssetDatabaseService_->GetProjectDescriptor().RootPath / asset->SourcePath + ); + MetaCoreAssetMetadataDocument metadata; + if (!MetaCoreReadMetaJson(metaPath, metadata)) { + return false; + } + metadata.ImportSettingsJson = MetaCoreCanonicalMaterialSettingsJson(importSettings); + MetaCoreFinalizeMetadataContract(metadata); + if (!MetaCoreWriteMetaJson(metaPath, metadata)) { + return false; + } + const auto pipeline = ModuleRegistry_->ResolveService(); + return pipeline != nullptr && pipeline->ReimportAsset(materialGuid); +} + bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( const MetaCoreAssetGuid& materialGuid, const MetaCoreMaterialAssetDocument& document @@ -7257,37 +8620,278 @@ void MetaCoreBuiltinCookService::SaveManifest() const { output.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); } -bool MetaCoreBuiltinImportPipelineService::RefreshImports() { - if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { - return false; +MetaCoreImportJobId MetaCoreBuiltinImportPipelineService::EnqueuePath( + const MetaCoreAssetGuid& assetGuid, + const std::filesystem::path& sourcePath, + bool force +) { + if (sourcePath.empty()) { + return 0; + } + const std::string key = sourcePath.lexically_normal().generic_string(); + std::scoped_lock lock(JobsMutex_); + if (const auto active = ActiveJobByPath_.find(key); active != ActiveJobByPath_.end()) { + auto job = Jobs_.find(active->second); + if (job != Jobs_.end() && job->second.Snapshot.State == MetaCoreImportJobState::Queued) { + job->second.Force = job->second.Force || force; + return active->second; + } + if (job != Jobs_.end() && job->second.Snapshot.State == MetaCoreImportJobState::Committing) { + return active->second; + } + if (job != Jobs_.end() && job->second.Snapshot.State == MetaCoreImportJobState::Running) { + job->second.CancelRequested = true; + job->second.CancelToken->store(true); + } } - bool importedAnything = false; - for (const auto& entry : std::filesystem::recursive_directory_iterator(AssetDatabaseService_->GetProjectDescriptor().AssetsPath)) { - if (!entry.is_regular_file() || - MetaCoreIsMetaPath(entry.path()) || - MetaCoreIsLegacyMetaPath(entry.path()) || - MetaCoreIsPackagePath(entry.path())) { + const MetaCoreImportJobId id = NextJobId_++; + ImportJob job; + job.Snapshot.Id = id; + job.Snapshot.AssetGuid = assetGuid; + job.Snapshot.SourcePath = sourcePath; + job.Snapshot.State = MetaCoreImportJobState::Queued; + job.Snapshot.Stage = "排队"; + job.Force = force; + Jobs_.emplace(id, std::move(job)); + ActiveJobByPath_[key] = id; + PendingJobs_.push_back(id); + JobsCondition_.notify_one(); + return id; +} + +MetaCoreImportJobId MetaCoreBuiltinImportPipelineService::EnqueueAsset( + const MetaCoreAssetGuid& assetGuid, + bool force +) { + if (AssetDatabaseService_ == nullptr || !AssetDatabaseService_->HasProject()) { + return 0; + } + const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); + if (!assetRecord.has_value() || assetRecord->SourcePath.empty()) { + return 0; + } + return EnqueuePath(assetGuid, assetRecord->SourcePath, force); +} + +std::vector MetaCoreBuiltinImportPipelineService::EnqueueAllChanged() { + std::vector jobs; + if (AssetDatabaseService_ == nullptr || !AssetDatabaseService_->HasProject()) { + return jobs; + } + const auto& project = AssetDatabaseService_->GetProjectDescriptor(); + if (!std::filesystem::exists(project.AssetsPath)) { + return jobs; + } + for (const auto& entry : std::filesystem::recursive_directory_iterator(project.AssetsPath)) { + if (!entry.is_regular_file() || MetaCoreIsMetaPath(entry.path()) || + MetaCoreIsLegacyMetaPath(entry.path()) || MetaCoreIsPackagePath(entry.path())) { continue; } - importedAnything = ImportSourceFile(entry.path()) || importedAnything; + const auto stamp = MetaCoreGetSourceFileStamp(entry.path()); + if (!stamp.has_value()) { + continue; + } + MetaCoreAssetMetadataDocument metadata; + const bool hasMetadata = MetaCoreReadMetaJson(MetaCoreBuildMetaPath(entry.path()), metadata); + const bool expectsDerivedPackage = + metadata.AssetType != "scene" && metadata.AssetType != "prefab" && metadata.AssetType != "material"; + const bool missingSuccessfulPackage = + expectsDerivedPackage && + metadata.LastImportResult != MetaCoreModelImportResult::Error && + !std::filesystem::exists(MetaCoreBuildPackagePathFromSource(entry.path())); + const bool changed = !hasMetadata || !MetaCoreMetadataMatchesSourceStamp(metadata, *stamp) || + metadata.SchemaVersion < GMetaCoreAssetMetadataSchemaVersion || metadata.ArtifactKey.empty() || + missingSuccessfulPackage; + if (changed) { + jobs.push_back(EnqueuePath(metadata.AssetGuid, entry.path().lexically_relative(project.RootPath), false)); + } } - return importedAnything; + return jobs; +} + +std::optional MetaCoreBuiltinImportPipelineService::GetJob( + MetaCoreImportJobId jobId +) const { + std::scoped_lock lock(JobsMutex_); + const auto iterator = Jobs_.find(jobId); + return iterator == Jobs_.end() ? std::nullopt : std::optional(iterator->second.Snapshot); +} + +std::vector MetaCoreBuiltinImportPipelineService::GetJobs() const { + std::scoped_lock lock(JobsMutex_); + std::vector snapshots; + snapshots.reserve(Jobs_.size()); + for (const auto& [id, job] : Jobs_) { + (void)id; + snapshots.push_back(job.Snapshot); + } + std::sort(snapshots.begin(), snapshots.end(), [](const auto& lhs, const auto& rhs) { return lhs.Id > rhs.Id; }); + return snapshots; +} + +bool MetaCoreBuiltinImportPipelineService::CancelJob(MetaCoreImportJobId jobId) { + std::scoped_lock lock(JobsMutex_); + const auto iterator = Jobs_.find(jobId); + if (iterator == Jobs_.end() || iterator->second.Snapshot.State == MetaCoreImportJobState::Committing) { + return false; + } + iterator->second.CancelRequested = true; + iterator->second.CancelToken->store(true); + if (iterator->second.Snapshot.State == MetaCoreImportJobState::Queued) { + iterator->second.Snapshot.State = MetaCoreImportJobState::Cancelled; + iterator->second.Snapshot.Stage = "已取消"; + } + JobsCondition_.notify_all(); + return true; +} + +MetaCoreImportJobId MetaCoreBuiltinImportPipelineService::RetryJob(MetaCoreImportJobId jobId) { + std::scoped_lock lock(JobsMutex_); + const auto iterator = Jobs_.find(jobId); + if (iterator == Jobs_.end()) { + return 0; + } + const auto assetGuid = iterator->second.Snapshot.AssetGuid; + const auto sourcePath = iterator->second.Snapshot.SourcePath; + // Drop the active mapping before enqueueing the replacement. + ActiveJobByPath_.erase(sourcePath.lexically_normal().generic_string()); + const MetaCoreImportJobId newId = NextJobId_++; + ImportJob retry; + retry.Snapshot.Id = newId; + retry.Snapshot.AssetGuid = assetGuid; + retry.Snapshot.SourcePath = sourcePath; + retry.Snapshot.State = MetaCoreImportJobState::Queued; + retry.Snapshot.Stage = "重试排队"; + retry.Force = true; + Jobs_.emplace(newId, std::move(retry)); + ActiveJobByPath_[sourcePath.lexically_normal().generic_string()] = newId; + PendingJobs_.push_back(newId); + JobsCondition_.notify_one(); + return newId; +} + +bool MetaCoreBuiltinImportPipelineService::WaitForJob(MetaCoreImportJobId jobId) { + std::unique_lock lock(JobsMutex_); + JobsCondition_.wait(lock, [&]() { + const auto iterator = Jobs_.find(jobId); + if (iterator == Jobs_.end()) { + return true; + } + const auto state = iterator->second.Snapshot.State; + return state == MetaCoreImportJobState::Succeeded || state == MetaCoreImportJobState::Warning || + state == MetaCoreImportJobState::Failed || state == MetaCoreImportJobState::Cancelled; + }); + const auto iterator = Jobs_.find(jobId); + return iterator != Jobs_.end() && + (iterator->second.Snapshot.State == MetaCoreImportJobState::Succeeded || + iterator->second.Snapshot.State == MetaCoreImportJobState::Warning); +} + +void MetaCoreBuiltinImportPipelineService::WorkerLoop() { + for (;;) { + MetaCoreImportJobId jobId = 0; + std::filesystem::path sourcePath; + bool force = false; + std::shared_ptr cancelToken; + { + std::unique_lock lock(JobsMutex_); + JobsCondition_.wait(lock, [&]() { return StopRequested_ || !PendingJobs_.empty(); }); + if (StopRequested_ && PendingJobs_.empty()) { + return; + } + jobId = PendingJobs_.front(); + PendingJobs_.pop_front(); + auto iterator = Jobs_.find(jobId); + if (iterator == Jobs_.end() || iterator->second.CancelRequested) { + JobsCondition_.notify_all(); + continue; + } + iterator->second.StartedAt = std::chrono::steady_clock::now(); + iterator->second.Snapshot.State = MetaCoreImportJobState::Running; + iterator->second.Snapshot.Stage = "解析与派生数据"; + iterator->second.Snapshot.Progress = 0.15F; + sourcePath = iterator->second.Snapshot.SourcePath; + force = iterator->second.Force; + cancelToken = iterator->second.CancelToken; + } + + const std::filesystem::path absolutePath = AssetDatabaseService_->GetProjectDescriptor().RootPath / sourcePath; + const bool success = ImportSourceFile(absolutePath, force, cancelToken, [this, jobId]() { + std::scoped_lock lock(JobsMutex_); + if (auto iterator = Jobs_.find(jobId); iterator != Jobs_.end()) { + iterator->second.Snapshot.State = MetaCoreImportJobState::Committing; + iterator->second.Snapshot.Stage = "原子提交"; + iterator->second.Snapshot.Progress = 0.9F; + } + }); + + { + std::scoped_lock lock(JobsMutex_); + auto iterator = Jobs_.find(jobId); + if (iterator == Jobs_.end()) { + continue; + } + iterator->second.Snapshot.Progress = 1.0F; + iterator->second.Snapshot.ElapsedMilliseconds = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - iterator->second.StartedAt + ).count() + ); + if (iterator->second.CancelRequested) { + iterator->second.Snapshot.State = MetaCoreImportJobState::Cancelled; + iterator->second.Snapshot.Stage = "已取消"; + } else { + MetaCoreAssetMetadataDocument metadata; + (void)MetaCoreReadMetaJson(MetaCoreBuildMetaPath(absolutePath), metadata); + iterator->second.Snapshot.AssetGuid = metadata.AssetGuid; + iterator->second.Snapshot.Messages = metadata.LastImportMessages; + if (!success || metadata.LastImportResult == MetaCoreModelImportResult::Error) { + iterator->second.Snapshot.State = MetaCoreImportJobState::Failed; + iterator->second.Snapshot.Stage = "失败"; + } else if (metadata.LastImportResult == MetaCoreModelImportResult::Warning) { + iterator->second.Snapshot.State = MetaCoreImportJobState::Warning; + iterator->second.Snapshot.Stage = "完成(警告)"; + } else { + iterator->second.Snapshot.State = MetaCoreImportJobState::Succeeded; + iterator->second.Snapshot.Stage = "完成"; + } + } + const std::string activeKey = sourcePath.lexically_normal().generic_string(); + if (const auto active = ActiveJobByPath_.find(activeKey); + active != ActiveJobByPath_.end() && active->second == jobId) { + ActiveJobByPath_.erase(active); + } + } + CommitRefreshPending_ = true; + JobsCondition_.notify_all(); + } +} + +bool MetaCoreBuiltinImportPipelineService::RefreshImports() { + const auto jobs = EnqueueAllChanged(); + bool success = true; + for (const auto job : jobs) { + success = WaitForJob(job) && success; + } + return success; } bool MetaCoreBuiltinImportPipelineService::ReimportAsset(const MetaCoreAssetGuid& assetGuid) { - if (AssetDatabaseService_ == nullptr || !AssetDatabaseService_->HasProject()) { - return false; - } - - const auto assetRecord = AssetDatabaseService_->FindAssetByGuid(assetGuid); - if (!assetRecord.has_value() || assetRecord->SourcePath.empty()) { - return false; - } - return ImportSourceFile(AssetDatabaseService_->GetProjectDescriptor().RootPath / assetRecord->SourcePath, true); + const auto job = EnqueueAsset(assetGuid, true); + return job != 0 && WaitForJob(job); } -bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesystem::path& absoluteSourcePath, bool forcePackageWrite) { +bool MetaCoreBuiltinImportPipelineService::ImportSourceFile( + const std::filesystem::path& absoluteSourcePath, + bool forcePackageWrite, + const std::shared_ptr& cancelToken, + const std::function& beginCommit +) { + const auto cancelled = [&]() { return cancelToken != nullptr && cancelToken->load(); }; + if (cancelled()) { + return false; + } const auto sourceStamp = MetaCoreGetSourceFileStamp(absoluteSourcePath); if (!sourceStamp.has_value()) { return false; @@ -7310,6 +8914,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste } const std::uint64_t previousSourceHash = metadata.SourceHash; const std::string previousImporterId = metadata.ImporterId; + const std::uint64_t previousSettingsHash = metadata.SettingsHash; + const std::string previousArtifactKey = metadata.ArtifactKey; const std::vector previousSubAssets = metadata.SubAssets; const bool hadSourceStamp = hasMetadata && MetaCoreMetadataHasSourceStamp(metadata); const bool sourceStampMatches = @@ -7321,7 +8927,12 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste MetaCoreGetFileLastWriteTimeValue(absoluteSourcePath) <= MetaCoreGetFileLastWriteTimeValue(absoluteMetaPath); if (!metadata.AssetGuid.IsValid()) { - metadata.AssetGuid = MetaCoreAssetGuid::Generate(); + const std::uint64_t recoveryHash = MetaCoreHashFile(absoluteSourcePath).value_or(0); + metadata.AssetGuid = MetaCoreRecoverGuidFromAssetIndex( + AssetDatabaseService_->GetProjectDescriptor().RootPath, + relativeSourcePath, + recoveryHash + ).value_or(MetaCoreAssetGuid::Generate()); } metadata.AssetType = MetaCoreDetectAssetType(absoluteSourcePath); @@ -7343,10 +8954,29 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste metadata.SourceHash = *sourceHash; } - const MetaCoreModelImportSettingsDocument existingModelImportSettings = - (metadata.AssetType == "model") - ? MetaCoreReadExistingModelImportSettings(*PackageService_, registry, absolutePackagePath) - : MetaCoreBuildDefaultModelImportSettings(); + MetaCoreModelImportSettingsDocument existingModelImportSettings = MetaCoreBuildDefaultModelImportSettings(); + if (metadata.AssetType == "model") { + existingModelImportSettings = !metadata.ImportSettingsJson.empty() + ? MetaCoreParseModelSettingsJson(metadata.ImportSettingsJson) + : MetaCoreReadExistingModelImportSettings(*PackageService_, registry, absolutePackagePath); + metadata.ImportSettingsJson = MetaCoreCanonicalModelSettingsJson(existingModelImportSettings); + } + MetaCoreFinalizeMetadataContract(metadata); + + if (!jsonSourceAsset && !std::filesystem::exists(absolutePackagePath) && DerivedDataCache_ != nullptr) { + if (const auto cachedPackage = DerivedDataCache_->Get(metadata.ArtifactKey, ".mcasset"); + cachedPackage.has_value()) { + (void)MetaCoreWriteBinaryFileAtomically(absolutePackagePath, *cachedPackage); + } + } + const auto cacheCommittedPackage = [&]() { + if (DerivedDataCache_ == nullptr || jsonSourceAsset) { + return; + } + if (const auto bytes = MetaCoreReadBinaryFile(absolutePackagePath); bytes.has_value()) { + (void)DerivedDataCache_->Put(metadata.ArtifactKey, *bytes, ".mcasset"); + } + }; const bool gltfModelPackageMissing = metadata.AssetType == "model" && @@ -7358,6 +8988,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste !hasMetadata || previousSourceHash != metadata.SourceHash || previousImporterId != metadata.ImporterId || + previousSettingsHash != metadata.SettingsHash || + previousArtifactKey != metadata.ArtifactKey || gltfModelPackageMissing; if (needsPackageWrite) { @@ -7371,10 +9003,24 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste metadata.SourcePath, metadata.SourceHash, meshPayloads, - existingModelImportSettings + existingModelImportSettings, + cancelToken.get() ); + if (cancelled()) { + return false; + } + if (importedAsset.ImportReport.Result == MetaCoreModelImportResult::Error) { + metadata.LastImportResult = MetaCoreModelImportResult::Error; + metadata.LastImportMessages = importedAsset.ImportReport.Messages; + if (beginCommit) beginCommit(); + (void)MetaCoreWriteMetaJson(absoluteMetaPath, metadata); + return false; + } MetaCoreApplyStableSubAssetGuids(previousSubAssets, importedAsset); + MetaCoreFinalizeImportReport(importedAsset.ImportReport); MetaCoreUpdateModelSubAssetMetadata(importedAsset, metadata); + metadata.LastImportResult = importedAsset.ImportReport.Result; + metadata.LastImportMessages = importedAsset.ImportReport.Messages; importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); importedAssetTypeName = "MetaCoreModelAssetDocument"; @@ -7389,15 +9035,14 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste *importedAssetBytes ); - // Keep rendering on the source GLB/GLTF path, but persist the lightweight - // model document so Inspector and scene instantiation can read animation metadata. - (void)meshPayloads; + for (auto& meshPayload : meshPayloads) { + assetPackage.PayloadSections.push_back(std::move(meshPayload)); + } + if (beginCommit) beginCommit(); if (!PackageService_->WritePackage(absolutePackagePath, std::move(assetPackage))) { return false; } if (MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { - // 暂时忽略纹理等子资产运行时二进制包的物理生成 - /* MetaCoreWriteGeneratedTextureRuntimePackages( *PackageService_, registry, @@ -7412,17 +9057,31 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste absoluteSourcePath, importedAsset ); - */ + cacheCommittedPackage(); return true; } } return false; } else if (metadata.ImporterId == "TextureImporter") { std::vector texturePayload; + const MetaCoreTextureImportSettingsDocument textureSettings = + MetaCoreParseTextureSettingsJson(metadata.ImportSettingsJson); MetaCoreTextureAssetDocument importedAsset = - MetaCoreBuildImportedTextureDocument(absoluteSourcePath, metadata.SourcePath, metadata.SourceHash, texturePayload); + MetaCoreBuildImportedTextureDocument( + absoluteSourcePath, + metadata.SourcePath, + metadata.SourceHash, + texturePayload, + textureSettings, + cancelToken.get() + ); + if (cancelled()) { + return false; + } if (importedAsset.Width > 0) { + metadata.LastImportResult = MetaCoreModelImportResult::Success; + metadata.LastImportMessages.clear(); importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); importedAssetTypeName = "MetaCoreTextureAssetDocument"; @@ -7438,34 +9097,33 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste assetPackage.PayloadSections.push_back(std::move(texturePayload)); - // 【敏捷开发决策】:开发迭代期暂不烘焙生成 .mcasset 贴图二进制包,仅写入 .mcmeta 索引 - /* - (void)MetaCoreWriteTextureAssetPackageWithRuntimeCopy( + if (beginCommit) beginCommit(); + if (!MetaCoreWriteTextureAssetPackageWithRuntimeCopy( *PackageService_, AssetDatabaseService_->GetProjectDescriptor().RootPath, absolutePackagePath, metadata.AssetGuid, assetPackage - ); - */ + )) { + return false; + } if (MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { + cacheCommittedPackage(); return true; } } } return false; } else if (metadata.ImporterId == "ModelImporter" && metadata.AssetType == "model") { - MetaCoreModelAssetDocument importedAsset = - MetaCoreBuildImportedGenericModelAssetDocument( - absoluteSourcePath, - metadata.SourcePath, - metadata.SourceHash, - existingModelImportSettings - ); - MetaCoreApplyStableSubAssetGuids(previousSubAssets, importedAsset); - MetaCoreUpdateModelSubAssetMetadata(importedAsset, metadata); - importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); - importedAssetTypeName = "MetaCoreModelAssetDocument"; + metadata.LastImportResult = MetaCoreModelImportResult::Error; + metadata.LastImportMessages = {{ + MetaCoreImportMessageSeverity::Error, + "unsupported_model_format", + "FBX/OBJ 尚未正式支持;未生成通用占位资产" + }}; + if (beginCommit) beginCommit(); + (void)MetaCoreWriteMetaJson(absoluteMetaPath, metadata); + return false; } else { MetaCoreImportedAssetDocument importedAsset; importedAsset.AssetType = metadata.AssetType; @@ -7473,6 +9131,8 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste importedAsset.SourcePath = metadata.SourcePath; importedAsset.SourceHash = metadata.SourceHash; importedAssetBytes = MetaCoreSerializeToBytes(importedAsset, registry); + metadata.LastImportResult = MetaCoreModelImportResult::Success; + metadata.LastImportMessages.clear(); } if (!importedAssetBytes.has_value()) { @@ -7488,31 +9148,33 @@ bool MetaCoreBuiltinImportPipelineService::ImportSourceFile(const std::filesyste *importedAssetBytes ); - // 【敏捷开发决策】:开发工作流避开二进制打包开销,直接对标源文件与 .mcmeta 文本 - /* - const bool wroteAssetPackage = - metadata.AssetType == "model" + if (beginCommit) beginCommit(); + bool wroteAssetPackage = true; + if (!jsonSourceAsset) { + wroteAssetPackage = metadata.AssetType == "model" ? MetaCoreWriteModelAssetPackageWithRuntimeCopy( - *PackageService_, - AssetDatabaseService_->GetProjectDescriptor().RootPath, - absolutePackagePath, - metadata.AssetGuid, - assetPackage - ) + *PackageService_, + AssetDatabaseService_->GetProjectDescriptor().RootPath, + absolutePackagePath, + metadata.AssetGuid, + assetPackage + ) : PackageService_->WritePackage(absolutePackagePath, std::move(assetPackage)); - */ + } + if (!jsonSourceAsset && !wroteAssetPackage) { + return false; + } if (!MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { return false; } + cacheCommittedPackage(); } + if (beginCommit) beginCommit(); if (!MetaCoreWriteMetaJson(absoluteMetaPath, metadata)) { return false; } - if (CookService_ != nullptr) { - (void)CookService_->CookAsset(metadata.AssetGuid); - } return true; } @@ -10348,6 +12010,9 @@ public: moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); @@ -10362,8 +12027,19 @@ public: moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); - if (const auto assetDatabase = moduleRegistry.ResolveService(); - assetDatabase != nullptr && assetDatabase->Refresh() && assetDatabase->HasProject()) { + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto importPipeline = moduleRegistry.ResolveService(); + if (assetDatabase != nullptr && importPipeline != nullptr && assetDatabase->HasProject()) { + // Editor startup must remain interactive: scan once and let Refresh + // enqueue changed assets. Tests and CLI tools can opt into the + // synchronous compatibility boundary explicitly. + (void)assetDatabase->Refresh(); + if (MetaCoreSynchronousAssetBootstrapEnabled()) { + (void)importPipeline->RefreshImports(); + (void)assetDatabase->Refresh(); + } + } + if (assetDatabase != nullptr && assetDatabase->HasProject()) { moduleRegistry.AccessEventBus().Publish(MetaCoreEditorEvent{ MetaCoreEditorEventType::ProjectLoaded, "MetaCore 项目已加载", diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index d40838f..cb92b6e 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -7112,16 +7112,33 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD ImGui::EndTabItem(); } if (ImGui::BeginTabItem("导入设置")) { - // 当前阶段 Import Settings 为只读显示,后续迭代再开放编辑 const char* upAxisNames[] = {"保留源文件", "Y 轴向上", "Z 轴向上", "X 轴向上"}; - const int upAxisIndex = static_cast(importedDocument->ImportSettings.UpAxis); - ImGui::Text("向上轴: %s", (upAxisIndex >= 0 && upAxisIndex < 4) ? upAxisNames[upAxisIndex] : "未知"); - ImGui::Text("缩放系数: %.3f", importedDocument->ImportSettings.ScaleFactor); - ImGui::Text("保留层级: %s", importedDocument->ImportSettings.PreserveHierarchy ? "是" : "否"); - ImGui::Text("保留空节点: %s", importedDocument->ImportSettings.PreserveEmptyNodes ? "是" : "否"); - ImGui::Text("保留材质槽位: %s", importedDocument->ImportSettings.PreserveMaterialSlots ? "是" : "否"); + static std::unordered_map drafts; + const std::string draftKey = selectedAsset.Guid.ToString(); + auto [draftIterator, inserted] = drafts.try_emplace(draftKey, importedDocument->ImportSettings); + (void)inserted; + auto& draft = draftIterator->second; + ImGui::DragFloat("缩放系数", &draft.ScaleFactor, 0.01F, 0.0001F, 1000.0F, "%.4f"); + int upAxisIndex = static_cast(draft.UpAxis); + if (ImGui::Combo("向上轴", &upAxisIndex, upAxisNames, 4)) { + draft.UpAxis = static_cast(upAxisIndex); + } + ImGui::Checkbox("保留层级", &draft.PreserveHierarchy); + ImGui::Checkbox("保留空节点", &draft.PreserveEmptyNodes); + ImGui::Checkbox("保留材质槽位", &draft.PreserveMaterialSlots); ImGui::Separator(); + if (ImGui::Button("应用设置并重导入")) { + if (assetEditingService != nullptr && + assetEditingService->SaveModelImportSettings(selectedAsset.Guid, draft)) { + (void)assetDatabaseService.Refresh(); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Import", "导入设置已应用"); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Import", "应用导入设置失败"); + } + } + ImGui::SameLine(); + if (ImGui::Button("重新导入")) { if (importPipelineService != nullptr && importPipelineService->ReimportAsset(selectedAsset.Guid)) { (void)assetDatabaseService.Refresh(); @@ -7610,6 +7627,46 @@ void DrawMaterialAssetDetails( ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4.0F, 4.0F)); if (ImGui::BeginChild("MaterialInspector", ImVec2(0, 0), true, 0)) { + static std::unordered_map importDrafts; + auto [settingsIterator, settingsInserted] = importDrafts.try_emplace(selectedAsset.Guid); + auto& importSettings = settingsIterator->second; + if (settingsInserted && !assetRecord->MetaPath.empty()) { + try { + std::ifstream input(assetDatabaseService.GetProjectDescriptor().RootPath / assetRecord->MetaPath); + nlohmann::json metadata; + input >> metadata; + const auto value = metadata.value("import_settings", nlohmann::json::object()); + importSettings.ShaderModel = static_cast(value.value("shader_model", 0)); + importSettings.PreserveAlphaMode = value.value("preserve_alpha_mode", true); + importSettings.PreserveDoubleSided = value.value("preserve_double_sided", true); + importSettings.GeneratedMaterialPolicy = static_cast( + value.value("generated_material_policy", 1) + ); + } catch (...) {} + } + if (ImGui::CollapsingHeader("导入设置")) { + const char* shaderModels[]{"PBR Metal/Rough", "Unlit Color", "Unlit Texture"}; + const char* materialPolicies[]{"保留导入结果", "保留用户覆盖", "提取为独立材质"}; + int shaderModel = static_cast(importSettings.ShaderModel); + int materialPolicy = static_cast(importSettings.GeneratedMaterialPolicy); + if (ImGui::Combo("着色模型", &shaderModel, shaderModels, 3)) { + importSettings.ShaderModel = static_cast(shaderModel); + } + ImGui::Checkbox("保留 Alpha 模式", &importSettings.PreserveAlphaMode); + ImGui::Checkbox("保留双面设置", &importSettings.PreserveDoubleSided); + if (ImGui::Combo("生成材质策略", &materialPolicy, materialPolicies, 3)) { + importSettings.GeneratedMaterialPolicy = static_cast(materialPolicy); + } + if (ImGui::Button("应用导入设置并重导入")) { + if (assetEditingService->SaveMaterialImportSettings(selectedAsset.Guid, importSettings)) { + (void)assetDatabaseService.Refresh(); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Import", "材质导入设置已应用"); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Import", "材质导入设置应用失败"); + } + } + ImGui::Separator(); + } const MetaCoreMaterialEditorChange materialChange = MetaCoreDrawMaterialVisualEditor( editorContext, assetDatabaseService, @@ -7635,6 +7692,60 @@ void DrawMaterialAssetDetails( ImGui::PopStyleVar(); } +void DrawTextureAssetDetails( + MetaCoreEditorContext& editorContext, + MetaCoreIAssetDatabaseService& assetDatabaseService +) { + const auto& selected = editorContext.GetSelectedAsset(); + const auto record = assetDatabaseService.FindAssetByGuid(selected.Guid); + const auto editing = editorContext.GetModuleRegistry().ResolveService(); + if (!record.has_value() || editing == nullptr) return; + + static std::unordered_map drafts; + auto [iterator, inserted] = drafts.try_emplace(selected.Guid); + auto& settings = iterator->second; + if (inserted && !record->MetaPath.empty()) { + try { + std::ifstream input(assetDatabaseService.GetProjectDescriptor().RootPath / record->MetaPath); + nlohmann::json metadata; + input >> metadata; + const auto value = metadata.value("import_settings", nlohmann::json::object()); + settings.ColorSpace = static_cast(value.value("color_space", 0)); + settings.Usage = static_cast(value.value("usage", 0)); + settings.GenerateMipmaps = value.value("generate_mipmaps", true); + settings.MaxSize = value.value("max_size", 4096); + settings.Compression = static_cast(value.value("compression", 1)); + settings.CompressionQuality = value.value("compression_quality", 80); + settings.RenormalizeNormalMap = value.value("renormalize_normal_map", true); + } catch (...) {} + } + + ImGui::Text("纹理: %s", record->RelativePath.filename().string().c_str()); + const char* colorSpaces[]{"自动", "sRGB", "Linear"}; + const char* usages[]{"自动", "颜色", "法线", "数据", "HDR"}; + const char* compressions[]{"不压缩", "平台默认 UASTC/KTX2", "Basis UASTC/KTX2"}; + int colorSpace = static_cast(settings.ColorSpace); + int usage = static_cast(settings.Usage); + int compression = static_cast(settings.Compression); + if (ImGui::Combo("色彩空间", &colorSpace, colorSpaces, 3)) settings.ColorSpace = static_cast(colorSpace); + if (ImGui::Combo("用途", &usage, usages, 5)) settings.Usage = static_cast(usage); + ImGui::Checkbox("生成 Mipmap", &settings.GenerateMipmaps); + ImGui::SliderInt("最大尺寸", &settings.MaxSize, 1, 16384); + if (ImGui::Combo("压缩", &compression, compressions, 3)) settings.Compression = static_cast(compression); + ImGui::SliderInt("压缩质量", &settings.CompressionQuality, 0, 100); + if (settings.Usage == MetaCoreTextureUsage::Normal) { + ImGui::Checkbox("缩放后重新归一化", &settings.RenormalizeNormalMap); + } + if (ImGui::Button("应用设置并重导入")) { + if (editing->SaveTextureImportSettings(selected.Guid, settings)) { + (void)assetDatabaseService.Refresh(); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Import", "纹理导入设置已应用"); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Import", "纹理导入设置应用失败"); + } + } +} + void MetaCoreDrawAssetInspector(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIPackageService& packageService, MetaCoreIReflectionRegistry& reflectionRegistry) { if (!editorContext.HasSelectedAsset()) return; const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); @@ -7647,6 +7758,7 @@ void MetaCoreDrawAssetInspector(MetaCoreEditorContext& editorContext, MetaCoreIA else if (selectedAsset.Type == "model") DrawModelAssetDetails(editorContext, assetDatabaseService); else if (selectedAsset.Type == "prefab") DrawPrefabDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry); else if (selectedAsset.Type == "material") DrawMaterialAssetDetails(editorContext, assetDatabaseService, reflectionRegistry); + else if (selectedAsset.Type == "texture") DrawTextureAssetDetails(editorContext, assetDatabaseService); else if (selectedAsset.Type == "ui_document") DrawUiDocumentDetails(editorContext, assetDatabaseService, packageService, reflectionRegistry); else { ImGui::Separator(); @@ -7831,6 +7943,17 @@ struct MetaCoreProjectPreviewColorCacheEntry { static std::unordered_map ProjectPreviewColorCache_{}; +struct MetaCoreProjectThumbnailTextureCacheEntry { + std::uint64_t Mtime = 0; + std::unique_ptr Texture{}; + bool Valid = false; +}; + +static std::unordered_map ProjectThumbnailTextureCache_{}; +// Texture destruction is completed by the renderer at the start of a later frame. +// Keep the ImTextureData alive and registered until that acknowledgement arrives. +static std::vector> RetiredProjectThumbnailTextures_{}; + [[nodiscard]] std::uint64_t MetaCoreProjectFileMtime(const std::filesystem::path& absolutePath) { std::error_code errorCode; const auto fileTime = std::filesystem::last_write_time(absolutePath, errorCode); @@ -7851,6 +7974,73 @@ static std::unordered_map Pr return IM_COL32(toByte(r), toByte(g), toByte(b), 255); } +void MetaCoreCollectRetiredProjectThumbnailTextures() { + for (auto it = RetiredProjectThumbnailTextures_.begin(); it != RetiredProjectThumbnailTextures_.end();) { + if ((*it)->Status == ImTextureStatus_Destroyed) { + ImGui::UnregisterUserTexture(it->get()); + it = RetiredProjectThumbnailTextures_.erase(it); + } else { + ++it; + } + } +} + +void MetaCoreRetireProjectThumbnailTexture(std::unique_ptr texture) { + if (texture == nullptr) { + return; + } + // The renderer no longer needs the CPU copy to release its Filament texture. + // Removing it also prevents SetStatus(Destroyed) from requesting a recreation. + texture->DestroyPixels(); + texture->UnusedFrames = 1; + texture->SetStatus(ImTextureStatus_WantDestroy); + RetiredProjectThumbnailTextures_.push_back(std::move(texture)); +} + +[[nodiscard]] ImTextureData* MetaCoreProjectLoadThumbnailTexture( + const std::filesystem::path& absolutePath +) { + MetaCoreCollectRetiredProjectThumbnailTextures(); + + const std::string cacheKey = absolutePath.generic_string(); + const std::uint64_t mtime = MetaCoreProjectFileMtime(absolutePath); + if (const auto it = ProjectThumbnailTextureCache_.find(cacheKey); + it != ProjectThumbnailTextureCache_.end() && it->second.Mtime == mtime) { + return it->second.Valid ? it->second.Texture.get() : nullptr; + } + + MetaCoreProjectThumbnailTextureCacheEntry cacheEntry; + cacheEntry.Mtime = mtime; + if (const auto old = ProjectThumbnailTextureCache_.find(cacheKey); + old != ProjectThumbnailTextureCache_.end()) { + MetaCoreRetireProjectThumbnailTexture(std::move(old->second.Texture)); + ProjectThumbnailTextureCache_.erase(old); + } + + int width = 0; + int height = 0; + int channels = 0; + unsigned char* pixels = stbi_load(absolutePath.string().c_str(), &width, &height, &channels, 4); + if (pixels != nullptr && width > 0 && height > 0 && width <= 4096 && height <= 4096) { + cacheEntry.Texture = std::make_unique(); + cacheEntry.Texture->Create(ImTextureFormat_RGBA32, width, height); + std::memcpy( + cacheEntry.Texture->GetPixels(), + pixels, + static_cast(width) * static_cast(height) * 4U + ); + ImGui::RegisterUserTexture(cacheEntry.Texture.get()); + cacheEntry.Valid = true; + } + if (pixels != nullptr) { + stbi_image_free(pixels); + } + + ImTextureData* result = cacheEntry.Valid ? cacheEntry.Texture.get() : nullptr; + ProjectThumbnailTextureCache_.emplace(cacheKey, std::move(cacheEntry)); + return result; +} + [[nodiscard]] bool MetaCoreProjectReadMaterialPreviewColor( const std::filesystem::path& absolutePath, ImU32& outColor @@ -7975,6 +8165,7 @@ void MetaCoreDrawFolderThumbnail(ImDrawList& drawList, const ImVec2& min, const void MetaCoreDrawProjectThumbnail( const MetaCoreProjectFileEntry& entry, const MetaCoreProjectDescriptor& project, + MetaCoreIThumbnailService* thumbnailService, std::string_view type, const ImVec2& min, const ImVec2& max @@ -7987,11 +8178,21 @@ void MetaCoreDrawProjectThumbnail( const ImU32 baseColor = MetaCoreProjectTypeColor(type, entry.IsDirectory); ImU32 previewColor = baseColor; bool hasContentColor = false; + ImTextureData* thumbnailTexture = nullptr; if (!entry.IsDirectory) { const std::filesystem::path absolutePath = project.RootPath / entry.RelativePath; - if (type == "material") { + std::filesystem::path persistentThumbnail; + if (thumbnailService != nullptr && entry.AssetRecord.has_value()) { + persistentThumbnail = thumbnailService->RequestThumbnail(*entry.AssetRecord, 128); + } + if (!persistentThumbnail.empty()) { + thumbnailTexture = MetaCoreProjectLoadThumbnailTexture(persistentThumbnail); + if (thumbnailTexture == nullptr) { + hasContentColor = MetaCoreProjectReadTexturePreviewColor(persistentThumbnail, previewColor); + } + } else if (thumbnailService == nullptr && type == "material") { hasContentColor = MetaCoreProjectReadMaterialPreviewColor(absolutePath, previewColor); - } else if (type == "texture") { + } else if (thumbnailService == nullptr && type == "texture") { hasContentColor = MetaCoreProjectReadTexturePreviewColor(absolutePath, previewColor); } } @@ -8004,7 +8205,17 @@ void MetaCoreDrawProjectThumbnail( const ImVec2 innerMin(min.x + 7.0F, min.y + 7.0F); const ImVec2 innerMax(max.x - 7.0F, max.y - 7.0F); - if (hasContentColor) { + if (thumbnailTexture != nullptr) { + drawList->AddImageRounded( + thumbnailTexture->GetTexRef(), + innerMin, + innerMax, + ImVec2(0.0F, 0.0F), + ImVec2(1.0F, 1.0F), + IM_COL32_WHITE, + 8.0F + ); + } else if (hasContentColor) { drawList->AddRectFilled(innerMin, innerMax, previewColor, 8.0F); drawList->AddRectFilled( ImVec2(innerMin.x, innerMin.y), @@ -8018,16 +8229,18 @@ void MetaCoreDrawProjectThumbnail( } drawList->AddRect(innerMin, innerMax, IM_COL32(255, 255, 255, 44), 8.0F); - const char* tag = MetaCoreProjectThumbnailTag(type, false); - const ImVec2 tagSize = ImGui::CalcTextSize(tag); - drawList->AddText( - ImVec2( - innerMin.x + ((innerMax.x - innerMin.x) - tagSize.x) * 0.5F, - innerMin.y + ((innerMax.y - innerMin.y) - tagSize.y) * 0.5F - ), - IM_COL32(255, 255, 255, 235), - tag - ); + if (thumbnailTexture == nullptr) { + const char* tag = MetaCoreProjectThumbnailTag(type, false); + const ImVec2 tagSize = ImGui::CalcTextSize(tag); + drawList->AddText( + ImVec2( + innerMin.x + ((innerMax.x - innerMin.x) - tagSize.x) * 0.5F, + innerMin.y + ((innerMax.y - innerMin.y) - tagSize.y) * 0.5F + ), + IM_COL32(255, 255, 255, 235), + tag + ); + } } void MetaCoreDrawProjectBreadcrumb( @@ -8436,7 +8649,12 @@ public: void DrawPanel(MetaCoreEditorContext& editorContext) override { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); + const auto importPipelineService = editorContext.GetModuleRegistry().ResolveService(); + const auto assetAuditService = editorContext.GetModuleRegistry().ResolveService(); if (!assetDatabaseService || !assetDatabaseService->HasProject()) return; + if (importPipelineService != nullptr) { + importPipelineService->Tick(); + } const auto& project = assetDatabaseService->GetProjectDescriptor(); if (editorContext.GetSelectedProjectDirectory().empty() || @@ -8475,8 +8693,50 @@ public: ImGui::TextDisabled("%s", project.RootPath.string().c_str()); ImGui::SameLine(); if (ImGui::Button("刷新")) { - MetaCoreHandleReloadAssets(editorContext); - assetDatabaseService->Refresh(); + if (importPipelineService != nullptr) { + (void)importPipelineService->EnqueueAllChanged(); + } + (void)assetDatabaseService->Refresh(); + } + if (importPipelineService != nullptr) { + const auto importJobs = importPipelineService->GetJobs(); + const auto activeCount = std::count_if(importJobs.begin(), importJobs.end(), [](const auto& job) { + return job.State == MetaCoreImportJobState::Queued || job.State == MetaCoreImportJobState::Running || + job.State == MetaCoreImportJobState::Committing; + }); + ImGui::SameLine(); + const std::string jobButton = "导入任务 (" + std::to_string(activeCount) + ")"; + if (ImGui::Button(jobButton.c_str())) { + ImGui::OpenPopup("ImportJobsPopup"); + } + if (ImGui::BeginPopup("ImportJobsPopup")) { + if (importJobs.empty()) { + ImGui::TextDisabled("没有导入任务"); + } + for (const auto& job : importJobs) { + ImGui::PushID(static_cast(job.Id)); + ImGui::Text("%s", job.SourcePath.generic_string().c_str()); + ImGui::ProgressBar(job.Progress, ImVec2(220.0F, 0.0F), job.Stage.c_str()); + const bool active = job.State == MetaCoreImportJobState::Queued || job.State == MetaCoreImportJobState::Running; + if (active) { + ImGui::SameLine(); + if (ImGui::SmallButton("取消")) { + (void)importPipelineService->CancelJob(job.Id); + } + } else if (job.State == MetaCoreImportJobState::Failed || job.State == MetaCoreImportJobState::Cancelled) { + ImGui::SameLine(); + if (ImGui::SmallButton("重试")) { + (void)importPipelineService->RetryJob(job.Id); + } + } + for (const auto& message : job.Messages) { + ImGui::TextWrapped("[%s] %s", message.Code.c_str(), message.Message.c_str()); + } + ImGui::Separator(); + ImGui::PopID(); + } + ImGui::EndPopup(); + } } ImGui::SameLine(); if (ImGui::Button("新建")) { @@ -8603,7 +8863,8 @@ public: const float thumbnailX = cardMin.x + (cardWidth - thumbnailSize) * 0.5F; const ImVec2 thumbnailMin(thumbnailX, cardMin.y + 10.0F); const ImVec2 thumbnailMax(thumbnailX + thumbnailSize, cardMin.y + 10.0F + thumbnailSize); - MetaCoreDrawProjectThumbnail(entry, project, type, thumbnailMin, thumbnailMax); + const auto thumbnailService = editorContext.GetModuleRegistry().ResolveService(); + MetaCoreDrawProjectThumbnail(entry, project, thumbnailService.get(), type, thumbnailMin, thumbnailMax); const std::string clippedName = MetaCoreProjectEllipsize(displayName, cardWidth - 14.0F); const std::string clippedType = MetaCoreProjectEllipsize(chineseType, cardWidth - 14.0F); @@ -8873,6 +9134,19 @@ public: if (PendingDeleteIsDirectory_) { ImGui::TextColored(ImVec4(1.0F, 0.65F, 0.2F, 1.0F), "文件夹会递归删除。"); } + std::vector blockers; + if (!PendingDeleteIsDirectory_ && assetAuditService != nullptr) { + if (const auto record = assetDatabaseService->FindAssetByRelativePath(PendingDeletePath_); record.has_value()) { + (void)assetAuditService->CanDelete(record->Guid, &blockers); + } + } + if (!blockers.empty()) { + ImGui::TextColored(ImVec4(1.0F, 0.55F, 0.25F, 1.0F), "检测到 %d 个引用,普通删除已阻止:", static_cast(blockers.size())); + for (const auto& blocker : blockers) { + ImGui::BulletText("%s %s", blocker.OwnerPath.generic_string().c_str(), blocker.FieldPath.c_str()); + } + } + ImGui::BeginDisabled(!blockers.empty()); if (ImGui::Button("删除", ImVec2(120, 0))) { const std::filesystem::path deletedPath = PendingDeletePath_.lexically_normal(); if (assetDatabaseService->DeletePath(deletedPath)) { @@ -8889,6 +9163,20 @@ public: } ImGui::CloseCurrentPopup(); } + ImGui::EndDisabled(); + if (!blockers.empty()) { + ImGui::SameLine(); + if (ImGui::Button("强制删除", ImVec2(120, 0))) { + const std::filesystem::path deletedPath = PendingDeletePath_.lexically_normal(); + if (!assetDatabaseService->DeletePath(deletedPath, true)) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "强制删除失败"); + } else if (MetaCoreIsSameOrChildPath(editorContext.GetSelectedAsset().RelativePath, deletedPath)) { + editorContext.ClearSelectedAsset(); + editorContext.ClearSelectedAssetSubId(); + } + ImGui::CloseCurrentPopup(); + } + } ImGui::SameLine(); if (ImGui::Button("取消", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); diff --git a/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp b/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp index 1f7e2e6..1438847 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.cpp @@ -461,9 +461,11 @@ struct MetaCoreGltfMaterialScalarPresence { const std::filesystem::path& relativeSourcePath, std::uint64_t sourceHash, std::vector>& outMeshPayloads, - MetaCoreModelImportSettingsDocument importSettings + MetaCoreModelImportSettingsDocument importSettings, + const std::atomic_bool* cancelRequested ) { MetaCoreModelAssetDocument document; + const auto cancelled = [&]() { return cancelRequested != nullptr && cancelRequested->load(); }; std::vector perfectPayload; { std::uint32_t vCount = 3; @@ -492,7 +494,9 @@ struct MetaCoreGltfMaterialScalarPresence { } const std::string filename = absoluteSourcePath.filename().string(); - if (filename == "Pump.gltf") { + // Filename-specific fixtures from the prototype are disabled deliberately: + // all assets now use the same production parser path. + if (false && filename == "Pump.gltf") { document.AssetType = "model"; document.RootDisplayName = "Pump.gltf"; document.ImporterId = "GltfModelImporter"; @@ -570,7 +574,7 @@ struct MetaCoreGltfMaterialScalarPresence { return document; } - if (filename == "Valve.gltf") { + if (false && filename == "Valve.gltf") { document.AssetType = "model"; document.RootDisplayName = "Valve.gltf"; document.ImporterId = "GltfModelImporter"; @@ -647,7 +651,7 @@ struct MetaCoreGltfMaterialScalarPresence { return document; } - if (filename == "EmbeddedTexture.gltf") { + if (false && filename == "EmbeddedTexture.gltf") { document.AssetType = "model"; document.RootDisplayName = "EmbeddedTexture.gltf"; document.ImporterId = "GltfModelImporter"; @@ -724,7 +728,7 @@ struct MetaCoreGltfMaterialScalarPresence { return document; } - if (filename == "Tank.glb") { + if (false && filename == "Tank.glb") { document.AssetType = "model"; document.RootDisplayName = "Tank.glb"; document.ImporterId = "GltfModelImporter"; @@ -794,53 +798,14 @@ struct MetaCoreGltfMaterialScalarPresence { document.SourceFormat = absoluteSourcePath.extension() == ".glb" ? "glb" : "gltf"; document.ImportSettings = importSettings; - const auto addFallbackSkeleton = [&]() { - document.ImportReport.UsedFallback = true; + const auto reportImportFailure = [&]() { + document.ImportReport.UsedFallback = false; MetaCoreAddImportMessage( document.ImportReport, - MetaCoreImportMessageSeverity::Warning, - "fallback_skeleton", - "源模型未能完整解析,已生成最小占位模型骨架" + MetaCoreImportMessageSeverity::Error, + "gltf_import_failed", + "源模型未能解析;生产导入不会生成占位资产" ); - MetaCoreImportedGltfNodeDocument rootNode; - rootNode.Name = absoluteSourcePath.stem().string(); - rootNode.StableNodePath = rootNode.Name; - rootNode.MeshIndex = 0; - document.Nodes.push_back(std::move(rootNode)); - - MetaCoreImportedGltfMeshDocument meshDocument; - meshDocument.Name = absoluteSourcePath.stem().string(); - meshDocument.PrimitiveCount = 1; - meshDocument.MaterialSlots.push_back(0); - document.Meshes.push_back(std::move(meshDocument)); - - MetaCoreMeshAssetDocument meshAsset; - meshAsset.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid( - relativeSourcePath, - "mesh", - absoluteSourcePath.stem().string(), - 0 - ); - meshAsset.Name = absoluteSourcePath.stem().string(); - meshAsset.StableImportKey = MetaCoreBuildStableImportKey("mesh", meshAsset.Name, 0); - document.GeneratedMeshAssets.push_back(std::move(meshAsset)); - - MetaCoreImportedGltfMaterialDocument materialDocument; - materialDocument.Name = absoluteSourcePath.stem().string() + "_Material"; - document.Materials.push_back(std::move(materialDocument)); - - MetaCoreMaterialAssetDocument materialAsset; - materialAsset.AssetGuid = MetaCoreMakeDeterministicGeneratedAssetGuid( - relativeSourcePath, - "material", - absoluteSourcePath.stem().string(), - 0 - ); - materialAsset.Name = absoluteSourcePath.stem().string() + "_Material"; - materialAsset.StableImportKey = MetaCoreBuildStableImportKey("material", materialAsset.Name, 0); - materialAsset.Metallic = 0.0F; - materialAsset.Roughness = 1.0F; - document.GeneratedMaterialAssets.push_back(std::move(materialAsset)); }; cgltf_data* data = nullptr; @@ -848,8 +813,8 @@ struct MetaCoreGltfMaterialScalarPresence { const std::string sourcePathString = absoluteSourcePath.string(); cgltf_result result = cgltf_parse_file(&options, sourcePathString.c_str(), &data); if (result != cgltf_result_success || data == nullptr) { - MetaCoreGltfTrace("GLTF 解析失败,使用降级骨架"); - addFallbackSkeleton(); + MetaCoreGltfTrace("GLTF 解析失败"); + reportImportFailure(); MetaCoreFinalizeImportReport(document.ImportReport); MetaCoreApplyModelImportSettings(document); return document; @@ -857,16 +822,21 @@ struct MetaCoreGltfMaterialScalarPresence { result = cgltf_load_buffers(&options, data, sourcePathString.c_str()); if (result != cgltf_result_success) { - MetaCoreGltfTrace("GLTF Buffer 加载失败,使用降级骨架"); + MetaCoreGltfTrace("GLTF Buffer 加载失败"); cgltf_free(data); - addFallbackSkeleton(); + reportImportFailure(); MetaCoreFinalizeImportReport(document.ImportReport); MetaCoreApplyModelImportSettings(document); return document; } + if (cancelled()) { + cgltf_free(data); + return document; + } if (data->images_count > 0) { for (std::size_t i = 0; i < data->images_count; ++i) { + if (cancelled()) { cgltf_free(data); return document; } const cgltf_image& image = data->images[i]; MetaCoreImportedGltfTextureDocument textureDoc; textureDoc.SourcePath = image.uri ? std::filesystem::path(image.uri).lexically_normal() : std::filesystem::path{}; @@ -889,6 +859,7 @@ struct MetaCoreGltfMaterialScalarPresence { const std::vector materialScalarPresence = MetaCoreReadGltfMaterialScalarPresence(data); for (std::size_t i = 0; i < data->materials_count; ++i) { + if (cancelled()) { cgltf_free(data); return document; } const cgltf_material& mat = data->materials[i]; const MetaCoreGltfMaterialScalarPresence scalarPresence = i < materialScalarPresence.size() @@ -972,6 +943,7 @@ struct MetaCoreGltfMaterialScalarPresence { if (data->meshes_count > 0) { for (std::size_t i = 0; i < data->meshes_count; ++i) { + if (cancelled()) { cgltf_free(data); return document; } const cgltf_mesh& mesh = data->meshes[i]; MetaCoreImportedGltfMeshDocument meshDoc; meshDoc.Name = MetaCoreGltfDisplayName(mesh.name, "Mesh_" + std::to_string(i)); @@ -998,6 +970,7 @@ struct MetaCoreGltfMaterialScalarPresence { glm::vec3 bmax{-1e30, -1e30, -1e30}; for (std::size_t p = 0; p < mesh.primitives_count; ++p) { + if (cancelled()) { cgltf_free(data); return document; } const cgltf_primitive& prim = mesh.primitives[p]; const std::uint32_t vertexOffset = static_cast(vertices.size()); @@ -1102,6 +1075,7 @@ struct MetaCoreGltfMaterialScalarPresence { if (data->nodes_count > 0) { for (std::size_t i = 0; i < data->nodes_count; ++i) { + if (cancelled()) { cgltf_free(data); return document; } const cgltf_node& node = data->nodes[i]; MetaCoreImportedGltfNodeDocument nodeDoc; if (node.name && strlen(node.name) > 0) { diff --git a/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h b/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h index c91e976..9b21b74 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h +++ b/Source/MetaCoreEditor/Private/MetaCoreGltfImporter.h @@ -4,6 +4,7 @@ #include #include #include +#include namespace MetaCore { @@ -12,7 +13,8 @@ namespace MetaCore { const std::filesystem::path& relativeSourcePath, std::uint64_t sourceHash, std::vector>& outMeshPayloads, - MetaCoreModelImportSettingsDocument importSettings = {} + MetaCoreModelImportSettingsDocument importSettings = {}, + const std::atomic_bool* cancelRequested = nullptr ); } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h index a8f72b6..6491399 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h @@ -71,6 +71,27 @@ struct MetaCoreAssetMetadataDocument { MC_PROPERTY() std::vector Dependencies{}; + + MC_PROPERTY() + std::uint32_t SchemaVersion = 2; + + MC_PROPERTY() + std::uint32_t ImporterVersion = 1; + + MC_PROPERTY() + std::string ImportSettingsJson{}; + + MC_PROPERTY() + std::uint64_t SettingsHash = 0; + + MC_PROPERTY() + std::string ArtifactKey{}; + + MC_PROPERTY() + MetaCoreModelImportResult LastImportResult = MetaCoreModelImportResult::Success; + + MC_PROPERTY() + std::vector LastImportMessages{}; }; MC_STRUCT() @@ -109,6 +130,18 @@ struct MetaCoreCookManifestEntry { MC_PROPERTY() std::uint64_t CookedKey = 0; + + MC_PROPERTY() + std::string ArtifactKey{}; + + MC_PROPERTY() + MetaCoreAssetTargetPlatform TargetPlatform = MetaCoreAssetTargetPlatform::Host; + + MC_PROPERTY() + std::uint32_t GeneratorVersion = 1; + + MC_PROPERTY() + std::vector DependencyClosure{}; }; MC_STRUCT() diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index 4cf3beb..91e2d6f 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -9,12 +9,14 @@ #include "MetaCoreScene/MetaCoreScene.h" #include +#include #include #include #include #include #include #include +#include #include namespace MetaCore { @@ -108,6 +110,47 @@ struct MetaCoreAssetRecord { std::filesystem::path MetaPath{}; std::string ImporterId{}; std::uint64_t SourceHash = 0; + MetaCoreModelImportResult ImportResult = MetaCoreModelImportResult::Success; + std::string ArtifactKey{}; + std::string DiagnosticSummary{}; +}; + +enum class MetaCoreImportJobState { + Queued = 0, + Running, + Committing, + Succeeded, + Warning, + Failed, + Cancelled +}; + +using MetaCoreImportJobId = std::uint64_t; + +struct MetaCoreImportJobSnapshot { + MetaCoreImportJobId Id = 0; + MetaCoreAssetGuid AssetGuid{}; + std::filesystem::path SourcePath{}; + MetaCoreImportJobState State = MetaCoreImportJobState::Queued; + std::string Stage{}; + float Progress = 0.0F; + std::vector Messages{}; + std::uint64_t ElapsedMilliseconds = 0; +}; + +enum class MetaCoreAssetReferenceStatus { + Valid = 0, + Missing, + Unreferenced, + BuildUnreachable +}; + +struct MetaCoreAssetReferenceFinding { + MetaCoreAssetGuid OwnerGuid{}; + std::filesystem::path OwnerPath{}; + std::string FieldPath{}; + MetaCoreAssetGuid TargetGuid{}; + MetaCoreAssetReferenceStatus Status = MetaCoreAssetReferenceStatus::Valid; }; class MetaCoreIReflectionRegistry : public MetaCoreIEditorService { @@ -143,7 +186,7 @@ public: [[nodiscard]] virtual bool RenamePath(const std::filesystem::path& relativePath, const std::filesystem::path& newName) = 0; [[nodiscard]] virtual bool MovePath(const std::filesystem::path& relativePath, const std::filesystem::path& targetDirectory) = 0; [[nodiscard]] virtual bool MovePathTo(const std::filesystem::path& relativePath, const std::filesystem::path& targetRelativePath) = 0; - [[nodiscard]] virtual bool DeletePath(const std::filesystem::path& relativePath) = 0; + [[nodiscard]] virtual bool DeletePath(const std::filesystem::path& relativePath, bool force = false) = 0; [[nodiscard]] virtual std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::optional FindAssetByRelativePath(const std::filesystem::path& relativePath) const = 0; @@ -167,6 +210,37 @@ class MetaCoreIImportPipelineService : public MetaCoreIEditorService { public: [[nodiscard]] virtual bool RefreshImports() = 0; [[nodiscard]] virtual bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) = 0; + [[nodiscard]] virtual MetaCoreImportJobId EnqueueAsset(const MetaCoreAssetGuid& assetGuid, bool force = false) = 0; + [[nodiscard]] virtual std::vector EnqueueAllChanged() = 0; + [[nodiscard]] virtual std::optional GetJob(MetaCoreImportJobId jobId) const = 0; + [[nodiscard]] virtual std::vector GetJobs() const = 0; + virtual bool CancelJob(MetaCoreImportJobId jobId) = 0; + [[nodiscard]] virtual MetaCoreImportJobId RetryJob(MetaCoreImportJobId jobId) = 0; + virtual void Tick() = 0; +}; + +class MetaCoreIDerivedDataCacheService : public MetaCoreIEditorService { +public: + virtual void SetProjectRoot(const std::filesystem::path& projectRoot) = 0; + [[nodiscard]] virtual std::filesystem::path ResolvePath(std::string_view artifactKey, std::string_view suffix = {}) const = 0; + [[nodiscard]] virtual bool Contains(std::string_view artifactKey, std::string_view suffix = {}) const = 0; + [[nodiscard]] virtual bool Put(std::string_view artifactKey, std::span data, std::string_view suffix = {}) = 0; + [[nodiscard]] virtual std::optional> Get(std::string_view artifactKey, std::string_view suffix = {}) const = 0; +}; + +class MetaCoreIThumbnailService : public MetaCoreIEditorService { +public: + virtual void SetProjectRoot(const std::filesystem::path& projectRoot) = 0; + [[nodiscard]] virtual std::filesystem::path RequestThumbnail(const MetaCoreAssetRecord& asset, std::uint32_t size = 128) = 0; + virtual void Tick() = 0; +}; + +class MetaCoreIAssetAuditService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual std::vector AuditAll() const = 0; + [[nodiscard]] virtual std::vector FindReferencesTo(const MetaCoreAssetGuid& assetGuid) const = 0; + [[nodiscard]] virtual bool CanDelete(const MetaCoreAssetGuid& assetGuid, std::vector* blockers = nullptr) const = 0; + [[nodiscard]] virtual bool ReplaceReference(const MetaCoreAssetReferenceFinding& finding, const MetaCoreAssetGuid& replacement) = 0; }; struct MetaCoreResolvedGeneratedAssetRecord { @@ -198,6 +272,16 @@ public: const MetaCoreModelImportSettingsDocument& importSettings ) = 0; + [[nodiscard]] virtual bool SaveTextureImportSettings( + const MetaCoreAssetGuid& textureGuid, + const MetaCoreTextureImportSettingsDocument& importSettings + ) = 0; + + [[nodiscard]] virtual bool SaveMaterialImportSettings( + const MetaCoreAssetGuid& materialGuid, + const MetaCoreMaterialImportSettingsDocument& importSettings + ) = 0; + [[nodiscard]] virtual bool SaveMaterialAsset( const MetaCoreAssetGuid& materialGuid, const MetaCoreMaterialAssetDocument& document diff --git a/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp b/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp index 55f166f..1c91bb4 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreAssetRegistry.cpp @@ -22,6 +22,14 @@ void MetaCoreAssetRegistry::ScanDirectory(const std::filesystem::path& rootPath) continue; } + // .mcasset is derived output, never an import source. Older pipeline + // versions could leave package metadata chains such as + // foo.glb.mcasset.mcmeta; parsing those on every scan both duplicates + // the source asset and makes the chain grow indefinitely. + if (entry.path().stem().extension() == ".mcasset") { + continue; + } + std::ifstream file(entry.path()); if (!file.is_open()) { continue; diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetTypes.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetTypes.h index 9d004cd..e9e4ca0 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetTypes.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreAssetTypes.h @@ -25,6 +25,43 @@ enum class MetaCoreMaterialAlphaMode { Blend }; +MC_ENUM() +enum class MetaCoreTextureColorSpace { + Automatic = 0, + Srgb, + Linear +}; + +MC_ENUM() +enum class MetaCoreTextureUsage { + Automatic = 0, + Color, + Normal, + Data, + HighDynamicRange +}; + +MC_ENUM() +enum class MetaCoreTextureCompression { + None = 0, + PlatformDefault, + BasisUastc +}; + +MC_ENUM() +enum class MetaCoreAssetTargetPlatform { + Host = 0, + LinuxX64, + WindowsX64 +}; + +MC_ENUM() +enum class MetaCoreGeneratedMaterialPolicy { + PreserveImported = 0, + PreserveUserOverrides, + ExtractStandalone +}; + MC_STRUCT() struct MetaCoreTextureAssetDocument { MC_GENERATED_BODY() @@ -61,6 +98,15 @@ struct MetaCoreTextureAssetDocument { MC_PROPERTY() std::string Format = "raw"; + + MC_PROPERTY() + std::int32_t MipCount = 1; + + MC_PROPERTY() + MetaCoreTextureColorSpace ColorSpace = MetaCoreTextureColorSpace::Automatic; + + MC_PROPERTY() + MetaCoreTextureCompression Compression = MetaCoreTextureCompression::None; }; MC_STRUCT() @@ -217,6 +263,49 @@ struct MetaCoreModelImportSettingsDocument { bool PreserveMaterialSlots = true; }; +MC_STRUCT() +struct MetaCoreTextureImportSettingsDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreTextureColorSpace ColorSpace = MetaCoreTextureColorSpace::Automatic; + + MC_PROPERTY() + MetaCoreTextureUsage Usage = MetaCoreTextureUsage::Automatic; + + MC_PROPERTY() + bool GenerateMipmaps = true; + + MC_PROPERTY() + std::int32_t MaxSize = 4096; + + MC_PROPERTY() + MetaCoreTextureCompression Compression = MetaCoreTextureCompression::PlatformDefault; + + MC_PROPERTY() + std::int32_t CompressionQuality = 80; + + MC_PROPERTY() + bool RenormalizeNormalMap = true; +}; + +MC_STRUCT() +struct MetaCoreMaterialImportSettingsDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreMaterialShaderModel ShaderModel = MetaCoreMaterialShaderModel::PbrMetalRough; + + MC_PROPERTY() + bool PreserveAlphaMode = true; + + MC_PROPERTY() + bool PreserveDoubleSided = true; + + MC_PROPERTY() + MetaCoreGeneratedMaterialPolicy GeneratedMaterialPolicy = MetaCoreGeneratedMaterialPolicy::PreserveUserOverrides; +}; + MC_STRUCT() struct MetaCoreImportMessageDocument { MC_GENERATED_BODY() diff --git a/TestProject/Assets/Materials/default_pbr.material.json.mcasset b/TestProject/Assets/Materials/default_pbr.material.json.mcasset new file mode 100644 index 0000000..bfea7cd Binary files /dev/null and b/TestProject/Assets/Materials/default_pbr.material.json.mcasset differ diff --git a/TestProject/Assets/Materials/default_pbr.material.json.mcmeta b/TestProject/Assets/Materials/default_pbr.material.json.mcmeta index b36b46e..7607149 100644 --- a/TestProject/Assets/Materials/default_pbr.material.json.mcmeta +++ b/TestProject/Assets/Materials/default_pbr.material.json.mcmeta @@ -1,8 +1,16 @@ { + "artifact_key": "1b8eb8422ae8a643", "asset_type": "asset", "guid": "7e3d10d5-d46f-4622-9615-25ce0abef1e7", + "import_settings": {}, "importer_id": "BinaryImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/Materials/default_pbr.material.json.mcasset", + "schema_version": 2, + "settings_hash": 11238273277574643119, "source_hash": 12650993429013095480, - "source_path": "Assets/Materials/default_pbr.material.json" + "source_mtime_ns": 1779698310960569594, + "source_path": "Assets/Materials/default_pbr.material.json", + "source_size_bytes": 136 } \ No newline at end of file diff --git a/TestProject/Assets/Models/Cube.glb.mcasset b/TestProject/Assets/Models/Cube.glb.mcasset new file mode 100644 index 0000000..0bc43f7 Binary files /dev/null and b/TestProject/Assets/Models/Cube.glb.mcasset differ diff --git a/TestProject/Assets/Models/Cube.glb.mcmeta b/TestProject/Assets/Models/Cube.glb.mcmeta index ba82544..53651d5 100644 --- a/TestProject/Assets/Models/Cube.glb.mcmeta +++ b/TestProject/Assets/Models/Cube.glb.mcmeta @@ -1,23 +1,48 @@ { + "artifact_key": "ba190289025adbd3", "asset_type": "model", + "dependencies": [ + "9cd50d41-df30-26ff-da7e-e01bb57f7135", + "d0a9dfe6-52cb-c306-8384-085ea2996675" + ], "guid": "e85ce6d3-12a9-475a-a224-9da4035ce22a", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_messages": [ + { + "code": "ambiguous_subasset_identity", + "message": "material 子资产名称为空或重复,GUID 稳定性退回序号匹配", + "severity": 1 + } + ], + "last_import_result": 1, "package_path": "Assets/Models/Cube.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 2668036311376285448, + "source_mtime_ns": 1779958286058240134, "source_path": "Assets/Models/Cube.glb", + "source_size_bytes": 1936, "sub_assets": [ { "guid": "9cd50d41-df30-26ff-da7e-e01bb57f7135", "index": 0, "name": "Cube", - "stable_import_key": "", + "stable_import_key": "mesh:Cube:0", "type": "mesh" }, { - "guid": "20a8eca7-ae5b-8ed9-c7a8-b64b59567283", + "guid": "d0a9dfe6-52cb-c306-8384-085ea2996675", "index": 0, - "name": "Cube_Material", - "stable_import_key": "", + "name": "Material", + "stable_import_key": "material:Material:0", "type": "material" } ] diff --git a/TestProject/Assets/Models/Plane.glb.mcasset b/TestProject/Assets/Models/Plane.glb.mcasset new file mode 100644 index 0000000..92da414 Binary files /dev/null and b/TestProject/Assets/Models/Plane.glb.mcasset differ diff --git a/TestProject/Assets/Models/Plane.glb.mcmeta b/TestProject/Assets/Models/Plane.glb.mcmeta index c70c88b..4ce7755 100644 --- a/TestProject/Assets/Models/Plane.glb.mcmeta +++ b/TestProject/Assets/Models/Plane.glb.mcmeta @@ -1,24 +1,41 @@ { + "artifact_key": "0f17577685a7ac42", "asset_type": "model", + "dependencies": [ + "a718a4d3-f5ce-7f3a-6fe6-877661f2ea55" + ], "guid": "75068577-37e3-4a9d-954a-54251c667933", + "import_settings": { + "preserve_empty_nodes": true, + "preserve_hierarchy": true, + "preserve_material_slots": true, + "scale_factor": 1.0, + "up_axis": 0 + }, "importer_id": "GltfModelImporter", + "importer_version": 2, + "last_import_messages": [ + { + "code": "ambiguous_subasset_identity", + "message": "mesh 子资产名称为空或重复,GUID 稳定性退回序号匹配", + "severity": 1 + } + ], + "last_import_result": 1, "package_path": "Assets/Models/Plane.glb.mcasset", + "schema_version": 2, + "settings_hash": 2032719019511346891, "source_hash": 7189019093832532692, + "source_mtime_ns": 1779958286061239979, "source_path": "Assets/Models/Plane.glb", + "source_size_bytes": 1036, "sub_assets": [ { - "guid": "804bca6d-b9ea-f7af-3f0a-f078d5de0d45", + "guid": "a718a4d3-f5ce-7f3a-6fe6-877661f2ea55", "index": 0, - "name": "Plane", - "stable_import_key": "", + "name": "平面", + "stable_import_key": "mesh:平面:0", "type": "mesh" - }, - { - "guid": "6c337003-dc9a-da7c-d685-6d27c673046a", - "index": 0, - "name": "Plane_Material", - "stable_import_key": "", - "type": "material" } ] } \ No newline at end of file diff --git a/TestProject/Assets/UI/main_menu.rcss.mcasset b/TestProject/Assets/UI/main_menu.rcss.mcasset new file mode 100644 index 0000000..1570f42 Binary files /dev/null and b/TestProject/Assets/UI/main_menu.rcss.mcasset differ diff --git a/TestProject/Assets/UI/main_menu.rcss.mcmeta b/TestProject/Assets/UI/main_menu.rcss.mcmeta index eb75bf9..0f32a98 100644 --- a/TestProject/Assets/UI/main_menu.rcss.mcmeta +++ b/TestProject/Assets/UI/main_menu.rcss.mcmeta @@ -1,8 +1,16 @@ { + "artifact_key": "877767f4951cc2ab", "asset_type": "ui_stylesheet", "guid": "9bd0aa89-1f68-4a4d-b9ed-1fcf1dc6180d", + "import_settings": {}, "importer_id": "UiStylesheetImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/UI/main_menu.rcss.mcasset", + "schema_version": 2, + "settings_hash": 11238273277574643119, "source_hash": 8787559501102875347, - "source_path": "Assets/UI/main_menu.rcss" + "source_mtime_ns": 1779698310960569594, + "source_path": "Assets/UI/main_menu.rcss", + "source_size_bytes": 162 } \ No newline at end of file diff --git a/TestProject/Assets/UI/main_menu.rml.mcasset b/TestProject/Assets/UI/main_menu.rml.mcasset new file mode 100644 index 0000000..2a78451 Binary files /dev/null and b/TestProject/Assets/UI/main_menu.rml.mcasset differ diff --git a/TestProject/Assets/UI/main_menu.rml.mcmeta b/TestProject/Assets/UI/main_menu.rml.mcmeta index 9af5147..f279666 100644 --- a/TestProject/Assets/UI/main_menu.rml.mcmeta +++ b/TestProject/Assets/UI/main_menu.rml.mcmeta @@ -1,8 +1,16 @@ { + "artifact_key": "3d142bee5d424926", "asset_type": "ui_document", "guid": "7fa1231a-4620-479f-8238-03531e78202a", + "import_settings": {}, "importer_id": "UiDocumentImporter", + "importer_version": 2, + "last_import_result": 0, "package_path": "Assets/UI/main_menu.rml.mcasset", + "schema_version": 2, + "settings_hash": 11238273277574643119, "source_hash": 11300316873669048992, - "source_path": "Assets/UI/main_menu.rml" + "source_mtime_ns": 1779698310960569594, + "source_path": "Assets/UI/main_menu.rml", + "source_size_bytes": 200 } \ No newline at end of file diff --git a/docs/designs/metacore-asset-pipeline-contract.md b/docs/designs/metacore-asset-pipeline-contract.md new file mode 100644 index 0000000..e441ee6 --- /dev/null +++ b/docs/designs/metacore-asset-pipeline-contract.md @@ -0,0 +1,47 @@ +# MetaCore 资产导入契约(R2) + +本文定义 R2 首版的正式输入范围、诊断语义和缓存身份。未列为“支持”的格式不得产生可被 Registry 当作成功结果的 `.mcasset`。 + +## 支持矩阵 + +| 输入 | 状态 | 说明 | +| --- | --- | --- | +| glTF 2.0 `.gltf` | 支持 | JSON、外部 Buffer、Data URI、外部或嵌入图片、节点层级、Mesh、PBR Material、UV 和动画元数据 | +| glTF 2.0 `.glb` | 支持 | 与 `.gltf` 使用相同导入契约;容器、JSON 或 Buffer 损坏时失败 | +| PNG/JPEG/TGA/BMP/PSD/GIF/HDR/PPM | 支持 | 进入统一纹理解码、色彩/法线处理、尺寸限制和 Mipmap 链 | +| MetaCore Material | 支持 | `.mcmaterial.json`,引用只保存 Asset GUID | +| FBX `.fbx` | 不支持 | 写入失败诊断,不写占位 `.mcasset` | +| Wavefront OBJ `.obj` | 不支持 | 写入失败诊断,不写占位 `.mcasset` | + +glTF 合法但尚未实现的可选能力应产生 Warning 并保留可用部分;无法解析容器/JSON、无法加载必需 Buffer 或无法解码必需图片应产生 Error。Error 不覆盖上一次成功产物。 + +## 稳定诊断码 + +| 代码 | 级别 | 含义 | +| --- | --- | --- | +| `gltf_import_failed` | Error | glTF/GLB 解析或必需 Buffer 加载失败 | +| `unsupported_model_format` | Error | FBX/OBJ 在当前版本不受支持 | +| `missing_texture_reference` | Warning | PBR Base Color 贴图索引无效 | +| `missing_metallic_roughness_texture_reference` | Warning | 金属粗糙贴图索引无效 | +| `subasset_guid_ambiguity` | Warning | 重名或匿名子资产无法按唯一规范名称匹配旧 GUID | +| `generated_root_node` | Info | 源文件没有节点层级,导入器生成逻辑根节点 | + +诊断码属于持久化契约,UI 文案可以本地化,但代码含义不得在不提升 Importer Version 的情况下改变。 + +## 元数据与派生数据身份 + +`.mcmeta` Schema 2 保留原 GUID,并记录 Importer Version、规范化 Import Settings、Settings Hash、Artifact Key、最近导入结果和诊断。旧元数据读取时补默认值,不因升级生成新 GUID。 + +Artifact/DDC Key 的输入集合为:源内容 Hash、规范化 Settings Hash、Importer/生成器版本、目标平台和工具版本。派生包位于 `Library/DerivedData`,缩略图位于 `Library/Thumbnails`,两者均可删除并重建,不进入版本控制。 + +Linux 纹理压缩后端使用 vcpkg 提供的 Basis Universal 编码器生成 UASTC KTX2。颜色纹理按 sRGB 编码,数据与法线纹理保持 Linear,法线缩放后可重新归一化;关闭压缩时保留未压缩 RGBA 像素链。编码器进程属于可取消的导入阶段,进入原子提交后不再中断。 + +子资产首先按稳定导入键匹配;键变化时,仅在“类型 + 规范名称”在新旧集合中都唯一的情况下恢复旧 GUID。歧义项使用稳定序号并报告 Warning。 + +## 提交与失败规则 + +- Hash、模型解析、纹理解码/压缩和缩略图生成在后台作业执行。 +- Registry、依赖图、编辑器事件与热重载只在串行提交边界更新。 +- 作业在原子提交前可取消;进入提交后完成本次提交。 +- 失败或取消不得覆盖上一次成功 `.mcasset`;失败状态和诊断仍写入 `.mcmeta`。 +- 删除默认执行反向引用预检;只有显式强制删除可以留下失效 GUID。 diff --git a/docs/designs/metacore-mature-engine-gap-and-work-plan.md b/docs/designs/metacore-mature-engine-gap-and-work-plan.md index 90f44ea..85df319 100644 --- a/docs/designs/metacore-mature-engine-gap-and-work-plan.md +++ b/docs/designs/metacore-mature-engine-gap-and-work-plan.md @@ -107,14 +107,16 @@ MetaCore 已经超过单纯编辑器 Demo 阶段,当前具备: #### 需要做的事 -- [ ] 明确 glTF/GLB 的正式支持矩阵和导入错误规范。 -- [ ] 删除或明确标识通用占位导入结果,避免占位资产被当成成功导入。 -- [ ] 建立 Model、Texture、Material 的版本化 Import Settings。 -- [ ] 实现纹理色彩空间、Mipmap、尺寸、压缩和法线贴图设置。 -- [ ] 建立缩略图与预览缓存,并定义缓存失效条件。 -- [ ] 把大型模型解析、纹理处理和派生数据生成移出 UI 主线程。 -- [ ] 增加资源引用检查、孤立资源检查和 GUID/路径修复工具。 -- [ ] 建立导入黄金样本库,覆盖层级、材质、贴图、动画和损坏输入。 +- [x] 明确 glTF/GLB 的正式支持矩阵和导入错误规范(见 [资产导入契约](metacore-asset-pipeline-contract.md))。 +- [x] 删除或明确标识通用占位导入结果,避免占位资产被当成成功导入。 +- [x] 建立 Model、Texture、Material 的版本化 Import Settings。 +- [x] 实现纹理色彩空间、Mipmap、尺寸、压缩和法线贴图设置;Linux 生产链使用 Basis Universal UASTC/KTX2,`None` 保留未压缩像素链。 +- [x] 建立缩略图与预览缓存,并定义缓存失效条件;纹理、材质球和固定相机模型预览均由后台服务生成并持久化。 +- [x] 把大型模型解析、纹理处理和派生数据生成移出 UI 主线程;解析循环和 Basis 编码支持协作式取消,原子提交阶段不可中断。 +- [x] 增加资源引用检查、孤立资源检查和 GUID/路径修复工具,并报告失效引用、无入边资产和构建不可达资产。 +- [x] 建立独立 Assets 黄金回归目标,覆盖 glTF 外部 Buffer、模型 Mesh 产物、纹理 KTX2、缩略图、损坏输入、不支持格式和构建不可达分析;层级、材质、贴图和动画覆盖继续由 Smoke 回归验证。 + +> 实施状态(2026-07-14):R2 的导入契约、版本化设置、DDC、事务化后台作业、UASTC/KTX2 纹理链、持久化预览、Asset Index、引用审计、安全删除、构建不可达分析及独立黄金回归均已完成。当前验收范围按项目决策锁定为 Linux;Windows 兼容与平台门禁不作为本问题的关闭条件,后续单独恢复。 #### 验收标准 diff --git a/tests/FilamentImGuiDemo.cpp b/tests/FilamentImGuiDemo.cpp index 1f7efe0..a035600 100644 --- a/tests/FilamentImGuiDemo.cpp +++ b/tests/FilamentImGuiDemo.cpp @@ -1,171 +1,128 @@ -#include -#include -#include -#include #include -#include -#include -#include -#include +#include #include +#include +#include #include +#include +#include +#include +#include #include -#include "MetaCoreRender/MetaCoreImGuiHelper.h" #include #include + +#include "MetaCoreRender/MetaCoreImGuiHelper.h" + +#include #include -#include using namespace filament; -LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { - switch (message) { - case WM_DESTROY: - PostQuitMessage(0); - return 0; - } - return DefWindowProc(hWnd, message, wParam, lParam); -} - int main() { - std::cout << "[Demo] Starting..." << std::endl; - - HINSTANCE hInstance = GetModuleHandle(nullptr); - int nCmdShow = SW_SHOW; + constexpr std::uint32_t displayWidth = 1280; + constexpr std::uint32_t displayHeight = 720; + constexpr std::uint32_t viewportWidth = 800; + constexpr std::uint32_t viewportHeight = 600; - // 1. 注册窗口类 - std::cout << "[Demo] Registering window class..." << std::endl; - WNDCLASSEXW wcex = {0}; - wcex.cbSize = sizeof(WNDCLASSEXW); - wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; - wcex.lpfnWndProc = WndProc; - wcex.hInstance = hInstance; - wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); - wcex.lpszClassName = L"FilamentImGuiDemoClass"; - RegisterClassExW(&wcex); - - // 2. 创建窗口 - std::cout << "[Demo] Creating window..." << std::endl; - HWND hWnd = CreateWindowExW(0, L"FilamentImGuiDemoClass", L"Filament + ImGui 离屏渲染最小化 Demo", WS_OVERLAPPEDWINDOW, - CW_USEDEFAULT, CW_USEDEFAULT, 1280, 720, nullptr, nullptr, hInstance, nullptr); - - if (!hWnd) { - std::cout << "[Demo] Failed to create window!" << std::endl; - return -1; - } - - ShowWindow(hWnd, nCmdShow); - UpdateWindow(hWnd); - - // 3. 初始化 Filament - std::cout << "[Demo] Creating Filament Engine (OpenGL)..." << std::endl; + std::cout << "[Demo] Creating headless Filament engine..." << std::endl; + // uiBlit currently ships OpenGL shader material data. The swap chain is + // offscreen, while Filament's Linux OpenGL platform still requires X11. Engine* engine = Engine::create(Engine::Backend::OPENGL); - if (!engine) { - std::cout << "[Demo] Failed to create Engine!" << std::endl; - return -1; + if (engine == nullptr) { + std::cerr << "[Demo] Failed to create Filament engine." << std::endl; + return 1; } - std::cout << "[Demo] Creating Renderer..." << std::endl; Renderer* renderer = engine->createRenderer(); - - std::cout << "[Demo] Creating SwapChain..." << std::endl; - SwapChain* swapChain = engine->createSwapChain(hWnd); - - std::cout << "[Demo] Creating Scene..." << std::endl; + SwapChain* swapChain = engine->createSwapChain(displayWidth, displayHeight); Scene* scene = engine->createScene(); - - std::cout << "[Demo] Creating View..." << std::endl; - View* view = engine->createView(); - view->setScene(scene); + View* sceneView = engine->createView(); + View* uiView = engine->createView(); + if (renderer == nullptr || swapChain == nullptr || scene == nullptr || + sceneView == nullptr || uiView == nullptr) { + std::cerr << "[Demo] Failed to create Filament headless resources." << std::endl; + Engine::destroy(&engine); + return 1; + } - std::cout << "[Demo] Creating Camera..." << std::endl; utils::Entity cameraEntity = utils::EntityManager::get().create(); Camera* camera = engine->createCamera(cameraEntity); - view->setCamera(camera); - view->setViewport({0, 0, 1280, 720}); + sceneView->setScene(scene); + sceneView->setCamera(camera); - std::cout << "[Demo] Creating Skybox..." << std::endl; - Skybox* skybox = Skybox::Builder().color({1.0f, 0.0f, 1.0f, 1.0f}).build(*engine); + Skybox* skybox = Skybox::Builder() + .color({1.0F, 0.0F, 1.0F, 1.0F}) + .build(*engine); scene->setSkybox(skybox); - // 4. 创建离屏 RenderTarget - std::cout << "[Demo] Creating offscreen texture..." << std::endl; Texture* viewportTexture = Texture::Builder() - .width(800).height(600).levels(1) + .width(viewportWidth) + .height(viewportHeight) + .levels(1) .usage(Texture::Usage::COLOR_ATTACHMENT | Texture::Usage::SAMPLEABLE) .format(Texture::InternalFormat::RGBA8) .build(*engine); - - std::cout << "[Demo] Creating RenderTarget..." << std::endl; RenderTarget* renderTarget = RenderTarget::Builder() .texture(RenderTarget::AttachmentPoint::COLOR, viewportTexture) .build(*engine); - std::cout << "[Demo] Creating offscreen View..." << std::endl; - View* offscreenView = engine->createView(); - offscreenView->setScene(scene); - offscreenView->setCamera(camera); - offscreenView->setRenderTarget(renderTarget); - offscreenView->setViewport({0, 0, 800, 600}); - offscreenView->setPostProcessingEnabled(true); + sceneView->setRenderTarget(renderTarget); + sceneView->setViewport({0, 0, viewportWidth, viewportHeight}); + sceneView->setPostProcessingEnabled(true); + uiView->setViewport({0, 0, displayWidth, displayHeight}); + uiView->setPostProcessingEnabled(false); - // 5. 初始化 ImGui - std::cout << "[Demo] Initializing ImGui..." << std::endl; IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); - io.DisplaySize = ImVec2(1280, 720); + io.DisplaySize = ImVec2(static_cast(displayWidth), static_cast(displayHeight)); + io.DeltaTime = 1.0F / 60.0F; - std::cout << "[Demo] Creating UI View..." << std::endl; - View* uiView = engine->createView(); - uiView->setViewport({0, 0, 1280, 720}); - uiView->setPostProcessingEnabled(false); + { + MetaCore::MetaCoreImGuiHelper imguiHelper(engine, uiView, "", ImGui::GetCurrentContext()); + imguiHelper.setDisplaySize(displayWidth, displayHeight); - std::cout << "[Demo] Creating MetaCoreImGuiHelper..." << std::endl; - MetaCore::MetaCoreImGuiHelper imguiHelper(engine, uiView, "C:\\Windows\\Fonts\\msyh.ttc"); + // This is a deterministic offscreen smoke demo rather than an interactive + // platform window. A few frames are enough to exercise both render paths. + for (int frame = 0; frame < 3; ++frame) { + if (!renderer->beginFrame(swapChain)) { + continue; + } - std::cout << "[Demo] Setting Display Size..." << std::endl; - imguiHelper.setDisplaySize(1280, 720); + renderer->render(sceneView); - std::cout << "[Demo] Entering message loop..." << std::endl; - MSG msg = {0}; - bool quit = false; - while (!quit) { - while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { - if (msg.message == WM_QUIT) quit = true; - TranslateMessage(&msg); - DispatchMessage(&msg); - } - - if (renderer->beginFrame(swapChain)) { - // 1. 渲染离屏 3D 场景 - renderer->render(offscreenView); - - // 2. 构建 ImGui 界面 ImGui::NewFrame(); - - ImGui::SetNextWindowSize(ImVec2(820, 640), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(820.0F, 640.0F), ImGuiCond_Always); ImGui::Begin("3D Viewport"); - - ImGui::Image((void*)viewportTexture, ImVec2(800, 600), ImVec2(0, 1), ImVec2(1, 0)); - + ImGui::Image( + reinterpret_cast(viewportTexture), + ImVec2(static_cast(viewportWidth), static_cast(viewportHeight)), + ImVec2(0.0F, 1.0F), + ImVec2(1.0F, 0.0F) + ); ImGui::End(); - ImGui::Render(); - // 3. 处理 ImGui 命令 imguiHelper.processImGuiCommands(ImGui::GetDrawData(), io); - - // 4. 渲染 UI renderer->render(uiView); - renderer->endFrame(); } - Sleep(16); } - std::cout << "[Demo] Cleaning up..." << std::endl; - // 省略详细清理,由操作系统回收 - + ImGui::DestroyContext(); + engine->destroy(renderTarget); + engine->destroy(viewportTexture); + engine->destroy(skybox); + engine->destroy(sceneView); + engine->destroy(uiView); + engine->destroy(scene); + engine->destroyCameraComponent(cameraEntity); + utils::EntityManager::get().destroy(cameraEntity); + engine->destroy(renderer); + engine->destroy(swapChain); + Engine::destroy(&engine); + + std::cout << "[Demo] Headless render completed." << std::endl; return 0; } diff --git a/tests/MetaCoreAssetPipelineTests.cpp b/tests/MetaCoreAssetPipelineTests.cpp new file mode 100644 index 0000000..92ad3c4 --- /dev/null +++ b/tests/MetaCoreAssetPipelineTests.cpp @@ -0,0 +1,165 @@ +#include "MetaCoreEditor/MetaCoreBuiltinModules.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Expect(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void WriteProject(const std::filesystem::path& root) { + std::filesystem::create_directories(root / "Assets"); + std::filesystem::create_directories(root / "Scenes"); + std::ofstream project(root / "MetaCore.project.json"); + project << R"({"name":"AssetGolden","version":"1.0","scenes":["Scenes/Main.mcscene.json"],"startup_scene":"Scenes/Main.mcscene.json"})"; +} + +void WriteGoldenInputs(const std::filesystem::path& root) { + std::ofstream texture(root / "Assets/Color.ppm", std::ios::binary); + texture << "P6\n8 8\n255\n"; + for (int pixel = 0; pixel < 64; ++pixel) { + const char rgb[3]{static_cast(pixel * 3), static_cast(128), static_cast(255 - pixel * 3)}; + texture.write(rgb, sizeof(rgb)); + } + std::ofstream(root / "Assets/Broken.glb", std::ios::binary) << "not-a-glb"; + std::ofstream(root / "Assets/Unsupported.obj") << "o Unsupported\n"; + std::ofstream(root / "Assets/Unused.mcmaterial.json") << R"({"BaseColor":[0.4,0.5,0.6]})"; + { + std::ofstream buffer(root / "Assets/Triangle.bin", std::ios::binary); + const float positions[]{-1.0F, -1.0F, 0.0F, 1.0F, -1.0F, 0.0F, 0.0F, 1.0F, 0.0F}; + const std::uint16_t indices[]{0, 1, 2}; + buffer.write(reinterpret_cast(positions), sizeof(positions)); + buffer.write(reinterpret_cast(indices), sizeof(indices)); + } + std::ofstream(root / "Assets/Triangle.gltf") << R"({ + "asset":{"version":"2.0"},"scene":0,"scenes":[{"nodes":[0]}], + "nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"indices":1}]}], + "buffers":[{"uri":"Triangle.bin","byteLength":42}], + "bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":6}], + "accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"}] + })"; + std::ofstream(root / "Scenes/Main.mcscene.json") << R"({"name":"Main","objects":[]})"; +} + +} // namespace + +int main() { + const auto root = std::filesystem::temp_directory_path() / "MetaCoreAssetPipelineGolden"; + std::filesystem::remove_all(root); + WriteProject(root); + WriteGoldenInputs(root); + setenv("METACORE_PROJECT_PATH", root.string().c_str(), 1); + setenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP", "1", 1); + + try { + MetaCore::MetaCoreEditorModuleRegistry registry; + auto module = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + module->Startup(registry); + + const auto database = registry.ResolveService(); + const auto packages = registry.ResolveService(); + const auto imports = registry.ResolveService(); + const auto audit = registry.ResolveService(); + const auto thumbnails = registry.ResolveService(); + Expect(database != nullptr && packages != nullptr && imports != nullptr && audit != nullptr && thumbnails != nullptr, "R2 services must resolve"); + + Expect( + !std::filesystem::exists(root / "Assets/Unused.mcmaterial.json.mcasset"), + "native JSON material must not generate a recursive .mcasset package" + ); + Expect(std::none_of(database->GetAssetRegistry().begin(), database->GetAssetRegistry().end(), [&](const auto& record) { + if (record.RelativePath.extension() != ".mcasset") return false; + auto source = root / record.RelativePath; + source.replace_extension(); + return std::filesystem::is_regular_file(source); + }), "derived packages must not be registered as duplicate source assets"); + + const auto texture = database->FindAssetByRelativePath("Assets/Color.ppm"); + Expect(texture.has_value(), "golden texture must be registered"); + Expect(texture->ImportResult != MetaCore::MetaCoreModelImportResult::Error, "golden texture import must succeed"); + const auto texturePackage = packages->ReadPackage(root / texture->PackagePath); + Expect(texturePackage.has_value() && texturePackage->PayloadSections.size() > 1, "texture package must contain KTX2 payload"); + const auto& bytes = texturePackage->PayloadSections[1]; + constexpr unsigned char signature[]{0xAB, 'K', 'T', 'X', ' ', '2', '0', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A}; + Expect(bytes.size() >= sizeof(signature), "KTX2 payload must contain identifier"); + for (std::size_t index = 0; index < sizeof(signature); ++index) { + Expect(std::to_integer(bytes[index]) == signature[index], "texture payload must be KTX2"); + } + + const auto broken = database->FindAssetByRelativePath("Assets/Broken.glb"); + const auto unsupported = database->FindAssetByRelativePath("Assets/Unsupported.obj"); + Expect(broken.has_value() && broken->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "broken GLB must fail"); + Expect(unsupported.has_value() && unsupported->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "OBJ must be unsupported"); + Expect(!std::filesystem::exists(root / "Assets/Broken.glb.mcasset"), "broken GLB must not create placeholder package"); + Expect(!std::filesystem::exists(root / "Assets/Unsupported.obj.mcasset"), "OBJ must not create placeholder package"); + const std::size_t jobCountBeforeNoOpRefresh = imports->GetJobs().size(); + Expect(database->Refresh(), "no-op refresh must succeed"); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Expect( + imports->GetJobs().size() == jobCountBeforeNoOpRefresh, + "failed unsupported assets must not be enqueued forever just because no package exists" + ); + + const auto model = database->FindAssetByRelativePath("Assets/Triangle.gltf"); + Expect(model.has_value() && model->ImportResult != MetaCore::MetaCoreModelImportResult::Error, "golden glTF must import"); + const auto modelPackage = packages->ReadPackage(root / model->PackagePath); + Expect(modelPackage.has_value() && modelPackage->PayloadSections.size() > 1, "model package must persist mesh payloads"); + std::filesystem::path modelThumbnail; + for (int attempt = 0; attempt < 100 && modelThumbnail.empty(); ++attempt) { + modelThumbnail = thumbnails->RequestThumbnail(*model, 96); + if (modelThumbnail.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Expect(!modelThumbnail.empty() && std::filesystem::file_size(modelThumbnail) > 100, "model geometry thumbnail must be generated"); + + const auto oversizedModelPath = root / "Assets/Oversized.glb"; + std::ofstream(oversizedModelPath, std::ios::binary).put('\0'); + std::filesystem::resize_file(oversizedModelPath, 64ULL * 1024ULL * 1024ULL + 1ULL); + auto oversizedModel = *model; + oversizedModel.Guid = MetaCore::MetaCoreAssetGuid::Generate(); + oversizedModel.RelativePath = "Assets/Oversized.glb"; + oversizedModel.SourcePath = oversizedModel.RelativePath; + oversizedModel.ArtifactKey = "oversized-model-thumbnail-fallback"; + std::filesystem::path oversizedThumbnail; + for (int attempt = 0; attempt < 100 && oversizedThumbnail.empty(); ++attempt) { + oversizedThumbnail = thumbnails->RequestThumbnail(oversizedModel, 96); + if (oversizedThumbnail.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Expect( + !oversizedThumbnail.empty() && std::filesystem::file_size(oversizedThumbnail) > 100, + "oversized models must use a bounded fallback thumbnail instead of parsing the full source" + ); + std::filesystem::remove(oversizedModelPath); + + const auto findings = audit->AuditAll(); + Expect(std::any_of(findings.begin(), findings.end(), [](const auto& finding) { + return finding.Status == MetaCore::MetaCoreAssetReferenceStatus::BuildUnreachable; + }), "audit must report build-unreachable assets"); + + module->Shutdown(registry); + registry.ShutdownServices(); + unsetenv("METACORE_PROJECT_PATH"); + unsetenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP"); + std::filesystem::remove_all(root); + std::cout << "MetaCoreAssetPipelineTests passed\n"; + return 0; + } catch (const std::exception& exception) { + unsetenv("METACORE_PROJECT_PATH"); + unsetenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP"); + std::filesystem::remove_all(root); + std::cerr << "MetaCoreAssetPipelineTests failed: " << exception.what() << '\n'; + return 1; + } +} diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index f06214f..fc821a6 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -2115,10 +2115,14 @@ void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() { const auto packageService = moduleRegistry.ResolveService(); const auto reflectionRegistry = moduleRegistry.ResolveService(); const auto assetEditingService = moduleRegistry.ResolveService(); + const auto derivedDataCache = moduleRegistry.ResolveService(); + const auto importPipeline = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(packageService != nullptr, "应解析到 PackageService"); MetaCoreExpect(reflectionRegistry != nullptr, "应解析到 ReflectionRegistry"); MetaCoreExpect(assetEditingService != nullptr, "应解析到 AssetEditingService"); + MetaCoreExpect(derivedDataCache != nullptr, "应解析到 DerivedDataCacheService"); + MetaCoreExpect(importPipeline != nullptr, "应解析到 ImportPipelineService"); const auto& project = assetDatabase->GetProjectDescriptor(); MetaCoreExpect(project.RuntimePath.filename() == "ConfigRuntime", "项目应读取 runtime_directory"); @@ -2140,6 +2144,9 @@ void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() { input >> metaJson; MetaCoreExpect(metaJson.value("source_size_bytes", 0ULL) > 0ULL, "gltf mcmeta 应记录源文件大小快速戳"); MetaCoreExpect(metaJson.contains("source_mtime_ns"), "gltf mcmeta 应记录源文件 mtime 快速戳"); + MetaCoreExpect(metaJson.value("schema_version", 0U) >= 2U, "gltf mcmeta 应使用版本化元数据契约"); + MetaCoreExpect(metaJson.value("settings_hash", 0ULL) != 0ULL, "gltf mcmeta 应记录规范化设置 Hash"); + MetaCoreExpect(!metaJson.value("artifact_key", std::string{}).empty(), "gltf mcmeta 应记录完整 Artifact Key"); } const auto embeddedTextureRecord = @@ -2156,27 +2163,36 @@ void MetaCoreTestProjectDescriptorAndGltfImporterSkeleton() { const std::filesystem::path glbMeta = tempProjectRoot / "Assets" / "Tank.glb.mcmeta"; const std::filesystem::path glbPackage = tempProjectRoot / "Assets" / "Tank.glb.mcasset"; MetaCoreExpect(std::filesystem::exists(glbMeta), "glb 应生成 mcmeta 文件"); - MetaCoreExpect(std::filesystem::exists(glbPackage), "glb 应生成轻量 mcasset 元数据包"); - - std::error_code removeError; - std::filesystem::remove(glbPackage, removeError); - MetaCoreExpect(!std::filesystem::exists(glbPackage), "测试前应能删除 glb mcasset"); + MetaCoreExpect(glbRecord->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "损坏 glb 应报告导入失败"); + MetaCoreExpect(!std::filesystem::exists(glbPackage), "损坏 glb 不得生成占位成功包"); MetaCoreExpect(assetDatabase->Refresh(), "缺失 glb mcasset 时刷新应成功"); - MetaCoreExpect(std::filesystem::exists(glbPackage), "source hash 未变但 glb mcasset 缺失时应自动补包"); + MetaCoreExpect(!std::filesystem::exists(glbPackage), "重复刷新不得为损坏 glb 生成占位包"); const auto objRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Crate.obj"); MetaCoreExpect(objRecord.has_value(), "应能找到 obj 资源记录"); MetaCoreExpect(objRecord->ImporterId == "ModelImporter", "obj 应使用通用 ModelImporter"); MetaCoreExpect(objRecord->Type == "model", "obj 资源类型应为 model"); + MetaCoreExpect(objRecord->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "obj 在本阶段应明确报告不支持"); const std::filesystem::path objMeta = tempProjectRoot / "Assets" / "Crate.obj.mcmeta"; MetaCoreExpect(std::filesystem::exists(objMeta), "obj 应生成 mcmeta 文件"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Crate.obj.mcasset"), "obj 不得生成占位成功包"); const auto fbxRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Robot.fbx"); MetaCoreExpect(fbxRecord.has_value(), "应能找到 fbx 资源记录"); MetaCoreExpect(fbxRecord->ImporterId == "ModelImporter", "fbx 应使用通用 ModelImporter"); MetaCoreExpect(fbxRecord->Type == "model", "fbx 资源类型应为 model"); + MetaCoreExpect(fbxRecord->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "fbx 在本阶段应明确报告不支持"); const std::filesystem::path fbxMeta = tempProjectRoot / "Assets" / "Robot.fbx.mcmeta"; MetaCoreExpect(std::filesystem::exists(fbxMeta), "fbx 应生成 mcmeta 文件"); + MetaCoreExpect(!std::filesystem::exists(tempProjectRoot / "Assets" / "Robot.fbx.mcasset"), "fbx 不得生成占位成功包"); + + const std::array cachePayload{ + std::byte{0x4D}, std::byte{0x43}, std::byte{0x44}, std::byte{0x44} + }; + MetaCoreExpect(derivedDataCache->Put("smoke-stable-key", cachePayload, ".bin"), "DDC 应能原子写入派生数据"); + const auto cachedPayload = derivedDataCache->Get("smoke-stable-key", ".bin"); + MetaCoreExpect(cachedPayload.has_value() && *cachedPayload == std::vector(cachePayload.begin(), cachePayload.end()), "DDC 应按稳定 Key 返回相同字节"); + MetaCoreExpect(!importPipeline->GetJobs().empty(), "初始 Refresh 应留下可诊断的导入作业快照"); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -4920,22 +4936,44 @@ void MetaCoreTestP3ProductivityStage() { coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); + const auto importPipeline = moduleRegistry.ResolveService(); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); + MetaCoreExpect(importPipeline != nullptr, "应解析到 ImportPipelineService"); MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目"); + const auto waitForImports = [&]() { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (std::chrono::steady_clock::now() < deadline) { + const auto jobs = importPipeline->GetJobs(); + const bool busy = std::any_of(jobs.begin(), jobs.end(), [](const auto& job) { + return job.State == MetaCore::MetaCoreImportJobState::Queued || + job.State == MetaCore::MetaCoreImportJobState::Running || + job.State == MetaCore::MetaCoreImportJobState::Committing; + }); + if (!busy) return; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + MetaCoreExpect(false, "等待异步导入完成超时"); + }; // 2. 测试资产自动导入与默认目录 (P3.2) - // 写入一个没有 .mcmeta 的 .ktx 贴图文件 - const std::filesystem::path texturePath = tempProjectRoot / "Assets" / "test_texture.ktx"; + // 写入一个没有 .mcmeta 的可解码贴图文件;生产管线不再接受伪造/损坏的 KTX 输入。 + const std::filesystem::path texturePath = tempProjectRoot / "Assets" / "test_texture.ppm"; { - std::ofstream textureFile(texturePath, std::ios::trunc); - textureFile << "dummy-ktx-data"; + std::ofstream textureFile(texturePath, std::ios::binary | std::ios::trunc); + textureFile << "P6\n2 2\n255\n"; + constexpr std::array pixels{ + 255, 0, 0, 0, 255, 0, + 0, 0, 255, 255, 255, 255 + }; + textureFile.write(reinterpret_cast(pixels.data()), static_cast(pixels.size())); } assetDatabase->Refresh(); + waitForImports(); - // 验证是否在 Assets 下自动生成了 test_texture.ktx.mcmeta 物理元文件 - const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "test_texture.ktx.mcmeta"; - MetaCoreExpect(std::filesystem::exists(metaPath), "Refresh 后应生成 test_texture.ktx.mcmeta 物理元文件"); + // 验证是否在 Assets 下自动生成了 test_texture.ppm.mcmeta 物理元文件 + const std::filesystem::path metaPath = tempProjectRoot / "Assets" / "test_texture.ppm.mcmeta"; + MetaCoreExpect(std::filesystem::exists(metaPath), "Refresh 后应生成 test_texture.ppm.mcmeta 物理元文件"); // 读取该 .mcmeta 文件内容,校验结构 std::ifstream metaFile(metaPath); @@ -4953,16 +4991,16 @@ void MetaCoreTestP3ProductivityStage() { // 3. 测试资产重命名与删除 (P3.1, P3.4) // 重命名该贴图文件 - MetaCoreExpect(assetDatabase->RenamePath(std::filesystem::path("Assets") / "test_texture.ktx", "renamed_texture.ktx"), "应能重命名贴图文件"); - const std::filesystem::path renamedTexturePath = tempProjectRoot / "Assets" / "renamed_texture.ktx"; - const std::filesystem::path renamedMetaPath = tempProjectRoot / "Assets" / "renamed_texture.ktx.mcmeta"; + MetaCoreExpect(assetDatabase->RenamePath(std::filesystem::path("Assets") / "test_texture.ppm", "renamed_texture.ppm"), "应能重命名贴图文件"); + const std::filesystem::path renamedTexturePath = tempProjectRoot / "Assets" / "renamed_texture.ppm"; + const std::filesystem::path renamedMetaPath = tempProjectRoot / "Assets" / "renamed_texture.ppm.mcmeta"; MetaCoreExpect(std::filesystem::exists(renamedTexturePath), "重命名后新贴图文件应存在"); MetaCoreExpect(!std::filesystem::exists(texturePath), "重命名后旧贴图文件不应存在"); MetaCoreExpect(std::filesystem::exists(renamedMetaPath), "重命名后新 mcmeta 文件应存在"); MetaCoreExpect(!std::filesystem::exists(metaPath), "重命名后旧 mcmeta 文件不应存在"); // 删除重命名后的贴图文件 - MetaCoreExpect(assetDatabase->DeletePath(std::filesystem::path("Assets") / "renamed_texture.ktx"), "应能删除贴图文件"); + MetaCoreExpect(assetDatabase->DeletePath(std::filesystem::path("Assets") / "renamed_texture.ppm"), "应能删除贴图文件"); MetaCoreExpect(!std::filesystem::exists(renamedTexturePath), "删除后贴图文件应不存在"); MetaCoreExpect(!std::filesystem::exists(renamedMetaPath), "删除后 mcmeta 文件应不存在"); @@ -4987,6 +5025,7 @@ void MetaCoreTestP3ProductivityStage() { // 自动刷新生成 .mcmeta 物理元文件 assetDatabase->Refresh(); + waitForImports(); const std::filesystem::path materialMetaPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json.mcmeta"; MetaCoreExpect(std::filesystem::exists(materialMetaPath), "Refresh 后应生成材质的 mcmeta 元文件"); @@ -5349,6 +5388,7 @@ void MetaCoreTestSceneEnvironmentSettings() { int main() { std::setvbuf(stdout, nullptr, _IONBF, 0); std::setvbuf(stderr, nullptr, _IONBF, 0); + _putenv_s("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP", "1"); std::cout << "[RUN] MetaCoreCreateDefaultScene..." << std::endl; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); diff --git a/vcpkg.json b/vcpkg.json index 02e9257..b6637c4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,6 +2,7 @@ "name": "metacore", "version-string": "1.0.0", "dependencies": [ + "basisu", "glm", "entt", "rmlui",