From 4026193be4aa2a945e8ff90e48215ed2c8264794 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sat, 13 Dec 2025 20:56:01 +0100 Subject: [PATCH 01/12] Created the base listener class --- Atlas Tracer/Client/Listener.swift | 73 ++++++++++++++++++++++++++++ Atlas Tracer/Views/ProjectView.swift | 6 +++ 2 files changed, 79 insertions(+) create mode 100644 Atlas Tracer/Client/Listener.swift diff --git a/Atlas Tracer/Client/Listener.swift b/Atlas Tracer/Client/Listener.swift new file mode 100644 index 0000000..e8f2c39 --- /dev/null +++ b/Atlas Tracer/Client/Listener.swift @@ -0,0 +1,73 @@ +// +// Listener.swift +// Atlas Tracer +// +// Created by Max Van den Eynde on 13/12/25. +// + +import Foundation +import Network + +let PORT = 55555 + +class Listener { + private let port: UInt16 + private var listener: NWListener? + private let queue: DispatchQueue = .init(label: "ListenerQueue", qos: .background) + + private(set) var messageHistory: [String] = [] + + init(port: UInt16) { + self.port = port + } + + func start() { + do { + let params = NWParameters.tcp + listener = try NWListener(using: params, on: NWEndpoint.Port(rawValue: port)!) + } catch { + print("Failed to create listener: ", error) + return + } + + listener?.newConnectionHandler = { [weak self] connection in + connection.start(queue: self?.queue ?? .global()) + self?.recieve(on: connection) + } + + listener?.start(queue: queue) + print("Listening in port: \(port)") + } + + func recieve(on connection: NWConnection) { + connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, _ in + if let data = data, !data.isEmpty { + let text = String(decoding: data, as: UTF8.self) + print("Received: \(text)") + + self?.messageHistory.append(text) + } + + if isComplete { + connection.cancel() + } else { + self?.recieve(on: connection) + } + } + } + + func send(to host: String, port: UInt16, message: String) { + let connection = NWConnection(host: NWEndpoint.Host(host), port: NWEndpoint.Port(rawValue: port)!, using: .tcp) + connection.start(queue: queue) + + let data = message.data(using: .utf8)! + connection.send(content: data, completion: .contentProcessed { error in + if let error = error { + print("Failed to send:", error) + } else { + print("Sent: \(message)") + } + connection.cancel() + }) + } +} diff --git a/Atlas Tracer/Views/ProjectView.swift b/Atlas Tracer/Views/ProjectView.swift index 73c1fd7..52d7a30 100644 --- a/Atlas Tracer/Views/ProjectView.swift +++ b/Atlas Tracer/Views/ProjectView.swift @@ -21,6 +21,7 @@ struct ProjectView: View { @State private var selectedView: String = "logs" @State private var projectState: String = "Not started" + @State private var listener: Listener? = nil var body: some View { NavigationSplitView { @@ -135,6 +136,11 @@ struct ProjectView: View { }.onChange(of: selectedView) { print(selectedView) } + .onAppear { + print("Initializing the listener") + listener = Listener(port: 5123) + listener?.start() + } } } } From dff4c2e44332f4b345daf83394f86964092dc495 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 14 Dec 2025 12:34:52 +0100 Subject: [PATCH 02/12] Removed the logic type of debugging --- Atlas Tracer/Project.swift | 5 +- Atlas Tracer/Views/CreateProject.swift | 19 +- Atlas Tracer/Views/ProjectView.swift | 4 - .../Views/TargetViews/GraphicsView.swift | 4 +- .../Views/TargetViews/LogicView.swift | 177 ------------------ README.md | 1 - 6 files changed, 7 insertions(+), 203 deletions(-) delete mode 100644 Atlas Tracer/Views/TargetViews/LogicView.swift diff --git a/Atlas Tracer/Project.swift b/Atlas Tracer/Project.swift index 7b4797e..cdff126 100644 --- a/Atlas Tracer/Project.swift +++ b/Atlas Tracer/Project.swift @@ -9,7 +9,6 @@ import Foundation enum ProjectType: String, Decodable, Encodable, Identifiable, CaseIterable { case graphics - case logic case resources case object case traces @@ -21,7 +20,6 @@ enum ProjectType: String, Decodable, Encodable, Identifiable, CaseIterable { func getName() -> String { switch self { case .graphics: return "Graphics" - case .logic: return "Logic" case .resources: return "Resources" case .object: return "Object" case .traces: return "Traces" @@ -33,7 +31,6 @@ enum ProjectType: String, Decodable, Encodable, Identifiable, CaseIterable { func getIconName() -> String { switch self { case .graphics: return "rotate.3d" - case .logic: return "cpu" case .resources: return "archivebox" case .custom: return "square.dashed" case .profiling: return "clock" @@ -61,7 +58,7 @@ class Project: Identifiable, Decodable, Encodable { let project = Project() project.logTypes = [.errors, .logs, .warnings] project.mainProjectType = .custom - project.customProjectTypes = [.graphics, .logic, .resources, .profiling, .traces, .object] + project.customProjectTypes = [.graphics, .resources, .profiling, .traces, .object] project.title = "No Project" return project } diff --git a/Atlas Tracer/Views/CreateProject.swift b/Atlas Tracer/Views/CreateProject.swift index 12bd95e..476b6e5 100644 --- a/Atlas Tracer/Views/CreateProject.swift +++ b/Atlas Tracer/Views/CreateProject.swift @@ -69,7 +69,7 @@ func getDebugTypeFromId(id: Int) -> String { case 4: return "Objects" case 5: - return "Logic" + return "" case 6: return "Traces" case 7: @@ -90,7 +90,7 @@ func getDebugEnumTypeFromId(id: Int) -> ProjectType { case 4: return .object case 5: - return .logic + return .custom case 6: return .traces case 7: @@ -110,7 +110,6 @@ struct SelectLogsAndExecutableView: View { @State private var executablePath: URL? = nil @State private var customGraphics: Bool = false - @State private var customLogic: Bool = false @State private var customResources: Bool = false @State private var customObject: Bool = false @State private var customTraces: Bool = false @@ -187,10 +186,6 @@ struct SelectLogsAndExecutableView: View { Text("Graphics") .bold() }.toggleStyle(.checkbox) - Toggle(isOn: self.$customLogic) { - Text("Logic") - .bold() - }.toggleStyle(.checkbox) Toggle(isOn: self.$customResources) { Text("Resources") .bold() @@ -254,9 +249,6 @@ struct SelectLogsAndExecutableView: View { newProject.logTypes.append(.logs) } - if self.customLogic { - newProject.customProjectTypes.append(.logic) - } if self.customObject { newProject.customProjectTypes.append(.object) } @@ -309,15 +301,12 @@ struct CreateProjectView: View { MainActionToggle(icon: "scale.3d", name: "Objects", color: Color.orange, id: 4, selected: self.$selected) .padding(.trailing, 20) - MainActionToggle(icon: "cpu", - name: "Logic", color: Color.green, id: 5, selected: self.$selected) + MainActionToggle(icon: "clock", + name: "Profiling", color: Color.teal, id: 7, selected: self.$selected) } HStack { MainActionToggle(icon: "memorychip", name: "Traces", color: Color.yellow, id: 6, selected: self.$selected) - .padding(.trailing, 20) - MainActionToggle(icon: "clock", - name: "Profiling", color: Color.teal, id: 7, selected: self.$selected) } } HStack { diff --git a/Atlas Tracer/Views/ProjectView.swift b/Atlas Tracer/Views/ProjectView.swift index 52d7a30..a3f29b8 100644 --- a/Atlas Tracer/Views/ProjectView.swift +++ b/Atlas Tracer/Views/ProjectView.swift @@ -62,10 +62,6 @@ struct ProjectView: View { GraphicsView() Spacer() } - if selectedView == "logic" { - LogicView() - Spacer() - } if selectedView == "resources" { ResourcesView() Spacer() diff --git a/Atlas Tracer/Views/TargetViews/GraphicsView.swift b/Atlas Tracer/Views/TargetViews/GraphicsView.swift index b70a291..c289063 100644 --- a/Atlas Tracer/Views/TargetViews/GraphicsView.swift +++ b/Atlas Tracer/Views/TargetViews/GraphicsView.swift @@ -30,14 +30,14 @@ enum DrawCallType: String, Codable, CaseIterable { } } -struct DrawCall { +struct DrawCall { // Sent from the client let callerObjectId: String let time: Date let type: DrawCallType let frame: Int } -struct FrameData: Identifiable { +struct FrameData: Identifiable { // Sent from the client let id = UUID() let frame: Int let drawCallCount: Int diff --git a/Atlas Tracer/Views/TargetViews/LogicView.swift b/Atlas Tracer/Views/TargetViews/LogicView.swift deleted file mode 100644 index e77628a..0000000 --- a/Atlas Tracer/Views/TargetViews/LogicView.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// LogicView.swift -// Atlas Tracer -// -// Created by Max Van den Eynde on 11/12/25. -// - -import SwiftUI - -struct LogicEntry: Identifiable { - let content: String - let deltaTime: String - let date: Date - let secondsDeltaTime: Float - - let id: UUID = .init() -} - -struct LogicCardView: View { - var logicEntry: LogicEntry - var isLast: Bool = false - - @State private var showDetail: Bool = false - - func getIcon() -> some View { - if isLast { - return AnyView( - Image(systemName: "diamond.fill") - .foregroundStyle(Color.blue) - ) - } else { - return AnyView( - Image(systemName: "diamond") - .foregroundStyle(Color.blue) - .bold() - ) - } - } - - var body: some View { - VStack { - RoundedRectangle(cornerRadius: 10) - .fill(.background) - .frame(height: 30) - .shadow(radius: 5) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(Color.blue, lineWidth: 3) - ) - .overlay { - HStack { - getIcon() - .padding(.leading, 9) - .padding(.trailing, 5) - Text(logicEntry.content) - .foregroundStyle(Color.blue) - .bold() - .lineLimit(1) - .truncationMode(.tail) - .frame(maxWidth: .infinity, alignment: .leading) - Spacer() - Text("took " + logicEntry.deltaTime) - .foregroundStyle(Color.blue) - .italic() - .lineLimit(1) - .padding(.trailing, 8) - Spacer() - } - } - .onTapGesture { - withAnimation { - showDetail.toggle() - } - } - - if showDetail { - HStack { - Text(logicEntry.content) - .foregroundStyle(Color.white) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: false) - .padding(.horizontal, 8) - Spacer() - } - .background { - RoundedRectangle(cornerRadius: 10) - .fill(Color.blue) - .frame(height: 30) - } - .padding(.vertical, 10) - } - } - } -} - -struct LogicView: View { - @State private var entries: [LogicEntry] = [ - LogicEntry(content: "Hello", deltaTime: "0.001s", date: Date.now, secondsDeltaTime: 0.001), - LogicEntry(content: "Hello", deltaTime: "0.002s", date: Date.now, secondsDeltaTime: 0.002), - LogicEntry(content: "Hello", deltaTime: "0.003s", date: Date.now, secondsDeltaTime: 0.003), - LogicEntry(content: "Hello", deltaTime: "0.004s", date: Date.now, secondsDeltaTime: 0.004), - ] - - @State private var selectedFilter = 0 - - @Environment(\.appEnv) private var environment - - var project: Project { - if environment.currentProject != nil { - return environment.currentProject! - } else { - environment.currentProject = Project.createSample() - return environment.currentProject! - } - } - - func applyFilter(entry: LogicEntry) -> Bool { - switch selectedFilter { - case 0: - return true - case 1: - return entry.secondsDeltaTime < 0.001 - case 2: - return entry.secondsDeltaTime < 0.1 && entry.secondsDeltaTime > 0.001 - case 3: - return entry.secondsDeltaTime > 0.1 - default: - return false - } - } - - var body: some View { - VStack(alignment: .leading) { - Text("Logs") - .font(.title) - .bold() - .padding(.horizontal) - .padding(.top) - Picker("Filter", selection: $selectedFilter) { - Text("All").tag(0) - if project.logTypes.contains(.logs) { - Text("Short Lived").tag(1) - } - if project.logTypes.contains(.warnings) { - Text("Medium Lived").tag(2) - } - if project.logTypes.contains(.errors) { - Text("Long Lived").tag(3) - } - }.pickerStyle(.segmented).padding(.horizontal) - VStack { - ScrollView { - ForEach(entries) { entry in - if applyFilter(entry: entry) { - LogicCardView(logicEntry: entry, isLast: entry.id == self.entries.last!.id) - .padding(.vertical, 4) - .padding(.horizontal, 10) - } - } - } - }.padding().background { - HStack { - RoundedRectangle(cornerRadius: 10) - .frame(width: 5) - .padding(.vertical, 7) - .padding(.leading, 70) - .foregroundStyle(Color.blue) - Spacer() - } - } - } - } -} - -#Preview { - LogicView() -} diff --git a/README.md b/README.md index 5e158ac..e1cc63c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ deciphers to give the user real-time data of what's going on the inside of the c Atlas Tracer works with different types of *sessions*, which they are basically different types of information that it can display: * Graphics Sessions: These are focused on draw calls and performance of the Graphics Engine and Vulkan. -* Logic Sessions: These are focused on what the engine is doing besides rendering, like computing atmosphere or the other decisions it is taking. * Resource Sessions: These are focused on what resources is the engine loading, how much time it spends doing it and other insights. * Object Sessions: These are focused on the objects rendered in the screen, meaning triangle count, and general computing time insights. * Trace Sessions: These are focused on how many memory is the engine using, including also render buffers. From 869d797823bf4e790cee846125a2873036b2c33b Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 14 Dec 2025 18:48:54 +0100 Subject: [PATCH 03/12] Updated the UI with respect to the engine's implementation --- .../Views/TargetViews/MemoryTracesView.swift | 4 ++-- .../Views/TargetViews/ObjectView.swift | 18 +----------------- .../Views/TargetViews/ProfilingView.swift | 4 ++-- .../Views/TargetViews/ResourcesView.swift | 4 ++-- 4 files changed, 7 insertions(+), 23 deletions(-) diff --git a/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift b/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift index bce189d..fc55bd3 100644 --- a/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift +++ b/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift @@ -80,7 +80,7 @@ enum ResourceKind: String, CaseIterable, Codable, Hashable, Identifiable { } } -struct Allocation: Identifiable { +struct Allocation: Identifiable { // Get from the engine let id = UUID() let label: String let kind: ResourceKind @@ -91,7 +91,7 @@ struct Allocation: Identifiable { let owner: String? } -struct FrameMemory: Identifiable { +struct FrameMemory: Identifiable { // Get from the engine let id = UUID() let frame: Int let totalMB: Double diff --git a/Atlas Tracer/Views/TargetViews/ObjectView.swift b/Atlas Tracer/Views/TargetViews/ObjectView.swift index c4fd705..592bb12 100644 --- a/Atlas Tracer/Views/TargetViews/ObjectView.swift +++ b/Atlas Tracer/Views/TargetViews/ObjectView.swift @@ -48,16 +48,12 @@ struct TracedObject: Identifiable, Hashable { let triangleCount: Int let materialCount: Int - let submeshCount: Int let vertexMemoryMB: Double let indexMemoryMB: Double let textureMemoryMB: Double - let uniformMemoryMB: Double let recentDrawCalls: Int - let visibleInFrames: Int - let lastUpdatedFrame: Int } struct ObjectMetricSample: Identifiable { @@ -243,8 +239,6 @@ struct ObjectView: View { Text("\(o.materialCount)").frame(width: 90, alignment: .trailing).monospacedDigit() - Text("\(o.submeshCount)").frame(width: 90, alignment: .trailing).monospacedDigit() - Text(String(format: "%.1f", o.totalMemoryMB)) .frame(width: 120, alignment: .trailing) .monospacedDigit() @@ -327,16 +321,13 @@ struct ObjectView: View { HStack(spacing: 24) { statTile(title: "Triangles", value: o.triangleCount.formatted(.number.grouping(.automatic))) statTile(title: "Materials", value: "\(o.materialCount)") - statTile(title: "Submeshes", value: "\(o.submeshCount)") statTile(title: "Recent Draw Calls", value: "\(o.recentDrawCalls)") - statTile(title: "Visible Frames", value: "\(o.visibleInFrames)") } HStack(spacing: 24) { statTile(title: "Vertex Mem", value: String(format: "%.1f MB", o.vertexMemoryMB)) statTile(title: "Index Mem", value: String(format: "%.1f MB", o.indexMemoryMB)) statTile(title: "Texture Mem", value: String(format: "%.1f MB", o.textureMemoryMB)) - statTile(title: "Uniform Mem", value: String(format: "%.1f MB", o.uniformMemoryMB)) statTile(title: "Total Mem", value: String(format: "%.1f MB", o.totalMemoryMB)) } @@ -455,7 +446,6 @@ struct ObjectView: View { }() let materials = Int.random(in: 1...8, using: &rng) - let submeshes = Int.random(in: 1...12, using: &rng) let vertexMB = Double(tri) * 3.0 * 24.0 / (1_024.0 * 1_024.0) * Double.random(in: 0.8...1.3, using: &rng) let indexMB = Double(tri) * 3.0 * 4.0 / (1_024.0 * 1_024.0) * Double.random(in: 0.8...1.2, using: &rng) // @@ -463,22 +453,16 @@ struct ObjectView: View { let uniformMB = Double.random(in: 0.05...0.5, using: &rng) let recentDC = Int.random(in: 0...120, using: &rng) - let visFrames = Int.random(in: 20...frameCount, using: &rng) - let lastUpd = Int.random(in: 0.. Date: Mon, 15 Dec 2025 07:28:19 +0100 Subject: [PATCH 04/12] Basic launch functions for the executable --- Atlas Tracer.xcodeproj/project.pbxproj | 28 +-- Atlas Tracer/Project.swift | 2 + Atlas Tracer/Views/CreateProject.swift | 4 + Atlas Tracer/Views/ProjectView.swift | 258 ++++++++++++++----------- 4 files changed, 157 insertions(+), 135 deletions(-) diff --git a/Atlas Tracer.xcodeproj/project.pbxproj b/Atlas Tracer.xcodeproj/project.pbxproj index 459d4e0..325de57 100644 --- a/Atlas Tracer.xcodeproj/project.pbxproj +++ b/Atlas Tracer.xcodeproj/project.pbxproj @@ -405,21 +405,9 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = R7W4NMR64S; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "Atlas Tracer"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; @@ -449,21 +437,9 @@ COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = R7W4NMR64S; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "Atlas Tracer"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; diff --git a/Atlas Tracer/Project.swift b/Atlas Tracer/Project.swift index cdff126..d303a55 100644 --- a/Atlas Tracer/Project.swift +++ b/Atlas Tracer/Project.swift @@ -51,6 +51,7 @@ class Project: Identifiable, Decodable, Encodable { var mainProjectType: ProjectType = .custom var customProjectTypes: [ProjectType] = [] var title: String = "" + var executablePath: String = "" init() {} @@ -60,6 +61,7 @@ class Project: Identifiable, Decodable, Encodable { project.mainProjectType = .custom project.customProjectTypes = [.graphics, .resources, .profiling, .traces, .object] project.title = "No Project" + project.executablePath = "/usr/bin/yes" return project } } diff --git a/Atlas Tracer/Views/CreateProject.swift b/Atlas Tracer/Views/CreateProject.swift index 476b6e5..194f0fe 100644 --- a/Atlas Tracer/Views/CreateProject.swift +++ b/Atlas Tracer/Views/CreateProject.swift @@ -237,6 +237,9 @@ struct SelectLogsAndExecutableView: View { func createProjectObject() { let newProject = Project() + if self.debugName.isEmpty || self.executablePath == nil { + return + } newProject.title = self.debugName newProject.mainProjectType = getDebugEnumTypeFromId(id: self.selected) if self.errorsOn { @@ -265,6 +268,7 @@ struct SelectLogsAndExecutableView: View { newProject.customProjectTypes.append(.traces) } + newProject.executablePath = self.executablePath!.absoluteString self.project = newProject } } diff --git a/Atlas Tracer/Views/ProjectView.swift b/Atlas Tracer/Views/ProjectView.swift index a3f29b8..433c2ef 100644 --- a/Atlas Tracer/Views/ProjectView.swift +++ b/Atlas Tracer/Views/ProjectView.swift @@ -9,133 +9,173 @@ import SwiftUI struct ProjectView: View { @Environment(\.appEnv) private var environment + @Environment(\.dismiss) private var dismiss - var project: Project { - if environment.currentProject != nil { - return environment.currentProject! - } else { - environment.currentProject = Project.createSample() - return environment.currentProject! - } + var project: Project? { + return environment.currentProject } @State private var selectedView: String = "logs" @State private var projectState: String = "Not started" @State private var listener: Listener? = nil + @State private var process: Process? = nil - var body: some View { - NavigationSplitView { - List(selection: $selectedView) { - Section("General") { - NavigationLink(value: "logs") { - Label("Logs", systemImage: "book.pages") - } - NavigationLink(value: "variables") { - Label("Runtime Variables", systemImage: "arrow.trianglehead.branch") - } - NavigationLink(value: "console") { - Label("Console", systemImage: "apple.terminal") - } - } + func startExecutable() { + guard let project = project else { return } - Section("Targets") { - if project.mainProjectType != .custom { - NavigationLink(value: project.mainProjectType.getName()) { - Label(project.mainProjectType.getName(), systemImage: project.mainProjectType.getIconName()) - } - } else { - ForEach(project.customProjectTypes) { customType in - NavigationLink(value: customType.getName()) { - Label(customType.getName(), systemImage: customType.getIconName()) + let zshURL = URL(fileURLWithPath: "/bin/zsh") + let executableURL = URL(string: project.executablePath)! + + let process = Process() + process.executableURL = zshURL + + process.arguments = [ + "-lc", + "\"\(executableURL.path)\"" + ] + + process.currentDirectoryURL = executableURL.deletingLastPathComponent() + process.environment = ProcessInfo.processInfo.environment + + listener = Listener(port: 5123) + listener?.start() + + do { + try process.run() + self.process = process + print("Engine launched successfully") + } catch { + print("Failed to launch engine:", error) + } + } + + func stopExecutable() { + process?.terminate() + } + + var body: some View { + Group { + if let project = project { + NavigationSplitView { + List(selection: $selectedView) { + Section("General") { + NavigationLink(value: "logs") { + Label("Logs", systemImage: "book.pages") + } + NavigationLink(value: "variables") { + Label("Runtime Variables", systemImage: "arrow.trianglehead.branch") + } + NavigationLink(value: "console") { + Label("Console", systemImage: "apple.terminal") } } - } - } - }.navigationTitle("Sidebar") - } detail: { - VStack { - if selectedView == "logs" { - LogView() - Spacer() - } - if selectedView == "graphics" { - GraphicsView() - Spacer() - } - if selectedView == "resources" { - ResourcesView() - Spacer() - } - if selectedView == "profiling" { - ProfilingView() - Spacer() - } - if selectedView == "traces" { - MemoryTracesView() - Spacer() - } - if selectedView == "object" { - ObjectView() - Spacer() - } - if selectedView == "variables" { - RuntimeVariablesView() - Spacer() - } - if selectedView == "console" { - ConsoleView() - Spacer() - } - } - .navigationTitle(project.title) - .navigationSubtitle(projectState) - .toolbar { - if projectState != "Stepping..." { - Button { - withAnimation { - if projectState == "Not started" { - projectState = "Started" + + Section("Targets") { + if project.mainProjectType != .custom { + NavigationLink(value: project.mainProjectType.getName()) { + Label(project.mainProjectType.getName(), systemImage: project.mainProjectType.getIconName()) + } } else { - projectState = "Not started" + ForEach(project.customProjectTypes) { customType in + NavigationLink(value: customType.getName()) { + Label(customType.getName(), systemImage: customType.getIconName()) + } + } } } - } label: { - if projectState == "Not started" { - Image(systemName: "play.fill") - } else { - Image(systemName: "stop.fill") + }.navigationTitle("Sidebar") + } detail: { + VStack { + if selectedView == "logs" { + LogView() + Spacer() } - }.help("Start the debug session") - } - if projectState == "Stepping..." || projectState == "Not started" { - Button { - withAnimation { - if projectState == "Stepping..." { - projectState = "Not started" - } else { - projectState = "Stepping..." - } + if selectedView == "graphics" { + GraphicsView() + Spacer() + } + if selectedView == "resources" { + ResourcesView() + Spacer() + } + if selectedView == "profiling" { + ProfilingView() + Spacer() + } + if selectedView == "traces" { + MemoryTracesView() + Spacer() + } + if selectedView == "object" { + ObjectView() + Spacer() + } + if selectedView == "variables" { + RuntimeVariablesView() + Spacer() + } + if selectedView == "console" { + ConsoleView() + Spacer() + } + } + .navigationTitle(project.title) + .navigationSubtitle(projectState) + .toolbar { + if projectState != "Stepping..." { + Button { + withAnimation { + if projectState == "Not started" { + startExecutable() + projectState = "Started" + } else { + stopExecutable() + projectState = "Not started" + } + } + } label: { + if projectState == "Not started" { + Image(systemName: "play.fill") + } else { + Image(systemName: "stop.fill") + } + }.help("Start the debug session") + } + if projectState == "Stepping..." || projectState == "Not started" { + Button { + withAnimation { + if projectState == "Stepping..." { + startExecutable() + projectState = "Not started" + } else { + stopExecutable() + projectState = "Stepping..." + } + } + } label: { + if projectState == "Stepping..." { + Image(systemName: "stop.fill") + } else { + Image(systemName: "rectangle.on.rectangle") + } + }.help("Step frame by frame") } - } label: { if projectState == "Stepping..." { - Image(systemName: "stop.fill") - } else { - Image(systemName: "rectangle.on.rectangle") + Button {} label: { + Image(systemName: "forward.circle.fill") + }.help("Step a frame") } - }.help("Step frame by frame") - } - if projectState == "Stepping..." { - Button {} label: { - Image(systemName: "forward.circle.fill") - }.help("Step a frame") + }.onChange(of: selectedView) { + print(selectedView) + } } - }.onChange(of: selectedView) { - print(selectedView) - } - .onAppear { - print("Initializing the listener") - listener = Listener(port: 5123) - listener?.start() + } else { + EmptyView() + .onAppear { + if let window = NSApplication.shared.windows.first(where: { $0.isKeyWindow }) { + window.close() + } + } } } } From 1ff1a34e5cce0d010d4c22cd155dc530387a2097 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Mon, 15 Dec 2025 20:34:17 +0100 Subject: [PATCH 05/12] Finished the log UI --- Atlas Tracer/Client/Interpreter.swift | 65 ++++ Atlas Tracer/Client/Listener.swift | 6 +- Atlas Tracer/Views/GeneralViews/LogView.swift | 337 +++++++++++++----- Atlas Tracer/Views/ProjectView.swift | 2 +- 4 files changed, 316 insertions(+), 94 deletions(-) create mode 100644 Atlas Tracer/Client/Interpreter.swift diff --git a/Atlas Tracer/Client/Interpreter.swift b/Atlas Tracer/Client/Interpreter.swift new file mode 100644 index 0000000..c027607 --- /dev/null +++ b/Atlas Tracer/Client/Interpreter.swift @@ -0,0 +1,65 @@ +// +// Interpreter.swift +// Atlas Tracer +// +// Created by Max Van den Eynde on 15/12/25. +// + +import Combine +import Foundation +import SwiftUI + +protocol Interpreter { + func incoming(_ message: String) +} + +struct DebugLog: Encodable, Decodable { + let severity: String + let message: String + let file: String + let line: Int + let type: String +} + +final class DebugInformation: ObservableObject { + static let shared = DebugInformation() + + @Published var logs: [DebugLog] = [] + + func addLog(_ log: DebugLog) { + DispatchQueue.main.async { + self.logs.append(log) + } + } + + func clearLogs() { + DispatchQueue.main.async { + self.logs.removeAll() + } + } + + private init() {} +} + +struct SimpleInformation: Decodable { + let type: String +} + +class MainInterpreter: Interpreter { + func incoming(_ message: String) { + guard let data = message.data(using: .utf8), + let simpleInfo = try? JSONDecoder().decode(SimpleInformation.self, from: data) + else { + return + } + + switch simpleInfo.type { + case "log": + if let log = try? JSONDecoder().decode(DebugLog.self, from: data) { + DebugInformation.shared.addLog(log) + } + default: + break + } + } +} diff --git a/Atlas Tracer/Client/Listener.swift b/Atlas Tracer/Client/Listener.swift index e8f2c39..58d9ddb 100644 --- a/Atlas Tracer/Client/Listener.swift +++ b/Atlas Tracer/Client/Listener.swift @@ -14,11 +14,13 @@ class Listener { private let port: UInt16 private var listener: NWListener? private let queue: DispatchQueue = .init(label: "ListenerQueue", qos: .background) + private var interpreter: Interpreter private(set) var messageHistory: [String] = [] - init(port: UInt16) { + init(port: UInt16, interpreter: Interpreter) { self.port = port + self.interpreter = interpreter } func start() { @@ -43,7 +45,7 @@ class Listener { connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, _ in if let data = data, !data.isEmpty { let text = String(decoding: data, as: UTF8.self) - print("Received: \(text)") + self?.interpreter.incoming(text) self?.messageHistory.append(text) } diff --git a/Atlas Tracer/Views/GeneralViews/LogView.swift b/Atlas Tracer/Views/GeneralViews/LogView.swift index 564a7b6..b6ec0df 100644 --- a/Atlas Tracer/Views/GeneralViews/LogView.swift +++ b/Atlas Tracer/Views/GeneralViews/LogView.swift @@ -34,94 +34,170 @@ struct LogCardView: View { case .log: return Color.green case .warning: - return Color.yellow + return Color.orange case .error: return Color.red } } - - func getIcon() -> some View { - if isLast { - return AnyView( - Image(systemName: "diamond.fill") - .foregroundStyle(colorFromLevel()) - ) - } else { - return AnyView( - Image(systemName: "diamond") - .foregroundStyle(colorFromLevel()) - .bold() - ) + + func iconName() -> String { + switch logEntry.level { + case .log: + return isLast ? "checkmark.circle.fill" : "checkmark.circle" + case .warning: + return isLast ? "exclamationmark.triangle.fill" : "exclamationmark.triangle" + case .error: + return isLast ? "xmark.circle.fill" : "xmark.circle" + } + } + + func severityText() -> String { + switch logEntry.level { + case .log: + return "LOG" + case .warning: + return "WARN" + case .error: + return "ERROR" } } var body: some View { - VStack { - RoundedRectangle(cornerRadius: 10) - .fill(.background) - .frame(height: 30) - .shadow(radius: 5) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(colorFromLevel(), lineWidth: 3) - ) - .overlay { - HStack { - getIcon() - .padding(.leading, 9) - .padding(.trailing, 5) + VStack(spacing: 0) { + HStack(spacing: 12) { + ZStack(alignment: .topTrailing) { + Image(systemName: iconName()) + .font(.system(size: 20, weight: isLast ? .bold : .medium)) + .foregroundStyle(colorFromLevel()) + .frame(width: 28, height: 28) + } + + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Text(severityText()) + .font(.system(size: 10, weight: .bold)) + .foregroundStyle(.white) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(colorFromLevel()) + ) + Text(logEntry.content) - .foregroundStyle(colorFromLevel()) - .bold() - .lineLimit(1) + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.primary) + .lineLimit(showDetail ? nil : 1) .truncationMode(.tail) - .frame(maxWidth: .infinity, alignment: .leading) - Spacer() - Text("at " + logEntry.file + ", line " + String(logEntry.line)) - .foregroundStyle(colorFromLevel()) - .italic() - .lineLimit(1) - .padding(.trailing, 8) + } + + HStack(spacing: 4) { + Image(systemName: "doc.text") + .font(.system(size: 10)) + .foregroundStyle(.secondary) + + Text(logEntry.file) + .font(.system(size: 11, weight: .regular)) + .foregroundStyle(.secondary) + + Text(":") + .font(.system(size: 11)) + .foregroundStyle(.secondary) + + Text("\(logEntry.line)") + .font(.system(size: 11, weight: .medium)) + .foregroundStyle(.secondary) + Spacer() + + Text(logEntry.time.formatted(date: .omitted, time: .shortened)) + .font(.system(size: 10, weight: .regular)) + .foregroundStyle(.tertiary) } } - .onTapGesture { - withAnimation { - showDetail.toggle() - } + + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(.tertiary) + .rotationEffect(.degrees(showDetail ? 90 : 0)) + .animation(.spring(response: 0.3), value: showDetail) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.2)) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .strokeBorder(colorFromLevel().opacity(0.3), lineWidth: 1) + ) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + showDetail.toggle() } + } if showDetail { - HStack { + VStack(alignment: .leading, spacing: 8) { + Divider() + .padding(.horizontal, 12) + + Text("Full Message") + .font(.system(size: 11, weight: .semibold)) + .foregroundStyle(.secondary) + .padding(.horizontal, 12) + Text(logEntry.content) - .foregroundStyle(Color.white) - .multilineTextAlignment(.leading) - .fixedSize(horizontal: false, vertical: false) - .padding(.horizontal, 8) - Spacer() - } - .background { - RoundedRectangle(cornerRadius: 10) - .fill(colorFromLevel()) - .frame(height: 30) + .font(.system(size: 13, weight: .regular, design: .monospaced)) + .foregroundStyle(.primary) + .padding(10) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(.systemGray)) + ) + .padding(.horizontal, 12) } - .padding(.vertical, 10) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color(.systemGray)) + ) + .transition(.opacity.combined(with: .move(edge: .top))) } } } } struct LogView: View { - @State private var logs: [LogEntry] = [ - LogEntry(content: "Hello from the log!", level: .log, file: "idk.h", line: 1, time: .now), - LogEntry(content: "Hello from the log!", level: .warning, file: "idk.h", line: 1, time: .now), - LogEntry(content: "Hello from the log!", level: .error, file: "idk.h", line: 1, time: .now), - LogEntry(content: "Hello from the log!", level: .log, file: "idk.h", line: 1, time: .now) - ] + private var logEntries: [LogEntry] { + debugInformation.logs.map { debugLog in + let level: LogLevel = { + switch debugLog.severity.lowercased() { + case "warning": return .warning + case "error": return .error + default: return .log + } + }() - @State private var selectedFilter = 0 + return LogEntry( + content: debugLog.message, + level: level, + file: debugLog.file, + line: debugLog.line, + time: Date() + ) + } + } + + private var filteredLogs: [LogEntry] { + logEntries.filter(applyFilter) + } + @State private var selectedFilter = 0 @Environment(\.appEnv) private var environment + @ObservedObject var debugInformation: DebugInformation = .shared var project: Project { if environment.currentProject != nil { @@ -146,47 +222,126 @@ struct LogView: View { return false } } + + func logCount(for level: LogLevel?) -> Int { + if let level = level { + return logEntries.filter { $0.level == level }.count + } + return logEntries.count + } var body: some View { - VStack(alignment: .leading) { - Text("Logs") - .font(.title) - .bold() - .padding(.horizontal) - .padding(.top) - Picker("Filter", selection: $selectedFilter) { - Text("All").tag(0) - if project.logTypes.contains(.logs) { - Text("Logs").tag(1) + VStack(spacing: 0) { + // Header + VStack(alignment: .leading, spacing: 16) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Logs") + .font(.system(size: 28, weight: .bold)) + + Text("\(filteredLogs.count) entries") + .font(.system(size: 14, weight: .regular)) + .foregroundStyle(.secondary) + } + + Spacer() + + // Clear button + Button(action: { + debugInformation.logs.removeAll() + }) { + HStack(spacing: 6) { + Image(systemName: "trash") + .font(.system(size: 12, weight: .semibold)) + Text("Clear") + .font(.system(size: 13, weight: .semibold)) + } + .foregroundStyle(.red) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background( + Capsule() + .fill(Color.red.opacity(0.1)) + ) + } + .buttonStyle(.plain) } - if project.logTypes.contains(.warnings) { - Text("Warnings").tag(2) + + Picker("Filter", selection: $selectedFilter) { + Label("\(logCount(for: nil))", systemImage: "line.3.horizontal.decrease.circle") + .tag(0) + + if project.logTypes.contains(.logs) { + Label("\(logCount(for: .log))", systemImage: "checkmark.circle") + .tag(1) + } + if project.logTypes.contains(.warnings) { + Label("\(logCount(for: .warning))", systemImage: "exclamationmark.triangle") + .tag(2) + } + if project.logTypes.contains(.errors) { + Label("\(logCount(for: .error))", systemImage: "xmark.circle") + .tag(3) + } } - if project.logTypes.contains(.errors) { - Text("Errors").tag(3) + .pickerStyle(.segmented) + } + .padding(20) + + if filteredLogs.isEmpty { + VStack(spacing: 12) { + Image(systemName: "tray") + .font(.system(size: 48, weight: .thin)) + .foregroundStyle(.tertiary) + + Text("No logs to display") + .font(.system(size: 16, weight: .medium)) + .foregroundStyle(.secondary) + + Text("Logs will appear here as they are generated") + .font(.system(size: 13, weight: .regular)) + .foregroundStyle(.tertiary) } - }.pickerStyle(.segmented).padding(.horizontal) - VStack { + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.white) + } else { ScrollView { - ForEach(logs) { log in - if applyFilter(log: log) { - LogCardView(logEntry: log, isLast: log.id == self.logs.last!.id) - .padding(.vertical, 4) - .padding(.horizontal, 10) + LazyVStack(spacing: 8) { + ForEach(Array(filteredLogs.enumerated()), id: \.element.id) { index, log in + HStack(alignment: .top, spacing: 12) { + VStack(spacing: 0) { + if index > 0 { + Rectangle() + .fill(Color.blue.opacity(0.3)) + .frame(width: 2, height: 20) + } + + Circle() + .fill(index == filteredLogs.count - 1 ? Color.blue : Color.blue.opacity(0.3)) + .frame(width: 8, height: 8) + + if index < filteredLogs.count - 1 { + Rectangle() + .fill(Color.blue.opacity(0.3)) + .frame(width: 2) + } + } + .frame(width: 8) + + LogCardView( + logEntry: log, + isLast: log.id == filteredLogs.last?.id + ) + } + .padding(.horizontal, 20) } } + .padding(.vertical, 20) } - }.padding().background { - HStack { - RoundedRectangle(cornerRadius: 10) - .frame(width: 5) - .padding(.vertical, 7) - .padding(.leading, 70) - .foregroundStyle(Color.blue) - Spacer() - } + .background(Color.white) } } + .background(Color.white) } } diff --git a/Atlas Tracer/Views/ProjectView.swift b/Atlas Tracer/Views/ProjectView.swift index 433c2ef..12a7227 100644 --- a/Atlas Tracer/Views/ProjectView.swift +++ b/Atlas Tracer/Views/ProjectView.swift @@ -37,7 +37,7 @@ struct ProjectView: View { process.currentDirectoryURL = executableURL.deletingLastPathComponent() process.environment = ProcessInfo.processInfo.environment - listener = Listener(port: 5123) + listener = Listener(port: 5123, interpreter: MainInterpreter()) listener?.start() do { From 90df6a5cc6c2ff6b14025ab857e3f0c4e4815e98 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Tue, 16 Dec 2025 07:32:23 +0100 Subject: [PATCH 06/12] Finished updating the Graphics View --- Atlas Tracer/Client/Interpreter.swift | 52 +- Atlas Tracer/Views/ProjectView.swift | 2 +- .../Views/TargetViews/GraphicsView.swift | 531 +++++++++--------- 3 files changed, 314 insertions(+), 271 deletions(-) diff --git a/Atlas Tracer/Client/Interpreter.swift b/Atlas Tracer/Client/Interpreter.swift index c027607..5f3bece 100644 --- a/Atlas Tracer/Client/Interpreter.swift +++ b/Atlas Tracer/Client/Interpreter.swift @@ -13,7 +13,7 @@ protocol Interpreter { func incoming(_ message: String) } -struct DebugLog: Encodable, Decodable { +struct DebugLog: Codable, Equatable { let severity: String let message: String let file: String @@ -21,10 +21,48 @@ struct DebugLog: Encodable, Decodable { let type: String } +enum DebugDrawCallType: Int, Codable { + case draw = 1 + case indexed = 2 + case patched = 3 +} + +struct DrawCallInfo: Codable, Equatable { + let type: String + let frameNumber: Int + let drawCallType: DebugDrawCallType + let callerObject: String + + enum CodingKeys: String, CodingKey { + case type + case frameNumber = "frame_number" + case drawCallType = "draw_call_type" + case callerObject = "caller_object" + } +} + +struct FrameDrawCallInfo: Codable, Equatable { + let type: String + let frameNumber: Int + let drawCallCount: Int + let frameTimeMs: Double + let fps: Double + + enum CodingKeys: String, CodingKey { + case type + case frameNumber = "frame_number" + case drawCallCount = "draw_call_count" + case frameTimeMs = "frame_time_ms" + case fps + } +} + final class DebugInformation: ObservableObject { static let shared = DebugInformation() @Published var logs: [DebugLog] = [] + @Published var drawCalls: [DrawCallInfo] = [] + @Published var frameDrawInsights: [FrameDrawCallInfo] = [] func addLog(_ log: DebugLog) { DispatchQueue.main.async { @@ -58,6 +96,18 @@ class MainInterpreter: Interpreter { if let log = try? JSONDecoder().decode(DebugLog.self, from: data) { DebugInformation.shared.addLog(log) } + case "draw_call": + if let info = try? JSONDecoder().decode(DrawCallInfo.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.drawCalls.append(info) + } + } + case "frame_draw_info": + if let info = try? JSONDecoder().decode(FrameDrawCallInfo.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.frameDrawInsights.append(info) + } + } default: break } diff --git a/Atlas Tracer/Views/ProjectView.swift b/Atlas Tracer/Views/ProjectView.swift index 12a7227..d3731f4 100644 --- a/Atlas Tracer/Views/ProjectView.swift +++ b/Atlas Tracer/Views/ProjectView.swift @@ -72,7 +72,7 @@ struct ProjectView: View { Section("Targets") { if project.mainProjectType != .custom { - NavigationLink(value: project.mainProjectType.getName()) { + NavigationLink(value: project.mainProjectType.getName().lowercased()) { Label(project.mainProjectType.getName(), systemImage: project.mainProjectType.getIconName()) } } else { diff --git a/Atlas Tracer/Views/TargetViews/GraphicsView.swift b/Atlas Tracer/Views/TargetViews/GraphicsView.swift index c289063..2f9ca84 100644 --- a/Atlas Tracer/Views/TargetViews/GraphicsView.swift +++ b/Atlas Tracer/Views/TargetViews/GraphicsView.swift @@ -8,6 +8,13 @@ import Charts import SwiftUI +extension Sequence where Element: Hashable { + func unique() -> [Element] { + var seen = Set() + return filter { seen.insert($0).inserted } + } +} + enum DrawCallType: String, Codable, CaseIterable { case drawCall case indexedDrawCall @@ -28,21 +35,22 @@ enum DrawCallType: String, Codable, CaseIterable { case .patchDrawCall: return "Patch Draw" } } + + static func fromDebug(_ type: DebugDrawCallType) -> DrawCallType { + switch type { + case .draw: return .drawCall + case .indexed: return .indexedDrawCall + case .patched: return .patchDrawCall + } + } } -struct DrawCall { // Sent from the client - let callerObjectId: String - let time: Date - let type: DrawCallType - let frame: Int -} - -struct FrameData: Identifiable { // Sent from the client +struct FrameData: Identifiable { let id = UUID() let frame: Int let drawCallCount: Int let fps: Double - let frameTime: Double // in milliseconds + let frameTime: Double } struct FrameDrawCallTypeData: Identifiable { @@ -59,77 +67,59 @@ struct ObjectStats: Identifiable { let drawCallBreakdown: [DrawCallType: Int] } -struct GraphicsView: View { - @State private var drawCalls: [DrawCall] = [] - @State private var frameData: [FrameData] = [] - @State private var frameTypeData: [FrameDrawCallTypeData] = [] - @State private var objectStats: [ObjectStats] = [] - - let objectA = "ObjectA" - let objectB = "ObjectB" - - func createRandomDrawCalls(count: Int) { - drawCalls.removeAll() - frameData.removeAll() - frameTypeData.removeAll() - objectStats.removeAll() - - var currentTime = Date() - var currentFrame = 0 - let drawCallsPerFrame = 10 - - for i in 0 ..< count { - let caller = Bool.random() ? objectA : objectB - let type = DrawCallType.allCases.randomElement()! - - if i > 0 && i % drawCallsPerFrame == 0 { - currentFrame += 1 - } - - let call = DrawCall( - callerObjectId: caller, - time: currentTime, - type: type, - frame: currentFrame - ) - - drawCalls.append(call) - currentTime = currentTime.addingTimeInterval(0.001) +struct FrameStats { + let avgFPS: Double + let minFPS: Double + let maxFPS: Double + let avgDrawCalls: Double + let maxDrawCalls: Int + let totalFrames: Int + + static let empty = FrameStats(frameData: []) + + init(frameData: [FrameData]) { + guard !frameData.isEmpty else { + self.avgFPS = 0 + self.minFPS = 0 + self.maxFPS = 0 + self.avgDrawCalls = 0 + self.maxDrawCalls = 0 + self.totalFrames = 0 + return } - - let groupedByFrame = Dictionary(grouping: drawCalls, by: { $0.frame }) - frameData = groupedByFrame.map { frame, calls in - let baseFrameTime = Double(calls.count) * 0.8 + Double.random(in: 2...8) - let fps = 1000.0 / baseFrameTime - - return FrameData( - frame: frame, - drawCallCount: calls.count, - fps: fps, - frameTime: baseFrameTime - ) - }.sorted(by: { $0.frame < $1.frame }) - frameTypeData = groupedByFrame.flatMap { frame, calls in - let typeGroups = Dictionary(grouping: calls, by: { $0.type }) - return typeGroups.map { type, typeCalls in - FrameDrawCallTypeData(frame: frame, type: type, count: typeCalls.count) - } - }.sorted(by: { $0.frame < $1.frame }) + var sumFPS = 0.0 + var sumDrawCalls = 0 + var min = Double.infinity + var max = 0.0 + var maxDC = 0 - let groupedByObject = Dictionary(grouping: drawCalls, by: { $0.callerObjectId }) - objectStats = groupedByObject.map { objectId, calls in - let breakdown = Dictionary(grouping: calls, by: { $0.type }) - .mapValues { $0.count } - - return ObjectStats( - objectId: objectId, - totalDrawCalls: calls.count, - drawCallBreakdown: breakdown - ) - }.sorted(by: { $0.totalDrawCalls > $1.totalDrawCalls }) + for frame in frameData { + sumFPS += frame.fps + sumDrawCalls += frame.drawCallCount + min = Swift.min(min, frame.fps) + max = Swift.max(max, frame.fps) + maxDC = Swift.max(maxDC, frame.drawCallCount) + } + + let count = frameData.count + self.avgFPS = sumFPS / Double(count) + self.minFPS = min + self.maxFPS = max + self.avgDrawCalls = Double(sumDrawCalls) / Double(count) + self.maxDrawCalls = maxDC + self.totalFrames = count } +} +struct GraphicsView: View { + @State private var frameData: [FrameData] = [] + @State private var frameTypeData: [FrameDrawCallTypeData] = [] + @State private var objectStats: [ObjectStats] = [] + @State private var stats = FrameStats.empty + + @ObservedObject var debugSession: DebugInformation = .shared + var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { @@ -138,215 +128,218 @@ struct GraphicsView: View { .bold() if !frameData.isEmpty { - VStack(alignment: .leading) { - Text("FPS Over Time") - .font(.headline) - - Chart(frameData) { data in - LineMark( - x: .value("Frame", data.frame), - y: .value("FPS", data.fps) - ) - .foregroundStyle(.green.gradient) - .interpolationMethod(.catmullRom) - - AreaMark( - x: .value("Frame", data.frame), - y: .value("FPS", data.fps) - ) - .foregroundStyle(.green.opacity(0.2)) - .interpolationMethod(.catmullRom) - - RuleMark(y: .value("Target", 60)) - .foregroundStyle(.yellow.opacity(0.5)) - .lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 5])) - } - .chartYScale(domain: 0...max(maxFPS + 10, 70)) - .chartYAxis { - AxisMarks(position: .leading) - } - .chartXAxis { - AxisMarks(values: .automatic(desiredCount: 10)) - } - .frame(height: 200) - - HStack(spacing: 30) { - VStack(alignment: .leading) { - Text("Avg FPS") - .font(.caption) - .foregroundColor(.secondary) - Text(String(format: "%.1f", averageFPS)) - .font(.title2) - .bold() - .foregroundColor(averageFPS >= 60 ? .green : .orange) - } - - VStack(alignment: .leading) { - Text("Min FPS") - .font(.caption) - .foregroundColor(.secondary) - Text(String(format: "%.1f", minFPS)) - .font(.title2) - .bold() - .foregroundColor(minFPS >= 60 ? .green : .red) - } - - VStack(alignment: .leading) { - Text("Max FPS") - .font(.caption) - .foregroundColor(.secondary) - Text(String(format: "%.1f", maxFPS)) - .font(.title2) - .bold() - .foregroundColor(.green) - } - } - .padding(.top, 10) - } - - Divider() - .padding(.vertical, 10) - - VStack(alignment: .leading) { - HStack { - Text("Draw Calls Per Frame by Type") - .font(.headline) - - Spacer() - - HStack(spacing: 15) { - ForEach(DrawCallType.allCases, id: \.self) { type in - HStack(spacing: 4) { - Circle() - .fill(type.color) - .frame(width: 10, height: 10) - Text(type.displayName) - .font(.caption) - } - } - } - } - - Chart(frameTypeData) { data in - BarMark( - x: .value("Frame", data.frame), - y: .value("Count", data.count) - ) - .foregroundStyle(data.type.color.gradient) - .position(by: .value("Type", data.type.rawValue)) - } - .chartYAxis { - AxisMarks(position: .leading) - } - .chartXAxis { - AxisMarks(values: .automatic(desiredCount: 10)) - } - .frame(height: 250) - - HStack(spacing: 30) { - VStack(alignment: .leading) { - Text("Total Frames") - .font(.caption) - .foregroundColor(.secondary) - Text("\(frameData.count)") - .font(.title2) - .bold() - } - - VStack(alignment: .leading) { - Text("Avg Draw Calls") - .font(.caption) - .foregroundColor(.secondary) - Text(String(format: "%.1f", averageDrawCalls)) - .font(.title2) - .bold() - } - - VStack(alignment: .leading) { - Text("Max Draw Calls") - .font(.caption) - .foregroundColor(.secondary) - Text("\(maxDrawCalls)") - .font(.title2) - .bold() - .foregroundColor(.red) - } - } - .padding(.top, 10) - } - - Divider() - .padding(.vertical, 10) - - VStack(alignment: .leading, spacing: 12) { - Text("Object Draw Call Statistics") - .font(.headline) - - ForEach(objectStats) { stat in - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(stat.objectId) - .font(.subheadline) - .bold() - - Spacer() - - Text("\(stat.totalDrawCalls) total calls") - .font(.subheadline) - .foregroundColor(.secondary) - } - - HStack(spacing: 20) { - ForEach(DrawCallType.allCases, id: \.self) { type in - if let count = stat.drawCallBreakdown[type], count > 0 { - HStack(spacing: 4) { - Circle() - .fill(type.color) - .frame(width: 8, height: 8) - Text("\(type.displayName): \(count)") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - } - } - .padding() - .background(Color.secondary.opacity(0.1)) - .cornerRadius(8) - } - } + fpsChartSection + Divider().padding(.vertical, 10) + drawCallsChartSection + Divider().padding(.vertical, 10) + objectStatsSection } } .padding() + .task(id: debugSession.frameDrawInsights) { + refreshData() + } + .task(id: debugSession.drawCalls) { + refreshData() + } + } + } + + private func refreshData() { + let insights = debugSession.frameDrawInsights + let calls = debugSession.drawCalls + + frameData = insights.map { + FrameData(frame: $0.frameNumber, drawCallCount: $0.drawCallCount, + fps: $0.fps, frameTime: $0.frameTimeMs) } - .onAppear { - createRandomDrawCalls(count: 100) + + stats = FrameStats(frameData: frameData) + + var typeData: [FrameDrawCallTypeData] = [] + typeData.reserveCapacity(insights.count * 3) + + for insight in insights { + let frameCalls = calls.filter { $0.frameNumber == insight.frameNumber } + + for type in DrawCallType.allCases { + let debugType: DebugDrawCallType = switch type { + case .drawCall: .draw + case .indexedDrawCall: .indexed + case .patchDrawCall: .patched + } + + let count = frameCalls.lazy.filter { $0.drawCallType == debugType }.count + if count > 0 { + typeData.append(FrameDrawCallTypeData(frame: insight.frameNumber, type: type, count: count)) + } + } + } + frameTypeData = typeData + + let objects = calls.map(\.callerObject).unique() + objectStats = objects.map { objectId in + let objectCalls = calls.filter { $0.callerObject == objectId } + var breakdown: [DrawCallType: Int] = [:] + + for call in objectCalls { + let type = DrawCallType.fromDebug(call.drawCallType) + breakdown[type, default: 0] += 1 + } + + return ObjectStats(objectId: objectId, totalDrawCalls: objectCalls.count, + drawCallBreakdown: breakdown) + } + } + + private var fpsChartSection: some View { + VStack(alignment: .leading) { + Text("FPS Over Time").font(.headline) + fpsChart.frame(height: 200) + + HStack(spacing: 30) { + StatColumn(title: "Avg FPS", value: String(format: "%.1f", stats.avgFPS), + color: stats.avgFPS >= 60 ? .green : .orange) + StatColumn(title: "Min FPS", value: String(format: "%.1f", stats.minFPS), + color: stats.minFPS >= 60 ? .green : .red) + StatColumn(title: "Max FPS", value: String(format: "%.1f", stats.maxFPS), + color: .green) + } + .padding(.top, 10) + } + } + + private var fpsChart: some View { + let minFrame = frameData.map(\.frame).min() ?? 0 + let maxFrame = frameData.map(\.frame).max() ?? 100 + let step = max(1, (maxFrame - minFrame) / 10) + + return Chart(frameData) { data in + LineMark(x: .value("Frame", data.frame), y: .value("FPS", data.fps)) + .foregroundStyle(.green.gradient) + .interpolationMethod(.catmullRom) + + AreaMark(x: .value("Frame", data.frame), y: .value("FPS", data.fps)) + .foregroundStyle(.green.opacity(0.2)) + .interpolationMethod(.catmullRom) + + RuleMark(y: .value("Target", 60)) + .foregroundStyle(.yellow.opacity(0.5)) + .lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 5])) + } + .chartXScale(domain: minFrame...maxFrame) + .chartYScale(domain: 0...max(stats.maxFPS + 10, 70)) + .chartYAxis { AxisMarks(position: .leading) } + .chartXAxis { + AxisMarks(values: Array(stride(from: minFrame, through: maxFrame, by: step))) { _ in + AxisGridLine() + AxisValueLabel() + } } } - private var averageDrawCalls: Double { - guard !frameData.isEmpty else { return 0 } - let total = frameData.reduce(0) { $0 + $1.drawCallCount } - return Double(total) / Double(frameData.count) + private var drawCallsChartSection: some View { + VStack(alignment: .leading) { + HStack { + Text("Draw Calls Per Frame by Type").font(.headline) + Spacer() + + HStack(spacing: 15) { + ForEach(DrawCallType.allCases, id: \.self) { type in + Label { + Text(type.displayName).font(.caption) + } icon: { + Circle().fill(type.color).frame(width: 10, height: 10) + } + .labelStyle(.titleAndIcon) + } + } + } + + drawCallsChart.frame(height: 250) + + HStack(spacing: 30) { + StatColumn(title: "Total Frames", value: "\(stats.totalFrames)", color: .primary) + StatColumn(title: "Avg Draw Calls", value: String(format: "%.1f", stats.avgDrawCalls), color: .primary) + StatColumn(title: "Max Draw Calls", value: "\(stats.maxDrawCalls)", color: .red) + } + .padding(.top, 10) + } } - private var maxDrawCalls: Int { - frameData.map { $0.drawCallCount }.max() ?? 0 + private var drawCallsChart: some View { + let minFrame = frameTypeData.map(\.frame).min() ?? 0 + let maxFrame = frameTypeData.map(\.frame).max() ?? 100 + let step = max(1, (maxFrame - minFrame) / 10) + + return Chart(frameTypeData) { data in + BarMark(x: .value("Frame", data.frame), y: .value("Count", data.count)) + .foregroundStyle(data.type.color.gradient) + .position(by: .value("Type", data.type.rawValue)) + } + .chartXScale(domain: minFrame...maxFrame) + .chartYAxis { AxisMarks(position: .leading) } + .chartXAxis { + AxisMarks(values: Array(stride(from: minFrame, through: maxFrame, by: step))) { _ in + AxisGridLine() + AxisValueLabel() + } + } } - private var averageFPS: Double { - guard !frameData.isEmpty else { return 0 } - let total = frameData.reduce(0.0) { $0 + $1.fps } - return total / Double(frameData.count) + private var objectStatsSection: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Object Draw Call Statistics").font(.headline) + + ForEach(objectStats) { stat in + ObjectStatCard(stat: stat) + } + } } +} + +struct StatColumn: View { + let title: String + let value: String + let color: Color - private var minFPS: Double { - frameData.map { $0.fps }.min() ?? 0 + var body: some View { + VStack(alignment: .leading) { + Text(title).font(.caption).foregroundColor(.secondary) + Text(value).font(.title2).bold().foregroundColor(color) + } } +} + +struct ObjectStatCard: View { + let stat: ObjectStats - private var maxFPS: Double { - frameData.map { $0.fps }.max() ?? 0 + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(stat.objectId).font(.subheadline).bold() + Spacer() + Text("\(stat.totalDrawCalls) total calls") + .font(.subheadline).foregroundColor(.secondary) + } + + HStack(spacing: 20) { + ForEach(DrawCallType.allCases, id: \.self) { type in + if let count = stat.drawCallBreakdown[type], count > 0 { + Label { + Text("\(type.displayName): \(count)") + .font(.caption).foregroundColor(.secondary) + } icon: { + Circle().fill(type.color).frame(width: 8, height: 8) + } + .labelStyle(.titleAndIcon) + } + } + } + } + .padding() + .background(Color.secondary.opacity(0.1)) + .cornerRadius(8) } } From d7634db55f399c08358fe58f576aca60ab74e19d Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Tue, 16 Dec 2025 07:35:59 +0100 Subject: [PATCH 07/12] Finished some view changes --- Atlas Tracer/Views/TargetViews/GraphicsView.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Atlas Tracer/Views/TargetViews/GraphicsView.swift b/Atlas Tracer/Views/TargetViews/GraphicsView.swift index 2f9ca84..a3cf65c 100644 --- a/Atlas Tracer/Views/TargetViews/GraphicsView.swift +++ b/Atlas Tracer/Views/TargetViews/GraphicsView.swift @@ -256,7 +256,10 @@ struct GraphicsView: View { } } - drawCallsChart.frame(height: 250) + ScrollView(.horizontal, showsIndicators: true) { + drawCallsChart + .frame(width: max(600, CGFloat(frameData.count) * 20), height: 250) + } HStack(spacing: 30) { StatColumn(title: "Total Frames", value: "\(stats.totalFrames)", color: .primary) From ba02b76c53e6c46415ba63f4c12af4f1db2ace79 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Thu, 18 Dec 2025 07:31:49 +0100 Subject: [PATCH 08/12] Finished the resource debugging view --- Atlas Tracer/Client/Interpreter.swift | 75 +++++- .../Views/TargetViews/ResourcesView.swift | 213 ++++++++++-------- 2 files changed, 190 insertions(+), 98 deletions(-) diff --git a/Atlas Tracer/Client/Interpreter.swift b/Atlas Tracer/Client/Interpreter.swift index 5f3bece..74230e0 100644 --- a/Atlas Tracer/Client/Interpreter.swift +++ b/Atlas Tracer/Client/Interpreter.swift @@ -9,6 +9,25 @@ import Combine import Foundation import SwiftUI +enum DebugDrawCallType: Int, Codable { + case draw = 1 + case indexed = 2 + case patched = 3 +} + +enum DebugResourceType: Int, Codable { + case texture = 1 + case buffer = 2 + case shader = 3 + case mesh = 4 +} + +enum DebugResourceOperation: Int, Codable { + case created = 1 + case loaded = 2 + case unloaded = 3 +} + protocol Interpreter { func incoming(_ message: String) } @@ -21,12 +40,6 @@ struct DebugLog: Codable, Equatable { let type: String } -enum DebugDrawCallType: Int, Codable { - case draw = 1 - case indexed = 2 - case patched = 3 -} - struct DrawCallInfo: Codable, Equatable { let type: String let frameNumber: Int @@ -57,12 +70,50 @@ struct FrameDrawCallInfo: Codable, Equatable { } } +struct DebugResourceEvent: Codable, Equatable { + let type: String + let callerObject: String + let resourceType: DebugResourceType + let operation: DebugResourceOperation + let frameNumber: Int + let sizeMb: Float + + enum CodingKeys: String, CodingKey { + case type + case callerObject = "caller_object" + case resourceType = "resource_type" + case frameNumber = "frame_number" + case sizeMb = "size_mb" + case operation + } +} + +struct DebugFrameResourceInformation: Codable, Equatable { + let type: String + let frameNumber: Int + let resourcesCreated: Int + let resourcesUnloaded: Int + let resourcesLoaded: Int + let totalMemoryMb: Float + + enum CodingKeys: String, CodingKey { + case type + case frameNumber = "frame_number" + case resourcesCreated = "resources_created" + case resourcesUnloaded = "resources_unloaded" + case resourcesLoaded = "resources_loaded" + case totalMemoryMb = "total_memory_mb" + } +} + final class DebugInformation: ObservableObject { static let shared = DebugInformation() @Published var logs: [DebugLog] = [] @Published var drawCalls: [DrawCallInfo] = [] @Published var frameDrawInsights: [FrameDrawCallInfo] = [] + @Published var resourceEvents: [DebugResourceEvent] = [] + @Published var frameResourceInformation: [DebugFrameResourceInformation] = [] func addLog(_ log: DebugLog) { DispatchQueue.main.async { @@ -108,6 +159,18 @@ class MainInterpreter: Interpreter { DebugInformation.shared.frameDrawInsights.append(info) } } + case "resource_event": + if let info = try? JSONDecoder().decode(DebugResourceEvent.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.resourceEvents.append(info) + } + } + case "frame_resources_info": + if let info = try? JSONDecoder().decode(DebugFrameResourceInformation.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.frameResourceInformation.append(info) + } + } default: break } diff --git a/Atlas Tracer/Views/TargetViews/ResourcesView.swift b/Atlas Tracer/Views/TargetViews/ResourcesView.swift index 91f942e..26b1615 100644 --- a/Atlas Tracer/Views/TargetViews/ResourcesView.swift +++ b/Atlas Tracer/Views/TargetViews/ResourcesView.swift @@ -48,7 +48,7 @@ enum ResourceOperation { case unloaded } -struct ResourceEvent { // Obtained from the engine +struct ResourceEvent { let objectId: String let type: ResourceType let operation: ResourceOperation @@ -57,7 +57,7 @@ struct ResourceEvent { // Obtained from the engine let sizeMB: Double } -struct ResourcesFrameData: Identifiable { // Obtained from the engine +struct ResourcesFrameData: Identifiable { let id = UUID() let frame: Int let frameTime: Double @@ -89,110 +89,121 @@ struct ResourcesView: View { @State private var objectStats: [ResourcesObjectStats] = [] @State private var currentTotalMemoryMB: Double = 0 - let objectA = "ObjectA" - let objectB = "ObjectB" - let objectC = "ObjectC" + @ObservedObject var debugInformation: DebugInformation = .shared - func createRandomResourceEvents(count: Int) { - resourceEvents.removeAll() - frameData.removeAll() - typeFrameData.removeAll() - objectStats.removeAll() + func refreshData() { + let debugResourceEvents = debugInformation.resourceEvents + let debugFrameResourceInsights = debugInformation.frameResourceInformation - var currentTime = Date() - var currentFrame = 0 - let eventsPerFrame = 5 - var activeResources: [ResourceEvent] = [] - - for i in 0 ..< count { - let objects = [objectA, objectB, objectC] - let objectId = objects.randomElement()! - let type = ResourceType.allCases.randomElement()! - - if i > 0 && i % eventsPerFrame == 0 { - currentFrame += 1 + // Convert debug resource events to UI resource events + var newResourceEvents: [ResourceEvent] = [] + for event in debugResourceEvents { + let type: ResourceType + switch event.resourceType { + case .texture: type = .texture + case .buffer: type = .buffer + case .shader: type = .shader + case .mesh: type = .mesh } let operation: ResourceOperation - if activeResources.count < 10 || Double.random(in: 0...1) < 0.7 { - operation = Bool.random() ? .created : .loaded - } else { - operation = .unloaded + switch event.operation { + case .created: operation = .created + case .loaded: operation = .loaded + case .unloaded: operation = .unloaded } - let sizeMB = type.averageSizeMB - - let event = ResourceEvent( - objectId: objectId, + newResourceEvents.append(ResourceEvent( + objectId: event.callerObject, type: type, operation: operation, - frame: currentFrame, - time: currentTime, - sizeMB: sizeMB - ) - - resourceEvents.append(event) + frame: event.frameNumber, + time: Date(), + sizeMB: Double(event.sizeMb) + )) + } + + // Convert frame resource information to frame data + var newFrameData: [ResourcesFrameData] = [] + for frameInfo in debugFrameResourceInsights { + // Try to get frame time from draw call insights + let frameTime = debugInformation.frameDrawInsights + .first(where: { $0.frameNumber == frameInfo.frameNumber })?.frameTimeMs ?? 16.67 - if operation == .created || operation == .loaded { - activeResources.append(event) - } else if operation == .unloaded && !activeResources.isEmpty { - activeResources.removeFirst() + newFrameData.append(ResourcesFrameData( + frame: frameInfo.frameNumber, + frameTime: frameTime, + resourcesLoaded: frameInfo.resourcesLoaded, + resourcesCreated: frameInfo.resourcesCreated, + resourcesUnloaded: frameInfo.resourcesUnloaded, + totalMemoryMB: Double(frameInfo.totalMemoryMb) + )) + } + + // Sort by frame number + newFrameData.sort { $0.frame < $1.frame } + + // Calculate type frame data (resources by type per frame) + var typeCountsByFrame: [Int: [ResourceType: Int]] = [:] + for event in newResourceEvents { + if event.operation != .unloaded { + typeCountsByFrame[event.frame, default: [:]][event.type, default: 0] += 1 + } + } + + var newTypeFrameData: [ResourceTypeFrameData] = [] + for (frame, typeCounts) in typeCountsByFrame { + for (type, count) in typeCounts { + newTypeFrameData.append(ResourceTypeFrameData( + frame: frame, + type: type, + count: count + )) } - - currentTime = currentTime.addingTimeInterval(0.002) } + newTypeFrameData.sort { $0.frame < $1.frame } - let groupedByFrame = Dictionary(grouping: resourceEvents, by: { $0.frame }) - var cumulativeMemory: Double = 0 + // Calculate object statistics + var objectResourceMap: [String: [ResourceEvent]] = [:] + for event in newResourceEvents { + objectResourceMap[event.objectId, default: []].append(event) + } - frameData = groupedByFrame.sorted(by: { $0.key < $1.key }).map { frame, events in - let created = events.filter { $0.operation == .created }.count - let loaded = events.filter { $0.operation == .loaded }.count - let unloaded = events.filter { $0.operation == .unloaded }.count - - let memoryAdded = events.filter { $0.operation == .created || $0.operation == .loaded } - .reduce(0.0) { $0 + $1.sizeMB } - let memoryRemoved = events.filter { $0.operation == .unloaded } - .reduce(0.0) { $0 + $1.sizeMB } - cumulativeMemory += memoryAdded - memoryRemoved - cumulativeMemory = max(0, cumulativeMemory) + var newObjectStats: [ResourcesObjectStats] = [] + for (objectId, events) in objectResourceMap { + var breakdown: [ResourceType: Int] = [:] + var totalMemory: Double = 0 - let frameTime = Double(created + loaded) * 1.2 + Double(unloaded) * 0.5 + Double.random(in: 8...15) + for event in events { + if event.operation != .unloaded { + breakdown[event.type, default: 0] += 1 + totalMemory += event.sizeMB + } + } - return ResourcesFrameData( - frame: frame, - frameTime: frameTime, - resourcesLoaded: loaded, - resourcesCreated: created, - resourcesUnloaded: unloaded, - totalMemoryMB: cumulativeMemory - ) + let resourceCount = breakdown.values.reduce(0, +) + if resourceCount > 0 { + newObjectStats.append(ResourcesObjectStats( + objectId: objectId, + resourceCount: resourceCount, + totalMemoryMB: totalMemory, + resourceBreakdown: breakdown + )) + } } - currentTotalMemoryMB = cumulativeMemory + // Sort by memory usage + newObjectStats.sort { $0.totalMemoryMB > $1.totalMemoryMB } - typeFrameData = groupedByFrame.flatMap { frame, events in - let activeEvents = events.filter { $0.operation == .created || $0.operation == .loaded } - let typeGroups = Dictionary(grouping: activeEvents, by: { $0.type }) - return typeGroups.map { type, typeEvents in - ResourceTypeFrameData(frame: frame, type: type, count: typeEvents.count) - } - }.sorted(by: { $0.frame < $1.frame }) + // Get current total memory + let currentTotalMemoryMb = newFrameData.last?.totalMemoryMB ?? 0 - let activeEvents = resourceEvents.filter { $0.operation == .created || $0.operation == .loaded } - let groupedByObject = Dictionary(grouping: activeEvents, by: { $0.objectId }) - objectStats = groupedByObject.map { objectId, events in - let breakdown = Dictionary(grouping: events, by: { $0.type }) - .mapValues { $0.count } - let totalMemory = events.reduce(0.0) { $0 + $1.sizeMB } - - return ResourcesObjectStats( - objectId: objectId, - resourceCount: events.count, - totalMemoryMB: totalMemory, - resourceBreakdown: breakdown - ) - }.sorted(by: { $0.resourceCount > $1.resourceCount }) + // Update state + resourceEvents = newResourceEvents + frameData = newFrameData + typeFrameData = newTypeFrameData + objectStats = newObjectStats + currentTotalMemoryMB = currentTotalMemoryMb } var body: some View { @@ -518,17 +529,23 @@ struct ResourcesView: View { .cornerRadius(8) } } + } else { + Text("No resource data available") + .font(.headline) + .foregroundColor(.secondary) + .padding() } - - Button("Generate Random Data (150 events)") { - createRandomResourceEvents(count: 150) - } - .buttonStyle(.borderedProminent) } .padding() } .onAppear { - createRandomResourceEvents(count: 150) + refreshData() + } + .onChange(of: debugInformation.resourceEvents) { + refreshData() + } + .onChange(of: debugInformation.frameResourceInformation) { + refreshData() } } @@ -565,6 +582,18 @@ struct ResourcesView: View { } } +extension DebugResourceType: CaseIterable { + static var allCases: [DebugResourceType] { + [.texture, .buffer, .shader, .mesh] + } +} + +extension DebugResourceOperation: CaseIterable { + static var allCases: [DebugResourceOperation] { + [.created, .loaded, .unloaded] + } +} + #Preview { ResourcesView() } From e73082faad373e0864ed8c67fd9038823320abe4 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Thu, 18 Dec 2025 20:32:57 +0100 Subject: [PATCH 09/12] Added the object debug pipeline and type to the application --- Atlas Tracer/Client/Interpreter.swift | 49 ++++++++ .../Views/TargetViews/ObjectView.swift | 116 ++++++++---------- 2 files changed, 102 insertions(+), 63 deletions(-) diff --git a/Atlas Tracer/Client/Interpreter.swift b/Atlas Tracer/Client/Interpreter.swift index 74230e0..53fa328 100644 --- a/Atlas Tracer/Client/Interpreter.swift +++ b/Atlas Tracer/Client/Interpreter.swift @@ -106,6 +106,41 @@ struct DebugFrameResourceInformation: Codable, Equatable { } } +enum DebugObjectType: Int, Codable { + case staticMesh = 1 + case skeletalMesh = 2 + case particleSystem = 3 + case lightProbe = 4 + case terrain = 5 + case other = 6 +} + +struct DebugObjectPacket: Codable, Equatable { + let type: String + let id: String + let drawCalls: Int + let objectType: DebugObjectType + let triangleCount: Int + let materialCount: Int + let vertexBufferMb: Float + let indexBufferMb: Float + let textureCount: Int + let frameCount: Int + + enum CodingKeys: String, CodingKey { + case type + case id + case drawCalls = "draw_calls" + case objectType = "object_type" + case triangleCount = "triangle_count" + case materialCount = "material_count" + case vertexBufferMb = "vertex_buffer_mb" + case indexBufferMb = "index_buffer_mb" + case textureCount = "texture_count" + case frameCount = "frame_count" + } +} + final class DebugInformation: ObservableObject { static let shared = DebugInformation() @@ -114,6 +149,7 @@ final class DebugInformation: ObservableObject { @Published var frameDrawInsights: [FrameDrawCallInfo] = [] @Published var resourceEvents: [DebugResourceEvent] = [] @Published var frameResourceInformation: [DebugFrameResourceInformation] = [] + @Published var objectData: [DebugObjectPacket] = [] func addLog(_ log: DebugLog) { DispatchQueue.main.async { @@ -171,6 +207,19 @@ class MainInterpreter: Interpreter { DebugInformation.shared.frameResourceInformation.append(info) } } + case "debug_object": + if let info = try? JSONDecoder().decode(DebugObjectPacket.self, from: data) { + DispatchQueue.main.async { + if let last = DebugInformation.shared.objectData.last { + if last.frameCount != info.frameCount { + DebugInformation.shared.objectData.removeAll() + } + DebugInformation.shared.objectData.append(info) + } else { + DebugInformation.shared.objectData.append(info) + } + } + } default: break } diff --git a/Atlas Tracer/Views/TargetViews/ObjectView.swift b/Atlas Tracer/Views/TargetViews/ObjectView.swift index 592bb12..d65a546 100644 --- a/Atlas Tracer/Views/TargetViews/ObjectView.swift +++ b/Atlas Tracer/Views/TargetViews/ObjectView.swift @@ -41,6 +41,19 @@ enum ObjectCategory: String, CaseIterable, Identifiable, Codable, Hashable { } } +private extension ObjectCategory { + static func fromDebug(_ type: DebugObjectType) -> ObjectCategory { + switch type { + case .staticMesh: return .staticMesh + case .skeletalMesh: return .skeletalMesh + case .particleSystem: return .particleSystem + case .lightProbe: return .lightProbe + case .terrain: return .terrain + case .other: return .other + } + } +} + struct TracedObject: Identifiable, Hashable { let id = UUID() let name: String @@ -73,6 +86,8 @@ struct CategoryFrameSample: Identifiable { } struct ObjectView: View { + @ObservedObject var debugSession: DebugInformation = .shared + @State private var objects: [TracedObject] = [] @State private var samples: [ObjectMetricSample] = [] @State private var categorySamples: [CategoryFrameSample] = [] @@ -80,8 +95,6 @@ struct ObjectView: View { @State private var selectedObject: TracedObject? @State private var selectedFrameIndex: Int = 0 - private let frameCount: Int = 240 - var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { @@ -104,9 +117,12 @@ struct ObjectView: View { } } .padding() - } - .onAppear { - generateSampleObjectData(frameCount: frameCount, objectCount: 35) + .onChange(of: debugSession.objectData) { _ in + refreshFromDebug() + } + .onAppear { + refreshFromDebug() + } } } @@ -120,9 +136,9 @@ struct ObjectView: View { Spacer() Button { - generateSampleObjectData(frameCount: frameCount, objectCount: 35) + refreshFromDebug() } label: { - Label("Simulate Capture", systemImage: "waveform") + Label("Refresh", systemImage: "arrow.clockwise") } } @@ -261,7 +277,10 @@ struct ObjectView: View { Slider(value: Binding( get: { Double(selectedFrameIndex) }, set: { selectedFrameIndex = Int($0.rounded()) } - ), in: 0...Double(max(0, frameCount - 1)), step: 1) + ), in: { + let maxFrame = samples.map { $0.frame }.max() ?? 0 + return 0...Double(max(maxFrame, 1)) + }(), step: 1) .frame(maxWidth: 400) Text("\(selectedFrameIndex)") @@ -410,7 +429,8 @@ struct ObjectView: View { private var frameDrawCallsSeries: [(frame: Int, value: Int)] { let grouped = Dictionary(grouping: samples, by: { $0.frame }) - return (0.. Date: Thu, 18 Dec 2025 20:46:29 +0100 Subject: [PATCH 10/12] Finished the traces memory view update --- Atlas Tracer/Client/Interpreter.swift | 75 ++++++++ .../Views/TargetViews/MemoryTracesView.swift | 170 ++++++++---------- 2 files changed, 152 insertions(+), 93 deletions(-) diff --git a/Atlas Tracer/Client/Interpreter.swift b/Atlas Tracer/Client/Interpreter.swift index 53fa328..6e6939a 100644 --- a/Atlas Tracer/Client/Interpreter.swift +++ b/Atlas Tracer/Client/Interpreter.swift @@ -141,6 +141,67 @@ struct DebugObjectPacket: Codable, Equatable { } } +enum DebugMemoryDomain: Int, Codable { + case GPU = 1 + case CPU = 2 +} + +enum DebugMemoryResourceKind: Int, Codable { + case vertexBuffer = 1 + case indexBuffer = 2 + case uniformBuffer = 3 + case storageBuffer = 4 + case texture2d = 5 + case texture3d = 6 + case textureCube = 7 + case renderTarget = 8 + case depthStencil = 9 + case sampler = 10 + case pipelineCache = 11 + case accelerationStructure = 12 + case other = 13 +} + +struct AllocationPacket: Codable, Equatable { + let description: String + let owner: String + let domain: DebugMemoryDomain + let kind: DebugMemoryResourceKind + let sizeMb: Float + let frameNumber: Int + let type: String + + enum CodingKeys: String, CodingKey { + case description + case owner + case domain + case kind + case sizeMb = "size_mb" + case frameNumber = "frame_number" + case type + } +} + +struct FrameMemoryPacket: Codable, Equatable { + let type: String + let frameNumber: Int + let totalAllocatedMb: Float + let totalGPUMb: Float + let totalCPUMb: Float + let allocationCount: Int + let deallocationCount: Int + + enum CodingKeys: String, CodingKey { + case type + case frameNumber = "frame_number" + case totalAllocatedMb = "total_allocated_mb" + case totalGPUMb = "total_gpu_mb" + case totalCPUMb = "total_cpu_mb" + case allocationCount = "allocation_count" + case deallocationCount = "deallocation_count" + } +} + final class DebugInformation: ObservableObject { static let shared = DebugInformation() @@ -150,6 +211,8 @@ final class DebugInformation: ObservableObject { @Published var resourceEvents: [DebugResourceEvent] = [] @Published var frameResourceInformation: [DebugFrameResourceInformation] = [] @Published var objectData: [DebugObjectPacket] = [] + @Published var allocationPackets: [AllocationPacket] = [] + @Published var frameMemoryPackets: [FrameMemoryPacket] = [] func addLog(_ log: DebugLog) { DispatchQueue.main.async { @@ -220,6 +283,18 @@ class MainInterpreter: Interpreter { } } } + case "frame_memory_info": + if let info = try? JSONDecoder().decode(FrameMemoryPacket.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.frameMemoryPackets.append(info) + } + } + case "allocation_event": + if let info = try? JSONDecoder().decode(AllocationPacket.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.allocationPackets.append(info) + } + } default: break } diff --git a/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift b/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift index fc55bd3..c6b2635 100644 --- a/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift +++ b/Atlas Tracer/Views/TargetViews/MemoryTracesView.swift @@ -124,6 +124,8 @@ struct AllocationEvent: Identifiable { } struct MemoryTracesView: View { + @ObservedObject var debugSession: DebugInformation = .shared + @State private var allocations: [Allocation] = [] @State private var frames: [FrameMemory] = [] @@ -175,8 +177,11 @@ struct MemoryTracesView: View { } .padding() } - .onAppear { - generateSampleMemoryData(frameCount: 300, allocationCount: 550) + .task(id: debugSession.frameMemoryPackets) { + refreshFromDebug() + } + .task(id: debugSession.allocationPackets) { + refreshFromDebug() } } @@ -198,10 +203,11 @@ struct MemoryTracesView: View { .frame(maxWidth: 260) Button { - generateSampleMemoryData(frameCount: 300, allocationCount: 550) + // Disabled button, no action } label: { - Label("Simulate Capture", systemImage: "waveform") + Label("Awaiting Data", systemImage: "waveform") } + .disabled(true) } Text("Track total memory, VRAM vs system usage, resource-type breakdown, allocation churn, and potential leaks.") @@ -510,8 +516,9 @@ struct MemoryTracesView: View { Slider(value: Binding( get: { Double(selectedFrameIndex) }, set: { selectedFrameIndex = Int($0.rounded()) } - ), in: 0...Double(max(0, frames.count - 1)), step: 1) + ), in: 0 ... Double(max(1, frames.count - 1)), step: 1) .frame(maxWidth: 400) + .disabled(frames.isEmpty) let clamped = clampedSelectedFrameIndex Text("\(frames.isEmpty ? 0 : frames[clamped].frame)") @@ -670,108 +677,85 @@ struct MemoryTracesView: View { let kind: ResourceKind } - private func generateSampleMemoryData(frameCount: Int, allocationCount: Int) { - allocations.removeAll() - frames.removeAll() - breakdown.removeAll() - events.removeAll() - - var rng = SystemRandomNumberGenerator() - - func sizeRange(for kind: ResourceKind) -> ClosedRange { - switch kind { - case .vertexBuffer: return 1...16 - case .indexBuffer: return 0.5...8 - case .uniformBuffer: return 0.1...2 - case .storageBuffer: return 2...64 - case .texture2D: return 4...128 - case .textureCube: return 8...64 - case .renderTarget: return 8...128 - case .depthStencil: return 8...64 - case .sampler: return 0.01...0.1 - case .pipelineCache: return 4...32 - case .accelerationStructure: return 16...128 - case .other: return 0.1...8 - } - } - - for i in 0.. MemoryDomain { + switch domain { + case .CPU: + return .cpu + case .GPU: + return .gpu } } - for frame in 0.. frame) } - - var domainTotals: [MemoryDomain: Double] = [.cpu: 0, .gpu: 0] - for a in live { - domainTotals[a.domain, default: 0] += a.sizeMB + func mapKind(_ kind: DebugMemoryResourceKind) -> ResourceKind { + switch kind { + case .vertexBuffer: return .vertexBuffer + case .indexBuffer: return .indexBuffer + case .uniformBuffer: return .uniformBuffer + case .storageBuffer: return .storageBuffer + case .texture2d: return .texture2D + case .texture3d: return .texture2D + case .textureCube: return .textureCube + case .renderTarget: return .renderTarget + case .depthStencil: return .depthStencil + case .sampler: return .sampler + case .pipelineCache: return .pipelineCache + case .accelerationStructure: return .accelerationStructure + case .other: return .other } - let total = domainTotals.values.reduce(0, +) - - let allocCount = perFrameAllocs[frame]?.count ?? 0 - let freeCount = perFrameFrees[frame]?.count ?? 0 + } - frames.append(FrameMemory(frame: frame, totalMB: total, totalsByDomain: domainTotals, allocationCount: allocCount, deallocationCount: freeCount)) + allocations = allocSrc.map { a in + Allocation( + label: a.description, + kind: mapKind(a.kind), + domain: mapDomain(a.domain), + sizeMB: Double(a.sizeMb), + createdAtFrame: a.frameNumber, + releasedAtFrame: nil, + owner: a.owner + ) + } + var newBreakdown: [FrameResourceBreakdown] = [] + for frame in frames { + let live = allocations.filter { $0.createdAtFrame <= frame.frame && ($0.releasedAtFrame == nil || $0.releasedAtFrame! > frame.frame) } let grouped = Dictionary(grouping: live, by: { PairKey(domain: $0.domain, kind: $0.kind) }) for (key, arr) in grouped { let sum = arr.reduce(0.0) { $0 + $1.sizeMB } - breakdown.append(FrameResourceBreakdown(frame: frame, domain: key.domain, kind: key.kind, sizeMB: sum)) - } - - if let newAllocs = perFrameAllocs[frame] { - for a in newAllocs { - events.append(AllocationEvent(frame: frame, action: .alloc, kind: a.kind, domain: a.domain, sizeMB: a.sizeMB, label: a.label)) - } - } - if let frees = perFrameFrees[frame] { - for a in frees { - events.append(AllocationEvent(frame: frame, action: .free, kind: a.kind, domain: a.domain, sizeMB: a.sizeMB, label: a.label)) - } + newBreakdown.append(FrameResourceBreakdown(frame: frame.frame, domain: key.domain, kind: key.kind, sizeMB: sum)) } } - - selectedFrameIndex = min(0, frames.count - 1) + breakdown = newBreakdown + + var newEvents: [AllocationEvent] = [] + for a in allocSrc { + newEvents.append(AllocationEvent( + frame: a.frameNumber, + action: .alloc, + kind: mapKind(a.kind), + domain: mapDomain(a.domain), + sizeMB: Double(a.sizeMb), + label: a.description + )) + } + events = newEvents } } From 315bf3a3fddd34904c4a17e4a14a647599a00e6e Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Thu, 18 Dec 2025 21:02:13 +0100 Subject: [PATCH 11/12] Finished the profiling view --- Atlas Tracer/Client/Interpreter.swift | 73 +++++++++- .../Views/TargetViews/ProfilingView.swift | 130 ++++++++---------- 2 files changed, 131 insertions(+), 72 deletions(-) diff --git a/Atlas Tracer/Client/Interpreter.swift b/Atlas Tracer/Client/Interpreter.swift index 6e6939a..f058c29 100644 --- a/Atlas Tracer/Client/Interpreter.swift +++ b/Atlas Tracer/Client/Interpreter.swift @@ -185,9 +185,9 @@ struct AllocationPacket: Codable, Equatable { struct FrameMemoryPacket: Codable, Equatable { let type: String let frameNumber: Int - let totalAllocatedMb: Float - let totalGPUMb: Float - let totalCPUMb: Float + let totalAllocatedMb: Double + let totalGPUMb: Double + let totalCPUMb: Double let allocationCount: Int let deallocationCount: Int @@ -202,6 +202,59 @@ struct FrameMemoryPacket: Codable, Equatable { } } +enum DebugTimingEventSubsystem: Int, Codable { + case rendering = 1 + case physics = 2 + case ai = 3 + case scripting = 4 + case animation = 5 + case audio = 6 + case networking = 7 + case io = 8 + case scene = 9 + case other = 10 +} + +struct FrameTimingPacket: Codable, Equatable { + let type: String + let frameNumber: Int + let cpuFrameTimeMs: Double + let gpuFrameTimeMs: Double + let mainThreadTimeMs: Double + let workerThreadTimeMs: Double + let memoryMb: Double + let cpuUsagePercent: Double + let gpuUsagePercent: Double + + enum CodingKeys: String, CodingKey { + case type + case frameNumber = "frame_number" + case cpuFrameTimeMs = "cpu_frame_time_ms" + case gpuFrameTimeMs = "gpu_frame_time_ms" + case mainThreadTimeMs = "main_thread_time_ms" + case workerThreadTimeMs = "worker_thread_time_ms" + case memoryMb = "memory_mb" + case cpuUsagePercent = "cpu_usage_percent" + case gpuUsagePercent = "gpu_usage_percent" + } +} + +struct TimingEventPacket: Codable, Equatable { + let type: String + let name: String + let subsystem: DebugTimingEventSubsystem + let durationMs: Double + let frameNumber: Int + + enum CodingKeys: String, CodingKey { + case type + case name + case subsystem + case durationMs = "duration_ms" + case frameNumber = "frame_number" + } +} + final class DebugInformation: ObservableObject { static let shared = DebugInformation() @@ -213,6 +266,8 @@ final class DebugInformation: ObservableObject { @Published var objectData: [DebugObjectPacket] = [] @Published var allocationPackets: [AllocationPacket] = [] @Published var frameMemoryPackets: [FrameMemoryPacket] = [] + @Published var frameTimingPackets: [FrameTimingPacket] = [] + @Published var timingEventPackets: [TimingEventPacket] = [] func addLog(_ log: DebugLog) { DispatchQueue.main.async { @@ -295,6 +350,18 @@ class MainInterpreter: Interpreter { DebugInformation.shared.allocationPackets.append(info) } } + case "frame_timing_info": + if let info = try? JSONDecoder().decode(FrameTimingPacket.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.frameTimingPackets.append(info) + } + } + case "timing_event": + if let info = try? JSONDecoder().decode(TimingEventPacket.self, from: data) { + DispatchQueue.main.async { + DebugInformation.shared.timingEventPackets.append(info) + } + } default: break } diff --git a/Atlas Tracer/Views/TargetViews/ProfilingView.swift b/Atlas Tracer/Views/TargetViews/ProfilingView.swift index 0233e16..64d06d9 100644 --- a/Atlas Tracer/Views/TargetViews/ProfilingView.swift +++ b/Atlas Tracer/Views/TargetViews/ProfilingView.swift @@ -86,6 +86,7 @@ struct FrameSubsystemTiming: Identifiable { } struct ProfilingView: View { + @ObservedObject var debugSession: DebugInformation = .shared @State private var frames: [FrameTiming] = [] @State private var frameSubsystemData: [FrameSubsystemTiming] = [] @@ -137,7 +138,13 @@ struct ProfilingView: View { .padding() } .onAppear { - generateSampleProfilingData(frameCount: 240) + refreshFromDebug() + } + .onChange(of: debugSession.frameTimingPackets) { + refreshFromDebug() + } + .onChange(of: debugSession.timingEventPackets) { + refreshFromDebug() } } @@ -163,10 +170,11 @@ struct ProfilingView: View { .frame(width: 140) Button { - generateSampleProfilingData(frameCount: 240) + // Disabled button action } label: { - Label("Simulate Capture", systemImage: "waveform") + Label("Awaiting Data", systemImage: "waveform") } + .disabled(true) } Text("Analyze CPU/GPU frame times, subsystem costs, and runtime stats to identify spikes, jank, and bottlenecks.") @@ -501,7 +509,7 @@ struct ProfilingView: View { set: { newValue in selectedFrameIndex = Int(newValue.rounded()) } - ), in: 0...Double(max(0, frames.count - 1)), step: 1) + ), in: 0 ... Double(max(0, frames.count - 1)), step: 1) .frame(maxWidth: 400) let clampedIndex = min(max(0, selectedFrameIndex), max(0, frames.count - 1)) @@ -543,87 +551,71 @@ struct ProfilingView: View { } } - private func generateSampleProfilingData(frameCount: Int) { - frames.removeAll() - frameSubsystemData.removeAll() - - var memory: Double = 800 // MB - var cpuUtil: Double = 45 - var gpuUtil: Double = 35 - - for frame in 0.. Subsystem { + switch debugSubsystem { + case .rendering: return .rendering + case .physics: return .physics + case .ai: return .ai + case .scripting: return .scripting + case .animation: return .animation + case .audio: return .audio + case .networking: return .networking + case .io: return .io + case .scene: return .scene + case .other: return .other } + } - var remaining = cpuMs - var breakdown: [Subsystem: Double] = [:] - - let order = Subsystem.allCases.shuffled() - for (idx, subsystem) in order.enumerated() { - if idx == order.count - 1 { - breakdown[subsystem] = max(0.2, remaining) - } else { - let maxPart = min(remaining * 0.6, 6.0) - let upperBound = max(0.5, maxPart) - let part = max(0.1, Double.random(in: 0.5...upperBound)) - breakdown[subsystem] = part - remaining = max(0.0, remaining - part) - } - } - - let mainThread = min(cpuMs, max(3.0, cpuMs * Double.random(in: 0.45...0.7))) - let workers = max(0.0, cpuMs - mainThread) - - memory += Double.random(in: -2.0...3.0) - memory = max(700, min(1600, memory)) + var newFrames: [FrameTiming] = [] - cpuUtil += Double.random(in: -4.0...4.0) - cpuUtil = max(10, min(100, cpuUtil)) + for packet in debugSession.frameTimingPackets { + let eventsForFrame = debugSession.timingEventPackets.filter { $0.frameNumber == packet.frameNumber } - gpuUtil += Double.random(in: -4.0...4.0) - gpuUtil = max(5, min(100, gpuUtil)) + var subsystemDurations: [Subsystem: Double] = [:] + for event in eventsForFrame { + let subsystem = mapSubsystem(event.subsystem) + subsystemDurations[subsystem, default: 0] += event.durationMs + } - var events: [ProfileEvent] = [] - var t = 0.0 - let eventCount = Int.random(in: 3...7) - for _ in 0..= cpuMs { break } + var profileEvents: [ProfileEvent] = [] + var runningStart: Double = 0 + for event in eventsForFrame { + let subsystem = mapSubsystem(event.subsystem) + let pe = ProfileEvent( + frame: packet.frameNumber, + name: event.name, + subsystem: subsystem, + startMs: runningStart, + durationMs: event.durationMs + ) + profileEvents.append(pe) + runningStart += event.durationMs } - let ft = FrameTiming( - frame: frame, - cpuMs: cpuMs, - gpuMs: gpuMs, - mainThreadMs: mainThread, - workerThreadsMs: workers, - memoryMB: memory, - cpuUtilPercent: cpuUtil, - gpuUtilPercent: gpuUtil, - subsystemBreakdown: breakdown, - events: events + let frameTiming = FrameTiming( + frame: packet.frameNumber, + cpuMs: packet.cpuFrameTimeMs, + gpuMs: packet.gpuFrameTimeMs, + mainThreadMs: packet.mainThreadTimeMs, + workerThreadsMs: packet.workerThreadTimeMs, + memoryMB: packet.memoryMb, + cpuUtilPercent: packet.cpuUsagePercent, + gpuUtilPercent: packet.gpuUsagePercent, + subsystemBreakdown: subsystemDurations, + events: profileEvents ) - frames.append(ft) + newFrames.append(frameTiming) } + frames = newFrames + frameSubsystemData = frames.flatMap { f in f.subsystemBreakdown.map { key, value in FrameSubsystemTiming(frame: f.frame, subsystem: key, ms: value) } } - - selectedFrameIndex = min(0, frames.count - 1) } private var averageCPU: Double { From daad3abe96c751f8fe98647935306ed5421851ef Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Wed, 24 Dec 2025 12:07:30 +0100 Subject: [PATCH 12/12] Fixed commands --- .../Views/GeneralViews/ConsoleView.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Atlas Tracer/Views/GeneralViews/ConsoleView.swift b/Atlas Tracer/Views/GeneralViews/ConsoleView.swift index d737221..f76b13a 100644 --- a/Atlas Tracer/Views/GeneralViews/ConsoleView.swift +++ b/Atlas Tracer/Views/GeneralViews/ConsoleView.swift @@ -34,13 +34,8 @@ struct ConsoleView: View { @State private var showCommandHelp: Bool = false let availableCommands: [CommandDefinition] = [ - CommandDefinition(name: "/trace", description: "Start a new trace session", syntax: "/trace [target]"), - CommandDefinition(name: "/stop", description: "Stop the current trace", syntax: "/stop"), - CommandDefinition(name: "/analyze", description: "Analyze trace results", syntax: "/analyze [options]"), - CommandDefinition(name: "/export", description: "Export trace data", syntax: "/export [format] [path]"), - CommandDefinition(name: "/clear", description: "Clear console history", syntax: "/clear"), - CommandDefinition(name: "/help", description: "Show all available commands", syntax: "/help [command]"), - CommandDefinition(name: "/config", description: "Configure trace settings", syntax: "/config [key] [value]"), + CommandDefinition(name: "/clear", description: "Clear the console", syntax: "/clear"), + CommandDefinition(name: "/log", description: "Log something to the console", syntax: "/log [contents]") ] var filteredCommands: [CommandDefinition] { @@ -64,6 +59,15 @@ struct ConsoleView: View { } command = "" return + } else if command.split(separator: " ").first == "/log" { + let parts = command.split(separator: " ", maxSplits: 1, omittingEmptySubsequences: true) + let rawContent = parts.count > 1 ? String(parts[1]) : "" + let content = rawContent.trimmingCharacters(in: .whitespacesAndNewlines) + withAnimation { + consoleHistory.append(ConsoleReturn(message: content, resolution: content.isEmpty ? .warning : .ok)) + } + command = "" + return } isProcessing = true