Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions Sources/UntoldEngine/Mesh/Mesh.swift
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,10 @@ public struct Material {
func loadRuntimeTexture(_ label: String, reference: RuntimeTextureReference?, isSRGB: Bool) -> MTLTexture? {
guard let reference, let url = reference.sourceURL else { return nil }
let fileExists = fileManager.fileExists(atPath: url.path)
Logger.log(message: "[UntoldTexture] \(label) '\(runtimeMaterial.name ?? "<unnamed material>")' -> \(url.path) | exists=\(fileExists)")
Logger.log(
message: "[UntoldTexture] \(label) '\(runtimeMaterial.name ?? "<unnamed material>")' -> \(url.path) | exists=\(fileExists)",
category: LogCategory.textureLoading.rawValue
)

// ASTC textures stored in the engine-native .utex container bypass
// MTKTextureLoader entirely and are uploaded directly to the GPU.
Expand Down Expand Up @@ -705,15 +708,21 @@ public struct Material {
if let rgbaImage = ctx.makeImage(),
let texture = try? textureLoader.newTexture(cgImage: rgbaImage, options: options)
{
Logger.log(message: "[UntoldTexture] Expanded grayscale \(label.lowercased()) to RGBA '\(runtimeMaterial.name ?? "<unnamed material>")' \(texture.width)x\(texture.height)")
Logger.log(
message: "[UntoldTexture] Expanded grayscale \(label.lowercased()) to RGBA '\(runtimeMaterial.name ?? "<unnamed material>")' \(texture.width)x\(texture.height)",
category: LogCategory.textureLoading.rawValue
)
return texture
}
}
}

do {
let texture = try textureLoader.newTexture(URL: url, options: options)
Logger.log(message: "[UntoldTexture] Loaded \(label.lowercased()) texture '\(runtimeMaterial.name ?? "<unnamed material>")' \(texture.width)x\(texture.height)")
Logger.log(
message: "[UntoldTexture] Loaded \(label.lowercased()) texture '\(runtimeMaterial.name ?? "<unnamed material>")' \(texture.width)x\(texture.height)",
category: LogCategory.textureLoading.rawValue
)
return texture
} catch {
handleError(.textureFailedLoading, "\(label) \(error.localizedDescription)", runtimeMaterial.name ?? "<unnamed material>")
Expand Down Expand Up @@ -1229,7 +1238,10 @@ final class TextureLoader {
keySource = "obj-identity(named-no-bracket)"
}
let isHit = textureCache[cacheKey] != nil
Logger.log(message: "[TextureCache] \(isHit ? "HIT " : "MISS") key=\(cacheKeyURL.absoluteString) source=\(keySource) mdlTex=0x\(String(UInt(bitPattern: ObjectIdentifier(mdlTex)), radix: 16)) name='\(textureName)' map=\(mapType) isSRGB=\(isSRGB)")
Logger.log(
message: "[TextureCache] \(isHit ? "HIT " : "MISS") key=\(cacheKeyURL.absoluteString) source=\(keySource) mdlTex=0x\(String(UInt(bitPattern: ObjectIdentifier(mdlTex)), radix: 16)) name='\(textureName)' map=\(mapType) isSRGB=\(isSRGB)",
category: LogCategory.textureLoading.rawValue
)
}

if let cached = textureCache[cacheKey] {
Expand Down Expand Up @@ -1259,7 +1271,10 @@ final class TextureLoader {
// skipped for large assets and the MDLTexture has no pixel data yet.
// Ask the MDLTexture to lazily fetch its own data from the USDZ package and retry.
// This loads only this one texture, not the entire asset.
Logger.log(message: "[TextureLoad] MDL path failed for '\(textureName)' — retrying with lazy hydration (\(initialError.localizedDescription))")
Logger.log(
message: "[TextureLoad] MDL path failed for '\(textureName)' — retrying with lazy hydration (\(initialError.localizedDescription))",
category: LogCategory.textureLoading.rawValue
)
if mdlTex.texelDataWithTopLeftOrigin(atMipLevel: 0, create: true) != nil,
let retryTex = try? mtkLoader.newTexture(texture: mdlTex, options: options)
{
Expand All @@ -1275,7 +1290,10 @@ final class TextureLoader {
outputSourceDimensions: &outputSourceDimensions
)
}
Logger.log(message: "[TextureLoad] Lazy hydration also failed for '\(textureName)' — falling through to URL paths")
Logger.log(
message: "[TextureLoad] Lazy hydration also failed for '\(textureName)' — falling through to URL paths",
category: LogCategory.textureLoading.rawValue
)
handleError(.textureFailedLoading)
}
}
Expand Down Expand Up @@ -1327,7 +1345,10 @@ final class TextureLoader {
outputSourceDimensions: &outputSourceDimensions
)
}
Logger.log(message: "[TextureLoad] USDZ package URL failed for '\(parsed.innerPath)' — falling through to remaining paths")
Logger.log(
message: "[TextureLoad] USDZ package URL failed for '\(parsed.innerPath)' — falling through to remaining paths",
category: LogCategory.textureLoading.rawValue
)
}

// 1) Try as-is (absolute or already-resolved)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ extension GeometryStreamingSystem {
}
}

