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
3 changes: 0 additions & 3 deletions Sources/UntoldEditor/Editor/EditorSceneView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/UntoldEditor/Editor/EditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
175 changes: 175 additions & 0 deletions Sources/UntoldEditor/Editor/EngineStatsView.swift
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/> 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

Check warning on line 105 in Sources/UntoldEditor/Editor/EngineStatsView.swift

View workflow job for this annotation

GitHub Actions / lint

Remove redundant @ViewBuilder attribute when it's not needed. (redundantViewBuilder)
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

Check warning on line 130 in Sources/UntoldEditor/Editor/EngineStatsView.swift

View workflow job for this annotation

GitHub Actions / lint

Remove redundant @ViewBuilder attribute when it's not needed. (redundantViewBuilder)
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)
}
}
102 changes: 102 additions & 0 deletions Sources/UntoldEditor/Editor/EnvironmentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
}
39 changes: 39 additions & 0 deletions Sources/UntoldEditor/Editor/ToolbarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down
Loading
Loading