From 6bb8b3e1e256f3182c7363aad2c1d67191157ea5 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Thu, 15 Oct 2026 18:09:55 -0700 Subject: [PATCH 1/2] Fix: resolve assets the Editor exports flat into Models/StreamModels/Animations (#1369) getResourceURL only probed the nested Models//. form, but the Editor's asset exporter (RuntimeExportLocation.swift) has always cooked runtime assets flat into the category root (Models/.), beside the per-model support folder rather than inside it. External projects that load an asset by filename via setEntityMeshAsync never matched that layout and failed with "Filename not found", even though the identical file loaded fine through the Editor. Add the flat form as an additional probe so both cases resolve through the same code path. --- Sources/UntoldEngine/Systems/LoadingSystem.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/UntoldEngine/Systems/LoadingSystem.swift b/Sources/UntoldEngine/Systems/LoadingSystem.swift index 780a0068e..afd6e00a5 100644 --- a/Sources/UntoldEngine/Systems/LoadingSystem.swift +++ b/Sources/UntoldEngine/Systems/LoadingSystem.swift @@ -109,9 +109,16 @@ public func getResourceURL(resourceName: String, ext: String, subName: String?) // Flat layout (no top-level "Assets") var searchPaths: [[String]] = [ + // Nested form (Models//.) is checked first, then the flat + // form (Models/.) the Editor's asset exporter actually writes -- + // it cooks the runtime asset into the category root, beside the per-model + // support folder, rather than inside it. See RuntimeExportLocation.swift. ["Models", resourceName, "\(resourceName).\(ext)"], + ["Models", "\(resourceName).\(ext)"], ["StreamModels", resourceName, "\(resourceName).\(ext)"], + ["StreamModels", "\(resourceName).\(ext)"], ["Animations", resourceName, "\(resourceName).\(ext)"], + ["Animations", "\(resourceName).\(ext)"], ["HDR", "\(resourceName).\(ext)"], ["Gaussians", "\(resourceName).\(ext)"], ["Scripts", "\(resourceName).\(ext)"], @@ -132,8 +139,11 @@ public func getResourceURL(resourceName: String, ext: String, subName: String?) // Then try structured subdirectories let searchPaths: [[String]] = [ ["Models", resourceName, "\(resourceName).\(ext)"], + ["Models", "\(resourceName).\(ext)"], ["StreamModels", resourceName, "\(resourceName).\(ext)"], + ["StreamModels", "\(resourceName).\(ext)"], ["Animations", resourceName, "\(resourceName).\(ext)"], + ["Animations", "\(resourceName).\(ext)"], ["HDR", "\(resourceName).\(ext)"], ["Gaussians", "\(resourceName).\(ext)"], ["Scripts", "\(resourceName).\(ext)"], From 72150232e8e3fb61fa5f3ab58d478bd14ab3d6c3 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Thu, 15 Oct 2026 18:10:28 -0700 Subject: [PATCH 2/2] [Bugfix] Fix HDR/IBL validation falling back to stale directory on scene restore (#1367) * [Bugfix] Fix HDR/IBL validation falling back to stale directory on scene restore serializeScene() only checked assetBasePath/HDR when validating the active HDR, incorrectly treating the engine's built-in default HDR as missing and nulling it out. deserializeScene() then cleared hdrURL but left hdrDirectoryURL stale, so the next IBL bake resolved an empty texture name against a leftover directory and failed to open it. - Validate HDR existence through LoadingSystem's full resource search (project assets, app bundle, engine bundle) instead of only assetBasePath/HDR. - Clear hdrDirectoryURL alongside hdrURL in deserializeScene. - Guard loadImage against an empty texture name. Fixes #1364 * [Bugfix] Guard makeHDRTexture against a nil render device The #1364 HDR-validation fix made serializeScene/deserializeScene more accurate about whether the active HDR actually resolves, which meant a bare ECS unit test (no real Metal device set up) started exercising generateHDR -> makeHDRTexture for the first time. renderInfo.device is an implicitly-unwrapped optional there, so a nil device crashed the test suite instead of failing gracefully through the existing LoadError.textureCreationFailed path. --- .../UntoldEngine/Scenes/SceneSerializer.swift | 33 +++++++++++-------- Sources/UntoldEngine/Utils/FuncUtils.swift | 6 +++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Sources/UntoldEngine/Scenes/SceneSerializer.swift b/Sources/UntoldEngine/Scenes/SceneSerializer.swift index f794a65c8..48eee439a 100644 --- a/Sources/UntoldEngine/Scenes/SceneSerializer.swift +++ b/Sources/UntoldEngine/Scenes/SceneSerializer.swift @@ -1309,21 +1309,24 @@ public func serializeScene() -> SceneData { var shouldApplyIBL = applyIBL if hdrURL.isEmpty == false { - var hdrExists = false - - // Check in user's asset base path (HDR folder) - if let basePath = assetBasePath { - let hdrPath = basePath.appendingPathComponent("HDR").appendingPathComponent(hdrURL) - hdrExists = FileManager.default.fileExists(atPath: hdrPath.path) + // Resolve through the same search LoadingSystem/generateHDR already use + // (assetBasePath/HDR, app bundle, engine bundle) rather than only checking + // assetBasePath/HDR directly. The active HDR may legitimately be one of the + // engine's built-in defaults (e.g. teatro_massimo_2k.hdr), which lives in the + // engine's resource bundle, not the project's asset folder -- checking only + // assetBasePath/HDR falsely reported those as "not found" and disabled IBL. + let nsName = hdrURL as NSString + let providedExtension = nsName.pathExtension + let candidates: [(base: String, ext: String)] = providedExtension.isEmpty + ? ["hdr", "exr", "png"].map { (hdrURL, $0) } + : [(nsName.deletingPathExtension, providedExtension)] + + let hdrExists = candidates.contains { candidate in + LoadingSystem.shared.resourceURL(forResource: candidate.base, withExtension: candidate.ext, subResource: nil) != nil + } - if !hdrExists { - Logger.logWarning(message: "[SceneSerializer] HDR file not found in assets: \(hdrURL), disabling IBL") - validatedHDR = nil - shouldApplyIBL = false - } - } else { - // No asset base path set, can't validate - Logger.logWarning(message: "[SceneSerializer] No asset base path set, cannot validate HDR") + if !hdrExists { + Logger.logWarning(message: "[SceneSerializer] HDR file not found in assets: \(hdrURL), disabling IBL") validatedHDR = nil shouldApplyIBL = false } @@ -1617,12 +1620,14 @@ public func deserializeScene( hdrURL = hdr } else { hdrURL = "" + hdrDirectoryURL = nil } } else { applyIBL = false renderEnvironment = false ambientIntensity = 0.4 hdrURL = "" + hdrDirectoryURL = nil } if let colorGrading = sceneData.colorGrading { diff --git a/Sources/UntoldEngine/Utils/FuncUtils.swift b/Sources/UntoldEngine/Utils/FuncUtils.swift index 33107a41e..2b1cc09ad 100644 --- a/Sources/UntoldEngine/Utils/FuncUtils.swift +++ b/Sources/UntoldEngine/Utils/FuncUtils.swift @@ -53,7 +53,7 @@ private func makeHDRTexture(width: Int, height: Int, label: String) throws -> MT 1 + floorf(log2f(fmaxf(Float(width), Float(height)))) ) - guard let texture = renderInfo.device.makeTexture(descriptor: descriptor) else { + guard let device = renderInfo.device, let texture = device.makeTexture(descriptor: descriptor) else { throw LoadError.textureCreationFailed } texture.label = label @@ -154,6 +154,10 @@ public func loadTexture( } public func loadImage(_ textureName: String, from directory: URL? = nil) throws -> URL { + guard !textureName.isEmpty else { + throw LoadError.urlCreationFailed(textureName) + } + if let directory { let fileURL = directory.appendingPathComponent(textureName) if FileManager.default.fileExists(atPath: fileURL.path) {