Logger.log(message: "[HLOD] Tile '\(tileId)' HLOD loaded.")
Logger.log(
message: "[TileStreaming][HLOD] Tile '\(tileId)' HLOD loaded.",
category: LogCategory.tileStreaming.rawValue
)
} else {
self.decrementHLODLoadCount()
if scene.exists(capturedHlodId) {
Expand Down Expand Up @@ -200,7 +203,10 @@ extension GeometryStreamingSystem {

unmarkLoadedHLODEntity(entityId)
recordTileRepresentationSwap(entityId: entityId, tileId: tileComp.tileId, representation: "hlod:unloaded")
Logger.log(message: "[HLOD] Tile '\(tileComp.tileId)' HLOD unloaded.")
Logger.log(
message: "[TileStreaming][HLOD] Tile '\(tileComp.tileId)' HLOD unloaded.",
category: LogCategory.tileStreaming.rawValue
)
}

// MARK: - Per-tile LOD level load / unload
Expand Down Expand Up @@ -309,7 +315,10 @@ extension GeometryStreamingSystem {
BatchingSystem.shared.notifyTileEntitiesResident(renderIds)
}
}
Logger.log(message: "[LOD] Tile '\(tileId)' LOD level \(capturedIndex + 1) loaded.")
Logger.log(
message: "[TileStreaming][LOD] Tile '\(tileId)' LOD level \(capturedIndex + 1) loaded.",
category: LogCategory.tileStreaming.rawValue
)
} else {
if scene.exists(capturedLodId) {
destroyEntity(entityId: capturedLodId)
Expand Down Expand Up @@ -390,7 +399,10 @@ extension GeometryStreamingSystem {
}

recordTileRepresentationSwap(entityId: entityId, tileId: tileComp.tileId, representation: "lod\(levelIndex + 1):unloaded")
Logger.log(message: "[LOD] Tile '\(tileComp.tileId)' LOD level \(levelIndex + 1) unloaded.")
Logger.log(
message: "[TileStreaming][LOD] Tile '\(tileComp.tileId)' LOD level \(levelIndex + 1) unloaded.",
category: LogCategory.tileStreaming.rawValue
)
}

/// Unload every LOD level for a tile stub. Called when the full tile reaches
Expand Down Expand Up @@ -426,7 +438,10 @@ extension GeometryStreamingSystem {
let tileURL = tileComp.tileURL
let tileId = tileComp.tileId

Logger.log(message: "[TileStreaming] Dispatching load for tile '\(tileId)'")
Logger.log(
message: "[TileStreaming] Dispatching load for tile '\(tileId)'",
category: LogCategory.tileStreaming.rawValue
)

// Create a dedicated mesh entity as a child of the tile stub before
// spawning the load Task. setEntityMeshAsync will attach all geometry
Expand Down Expand Up @@ -526,7 +541,10 @@ extension GeometryStreamingSystem {
}
unmarkLoadingTileEntity(entityId)
unmarkLoadedTileEntity(entityId)
Logger.log(message: "[TileStreaming] Tile '\(tileId)' cancelled load cleaned up.")
Logger.log(
message: "[TileStreaming] Tile '\(tileId)' cancelled load cleaned up.",
category: LogCategory.tileStreaming.rawValue
)
return
}

Expand All @@ -547,10 +565,12 @@ extension GeometryStreamingSystem {
self.markLoadedTileEntity(entityId)
self.recordTileRepresentationSwap(entityId: entityId, tileId: tileId, representation: "tile:parsed")

// Full geometry is now resident — unload the coarse HLOD mesh
// and any per-tile LOD levels that were showing while loading.
self.unloadHLOD(entityId: entityId)
self.unloadAllLODLevels(entityId: entityId)
// Only drop fallback coverage once full geometry is renderable.
// OCC tiles may be parsed before enough child stubs have uploaded.
if self.tileHasUsableFullGeometry(tc) {
self.unloadHLOD(entityId: entityId)
self.unloadAllLODLevels(entityId: entityId)
}

// Tag the tile's mesh hierarchy for cell-based static batching.
// setEntityStaticBatchComponent walks the full child tree and
Expand Down Expand Up @@ -593,7 +613,10 @@ extension GeometryStreamingSystem {
let selectableSuffix = selectableRenderIds.isEmpty
? ""
: " selectable=[\(selectableNames)]"
Logger.log(message: "[TileStreaming] Tile '\(tileId)' parsed (\(occCount) OCC stubs pending GPU upload, render=\(tileRenderIds.count), selectable=\(selectableRenderIds.count)). geom=\(budgetStats.meshMemoryUsed / (1024 * 1024))MB/\(budgetStats.geometryBudget / (1024 * 1024))MB (\(geomPct)%)\(selectableSuffix)")
Logger.log(
message: "[TileStreaming] Tile '\(tileId)' parsed (\(occCount) OCC stubs pending GPU upload, render=\(tileRenderIds.count), selectable=\(selectableRenderIds.count)). geom=\(budgetStats.meshMemoryUsed / (1024 * 1024))MB/\(budgetStats.geometryBudget / (1024 * 1024))MB (\(geomPct)%)\(selectableSuffix)",
category: LogCategory.tileStreaming.rawValue
)
} else {
// Destroy the pre-created child entity on failure so it
// doesn't leak as an empty, invisible stub.
Expand Down Expand Up @@ -789,7 +812,10 @@ extension GeometryStreamingSystem {

let unloadMs = (CFAbsoluteTimeGetCurrent() - unloadStart) * 1000.0
let unloadSuffix = unloadMs > 50 ? " ⚠️ slow=\(String(format: "%.0f", unloadMs))ms" : ""
Logger.log(message: "[TileStreaming] Tile '\(tileId)' unloaded (\(descendants.count) child entities destroyed).\(unloadSuffix)")
Logger.log(
message: "[TileStreaming] Tile '\(tileId)' unloaded (\(descendants.count) child entities destroyed).\(unloadSuffix)",
category: LogCategory.tileStreaming.rawValue
)
}
}

Expand Down
Loading
Loading