diff --git a/Sources/UntoldEditor/Editor/EditorSceneView.swift b/Sources/UntoldEditor/Editor/EditorSceneView.swift
index 4574641..fc2d73b 100644
--- a/Sources/UntoldEditor/Editor/EditorSceneView.swift
+++ b/Sources/UntoldEditor/Editor/EditorSceneView.swift
@@ -26,9 +26,6 @@ struct EditorSceneView: View, UntoldRendererDelegate {
CameraSystem.shared.activeCamera = sceneCamera
- // Initialize ray vs model pipeline
- initRayPickerCompute()
-
// Load Debug meshes and other editor / debug resources
loadLightDebugMeshes()
}
diff --git a/Sources/UntoldEditor/Editor/EditorView.swift b/Sources/UntoldEditor/Editor/EditorView.swift
index f749ead..c543aba 100644
--- a/Sources/UntoldEditor/Editor/EditorView.swift
+++ b/Sources/UntoldEditor/Editor/EditorView.swift
@@ -94,6 +94,9 @@ public struct EditorView: View {
VStack(spacing: 0) {
EditorSceneView(renderer: renderer!)
.frame(maxWidth: .infinity, maxHeight: .infinity)
+ .overlay(alignment: .topLeading) {
+ EngineStatsOverlayView()
+ }
TransformManipulationToolbar(controller: editorController!)
.frame(height: 40)
TabView {
diff --git a/Sources/UntoldEditor/Editor/EngineStatsView.swift b/Sources/UntoldEditor/Editor/EngineStatsView.swift
new file mode 100644
index 0000000..4432a9f
--- /dev/null
+++ b/Sources/UntoldEditor/Editor/EngineStatsView.swift
@@ -0,0 +1,175 @@
+//
+// EngineStatsView.swift
+// UntoldEditor
+//
+// Copyright (C) Untold Engine Studios
+// Licensed under the GNU LGPL v3.0 or later.
+// See the LICENSE file or for details.
+//
+
+import Combine
+import SwiftUI
+import UntoldEngine
+
+enum EngineStatsOverlayMode {
+ case off
+ case simplified
+ case advanced
+}
+
+final class EditorEngineStatsStore: ObservableObject {
+ static let shared = EditorEngineStatsStore()
+
+ @Published private(set) var snapshot: EngineStatsSnapshot = .init()
+ @Published var overlayMode: EngineStatsOverlayMode = .off
+ @Published var loggingEnabled: Bool
+ @Published var loggingProfile: EngineStatsLoggingProfile
+ @Published var loggingIntervalSeconds: Double
+
+ private var pollCancellable: AnyCancellable?
+
+ private init() {
+ loggingEnabled = EngineStatsMonitor.shared.enableLogging
+ loggingProfile = EngineStatsMonitor.shared.loggingProfile
+ loggingIntervalSeconds = EngineStatsMonitor.shared.loggingIntervalSeconds
+
+ snapshot = getEngineStatsSnapshot()
+
+ pollCancellable = Timer.publish(every: 0.2, on: .main, in: .common)
+ .autoconnect()
+ .sink { [weak self] _ in
+ self?.snapshot = getEngineStatsSnapshot()
+ }
+ }
+
+ func setLoggingEnabled(_ enabled: Bool) {
+ loggingEnabled = enabled
+ setEngineStatsLogging(
+ enabled: loggingEnabled,
+ profile: loggingProfile,
+ intervalSeconds: loggingIntervalSeconds
+ )
+ }
+
+ func setLoggingProfile(_ profile: EngineStatsLoggingProfile) {
+ loggingProfile = profile
+ setEngineStatsLogging(
+ enabled: loggingEnabled,
+ profile: loggingProfile,
+ intervalSeconds: loggingIntervalSeconds
+ )
+ }
+
+ func setLoggingInterval(_ seconds: Double) {
+ loggingIntervalSeconds = max(0.1, seconds)
+ setEngineStatsLogging(
+ enabled: loggingEnabled,
+ profile: loggingProfile,
+ intervalSeconds: loggingIntervalSeconds
+ )
+ }
+
+ func setOverlaySimplifiedEnabled(_ enabled: Bool) {
+ if enabled {
+ overlayMode = .simplified
+ } else if overlayMode == .simplified {
+ overlayMode = .off
+ }
+ }
+
+ func setOverlayAdvancedEnabled(_ enabled: Bool) {
+ if enabled {
+ overlayMode = .advanced
+ } else if overlayMode == .advanced {
+ overlayMode = .off
+ }
+ }
+}
+
+struct EngineStatsOverlayView: View {
+ @ObservedObject private var store = EditorEngineStatsStore.shared
+
+ var body: some View {
+ Group {
+ switch store.overlayMode {
+ case .off:
+ EmptyView()
+ case .simplified:
+ simplifiedOverlay(store.snapshot)
+ case .advanced:
+ advancedOverlay(store.snapshot)
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func simplifiedOverlay(_ snapshot: EngineStatsSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Engine Stats")
+ .font(.system(size: 11, weight: .semibold))
+ .foregroundColor(.white)
+
+ Text(compactOverlayLine(snapshot))
+ .font(.system(size: 10, design: .monospaced))
+ .foregroundColor(.white.opacity(0.95))
+ .lineLimit(3)
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 8)
+ .background(Color.black.opacity(0.45))
+ .overlay(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(Color.white.opacity(0.12), lineWidth: 1)
+ )
+ .cornerRadius(8)
+ .shadow(color: Color.black.opacity(0.2), radius: 8, x: 0, y: 2)
+ .allowsHitTesting(false)
+ .padding(12)
+ }
+
+ @ViewBuilder
+ private func advancedOverlay(_ snapshot: EngineStatsSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ Text("Engine Stats (Advanced)")
+ .font(.system(size: 11, weight: .semibold))
+ .foregroundColor(.white)
+
+ Text(formatEngineStatsOverlay(snapshot))
+ .font(.system(size: 10, design: .monospaced))
+ .foregroundColor(.white.opacity(0.95))
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ .padding(.horizontal, 10)
+ .padding(.vertical, 8)
+ .background(Color.black.opacity(0.38))
+ .overlay(
+ RoundedRectangle(cornerRadius: 8)
+ .stroke(Color.white.opacity(0.12), lineWidth: 1)
+ )
+ .cornerRadius(8)
+ .shadow(color: Color.black.opacity(0.2), radius: 8, x: 0, y: 2)
+ .allowsHitTesting(false)
+ .padding(12)
+ }
+
+ private func compactOverlayLine(_ snapshot: EngineStatsSnapshot) -> String {
+ "F\(snapshot.frameIndex) " +
+ "fps \(formatFPS(snapshot.timing.frameTotalMs)) " +
+ "frame \(formatMs(snapshot.timing.frameTotalMs))ms " +
+ "upd \(formatMs(snapshot.timing.updateMs)) " +
+ "rnd \(formatMs(snapshot.timing.renderTotalMs)) " +
+ "cul \(formatMs(snapshot.timing.cullingMs)) " +
+ "| draws \(snapshot.render.drawCallsTotal) " +
+ "tris \(snapshot.render.trianglesTotal) " +
+ "vis \(snapshot.render.visibleInstances)"
+ }
+
+ private func formatMs(_ value: Double) -> String {
+ String(format: "%.2f", value)
+ }
+
+ private func formatFPS(_ frameMs: Double) -> String {
+ guard frameMs > 0 else { return "0.0" }
+ return String(format: "%.1f", 1000.0 / frameMs)
+ }
+}
diff --git a/Sources/UntoldEditor/Editor/EnvironmentView.swift b/Sources/UntoldEditor/Editor/EnvironmentView.swift
index 9f848c3..5678bd4 100644
--- a/Sources/UntoldEditor/Editor/EnvironmentView.swift
+++ b/Sources/UntoldEditor/Editor/EnvironmentView.swift
@@ -418,6 +418,12 @@ struct PostProcessingEditorView: View {
struct DebuggerEditorView: View {
@ObservedObject var settings = DebugSettings.shared
+ @State private var spatialEnabled = false
+ @State private var showOctreeLeafBounds = false
+ @State private var occupiedOnly = true
+ @State private var maxLeafNodeCount = 2000
+ @State private var leafColorMode: SpatialDebugLeafColorMode = .plain
+ @State private var colorRenderablesByLOD = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -428,7 +434,103 @@ struct DebuggerEditorView: View {
Text(name)
}
}
+
+ Divider()
+
+ Text("Spatial Debugger")
+ .font(.headline)
+
+ Toggle("Enable Spatial Debug", isOn: $spatialEnabled)
+ .onChange(of: spatialEnabled) { _, enabled in
+ if enabled, !showOctreeLeafBounds, !colorRenderablesByLOD {
+ showOctreeLeafBounds = true
+ }
+ applySpatialDebugSettings()
+ }
+
+ VStack(alignment: .leading, spacing: 8) {
+ Toggle("Show Octree Leaf Bounds", isOn: $showOctreeLeafBounds)
+ .onChange(of: showOctreeLeafBounds) { _, _ in
+ applySpatialDebugSettings()
+ }
+
+ Picker("Leaf Color Mode", selection: $leafColorMode) {
+ Text("Plain").tag(SpatialDebugLeafColorMode.plain)
+ Text("Residency").tag(SpatialDebugLeafColorMode.residency)
+ Text("Culling").tag(SpatialDebugLeafColorMode.culling)
+ }
+ .onChange(of: leafColorMode) { _, _ in
+ applySpatialDebugSettings()
+ }
+
+ Toggle("Occupied Leaves Only", isOn: $occupiedOnly)
+ .onChange(of: occupiedOnly) { _, _ in
+ applySpatialDebugSettings()
+ }
+
+ HStack(spacing: 8) {
+ Text("Max Leaf Nodes")
+ .font(.system(size: 12))
+
+ CommitAndDefocusIntField(value: Binding(
+ get: { maxLeafNodeCount },
+ set: { newValue in
+ maxLeafNodeCount = max(0, newValue)
+ }
+ ))
+ .frame(width: 80)
+ .onChange(of: maxLeafNodeCount) { _, _ in
+ applySpatialDebugSettings()
+ }
+ }
+ }
+ .disabled(!spatialEnabled)
+
+ Toggle("Color Renderables by LOD", isOn: $colorRenderablesByLOD)
+ .onChange(of: colorRenderablesByLOD) { _, _ in
+ applySpatialDebugSettings()
+ }
+ .disabled(!spatialEnabled)
+
+ Button("Disable All Spatial Debug") {
+ disableSpatialDebugVisualization()
+ syncSpatialDebugSettingsFromEngine()
+ }
+
+ Divider()
+
+ Text("Stats overlay controls are in the top toolbar.")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
}
.padding()
+ .onAppear {
+ syncSpatialDebugSettingsFromEngine()
+ }
+ }
+
+ private func applySpatialDebugSettings() {
+ guard spatialEnabled else {
+ disableSpatialDebugVisualization()
+ return
+ }
+
+ setOctreeLeafBoundsDebug(
+ enabled: showOctreeLeafBounds,
+ maxLeafNodeCount: maxLeafNodeCount,
+ occupiedOnly: occupiedOnly,
+ colorMode: leafColorMode
+ )
+ setLODLevelDebug(enabled: colorRenderablesByLOD)
+ }
+
+ private func syncSpatialDebugSettingsFromEngine() {
+ let spatialDebug = SpatialDebugVisualization.shared
+ spatialEnabled = spatialDebug.enabled
+ showOctreeLeafBounds = spatialDebug.showOctreeLeafBounds
+ occupiedOnly = spatialDebug.octreeLeafOccupiedOnly
+ maxLeafNodeCount = spatialDebug.maxLeafNodeCount
+ leafColorMode = spatialDebug.octreeLeafColorMode
+ colorRenderablesByLOD = spatialDebug.colorRenderablesByLOD
}
}
diff --git a/Sources/UntoldEditor/Editor/ToolbarView.swift b/Sources/UntoldEditor/Editor/ToolbarView.swift
index 71dc186..4d0bc80 100644
--- a/Sources/UntoldEditor/Editor/ToolbarView.swift
+++ b/Sources/UntoldEditor/Editor/ToolbarView.swift
@@ -14,6 +14,7 @@
struct ToolbarView: View {
@ObservedObject var selectionManager: SelectionManager
@ObservedObject var editorBasePath = EditorAssetBasePath.shared
+ @ObservedObject private var statsStore = EditorEngineStatsStore.shared
private let editorVersionLabel = "v0.12.10"
var onSave: () -> Void
@@ -149,6 +150,8 @@
.scaleEffect(0.85)
.frame(height: 20)
+ Divider().frame(height: 24)
+
Menu {
Button("Save Scene", systemImage: "square.and.arrow.down.on.square", action: onSave)
Button("Save Scene As…", systemImage: "square.and.arrow.down", action: onSaveAs)
@@ -165,6 +168,42 @@
}
.menuStyle(.borderlessButton)
.focusable(false)
+
+ Divider().frame(height: 24)
+
+ Toggle(isOn: Binding(
+ get: { statsStore.overlayMode != .off },
+ set: { enabled in
+ if enabled {
+ statsStore.setOverlaySimplifiedEnabled(true)
+ } else {
+ statsStore.setOverlaySimplifiedEnabled(false)
+ statsStore.setOverlayAdvancedEnabled(false)
+ }
+ }
+ )) {
+ Text("FPS")
+ .font(.system(size: 11, weight: .semibold))
+ }
+ .toggleStyle(.switch)
+ .scaleEffect(0.85)
+ .frame(height: 20)
+
+ Toggle(isOn: Binding(
+ get: { statsStore.overlayMode == .advanced },
+ set: { enabled in
+ if enabled {
+ statsStore.setOverlayAdvancedEnabled(true)
+ } else if statsStore.overlayMode != .off {
+ statsStore.setOverlaySimplifiedEnabled(true)
+ }
+ }
+ )) {
+ Text("FPS Advanced")
+ .font(.system(size: 11, weight: .semibold))
+ }
+ .toggleStyle(.checkbox)
+ .disabled(statsStore.overlayMode == .off)
}
}
diff --git a/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift b/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift
index c0457b5..32904da 100644
--- a/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift
+++ b/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift
@@ -476,62 +476,12 @@
guard let hit = pickEntity(
rayOrigin: rayContext.rayOrigin,
rayDirection: rayContext.rayDirection,
- options: ScenePickOptions(isGizmoActive: gizmoActive)
+ options: ScenePickOptions(isGizmoActive: gizmoActive, backend: .octreeGPUPreferred)
) else {
return (.invalid, false)
}
return (hit.entityId, true)
}
-
- /// The gpu ray cast is not working for large scenes. TODO: fix getRaycasterdEntityGPU
- internal func getRaycastedEntityGPU(currentLocation: NSPoint, view: NSView) -> (entityId: EntityID, hit: Bool) {
- var hitEntityId: EntityID = .invalid
- var hitEntity = false
-
- guard let cameraComponent = scene.get(component: CameraComponent.self, for: findSceneCamera()) else {
- handleError(.noActiveCamera)
- return (hitEntityId, hitEntity)
- }
-
- let currentCGPoint = simd_float2(Float(currentLocation.x), Float(currentLocation.y))
-
- let rayDirection: simd_float3 = rayDirectionInWorldSpace(uMouseLocation: currentCGPoint, uViewPortDim: simd_float2(Float(view.bounds.width), Float(view.bounds.height)), uPerspectiveSpace: renderInfo.perspectiveSpace, uViewSpace: cameraComponent.viewSpace)
-
- if getAllGameEntitiesWithMeshes().count == 0 {
- return (hitEntityId, hitEntity)
- }
-
- if let rtxCommandBuffer = renderInfo.commandQueue.makeCommandBuffer() {
- executeRayVsModelHit(rtxCommandBuffer, cameraComponent.localPosition, rayDirection)
-
- rtxCommandBuffer.addCompletedHandler { commandBuffer in
- if let error = commandBuffer.error {
- // Handle error if any
- print("Command buffer completed with error: \(error)")
- } else {
- if let data = bufferResources.rayModelInstanceBuffer?.contents().assumingMemoryBound(to: Int32.self) {
- let value = data.pointee
-
- if value != -1 {
- hitEntityId = accelStructResources.entityIDIndex[Int(value)]
- hitEntity = true
- }
- }
- }
-
- cleanUpAccelStructures()
- }
-
- rtxCommandBuffer.commit()
- rtxCommandBuffer.waitUntilCompleted()
- }
-
- if hitEntity {
- return (hitEntityId, hitEntity)
- }
-
- return (hitEntityId, hitEntity)
- }
}
#endif
diff --git a/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift b/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift
index 3f0d3b3..f523d4c 100644
--- a/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift
+++ b/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift
@@ -7,6 +7,7 @@
// See the LICENSE file or for details.
//
import MetalKit
+import QuartzCore
import UntoldEngine
func EditorUpdateRenderingSystem(in view: MTKView) {
@@ -14,11 +15,31 @@ func EditorUpdateRenderingSystem(in view: MTKView) {
commandBufferSemaphore.wait()
if let commandBuffer = renderInfo.commandQueue.makeCommandBuffer() {
+ #if ENGINE_STATS_ENABLED
+ let renderTotalStart = CACurrentMediaTime()
+ #endif
renderInfo.lastCommandBuffer = commandBuffer
+
+ #if ENGINE_STATS_ENABLED
+ let renderPrepStart = CACurrentMediaTime()
+ let cullingStart = CACurrentMediaTime()
+ #endif
performFrustumCulling(commandBuffer: commandBuffer)
+ #if ENGINE_STATS_ENABLED
+ let cullingMs = (CACurrentMediaTime() - cullingStart) * 1000.0
+ EngineStatsMonitor.shared.update { snapshot in
+ snapshot.timing.cullingMs += cullingMs
+ }
+ #endif
executeGaussianDepth(commandBuffer)
executeBitonicSort(commandBuffer)
+ #if ENGINE_STATS_ENABLED
+ let renderPrepMs = (CACurrentMediaTime() - renderPrepStart) * 1000.0
+ EngineStatsMonitor.shared.update { snapshot in
+ snapshot.timing.renderPrepMs += renderPrepMs
+ }
+ #endif
if let renderPassDescriptor = view.currentRenderPassDescriptor {
renderInfo.renderPassDescriptor = renderPassDescriptor
@@ -45,14 +66,30 @@ func EditorUpdateRenderingSystem(in view: MTKView) {
let sortedPasses = try! topologicalSortGraph(graph: graph)
// execute it
+ #if ENGINE_STATS_ENABLED
+ let encodeStart = CACurrentMediaTime()
+ #endif
executeGraph(graph, sortedPasses, commandBuffer)
+ // Keep editor in sync with runtime temporal HZB:
+ // render depth this frame -> build HZB -> consume next frame during culling
+ buildHZBDepthPyramid(commandBuffer)
+ #if ENGINE_STATS_ENABLED
+ let encodeMs = (CACurrentMediaTime() - encodeStart) * 1000.0
+ EngineStatsMonitor.shared.update { snapshot in
+ snapshot.timing.encodeMs += encodeMs
+ }
+ #endif
}
if let drawable = view.currentDrawable {
commandBuffer.present(drawable)
}
- commandBuffer.addCompletedHandler { _ in
+ commandBuffer.addCompletedHandler { cb in
+ #if ENGINE_STATS_ENABLED
+ let gpuExecutionMs = (cb.gpuEndTime - cb.gpuStartTime) * 1000.0
+ EngineStatsMonitor.shared.recordGPUCompletion(executionMs: gpuExecutionMs)
+ #endif
// Release the in-flight slot
commandBufferSemaphore.signal()
DispatchQueue.main.async {
@@ -61,7 +98,18 @@ func EditorUpdateRenderingSystem(in view: MTKView) {
}
}
+ #if ENGINE_STATS_ENABLED
+ let submitStart = CACurrentMediaTime()
+ #endif
commandBuffer.commit()
+ #if ENGINE_STATS_ENABLED
+ let submitMs = (CACurrentMediaTime() - submitStart) * 1000.0
+ let renderTotalMs = (CACurrentMediaTime() - renderTotalStart) * 1000.0
+ EngineStatsMonitor.shared.update { snapshot in
+ snapshot.timing.submitMs += submitMs
+ snapshot.timing.renderTotalMs += renderTotalMs
+ }
+ #endif
} else {
// Failed to create command buffer - release slot
commandBufferSemaphore.signal()
@@ -116,6 +164,14 @@ func buildEditModeGraph() -> RenderGraphResult {
)
graph[transparencyPass.id] = transparencyPass
+ // Spatial debug overlays are rendered on top of lit scene color.
+ let spatialDebugPass = RenderPass(
+ id: "spatialDebug",
+ dependencies: [transparencyPass.id],
+ execute: RenderPasses.spatialDebugBoundsExecution
+ )
+ graph[spatialDebugPass.id] = spatialDebugPass
+
let highlightPass = RenderPass(
id: "outline", dependencies: [batchedModelPass.id], execute: RenderPasses.highlightExecution
)
@@ -134,7 +190,7 @@ func buildEditModeGraph() -> RenderGraphResult {
graph[gaussianPass.id] = gaussianPass
let preCompPass = RenderPass(
- id: "precomp", dependencies: [modelPass.id, gizmoPass.id, transparencyPass.id, gaussianPass.id], execute: RenderPasses.preCompositeExecution
+ id: "precomp", dependencies: [modelPass.id, gizmoPass.id, spatialDebugPass.id, gaussianPass.id], execute: RenderPasses.preCompositeExecution
)
graph[preCompPass.id] = preCompPass
diff --git a/Sources/UntoldEditor/Systems/RayModelIntersecions.swift b/Sources/UntoldEditor/Systems/RayModelIntersecions.swift
deleted file mode 100644
index bdbcb7b..0000000
--- a/Sources/UntoldEditor/Systems/RayModelIntersecions.swift
+++ /dev/null
@@ -1,322 +0,0 @@
-//
-// RayModelIntersecions.swift
-//
-//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
-//
-
-import CShaderTypes
-import Foundation
-import Metal
-import UntoldEngine
-
-func newAccelerationStructure(
- _ accelerationStructureDescriptor: MTLAccelerationStructureDescriptor, _ name: String
-) -> MTLAccelerationStructure? {
- // 1. Query for the sizes needed to store and build the acceleration structure
- let accelSize: MTLAccelerationStructureSizes = renderInfo.device.accelerationStructureSizes(
- descriptor: accelerationStructureDescriptor
- )
-
- // 2. Allocate an acceleration structure large enough for the descriptor. It only allocates memory. It does not build the structure
- guard
- let accelerationStructure: MTLAccelerationStructure = renderInfo.device
- .makeAccelerationStructure(size: accelSize.accelerationStructureSize)
- else {
- print("failed to allocate acceleration structure")
- return nil
- }
-
- accelerationStructure.label = name
-
- // 3. Allocate scrath space
- guard
- let scratchBuffer: MTLBuffer = renderInfo.device.makeBuffer(
- length: accelSize.buildScratchBufferSize, options: .storageModePrivate
- )
- else {
- print("Failed to allocate scratch buffer")
- return nil
- }
-
- // 4. Create a command buffer that performs the acceleration structure build
- guard let commandBuffer = renderInfo.commandQueue.makeCommandBuffer(),
- let commandEncoder = commandBuffer.makeAccelerationStructureCommandEncoder()
- else {
- print("Failed to create command buffer or command encoder")
- return nil
- }
-
- // 6. build acceleration structure
- commandEncoder.build(
- accelerationStructure: accelerationStructure, descriptor: accelerationStructureDescriptor,
- scratchBuffer: scratchBuffer, scratchBufferOffset: 0
- )
-
- // End encoding, and commit the command buffer so the GPU can start building the
- // acceleration structure.
- commandEncoder.endEncoding()
- commandBuffer.commit()
-
- commandBuffer.waitUntilCompleted()
-
- if accelerationStructure.size == 0 {
- print("Acceleration structure size is zero after building")
- return nil
- }
-
- return accelerationStructure
-}
-
-// Create acceleration structures for the scene. The scene contains primitive acceleration
-// structures and an instance acceleration structure. The primitive acceleration structures
-// contain primitives, such as triangles and spheres. The instance acceleration structure contains
-// copies, or instances, of the primitive acceleration structures, each with their own
-// transformation matrix that describes where to place them in the scene.
-
-func createAccelerationStructures(_: Bool) {
- let transformId = getComponentId(for: WorldTransformComponent.self)
- let renderId = getComponentId(for: RenderComponent.self)
-
- var entities: [EntityID] = []
-
- // only ray cast gizmo components
- if gizmoActive, InputSystem.shared.keyState.shiftPressed == false {
- let gizmoId = getComponentId(for: GizmoComponent.self)
- entities = queryEntitiesWithComponentIds([transformId, renderId, gizmoId], in: scene)
- } else {
- entities = visibleEntityIds
- }
-
- // Iterate over the entities found by the component query
- for (i, entityId) in entities.enumerated() {
- // Skip entities pending destroy
- if scene.mask(for: entityId) == nil { continue }
-
- guard let renderComponent = scene.get(component: RenderComponent.self, for: entityId) else {
- handleError(.noRenderComponent, entityId)
- continue
- }
-
- guard let worldTransform = scene.get(component: WorldTransformComponent.self, for: entityId) else {
- handleError(.noWorldTransformComponent, entityId)
- continue
- }
-
- guard scene.get(component: LocalTransformComponent.self, for: entityId) != nil else {
- handleError(.noLocalTransformComponent, entityId)
- continue
- }
-
- let finalTransform = worldTransform.space
-
- // 1. Create a geometry descriptor
- var geometryDescriptors: [MTLAccelerationStructureGeometryDescriptor] = []
-
- for mesh in renderComponent.mesh {
- let geometryDescriptor = MTLAccelerationStructureTriangleGeometryDescriptor()
-
- geometryDescriptor.vertexBuffer = mesh.metalKitMesh.vertexBuffers[Int(modelPassVerticesIndex.rawValue)].buffer
- geometryDescriptor.vertexStride = MemoryLayout.stride
- geometryDescriptor.indexBuffer = mesh.submeshes[0].metalKitSubmesh.indexBuffer.buffer
- geometryDescriptor.indexType = mesh.submeshes[0].metalKitSubmesh.indexType
- geometryDescriptor.triangleCount = mesh.submeshes[0].metalKitSubmesh.indexCount / 3
- geometryDescriptor.vertexFormat = .float4
-
- geometryDescriptors.append(geometryDescriptor)
- }
-
- let column0: MTLPackedFloat3 = MTLPackedFloat3Make(
- finalTransform.columns.0.x, finalTransform.columns.0.y, finalTransform.columns.0.z
- )
- let column1: MTLPackedFloat3 = MTLPackedFloat3Make(
- finalTransform.columns.1.x, finalTransform.columns.1.y, finalTransform.columns.1.z
- )
- let column2: MTLPackedFloat3 = MTLPackedFloat3Make(
- finalTransform.columns.2.x, finalTransform.columns.2.y, finalTransform.columns.2.z
- )
- let column3: MTLPackedFloat3 = MTLPackedFloat3Make(
- finalTransform.columns.3.x, finalTransform.columns.3.y, finalTransform.columns.3.z
- )
-
- let localSpace = MTLPackedFloat4x3(
- columns: (column0, column1, column2, column3)
- )
-
- let mask: Int32 = GEOMETRY_MASK_TRIANGLE
-
- // 2. Create a primitive acceleration structure
- let accelerationStructureDescriptor =
- MTLPrimitiveAccelerationStructureDescriptor()
-
- // 3. Add geometry descriptor to acceleration structure descriptor
- accelerationStructureDescriptor.geometryDescriptors = geometryDescriptors
-
- // build the acceleration structure
- if let accelerationStructure: MTLAccelerationStructure = newAccelerationStructure(
- accelerationStructureDescriptor, getEntityName(entityId: entityId)
- ) {
- // Add the acceleration structure to the array of primitive acceleration structures.
- accelStructResources.primitiveAccelerationStructures.append(accelerationStructure)
- accelStructResources.instanceTransforms.append(localSpace)
- accelStructResources.accelerationStructIndex.append(UInt32(i))
- accelStructResources.entityIDIndex.append(entityId)
- accelStructResources.mask.append(mask)
- // print("Acceleration structure for \(String(describing: r.mesh.name)) properly created")
-
- } else {
- print("Failed to create acceleration structure for entity \(String(describing: getEntityName(entityId: entityId)))")
- }
- }
-}
-
-func createInstanceAccelerationStructures() {
- // Allocate a buffer of acceleration structure instance descriptors. Each descriptor represents
- // an instance of one of the primitive acceleration structures created above, with its own
- // transformation matrix.
-
- // 2. ALlocate the instance descriptor buffer
- let size = MemoryLayout.stride
- let instanceDescriptorBufferSize =
- size * accelStructResources.primitiveAccelerationStructures.count
-
- accelStructResources.instanceBuffer = renderInfo.device.makeBuffer(
- length: instanceDescriptorBufferSize, options: .storageModeShared
- )!
-
- // 3. Populate instance descriptors
- let instaceDescriptorBuffer = accelStructResources.instanceBuffer!.contents().assumingMemoryBound(
- to: MTLAccelerationStructureInstanceDescriptor.self
- )
-
- for (instanceIndex, _) in accelStructResources.primitiveAccelerationStructures.enumerated() {
- instaceDescriptorBuffer[instanceIndex].accelerationStructureIndex =
- accelStructResources.accelerationStructIndex[instanceIndex]
- instaceDescriptorBuffer[instanceIndex].mask = UInt32(accelStructResources.mask[instanceIndex])
-
- let transform = accelStructResources.instanceTransforms[instanceIndex]
-
- instaceDescriptorBuffer[instanceIndex].transformationMatrix = transform
- }
-
- // create an instance acceleration structure descriptor
- let instanceAccelDescriptor =
- MTLInstanceAccelerationStructureDescriptor()
-
- instanceAccelDescriptor.instancedAccelerationStructures =
- accelStructResources.primitiveAccelerationStructures
- instanceAccelDescriptor.instanceCount = accelStructResources.primitiveAccelerationStructures.count
- instanceAccelDescriptor.instanceDescriptorBuffer = accelStructResources.instanceBuffer
-
- // Create the instance acceleration structure that contains all instances in the scene.
- accelStructResources.instanceAccelerationStructure = newAccelerationStructure(
- instanceAccelDescriptor, "scene instance accel struct"
- )
-}
-
-func initRayPickerCompute() {
- // create ray vs model pipeline
- // create kernel
- guard
- let rayModelIntersectKernel = renderInfo.library.makeFunction(name: "rayModelIntersectKernel")
- else {
- handleError(.kernelCreationFailed, rayModelIntersectPipeline.name!)
- return
- }
-
- // create a pipeline
- do {
- rayModelIntersectPipeline.pipelineState = try renderInfo.device.makeComputePipelineState(
- function: rayModelIntersectKernel
- )
-
- rayModelIntersectPipeline.name = "ray vs model intersect pipe"
- rayModelIntersectPipeline.success = true
- } catch {
- rayModelIntersectPipeline.success = false
- handleError(.pipelineStateCreationFailed, rayModelIntersectPipeline.name!)
- return
- }
-
- bufferResources.rayModelInstanceBuffer = renderInfo.device.makeBuffer(
- length: MemoryLayout.stride, options: .storageModeShared
- )
-}
-
-func cleanUpAccelStructures() {
- // clean up all acceleration structures resources
- accelStructResources.primitiveAccelerationStructures.removeAll()
- accelStructResources.instanceTransforms.removeAll()
- accelStructResources.accelerationStructIndex.removeAll()
- accelStructResources.entityIDIndex.removeAll()
- accelStructResources.instanceAccelerationStructure = nil
- accelStructResources.instanceBuffer = nil
-}
-
-func prepareUserHitRayAccelStructures() {
- // clean up all acceleration structures resources
- cleanUpAccelStructures()
-
- // create acceleration structures
- createAccelerationStructures(false)
-
- // create instance acceleration structures
- createInstanceAccelerationStructures()
-}
-
-func executeRayVsModelHit(
- _ commandBuffer: MTLCommandBuffer, _ origin: simd_float3, _ direction: simd_float3
-) {
- // prepare acceleration structure
- prepareUserHitRayAccelStructures()
-
- if rayModelIntersectPipeline.success == false {
- handleError(.pipelineStateNulled, rayModelIntersectPipeline.name!)
- return
- }
-
- // ray tracing
- let computeEncoder: MTLComputeCommandEncoder = commandBuffer.makeComputeCommandEncoder()!
-
- computeEncoder.label = "User Ray-Hit pass"
-
- computeEncoder.setComputePipelineState(rayModelIntersectPipeline.pipelineState!)
-
- computeEncoder.setAccelerationStructure(
- accelStructResources.instanceAccelerationStructure,
- bufferIndex: Int(rayModelAccelStructIndex.rawValue)
- )
-
- computeEncoder.setBuffer(
- accelStructResources.instanceBuffer, offset: 0, index: Int(rayModelBufferInstanceIndex.rawValue)
- )
-
- var rayOrigin = origin
- var rayDirection = direction
-
- computeEncoder.setBytes(
- &rayOrigin, length: MemoryLayout.stride, index: Int(rayModelOriginIndex.rawValue)
- )
-
- computeEncoder.setBytes(
- &rayDirection, length: MemoryLayout.stride,
- index: Int(rayModelDirectionIndex.rawValue)
- )
-
- computeEncoder.setBuffer(
- bufferResources.rayModelInstanceBuffer, offset: 0, index: Int(rayModelInstanceHitIndex.rawValue)
- )
-
- // Set threadExecutionWidth and maxTotalThreadsPerThreadgroup to 1 to dispatch only one thread
- let w = 1
- let h = 1
-
- let threadsPerThreadgroup: MTLSize = MTLSizeMake(w, h, 1)
- let threadsPerGrid: MTLSize = MTLSizeMake(1, 1, 1) // Dispatch a single thread
-
- computeEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerThreadgroup)
-
- computeEncoder.endEncoding()
-}
diff --git a/Sources/UntoldEditor/Utils/EditorGlobals.swift b/Sources/UntoldEditor/Utils/EditorGlobals.swift
index e5dd968..476ef00 100644
--- a/Sources/UntoldEditor/Utils/EditorGlobals.swift
+++ b/Sources/UntoldEditor/Utils/EditorGlobals.swift
@@ -26,10 +26,6 @@ let gizmoDesiredScreenSize: Float = 75.0 // pixels
var spawnDistance: Float = 2.0
-var accelStructResources = AccelStructResources()
-
-var rayModelIntersectPipeline = ComputePipeline()
-
/// Visual Debugger
enum DebugSelection: Int {
case normalOutput
diff --git a/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift b/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift
index 60439b2..521ef75 100644
--- a/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift
+++ b/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift
@@ -65,7 +65,7 @@ final class BuildEditModeGraphTests: XCTestCase {
let expectedIDs: Set = [
"environment", "shadow", "batchedShadow", "model", "batchedModel", "lightPass",
- "transparency", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "outputTransform",
+ "transparency", "spatialDebug", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "outputTransform",
]
XCTAssertEqual(Set(graph.keys), expectedIDs)
@@ -76,11 +76,12 @@ final class BuildEditModeGraphTests: XCTestCase {
assertDeps(graph, "batchedModel", ["model"])
assertDeps(graph, "lightPass", ["batchedModel", "model", "shadow"])
assertDeps(graph, "transparency", ["lightPass"])
+ assertDeps(graph, "spatialDebug", ["transparency"])
assertDeps(graph, "outline", ["batchedModel"])
assertDeps(graph, "lightVisualPass", ["outline"])
assertDeps(graph, "gizmo", ["lightVisualPass"])
assertDeps(graph, "gaussian", ["model"])
- assertDeps(graph, "precomp", ["model", "gizmo", "transparency", "gaussian"])
+ assertDeps(graph, "precomp", ["model", "gizmo", "spatialDebug", "gaussian"])
assertDeps(graph, "look", ["precomp"])
assertDeps(graph, "outputTransform", ["look"])
@@ -102,7 +103,7 @@ final class BuildEditModeGraphTests: XCTestCase {
let expectedIDs: Set = [
"environment", "shadow", "batchedShadow", "model", "batchedModel", "lightPass",
- "transparency", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "fxaa", "outputTransform",
+ "transparency", "spatialDebug", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "fxaa", "outputTransform",
]
XCTAssertEqual(Set(graph.keys), expectedIDs)
@@ -113,11 +114,12 @@ final class BuildEditModeGraphTests: XCTestCase {
assertDeps(graph, "batchedModel", ["model"])
assertDeps(graph, "lightPass", ["batchedModel", "model", "shadow"])
assertDeps(graph, "transparency", ["lightPass"])
+ assertDeps(graph, "spatialDebug", ["transparency"])
assertDeps(graph, "outline", ["batchedModel"])
assertDeps(graph, "lightVisualPass", ["outline"])
assertDeps(graph, "gizmo", ["lightVisualPass"])
assertDeps(graph, "gaussian", ["model"])
- assertDeps(graph, "precomp", ["model", "gizmo", "transparency", "gaussian"])
+ assertDeps(graph, "precomp", ["model", "gizmo", "spatialDebug", "gaussian"])
assertDeps(graph, "look", ["precomp"])
assertDeps(graph, "fxaa", ["look"])
assertDeps(graph, "outputTransform", ["fxaa"])
@@ -140,7 +142,7 @@ final class BuildEditModeGraphTests: XCTestCase {
let expectedIDs: Set = [
"grid", "shadow", "batchedShadow", "model", "batchedModel", "lightPass",
- "transparency", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "outputTransform",
+ "transparency", "spatialDebug", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "outputTransform",
]
XCTAssertEqual(Set(graph.keys), expectedIDs)
@@ -151,11 +153,12 @@ final class BuildEditModeGraphTests: XCTestCase {
assertDeps(graph, "batchedModel", ["model"])
assertDeps(graph, "lightPass", ["batchedModel", "model", "shadow"])
assertDeps(graph, "transparency", ["lightPass"])
+ assertDeps(graph, "spatialDebug", ["transparency"])
assertDeps(graph, "outline", ["batchedModel"])
assertDeps(graph, "lightVisualPass", ["outline"])
assertDeps(graph, "gizmo", ["lightVisualPass"])
assertDeps(graph, "gaussian", ["model"])
- assertDeps(graph, "precomp", ["model", "gizmo", "transparency", "gaussian"])
+ assertDeps(graph, "precomp", ["model", "gizmo", "spatialDebug", "gaussian"])
assertDeps(graph, "look", ["precomp"])
assertDeps(graph, "outputTransform", ["look"])
@@ -177,7 +180,7 @@ final class BuildEditModeGraphTests: XCTestCase {
let expectedIDs: Set = [
"grid", "shadow", "batchedShadow", "model", "batchedModel", "lightPass",
- "transparency", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "fxaa", "outputTransform",
+ "transparency", "spatialDebug", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", "look", "fxaa", "outputTransform",
]
XCTAssertEqual(Set(graph.keys), expectedIDs)
@@ -188,11 +191,12 @@ final class BuildEditModeGraphTests: XCTestCase {
assertDeps(graph, "batchedModel", ["model"])
assertDeps(graph, "lightPass", ["batchedModel", "model", "shadow"])
assertDeps(graph, "transparency", ["lightPass"])
+ assertDeps(graph, "spatialDebug", ["transparency"])
assertDeps(graph, "outline", ["batchedModel"])
assertDeps(graph, "lightVisualPass", ["outline"])
assertDeps(graph, "gizmo", ["lightVisualPass"])
assertDeps(graph, "gaussian", ["model"])
- assertDeps(graph, "precomp", ["model", "gizmo", "transparency", "gaussian"])
+ assertDeps(graph, "precomp", ["model", "gizmo", "spatialDebug", "gaussian"])
assertDeps(graph, "look", ["precomp"])
assertDeps(graph, "fxaa", ["look"])
assertDeps(graph, "outputTransform", ["fxaa"])
diff --git a/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift b/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift
index 167d96f..6a19095 100644
--- a/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift
+++ b/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift
@@ -78,6 +78,7 @@ final class EditorRenderingSystemTests: XCTestCase {
XCTAssertNotNil(graph["outline"], "Highlight/outline pass should exist")
XCTAssertNotNil(graph["lightVisualPass"], "Light visual pass should exist")
XCTAssertNotNil(graph["gizmo"], "Gizmo pass should exist")
+ XCTAssertNotNil(graph["spatialDebug"], "Spatial debug pass should exist")
XCTAssertNotNil(graph["precomp"], "Pre-composite pass should exist")
XCTAssertNotNil(graph["look"], "Look pass should exist")
XCTAssertNotNil(graph["outputTransform"], "Output transform pass should exist")
@@ -106,6 +107,7 @@ final class EditorRenderingSystemTests: XCTestCase {
XCTAssertNotNil(graph["outline"], "Highlight/outline pass should exist")
XCTAssertNotNil(graph["lightVisualPass"], "Light visual pass should exist")
XCTAssertNotNil(graph["gizmo"], "Gizmo pass should exist")
+ XCTAssertNotNil(graph["spatialDebug"], "Spatial debug pass should exist")
XCTAssertNotNil(graph["precomp"], "Pre-composite pass should exist")
XCTAssertNotNil(graph["look"], "Look pass should exist")
XCTAssertNotNil(graph["outputTransform"], "Output transform pass should exist")
@@ -134,11 +136,12 @@ final class EditorRenderingSystemTests: XCTestCase {
XCTAssertEqual(graph["lightVisualPass"]?.dependencies, ["outline"], "Light visual pass should depend on outline")
XCTAssertEqual(graph["gizmo"]?.dependencies, ["lightVisualPass"], "Gizmo should depend on light visual pass")
XCTAssertEqual(graph["transparency"]?.dependencies, ["lightPass"], "Transparency should depend on light pass")
+ XCTAssertEqual(graph["spatialDebug"]?.dependencies, ["transparency"], "Spatial debug should depend on transparency")
let precompDeps = graph["precomp"]?.dependencies ?? []
XCTAssertTrue(precompDeps.contains("model"), "Precomp should depend on model")
XCTAssertTrue(precompDeps.contains("gizmo"), "Precomp should depend on gizmo")
- XCTAssertTrue(precompDeps.contains("transparency"), "Precomp should depend on transparency")
+ XCTAssertTrue(precompDeps.contains("spatialDebug"), "Precomp should depend on spatialDebug")
XCTAssertEqual(graph["look"]?.dependencies, ["precomp"], "Look should depend on precomp")
XCTAssertEqual(graph["outputTransform"]?.dependencies, ["look"], "Output transform should depend on look")
@@ -191,6 +194,8 @@ final class EditorRenderingSystemTests: XCTestCase {
("model", "outline"),
("outline", "lightVisualPass"),
("lightVisualPass", "gizmo"),
+ ("transparency", "spatialDebug"),
+ ("spatialDebug", "precomp"),
("gizmo", "precomp"),
("lightPass", "precomp"),
("precomp", "look"),
@@ -291,7 +296,7 @@ final class EditorRenderingSystemTests: XCTestCase {
XCTAssertEqual(precompPass.dependencies.count, 4, "Precomp should have exactly 4 dependencies")
XCTAssertTrue(precompPass.dependencies.contains("model"), "Precomp should depend on model")
XCTAssertTrue(precompPass.dependencies.contains("gizmo"), "Precomp should depend on gizmo")
- XCTAssertTrue(precompPass.dependencies.contains("transparency"), "Precomp should depend on transparency")
+ XCTAssertTrue(precompPass.dependencies.contains("spatialDebug"), "Precomp should depend on spatialDebug")
XCTAssertTrue(precompPass.dependencies.contains("gaussian"), "Precomp should depend on gaussian")
}