From 94d225998258a7a924d90e05767cb03e9e015e49 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 19:39:51 +0200 Subject: [PATCH 01/17] [Performance] Record compositor deadline margins and semaphore waits per frame On visionOS the frame loop waited for the compositor's optimal input time but recorded nothing about the outcome, so the stats could not say whether a frame was submitted late or finished on the GPU after its rendering deadline. - Add EngineCompositorStats to the stats snapshot: update phase ms, input slack against optimalInputTime, submission ms, GPU margin against the drawable's renderingDeadline and presentationTime, missed-deadline flag and cumulative counts (with a miss rate), missing-anchor count, view count and per-view drawable size. - Record them in UntoldEngineXR.renderNewFrame/executeXRSystemPass; the GPU margins come from the command buffer completion handler. - Add timing.semaphoreWaitMs on the macOS and visionOS paths. - Add a Compositor signpost category (CompositorUpdate, CompositorWaitForInput, CompositorSubmission) and a MissedDeadline point event so Instruments shows the phases next to the Metal System Trace GPU track. - Print a Compositor line in the verbose stats output when views > 0, and document the fields in UsingProfiler.md. --- .../Profiling/EngineProfiler.swift | 5 + .../Profiling/EngineSignposts.swift | 26 +++++ .../Profiling/EngineStatsFormatter.swift | 13 ++- .../Profiling/EngineStatsMonitor.swift | 52 +++++++++ .../Profiling/EngineStatsSnapshot.swift | 80 +++++++++++++- .../Systems/RenderingSystem.swift | 9 ++ Sources/UntoldEngineXR/UntoldEngineXR.swift | 54 +++++++++ .../EngineStatsMonitorTests.swift | 103 ++++++++++++++++++ docs/API/UsingProfiler.md | 18 +++ 9 files changed, 357 insertions(+), 3 deletions(-) diff --git a/Sources/UntoldEngine/Profiling/EngineProfiler.swift b/Sources/UntoldEngine/Profiling/EngineProfiler.swift index 10981beb4..bce0b3758 100644 --- a/Sources/UntoldEngine/Profiling/EngineProfiler.swift +++ b/Sources/UntoldEngine/Profiling/EngineProfiler.swift @@ -67,6 +67,11 @@ public final class EngineProfiler: @unchecked Sendable { signposts.endScope(scope) } + public func emitEvent(_ event: ProfileEvent) { + guard isEnabled else { return } + signposts.emitEvent(event) + } + public func attach(to commandBuffer: MTLCommandBuffer, label: String? = nil) { guard isEnabled else { return } gpuMetrics.attach(to: commandBuffer, label: label) diff --git a/Sources/UntoldEngine/Profiling/EngineSignposts.swift b/Sources/UntoldEngine/Profiling/EngineSignposts.swift index 5a357e5c5..3fb70bc37 100644 --- a/Sources/UntoldEngine/Profiling/EngineSignposts.swift +++ b/Sources/UntoldEngine/Profiling/EngineSignposts.swift @@ -40,6 +40,17 @@ public enum ProfileScope { case gaussianDepth case gaussianSort case gaussianDraw + + // Compositor Services frame phases (visionOS) + case compositorUpdate + case compositorWaitForInput + case compositorSubmission +} + +/// Point events (no duration) that mark something worth seeing on the Instruments timeline. +public enum ProfileEvent { + /// The GPU finished a frame after the compositor's rendering deadline. + case missedDeadline } final class EngineSignposts { @@ -51,6 +62,7 @@ final class EngineSignposts { private static let cullingLog = OSLog(subsystem: subsystem, category: "Culling") private static let streamingLog = OSLog(subsystem: subsystem, category: "Streaming") private static let batchingLog = OSLog(subsystem: subsystem, category: "Batching") + private static let compositorLog = OSLog(subsystem: subsystem, category: "Compositor") private static let gaussianLog = OSLog(subsystem: subsystem, category: "Gaussian") // One stable signpost ID per scope. @@ -65,6 +77,10 @@ final class EngineSignposts { private static let geometryStreamingID = OSSignpostID(log: streamingLog) private static let batchingTickID = OSSignpostID(log: batchingLog) private static let batchingRebuildID = OSSignpostID(log: batchingLog) + private static let compositorUpdateID = OSSignpostID(log: compositorLog) + private static let compositorWaitForInputID = OSSignpostID(log: compositorLog) + private static let compositorSubmissionID = OSSignpostID(log: compositorLog) + private static let missedDeadlineID = OSSignpostID(log: compositorLog) private static let gaussianCullID = OSSignpostID(log: gaussianLog) private static let gaussianDepthID = OSSignpostID(log: gaussianLog) private static let gaussianSortID = OSSignpostID(log: gaussianLog) @@ -80,6 +96,13 @@ final class EngineSignposts { os_signpost(.end, log: log, name: name, signpostID: id) } + func emitEvent(_ event: ProfileEvent) { + switch event { + case .missedDeadline: + os_signpost(.event, log: Self.compositorLog, name: "MissedDeadline", signpostID: Self.missedDeadlineID) + } + } + private func descriptor(for scope: ProfileScope) -> (OSLog, OSSignpostID, StaticString) { switch scope { case .frame: return (Self.frameLog, Self.frameID, "Frame") @@ -97,6 +120,9 @@ final class EngineSignposts { case .gaussianDepth: return (Self.gaussianLog, Self.gaussianDepthID, "GaussianDepth") case .gaussianSort: return (Self.gaussianLog, Self.gaussianSortID, "GaussianSort") case .gaussianDraw: return (Self.gaussianLog, Self.gaussianDrawID, "GaussianDraw") + case .compositorUpdate: return (Self.compositorLog, Self.compositorUpdateID, "CompositorUpdate") + case .compositorWaitForInput: return (Self.compositorLog, Self.compositorWaitForInputID, "CompositorWaitForInput") + case .compositorSubmission: return (Self.compositorLog, Self.compositorSubmissionID, "CompositorSubmission") } } } diff --git a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift index fea12154e..b30a5c641 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift @@ -56,6 +56,7 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin let texBudgetMB = formatMB(snapshot.memory.textureBudgetBytes) let memPct = String(format: "%.0f%%", snapshot.memory.utilizationPercent * 100) let pressure = snapshot.memory.isUnderPressure ? " PRESSURE" : "" + let compositorLine = compositorStatsLine(snapshot) return """ Frame \(snapshot.frameIndex) | CPU \(formatMs(snapshot.timing.smoothedFrameMs))ms (\(formatFPS(frameMs: snapshot.timing.smoothedFrameMs)) fps, smoothed) GPU \(formatMs(snapshot.timing.gpuExecutionMs))ms exec / \(formatFPS(frameMs: snapshot.timing.gpuFrameCadenceMs)) fps cadence [\(bottleneck)] Timing: frame \(formatMs(snapshot.timing.frameTotalMs))ms (raw CPU) | update \(formatMs(snapshot.timing.updateMs))ms | render \(formatMs(snapshot.timing.renderTotalMs))ms | cull \(formatMs(snapshot.timing.cullingMs))ms | stream \(formatMs(snapshot.timing.streamingRegionMs + snapshot.timing.geometryStreamingMs))ms | batchTick \(formatMs(snapshot.timing.batchingTickMs))ms | batchRebuild \(formatMs(snapshot.timing.batchingRebuildMs))ms @@ -66,10 +67,20 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin TileReps: resident full/lod/hlod \(snapshot.streaming.residentFullTileRepresentations)/\(snapshot.streaming.residentLODRepresentations)/\(snapshot.streaming.residentHLODRepresentations) | visible full/lod/hlod \(snapshot.streaming.visibleFullTileRepresentations)/\(snapshot.streaming.visibleLODRepresentations)/\(snapshot.streaming.visibleHLODRepresentations) | overlap visible full+lod/full+hlod/lod+hlod \(snapshot.streaming.fullAndLODVisibleOverlapTiles)/\(snapshot.streaming.fullAndHLODVisibleOverlapTiles)/\(snapshot.streaming.lodAndHLODVisibleOverlapTiles) residentFull+fallback \(snapshot.streaming.fullAndFallbackResidentOverlapTiles) | fades \(snapshot.streaming.activeTileRepresentationFades) waiting \(snapshot.streaming.waitingTileRepresentationFades) TileRenderCost: visible full/lod/hlod \(snapshot.render.tileFullVisibleInstances)/\(snapshot.render.tileLODVisibleInstances)/\(snapshot.render.tileHLODVisibleInstances) | draws full/lod/hlod \(snapshot.render.tileFullDrawsEstimate)/\(snapshot.render.tileLODDrawsEstimate)/\(snapshot.render.tileHLODDrawsEstimate) | tris full/lod/hlod \(snapshot.render.tileFullTrianglesEstimate)/\(snapshot.render.tileLODTrianglesEstimate)/\(snapshot.render.tileHLODTrianglesEstimate) Batching: groups \(snapshot.batching.batchGroupCount) | batchedMeshes \(snapshot.batching.batchedMeshCount) | dirty \(snapshot.batching.dirtyCellsBeforePrune)→\(snapshot.batching.dirtyCellsAfterPrune) | defWork \(snapshot.batching.deferredByWorkBudget) skipComplex \(snapshot.batching.skippedByComplexityGuard) | dispatched \(snapshot.batching.dispatchedBuilds)→\(snapshot.batching.lastRebuildOutputBatchCount) groups | rebuilds/s \(snapshot.batching.rebuildsThisSecond) | rebuildMs \(formatMs(snapshot.batching.lastRebuildCostMs)) - Memory: mesh \(meshMB)/\(meshBudgetMB)mb | tex \(texMB)/\(texBudgetMB)mb | total \(memPct) | entities \(snapshot.memory.trackedEntityCount)\(pressure) + Memory: mesh \(meshMB)/\(meshBudgetMB)mb | tex \(texMB)/\(texBudgetMB)mb | total \(memPct) | entities \(snapshot.memory.trackedEntityCount)\(pressure)\(compositorLine) """ } +/// One line of Compositor Services frame accounting, prefixed with a newline so it can be appended +/// to the expanded block. Empty when the frame was not rendered through Compositor Services. +private func compositorStatsLine(_ snapshot: EngineStatsSnapshot) -> String { + let c = snapshot.compositor + guard c.viewCount > 0 else { return "" } + let missRate = String(format: "%.2f%%", c.missedDeadlineRate * 100) + let missed = c.missedDeadline ? " MISSED" : "" + return "\nCompositor: views \(c.viewCount) @ \(c.viewTextureWidth)x\(c.viewTextureHeight) | update \(formatMs(c.updateMs))ms | inputSlack \(formatMs(c.inputSlackMs))ms | submit \(formatMs(c.submissionMs))ms | semWait \(formatMs(snapshot.timing.semaphoreWaitMs))ms | deadlineMargin \(formatMs(c.deadlineMarginMs))ms\(missed) | presentMargin \(formatMs(c.presentationMarginMs))ms | missed \(c.missedDeadlineCount)/\(c.deadlineSampleCount) (\(missRate)) | noAnchor \(c.missingAnchorCount)" +} + private func formatMB(_ bytes: Int) -> String { String(format: "%.0f", Double(bytes) / (1024 * 1024)) } diff --git a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift index d296cd59b..bb33bd789 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift @@ -45,6 +45,14 @@ public final class EngineStatsMonitor: @unchecked Sendable { private var _lastGPUCompletionTime: Double = 0.0 // 0 = no completion seen yet private var _lastGPUCommandBufferEndTime: CFTimeInterval = 0.0 // gpuEndTime of the last frame recorded + // Compositor deadline accounting (visionOS) — written from addCompletedHandler, read at completeFrame() + private var _latestDeadlineMarginMs: Double = 0.0 + private var _latestPresentationMarginMs: Double = 0.0 + private var _latestMissedDeadline: Bool = false + private var _missedDeadlineCount: Int = 0 + private var _deadlineSampleCount: Int = 0 + private var _missingAnchorCount: Int = 0 + // 30-frame rolling average for smoothed CPU frame time private let kSmoothingWindow = 30 private var _frameMsBuffer: [Double] = .init(repeating: 0.0, count: 30) @@ -146,6 +154,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { currentSnapshot.frameIndex &+= 1 currentSnapshot.timestampSeconds = timestampSeconds currentSnapshot.timing = .init() + currentSnapshot.compositor = .init() lock.unlock() #endif } @@ -191,6 +200,35 @@ public final class EngineStatsMonitor: @unchecked Sendable { #endif } + /// Called from MTLCommandBuffer.addCompletedHandler on visionOS with the GPU completion time + /// measured against the compositor's rendering deadline and presentation time for that frame. + /// Safe to call from any thread. + public func recordCompositorCompletion(deadlineMarginMs: Double, presentationMarginMs: Double) { + #if ENGINE_STATS_ENABLED + lock.lock() + _latestDeadlineMarginMs = deadlineMarginMs + _latestPresentationMarginMs = presentationMarginMs + _latestMissedDeadline = deadlineMarginMs < 0.0 + _deadlineSampleCount += 1 + if deadlineMarginMs < 0.0 { + _missedDeadlineCount += 1 + } + lock.unlock() + #else + _ = deadlineMarginMs + _ = presentationMarginMs + #endif + } + + /// Called once per frame that is presented without a fresh device anchor (visionOS). + public func recordMissingAnchor() { + #if ENGINE_STATS_ENABLED + lock.lock() + _missingAnchorCount += 1 + lock.unlock() + #endif + } + /// Publishes the current frame so API readers can safely consume it next frame. public func completeFrame() { #if ENGINE_STATS_ENABLED @@ -199,6 +237,14 @@ public final class EngineStatsMonitor: @unchecked Sendable { currentSnapshot.timing.gpuExecutionMs = _latestGPUExecutionMs currentSnapshot.timing.gpuFrameCadenceMs = _latestGPUFrameCadenceMs + // Pull in the latest compositor deadline sample and the cumulative counters + currentSnapshot.compositor.deadlineMarginMs = _latestDeadlineMarginMs + currentSnapshot.compositor.presentationMarginMs = _latestPresentationMarginMs + currentSnapshot.compositor.missedDeadline = _latestMissedDeadline + currentSnapshot.compositor.missedDeadlineCount = _missedDeadlineCount + currentSnapshot.compositor.deadlineSampleCount = _deadlineSampleCount + currentSnapshot.compositor.missingAnchorCount = _missingAnchorCount + // Update 30-frame rolling average for CPU frame time let frameMs = currentSnapshot.timing.frameTotalMs _frameMsBuffer[_frameMsBufferIndex] = frameMs @@ -235,6 +281,12 @@ public final class EngineStatsMonitor: @unchecked Sendable { _latestGPUExecutionMs = 0.0 _latestGPUFrameCadenceMs = 0.0 _lastGPUCompletionTime = 0.0 + _latestDeadlineMarginMs = 0.0 + _latestPresentationMarginMs = 0.0 + _latestMissedDeadline = false + _missedDeadlineCount = 0 + _deadlineSampleCount = 0 + _missingAnchorCount = 0 _frameMsBuffer = .init(repeating: 0.0, count: kSmoothingWindow) _frameMsBufferIndex = 0 _frameMsFilled = 0 diff --git a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift index 0ceb1821a..5e380a969 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift @@ -43,6 +43,9 @@ public struct EngineTimingStats { public var geometryStreamingMs: Double = 0.0 public var batchingTickMs: Double = 0.0 public var batchingRebuildMs: Double = 0.0 + /// CPU time spent waiting for a free in-flight command buffer slot before encoding. + /// Persistently non-zero means the GPU (or the compositor) is pacing the CPU. + public var semaphoreWaitMs: Double = 0.0 public init( frameTotalMs: Double = 0.0, @@ -61,7 +64,8 @@ public struct EngineTimingStats { batchingTickMs: Double = 0.0, batchingRebuildMs: Double = 0.0, presentationDeltaMs: Double = 0.0, - skippedFrames: Int = 0 + skippedFrames: Int = 0, + semaphoreWaitMs: Double = 0.0 ) { self.frameTotalMs = frameTotalMs self.smoothedFrameMs = smoothedFrameMs @@ -80,6 +84,75 @@ public struct EngineTimingStats { self.batchingRebuildMs = batchingRebuildMs self.presentationDeltaMs = presentationDeltaMs self.skippedFrames = skippedFrames + self.semaphoreWaitMs = semaphoreWaitMs + } +} + +/// Compositor Services frame accounting. Only populated on visionOS, where the compositor +/// hands the app a deadline for every frame; on other platforms every field stays at its default. +/// +/// The per-frame values describe the last completed frame. The GPU-side values (deadline and +/// presentation margins) come from the command buffer completion handler and therefore describe +/// the most recently *completed* command buffer, which can lag the CPU frame by one or two frames. +public struct EngineCompositorStats { + /// CPU time between `startUpdate()` and `endUpdate()`. + public var updateMs: Double = 0.0 + /// Time left until `optimalInputTime` when the update phase ended. + /// Negative means the update phase ran past the compositor's optimal input time. + public var inputSlackMs: Double = 0.0 + /// CPU time between `startSubmission()` and the command buffer commit. + public var submissionMs: Double = 0.0 + /// Time between the GPU finishing the frame and the compositor's rendering deadline. + /// Negative means the deadline was missed and the compositor reprojected an older frame. + public var deadlineMarginMs: Double = 0.0 + /// Time between the GPU finishing the frame and the frame's presentation time. + public var presentationMarginMs: Double = 0.0 + /// Whether the most recently completed frame missed its rendering deadline. + public var missedDeadline: Bool = false + /// Frames whose GPU work finished after the rendering deadline since the monitor was reset. + public var missedDeadlineCount: Int = 0 + /// Frames with a GPU completion sample since the monitor was reset (denominator for the miss rate). + public var deadlineSampleCount: Int = 0 + /// Frames rendered without a fresh device anchor since the monitor was reset. + public var missingAnchorCount: Int = 0 + /// Number of views (eyes) rendered this frame. Zero outside Compositor Services. + public var viewCount: Int = 0 + /// Drawable size per view in pixels. + public var viewTextureWidth: Int = 0 + public var viewTextureHeight: Int = 0 + + /// Fraction of sampled frames that missed the rendering deadline, in the range 0...1. + public var missedDeadlineRate: Double { + guard deadlineSampleCount > 0 else { return 0.0 } + return Double(missedDeadlineCount) / Double(deadlineSampleCount) + } + + public init( + updateMs: Double = 0.0, + inputSlackMs: Double = 0.0, + submissionMs: Double = 0.0, + deadlineMarginMs: Double = 0.0, + presentationMarginMs: Double = 0.0, + missedDeadline: Bool = false, + missedDeadlineCount: Int = 0, + deadlineSampleCount: Int = 0, + missingAnchorCount: Int = 0, + viewCount: Int = 0, + viewTextureWidth: Int = 0, + viewTextureHeight: Int = 0 + ) { + self.updateMs = updateMs + self.inputSlackMs = inputSlackMs + self.submissionMs = submissionMs + self.deadlineMarginMs = deadlineMarginMs + self.presentationMarginMs = presentationMarginMs + self.missedDeadline = missedDeadline + self.missedDeadlineCount = missedDeadlineCount + self.deadlineSampleCount = deadlineSampleCount + self.missingAnchorCount = missingAnchorCount + self.viewCount = viewCount + self.viewTextureWidth = viewTextureWidth + self.viewTextureHeight = viewTextureHeight } } @@ -372,6 +445,7 @@ public struct EngineStatsSnapshot { public var streaming: EngineStreamingStats = .init() public var batching: EngineBatchingStats = .init() public var memory: EngineMemoryStats = .init() + public var compositor: EngineCompositorStats = .init() public init( frameIndex: UInt64 = 0, @@ -381,7 +455,8 @@ public struct EngineStatsSnapshot { culling: EngineCullingStats = .init(), streaming: EngineStreamingStats = .init(), batching: EngineBatchingStats = .init(), - memory: EngineMemoryStats = .init() + memory: EngineMemoryStats = .init(), + compositor: EngineCompositorStats = .init() ) { self.frameIndex = frameIndex self.timestampSeconds = timestampSeconds @@ -391,5 +466,6 @@ public struct EngineStatsSnapshot { self.streaming = streaming self.batching = batching self.memory = memory + self.compositor = compositor } } diff --git a/Sources/UntoldEngine/Systems/RenderingSystem.swift b/Sources/UntoldEngine/Systems/RenderingSystem.swift index 06b69cf95..2784ad871 100644 --- a/Sources/UntoldEngine/Systems/RenderingSystem.swift +++ b/Sources/UntoldEngine/Systems/RenderingSystem.swift @@ -34,7 +34,16 @@ func UpdateRenderingSystem(in view: MTKView) { } // Wait for available command buffer slot to prevent unbounded memory growth + #if ENGINE_STATS_ENABLED + let semaphoreWaitStart = CACurrentMediaTime() + #endif commandBufferSemaphore.wait() + #if ENGINE_STATS_ENABLED + let semaphoreWaitMs = (CACurrentMediaTime() - semaphoreWaitStart) * 1000.0 + EngineStatsMonitor.shared.update { snapshot in + snapshot.timing.semaphoreWaitMs = semaphoreWaitMs + } + #endif if let commandBuffer = renderInfo.commandQueue.makeCommandBuffer() { #if ENGINE_STATS_ENABLED diff --git a/Sources/UntoldEngineXR/UntoldEngineXR.swift b/Sources/UntoldEngineXR/UntoldEngineXR.swift index 0aa7d18de..eeeb77f2b 100644 --- a/Sources/UntoldEngineXR/UntoldEngineXR.swift +++ b/Sources/UntoldEngineXR/UntoldEngineXR.swift @@ -786,6 +786,7 @@ EngineStatsMonitor.shared.beginFrame(timestampSeconds: xrFrameStartTime) RenderStatsCollector.shared.reset() #endif + EngineProfiler.shared.beginScope(.compositorUpdate) // Snapshot loading gate once per frame to keep update/submission behavior consistent. let loading = AssetLoadingGate.shared.isLoadingAny @@ -824,6 +825,16 @@ } // 6. Call endupdate() to mark the end of the update phase + EngineProfiler.shared.endScope(.compositorUpdate) + #if ENGINE_STATS_ENABLED + let updateEndTime = CACurrentMediaTime() + let optimalInputTimeCA = Self.mediaTime(of: timing.optimalInputTime) + EngineStatsMonitor.shared.update { snapshot in + snapshot.compositor.updateMs = (updateEndTime - xrFrameStartTime) * 1000.0 + // Negative slack means the update phase ran past the compositor's optimal input time. + snapshot.compositor.inputSlackMs = (optimalInputTimeCA - updateEndTime) * 1000.0 + } + #endif frame.endUpdate() // 7. Call wait(until:tolerace) to puase your render loop until the optimal rendering time @@ -835,9 +846,11 @@ // the wait's length earlier and the head is predicted from earlier; the automatic // mode waits while the frames meet the deadline and not while they do not. let waitLengthMs = max(0, Self.milliseconds(LayerRenderer.Clock().now.duration(to: timing.optimalInputTime))) + EngineProfiler.shared.beginScope(.compositorWaitForInput) if waitsForOptimalInput(skippedWaitMs: waitLengthMs) { LayerRenderer.Clock().wait(until: timing.optimalInputTime, tolerance: .zero) } + EngineProfiler.shared.endScope(.compositorWaitForInput) #if ENGINE_STATS_ENABLED let xrWaitMs = (CACurrentMediaTime() - xrWaitStart) * 1000.0 EngineStatsMonitor.shared.update { snapshot in @@ -857,11 +870,13 @@ // 8. Call startSubmission() to mark the start of submission phase frame.startSubmission() + EngineProfiler.shared.beginScope(.compositorSubmission) #if ENGINE_STATS_ENABLED let xrSubmissionStart = CACurrentMediaTime() #endif var shouldEndSubmission = true defer { + EngineProfiler.shared.endScope(.compositorSubmission) if shouldEndSubmission, layerRenderer.state == .running { frame.endSubmission() } @@ -879,6 +894,9 @@ } guard !drawables.isEmpty else { #if ENGINE_STATS_ENABLED + EngineStatsMonitor.shared.update { snapshot in + snapshot.compositor.submissionMs = (CACurrentMediaTime() - xrSubmissionStart) * 1000.0 + } renderer.finalizeXRStatsAndMonitors(frameStartTime: xrFrameStartTime) #else renderer.finalizeXRStatsAndMonitors(frameStartTime: 0.0) @@ -907,6 +925,7 @@ // The frame's presentation time is checked on its first drawable (the built-in // display); a capture drawable shares the timing. var presentationChecked = false + var anchorMissingThisFrame = false for drawable in drawables { // 10. Fetch the predicted device anchor from ARKit using the frameTiming information, and // apply the anchor to your frame @@ -938,12 +957,14 @@ drawable.deviceAnchor = anchor } else if let cachedAnchor = lastValidDeviceAnchor { missingAnchorFrameCount += 1 + anchorMissingThisFrame = true if shouldLogAnchorDiagnostics() { printXRAnchorDiagnostics(message: "XR device anchor missing; using cached anchor") } drawable.deviceAnchor = cachedAnchor } else { missingAnchorFrameCount += 1 + anchorMissingThisFrame = true if shouldLogAnchorDiagnostics() { printXRAnchorDiagnostics(message: "XR device anchor missing; no cached anchor available") } @@ -964,10 +985,17 @@ presentCleared(drawable) } } + if anchorMissingThisFrame { + // Once per frame: a capture drawable shares the frame's anchor. + #if ENGINE_STATS_ENABLED + EngineStatsMonitor.shared.recordMissingAnchor() + #endif + } #if ENGINE_STATS_ENABLED let xrEncodeMs = (CACurrentMediaTime() - xrSubmissionStart) * 1000.0 EngineStatsMonitor.shared.update { snapshot in snapshot.timing.encodeMs = xrEncodeMs + snapshot.compositor.submissionMs = xrEncodeMs } renderer.finalizeXRStatsAndMonitors(frameStartTime: xrFrameStartTime) #else @@ -1183,6 +1211,11 @@ commandBufferSemaphore.wait() } let semaphoreWaitMs = (CACurrentMediaTime() - semaphoreWaitStart) * 1000.0 + #if ENGINE_STATS_ENABLED + EngineStatsMonitor.shared.update { snapshot in + snapshot.timing.semaphoreWaitMs += semaphoreWaitMs + } + #endif if semaphoreWaitMs > 16.0, shouldLogXRStallDiagnostics() { printXRCommandBufferStallDiagnostics(waitMs: semaphoreWaitMs, phase: "acquired") } @@ -1211,6 +1244,16 @@ print("✓ Updated VisionOS viewport to: \(actualViewPort)") } } + #if ENGINE_STATS_ENABLED + let viewCount = drawable.views.count + let viewTextureWidth = drawable.colorTextures.first?.width ?? 0 + let viewTextureHeight = drawable.colorTextures.first?.height ?? 0 + EngineStatsMonitor.shared.update { snapshot in + snapshot.compositor.viewCount = viewCount + snapshot.compositor.viewTextureWidth = viewTextureWidth + snapshot.compositor.viewTextureHeight = viewTextureHeight + } + #endif // Update visible entity list only when not loading (avoids reading mutating ECS data). // When loading, we render from the last-known-good visible list. @@ -1334,9 +1377,20 @@ // The frame's GPU time beyond the previous frame's: the two overlap on the // GPU, and the command buffer's own span would count the overlap twice. EngineStatsMonitor.shared.recordGPUCompletion(gpuStartTime: cb.gpuStartTime, gpuEndTime: cb.gpuEndTime) + // The GPU's margin against the compositor's deadlines, for the stats; gpuEndTime + // is zero where the driver does not report it. + if countsDeadline, cb.gpuEndTime > 0.0 { + EngineStatsMonitor.shared.recordCompositorCompletion( + deadlineMarginMs: (deadline - cb.gpuEndTime) * 1000.0, + presentationMarginMs: (presentation - cb.gpuEndTime) * 1000.0 + ) + } #endif if countsDeadline { self?.noteGPUCompletion(at: cb.gpuEndTime, start: cb.gpuStartTime, deadline: deadline, presentation: presentation) + if cb.gpuEndTime > deadline { + EngineProfiler.shared.emitEvent(.missedDeadline) + } } commandBufferSemaphore.signal() } diff --git a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift index 8ba16bd8c..aba6a8133 100644 --- a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift +++ b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift @@ -240,4 +240,107 @@ final class EngineStatsMonitorTests: XCTestCase { XCTAssertFalse(overlay.contains("nan"), "Output should not contain NaN") XCTAssertFalse(compact.contains("nan"), "Output should not contain NaN") } + + // MARK: - Compositor frame accounting + + func testRecordCompositorCompletion_publishesLatestMarginAndCumulativeCounts() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.recordCompositorCompletion(deadlineMarginMs: -1.5, presentationMarginMs: 2.0) + EngineStatsMonitor.shared.recordCompositorCompletion(deadlineMarginMs: 3.0, presentationMarginMs: 5.5) + EngineStatsMonitor.shared.completeFrame() + + let published = getEngineStatsSnapshot() + XCTAssertEqual(published.compositor.deadlineMarginMs, 3.0) + XCTAssertEqual(published.compositor.presentationMarginMs, 5.5) + XCTAssertFalse(published.compositor.missedDeadline) + XCTAssertEqual(published.compositor.missedDeadlineCount, 1) + XCTAssertEqual(published.compositor.deadlineSampleCount, 2) + XCTAssertEqual(published.compositor.missedDeadlineRate, 0.5, accuracy: 1e-9) + } + + func testRecordCompositorCompletion_negativeMargin_flagsMissedDeadline() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.recordCompositorCompletion(deadlineMarginMs: -0.25, presentationMarginMs: 1.0) + EngineStatsMonitor.shared.completeFrame() + + let published = getEngineStatsSnapshot() + XCTAssertTrue(published.compositor.missedDeadline) + XCTAssertEqual(published.compositor.missedDeadlineCount, 1) + XCTAssertEqual(published.compositor.deadlineSampleCount, 1) + XCTAssertEqual(published.compositor.missedDeadlineRate, 1.0, accuracy: 1e-9) + } + + func testCompositorCounters_survive_beginFrame_butPerFrameFieldsReset() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.update { snapshot in + snapshot.compositor.updateMs = 4.0 + snapshot.compositor.inputSlackMs = 1.25 + snapshot.compositor.viewCount = 2 + } + EngineStatsMonitor.shared.recordMissingAnchor() + EngineStatsMonitor.shared.recordCompositorCompletion(deadlineMarginMs: -2.0, presentationMarginMs: 0.5) + EngineStatsMonitor.shared.completeFrame() + + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 2.0) + let inProgress = getEngineStatsSnapshotInProgress() + XCTAssertEqual(inProgress.compositor.updateMs, 0.0) + XCTAssertEqual(inProgress.compositor.inputSlackMs, 0.0) + XCTAssertEqual(inProgress.compositor.viewCount, 0) + + EngineStatsMonitor.shared.completeFrame() + let published = getEngineStatsSnapshot() + XCTAssertEqual(published.compositor.missedDeadlineCount, 1) + XCTAssertEqual(published.compositor.deadlineSampleCount, 1) + XCTAssertEqual(published.compositor.missingAnchorCount, 1) + // The latest GPU sample carries over until a newer completion arrives. + XCTAssertEqual(published.compositor.deadlineMarginMs, -2.0) + } + + func testReset_clearsCompositorCounters() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.recordMissingAnchor() + EngineStatsMonitor.shared.recordCompositorCompletion(deadlineMarginMs: -2.0, presentationMarginMs: 0.5) + EngineStatsMonitor.shared.completeFrame() + + EngineStatsMonitor.shared.reset() + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 2.0) + EngineStatsMonitor.shared.completeFrame() + + let published = getEngineStatsSnapshot() + XCTAssertEqual(published.compositor.missedDeadlineCount, 0) + XCTAssertEqual(published.compositor.deadlineSampleCount, 0) + XCTAssertEqual(published.compositor.missingAnchorCount, 0) + XCTAssertEqual(published.compositor.deadlineMarginMs, 0.0) + XCTAssertFalse(published.compositor.missedDeadline) + } + + func testTimingSemaphoreWaitMs_roundTripsThroughSnapshot() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.update { snapshot in + snapshot.timing.semaphoreWaitMs = 0.75 + } + EngineStatsMonitor.shared.completeFrame() + + XCTAssertEqual(getEngineStatsSnapshot().timing.semaphoreWaitMs, 0.75) + } + + func testFormatEngineStats_compositorLine_onlyForCompositorFrames() { + var snapshot = EngineStatsSnapshot() + XCTAssertFalse(formatEngineStatsOverlay(snapshot).contains("Compositor:")) + + snapshot.compositor.viewCount = 2 + snapshot.compositor.viewTextureWidth = 2048 + snapshot.compositor.viewTextureHeight = 1984 + snapshot.compositor.deadlineMarginMs = -0.5 + snapshot.compositor.missedDeadline = true + snapshot.compositor.missedDeadlineCount = 3 + snapshot.compositor.deadlineSampleCount = 300 + snapshot.timing.semaphoreWaitMs = 0.1 + + let overlay = formatEngineStatsOverlay(snapshot) + XCTAssertTrue(overlay.contains("Compositor: views 2 @ 2048x1984"), overlay) + XCTAssertTrue(overlay.contains("deadlineMargin -0.50ms MISSED"), overlay) + XCTAssertTrue(overlay.contains("missed 3/300 (1.00%)"), overlay) + XCTAssertTrue(overlay.contains("semWait 0.10ms"), overlay) + } } diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md index 006e3e489..cf9c11ca3 100644 --- a/docs/API/UsingProfiler.md +++ b/docs/API/UsingProfiler.md @@ -47,8 +47,25 @@ TileReps: resident full/lod/hlod 24/8/2 | visible full/lod/hlod 18/5/1 | overlap TileRenderCost: visible full/lod/hlod 18/5/1 | draws full/lod/hlod 22/6/1 | tris full/lod/hlod 84000/9000/1200 Batching: groups 132 | batchedMeshes 916 | dirty 0→0 | defWork 0 skipComplex 2 | dispatched 0→0 groups | rebuilds/s 0 | rebuildMs 0.00 Memory: mesh 312/512mb | tex 198/512mb | total 50% | entities 847 +Compositor: views 2 @ 2048x1984 | update 2.10ms | inputSlack 1.45ms | submit 3.20ms | semWait 0.00ms | deadlineMargin 2.35ms | presentMargin 6.80ms | missed 3/5400 (0.06%) | noAnchor 0 ``` +**Compositor line** (visionOS only, absent elsewhere) — how the frame did against the Compositor Services deadlines. This is the number that matters on Apple Vision Pro: a frame whose GPU work finishes after the rendering deadline is reprojected by the compositor and felt as judder, whatever the average frame rate says. + +| Field | What it tells you | +|---|---| +| `views W@HxV` | Views (eyes) rendered and the per-view drawable size. Changes with foveation and render quality. | +| `update` | CPU time between `startUpdate()` and `endUpdate()`: game update, input, streaming ticks. | +| `inputSlack` | Time left until the compositor's `optimalInputTime` when the update phase ended. Negative means the update ran late and the submission started behind schedule. | +| `submit` | CPU time from `startSubmission()` to the command buffer commit, both eyes. Includes `semWait`. | +| `semWait` | Time spent waiting for a free in-flight command buffer. Persistently non-zero means the GPU is pacing the CPU. Also reported on macOS and iOS as `timing.semaphoreWaitMs`. | +| `deadlineMargin` | Time between the GPU finishing the most recently completed frame and that frame's `renderingDeadline`. Negative (and `MISSED`) means the compositor did not get the frame in time. Comes from the command buffer completion handler, so it can lag the CPU frame by one or two frames. | +| `presentMargin` | Same, against the frame's `presentationTime`. | +| `missed a/b (rate)` | Frames that missed the deadline over frames sampled since the monitor was reset. Steady-state target: well under 1 in 1000. | +| `noAnchor` | Frames presented without a fresh device anchor since reset (tracking gaps). | + +Programmatic access: `getEngineStatsSnapshot().compositor` (`EngineCompositorStats`), including `missedDeadlineRate`. + **Streaming line 1** — entity counts and slot pressure: | Field | What it tells you | @@ -376,6 +393,7 @@ When metrics are enabled, the engine emits signpost scopes: - `RenderPrep` - `Encode` - `Submit` +- `CompositorUpdate`, `CompositorWaitForInput`, `CompositorSubmission` (visionOS, category `Compositor`), plus a `MissedDeadline` point event whenever the GPU finishes a frame after the compositor's rendering deadline. Put the Metal System Trace GPU track next to this lane to see whether a miss was CPU-late (submission ends after the optimal input time) or GPU-late. To inspect timeline data: From 3891fe1ba0fa65c3194d0cf1cb5d3f90825deeb2 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 19:42:14 +0200 Subject: [PATCH 02/17] [Performance] Attribute update-phase CPU time to engine systems The Update scope was one number; when it grew there was no way to tell scene-graph propagation from animation, scripting, physics or the game callback without a Time Profiler session. - Wrap traverseSceneGraph, extension updates, LOD selection, animation, USC scripting, each fixed-step physics update, custom systems and the game update callback in a profileSystem helper that emits a signpost in a new Systems category and, when engine stats are compiled in, adds the CPU time to new per-system fields on EngineTimingStats (physics and custom systems sum over the fixed steps; physicsStepCount records the steps taken). - Print a Systems line in the verbose stats output and document it. --- .../Profiling/EngineSignposts.swift | 27 +++++++ .../Profiling/EngineStatsFormatter.swift | 1 + .../Profiling/EngineStatsSnapshot.swift | 35 +++++++- .../UntoldEngine/Renderer/UntoldEngine.swift | 79 +++++++++++++++---- .../EngineStatsMonitorTests.swift | 14 ++++ docs/API/UsingProfiler.md | 15 ++++ 6 files changed, 154 insertions(+), 17 deletions(-) diff --git a/Sources/UntoldEngine/Profiling/EngineSignposts.swift b/Sources/UntoldEngine/Profiling/EngineSignposts.swift index 3fb70bc37..99428d0f9 100644 --- a/Sources/UntoldEngine/Profiling/EngineSignposts.swift +++ b/Sources/UntoldEngine/Profiling/EngineSignposts.swift @@ -45,6 +45,16 @@ public enum ProfileScope { case compositorUpdate case compositorWaitForInput case compositorSubmission + + // Per-system CPU work inside the Update scope + case scenegraph + case extensionsUpdate + case lod + case animation + case scripting + case physics + case customSystems + case gameUpdate } /// Point events (no duration) that mark something worth seeing on the Instruments timeline. @@ -63,6 +73,7 @@ final class EngineSignposts { private static let streamingLog = OSLog(subsystem: subsystem, category: "Streaming") private static let batchingLog = OSLog(subsystem: subsystem, category: "Batching") private static let compositorLog = OSLog(subsystem: subsystem, category: "Compositor") + private static let systemsLog = OSLog(subsystem: subsystem, category: "Systems") private static let gaussianLog = OSLog(subsystem: subsystem, category: "Gaussian") // One stable signpost ID per scope. @@ -81,6 +92,14 @@ final class EngineSignposts { private static let compositorWaitForInputID = OSSignpostID(log: compositorLog) private static let compositorSubmissionID = OSSignpostID(log: compositorLog) private static let missedDeadlineID = OSSignpostID(log: compositorLog) + private static let scenegraphID = OSSignpostID(log: systemsLog) + private static let extensionsUpdateID = OSSignpostID(log: systemsLog) + private static let lodID = OSSignpostID(log: systemsLog) + private static let animationID = OSSignpostID(log: systemsLog) + private static let scriptingID = OSSignpostID(log: systemsLog) + private static let physicsID = OSSignpostID(log: systemsLog) + private static let customSystemsID = OSSignpostID(log: systemsLog) + private static let gameUpdateID = OSSignpostID(log: systemsLog) private static let gaussianCullID = OSSignpostID(log: gaussianLog) private static let gaussianDepthID = OSSignpostID(log: gaussianLog) private static let gaussianSortID = OSSignpostID(log: gaussianLog) @@ -123,6 +142,14 @@ final class EngineSignposts { case .compositorUpdate: return (Self.compositorLog, Self.compositorUpdateID, "CompositorUpdate") case .compositorWaitForInput: return (Self.compositorLog, Self.compositorWaitForInputID, "CompositorWaitForInput") case .compositorSubmission: return (Self.compositorLog, Self.compositorSubmissionID, "CompositorSubmission") + case .scenegraph: return (Self.systemsLog, Self.scenegraphID, "Scenegraph") + case .extensionsUpdate: return (Self.systemsLog, Self.extensionsUpdateID, "ExtensionsUpdate") + case .lod: return (Self.systemsLog, Self.lodID, "LOD") + case .animation: return (Self.systemsLog, Self.animationID, "Animation") + case .scripting: return (Self.systemsLog, Self.scriptingID, "Scripting") + case .physics: return (Self.systemsLog, Self.physicsID, "Physics") + case .customSystems: return (Self.systemsLog, Self.customSystemsID, "CustomSystems") + case .gameUpdate: return (Self.systemsLog, Self.gameUpdateID, "GameUpdate") } } } diff --git a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift index b30a5c641..1db7e191a 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift @@ -60,6 +60,7 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin return """ Frame \(snapshot.frameIndex) | CPU \(formatMs(snapshot.timing.smoothedFrameMs))ms (\(formatFPS(frameMs: snapshot.timing.smoothedFrameMs)) fps, smoothed) GPU \(formatMs(snapshot.timing.gpuExecutionMs))ms exec / \(formatFPS(frameMs: snapshot.timing.gpuFrameCadenceMs)) fps cadence [\(bottleneck)] Timing: frame \(formatMs(snapshot.timing.frameTotalMs))ms (raw CPU) | update \(formatMs(snapshot.timing.updateMs))ms | render \(formatMs(snapshot.timing.renderTotalMs))ms | cull \(formatMs(snapshot.timing.cullingMs))ms | stream \(formatMs(snapshot.timing.streamingRegionMs + snapshot.timing.geometryStreamingMs))ms | batchTick \(formatMs(snapshot.timing.batchingTickMs))ms | batchRebuild \(formatMs(snapshot.timing.batchingRebuildMs))ms + Systems: scenegraph \(formatMs(snapshot.timing.scenegraphMs))ms | extensions \(formatMs(snapshot.timing.extensionsUpdateMs))ms | lod \(formatMs(snapshot.timing.lodMs))ms | animation \(formatMs(snapshot.timing.animationMs))ms | scripting \(formatMs(snapshot.timing.scriptingMs))ms | physics \(formatMs(snapshot.timing.physicsMs))ms (\(snapshot.timing.physicsStepCount) steps) | custom \(formatMs(snapshot.timing.customSystemsMs))ms | game \(formatMs(snapshot.timing.gameUpdateMs))ms | semWait \(formatMs(snapshot.timing.semaphoreWaitMs))ms Render: draws \(snapshot.render.drawCallsTotal) (opaque \(snapshot.render.drawCallsOpaque), transparent \(snapshot.render.drawCallsTransparent), shadow \(snapshot.render.drawCallsShadow), batched \(snapshot.render.drawCallsBatched)) | triangles \(snapshot.render.trianglesTotal) | visible \(snapshot.render.visibleInstances) Culling: frustum \(snapshot.culling.frustumPassed)/\(snapshot.culling.frustumTested) failed \(snapshot.culling.frustumFailed) | occlusion \(snapshot.culling.occlusionPassed)/\(snapshot.culling.occlusionTested) failed \(snapshot.culling.occlusionFailed) | usedHZB \(snapshot.culling.usedHZB) validHZB \(snapshot.culling.hzbIsValid) Streaming: loaded \(snapshot.streaming.loadedStreamingEntities) loading \(snapshot.streaming.loadingStreamingEntities) unloaded \(snapshot.streaming.unloadedStreamingEntities) | active \(snapshot.streaming.activeLoads) | nearby \(snapshot.streaming.nearbyEntitiesQueried) candidates \(snapshot.streaming.loadCandidates) slots \(snapshot.streaming.availableLoadSlots) | backlog \(snapshot.streaming.pendingLoadBacklog) | pendingUploads \(snapshot.streaming.pendingUploadCount) | gateMs \(formatMs(snapshot.streaming.blockedByGateMs)) diff --git a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift index 5e380a969..697d30ddb 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift @@ -47,6 +47,21 @@ public struct EngineTimingStats { /// Persistently non-zero means the GPU (or the compositor) is pacing the CPU. public var semaphoreWaitMs: Double = 0.0 + // Per-system CPU time inside updateMs. Systems that did not run this frame stay at zero. + public var scenegraphMs: Double = 0.0 + public var extensionsUpdateMs: Double = 0.0 + public var lodMs: Double = 0.0 + public var animationMs: Double = 0.0 + public var scriptingMs: Double = 0.0 + /// All fixed-step physics updates this frame, summed. + public var physicsMs: Double = 0.0 + /// Number of fixed-step physics updates this frame (0 when the accumulator did not reach a step). + public var physicsStepCount: Int = 0 + /// All fixed-step custom system updates this frame, summed. + public var customSystemsMs: Double = 0.0 + /// The app's game update callback. + public var gameUpdateMs: Double = 0.0 + public init( frameTotalMs: Double = 0.0, smoothedFrameMs: Double = 0.0, @@ -65,7 +80,16 @@ public struct EngineTimingStats { batchingRebuildMs: Double = 0.0, presentationDeltaMs: Double = 0.0, skippedFrames: Int = 0, - semaphoreWaitMs: Double = 0.0 + semaphoreWaitMs: Double = 0.0, + scenegraphMs: Double = 0.0, + extensionsUpdateMs: Double = 0.0, + lodMs: Double = 0.0, + animationMs: Double = 0.0, + scriptingMs: Double = 0.0, + physicsMs: Double = 0.0, + physicsStepCount: Int = 0, + customSystemsMs: Double = 0.0, + gameUpdateMs: Double = 0.0 ) { self.frameTotalMs = frameTotalMs self.smoothedFrameMs = smoothedFrameMs @@ -85,6 +109,15 @@ public struct EngineTimingStats { self.presentationDeltaMs = presentationDeltaMs self.skippedFrames = skippedFrames self.semaphoreWaitMs = semaphoreWaitMs + self.scenegraphMs = scenegraphMs + self.extensionsUpdateMs = extensionsUpdateMs + self.lodMs = lodMs + self.animationMs = animationMs + self.scriptingMs = scriptingMs + self.physicsMs = physicsMs + self.physicsStepCount = physicsStepCount + self.customSystemsMs = customSystemsMs + self.gameUpdateMs = gameUpdateMs } } diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index 6f76b8e9c..995dc88ae 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -644,22 +644,28 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { #endif EngineProfiler.shared.beginScope(.update) calculateDeltaTime(presentationTime: presentationTime, nominalPeriod: nominalPeriod) - traverseSceneGraph() + profileSystem(.scenegraph, \.scenegraphMs) { + traverseSceneGraph() + } handleInputCallback?() let extensionUpdateContext = makeEngineExtensionUpdateContext() - RenderExtensionRegistry.shared.updateExtensions( - deltaTime: timeSinceLastUpdate, - context: extensionUpdateContext - ) - EngineExtensionRegistry.shared.updateExtensions( - deltaTime: timeSinceLastUpdate, - context: extensionUpdateContext - ) + profileSystem(.extensionsUpdate, \.extensionsUpdateMs) { + RenderExtensionRegistry.shared.updateExtensions( + deltaTime: timeSinceLastUpdate, + context: extensionUpdateContext + ) + EngineExtensionRegistry.shared.updateExtensions( + deltaTime: timeSinceLastUpdate, + context: extensionUpdateContext + ) + } // 3. LOD selection (decides which representation is active, checks residency) - LODSystem.shared.update(deltaTime: fixedStep) - GaussianLODSystem.shared.update(deltaTime: fixedStep) + profileSystem(.lod, \.lodMs) { + LODSystem.shared.update(deltaTime: fixedStep) + GaussianLODSystem.shared.update(deltaTime: fixedStep) + } // 4. Flush events (residency and LOD change events are processed) SystemEventBus.shared.flushEvents() @@ -686,17 +692,23 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { OctreeSystem.shared.updateDirtyBounds() if gameMode == true { - AnimationSystem.shared.update(timeSinceLastUpdate) + profileSystem(.animation, \.animationMs) { + AnimationSystem.shared.update(timeSinceLastUpdate) + } // USC scripts (runs every frame in Play mode) - USCSystem.shared.update(timeSinceLastUpdate) + profileSystem(.scripting, \.scriptingMs) { + USCSystem.shared.update(timeSinceLastUpdate) + } // fixed‐timestep physics physicsAccumulator += timeSinceLastUpdate let maxSteps = 5 var steps = 0 while physicsAccumulator >= fixedStep, steps < maxSteps { - updatePhysicsSystem(deltaTime: fixedStep) + profileSystem(.physics, \.physicsMs) { + updatePhysicsSystem(deltaTime: fixedStep) + } EngineExtensionRegistry.shared.fixedUpdateExtensions( deltaTime: fixedStep, context: extensionUpdateContext @@ -705,13 +717,23 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { deltaTime: fixedStep, context: extensionUpdateContext ) - updateCustomSystems(deltaTime: fixedStep) + profileSystem(.customSystems, \.customSystemsMs) { + updateCustomSystems(deltaTime: fixedStep) + } physicsAccumulator -= fixedStep steps += 1 } + #if ENGINE_STATS_ENABLED + let physicsSteps = steps + EngineStatsMonitor.shared.update { snapshot in + snapshot.timing.physicsStepCount = physicsSteps + } + #endif // user game update - gameUpdateCallback?(timeSinceLastUpdate) + profileSystem(.gameUpdate, \.gameUpdateMs) { + gameUpdateCallback?(timeSinceLastUpdate) + } frameUpdateCallback?(currentFrameTime) } @@ -883,6 +905,31 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { } /// XR path finalization hook. Call this once after XR submission for the frame. + /// Runs `body` inside a profiler signpost scope and, when engine stats are compiled in, adds its + /// CPU time to the `EngineTimingStats` field selected by `keyPath`. Systems that run several times + /// per frame (fixed-step physics) accumulate. + @inline(__always) + private func profileSystem( + _ scope: ProfileScope, + _ keyPath: WritableKeyPath, + _ body: () -> Void + ) { + EngineProfiler.shared.beginScope(scope) + #if ENGINE_STATS_ENABLED + let start = CACurrentMediaTime() + #endif + body() + #if ENGINE_STATS_ENABLED + let elapsedMs = (CACurrentMediaTime() - start) * 1000.0 + EngineStatsMonitor.shared.update { snapshot in + snapshot.timing[keyPath: keyPath] += elapsedMs + } + #else + _ = keyPath + #endif + EngineProfiler.shared.endScope(scope) + } + public func finalizeXRStatsAndMonitors(frameStartTime: Double) { #if ENGINE_STATS_ENABLED publishEngineStats(frameStartTime: frameStartTime) diff --git a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift index aba6a8133..af7123155 100644 --- a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift +++ b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift @@ -314,6 +314,20 @@ final class EngineStatsMonitorTests: XCTestCase { XCTAssertFalse(published.compositor.missedDeadline) } + func testFormatEngineStatsOverlay_containsSystemsLine() { + var snapshot = EngineStatsSnapshot() + snapshot.timing.animationMs = 1.5 + snapshot.timing.physicsMs = 0.75 + snapshot.timing.physicsStepCount = 2 + snapshot.timing.gameUpdateMs = 0.25 + + let overlay = formatEngineStatsOverlay(snapshot) + XCTAssertTrue(overlay.contains("Systems: "), overlay) + XCTAssertTrue(overlay.contains("animation 1.50ms"), overlay) + XCTAssertTrue(overlay.contains("physics 0.75ms (2 steps)"), overlay) + XCTAssertTrue(overlay.contains("game 0.25ms"), overlay) + } + func testTimingSemaphoreWaitMs_roundTripsThroughSnapshot() { EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) EngineStatsMonitor.shared.update { snapshot in diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md index cf9c11ca3..fc19544f7 100644 --- a/docs/API/UsingProfiler.md +++ b/docs/API/UsingProfiler.md @@ -39,6 +39,7 @@ With `profile: .verbose`, the engine logs a multi-line snapshot every interval: ``` Frame 1234 | CPU 12.34ms (81.0 fps, smoothed) GPU 8.45ms exec / 90.0 fps cadence [GPU-bound] Timing: frame 12.34ms (raw CPU) | update 1.23ms | render 8.45ms | cull 0.45ms | stream 2.34ms | batchTick 0.12ms | batchRebuild 0.00ms +Systems: scenegraph 0.05ms | extensions 0.02ms | lod 0.10ms | animation 0.60ms | scripting 0.01ms | physics 0.30ms (1 steps) | custom 0.00ms | game 0.12ms | semWait 0.00ms Render: draws 45 (opaque 32, transparent 3, shadow 8, batched 28) | triangles 125000 | visible 89 Culling: frustum 234/512 failed 278 | occlusion 198/234 failed 36 | usedHZB true validHZB true Streaming: loaded 847 loading 3 unloaded 12 | active 3 | nearby 124 candidates 5 slots 4 | backlog 0 | pendingUploads 3 | gateMs 0.00 @@ -66,6 +67,20 @@ Compositor: views 2 @ 2048x1984 | update 2.10ms | inputSlack 1.45ms | submit 3.2 Programmatic access: `getEngineStatsSnapshot().compositor` (`EngineCompositorStats`), including `missedDeadlineRate`. +**Systems line** — where the `update` time goes, per engine system. Each value is the CPU time of that system this frame; `physics` and `custom` sum every fixed step and the step count is shown. The same scopes are emitted as signposts in the `Systems` category, so Instruments shows them as lanes without symbolication. + +| Field | What it tells you | +|---|---| +| `scenegraph` | World-transform propagation. Non-zero only when something moved; large values mean many dirty transforms or a deep hierarchy scan. | +| `extensions` | Render and engine extension `update` hooks. | +| `lod` | Mesh and Gaussian LOD selection (interval and camera-displacement gated). | +| `animation` | Clip sampling, blending, root motion, IK, pose and joint matrix updates for every animated entity. | +| `scripting` | USC script execution. | +| `physics` | Fixed-step physics, summed over the steps taken this frame. More than one step per frame at 90 Hz means the frame is late. | +| `custom` | Registered custom systems, summed over the fixed steps. | +| `game` | The app's `gameUpdate` callback. | +| `semWait` | Time waiting for a free in-flight command buffer before encoding (see the Compositor line). | + **Streaming line 1** — entity counts and slot pressure: | Field | What it tells you | From 361828a9347ded755214e9e549b0ec8e0bfd02fe Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 19:52:50 +0200 Subject: [PATCH 03/17] [Performance] Time every render, compute and blit pass on the GPU GPU time was one number per command buffer, which on visionOS holds culling, Gaussian sorting, both eyes and the HZB build. There was no way to attribute it to a pass without a Metal System Trace session. The engine's GPUPassTiming (Profiling/GPUPassTiming.swift) times the passes: MTLCounterSampleBuffer timestamps at stage boundaries, a ring of sample buffers per in-flight frame, resolution when the command buffer completes, passes merged by label (per-eye passes sum), switched on with setRendering(.gpuPassTiming(.enabled)). - GPUPassTimingSnapshot: the timer's last resolved frame as the engine stats carry it (the passes, their sum, ms(for:)), published into EngineStatsSnapshot.gpuPasses once per frame. - The verbose stats output prints the heaviest passes; the timer and the snapshot are documented in docs/API/UsingProfiler.md. - Passes the timer did not cover are timed too: the three Gaussian compute passes, the Gaussian occlusion depth copy, the extension surfaces and the scene render target. Picking encodes on a command buffer of its own and stays untimed. - GPUPassStatsTests: the stats carry the timed passes. --- .../Profiling/EngineStatsFormatter.swift | 13 ++++- .../Profiling/EngineStatsSnapshot.swift | 6 +- .../Profiling/GPUPassTimingSnapshot.swift | 40 +++++++++++++ .../RenderExtensionModelSurface.swift | 1 + .../UntoldEngine/Renderer/RenderPasses.swift | 2 +- .../Renderer/SampleRenderExtension.swift | 1 + .../UntoldEngine/Renderer/UntoldEngine.swift | 2 + .../UntoldEngine/Systems/GaussianSystem.swift | 6 +- .../UntoldEngine/Systems/GraphBuilder.swift | 7 ++- .../GPUPassStatsTests.swift | 58 +++++++++++++++++++ docs/API/UsingProfiler.md | 33 +++++++++++ 11 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift create mode 100644 Tests/UntoldEngineRenderTests/GPUPassStatsTests.swift diff --git a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift index 1db7e191a..eaff3f59f 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift @@ -57,6 +57,7 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin let memPct = String(format: "%.0f%%", snapshot.memory.utilizationPercent * 100) let pressure = snapshot.memory.isUnderPressure ? " PRESSURE" : "" let compositorLine = compositorStatsLine(snapshot) + let gpuPassLine = gpuPassStatsLine(snapshot) return """ Frame \(snapshot.frameIndex) | CPU \(formatMs(snapshot.timing.smoothedFrameMs))ms (\(formatFPS(frameMs: snapshot.timing.smoothedFrameMs)) fps, smoothed) GPU \(formatMs(snapshot.timing.gpuExecutionMs))ms exec / \(formatFPS(frameMs: snapshot.timing.gpuFrameCadenceMs)) fps cadence [\(bottleneck)] Timing: frame \(formatMs(snapshot.timing.frameTotalMs))ms (raw CPU) | update \(formatMs(snapshot.timing.updateMs))ms | render \(formatMs(snapshot.timing.renderTotalMs))ms | cull \(formatMs(snapshot.timing.cullingMs))ms | stream \(formatMs(snapshot.timing.streamingRegionMs + snapshot.timing.geometryStreamingMs))ms | batchTick \(formatMs(snapshot.timing.batchingTickMs))ms | batchRebuild \(formatMs(snapshot.timing.batchingRebuildMs))ms @@ -68,10 +69,20 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin TileReps: resident full/lod/hlod \(snapshot.streaming.residentFullTileRepresentations)/\(snapshot.streaming.residentLODRepresentations)/\(snapshot.streaming.residentHLODRepresentations) | visible full/lod/hlod \(snapshot.streaming.visibleFullTileRepresentations)/\(snapshot.streaming.visibleLODRepresentations)/\(snapshot.streaming.visibleHLODRepresentations) | overlap visible full+lod/full+hlod/lod+hlod \(snapshot.streaming.fullAndLODVisibleOverlapTiles)/\(snapshot.streaming.fullAndHLODVisibleOverlapTiles)/\(snapshot.streaming.lodAndHLODVisibleOverlapTiles) residentFull+fallback \(snapshot.streaming.fullAndFallbackResidentOverlapTiles) | fades \(snapshot.streaming.activeTileRepresentationFades) waiting \(snapshot.streaming.waitingTileRepresentationFades) TileRenderCost: visible full/lod/hlod \(snapshot.render.tileFullVisibleInstances)/\(snapshot.render.tileLODVisibleInstances)/\(snapshot.render.tileHLODVisibleInstances) | draws full/lod/hlod \(snapshot.render.tileFullDrawsEstimate)/\(snapshot.render.tileLODDrawsEstimate)/\(snapshot.render.tileHLODDrawsEstimate) | tris full/lod/hlod \(snapshot.render.tileFullTrianglesEstimate)/\(snapshot.render.tileLODTrianglesEstimate)/\(snapshot.render.tileHLODTrianglesEstimate) Batching: groups \(snapshot.batching.batchGroupCount) | batchedMeshes \(snapshot.batching.batchedMeshCount) | dirty \(snapshot.batching.dirtyCellsBeforePrune)→\(snapshot.batching.dirtyCellsAfterPrune) | defWork \(snapshot.batching.deferredByWorkBudget) skipComplex \(snapshot.batching.skippedByComplexityGuard) | dispatched \(snapshot.batching.dispatchedBuilds)→\(snapshot.batching.lastRebuildOutputBatchCount) groups | rebuilds/s \(snapshot.batching.rebuildsThisSecond) | rebuildMs \(formatMs(snapshot.batching.lastRebuildCostMs)) - Memory: mesh \(meshMB)/\(meshBudgetMB)mb | tex \(texMB)/\(texBudgetMB)mb | total \(memPct) | entities \(snapshot.memory.trackedEntityCount)\(pressure)\(compositorLine) + Memory: mesh \(meshMB)/\(meshBudgetMB)mb | tex \(texMB)/\(texBudgetMB)mb | total \(memPct) | entities \(snapshot.memory.trackedEntityCount)\(pressure)\(compositorLine)\(gpuPassLine) """ } +/// One line with the heaviest GPU passes of the last resolved frame, prefixed with a newline. +/// Empty when the GPU pass timer is off. +private func gpuPassStatsLine(_ snapshot: EngineStatsSnapshot) -> String { + let g = snapshot.gpuPasses + guard !g.passes.isEmpty else { return "" } + let shown = g.passes.sorted { $0.totalMs > $1.totalMs }.prefix(12) + let entries = shown.map { "\($0.label) \(formatMs($0.totalMs))" }.joined(separator: " | ") + return "\nGPU passes (\(g.passes.count), sum \(formatMs(g.totalMs))ms): \(entries)" +} + /// One line of Compositor Services frame accounting, prefixed with a newline so it can be appended /// to the expanded block. Empty when the frame was not rendered through Compositor Services. private func compositorStatsLine(_ snapshot: EngineStatsSnapshot) -> String { diff --git a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift index 697d30ddb..071ba2a27 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift @@ -479,6 +479,8 @@ public struct EngineStatsSnapshot { public var batching: EngineBatchingStats = .init() public var memory: EngineMemoryStats = .init() public var compositor: EngineCompositorStats = .init() + /// Per-pass GPU timings of the last resolved frame (see `GPUPassTiming`). Empty when the timer is off. + public var gpuPasses: GPUPassTimingSnapshot = .init() public init( frameIndex: UInt64 = 0, @@ -489,7 +491,8 @@ public struct EngineStatsSnapshot { streaming: EngineStreamingStats = .init(), batching: EngineBatchingStats = .init(), memory: EngineMemoryStats = .init(), - compositor: EngineCompositorStats = .init() + compositor: EngineCompositorStats = .init(), + gpuPasses: GPUPassTimingSnapshot = .init() ) { self.frameIndex = frameIndex self.timestampSeconds = timestampSeconds @@ -500,5 +503,6 @@ public struct EngineStatsSnapshot { self.batching = batching self.memory = memory self.compositor = compositor + self.gpuPasses = gpuPasses } } diff --git a/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift b/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift new file mode 100644 index 000000000..11622d3f1 --- /dev/null +++ b/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift @@ -0,0 +1,40 @@ +// +// GPUPassTimingSnapshot.swift +// UntoldEngine +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation + +/// The per-pass GPU times of the last frame `GPUPassTiming` resolved, as the engine stats +/// carry them: a copy taken when the frame's stats are published. +public struct GPUPassTimingSnapshot: Equatable, Sendable { + /// A number that changes with each resolved frame; zero before the first. + public var frameIndex: Int = 0 + /// The frame's passes, heaviest first. Empty when the timer is off. + public var passes: [GPUPassTiming.PassTime] = [] + /// Sum of the passes' times. Passes can overlap on the GPU, so this is an upper bound on + /// the frame's GPU time. + public var totalMs: Double = 0.0 + + public init(frameIndex: Int = 0, passes: [GPUPassTiming.PassTime] = []) { + self.frameIndex = frameIndex + self.passes = passes + totalMs = passes.reduce(0) { $0 + $1.totalMs } + } + + /// The time of the pass with this label, or nil when the frame had none. + public func ms(for label: String) -> Double? { + passes.first { $0.label == label }?.totalMs + } + + /// The timer's last resolved frame. + public static func current() -> GPUPassTimingSnapshot { + let frame = GPUPassTiming.shared.lastFrame() + return GPUPassTimingSnapshot(frameIndex: frame.index, passes: frame.passes) + } +} diff --git a/Sources/UntoldEngine/Renderer/RenderExtensionModelSurface.swift b/Sources/UntoldEngine/Renderer/RenderExtensionModelSurface.swift index e80f6d21e..aa0b1301c 100644 --- a/Sources/UntoldEngine/Renderer/RenderExtensionModelSurface.swift +++ b/Sources/UntoldEngine/Renderer/RenderExtensionModelSurface.swift @@ -246,6 +246,7 @@ public extension RenderPassContext { descriptor.depthAttachment.loadAction = .load descriptor.depthAttachment.storeAction = .store + GPUPassTiming.shared.attach(descriptor, label) guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { handleError(.renderPassCreationFailed, label) return diff --git a/Sources/UntoldEngine/Renderer/RenderPasses.swift b/Sources/UntoldEngine/Renderer/RenderPasses.swift index ba656e4e9..479b7e8a5 100644 --- a/Sources/UntoldEngine/Renderer/RenderPasses.swift +++ b/Sources/UntoldEngine/Renderer/RenderPasses.swift @@ -5360,7 +5360,7 @@ public enum RenderPasses { { let width = min(sourceDepth.width, opaqueDepthSnapshot.width) let height = min(sourceDepth.height, opaqueDepthSnapshot.height) - if width > 0, height > 0, let blitEncoder = commandBuffer.makeBlitCommandEncoder() { + if width > 0, height > 0, let blitEncoder = GPUPassTiming.shared.makeBlitEncoder(commandBuffer, "GaussianDepthCopy") { blitEncoder.label = "Copy Opaque Depth for Gaussian Occlusion" blitEncoder.copy( from: sourceDepth, diff --git a/Sources/UntoldEngine/Renderer/SampleRenderExtension.swift b/Sources/UntoldEngine/Renderer/SampleRenderExtension.swift index e3c6f0a88..f57ccb226 100644 --- a/Sources/UntoldEngine/Renderer/SampleRenderExtension.swift +++ b/Sources/UntoldEngine/Renderer/SampleRenderExtension.swift @@ -74,6 +74,7 @@ public final class SampleRenderExtension: RenderExtension, @unchecked Sendable { clearColor.w ) + GPUPassTiming.shared.attach(descriptor, "SampleExtension") guard let encoder = context.commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index 995dc88ae..d4507ea8c 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -342,11 +342,13 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { let tileRenderCosts = auditVisibleTileRenderCosts() let memStats = MemoryBudgetManager.shared.getStats() let frame = currentFrameTime + let gpuPasses = GPUPassTimingSnapshot.current() EngineStatsMonitor.shared.update { snapshot in snapshot.timing.frameTotalMs = frameTotalMs snapshot.timing.presentationDeltaMs = frame.deltaTime * 1000.0 snapshot.timing.skippedFrames = frame.skippedFrames + snapshot.gpuPasses = gpuPasses snapshot.render.drawCallsTotal = drawStats.drawCallsTotal snapshot.render.drawCallsOpaque = drawStats.drawCallsOpaque diff --git a/Sources/UntoldEngine/Systems/GaussianSystem.swift b/Sources/UntoldEngine/Systems/GaussianSystem.swift index 587f75bcd..0fe512479 100644 --- a/Sources/UntoldEngine/Systems/GaussianSystem.swift +++ b/Sources/UntoldEngine/Systems/GaussianSystem.swift @@ -308,7 +308,7 @@ public func executeGaussianFrustumCulling(_ commandBuffer: MTLCommandBuffer) { return } - guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } + guard let computeEncoder = GPUPassTiming.shared.makeComputeEncoder(commandBuffer, "GaussianCull") else { return } computeEncoder.label = "Gaussian Frustum Culling" // Chunked (.untoldgs) entities cull chunk by chunk and are fitted to the budget; their @@ -844,7 +844,7 @@ public func executeGaussianPreprocess(_ commandBuffer: MTLCommandBuffer) { let uniformQuotas = GaussianDebugOptions.shared.disableScreenWeightedQuotas let levelSwitches = GaussianPagingFrameSwitches() - guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } + guard let computeEncoder = GPUPassTiming.shared.makeComputeEncoder(commandBuffer, "GaussianPreprocess") else { return } computeEncoder.label = "Gaussian Preprocess" // Zero the shared set's append counter on this same serial encoder, so the reset can never @@ -1174,7 +1174,7 @@ public func executeRadixSort(_ commandBuffer: MTLCommandBuffer) { // Single compute encoder for all passes. Dispatches within one encoder execute // sequentially on the GPU, so no inter-encoder synchronisation is needed. - guard let enc = commandBuffer.makeComputeCommandEncoder() else { return } + guard let enc = GPUPassTiming.shared.makeComputeEncoder(commandBuffer, "RadixSort") else { return } enc.label = "Radix Sort" for pass in 0 ..< 4 { diff --git a/Sources/UntoldEngine/Systems/GraphBuilder.swift b/Sources/UntoldEngine/Systems/GraphBuilder.swift index 76774a7aa..93325117f 100644 --- a/Sources/UntoldEngine/Systems/GraphBuilder.swift +++ b/Sources/UntoldEngine/Systems/GraphBuilder.swift @@ -957,8 +957,11 @@ func makeSceneRenderTargetAccess( guard let encoderDescriptor = makeSceneRenderPassDescriptor( copying: descriptor, actions: actions - ), let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: encoderDescriptor) - else { + ) else { + return nil + } + GPUPassTiming.shared.attach(encoderDescriptor, label ?? "SceneTarget") + guard let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: encoderDescriptor) else { return nil } encoder.label = label diff --git a/Tests/UntoldEngineRenderTests/GPUPassStatsTests.swift b/Tests/UntoldEngineRenderTests/GPUPassStatsTests.swift new file mode 100644 index 000000000..1c113487a --- /dev/null +++ b/Tests/UntoldEngineRenderTests/GPUPassStatsTests.swift @@ -0,0 +1,58 @@ +// +// GPUPassStatsTests.swift +// UntoldEngine +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import simd +@testable import UntoldEngine +import XCTest + +/// The engine stats carry the GPU pass timer's last frame: with the timing on, the snapshot +/// lists the frame's passes and the overlay prints them; off, it lists none. +@MainActor +final class GPUPassStatsTests: BaseRenderSetup { + override func tearDown() async throws { + setRendering(.gpuPassTiming(.disabled)) + destroyAllEntities() + try await super.tearDown() + } + + override func initializeAssets() { + let camera = createEntity() + createGameCamera(entityId: camera) + CameraSystem.shared.activeCamera = camera + cameraLookAt(entityId: camera, eye: simd_float3(0, 1, 4), target: .zero, up: simd_float3(0, 1, 0)) + let box = createEntity() + setEntityMeshDirect(entityId: box, meshes: BasicPrimitives.createCube(extent: 1.0), assetName: "Box") + translateTo(entityId: box, position: simd_float3(0, 0.5, 0)) + let sun = createEntity() + createDirLight(entityId: sun) + rotateTo(entityId: sun, pitch: -40, yaw: 30, roll: 0) + } + + private func drawFrames(_ count: Int) { + for _ in 0 ..< count { + renderer.draw(in: renderer.metalView) + renderInfo.lastCommandBuffer?.waitUntilCompleted() + } + } + + func testTheStatsCarryTheTimedPasses() throws { + try XCTSkipUnless(renderInfo.device.supportsCounterSampling(.atStageBoundary), "no stage-boundary sampling here") + drawFrames(3) + XCTAssertTrue(getEngineStatsSnapshot().gpuPasses.passes.isEmpty, "off by default") + + setRendering(.gpuPassTiming(.enabled)) + drawFrames(10) + let stats = getEngineStatsSnapshot() + XCTAssertFalse(stats.gpuPasses.passes.isEmpty, "the stats carry the frame's passes") + XCTAssertEqual(stats.gpuPasses.totalMs, stats.gpuPasses.passes.reduce(0) { $0 + $1.totalMs }, accuracy: 1e-9) + XCTAssertGreaterThan(stats.gpuPasses.totalMs, 0) + XCTAssertTrue(formatEngineStatsOverlay(stats).contains("GPU passes ("), formatEngineStatsOverlay(stats)) + } +} diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md index fc19544f7..03c4e134d 100644 --- a/docs/API/UsingProfiler.md +++ b/docs/API/UsingProfiler.md @@ -158,6 +158,39 @@ print("Update: \(frameStats.timing.updateMs) ms") print("Render: \(frameStats.timing.renderTotalMs) ms") ``` +## GPU Pass Timing + +`GPUPassTiming` times every render, compute and blit pass of the frame on the GPU itself, from `MTLCounterSampleBuffer` timestamps at the stage boundaries: a render pass's vertex and fragment stages, a compute or blit pass as a whole. It is off by default, and a GPU without stage-boundary sampling measures nothing. + +Enable it from the settings API: + +```swift +setRendering(.gpuPassTiming(.enabled)) +``` + +Read the recent frames' averages or the last resolved frame from the timer, or the last resolved frame through the engine stats snapshot: + +```swift +for pass in GPUPassTiming.shared.averages() { // heaviest first + print("\(pass.label): vertex \(pass.vertexMs) ms, fragment \(pass.fragmentMs) ms") +} +let last = getEngineStatsSnapshot().gpuPasses // GPUPassTimingSnapshot +print(last.ms(for: "Shadows") ?? 0) +``` + +The verbose stats output adds one line with the twelve heaviest passes of the last resolved frame: + +``` +GPU passes (9, sum 6.21ms): Forward 3.10 | Shadows 0.62 | SSAO 0.55 | Post 0.40 | HZB 0.12 | ... +``` + +| Field | What it tells you | +|---|---| +| `label` | The pass's name in the timer. The same label in one frame (one pass per eye on the headset, one per shadow cascade) adds up. | +| `sum` | Sum of all pass times. Passes can overlap on the GPU, so this is an upper bound on the command buffer's GPU time. | + +Results come from the command buffer's completion handler, so the snapshot describes the most recently *completed* frame. + ## OOC And Asset Triage Mode High-volume instrumentation categories are disabled by default: From 4418ee13d21091e1acaed0ecbb4c0335d9306046 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 19:59:10 +0200 Subject: [PATCH 04/17] [Performance] Count hitches, record runs to JSON Lines, sample thermal state and GPU memory Mean and p99 frame times hide single long frames, and a benchmark run had no way to leave a file behind that a script could compare. - EngineHitchStats: cumulative frame-time histogram (buckets straddling the 90 Hz and 60 Hz periods), frames over a configurable budget (EngineStatsMonitor.frameBudgetMs, set to the 90 Hz period by the visionOS runtime), and a rolling one-second window with its worst frame. - EngineStatsRecorder with startEngineStatsRecording(to:interval:) and stopEngineStatsRecording(): every published snapshot (or the last of each second) written as JSON Lines on a utility queue, plus a summary line with frame percentiles, frames over budget, mean GPU time, deadline misses, mean GPU time per pass and peak GPU allocation. Every stats struct is now Codable and Sendable. - Memory stats gain MTLDevice.currentAllocatedSize, os_proc_available_memory (iOS/visionOS) and ProcessInfo.thermalState; thermal transitions emit a ThermalStateChanged signpost event. - The AR path times its passes too: GPUPassTiming's frame begins and ends around its command buffer. - Hitches line, memory fields and the recorder documented; tests added. --- .../Profiling/EngineSignposts.swift | 5 + .../Profiling/EngineStatsFormatter.swift | 25 ++- .../Profiling/EngineStatsMonitor.swift | 96 ++++++++ .../Profiling/EngineStatsRecorder.swift | 208 ++++++++++++++++++ .../Profiling/EngineStatsSnapshot.swift | 89 +++++++- .../Profiling/GPUPassTiming.swift | 2 +- .../Profiling/GPUPassTimingSnapshot.swift | 2 +- .../UntoldEngine/Renderer/UntoldEngine.swift | 10 + Sources/UntoldEngineAR/UntoldEngineAR.swift | 2 + Sources/UntoldEngineXR/UntoldEngineXR.swift | 2 + .../EngineStatsMonitorTests.swift | 170 ++++++++++++++ docs/API/UsingProfiler.md | 34 ++- 12 files changed, 632 insertions(+), 13 deletions(-) create mode 100644 Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift diff --git a/Sources/UntoldEngine/Profiling/EngineSignposts.swift b/Sources/UntoldEngine/Profiling/EngineSignposts.swift index 99428d0f9..c8f8a74c5 100644 --- a/Sources/UntoldEngine/Profiling/EngineSignposts.swift +++ b/Sources/UntoldEngine/Profiling/EngineSignposts.swift @@ -61,6 +61,8 @@ public enum ProfileScope { public enum ProfileEvent { /// The GPU finished a frame after the compositor's rendering deadline. case missedDeadline + /// `ProcessInfo.thermalState` changed between two frames. + case thermalStateChanged } final class EngineSignposts { @@ -92,6 +94,7 @@ final class EngineSignposts { private static let compositorWaitForInputID = OSSignpostID(log: compositorLog) private static let compositorSubmissionID = OSSignpostID(log: compositorLog) private static let missedDeadlineID = OSSignpostID(log: compositorLog) + private static let thermalStateChangedID = OSSignpostID(log: frameLog) private static let scenegraphID = OSSignpostID(log: systemsLog) private static let extensionsUpdateID = OSSignpostID(log: systemsLog) private static let lodID = OSSignpostID(log: systemsLog) @@ -119,6 +122,8 @@ final class EngineSignposts { switch event { case .missedDeadline: os_signpost(.event, log: Self.compositorLog, name: "MissedDeadline", signpostID: Self.missedDeadlineID) + case .thermalStateChanged: + os_signpost(.event, log: Self.frameLog, name: "ThermalStateChanged", signpostID: Self.thermalStateChangedID) } } diff --git a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift index eaff3f59f..3debc1484 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsFormatter.swift @@ -58,6 +58,9 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin let pressure = snapshot.memory.isUnderPressure ? " PRESSURE" : "" let compositorLine = compositorStatsLine(snapshot) let gpuPassLine = gpuPassStatsLine(snapshot) + let hitchLine = hitchStatsLine(snapshot) + let gpuAllocMB = formatMB(snapshot.memory.gpuAllocatedBytes) + let availableMB = snapshot.memory.availableMemoryBytes > 0 ? " | avail \(formatMB(snapshot.memory.availableMemoryBytes))mb" : "" return """ Frame \(snapshot.frameIndex) | CPU \(formatMs(snapshot.timing.smoothedFrameMs))ms (\(formatFPS(frameMs: snapshot.timing.smoothedFrameMs)) fps, smoothed) GPU \(formatMs(snapshot.timing.gpuExecutionMs))ms exec / \(formatFPS(frameMs: snapshot.timing.gpuFrameCadenceMs)) fps cadence [\(bottleneck)] Timing: frame \(formatMs(snapshot.timing.frameTotalMs))ms (raw CPU) | update \(formatMs(snapshot.timing.updateMs))ms | render \(formatMs(snapshot.timing.renderTotalMs))ms | cull \(formatMs(snapshot.timing.cullingMs))ms | stream \(formatMs(snapshot.timing.streamingRegionMs + snapshot.timing.geometryStreamingMs))ms | batchTick \(formatMs(snapshot.timing.batchingTickMs))ms | batchRebuild \(formatMs(snapshot.timing.batchingRebuildMs))ms @@ -69,10 +72,30 @@ private func expandedEngineStatsString(_ snapshot: EngineStatsSnapshot) -> Strin TileReps: resident full/lod/hlod \(snapshot.streaming.residentFullTileRepresentations)/\(snapshot.streaming.residentLODRepresentations)/\(snapshot.streaming.residentHLODRepresentations) | visible full/lod/hlod \(snapshot.streaming.visibleFullTileRepresentations)/\(snapshot.streaming.visibleLODRepresentations)/\(snapshot.streaming.visibleHLODRepresentations) | overlap visible full+lod/full+hlod/lod+hlod \(snapshot.streaming.fullAndLODVisibleOverlapTiles)/\(snapshot.streaming.fullAndHLODVisibleOverlapTiles)/\(snapshot.streaming.lodAndHLODVisibleOverlapTiles) residentFull+fallback \(snapshot.streaming.fullAndFallbackResidentOverlapTiles) | fades \(snapshot.streaming.activeTileRepresentationFades) waiting \(snapshot.streaming.waitingTileRepresentationFades) TileRenderCost: visible full/lod/hlod \(snapshot.render.tileFullVisibleInstances)/\(snapshot.render.tileLODVisibleInstances)/\(snapshot.render.tileHLODVisibleInstances) | draws full/lod/hlod \(snapshot.render.tileFullDrawsEstimate)/\(snapshot.render.tileLODDrawsEstimate)/\(snapshot.render.tileHLODDrawsEstimate) | tris full/lod/hlod \(snapshot.render.tileFullTrianglesEstimate)/\(snapshot.render.tileLODTrianglesEstimate)/\(snapshot.render.tileHLODTrianglesEstimate) Batching: groups \(snapshot.batching.batchGroupCount) | batchedMeshes \(snapshot.batching.batchedMeshCount) | dirty \(snapshot.batching.dirtyCellsBeforePrune)→\(snapshot.batching.dirtyCellsAfterPrune) | defWork \(snapshot.batching.deferredByWorkBudget) skipComplex \(snapshot.batching.skippedByComplexityGuard) | dispatched \(snapshot.batching.dispatchedBuilds)→\(snapshot.batching.lastRebuildOutputBatchCount) groups | rebuilds/s \(snapshot.batching.rebuildsThisSecond) | rebuildMs \(formatMs(snapshot.batching.lastRebuildCostMs)) - Memory: mesh \(meshMB)/\(meshBudgetMB)mb | tex \(texMB)/\(texBudgetMB)mb | total \(memPct) | entities \(snapshot.memory.trackedEntityCount)\(pressure)\(compositorLine)\(gpuPassLine) + Memory: mesh \(meshMB)/\(meshBudgetMB)mb | tex \(texMB)/\(texBudgetMB)mb | total \(memPct) | entities \(snapshot.memory.trackedEntityCount)\(pressure) | gpuAlloc \(gpuAllocMB)mb\(availableMB) | thermal \(snapshot.memory.thermalStateName)\(hitchLine)\(compositorLine)\(gpuPassLine) """ } +/// One line of frame-time distribution, prefixed with a newline. Empty until a frame was sampled. +private func hitchStatsLine(_ snapshot: EngineStatsSnapshot) -> String { + let h = snapshot.hitches + guard h.framesSampled > 0 else { return "" } + let rate = String(format: "%.2f%%", h.overBudgetRate * 100) + var buckets: [String] = [] + for (index, count) in h.histogram.enumerated() { + if index < EngineHitchStats.bucketUpperBoundsMs.count { + buckets.append("<\(formatBound(EngineHitchStats.bucketUpperBoundsMs[index])) \(count)") + } else { + buckets.append(">=\(formatBound(EngineHitchStats.bucketUpperBoundsMs.last ?? 0)) \(count)") + } + } + return "\nHitches: budget \(formatMs(h.frameBudgetMs))ms | over \(h.framesOverBudget)/\(h.framesSampled) (\(rate)) | lastSec \(h.framesOverBudgetLastSecond)/\(h.framesLastSecond) over, worst \(formatMs(h.worstFrameMsLastSecond))ms | hist \(buckets.joined(separator: " | "))" +} + +private func formatBound(_ value: Double) -> String { + value == value.rounded() ? String(format: "%.0f", value) : String(format: "%.1f", value) +} + /// One line with the heaviest GPU passes of the last resolved frame, prefixed with a newline. /// Empty when the GPU pass timer is off. private func gpuPassStatsLine(_ snapshot: EngineStatsSnapshot) -> String { diff --git a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift index bb33bd789..75812bcf3 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift @@ -53,6 +53,19 @@ public final class EngineStatsMonitor: @unchecked Sendable { private var _deadlineSampleCount: Int = 0 private var _missingAnchorCount: Int = 0 + // Hitch accounting (frame-time histogram, over-budget counts, one-second window) + private var _frameBudgetMs: Double = 1000.0 / 60.0 + private var _hitches = EngineHitchStats() + private var _windowStartSeconds: Double = 0.0 + private var _windowStarted = false + private var _windowFrames: Int = 0 + private var _windowFramesOverBudget: Int = 0 + private var _windowWorstFrameMs: Double = 0.0 + private var _lastThermalState: Int = -1 + + /// Optional JSON Lines recorder fed with every published snapshot. + private var recorder: EngineStatsRecorder? + // 30-frame rolling average for smoothed CPU frame time private let kSmoothingWindow = 30 private var _frameMsBuffer: [Double] = .init(repeating: 0.0, count: 30) @@ -85,6 +98,40 @@ public final class EngineStatsMonitor: @unchecked Sendable { } } + /// Frame budget for the hitch counts (`EngineHitchStats`). Defaults to 60 Hz; the visionOS + /// runtime sets it to the 90 Hz period. Changing it does not rewrite past counts. + public var frameBudgetMs: Double { + get { + #if ENGINE_STATS_ENABLED + lock.lock() + defer { lock.unlock() } + return _frameBudgetMs + #else + return 1000.0 / 60.0 + #endif + } + set { + #if ENGINE_STATS_ENABLED + lock.lock() + _frameBudgetMs = max(0.1, newValue) + lock.unlock() + #endif + } + } + + /// Attaches a recorder that receives every published snapshot, replacing any earlier one. + func attachRecorder(_ newRecorder: EngineStatsRecorder?) -> EngineStatsRecorder? { + #if ENGINE_STATS_ENABLED + lock.lock() + let previous = recorder + recorder = newRecorder + lock.unlock() + return previous + #else + return newRecorder + #endif + } + public var loggingProfile: EngineStatsLoggingProfile { get { #if ENGINE_STATS_ENABLED @@ -258,8 +305,50 @@ public final class EngineStatsMonitor: @unchecked Sendable { ? sum / Double(_frameMsFilled) : frameMs + // Hitch accounting: cumulative histogram and over-budget count, plus a one-second window + _hitches.frameBudgetMs = _frameBudgetMs + _hitches.framesSampled += 1 + let overBudget = frameMs > _frameBudgetMs + if overBudget { + _hitches.framesOverBudget += 1 + } + _hitches.histogram[EngineHitchStats.bucketIndex(forFrameMs: frameMs)] += 1 + + let now = currentSnapshot.timestampSeconds + if !_windowStarted { + _windowStarted = true + _windowStartSeconds = now + } + if now - _windowStartSeconds >= 1.0 { + _hitches.framesLastSecond = _windowFrames + _hitches.framesOverBudgetLastSecond = _windowFramesOverBudget + _hitches.worstFrameMsLastSecond = _windowWorstFrameMs + _windowFrames = 0 + _windowFramesOverBudget = 0 + _windowWorstFrameMs = 0.0 + _windowStartSeconds = now + } + _windowFrames += 1 + if overBudget { + _windowFramesOverBudget += 1 + } + _windowWorstFrameMs = max(_windowWorstFrameMs, frameMs) + currentSnapshot.hitches = _hitches + + // Thermal transitions are rare and worth a mark on the Instruments timeline + let thermalState = currentSnapshot.memory.thermalState + let thermalChanged = _lastThermalState >= 0 && thermalState != _lastThermalState + _lastThermalState = thermalState + publishedSnapshot = currentSnapshot + let recordedSnapshot = currentSnapshot + let activeRecorder = recorder lock.unlock() + + if thermalChanged { + EngineProfiler.shared.emitEvent(.thermalStateChanged) + } + activeRecorder?.enqueue(recordedSnapshot) #endif } @@ -287,6 +376,13 @@ public final class EngineStatsMonitor: @unchecked Sendable { _missedDeadlineCount = 0 _deadlineSampleCount = 0 _missingAnchorCount = 0 + _hitches = EngineHitchStats(frameBudgetMs: _frameBudgetMs) + _windowStartSeconds = 0.0 + _windowStarted = false + _windowFrames = 0 + _windowFramesOverBudget = 0 + _windowWorstFrameMs = 0.0 + _lastThermalState = -1 _frameMsBuffer = .init(repeating: 0.0, count: kSmoothingWindow) _frameMsBufferIndex = 0 _frameMsFilled = 0 diff --git a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift new file mode 100644 index 000000000..3dd267875 --- /dev/null +++ b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift @@ -0,0 +1,208 @@ +// +// EngineStatsRecorder.swift +// UntoldEngine +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation + +public enum EngineStatsRecordingInterval: Sendable { + /// One line per published frame. Large files, exact hitch positions. + case perFrame + /// One line per second (the last snapshot of each second). The summary still covers every frame. + case perSecond +} + +public enum EngineStatsRecordingError: Error { + /// Engine stats are not compiled into this build (`ENGINE_STATS_ENABLED`), so there is nothing to record. + case statsNotCompiledIn + case cannotCreateFile(URL) +} + +/// Whole-run numbers written as the last line of a recording and returned by `stopEngineStatsRecording()`. +public struct EngineStatsRecordingSummary: Codable, Equatable, Sendable { + public var frames: Int = 0 + public var durationSeconds: Double = 0.0 + public var frameBudgetMs: Double = 0.0 + public var meanFrameMs: Double = 0.0 + public var p50FrameMs: Double = 0.0 + public var p95FrameMs: Double = 0.0 + public var p99FrameMs: Double = 0.0 + public var worstFrameMs: Double = 0.0 + public var framesOverBudget: Int = 0 + public var meanGPUExecutionMs: Double = 0.0 + /// Compositor deadline misses and samples over the run (visionOS), zero elsewhere. + public var missedDeadlines: Int = 0 + public var deadlineSamples: Int = 0 + /// Mean GPU time per pass label over the frames in which the pass appeared. + public var gpuPassMeanMs: [String: Double] = [:] + public var worstThermalState: Int = 0 + public var peakGPUAllocatedBytes: Int = 0 + + public init() {} +} + +/// Writes engine stats snapshots to a JSON Lines file off the render thread. +/// +/// Each line is `{"type":"frame","frame":{...EngineStatsSnapshot...}}`; the last line written by +/// `stop()` is `{"type":"summary","summary":{...EngineStatsRecordingSummary...}}`. Use +/// `startEngineStatsRecording(to:interval:)` and `stopEngineStatsRecording()` rather than this +/// type directly; the monitor feeds it every published snapshot. +public final class EngineStatsRecorder: @unchecked Sendable { + private struct Line: Codable { + var type: String + var frame: EngineStatsSnapshot? + var summary: EngineStatsRecordingSummary? + } + + public let url: URL + public let interval: EngineStatsRecordingInterval + + private let queue = DispatchQueue(label: "com.untoldengine.profiling.recorder", qos: .utility) + private let handle: FileHandle + private let encoder: JSONEncoder + private var isStopped = false + + // Accumulated on the queue + private var frameTimes: [Double] = [] + private var gpuExecutionSum = 0.0 + private var gpuExecutionSamples = 0 + private var gpuPassSums: [String: (sum: Double, samples: Int)] = [:] + private var firstTimestamp: Double? + private var lastTimestamp: Double = 0.0 + private var lastWrittenSecond: Double = -1.0 + private var lastSnapshot: EngineStatsSnapshot? + private var summary = EngineStatsRecordingSummary() + + init(url: URL, interval: EngineStatsRecordingInterval) throws { + self.url = url + self.interval = interval + let fileManager = FileManager.default + try? fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) + guard fileManager.createFile(atPath: url.path, contents: nil), let handle = try? FileHandle(forWritingTo: url) else { + throw EngineStatsRecordingError.cannotCreateFile(url) + } + self.handle = handle + encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + encoder.nonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "inf", negativeInfinity: "-inf", nan: "nan") + } + + /// Called by the monitor with each published snapshot; returns immediately. + func enqueue(_ snapshot: EngineStatsSnapshot) { + queue.async { [self] in + guard !isStopped else { return } + accumulate(snapshot) + switch interval { + case .perFrame: + write(Line(type: "frame", frame: snapshot, summary: nil)) + case .perSecond: + let second = snapshot.timestampSeconds.rounded(.down) + if second != lastWrittenSecond { + lastWrittenSecond = second + write(Line(type: "frame", frame: snapshot, summary: nil)) + } + } + } + } + + /// Writes the summary line, closes the file and returns the summary. Idempotent. + @discardableResult + public func stop() -> EngineStatsRecordingSummary { + queue.sync { [self] in + guard !isStopped else { return summary } + isStopped = true + summary = makeSummary() + write(Line(type: "summary", frame: nil, summary: summary)) + try? handle.close() + return summary + } + } + + // MARK: - Queue-only helpers + + private func accumulate(_ snapshot: EngineStatsSnapshot) { + frameTimes.append(snapshot.timing.frameTotalMs) + if snapshot.timing.gpuExecutionMs > 0 { + gpuExecutionSum += snapshot.timing.gpuExecutionMs + gpuExecutionSamples += 1 + } + for pass in snapshot.gpuPasses.passes { + let entry = gpuPassSums[pass.label] ?? (0.0, 0) + gpuPassSums[pass.label] = (entry.sum + pass.totalMs, entry.samples + 1) + } + if firstTimestamp == nil { + firstTimestamp = snapshot.timestampSeconds + } + lastTimestamp = snapshot.timestampSeconds + lastSnapshot = snapshot + } + + private func makeSummary() -> EngineStatsRecordingSummary { + var result = EngineStatsRecordingSummary() + result.frames = frameTimes.count + if let first = firstTimestamp { + result.durationSeconds = max(0.0, lastTimestamp - first) + } + if !frameTimes.isEmpty { + let sorted = frameTimes.sorted() + result.meanFrameMs = frameTimes.reduce(0.0, +) / Double(frameTimes.count) + result.p50FrameMs = sorted[Int(Double(sorted.count - 1) * 0.50)] + result.p95FrameMs = sorted[Int(Double(sorted.count - 1) * 0.95)] + result.p99FrameMs = sorted[Int(Double(sorted.count - 1) * 0.99)] + result.worstFrameMs = sorted[sorted.count - 1] + } + if let last = lastSnapshot { + result.frameBudgetMs = last.hitches.frameBudgetMs + result.framesOverBudget = frameTimes.filter { $0 > last.hitches.frameBudgetMs }.count + result.missedDeadlines = last.compositor.missedDeadlineCount + result.deadlineSamples = last.compositor.deadlineSampleCount + } + if gpuExecutionSamples > 0 { + result.meanGPUExecutionMs = gpuExecutionSum / Double(gpuExecutionSamples) + } + for (label, entry) in gpuPassSums where entry.samples > 0 { + result.gpuPassMeanMs[label] = entry.sum / Double(entry.samples) + } + result.worstThermalState = lastSnapshotMaxThermal + result.peakGPUAllocatedBytes = peakGPUAllocated + return result + } + + private var lastSnapshotMaxThermal = 0 + private var peakGPUAllocated = 0 + + private func write(_ line: Line) { + if let frame = line.frame { + lastSnapshotMaxThermal = max(lastSnapshotMaxThermal, frame.memory.thermalState) + peakGPUAllocated = max(peakGPUAllocated, frame.memory.gpuAllocatedBytes) + } + guard var data = try? encoder.encode(line) else { return } + data.append(0x0A) + handle.write(data) + } +} + +/// Starts writing every published engine stats snapshot to `url` as JSON Lines. Replaces an +/// active recording (which is stopped and summarised first). Throws when engine stats are not +/// compiled in or the file cannot be created. +public func startEngineStatsRecording(to url: URL, interval: EngineStatsRecordingInterval = .perSecond) throws { + #if ENGINE_STATS_ENABLED + let recorder = try EngineStatsRecorder(url: url, interval: interval) + EngineStatsMonitor.shared.attachRecorder(recorder)?.stop() + #else + _ = url + _ = interval + throw EngineStatsRecordingError.statsNotCompiledIn + #endif +} + +/// Stops the active recording, writes its summary line and returns the summary; nil when nothing was recording. +@discardableResult +public func stopEngineStatsRecording() -> EngineStatsRecordingSummary? { + EngineStatsMonitor.shared.attachRecorder(nil)?.stop() +} diff --git a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift index 071ba2a27..b38ce54d5 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsSnapshot.swift @@ -11,7 +11,7 @@ import Foundation import simd -public struct EngineTimingStats { +public struct EngineTimingStats: Codable, Sendable { public var frameTotalMs: Double = 0.0 /// CPU frame time averaged over the last 30 frames — use this for smooth FPS display. public var smoothedFrameMs: Double = 0.0 @@ -127,7 +127,7 @@ public struct EngineTimingStats { /// The per-frame values describe the last completed frame. The GPU-side values (deadline and /// presentation margins) come from the command buffer completion handler and therefore describe /// the most recently *completed* command buffer, which can lag the CPU frame by one or two frames. -public struct EngineCompositorStats { +public struct EngineCompositorStats: Codable, Sendable { /// CPU time between `startUpdate()` and `endUpdate()`. public var updateMs: Double = 0.0 /// Time left until `optimalInputTime` when the update phase ended. @@ -189,7 +189,7 @@ public struct EngineCompositorStats { } } -public struct EngineRenderStats { +public struct EngineRenderStats: Codable, Sendable { public var drawCallsTotal: Int = 0 public var drawCallsOpaque: Int = 0 public var drawCallsTransparent: Int = 0 @@ -244,7 +244,7 @@ public struct EngineRenderStats { } } -public struct EngineCullingStats { +public struct EngineCullingStats: Codable, Sendable { public var frustumTested: Int = 0 public var frustumPassed: Int = 0 public var frustumFailed: Int = 0 @@ -287,7 +287,7 @@ public struct EngineCullingStats { } } -public struct EngineStreamingStats { +public struct EngineStreamingStats: Codable, Sendable { public var activeLoads: Int = 0 public var loadCandidates: Int = 0 public var pendingLoadBacklog: Int = 0 @@ -400,7 +400,7 @@ public struct EngineStreamingStats { } } -public struct EngineBatchingStats { +public struct EngineBatchingStats: Codable, Sendable { public var batchGroupCount: Int = 0 public var batchedMeshCount: Int = 0 public var rebuildsThisSecond: Int = 0 @@ -441,7 +441,7 @@ public struct EngineBatchingStats { } } -public struct EngineMemoryStats { +public struct EngineMemoryStats: Codable, Sendable { public var meshMemoryBytes: Int = 0 public var textureMemoryBytes: Int = 0 public var geometryBudgetBytes: Int = 0 @@ -449,6 +449,23 @@ public struct EngineMemoryStats { public var utilizationPercent: Double = 0.0 public var isUnderPressure: Bool = false public var trackedEntityCount: Int = 0 + /// Bytes the Metal device currently has allocated for this process (`MTLDevice.currentAllocatedSize`). + public var gpuAllocatedBytes: Int = 0 + /// Memory the process may still allocate before the system terminates it (`os_proc_available_memory`). + /// Zero on platforms that do not report it (macOS). + public var availableMemoryBytes: Int = 0 + /// `ProcessInfo.ThermalState` raw value: 0 nominal, 1 fair, 2 serious, 3 critical. + public var thermalState: Int = 0 + + public var thermalStateName: String { + switch thermalState { + case 0: return "nominal" + case 1: return "fair" + case 2: return "serious" + case 3: return "critical" + default: return "unknown" + } + } public init( meshMemoryBytes: Int = 0, @@ -469,7 +486,7 @@ public struct EngineMemoryStats { } } -public struct EngineStatsSnapshot { +public struct EngineStatsSnapshot: Codable, Sendable { public var frameIndex: UInt64 = 0 public var timestampSeconds: Double = 0.0 public var timing: EngineTimingStats = .init() @@ -481,6 +498,8 @@ public struct EngineStatsSnapshot { public var compositor: EngineCompositorStats = .init() /// Per-pass GPU timings of the last resolved frame (see `GPUPassTiming`). Empty when the timer is off. public var gpuPasses: GPUPassTimingSnapshot = .init() + /// Frame-time distribution and over-budget counts since the monitor was reset. + public var hitches: EngineHitchStats = .init() public init( frameIndex: UInt64 = 0, @@ -492,7 +511,8 @@ public struct EngineStatsSnapshot { batching: EngineBatchingStats = .init(), memory: EngineMemoryStats = .init(), compositor: EngineCompositorStats = .init(), - gpuPasses: GPUPassTimingSnapshot = .init() + gpuPasses: GPUPassTimingSnapshot = .init(), + hitches: EngineHitchStats = .init() ) { self.frameIndex = frameIndex self.timestampSeconds = timestampSeconds @@ -504,5 +524,56 @@ public struct EngineStatsSnapshot { self.memory = memory self.compositor = compositor self.gpuPasses = gpuPasses + self.hitches = hitches + } +} + +/// Frame-time distribution since the monitor was reset, plus the last completed one-second window. +/// Mean and percentile numbers hide single long frames; these counts do not. +public struct EngineHitchStats: Codable, Sendable { + /// Upper bounds of the histogram buckets in milliseconds; the last bucket is open-ended. + /// 11.2 and 16.8 sit just above the 90 Hz and 60 Hz frame periods. + public static let bucketUpperBoundsMs: [Double] = [4.0, 8.0, 11.2, 16.8, 33.4, 100.0] + + /// Frame budget used for the over-budget counts. 16.67 ms at 60 Hz, 11.11 ms at 90 Hz. + public var frameBudgetMs: Double = 1000.0 / 60.0 + public var framesSampled: Int = 0 + public var framesOverBudget: Int = 0 + /// Frames, frames over budget and worst frame time in the last completed one-second window. + public var framesLastSecond: Int = 0 + public var framesOverBudgetLastSecond: Int = 0 + public var worstFrameMsLastSecond: Double = 0.0 + /// Cumulative frame count per bucket. Bucket `i` holds frames below `bucketUpperBoundsMs[i]`; + /// the final bucket holds everything at or above the last bound. + public var histogram: [Int] = Array(repeating: 0, count: EngineHitchStats.bucketUpperBoundsMs.count + 1) + + public var overBudgetRate: Double { + guard framesSampled > 0 else { return 0.0 } + return Double(framesOverBudget) / Double(framesSampled) + } + + public static func bucketIndex(forFrameMs frameMs: Double) -> Int { + for (index, bound) in bucketUpperBoundsMs.enumerated() where frameMs < bound { + return index + } + return bucketUpperBoundsMs.count + } + + public init( + frameBudgetMs: Double = 1000.0 / 60.0, + framesSampled: Int = 0, + framesOverBudget: Int = 0, + framesLastSecond: Int = 0, + framesOverBudgetLastSecond: Int = 0, + worstFrameMsLastSecond: Double = 0.0, + histogram: [Int] = Array(repeating: 0, count: EngineHitchStats.bucketUpperBoundsMs.count + 1) + ) { + self.frameBudgetMs = frameBudgetMs + self.framesSampled = framesSampled + self.framesOverBudget = framesOverBudget + self.framesLastSecond = framesLastSecond + self.framesOverBudgetLastSecond = framesOverBudgetLastSecond + self.worstFrameMsLastSecond = worstFrameMsLastSecond + self.histogram = histogram } } diff --git a/Sources/UntoldEngine/Profiling/GPUPassTiming.swift b/Sources/UntoldEngine/Profiling/GPUPassTiming.swift index d7b919eb7..af89d0f38 100644 --- a/Sources/UntoldEngine/Profiling/GPUPassTiming.swift +++ b/Sources/UntoldEngine/Profiling/GPUPassTiming.swift @@ -24,7 +24,7 @@ public final class GPUPassTiming: @unchecked Sendable { /// One pass's time per frame, in milliseconds: a render pass's vertex and fragment stages; /// a compute or blit pass has only `fragmentMs`, the whole pass. - public struct PassTime: Sendable, Equatable { + public struct PassTime: Codable, Sendable, Equatable { public let label: String public let vertexMs: Double public let fragmentMs: Double diff --git a/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift b/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift index 11622d3f1..c459a4871 100644 --- a/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift +++ b/Sources/UntoldEngine/Profiling/GPUPassTimingSnapshot.swift @@ -12,7 +12,7 @@ import Foundation /// The per-pass GPU times of the last frame `GPUPassTiming` resolved, as the engine stats /// carry them: a copy taken when the frame's stats are published. -public struct GPUPassTimingSnapshot: Equatable, Sendable { +public struct GPUPassTimingSnapshot: Codable, Equatable, Sendable { /// A number that changes with each resolved frame; zero before the first. public var frameIndex: Int = 0 /// The frame's passes, heaviest first. Empty when the timer is off. diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index d4507ea8c..2ffbdb39f 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -343,6 +343,13 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { let memStats = MemoryBudgetManager.shared.getStats() let frame = currentFrameTime let gpuPasses = GPUPassTimingSnapshot.current() + let gpuAllocatedBytes = renderInfo.device?.currentAllocatedSize ?? 0 + let thermalState = ProcessInfo.processInfo.thermalState.rawValue + #if os(iOS) || os(visionOS) || os(tvOS) + let availableMemoryBytes = Int(os_proc_available_memory()) + #else + let availableMemoryBytes = 0 + #endif EngineStatsMonitor.shared.update { snapshot in snapshot.timing.frameTotalMs = frameTotalMs @@ -440,6 +447,9 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { snapshot.memory.utilizationPercent = Double(memStats.utilizationPercent) snapshot.memory.isUnderPressure = memStats.isUnderPressure snapshot.memory.trackedEntityCount = memStats.trackedEntityCount + snapshot.memory.gpuAllocatedBytes = gpuAllocatedBytes + snapshot.memory.availableMemoryBytes = availableMemoryBytes + snapshot.memory.thermalState = thermalState } EngineStatsMonitor.shared.completeFrame() } diff --git a/Sources/UntoldEngineAR/UntoldEngineAR.swift b/Sources/UntoldEngineAR/UntoldEngineAR.swift index d73f97873..12ce33520 100644 --- a/Sources/UntoldEngineAR/UntoldEngineAR.swift +++ b/Sources/UntoldEngineAR/UntoldEngineAR.swift @@ -117,6 +117,7 @@ commandBufferSemaphore.signal() return } + GPUPassTiming.shared.beginFrame(device: renderInfo.device) commandBuffer.label = "AR Command Buffer" @@ -166,6 +167,7 @@ EngineProfiler.shared.attach(to: commandBuffer, label: "ARFrame") // Finalize rendering here & push the command buffer to the GPU + GPUPassTiming.shared.endFrame(commandBuffer) commandBuffer.commit() } diff --git a/Sources/UntoldEngineXR/UntoldEngineXR.swift b/Sources/UntoldEngineXR/UntoldEngineXR.swift index eeeb77f2b..1541656a7 100644 --- a/Sources/UntoldEngineXR/UntoldEngineXR.swift +++ b/Sources/UntoldEngineXR/UntoldEngineXR.swift @@ -315,6 +315,8 @@ } renderer = untoldrenderer + // Hitch counts measure against the compositor's nominal 90 Hz period. + EngineStatsMonitor.shared.frameBudgetMs = 1000.0 / 90.0 } @MainActor diff --git a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift index af7123155..81378407d 100644 --- a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift +++ b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift @@ -9,6 +9,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import QuartzCore +import simd @testable import UntoldEngine import XCTest @@ -357,4 +358,173 @@ final class EngineStatsMonitorTests: XCTestCase { XCTAssertTrue(overlay.contains("missed 3/300 (1.00%)"), overlay) XCTAssertTrue(overlay.contains("semWait 0.10ms"), overlay) } + + // MARK: - Hitch accounting + + func testHitches_countOverBudgetFramesAndFillHistogram() { + EngineStatsMonitor.shared.frameBudgetMs = 10.0 + defer { EngineStatsMonitor.shared.frameBudgetMs = 1000.0 / 60.0 } + + for (index, frameMs) in [5.0, 12.0, 50.0].enumerated() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: Double(index) * 0.01) + EngineStatsMonitor.shared.update { $0.timing.frameTotalMs = frameMs } + EngineStatsMonitor.shared.completeFrame() + } + + let hitches = getEngineStatsSnapshot().hitches + XCTAssertEqual(hitches.frameBudgetMs, 10.0) + XCTAssertEqual(hitches.framesSampled, 3) + XCTAssertEqual(hitches.framesOverBudget, 2) + XCTAssertEqual(hitches.overBudgetRate, 2.0 / 3.0, accuracy: 1e-9) + // Buckets: <4, <8, <11.2, <16.8, <33.4, <100, >=100 + XCTAssertEqual(hitches.histogram, [0, 1, 0, 1, 0, 1, 0]) + } + + func testHitches_bucketIndex_boundaries() { + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 0.0), 0) + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 3.99), 0) + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 4.0), 1) + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 11.1), 2) + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 16.7), 3) + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 100.0), 6) + XCTAssertEqual(EngineHitchStats.bucketIndex(forFrameMs: 5000.0), 6) + } + + func testHitches_oneSecondWindowRollsOver() { + EngineStatsMonitor.shared.frameBudgetMs = 10.0 + defer { EngineStatsMonitor.shared.frameBudgetMs = 1000.0 / 60.0 } + + let frames: [(seconds: Double, ms: Double)] = [(0.0, 20.0), (0.5, 5.0), (1.2, 5.0)] + for frame in frames { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: frame.seconds) + EngineStatsMonitor.shared.update { $0.timing.frameTotalMs = frame.ms } + EngineStatsMonitor.shared.completeFrame() + } + + let hitches = getEngineStatsSnapshot().hitches + XCTAssertEqual(hitches.framesLastSecond, 2) + XCTAssertEqual(hitches.framesOverBudgetLastSecond, 1) + XCTAssertEqual(hitches.worstFrameMsLastSecond, 20.0) + XCTAssertEqual(hitches.framesSampled, 3) + } + + func testReset_clearsHitchesButKeepsBudget() { + EngineStatsMonitor.shared.frameBudgetMs = 11.0 + defer { EngineStatsMonitor.shared.frameBudgetMs = 1000.0 / 60.0 } + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.update { $0.timing.frameTotalMs = 30.0 } + EngineStatsMonitor.shared.completeFrame() + + EngineStatsMonitor.shared.reset() + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 2.0) + EngineStatsMonitor.shared.completeFrame() + + let hitches = getEngineStatsSnapshot().hitches + XCTAssertEqual(hitches.frameBudgetMs, 11.0) + XCTAssertEqual(hitches.framesSampled, 1) + XCTAssertEqual(hitches.framesOverBudget, 0) + } + + func testFormatEngineStatsOverlay_containsHitchesAndThermal() { + var snapshot = EngineStatsSnapshot() + snapshot.hitches.framesSampled = 100 + snapshot.hitches.framesOverBudget = 4 + snapshot.hitches.frameBudgetMs = 11.11 + snapshot.memory.thermalState = 2 + snapshot.memory.gpuAllocatedBytes = 300 * 1024 * 1024 + + let overlay = formatEngineStatsOverlay(snapshot) + XCTAssertTrue(overlay.contains("Hitches: budget 11.11ms | over 4/100 (4.00%)"), overlay) + XCTAssertTrue(overlay.contains("<11.2 "), overlay) + XCTAssertTrue(overlay.contains("thermal serious"), overlay) + XCTAssertTrue(overlay.contains("gpuAlloc 300mb"), overlay) + } + + // MARK: - Codable and recording + + func testSnapshot_codableRoundTrip() throws { + var snapshot = EngineStatsSnapshot() + snapshot.frameIndex = 99 + snapshot.timing.animationMs = 1.25 + snapshot.culling.selectedHZBMipSize = simd_int2(256, 128) + snapshot.compositor.deadlineMarginMs = -0.5 + snapshot.gpuPasses = GPUPassTimingSnapshot(frameIndex: 3, passes: [GPUPassTiming.PassTime(label: "Light Pass", vertexMs: 0.5, fragmentMs: 1.5)]) + snapshot.hitches.histogram[2] = 7 + + let data = try JSONEncoder().encode(snapshot) + let decoded = try JSONDecoder().decode(EngineStatsSnapshot.self, from: data) + + XCTAssertEqual(decoded.frameIndex, 99) + XCTAssertEqual(decoded.timing.animationMs, 1.25) + XCTAssertEqual(decoded.culling.selectedHZBMipSize, simd_int2(256, 128)) + XCTAssertEqual(decoded.compositor.deadlineMarginMs, -0.5) + XCTAssertEqual(decoded.gpuPasses, snapshot.gpuPasses) + XCTAssertEqual(decoded.hitches.histogram[2], 7) + } + + func testRecorder_perFrame_writesFramesAndSummary() throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("untold-stats-\(UUID().uuidString).jsonl") + defer { try? FileManager.default.removeItem(at: url) } + + EngineStatsMonitor.shared.frameBudgetMs = 15.0 + defer { EngineStatsMonitor.shared.frameBudgetMs = 1000.0 / 60.0 } + try startEngineStatsRecording(to: url, interval: .perFrame) + + for (index, frameMs) in [10.0, 20.0, 30.0].enumerated() { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 100.0 + Double(index) * 0.5) + EngineStatsMonitor.shared.update { snapshot in + snapshot.timing.frameTotalMs = frameMs + snapshot.gpuPasses = GPUPassTimingSnapshot(passes: [GPUPassTiming.PassTime(label: "Model Pass", vertexMs: 0, fragmentMs: frameMs / 4)]) + } + EngineStatsMonitor.shared.recordGPUCompletion(executionMs: frameMs / 2) + EngineStatsMonitor.shared.completeFrame() + } + + let summary = try XCTUnwrap(stopEngineStatsRecording()) + XCTAssertEqual(summary.frames, 3) + XCTAssertEqual(summary.meanFrameMs, 20.0, accuracy: 1e-9) + XCTAssertEqual(summary.worstFrameMs, 30.0) + XCTAssertEqual(summary.p50FrameMs, 20.0) + XCTAssertEqual(summary.framesOverBudget, 2) + XCTAssertEqual(summary.frameBudgetMs, 15.0) + XCTAssertEqual(summary.durationSeconds, 1.0, accuracy: 1e-9) + XCTAssertEqual(summary.meanGPUExecutionMs, 10.0, accuracy: 1e-9) + XCTAssertEqual(summary.gpuPassMeanMs["Model Pass"] ?? 0, 5.0, accuracy: 1e-9) + XCTAssertNil(stopEngineStatsRecording(), "Stopping twice must not report a second recording") + + let lines = try String(contentsOf: url, encoding: .utf8) + .split(separator: "\n", omittingEmptySubsequences: true) + XCTAssertEqual(lines.count, 4, "3 frame lines + 1 summary line") + + let decoder = JSONDecoder() + struct Line: Decodable { + var type: String + var frame: EngineStatsSnapshot? + var summary: EngineStatsRecordingSummary? + } + let decoded = try lines.map { try decoder.decode(Line.self, from: Data($0.utf8)) } + XCTAssertEqual(decoded.map(\.type), ["frame", "frame", "frame", "summary"]) + XCTAssertEqual(decoded[1].frame?.timing.frameTotalMs, 20.0) + XCTAssertEqual(decoded[3].summary, summary) + } + + func testRecorder_perSecond_writesOneLinePerSecond() throws { + let url = FileManager.default.temporaryDirectory + .appendingPathComponent("untold-stats-\(UUID().uuidString).jsonl") + defer { try? FileManager.default.removeItem(at: url) } + + try startEngineStatsRecording(to: url, interval: .perSecond) + for seconds in [10.0, 10.3, 10.9, 11.1, 11.5] { + EngineStatsMonitor.shared.beginFrame(timestampSeconds: seconds) + EngineStatsMonitor.shared.update { $0.timing.frameTotalMs = 8.0 } + EngineStatsMonitor.shared.completeFrame() + } + let summary = try XCTUnwrap(stopEngineStatsRecording()) + XCTAssertEqual(summary.frames, 5, "The summary covers every frame, not only the written ones") + + let lines = try String(contentsOf: url, encoding: .utf8) + .split(separator: "\n", omittingEmptySubsequences: true) + XCTAssertEqual(lines.count, 3, "one line for second 10, one for second 11, one summary") + } } diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md index 03c4e134d..476449273 100644 --- a/docs/API/UsingProfiler.md +++ b/docs/API/UsingProfiler.md @@ -47,7 +47,8 @@ Streaming: tick=true workMs 1.23 | evictions 0 | avgLoadMs 45.67 | applyMs 0.89 TileReps: resident full/lod/hlod 24/8/2 | visible full/lod/hlod 18/5/1 | overlap visible full+lod/full+hlod/lod+hlod 0/0/0 residentFull+fallback 0 | fades 0 waiting 0 TileRenderCost: visible full/lod/hlod 18/5/1 | draws full/lod/hlod 22/6/1 | tris full/lod/hlod 84000/9000/1200 Batching: groups 132 | batchedMeshes 916 | dirty 0→0 | defWork 0 skipComplex 2 | dispatched 0→0 groups | rebuilds/s 0 | rebuildMs 0.00 -Memory: mesh 312/512mb | tex 198/512mb | total 50% | entities 847 +Memory: mesh 312/512mb | tex 198/512mb | total 50% | entities 847 | gpuAlloc 910mb | avail 2400mb | thermal nominal +Hitches: budget 11.11ms | over 3/5400 (0.06%) | lastSec 0/90 over, worst 9.80ms | hist <4 0 | <8 1200 | <11.2 4190 | <16.8 8 | <33.4 2 | <100 0 | >=100 0 Compositor: views 2 @ 2048x1984 | update 2.10ms | inputSlack 1.45ms | submit 3.20ms | semWait 0.00ms | deadlineMargin 2.35ms | presentMargin 6.80ms | missed 3/5400 (0.06%) | noAnchor 0 ``` @@ -142,6 +143,20 @@ Programmatic access: `getEngineStatsSnapshot().compositor` (`EngineCompositorSta | `tex X/Ymb` | Texture memory used vs texture budget. | | `total X%` | Combined utilization across both pools. | | `PRESSURE` | Appears when either pool hits ≥ 85 % utilization. | +| `gpuAlloc` | Bytes the Metal device has allocated for the process (`MTLDevice.currentAllocatedSize`): drawables, render targets, streamed meshes and textures together. | +| `avail` | Memory the process may still allocate before the system terminates it (`os_proc_available_memory`). iOS and visionOS only. | +| `thermal` | `ProcessInfo.thermalState`: nominal, fair, serious or critical. A `ThermalStateChanged` signpost event marks every transition. A benchmark that ends in `serious` is a failed run whatever the frame times say. | + +**Hitches line** — the frame-time distribution since the monitor was reset. Means and percentiles hide single long frames; these counts do not. + +| Field | What it tells you | +|---|---| +| `budget` | Frame budget for the over-budget counts. 16.67 ms by default; the visionOS runtime sets 11.11 ms (90 Hz). Change it with `EngineStatsMonitor.shared.frameBudgetMs`. | +| `over a/b (rate)` | Frames over budget out of all frames sampled. | +| `lastSec x/y over, worst` | The last completed one-second window: frames over budget, frames, and the worst frame time. Use this for a live overlay. | +| `hist` | Cumulative frame count per bucket. The bucket bounds (4, 8, 11.2, 16.8, 33.4, 100 ms) straddle the 90 Hz and 60 Hz periods, so the `<11.2`/`<16.8` split shows how many frames just missed 90 Hz. | + +Programmatic access: `getEngineStatsSnapshot().hitches` (`EngineHitchStats`, with `overBudgetRate`). --- @@ -158,6 +173,23 @@ print("Update: \(frameStats.timing.updateMs) ms") print("Render: \(frameStats.timing.renderTotalMs) ms") ``` +## Recording A Run To Disk + +Every field of `EngineStatsSnapshot` is `Codable`. A recorder writes the published snapshots to a JSON Lines file on a utility queue, so a benchmark can dump a whole run and a script can compare it against a baseline: + +```swift +let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + .appendingPathComponent("perf/starter-scene.jsonl") +try startEngineStatsRecording(to: url, interval: .perSecond) // or .perFrame +// ... run the scene ... +let summary = stopEngineStatsRecording() +print(summary?.p99FrameMs ?? 0, summary?.missedDeadlines ?? 0) +``` + +Each line is `{"type":"frame","frame":{...}}` with the full snapshot; `.perFrame` writes every frame, `.perSecond` the last frame of each second. The final line is `{"type":"summary","summary":{...}}` (`EngineStatsRecordingSummary`): frame count and duration, mean, p50, p95, p99 and worst frame time, frames over budget, mean GPU execution time, compositor deadline misses, the mean GPU time per pass label, the worst thermal state seen and the peak GPU allocation. The summary always covers every frame, whatever the interval. On a device, pull the file with `xcrun devicectl device copy from`. + +`startEngineStatsRecording` throws `EngineStatsRecordingError.statsNotCompiledIn` in builds without `ENGINE_STATS_ENABLED`. + ## GPU Pass Timing `GPUPassTiming` times every render, compute and blit pass of the frame on the GPU itself, from `MTLCounterSampleBuffer` timestamps at the stage boundaries: a render pass's vertex and fragment stages, a compute or blit pass as a whole. It is off by default, and a GPU without stage-boundary sampling measures nothing. From cd2972e0591699b4d552f8084d15915b5c0d9db2 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 20:53:04 +0200 Subject: [PATCH 05/17] [Performance] Make engine stats collection a runtime switch in every build configuration ENGINE_STATS_ENABLED was defined for debug builds only, so a release build (the one that ships and the one to measure on a device) had no stats at all unless the package was rebuilt with -DENGINE_STATS_ENABLED. - Define ENGINE_STATS_ENABLED for the engine and XR targets in every configuration. - EngineStatsMonitor gains isCollecting (an OSAllocatedUnfairLock read on every frame-loop call without the monitor lock): on by default in debug builds, off in release, overridable with UNTOLD_STATS=1. All per-frame entry points return at once when it is off, publishEngineStats skips gathering the snapshot, and RenderStatsCollector samples the flag at frame start so the per-draw path reads one Bool. - setEngineStatsCollection(enabled:) / isEngineStatsCollectionEnabled(); setEngine(.metrics(.enabled/.disabled)) toggles collection together with the profiler. - Tests for the switch; UsingProfiler.md build notes updated. --- Package.swift | 8 ++-- .../Profiling/EngineStatsMonitor.swift | 42 ++++++++++++++++ .../Profiling/RenderStatsCollector.swift | 5 ++ .../UntoldEngine/Renderer/UntoldEngine.swift | 3 ++ .../Utils/EngineSettingsAPI.swift | 2 + .../EngineStatsMonitorTests.swift | 48 +++++++++++++++++++ docs/API/UsingProfiler.md | 6 +-- 7 files changed, 108 insertions(+), 6 deletions(-) diff --git a/Package.swift b/Package.swift index a8d0e7be1..ecff90d5a 100644 --- a/Package.swift +++ b/Package.swift @@ -119,8 +119,10 @@ let package = Package( resources: engineResources, swiftSettings: [ // Compile engine stats collection only in debug by default. - // Release builds can still opt in explicitly with -DENGINE_STATS_ENABLED. - .define("ENGINE_STATS_ENABLED", .when(configuration: .debug)), + // Compiled into every configuration; collection is a runtime switch + // (setEngineStatsCollection(enabled:), UNTOLD_STATS=1) that defaults to on in + // debug builds and off in release builds. + .define("ENGINE_STATS_ENABLED"), .swiftLanguageMode(.v6), ], linkerSettings: [ @@ -163,7 +165,7 @@ let package = Package( // Must mirror the engine flag so #if ENGINE_STATS_ENABLED blocks in this // target are active; without it, xrFrameStartTime is never declared and // finalizeXRStatsAndMonitors receives 0.0, producing garbage frameTotalMs. - .define("ENGINE_STATS_ENABLED", .when(configuration: .debug)), + .define("ENGINE_STATS_ENABLED"), .swiftLanguageMode(.v6), ], linkerSettings: [ diff --git a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift index 75812bcf3..e1155e8f2 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift @@ -9,6 +9,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import Foundation +import os import QuartzCore /// The time the GPU spent on a frame's command buffer beyond the previous frame's: the part @@ -30,6 +31,10 @@ public enum EngineStatsLoggingProfile: Equatable { public final class EngineStatsMonitor: @unchecked Sendable { public static let shared = EngineStatsMonitor() + /// Runtime switch for stats collection. Read on every frame-loop call site without taking the + /// monitor lock; defaults to on in debug builds and off in release builds, or to `UNTOLD_STATS=1`. + private let collecting: OSAllocatedUnfairLock + #if ENGINE_STATS_ENABLED private let lock = NSLock() private var currentSnapshot: EngineStatsSnapshot = .init() @@ -74,11 +79,30 @@ public final class EngineStatsMonitor: @unchecked Sendable { #endif private init() { + let environmentValue = ProcessInfo.processInfo.environment["UNTOLD_STATS"] + let initialState: Bool + if let environmentValue { + initialState = environmentValue == "1" + } else { + #if DEBUG + initialState = true + #else + initialState = false + #endif + } + collecting = OSAllocatedUnfairLock(initialState: initialState) #if ENGINE_STATS_ENABLED lastLogTime = CACurrentMediaTime() #endif } + /// Whether the monitor collects and publishes stats. When false every per-frame call returns + /// at once and `snapshot()` keeps returning the last published frame. + public var isCollecting: Bool { + get { collecting.withLock { $0 } } + set { collecting.withLock { $0 = newValue } } + } + public var enableLogging: Bool { get { #if ENGINE_STATS_ENABLED @@ -197,6 +221,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { public func beginFrame(timestampSeconds: Double = CACurrentMediaTime()) { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } lock.lock() currentSnapshot.frameIndex &+= 1 currentSnapshot.timestampSeconds = timestampSeconds @@ -208,6 +233,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { public func update(_ updater: (inout EngineStatsSnapshot) -> Void) { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } lock.lock() updater(¤tSnapshot) lock.unlock() @@ -218,6 +244,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { /// Safe to call from any thread. public func recordGPUCompletion(executionMs: Double) { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } let now = CACurrentMediaTime() lock.lock() let cadenceMs = _lastGPUCompletionTime > 0 @@ -252,6 +279,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { /// Safe to call from any thread. public func recordCompositorCompletion(deadlineMarginMs: Double, presentationMarginMs: Double) { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } lock.lock() _latestDeadlineMarginMs = deadlineMarginMs _latestPresentationMarginMs = presentationMarginMs @@ -270,6 +298,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { /// Called once per frame that is presented without a fresh device anchor (visionOS). public func recordMissingAnchor() { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } lock.lock() _missingAnchorCount += 1 lock.unlock() @@ -279,6 +308,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { /// Publishes the current frame so API readers can safely consume it next frame. public func completeFrame() { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } lock.lock() // Pull in latest GPU timing from async handler currentSnapshot.timing.gpuExecutionMs = _latestGPUExecutionMs @@ -392,6 +422,7 @@ public final class EngineStatsMonitor: @unchecked Sendable { public func tick() { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } let now = CACurrentMediaTime() var shouldLog = false var snapshotToLog: EngineStatsSnapshot = .init() @@ -438,6 +469,17 @@ public func setEngineStatsLogging(enabled: Bool) { EngineStatsMonitor.shared.enableLogging = enabled } +/// Turns stats collection on or off at runtime. Collection is on by default in debug builds and +/// off in release builds (`UNTOLD_STATS=1` in the environment turns it on anywhere). Turning it on +/// in a release build is how device measurements are taken. +public func setEngineStatsCollection(enabled: Bool) { + EngineStatsMonitor.shared.isCollecting = enabled +} + +public func isEngineStatsCollectionEnabled() -> Bool { + EngineStatsMonitor.shared.isCollecting +} + public func setEngineStatsLogging( enabled: Bool, profile: EngineStatsLoggingProfile, diff --git a/Sources/UntoldEngine/Profiling/RenderStatsCollector.swift b/Sources/UntoldEngine/Profiling/RenderStatsCollector.swift index a46d3d6eb..8a863a17b 100644 --- a/Sources/UntoldEngine/Profiling/RenderStatsCollector.swift +++ b/Sources/UntoldEngine/Profiling/RenderStatsCollector.swift @@ -33,6 +33,9 @@ public final class RenderStatsCollector: @unchecked Sendable { #if ENGINE_STATS_ENABLED private let lock = NSLock() private var stats = RenderDrawStats() + /// Sampled from the monitor at `reset()` (frame start, render thread) so the per-draw path + /// reads one Bool instead of taking a lock. Draw recording happens on the same thread. + private var isCollecting = false #endif private init() {} @@ -41,6 +44,7 @@ public final class RenderStatsCollector: @unchecked Sendable { #if ENGINE_STATS_ENABLED lock.lock() stats = .init() + isCollecting = EngineStatsMonitor.shared.isCollecting lock.unlock() #endif } @@ -96,6 +100,7 @@ public final class RenderStatsCollector: @unchecked Sendable { batched: Bool ) { #if ENGINE_STATS_ENABLED + guard isCollecting else { return } let safeElementCount = max(0, elementCount) let safeInstanceCount = max(1, instanceCount) let triangles = triangleCount(for: primitiveType, elementCount: safeElementCount) * safeInstanceCount diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index 2ffbdb39f..5526e76b3 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -322,6 +322,9 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { } private func publishEngineStats(frameStartTime: Double) { + // Gathering the per-frame snapshot walks visible entities and batch groups; skip all of + // it when collection is off (the default in release builds). + guard EngineStatsMonitor.shared.isCollecting else { return } let frameTotalMs: Double if let dt = timeSinceLastUpdate as Float?, dt > 0 { frameTotalMs = Double(dt) * 1000.0 diff --git a/Sources/UntoldEngine/Utils/EngineSettingsAPI.swift b/Sources/UntoldEngine/Utils/EngineSettingsAPI.swift index 14be05ca4..aedf7e98b 100644 --- a/Sources/UntoldEngine/Utils/EngineSettingsAPI.swift +++ b/Sources/UntoldEngine/Utils/EngineSettingsAPI.swift @@ -481,8 +481,10 @@ public func setEngine(_ property: EngineProperty) { assetBasePath = url case .metrics(.enabled): enableEngineMetrics = true + setEngineStatsCollection(enabled: true) case .metrics(.disabled): enableEngineMetrics = false + setEngineStatsCollection(enabled: false) } } diff --git a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift index 81378407d..b3c3fd34c 100644 --- a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift +++ b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift @@ -527,4 +527,52 @@ final class EngineStatsMonitorTests: XCTestCase { .split(separator: "\n", omittingEmptySubsequences: true) XCTAssertEqual(lines.count, 3, "one line for second 10, one for second 11, one summary") } + + // MARK: - Runtime collection switch + + func testCollectionOff_ignoresFrameCallsAndKeepsLastPublishedFrame() { + let wasCollecting = isEngineStatsCollectionEnabled() + defer { setEngineStatsCollection(enabled: wasCollecting) } + + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 1.0) + EngineStatsMonitor.shared.update { $0.timing.updateMs = 2.5 } + EngineStatsMonitor.shared.completeFrame() + XCTAssertEqual(getEngineStatsSnapshot().frameIndex, 1) + + setEngineStatsCollection(enabled: false) + XCTAssertFalse(isEngineStatsCollectionEnabled()) + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 2.0) + EngineStatsMonitor.shared.update { $0.timing.updateMs = 9.0 } + EngineStatsMonitor.shared.recordGPUCompletion(executionMs: 4.0) + EngineStatsMonitor.shared.completeFrame() + + let published = getEngineStatsSnapshot() + XCTAssertEqual(published.frameIndex, 1, "Frames must not advance while collection is off") + XCTAssertEqual(published.timing.updateMs, 2.5) + XCTAssertEqual(published.timing.gpuExecutionMs, 0.0) + + setEngineStatsCollection(enabled: true) + EngineStatsMonitor.shared.beginFrame(timestampSeconds: 3.0) + EngineStatsMonitor.shared.update { $0.timing.updateMs = 1.0 } + EngineStatsMonitor.shared.completeFrame() + XCTAssertEqual(getEngineStatsSnapshot().frameIndex, 2) + XCTAssertEqual(getEngineStatsSnapshot().timing.updateMs, 1.0) + } + + func testSetEngineMetrics_alsoTogglesCollection() { + let wasCollecting = isEngineStatsCollectionEnabled() + let wasMetrics = enableEngineMetrics + defer { + setEngineStatsCollection(enabled: wasCollecting) + enableEngineMetrics = wasMetrics + } + + setEngine(.metrics(.disabled)) + XCTAssertFalse(isEngineStatsCollectionEnabled()) + XCTAssertFalse(enableEngineMetrics) + + setEngine(.metrics(.enabled)) + XCTAssertTrue(isEngineStatsCollectionEnabled()) + XCTAssertTrue(enableEngineMetrics) + } } diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md index 476449273..973358cd8 100644 --- a/docs/API/UsingProfiler.md +++ b/docs/API/UsingProfiler.md @@ -485,10 +485,10 @@ To inspect timeline data: ## Build Configuration Notes - `EngineProfiler` is available in all configs, but disabled by default until enabled at runtime. -- `EngineStats` collection is compiled in debug by default (`ENGINE_STATS_ENABLED`). -- For release profiling with `EngineStats`, build with `-DENGINE_STATS_ENABLED`. +- `EngineStats` collection is compiled into every configuration (`ENGINE_STATS_ENABLED`) and switched at runtime: on by default in debug builds, off by default in release builds. Turn it on with `setEngine(.metrics(.enabled))` (which also enables the profiler), `setEngineStatsCollection(enabled: true)`, or `UNTOLD_STATS=1` in the environment. This is how device measurements are taken from a release build. +- When collection is off, every per-frame call returns immediately, draw counting is skipped, and the per-frame snapshot is not gathered; `getEngineStatsSnapshot()` keeps returning the last published frame. -If `ENGINE_STATS_ENABLED` is not compiled in, `getEngineStatsSnapshot()` returns default values and `setEngineStatsLogging(...)` is effectively a no-op. +If `ENGINE_STATS_ENABLED` is removed from the package settings, `getEngineStatsSnapshot()` returns default values and `setEngineStatsLogging(...)` is effectively a no-op. ## Category Toggle Notes From 9f70c14488407a75c9498d33314e98bde7413a69 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 21:17:20 +0200 Subject: [PATCH 06/17] [Bugfix] The recorder's run duration counts only the frames that carry a timestamp --- Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift index 3dd267875..c00a6be83 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift @@ -135,10 +135,12 @@ public final class EngineStatsRecorder: @unchecked Sendable { let entry = gpuPassSums[pass.label] ?? (0.0, 0) gpuPassSums[pass.label] = (entry.sum + pass.totalMs, entry.samples + 1) } - if firstTimestamp == nil { - firstTimestamp = snapshot.timestampSeconds + if snapshot.timestampSeconds > 0 { + if firstTimestamp == nil { + firstTimestamp = snapshot.timestampSeconds + } + lastTimestamp = snapshot.timestampSeconds } - lastTimestamp = snapshot.timestampSeconds lastSnapshot = snapshot } From 645c557f686198680a05a9a7e760c995a21202bb Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 21:20:24 +0200 Subject: [PATCH 07/17] [Feature] Add the PerfBench benchmark app and the run/compare scripts A repeatable way to measure a build: the same scenes, drawable size and camera motion, the engine stats and the GPU pass timer on, in release, on macOS, iOS or Apple Vision Pro, with the results pulled into one folder and compared against a stored baseline. Examples/PerfBench (XcodeGen project, engine referenced by path): - Seven scenes: 1k individual primitives, 10k batched primitives, 64 point lights, the post-processing chain, 16 skinned characters, the stadium and grass assets, a Gaussian splat. Assets come from the render-test resources and are copied into the bundle. - BenchRunner builds, warms up, records (JSON Lines per frame through the engine recorder), tears down and moves on, then writes summary.json with the device, the render configuration (platform, viewport, view count, layout, foveation, immersion, refresh rate) and every scene's summary and last snapshot; configuration comes from UNTOLD_BENCH_* environment variables so devicectl can drive it, and the app exits when done. - macOS: fixed 1920x1080 drawable, orbiting camera. iOS: SwiftUI host. visionOS: CompositorLayer with the engine's dedicated layout, scenes placed in front of the user, full or mixed immersion. - The hitch budget is one and a half refresh intervals (a skipped refresh). scripts/perf/run_bench.sh builds the target (arm64 only), runs it locally or installs and launches it on a device with the environment, optionally records an Instruments trace, copies the run folder back and calls scripts/perf/compare_baseline.py, which compares frame percentiles, GPU time, over-budget and missed-deadline rates and per-pass GPU means against perf/baselines///.json and refuses to compare different render configurations. --- .gitignore | 4 + Examples/PerfBench/.gitignore | 4 + Examples/PerfBench/README.md | 96 ++++++ .../Sources/PerfBenchCore/BenchConfig.swift | 76 +++++ .../Sources/PerfBenchCore/BenchResults.swift | 134 ++++++++ .../Sources/PerfBenchCore/BenchRunner.swift | 201 ++++++++++++ .../Sources/PerfBenchCore/BenchScenes.swift | 306 ++++++++++++++++++ Examples/PerfBench/Sources/iOS/Info.plist | 42 +++ .../PerfBench/Sources/iOS/PerfBenchApp.swift | 104 ++++++ .../PerfBench/Sources/macOS/AppDelegate.swift | 120 +++++++ Examples/PerfBench/Sources/macOS/Info.plist | 26 ++ Examples/PerfBench/Sources/macOS/main.swift | 22 ++ .../PerfBench/Sources/visionOS/Info.plist | 35 ++ .../Sources/visionOS/PerfBenchApp.swift | 170 ++++++++++ Examples/PerfBench/project.yml | 116 +++++++ docs/API/UsingProfiler.md | 4 + perf/baselines/README.md | 10 + perf/results/.gitkeep | 0 scripts/perf/compare_baseline.py | 130 ++++++++ scripts/perf/run_bench.sh | 137 ++++++++ 20 files changed, 1737 insertions(+) create mode 100644 Examples/PerfBench/.gitignore create mode 100644 Examples/PerfBench/README.md create mode 100644 Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift create mode 100644 Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift create mode 100644 Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift create mode 100644 Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift create mode 100644 Examples/PerfBench/Sources/iOS/Info.plist create mode 100644 Examples/PerfBench/Sources/iOS/PerfBenchApp.swift create mode 100644 Examples/PerfBench/Sources/macOS/AppDelegate.swift create mode 100644 Examples/PerfBench/Sources/macOS/Info.plist create mode 100644 Examples/PerfBench/Sources/macOS/main.swift create mode 100644 Examples/PerfBench/Sources/visionOS/Info.plist create mode 100644 Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift create mode 100644 Examples/PerfBench/project.yml create mode 100644 perf/baselines/README.md create mode 100644 perf/results/.gitkeep create mode 100755 scripts/perf/compare_baseline.py create mode 100755 scripts/perf/run_bench.sh diff --git a/.gitignore b/.gitignore index b5d090184..8842f78b6 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,7 @@ scripts/tests/__pycache__/ # Local SwiftPM scratch directories other than .build (e.g. .build-strict for the strict-concurrency build). .build*/ + +# PerfBench run output (scripts/perf/run_bench.sh) +perf/results/* +!perf/results/.gitkeep diff --git a/Examples/PerfBench/.gitignore b/Examples/PerfBench/.gitignore new file mode 100644 index 000000000..3793b5ae9 --- /dev/null +++ b/Examples/PerfBench/.gitignore @@ -0,0 +1,4 @@ +*.xcodeproj +.build/ +DerivedData/ +results/ diff --git a/Examples/PerfBench/README.md b/Examples/PerfBench/README.md new file mode 100644 index 000000000..47c363537 --- /dev/null +++ b/Examples/PerfBench/README.md @@ -0,0 +1,96 @@ +# PerfBench + +A benchmark app for the engine on macOS, iOS and visionOS. It builds a fixed set of scenes one after +another, warms each up, records every frame through the engine's stats recorder, and writes one +JSON Lines file per scene plus a `summary.json` for the run. `scripts/perf/run_bench.sh` builds it, +runs it on a Mac or a paired device, collects the run folder and compares it with the stored +baseline for that device model. + +The point is repeatability: the same scenes, the same drawable size, the same camera motion, the +engine stats and the GPU pass timer on, in a release build, so a number from before a change can be +compared with a number from after it. + +## Scenes + +| id | What it measures | +|---|---| +| `primitives-1k` | 1024 individual cubes and spheres, 4 point lights, cascaded shadows: per-entity CPU cost and encoder overhead. | +| `primitives-10k` | 10 000 primitives merged by the static batcher: batched draw cost and light-loop cost at scale. | +| `lights-64` | 1024 batched primitives with 64 point lights: the per-pixel light loop. | +| `postfx` | The 1k grid with SSAO, bloom, depth of field and SMAA: the full-screen pass chain. | +| `animation-16` | 16 skinned characters running: the animation system and GPU skinning. | +| `stadium` | The stadium and grass assets: material binds and textures. | +| `gaussian` | A small Gaussian splat plus primitives: the splat cull, depth-key and radix-sort passes. | + +Assets come from `Tests/UntoldEngineRenderTests/Resources`; the project copies the `Models` and +`Animations` folders and the splat file into the app bundle. + +On macOS and iOS the camera orbits each scene (`BenchCameraOrbit`). On visionOS the head is the +camera, so the scenes are built three metres in front of the user and the orbit is ignored; keep +still during a run, or accept the head motion as part of the measurement. + +Frames are display-paced on every platform (the display's maximum refresh rate on macOS and iOS, +the compositor on visionOS), so a scene that fits in a frame reports the refresh interval as its +frame time. Read the cost of such a scene from `meanGPUExecutionMs`, the per-pass GPU means and the +per-system CPU times in the snapshots; frame time and the over-budget count become the signal only +when a scene no longer fits. A frame counts as over budget when it took more than one and a half +refresh intervals, that is when a refresh was skipped. + +## Running + +```bash +brew install xcodegen # once + +scripts/perf/run_bench.sh macos # this Mac +scripts/perf/run_bench.sh visionos --device "Javier's Apple Vision Pro" +scripts/perf/run_bench.sh ios --device + +scripts/perf/run_bench.sh macos --scenes primitives-1k,postfx --seconds 10 +scripts/perf/run_bench.sh macos --update-baseline # store this run as the baseline +scripts/perf/run_bench.sh visionos --device --xctrace "Metal System Trace" +``` + +Runs land in `perf/results/-/` with `.jsonl`, `summary.json` and the +console log. The comparison prints one block per scene and exits non-zero on a regression (10 % +on frame-time and GPU-time metrics, 0.5 points on the over-budget and missed-deadline rates, 20 % +on a per-pass GPU mean). + +The app can also be opened in Xcode (`xcodegen generate`, then `PerfBench.xcodeproj`) and started +from its Start button; it reads its configuration from the environment: + +| Variable | Meaning | Default | +|---|---|---| +| `UNTOLD_BENCH_SCENES` | Comma-separated scene ids, or `all` | `all` | +| `UNTOLD_BENCH_WARMUP` | Seconds rendered before recording, per scene | `3` | +| `UNTOLD_BENCH_SECONDS` | Seconds recorded per scene | `15` | +| `UNTOLD_BENCH_OUTPUT` | Output directory | `Documents/PerfBench` | +| `UNTOLD_BENCH_RUN_ID` | Run folder name | timestamp | +| `UNTOLD_BENCH_LABEL` | Free text stored in the summary | empty | +| `UNTOLD_BENCH_KEEP_OPEN` | `1` keeps the app open after the last scene | exit | +| `UNTOLD_BENCH_AUTOSTART` | `0` waits for the Start button | start on launch | +| `UNTOLD_BENCH_IMMERSION` | `full` or `mixed` (visionOS) | `full` | +| `UNTOLD_BENCH_PER_FRAME` | `0` writes one line per second instead of per frame | per frame | + +On a device the app exits when the run is done so that `devicectl ... --console` returns; the run +folder is then pulled from the app's Documents container. With the app opened from Xcode, set +`UNTOLD_BENCH_KEEP_OPEN=1` in the scheme to read the report on screen. + +## What a run records + +Every line of `.jsonl` is a full `EngineStatsSnapshot` (frame timing, per-system CPU time, +GPU pass times, culling, streaming, batching, memory, thermal state, compositor deadline margins on +visionOS, hitch histogram), and the last line is the `EngineStatsRecordingSummary` for the scene. +`summary.json` holds the device (model, OS, GPU), the render configuration (platform, viewport, +view count, texture layout, foveation, immersion) and every scene's summary and last snapshot. +See `docs/API/UsingProfiler.md` for the meaning of each field. + +## Reading a regression + +1. Frame p95/p99 up, GPU time flat: CPU-side. Open the run's `jsonl` and look at `timing`: which + system grew (`animationMs`, `scenegraphMs`, ...), or `encodeMs` for draw submission. +2. GPU time up: `gpuPasses` says which pass. Reproduce the scene in the app from Xcode and take a + GPU frame capture of that pass. +3. Missed deadlines up on visionOS with everything else flat: look at `compositor.inputSlackMs` + (late update phase) and `timing.semaphoreWaitMs` (GPU pacing the CPU), then a Metal System + Trace with `--xctrace`. +4. `worstThermalState` above 1: the device throttled; rerun cool before believing the numbers. diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift new file mode 100644 index 000000000..aad7ff208 --- /dev/null +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift @@ -0,0 +1,76 @@ +// +// BenchConfig.swift +// PerfBench +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation + +/// Run configuration, read from the environment so the driver script and `devicectl` can set it. +/// +/// | Variable | Meaning | Default | +/// |---|---|---| +/// | `UNTOLD_BENCH_SCENES` | Comma-separated scene ids, or `all` | `all` | +/// | `UNTOLD_BENCH_WARMUP` | Seconds rendered before recording starts, per scene | `3` | +/// | `UNTOLD_BENCH_SECONDS` | Seconds recorded per scene | `15` | +/// | `UNTOLD_BENCH_OUTPUT` | Output directory | `/PerfBench` | +/// | `UNTOLD_BENCH_RUN_ID` | Run folder name | timestamp | +/// | `UNTOLD_BENCH_LABEL` | Free text stored in the summary (commit, branch, note) | empty | +/// | `UNTOLD_BENCH_KEEP_OPEN` | `1` keeps the app running after the last scene | exit when done | +/// | `UNTOLD_BENCH_AUTOSTART` | `0` waits for the Start button (visionOS) | start on launch | +/// | `UNTOLD_BENCH_IMMERSION` | `full` or `mixed` (visionOS) | `full` | +/// | `UNTOLD_BENCH_PER_FRAME` | `0` records one line per second instead of per frame | per frame | +public struct BenchConfig: Sendable { + public var sceneIDs: [String] + public var warmupSeconds: Double + public var measureSeconds: Double + public var outputDirectory: URL + public var runID: String + public var label: String + public var exitWhenDone: Bool + public var autoStart: Bool + public var immersion: String + public var perFrame: Bool + + @MainActor + public static func fromEnvironment(_ environment: [String: String] = ProcessInfo.processInfo.environment) -> BenchConfig { + func double(_ key: String, _ fallback: Double) -> Double { + guard let raw = environment[key], let value = Double(raw), value > 0 else { return fallback } + return value + } + let scenesRaw = environment["UNTOLD_BENCH_SCENES"] ?? "all" + let sceneIDs = scenesRaw == "all" + ? BenchScenes.all.map(\.id) + : scenesRaw.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } + + let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + ?? FileManager.default.temporaryDirectory + let outputDirectory = environment["UNTOLD_BENCH_OUTPUT"].map { URL(fileURLWithPath: $0, isDirectory: true) } + ?? documents.appendingPathComponent("PerfBench", isDirectory: true) + + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd-HHmmss" + formatter.locale = Locale(identifier: "en_US_POSIX") + + return BenchConfig( + sceneIDs: sceneIDs, + warmupSeconds: double("UNTOLD_BENCH_WARMUP", 3.0), + measureSeconds: double("UNTOLD_BENCH_SECONDS", 15.0), + outputDirectory: outputDirectory, + runID: environment["UNTOLD_BENCH_RUN_ID"] ?? formatter.string(from: Date()), + label: environment["UNTOLD_BENCH_LABEL"] ?? "", + exitWhenDone: environment["UNTOLD_BENCH_KEEP_OPEN"] != "1", + autoStart: environment["UNTOLD_BENCH_AUTOSTART"] != "0", + immersion: environment["UNTOLD_BENCH_IMMERSION"] ?? "full", + perFrame: environment["UNTOLD_BENCH_PER_FRAME"] != "0" + ) + } + + public var runDirectory: URL { + outputDirectory.appendingPathComponent(runID, isDirectory: true) + } +} diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift new file mode 100644 index 000000000..df6c667a2 --- /dev/null +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift @@ -0,0 +1,134 @@ +// +// BenchResults.swift +// PerfBench +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation +import Metal +import UntoldEngine + +/// What the run was measured on. Baselines are keyed by `model`. +public struct BenchDeviceInfo: Codable, Sendable { + public var model: String + public var osVersion: String + public var gpuName: String + public var processorCount: Int + public var physicalMemoryBytes: UInt64 + public var thermalStateAtStart: Int + public var isLowPowerMode: Bool + + public static func current() -> BenchDeviceInfo { + BenchDeviceInfo( + model: modelIdentifier(), + osVersion: ProcessInfo.processInfo.operatingSystemVersionString, + gpuName: MTLCreateSystemDefaultDevice()?.name ?? "unknown", + processorCount: ProcessInfo.processInfo.processorCount, + physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory, + thermalStateAtStart: ProcessInfo.processInfo.thermalState.rawValue, + isLowPowerMode: ProcessInfo.processInfo.isLowPowerModeEnabled + ) + } + + private static func modelIdentifier() -> String { + #if os(macOS) + let key = "hw.model" + #else + let key = "hw.machine" + #endif + var size = 0 + sysctlbyname(key, nil, &size, nil, 0) + guard size > 0 else { return "unknown" } + var buffer = [CChar](repeating: 0, count: size) + sysctlbyname(key, &buffer, &size, nil, 0) + return String(cString: buffer) + } +} + +/// Render settings the app chose, so a baseline is never compared across different ones. +public struct BenchRenderSettings: Codable, Sendable { + public var platform: String + public var viewportWidth: Int = 0 + public var viewportHeight: Int = 0 + public var viewCount: Int = 1 + public var layout: String = "single" + public var foveation: Bool = false + public var renderQuality: Double = 1.0 + public var immersion: String = "none" + /// Whether presentation is paced by the display. True everywhere: on macOS and iOS the view + /// presents at the display's maximum refresh rate, on visionOS the compositor paces frames. + /// Frame time is therefore bounded below by the refresh interval; GPU time, per-pass and + /// per-system times are the cost metrics for scenes that fit in a frame. + public var vsync: Bool = true + /// Refresh rate the frame budget is derived from (0 when unknown). + public var displayRefreshHz: Double = 0.0 + + public init(platform: String) { + self.platform = platform + } +} + +public struct BenchSceneResult: Codable, Sendable { + public var id: String + public var title: String + public var notes: String + /// Recording file, relative to the run directory. + public var file: String + public var warmupSeconds: Double + public var measureSeconds: Double + public var summary: EngineStatsRecordingSummary + /// The last published snapshot of the scene, for the compositor, hitch and pass details. + public var lastSnapshot: EngineStatsSnapshot +} + +public struct BenchRunSummary: Codable, Sendable { + public var runID: String + public var label: String + public var startedAt: String + public var finishedAt: String = "" + public var device: BenchDeviceInfo + public var render: BenchRenderSettings + public var scenes: [BenchSceneResult] = [] + + public init(runID: String, label: String, device: BenchDeviceInfo, render: BenchRenderSettings) { + self.runID = runID + self.label = label + startedAt = ISO8601DateFormatter().string(from: Date()) + self.device = device + self.render = render + } + + /// Writes `summary.json` into `directory` and returns the encoded data. + @discardableResult + public func write(to directory: URL) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + encoder.nonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "inf", negativeInfinity: "-inf", nan: "nan") + let data = try encoder.encode(self) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try data.write(to: directory.appendingPathComponent("summary.json")) + return data + } + + /// One line per scene for the console. + public var consoleReport: String { + var lines: [String] = [] + lines.append("PerfBench \(runID) on \(device.model) (\(device.gpuName), \(device.osVersion)) \(render.platform) \(render.viewportWidth)x\(render.viewportHeight) x\(render.viewCount)") + for scene in scenes { + let s = scene.summary + let miss = s.deadlineSamples > 0 + ? String(format: " deadlineMiss %.2f%%", Double(s.missedDeadlines) / Double(s.deadlineSamples) * 100) + : "" + lines.append(String( + format: " %@: frames %d | mean %.2f p95 %.2f p99 %.2f worst %.2f ms | over budget %d | gpu %.2f ms%@ | thermal %d", + scene.id, s.frames, s.meanFrameMs, s.p95FrameMs, s.p99FrameMs, s.worstFrameMs, + s.framesOverBudget, s.meanGPUExecutionMs, miss, s.worstThermalState + )) + } + return lines.joined(separator: "\n") + } +} diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift new file mode 100644 index 000000000..b7c2cf72d --- /dev/null +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift @@ -0,0 +1,201 @@ +// +// BenchRunner.swift +// PerfBench +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Combine +import Foundation +import QuartzCore +import simd +import UntoldEngine + +/// Drives the scenes one after another from the engine's update callback: +/// build → warm up → record → tear down, then writes `summary.json` and reports. +@MainActor +public final class BenchRunner: ObservableObject { + public enum Phase: Equatable { + case idle + case building(Int) + case warmup(Int) + case recording(Int) + case tearingDown(Int) + case finished + } + + @Published public private(set) var phase: Phase = .idle + @Published public private(set) var statusText: String = "Ready" + @Published public private(set) var report: String = "" + + public let config: BenchConfig + public var render: BenchRenderSettings + /// Where scenes are built. Zero on macOS and iOS; a point in front of the user on visionOS. + public var sceneOrigin: simd_float3 = .zero + /// Whether the runner moves the camera along each scene's orbit (off on visionOS). + public var drivesCamera = true + public var onFinished: ((BenchRunSummary) -> Void)? + + private let scenes: [BenchScene] + private var summary: BenchRunSummary + private var phaseDeadline: Double = 0.0 + private var sceneStart: Double = 0.0 + private var cameraEntity: EntityID? + + public init(config: BenchConfig, render: BenchRenderSettings) { + self.config = config + self.render = render + scenes = BenchScenes.scenes(withIDs: config.sceneIDs) + summary = BenchRunSummary(runID: config.runID, label: config.label, device: .current(), render: render) + } + + public var isRunning: Bool { + switch phase { + case .idle, .finished: return false + default: return true + } + } + + /// Starts the run. Safe to call once; later calls are ignored. + public func start() { + guard phase == .idle else { return } + guard !scenes.isEmpty else { + statusText = "No scenes match \(config.sceneIDs)" + phase = .finished + return + } + try? FileManager.default.createDirectory(at: config.runDirectory, withIntermediateDirectories: true) + setEngine(.metrics(.enabled)) + GPUPassTiming.shared.setEnabled(true) + setEngineStatsLogging(enabled: false) + if render.displayRefreshHz > 0 { + // Display-paced frames sit right at the refresh interval, so "over budget" means a + // refresh was skipped: more than one and a half intervals. + EngineStatsMonitor.shared.frameBudgetMs = 1.5 * 1000.0 / render.displayRefreshHz + } + gameMode = true + phase = .building(0) + } + + /// Call from the engine's game update callback every frame. + public func update(deltaTime _: Float) { + let now = CACurrentMediaTime() + switch phase { + case .idle, .finished: + return + + case let .building(index): + let scene = scenes[index] + statusText = "Building \(scene.id)" + setSceneReady(false) + scene.build(origin: sceneOrigin) + cameraEntity = CameraSystem.shared.activeCamera + setSceneReady(true) + EngineStatsMonitor.shared.reset() + sceneStart = now + phaseDeadline = now + config.warmupSeconds + phase = .warmup(index) + + case let .warmup(index): + driveCamera(scenes[index], elapsed: now - sceneStart) + statusText = String(format: "%@: warming up %.0fs", scenes[index].id, phaseDeadline - now) + if now >= phaseDeadline { + EngineStatsMonitor.shared.reset() + let file = config.runDirectory.appendingPathComponent("\(scenes[index].id).jsonl") + do { + try startEngineStatsRecording(to: file, interval: config.perFrame ? .perFrame : .perSecond) + } catch { + statusText = "Recording failed: \(error)" + } + phaseDeadline = now + config.measureSeconds + phase = .recording(index) + } + + case let .recording(index): + driveCamera(scenes[index], elapsed: now - sceneStart) + statusText = String(format: "%@: recording %.0fs", scenes[index].id, phaseDeadline - now) + if now >= phaseDeadline { + finishScene(index) + phaseDeadline = now + 0.75 + phase = .tearingDown(index) + } + + case let .tearingDown(index): + if now >= phaseDeadline { + let next = index + 1 + if next < scenes.count { + phase = .building(next) + } else { + finishRun() + } + } + } + } + + // MARK: - Internals + + private func driveCamera(_ scene: BenchScene, elapsed: Double) { + guard drivesCamera, let cameraEntity else { return } + let orbit = scene.orbit + let eye = sceneOrigin + orbit.eye(at: Float(elapsed)) + cameraLookAt(entityId: cameraEntity, eye: eye, target: sceneOrigin + orbit.center, up: simd_float3(0, 1, 0)) + } + + private func finishScene(_ index: Int) { + let scene = scenes[index] + let recording = stopEngineStatsRecording() ?? EngineStatsRecordingSummary() + let snapshot = getEngineStatsSnapshot() + if render.viewportWidth == 0 { + render.viewportWidth = snapshot.compositor.viewTextureWidth + render.viewportHeight = snapshot.compositor.viewTextureHeight + render.viewCount = max(1, snapshot.compositor.viewCount) + summary.render = render + } + summary.scenes.append(BenchSceneResult( + id: scene.id, + title: scene.title, + notes: scene.notes, + file: "\(scene.id).jsonl", + warmupSeconds: config.warmupSeconds, + measureSeconds: config.measureSeconds, + summary: recording, + lastSnapshot: snapshot + )) + scene.teardown() + cameraEntity = nil + setSceneReady(false) + destroyAllEntities() + // Frames keep rendering while destroys finalize; give the engine a camera so it does not + // log a missing active camera every frame until the next scene builds its own. + let placeholder = createEntity() + setEntityName(entityId: placeholder, name: "Bench Idle Camera") + createGameCamera(entityId: placeholder) + cameraLookAt(entityId: placeholder, eye: simd_float3(0, 2, 6), target: .zero, up: simd_float3(0, 1, 0)) + setCamera(.active(placeholder)) + } + + private func finishRun() { + summary.finishedAt = ISO8601DateFormatter().string(from: Date()) + do { + let data = try summary.write(to: config.runDirectory) + report = summary.consoleReport + print(report) + // One line the driver script can pick out of the console stream. + if let json = String(data: data, encoding: .utf8) { + print("PERFBENCH_SUMMARY_JSON " + json.replacingOccurrences(of: "\n", with: " ")) + } + print("PERFBENCH_DONE \(config.runDirectory.path)") + } catch { + report = "Failed to write summary: \(error)" + print(report) + print("PERFBENCH_FAILED") + } + fflush(stdout) + statusText = "Done" + phase = .finished + onFinished?(summary) + } +} diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift new file mode 100644 index 000000000..159b1bf4d --- /dev/null +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift @@ -0,0 +1,306 @@ +// +// BenchScenes.swift +// PerfBench +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Foundation +import simd +import SwiftUI +import UntoldEngine + +/// How the runner moves the camera on platforms where the camera is not the user's head. +public struct BenchCameraOrbit: Sendable { + public var center: simd_float3 + public var radius: Float + public var height: Float + /// Seconds for one full orbit. + public var period: Float + + public init(center: simd_float3, radius: Float, height: Float, period: Float = 12.0) { + self.center = center + self.radius = radius + self.height = height + self.period = period + } + + public func eye(at seconds: Float) -> simd_float3 { + let angle = 2.0 * Float.pi * seconds / period + return center + simd_float3(radius * cos(angle), height, radius * sin(angle)) + } +} + +/// One benchmark scene. `build` creates every entity relative to `origin`; the runner destroys +/// them afterwards, so `teardown` only has to undo global settings the scene changed. +@MainActor +public protocol BenchScene { + var id: String { get } + var title: String { get } + var notes: String { get } + /// Camera orbit for macOS and iOS. On visionOS the head is the camera and the orbit is ignored. + var orbit: BenchCameraOrbit { get } + func build(origin: simd_float3) + func teardown() +} + +public extension BenchScene { + func teardown() {} +} + +public enum BenchScenes { + @MainActor public static let all: [BenchScene] = [ + PrimitivesScene(id: "primitives-1k", gridSize: 32, spacing: 1.0, batched: false, pointLights: 4), + PrimitivesScene(id: "primitives-10k", gridSize: 100, spacing: 1.0, batched: true, pointLights: 4), + PrimitivesScene(id: "lights-64", gridSize: 32, spacing: 1.0, batched: true, pointLights: 64), + PostFXScene(), + AnimationScene(), + StadiumScene(), + GaussianScene(), + ] + + @MainActor public static func scenes(withIDs ids: [String]) -> [BenchScene] { + ids.compactMap { id in all.first { $0.id == id } } + } +} + +// MARK: - Shared helpers + +@MainActor +enum BenchSceneBuilder { + static func makeCamera(eye: simd_float3, target: simd_float3) { + let camera = createEntity() + setEntityName(entityId: camera, name: "Bench Camera") + createGameCamera(entityId: camera) + cameraLookAt(entityId: camera, eye: eye, target: target, up: simd_float3(0, 1, 0)) + setCamera(.active(camera)) + } + + static func makeSun(pitch: Float = -50.0, intensity: Float = 1.5) { + let sun = createEntity() + setEntityName(entityId: sun, name: "Bench Sun") + createDirLight(entityId: sun) + rotateTo(entityId: sun, angle: pitch, axis: simd_float3(1, 0, 0)) + setLight(entityId: sun, .color(simd_float3(1.0, 0.94, 0.86))) + setLight(entityId: sun, .intensity(intensity)) + setLight(entityId: sun, .directional(.active)) + } + + static func makePointLights(count: Int, around center: simd_float3, radius: Float, height: Float) { + for index in 0 ..< count { + let light = createEntity() + setEntityName(entityId: light, name: "Bench Point \(index)") + createPointLight(entityId: light) + let angle = 2.0 * Float.pi * Float(index) / Float(max(1, count)) + translateTo(entityId: light, position: center + simd_float3(radius * cos(angle), height, radius * sin(angle))) + let hue = Double(index % 12) / 12.0 + let rgb = Color(hue: hue, saturation: 0.55, brightness: 1.0) + setLight(entityId: light, .color(Self.rgb(rgb))) + setLight(entityId: light, .intensity(6.0)) + setLight(entityId: light, .point(.range(8.0))) + } + } + + static func rgb(_ color: Color) -> simd_float3 { + #if canImport(AppKit) + let native = NSColor(color).usingColorSpace(.deviceRGB) ?? NSColor.white + return simd_float3(Float(native.redComponent), Float(native.greenComponent), Float(native.blueComponent)) + #else + var r: CGFloat = 1, g: CGFloat = 1, b: CGFloat = 1, a: CGFloat = 1 + UIColor(color).getRed(&r, green: &g, blue: &b, alpha: &a) + return simd_float3(Float(r), Float(g), Float(b)) + #endif + } + + static func makeGrid( + origin: simd_float3, + gridSize: Int, + spacing: Float, + batched: Bool, + namePrefix: String + ) -> [EntityID] { + let cube = BasicPrimitives.createCube(extent: spacing * 0.35) + let sphere = BasicPrimitives.createSphere(extent: spacing * 0.22, segments: [24, 12]) + let half = Float(gridSize) * spacing * 0.5 + var entities: [EntityID] = [] + entities.reserveCapacity(gridSize * gridSize) + for z in 0 ..< gridSize { + for x in 0 ..< gridSize { + let entity = createEntity() + setEntityName(entityId: entity, name: "\(namePrefix)_\(x)_\(z)") + let useSphere = (x + z) % 3 == 0 + setEntityMeshDirect( + entityId: entity, + meshes: useSphere ? sphere : cube, + assetName: useSphere ? "bench_sphere" : "bench_cube" + ) + let y = 0.15 * sin(Float(x) * 0.35) * cos(Float(z) * 0.35) + translateTo( + entityId: entity, + position: origin + simd_float3(Float(x) * spacing - half, y, Float(z) * spacing - half) + ) + let hue = Double((x * 7 + z * 3) % 16) / 16.0 + updateMaterialColor(entityId: entity, color: Color(hue: hue, saturation: 0.6, brightness: 0.85)) + updateMaterialRoughness(entityId: entity, roughness: useSphere ? 0.25 : 0.7) + if batched { + setEntityStaticBatchComponent(entityId: entity) + } + entities.append(entity) + } + } + if batched { + setBatching(.enabled(true)) + generateBatches() + } + return entities + } +} + +// MARK: - Scenes + +/// A grid of individual primitives: draw-call and per-entity CPU cost, shadows, a few lights. +@MainActor +final class PrimitivesScene: BenchScene { + let id: String + let gridSize: Int + let spacing: Float + let batched: Bool + let pointLights: Int + + init(id: String, gridSize: Int, spacing: Float, batched: Bool, pointLights: Int) { + self.id = id + self.gridSize = gridSize + self.spacing = spacing + self.batched = batched + self.pointLights = pointLights + } + + var title: String { + "\(gridSize * gridSize) primitives\(batched ? ", static batching" : ""), \(pointLights) point lights" + } + + var notes: String { + batched + ? "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost." + : "One entity and draw per primitive; measures per-entity CPU cost and encoder overhead." + } + + var orbit: BenchCameraOrbit { + let extent = Float(gridSize) * spacing + return BenchCameraOrbit(center: .zero, radius: extent * 0.7, height: extent * 0.35, period: 14.0) + } + + func build(origin: simd_float3) { + BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin) + BenchSceneBuilder.makeSun() + _ = BenchSceneBuilder.makeGrid(origin: origin, gridSize: gridSize, spacing: spacing, batched: batched, namePrefix: id) + let extent = Float(gridSize) * spacing + BenchSceneBuilder.makePointLights(count: pointLights, around: origin, radius: extent * 0.4, height: 2.0) + } + + func teardown() { + if batched { + setBatching(.enabled(false)) + } + } +} + +/// The 1k primitive grid with SSAO, bloom, depth of field and SMAA on: the post-processing chain. +@MainActor +final class PostFXScene: BenchScene { + let id = "postfx" + let title = "1024 primitives with SSAO, bloom, depth of field and SMAA" + let notes = "Same geometry as primitives-1k; the difference is the full-screen pass chain." + let orbit = BenchCameraOrbit(center: .zero, radius: 22.0, height: 11.0, period: 14.0) + + func build(origin: simd_float3) { + BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin) + BenchSceneBuilder.makeSun() + _ = BenchSceneBuilder.makeGrid(origin: origin, gridSize: 32, spacing: 1.0, batched: false, namePrefix: id) + BenchSceneBuilder.makePointLights(count: 4, around: origin, radius: 12.0, height: 2.0) + setRendering(.postProcessing(.enabled)) + setRendering(.antiAliasing(.smaa)) + setPostFX(.ssao(.enabled(true))) + setPostFX(.ssao(.quality(.balanced))) + setPostFX(.bloomThreshold(.enabled(true))) + setPostFX(.bloomComposite(.enabled(true))) + setPostFX(.depthOfField(.enabled(true))) + } + + func teardown() { + setPostFX(.depthOfField(.enabled(false))) + setPostFX(.bloomComposite(.enabled(false))) + setPostFX(.bloomThreshold(.enabled(false))) + setPostFX(.ssao(.enabled(false))) + setRendering(.antiAliasing(.none)) + } +} + +/// Sixteen skinned characters playing a clip: animation sampling, joint updates and the deformation pass. +@MainActor +final class AnimationScene: BenchScene { + let id = "animation-16" + let title = "16 skinned characters running" + let notes = "redplayer.untold with the running clip; measures the animation system and GPU skinning." + let orbit = BenchCameraOrbit(center: simd_float3(0, 1, 0), radius: 11.0, height: 3.0, period: 14.0) + + func build(origin: simd_float3) { + BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin + simd_float3(0, 1, 0)) + BenchSceneBuilder.makeSun() + AnimationSystem.shared.isEnabled = true + for index in 0 ..< 16 { + let entity = createEntity() + setEntityName(entityId: entity, name: "Bench Player \(index)") + setEntityMesh(entityId: entity, filename: "redplayer", withExtension: "untold") + setEntityAnimations(entityId: entity, filename: "running", withExtension: "untold", name: "running") + changeAnimation(entityId: entity, name: "running") + let x = Float(index % 4) * 2.2 - 3.3 + let z = Float(index / 4) * 2.2 - 3.3 + translateTo(entityId: entity, position: origin + simd_float3(x, 0, z)) + } + } +} + +/// A textured, multi-material asset with a ground plane: material binds and texture bandwidth. +@MainActor +final class StadiumScene: BenchScene { + let id = "stadium" + let title = "Stadium and grass (textured assets)" + let notes = "stadium.untold and grass.untold from the render-test resources." + let orbit = BenchCameraOrbit(center: .zero, radius: 60.0, height: 22.0, period: 18.0) + + func build(origin: simd_float3) { + BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin) + BenchSceneBuilder.makeSun(pitch: -40.0) + let stadium = createEntity() + setEntityName(entityId: stadium, name: "Bench Stadium") + setEntityMesh(entityId: stadium, filename: "stadium", withExtension: "untold") + translateTo(entityId: stadium, position: origin) + let grass = createEntity() + setEntityName(entityId: grass, name: "Bench Grass") + setEntityMesh(entityId: grass, filename: "grass", withExtension: "untold") + translateTo(entityId: grass, position: origin) + } +} + +/// A Gaussian splat next to primitives: the splat cull, depth-key and radix-sort passes every frame. +@MainActor +final class GaussianScene: BenchScene { + let id = "gaussian" + let title = "Gaussian splat with primitives" + let notes = "test_gaussians.ply (small) plus a 16x16 grid; exercises the sort pipeline, not splat count." + let orbit = BenchCameraOrbit(center: .zero, radius: 10.0, height: 4.0, period: 14.0) + + func build(origin: simd_float3) { + BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin) + BenchSceneBuilder.makeSun() + let splat = createEntity() + setEntityName(entityId: splat, name: "Bench Gaussian") + setEntityGaussian(entityId: splat, filename: "test_gaussians", withExtension: "ply") + translateTo(entityId: splat, position: origin + simd_float3(0, 1, 0)) + _ = BenchSceneBuilder.makeGrid(origin: origin, gridSize: 16, spacing: 1.0, batched: false, namePrefix: id) + } +} diff --git a/Examples/PerfBench/Sources/iOS/Info.plist b/Examples/PerfBench/Sources/iOS/Info.plist new file mode 100644 index 000000000..211e9f2e5 --- /dev/null +++ b/Examples/PerfBench/Sources/iOS/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + + + diff --git a/Examples/PerfBench/Sources/iOS/PerfBenchApp.swift b/Examples/PerfBench/Sources/iOS/PerfBenchApp.swift new file mode 100644 index 000000000..c56987678 --- /dev/null +++ b/Examples/PerfBench/Sources/iOS/PerfBenchApp.swift @@ -0,0 +1,104 @@ +// +// PerfBenchApp.swift +// PerfBench (iOS) +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import MetalKit +import SwiftUI +import UntoldEngine + +@main +struct PerfBenchApp: App { + var body: some SwiftUI.Scene { + WindowGroup { + BenchRootView() + } + } +} + +@MainActor +private final class BenchHost: ObservableObject { + let renderer: UntoldRenderer? + let runner: BenchRunner + let config: BenchConfig + + init() { + config = BenchConfig.fromEnvironment() + var render = BenchRenderSettings(platform: "iOS") + render.layout = "single" + render.displayRefreshHz = Double(UIScreen.main.maximumFramesPerSecond) + runner = BenchRunner(config: config, render: render) + + let view = MTKView() + view.preferredFramesPerSecond = UIScreen.main.maximumFramesPerSecond + if let device = MTLCreateSystemDefaultDevice(), let created = UntoldRenderer.createiOS(device: device, view: view) { + renderer = created + let scale = UIScreen.main.nativeScale + let bounds = UIScreen.main.nativeBounds + runner.render.viewportWidth = Int(max(bounds.width, bounds.height) / scale * scale) + runner.render.viewportHeight = Int(min(bounds.width, bounds.height) / scale * scale) + created.setupCallbacks( + gameUpdate: { [runner] deltaTime in + runner.update(deltaTime: deltaTime) + }, + handleInput: {} + ) + } else { + renderer = nil + } + + runner.onFinished = { [config] _ in + if config.exitWhenDone { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + exit(0) + } + } + } + } +} + +private struct BenchRootView: View { + @StateObject private var host = BenchHost() + + var body: some View { + ZStack(alignment: .topLeading) { + if let renderer = host.renderer { + SceneView(renderer: renderer) + .ignoresSafeArea() + } else { + Text("Metal is not available").padding() + } + BenchStatusOverlay(runner: host.runner) + } + .onAppear { + if host.config.autoStart { + host.runner.start() + } + } + } +} + +private struct BenchStatusOverlay: View { + @ObservedObject var runner: BenchRunner + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("PerfBench").font(.headline) + Text(runner.statusText).font(.caption).monospaced() + if runner.phase == .idle { + Button("Start") { runner.start() } + } + if !runner.report.isEmpty { + Text(runner.report).font(.caption2).monospaced() + } + } + .padding(12) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + .padding(16) + } +} diff --git a/Examples/PerfBench/Sources/macOS/AppDelegate.swift b/Examples/PerfBench/Sources/macOS/AppDelegate.swift new file mode 100644 index 000000000..05b692236 --- /dev/null +++ b/Examples/PerfBench/Sources/macOS/AppDelegate.swift @@ -0,0 +1,120 @@ +// +// AppDelegate.swift +// PerfBench (macOS) +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import AppKit +import MetalKit +import simd +import SwiftUI +import UntoldEngine + +@MainActor +final class AppDelegate: NSObject, NSApplicationDelegate { + private enum Constants { + /// Fixed drawable size so runs on different displays compare; contents scale is pinned to 1. + static let drawableSize = CGSize(width: 1920, height: 1080) + } + + private var window: NSWindow! + private var renderer: UntoldRenderer! + private var runner: BenchRunner! + + func applicationDidFinishLaunching(_: Notification) { + let config = BenchConfig.fromEnvironment() + var render = BenchRenderSettings(platform: "macOS") + render.viewportWidth = Int(Constants.drawableSize.width) + render.viewportHeight = Int(Constants.drawableSize.height) + render.layout = "single" + render.vsync = true + render.displayRefreshHz = Double(NSScreen.main?.maximumFramesPerSecond ?? 60) + runner = BenchRunner(config: config, render: render) + + guard let renderer = UntoldRenderer.create() else { + print("PERFBENCH_FAILED renderer") + NSApp.terminate(nil) + return + } + self.renderer = renderer + let view = renderer.metalView + // Fixed drawable size, the same recipe the engine's render tests use: no auto-resizing, + // an explicit drawable size and viewport, and one resize report so the engine rebuilds + // its viewport-sized resources on the first frame. + view.autoResizeDrawable = false + view.frame = NSRect(origin: .zero, size: Constants.drawableSize) + renderer.mtkView(view, drawableSizeWillChange: Constants.drawableSize) + view.drawableSize = Constants.drawableSize + (view.layer as? CAMetalLayer)?.contentsScale = 1.0 + renderInfo.viewPort = simd_float2(Float(Constants.drawableSize.width), Float(Constants.drawableSize.height)) + + window = NSWindow( + contentRect: NSRect(origin: .zero, size: Constants.drawableSize), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + window.title = "Untold PerfBench" + // Windowed presentation hands drawables back at the display rate whatever the layer's + // sync setting, so run at the display's maximum refresh and derive the budget from it. + var viewOptions = UntoldViewOptions.default + viewOptions.preferredFramesPerSecond = Int(render.displayRefreshHz) + window.contentView = NSHostingView(rootView: BenchView(renderer: renderer, runner: runner, viewOptions: viewOptions)) + window.center() + window.makeKeyAndOrderFront(nil) + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + + renderer.setupCallbacks( + gameUpdate: { [weak self] deltaTime in + self?.runner.update(deltaTime: deltaTime) + }, + handleInput: {} + ) + + runner.onFinished = { _ in + if config.exitWhenDone { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + NSApp.terminate(nil) + } + } + } + + if config.autoStart { + runner.start() + } + } + + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + true + } +} + +private struct BenchView: View { + let renderer: UntoldRenderer + @ObservedObject var runner: BenchRunner + let viewOptions: UntoldViewOptions + + var body: some View { + ZStack(alignment: .topLeading) { + SceneView(renderer: renderer, options: viewOptions) + VStack(alignment: .leading, spacing: 6) { + Text("PerfBench").font(.headline) + Text(runner.statusText).font(.caption).monospaced() + if !runner.isRunning, runner.phase == .idle { + Button("Start") { runner.start() } + } + if !runner.report.isEmpty { + Text(runner.report).font(.caption2).monospaced() + } + } + .padding(12) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + .padding(16) + } + } +} diff --git a/Examples/PerfBench/Sources/macOS/Info.plist b/Examples/PerfBench/Sources/macOS/Info.plist new file mode 100644 index 000000000..37ff065f5 --- /dev/null +++ b/Examples/PerfBench/Sources/macOS/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHighResolutionCapable + + + diff --git a/Examples/PerfBench/Sources/macOS/main.swift b/Examples/PerfBench/Sources/macOS/main.swift new file mode 100644 index 000000000..85d7b34d0 --- /dev/null +++ b/Examples/PerfBench/Sources/macOS/main.swift @@ -0,0 +1,22 @@ +// +// main.swift +// PerfBench (macOS) +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import AppKit + +// Line-buffer stdout so the driver script sees progress and the last lines survive a crash. +setvbuf(stdout, nil, _IOLBF, 0) + +// Top-level code is not main-actor isolated in Swift 5 language mode; the app is. +MainActor.assumeIsolated { + let app = NSApplication.shared + let delegate = AppDelegate() + app.delegate = delegate + app.run() +} diff --git a/Examples/PerfBench/Sources/visionOS/Info.plist b/Examples/PerfBench/Sources/visionOS/Info.plist new file mode 100644 index 000000000..efb00cf56 --- /dev/null +++ b/Examples/PerfBench/Sources/visionOS/Info.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + UIApplicationSceneManifest + + UIApplicationPreferredDefaultSceneSessionRole + UIWindowSceneSessionRoleApplication + UIApplicationSupportsMultipleScenes + + + NSWorldSensingUsageDescription + PerfBench renders the benchmark scenes in your space. + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + + + diff --git a/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift b/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift new file mode 100644 index 000000000..65cdc7d7e --- /dev/null +++ b/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift @@ -0,0 +1,170 @@ +// +// PerfBenchApp.swift +// PerfBench (visionOS) +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import CompositorServices +import Foundation +import simd +import SwiftUI +import UntoldEngine +import UntoldEngineXR + +/// Keeps the XR runtime and its render thread alive for the life of the app. +@MainActor +final class XRHolder { + static let shared = XRHolder() + var xr: UntoldEngineXR? + var renderThread: Thread? + /// The layer configuration the compositor was given, recorded into the run summary. + var layout = "dedicated" + var foveation = false +} + +/// Matches the engine's current expectations: dedicated per-eye textures, no foveation. +/// The runner records these values so baselines never mix configurations. +struct PerfBenchLayerConfiguration: CompositorLayerConfiguration { + func makeConfiguration(capabilities _: LayerRenderer.Capabilities, configuration: inout LayerRenderer.Configuration) { + configuration.layout = .dedicated + configuration.isFoveationEnabled = false + configuration.colorFormat = .bgra8Unorm_srgb + } +} + +@MainActor +final class BenchHost: ObservableObject { + static let shared = BenchHost() + + let config: BenchConfig + let runner: BenchRunner + + private init() { + config = BenchConfig.fromEnvironment() + var render = BenchRenderSettings(platform: "visionOS") + render.layout = XRHolder.shared.layout + render.foveation = XRHolder.shared.foveation + render.immersion = config.immersion + render.displayRefreshHz = 90.0 + runner = BenchRunner(config: config, render: render) + // Scenes are built three metres in front of the user, slightly below eye level. + runner.sceneOrigin = simd_float3(0.0, -0.8, -3.0) + runner.drivesCamera = false + runner.onFinished = { [config] _ in + if config.exitWhenDone { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + exit(0) + } + } + } + } + + var immersionStyle: ImmersionStyle { + config.immersion == "mixed" ? .mixed : .full + } + + var isMixedImmersion: Bool { + config.immersion == "mixed" + } +} + +@main +struct PerfBenchApp: App { + @State private var immersionStyle: ImmersionStyle = BenchHost.shared.immersionStyle + + var body: some SwiftUI.Scene { + WindowGroup { + ContentView() + } + .windowStyle(.plain) + .defaultSize(width: 640, height: 420) + + ImmersiveSpace(id: "PerfBenchSpace") { + CompositorLayer(configuration: PerfBenchLayerConfiguration()) { layerRenderer in + Task { @MainActor in + startXR(layerRenderer: layerRenderer) + } + } + } + .immersionStyle(selection: $immersionStyle, in: .mixed, .full) + } + + @MainActor + private func startXR(layerRenderer: LayerRenderer) { + guard XRHolder.shared.xr == nil else { return } + guard let xr = UntoldEngineXR(layerRenderer: layerRenderer) else { + print("PERFBENCH_FAILED xr") + return + } + XRHolder.shared.xr = xr + let host = BenchHost.shared + // The XR module's UntoldImmersionMode cannot be named here (the module and its class share + // a name), so let the parameter type pick the enum. + xr.setImmersionMode(xrImmersionMode: host.isMixedImmersion ? .mixed : .full) + xr.setupCallbacks( + gameUpdate: { deltaTime in + // The XR loop runs on its own thread; the runner is main-actor bound but the + // engine's callbacks are invoked synchronously from the frame loop, so hop in place. + MainActor.assumeIsolated { + host.runner.update(deltaTime: deltaTime) + } + }, + handleInput: {} + ) + let thread = Thread { + xr.start() + xr.runLoop() + } + thread.name = "XR Render Thread" + thread.qualityOfService = .userInteractive + XRHolder.shared.renderThread = thread + thread.start() + if host.config.autoStart { + // Give the compositor a few frames before the first scene is built. + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + host.runner.start() + } + } + } +} + +struct ContentView: View { + @Environment(\.openImmersiveSpace) private var openImmersiveSpace + @ObservedObject private var runner = BenchHost.shared.runner + @State private var opened = false + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Untold PerfBench").font(.extraLargeTitle).fontWeight(.bold) + Text(runner.statusText).font(.title3).monospaced() + if !runner.report.isEmpty { + ScrollView { + Text(runner.report).font(.caption).monospaced() + } + .frame(maxHeight: 200) + } + HStack { + Button("Open Space") { open() }.disabled(opened) + Button("Start") { runner.start() }.disabled(!opened || runner.phase != .idle) + } + } + .padding(40) + .task { + if BenchHost.shared.config.autoStart { + open() + } + } + } + + private func open() { + guard !opened else { return } + opened = true + Task { + await openImmersiveSpace(id: "PerfBenchSpace") + } + } +} diff --git a/Examples/PerfBench/project.yml b/Examples/PerfBench/project.yml new file mode 100644 index 000000000..7e5a8decc --- /dev/null +++ b/Examples/PerfBench/project.yml @@ -0,0 +1,116 @@ +name: PerfBench + +# The engine is referenced by path so the benchmark always measures the checkout it sits in. +packages: + UntoldEngine: + path: ../.. + +options: + bundleIdPrefix: com.untoldengine.perfbench + deploymentTarget: + macOS: "14.0" + iOS: "17.0" + visionOS: "2.0" + +settings: + base: + # The engine uses Float16/simd_half types that do not exist on x86_64; build arm64 only. + ARCHS: arm64 + ONLY_ACTIVE_ARCH: "YES" + SWIFT_VERSION: 5.0 + MARKETING_VERSION: "1.0" + CURRENT_PROJECT_VERSION: "1" + CODE_SIGN_STYLE: Automatic + MTL_HEADER_SEARCH_PATHS: "$(inherited) $(SRCROOT)/../../Sources/UntoldEngineShaderSupport/include" + configs: + Debug: + SWIFT_OPTIMIZATION_LEVEL: -Onone + DEBUG_INFORMATION_FORMAT: dwarf-with-dsym + Release: + SWIFT_COMPILATION_MODE: wholemodule + SWIFT_OPTIMIZATION_LEVEL: -O + DEBUG_INFORMATION_FORMAT: dwarf-with-dsym + +# Benchmark assets come straight from the engine's render-test resources; the engine looks for +# Models//. and Animations//. inside the bundle, and finds a +# file at the bundle root by name. +targetTemplates: + BenchApp: + type: application + sources: + - path: Sources/PerfBenchCore + - path: ../../Tests/UntoldEngineRenderTests/Resources/Models + type: folder + buildPhase: resources + - path: ../../Tests/UntoldEngineRenderTests/Resources/Animations + type: folder + buildPhase: resources + - path: ../../Tests/UntoldEngineRenderTests/Resources/Guassians/test_gaussians.ply + buildPhase: resources + dependencies: + - package: UntoldEngine + product: UntoldEngine + - package: UntoldEngine + product: UntoldEngineShaderSupport + +targets: + PerfBench-macOS: + templates: [BenchApp] + platform: macOS + sources: + - path: Sources/macOS + excludes: ["Info.plist"] + settings: + base: + PRODUCT_NAME: PerfBench + PRODUCT_BUNDLE_IDENTIFIER: com.untoldengine.perfbench.macos + INFOPLIST_FILE: Sources/macOS/Info.plist + + PerfBench-iOS: + templates: [BenchApp] + platform: iOS + sources: + - path: Sources/iOS + excludes: ["Info.plist"] + settings: + base: + PRODUCT_NAME: PerfBench + PRODUCT_BUNDLE_IDENTIFIER: com.untoldengine.perfbench.ios + INFOPLIST_FILE: Sources/iOS/Info.plist + TARGETED_DEVICE_FAMILY: "1,2" + SUPPORTED_INTERFACE_ORIENTATIONS: UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + PerfBench-visionOS: + templates: [BenchApp] + platform: visionOS + sources: + - path: Sources/visionOS + excludes: ["Info.plist"] + dependencies: + - package: UntoldEngine + product: UntoldEngineXR + settings: + base: + PRODUCT_NAME: PerfBench + PRODUCT_BUNDLE_IDENTIFIER: com.untoldengine.perfbench.visionos + INFOPLIST_FILE: Sources/visionOS/Info.plist + +schemes: + PerfBench-macOS: + build: + targets: + PerfBench-macOS: all + run: + config: Release + PerfBench-iOS: + build: + targets: + PerfBench-iOS: all + run: + config: Release + PerfBench-visionOS: + build: + targets: + PerfBench-visionOS: all + run: + config: Release diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md index 973358cd8..1defe3d7e 100644 --- a/docs/API/UsingProfiler.md +++ b/docs/API/UsingProfiler.md @@ -190,6 +190,10 @@ Each line is `{"type":"frame","frame":{...}}` with the full snapshot; `.perFrame `startEngineStatsRecording` throws `EngineStatsRecordingError.statsNotCompiledIn` in builds without `ENGINE_STATS_ENABLED`. +## Benchmarking A Build + +`Examples/PerfBench` is a benchmark app for macOS, iOS and visionOS that runs a fixed set of scenes with the stats and the GPU pass timer on, records every frame with the recorder above and writes a `summary.json` per run. `scripts/perf/run_bench.sh ` builds it, runs it (on a paired device through `devicectl`), pulls the results into `perf/results/` and compares them with the baseline stored under `perf/baselines//` (`--update-baseline` writes one). See `Examples/PerfBench/README.md`. + ## GPU Pass Timing `GPUPassTiming` times every render, compute and blit pass of the frame on the GPU itself, from `MTLCounterSampleBuffer` timestamps at the stage boundaries: a render pass's vertex and fragment stages, a compute or blit pass as a whole. It is off by default, and a GPU without stage-boundary sampling measures nothing. diff --git a/perf/baselines/README.md b/perf/baselines/README.md new file mode 100644 index 000000000..af93877ea --- /dev/null +++ b/perf/baselines/README.md @@ -0,0 +1,10 @@ +# Performance baselines + +One folder per device model (as `hw.model` on macOS or `hw.machine` elsewhere, e.g. `Mac16,6`, +`RealityDevice17,1`, `iPhone18,1`), one folder per platform inside it, one JSON per benchmark scene. +Each file is the scene result exactly as `Examples/PerfBench` wrote it plus the render +configuration it was measured with, so `scripts/perf/compare_baseline.py` refuses to compare runs +that differ in platform, texture layout, foveation, viewport or view count. + +Write a baseline with `scripts/perf/run_bench.sh --update-baseline` from a known-good +commit, in release, on a cool device, and record the commit in the `label` field. diff --git a/perf/results/.gitkeep b/perf/results/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/perf/compare_baseline.py b/scripts/perf/compare_baseline.py new file mode 100755 index 000000000..39fba1335 --- /dev/null +++ b/scripts/perf/compare_baseline.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +"""Compare a PerfBench summary.json against the stored baseline for the same device model. + +Baselines live in perf/baselines///.json and hold the scene result as the +app wrote it, so a baseline is only ever compared with the same platform, layout, foveation and +viewport. Exit status 1 means at least one metric regressed past its threshold. + + compare_baseline.py perf/results//summary.json [--baselines DIR] [--update] + [--time-tolerance 0.10] [--rate-tolerance 0.005] +""" +import argparse +import json +import os +import sys + +TIME_METRICS = [ + ("p95FrameMs", "p95 frame ms"), + ("p99FrameMs", "p99 frame ms"), + ("meanFrameMs", "mean frame ms"), + ("meanGPUExecutionMs", "mean GPU ms"), +] + + +def load(path): + with open(path) as f: + return json.load(f) + + +def safe_model(name): + return "".join(c if c.isalnum() or c in "-_." else "_" for c in name) + + +def rate(numerator, denominator): + return (numerator / denominator) if denominator else 0.0 + + +def scene_rates(scene): + s = scene["summary"] + frames = s.get("frames", 0) + return { + "overBudgetRate": rate(s.get("framesOverBudget", 0), frames), + "missedDeadlineRate": rate(s.get("missedDeadlines", 0), s.get("deadlineSamples", 0)), + } + + +def config_key(summary): + r = summary["render"] + return (r["platform"], r.get("layout"), r.get("foveation"), r.get("viewportWidth"), r.get("viewportHeight"), r.get("viewCount")) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("summary") + ap.add_argument("--baselines", default=os.path.join(os.path.dirname(__file__), "..", "..", "perf", "baselines")) + ap.add_argument("--update", action="store_true", help="write this run as the baseline") + ap.add_argument("--time-tolerance", type=float, default=0.10, help="relative slack for time metrics") + ap.add_argument("--rate-tolerance", type=float, default=0.005, help="absolute slack for over-budget and missed-deadline rates") + ap.add_argument("--pass-tolerance", type=float, default=0.20, help="relative slack for per-pass GPU means") + args = ap.parse_args() + + run = load(args.summary) + model = safe_model(run["device"]["model"]) + platform = run["render"]["platform"] + base_dir = os.path.join(args.baselines, model, platform) + print(f"run {run['runID']} label={run.get('label','')!r} device={run['device']['model']} platform={platform} " + f"viewport={run['render'].get('viewportWidth')}x{run['render'].get('viewportHeight')} x{run['render'].get('viewCount')}") + + if args.update: + os.makedirs(base_dir, exist_ok=True) + for scene in run["scenes"]: + record = {"render": run["render"], "device": run["device"], "label": run.get("label", ""), + "runID": run["runID"], "scene": scene} + with open(os.path.join(base_dir, f"{scene['id']}.json"), "w") as f: + json.dump(record, f, indent=2, sort_keys=True) + print(f"baseline written to {base_dir} for {len(run['scenes'])} scene(s)") + return 0 + + regressions = 0 + for scene in run["scenes"]: + path = os.path.join(base_dir, f"{scene['id']}.json") + s = scene["summary"] + rates = scene_rates(scene) + head = f"{scene['id']}: frames {s.get('frames',0)} p95 {s.get('p95FrameMs',0):.2f} p99 {s.get('p99FrameMs',0):.2f} " \ + f"gpu {s.get('meanGPUExecutionMs',0):.2f} overBudget {rates['overBudgetRate']*100:.2f}% missed {rates['missedDeadlineRate']*100:.2f}%" + if not os.path.exists(path): + print(f"{head} [no baseline]") + continue + base = load(path) + if config_key({"render": base["render"]}) != config_key(run): + print(f"{head} [baseline has a different render configuration, skipped]") + continue + b = base["scene"]["summary"] + brates = scene_rates(base["scene"]) + print(head) + for key, label in TIME_METRICS: + now, ref = s.get(key, 0.0), b.get(key, 0.0) + if ref <= 0: + continue + delta = (now - ref) / ref + flag = "REGRESSION" if delta > args.time_tolerance else ("better" if delta < -args.time_tolerance else "ok") + if flag == "REGRESSION": + regressions += 1 + print(f" {label:14s} {now:8.2f} vs {ref:8.2f} {delta*100:+6.1f}% {flag}") + for key in ("overBudgetRate", "missedDeadlineRate"): + now, ref = rates[key], brates[key] + delta = now - ref + flag = "REGRESSION" if delta > args.rate_tolerance else "ok" + if flag == "REGRESSION": + regressions += 1 + print(f" {key:14s} {now*100:7.2f}% vs {ref*100:7.2f}% {delta*100:+6.2f}pt {flag}") + passes_now = s.get("gpuPassMeanMs", {}) + passes_ref = b.get("gpuPassMeanMs", {}) + for label in sorted(passes_ref): + ref = passes_ref[label] + now = passes_now.get(label) + if now is None or ref < 0.05: + continue + delta = (now - ref) / ref + if delta > args.pass_tolerance: + regressions += 1 + print(f" pass {label!r}: {now:.3f} vs {ref:.3f} ms {delta*100:+6.1f}% REGRESSION") + if regressions: + print(f"{regressions} regression(s)") + return 1 + print("no regressions") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/perf/run_bench.sh b/scripts/perf/run_bench.sh new file mode 100755 index 000000000..0190b5f58 --- /dev/null +++ b/scripts/perf/run_bench.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# Builds and runs Examples/PerfBench on macOS, an iOS device or an Apple Vision Pro, collects the +# run folder (JSON Lines per scene plus summary.json) and compares it against the stored baseline. +# +# Usage: +# scripts/perf/run_bench.sh macos [options] +# scripts/perf/run_bench.sh ios --device [options] +# scripts/perf/run_bench.sh visionos --device [options] +# +# Options: +# --scenes a,b,c scene ids (default: all) +# --seconds N recorded seconds per scene (default: 15) +# --warmup N warm-up seconds per scene (default: 3) +# --out DIR where run folders land (default: perf/results) +# --label TEXT stored in summary.json (default: git describe) +# --config Debug|Release (default: Release) +# --xctrace TEMPLATE also record an Instruments trace, e.g. "Metal System Trace" (device runs) +# --no-compare skip the baseline comparison +# --update-baseline write this run as the new baseline for the device model +# +# Requirements: xcodegen (brew install xcodegen), Xcode with the platform SDKs, a paired device for +# device runs (xcrun devicectl list devices). + +set -euo pipefail + +PLATFORM="${1:-}" +[ -n "$PLATFORM" ] || { sed -n '2,24p' "$0"; exit 2; } +shift + +REPO="$(cd "$(dirname "$0")/../.." && pwd)" +BENCH_DIR="$REPO/Examples/PerfBench" +OUT_DIR="$REPO/perf/results" +BASELINES="$REPO/perf/baselines" +SCENES="all" +SECONDS_PER_SCENE=15 +WARMUP=3 +LABEL="$(git -C "$REPO" describe --always --dirty 2>/dev/null || echo unknown)" +CONFIG=Release +DEVICE="" +XCTRACE_TEMPLATE="" +COMPARE=1 +UPDATE_BASELINE=0 + +while [ $# -gt 0 ]; do + case "$1" in + --scenes) SCENES="$2"; shift 2 ;; + --seconds) SECONDS_PER_SCENE="$2"; shift 2 ;; + --warmup) WARMUP="$2"; shift 2 ;; + --out) OUT_DIR="$2"; shift 2 ;; + --label) LABEL="$2"; shift 2 ;; + --config) CONFIG="$2"; shift 2 ;; + --device) DEVICE="$2"; shift 2 ;; + --xctrace) XCTRACE_TEMPLATE="$2"; shift 2 ;; + --no-compare) COMPARE=0; shift ;; + --update-baseline) UPDATE_BASELINE=1; shift ;; + *) echo "unknown option $1"; exit 2 ;; + esac +done + +RUN_ID="$(date +%Y%m%d-%H%M%S)-$PLATFORM" +DERIVED="$BENCH_DIR/.build/DerivedData" +mkdir -p "$OUT_DIR" + +case "$PLATFORM" in + macos) SCHEME="PerfBench-macOS"; DESTINATION="platform=macOS,arch=arm64" ;; + ios) SCHEME="PerfBench-iOS"; [ -n "$DEVICE" ] || { echo "--device is required for ios"; exit 2; }; DESTINATION="id=$DEVICE" ;; + visionos) SCHEME="PerfBench-visionOS"; [ -n "$DEVICE" ] || { echo "--device is required for visionos"; exit 2; }; DESTINATION="id=$DEVICE" ;; + *) echo "platform must be macos, ios or visionos"; exit 2 ;; +esac + +echo "== Generating project" +(cd "$BENCH_DIR" && xcodegen generate --quiet) + +echo "== Building $SCHEME ($CONFIG)" +xcodebuild build \ + -project "$BENCH_DIR/PerfBench.xcodeproj" \ + -scheme "$SCHEME" \ + -configuration "$CONFIG" \ + -destination "$DESTINATION" \ + -derivedDataPath "$DERIVED" \ + -allowProvisioningUpdates \ + ARCHS=arm64 ONLY_ACTIVE_ARCH=YES \ + -quiet + +case "$PLATFORM" in + macos) + APP="$DERIVED/Build/Products/$CONFIG/PerfBench.app" + echo "== Running $APP" + UNTOLD_BENCH_SCENES="$SCENES" UNTOLD_BENCH_SECONDS="$SECONDS_PER_SCENE" UNTOLD_BENCH_WARMUP="$WARMUP" \ + UNTOLD_BENCH_OUTPUT="$OUT_DIR" UNTOLD_BENCH_RUN_ID="$RUN_ID" UNTOLD_BENCH_LABEL="$LABEL" \ + UNTOLD_STATS=1 \ + "$APP/Contents/MacOS/PerfBench" | tee "$OUT_DIR/$RUN_ID.log" | grep -v "^PERFBENCH_SUMMARY_JSON" + ;; + + ios|visionos) + if [ "$PLATFORM" = ios ]; then + APP="$DERIVED/Build/Products/$CONFIG-iphoneos/PerfBench.app"; BUNDLE_ID="com.untoldengine.perfbench.ios" + else + APP="$DERIVED/Build/Products/$CONFIG-xros/PerfBench.app"; BUNDLE_ID="com.untoldengine.perfbench.visionos" + fi + echo "== Installing on $DEVICE" + xcrun devicectl device install app --device "$DEVICE" "$APP" --quiet + ENV_JSON=$(printf '{"UNTOLD_BENCH_SCENES":"%s","UNTOLD_BENCH_SECONDS":"%s","UNTOLD_BENCH_WARMUP":"%s","UNTOLD_BENCH_RUN_ID":"%s","UNTOLD_BENCH_LABEL":"%s","UNTOLD_STATS":"1"}' \ + "$SCENES" "$SECONDS_PER_SCENE" "$WARMUP" "$RUN_ID" "$LABEL") + TRACE_PID="" + if [ -n "$XCTRACE_TEMPLATE" ]; then + TRACE="$OUT_DIR/$RUN_ID.trace" + echo "== Recording '$XCTRACE_TEMPLATE' to $TRACE" + xcrun xctrace record --template "$XCTRACE_TEMPLATE" --device "$DEVICE" --output "$TRACE" \ + --launch -- "$BUNDLE_ID" >"$OUT_DIR/$RUN_ID.xctrace.log" 2>&1 & + TRACE_PID=$! + echo " (xctrace launched the app; environment is taken from the DEVICECTL defaults, use the app's Start button if it did not autostart)" + else + echo "== Launching $BUNDLE_ID (console attached until the app exits)" + xcrun devicectl device process launch --device "$DEVICE" --console --terminate-existing \ + --environment-variables "$ENV_JSON" "$BUNDLE_ID" | tee "$OUT_DIR/$RUN_ID.log" | grep -v "^PERFBENCH_SUMMARY_JSON" || true + fi + if [ -n "$TRACE_PID" ]; then + wait "$TRACE_PID" || true + fi + echo "== Copying results" + xcrun devicectl device copy from --device "$DEVICE" --domain-type appDataContainer --domain-identifier "$BUNDLE_ID" \ + --source "Documents/PerfBench/$RUN_ID" --destination "$OUT_DIR/$RUN_ID" --quiet || { + echo " copy failed; the run may still be on the device under Documents/PerfBench/$RUN_ID"; } + ;; +esac + +RUN_DIR="$OUT_DIR/$RUN_ID" +[ -f "$RUN_DIR/summary.json" ] || { echo "no summary.json in $RUN_DIR"; exit 1; } +echo "== Results: $RUN_DIR" + +if [ "$UPDATE_BASELINE" = 1 ]; then + python3 "$REPO/scripts/perf/compare_baseline.py" "$RUN_DIR/summary.json" --baselines "$BASELINES" --update +elif [ "$COMPARE" = 1 ]; then + python3 "$REPO/scripts/perf/compare_baseline.py" "$RUN_DIR/summary.json" --baselines "$BASELINES" +fi From 6230f593459add5adfed0e8642e6148dfbab8e03 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 21:25:09 +0200 Subject: [PATCH 08/17] [Chores] Add the macOS baseline for Mac16,5 from the first PerfBench run 15 s per scene, 1920x1080, display-paced at 120 Hz, release build of 0226477c, so later runs on this Mac have something to compare with. --- .../baselines/Mac16_5/macOS/animation-16.json | 378 ++++++++++++++++ perf/baselines/Mac16_5/macOS/gaussian.json | 396 +++++++++++++++++ perf/baselines/Mac16_5/macOS/lights-64.json | 390 +++++++++++++++++ perf/baselines/Mac16_5/macOS/postfx.json | 402 ++++++++++++++++++ .../Mac16_5/macOS/primitives-10k.json | 390 +++++++++++++++++ .../Mac16_5/macOS/primitives-1k.json | 378 ++++++++++++++++ perf/baselines/Mac16_5/macOS/stadium.json | 378 ++++++++++++++++ 7 files changed, 2712 insertions(+) create mode 100644 perf/baselines/Mac16_5/macOS/animation-16.json create mode 100644 perf/baselines/Mac16_5/macOS/gaussian.json create mode 100644 perf/baselines/Mac16_5/macOS/lights-64.json create mode 100644 perf/baselines/Mac16_5/macOS/postfx.json create mode 100644 perf/baselines/Mac16_5/macOS/primitives-10k.json create mode 100644 perf/baselines/Mac16_5/macOS/primitives-1k.json create mode 100644 perf/baselines/Mac16_5/macOS/stadium.json diff --git a/perf/baselines/Mac16_5/macOS/animation-16.json b/perf/baselines/Mac16_5/macOS/animation-16.json new file mode 100644 index 000000000..3ff986c61 --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/animation-16.json @@ -0,0 +1,378 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "animation-16.jsonl", + "id": "animation-16", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 16, + "frustumTested": 16, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 16, + "occlusionTested": 16, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1800, + "gpuPasses": { + "frameIndex": 1800, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.010875 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.142291 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.01975 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.009417 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.025542 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.166 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.078333 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.07875 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 0.490208 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.071167 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.595708 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.064542 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.075167 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.281209 + }, + { + "count": 1, + "label": "Post-Processing Pass", + "ms": 0.132833 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.201125 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.530708 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.038583 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.016834 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.011584 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.010458 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.010333 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.010667 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.010459 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.010458 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.0105 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.010125 + } + ], + "passesSkipped": 0, + "totalMs": 3.113626 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 120, + "framesOverBudget": 0, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1801, + "histogram": [ + 0, + 355, + 1446, + 0, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 9.411750361323357 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 649674752, + "isUnderPressure": false, + "meshMemoryBytes": 1326600, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 786432, + "thermalState": 0, + "trackedEntityCount": 17, + "utilizationPercent": 4.5756933104712516e-05 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 16, + "drawCallsShadow": 24, + "drawCallsTotal": 46, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 17531, + "visibleInstances": 16 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 17, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 133001.1765410417, + "timing": { + "animationMs": 0.2791666775010526, + "batchingRebuildMs": 0, + "batchingTickMs": 0.00120833283290267, + "cullingMs": 0.07700000423938036, + "customSystemsMs": 0, + "encodeMs": 0.59870834229514, + "extensionsUpdateMs": 0.0011250085663050413, + "frameTotalMs": 7.451375015079975, + "gameUpdateMs": 0.05991666694171727, + "geometryStreamingMs": 0.00200001522898674, + "gpuExecutionMs": 1.7440833325963467, + "gpuFrameCadenceMs": 4.094624979188666, + "lodMs": 0.003374996595084667, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.0826666655484587, + "renderTotalMs": 0.8604166505392641, + "scenegraphMs": 0.0657916534692049, + "scriptingMs": 0.00016667763702571392, + "semaphoreWaitMs": 0.00020832521840929985, + "smoothedFrameMs": 8.323338984822234, + "streamingRegionMs": 0.00120833283290267, + "submitMs": 0.008083326974883676, + "updateMs": 0.4175000067334622 + } + }, + "measureSeconds": 15, + "notes": "redplayer.untold with the running clip; measures the animation system and GPU skinning.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.990734916675137, + "frameBudgetMs": 12.5, + "frames": 1801, + "framesOverBudget": 0, + "gpuPassMeanMs": { + "Block Scan Pass": 0.015838977222222196, + "Compact and Stream Pass": 0.010527443888888906, + "Copy Opaque Depth for Gaussian Occlusion": 0.06240952388888884, + "Copy Opaque Depth for HZB": 0.06055892166666668, + "G-Buffer + Light Pass (TBDR)": 0.39934781777777845, + "Gaussian Pass": 0.5639132361111119, + "Grid Pass": 0.23094499277777808, + "HZB Build Mip 0": 0.34982720444444454, + "HZB Build Mip 1": 0.0657342605555556, + "HZB Build Mip 10": 0.011880018888888878, + "HZB Build Mip 2": 0.023444740555555583, + "HZB Build Mip 3": 0.016003627777777828, + "HZB Build Mip 4": 0.013530129999999994, + "HZB Build Mip 5": 0.012882871111111098, + "HZB Build Mip 6": 0.01187811555555557, + "HZB Build Mip 7": 0.012686277777777805, + "HZB Build Mip 8": 0.01216999722222222, + "HZB Build Mip 9": 0.01132880611111111, + "HZB Occlusion Culling pass": 0.022272708333333363, + "Local Scan Pass": 0.01997584277777772, + "Mark Visible Pass": 0.023161619444444427, + "Output Transform Pass": 0.3741455016666666, + "Post-Processing Pass": 0.23537627000000047, + "Pre Composite Pass": 0.3161726316666667, + "Shadow Cascade 0": 0.17956388499999967, + "Shadow Cascade 1": 0.1690363944444448, + "Transparency Pass": 0.1862558866666666 + }, + "meanFrameMs": 8.333283875485069, + "meanGPUExecutionMs": 2.2650290276214946, + "missedDeadlines": 0, + "p50FrameMs": 8.327417075634003, + "p95FrameMs": 9.313416667282581, + "p99FrameMs": 9.433208033442497, + "peakGPUAllocatedBytes": 649674752, + "worstFrameMs": 9.82670858502388, + "worstThermalState": 0 + }, + "title": "16 skinned characters running", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/gaussian.json b/perf/baselines/Mac16_5/macOS/gaussian.json new file mode 100644 index 000000000..86cfd76af --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/gaussian.json @@ -0,0 +1,396 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "gaussian.jsonl", + "id": "gaussian", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 256, + "frustumTested": 256, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 256, + "occlusionTested": 256, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1800, + "gpuPasses": { + "frameIndex": 1801, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.021417 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.060166 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.014708 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.01525 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.028709 + }, + { + "count": 1, + "label": "Gaussian Frustum Culling", + "ms": 0.096166 + }, + { + "count": 1, + "label": "Gaussian Preprocess", + "ms": 0.033083 + }, + { + "count": 1, + "label": "Radix Sort", + "ms": 0.45725 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.174417 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.050667 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.111292 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 1.0695 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.066542 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 1.265375 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.056417 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.380416 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.343875 + }, + { + "count": 1, + "label": "Post-Processing Pass", + "ms": 0.111375 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.146833 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.251625 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.051958 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.01575 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.008 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.009875 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.009291 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.0095 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.009417 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.009667 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.009709 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.008583 + } + ], + "passesSkipped": 0, + "totalMs": 4.896832999999999 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 121, + "framesOverBudget": 0, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1801, + "histogram": [ + 0, + 365, + 1436, + 0, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 9.72025003284216 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 648036352, + "isUnderPressure": false, + "meshMemoryBytes": 3306392, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 258, + "utilizationPercent": 7.159871165640652e-05 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 256, + "drawCallsShadow": 366, + "drawCallsTotal": 629, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 124623, + "visibleInstances": 256 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 257, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": true, + "updateWorkMs": 0.1710653305053711, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 133038.71068600003, + "timing": { + "animationMs": 0.0019166618585586548, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0017499842215329409, + "cullingMs": 0.19245833391323686, + "customSystemsMs": 0, + "encodeMs": 1.8227499967906624, + "extensionsUpdateMs": 0.0017499842215329409, + "frameTotalMs": 8.519458584487438, + "gameUpdateMs": 0.054750009439885616, + "geometryStreamingMs": 0.17333333380520344, + "gpuExecutionMs": 2.8509999974630773, + "gpuFrameCadenceMs": 9.638416668167338, + "lodMs": 0.0021250161807984114, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.2584166650194675, + "renderTotalMs": 5.631958309095353, + "scenegraphMs": 0.09825002052821219, + "scriptingMs": 0.0009166542440652847, + "semaphoreWaitMs": 0.0003333261702209711, + "smoothedFrameMs": 8.35369162571927, + "streamingRegionMs": 0.001374981366097927, + "submitMs": 0.00845832983031869, + "updateMs": 0.1687916519585997 + } + }, + "measureSeconds": 15, + "notes": "test_gaussians.ply (small) plus a 16x16 grid; exercises the sort pipeline, not splat count.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.992544791661203, + "frameBudgetMs": 12.5, + "frames": 1801, + "framesOverBudget": 0, + "gpuPassMeanMs": { + "Block Scan Pass": 0.057722041643531403, + "Compact and Stream Pass": 0.03660256579677954, + "Copy Opaque Depth for Gaussian Occlusion": 0.07394986451971129, + "Copy Opaque Depth for HZB": 0.061220394780677395, + "G-Buffer + Light Pass (TBDR)": 0.5367001871182673, + "Gaussian Frustum Culling": 0.14691374902831747, + "Gaussian Pass": 0.5734694353137145, + "Gaussian Preprocess": 0.061405278734036575, + "Grid Pass": 0.47163356857301436, + "HZB Build Mip 0": 0.37824188561910094, + "HZB Build Mip 1": 0.05621375069405891, + "HZB Build Mip 10": 0.010453197112715168, + "HZB Build Mip 2": 0.02094383120488615, + "HZB Build Mip 3": 0.013187270405330428, + "HZB Build Mip 4": 0.011215890061077195, + "HZB Build Mip 5": 0.010406851193781255, + "HZB Build Mip 6": 0.010334520821765694, + "HZB Build Mip 7": 0.01036684675180458, + "HZB Build Mip 8": 0.010294277068295433, + "HZB Build Mip 9": 0.010563957245974448, + "HZB Occlusion Culling pass": 0.05035325985563576, + "Local Scan Pass": 0.10652645474736276, + "Mark Visible Pass": 0.20184244031093876, + "Output Transform Pass": 0.3012305635757917, + "Post-Processing Pass": 0.1968112109938924, + "Pre Composite Pass": 0.2893822065519154, + "Radix Sort": 0.4973709405885617, + "Shadow Cascade 0": 0.21276293947806782, + "Shadow Cascade 1": 0.19056878067740152, + "Transparency Pass": 0.16417267184897294 + }, + "meanFrameMs": 8.333416052187468, + "meanGPUExecutionMs": 2.7516523458296045, + "missedDeadlines": 0, + "p50FrameMs": 8.332625031471252, + "p95FrameMs": 9.311875328421593, + "p99FrameMs": 9.674250148236752, + "peakGPUAllocatedBytes": 648036352, + "worstFrameMs": 10.519708506762981, + "worstThermalState": 0 + }, + "title": "Gaussian splat with primitives", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/lights-64.json b/perf/baselines/Mac16_5/macOS/lights-64.json new file mode 100644 index 000000000..849afe4b9 --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/lights-64.json @@ -0,0 +1,390 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "lights-64.jsonl", + "id": "lights-64", + "lastSnapshot": { + "batching": { + "batchGroupCount": 237, + "batchedMeshCount": 1005, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 1024, + "frustumTested": 1024, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 1024, + "occlusionTested": 1024, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1795, + "gpuPasses": { + "frameIndex": 1796, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.008833 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.027541 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.008542 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.004333 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.039708 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.621958 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.027542 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.01625 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 0", + "ms": 0.11375 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 1", + "ms": 0.227959 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 1.296666 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.239875 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.256583 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.047 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.342916 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.352959 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 0.299292 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.219416 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.121333 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.102833 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.035333 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.016875 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.01525 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.016667 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.015167 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.014167 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.01225 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.016125 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.016 + } + ], + "passesSkipped": 0, + "totalMs": 4.533122999999999 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 121, + "framesOverBudget": 5, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1796, + "histogram": [ + 0, + 444, + 1342, + 10, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 10.331541299819946 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 521420800, + "isUnderPressure": false, + "meshMemoryBytes": 12216512, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 1089, + "utilizationPercent": 0.0002645440981723368 + }, + "render": { + "drawCallsBatched": 708, + "drawCallsOpaque": 256, + "drawCallsShadow": 471, + "drawCallsTotal": 734, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 589609, + "visibleInstances": 1024 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 1089, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 132963.6275057917, + "timing": { + "animationMs": 0.0009999785106629133, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0013750104699283838, + "cullingMs": 0.3834583330899477, + "customSystemsMs": 0, + "encodeMs": 2.8640000091399997, + "extensionsUpdateMs": 0.00100000761449337, + "frameTotalMs": 8.333458565175533, + "gameUpdateMs": 0.03825000021606684, + "geometryStreamingMs": 0.0014583347365260124, + "gpuExecutionMs": 3.4927500237245113, + "gpuFrameCadenceMs": 9.14804165950045, + "lodMs": 0.0019166618585586548, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.38691668305546045, + "renderTotalMs": 6.7863333388231695, + "scenegraphMs": 0.22016666480340064, + "scriptingMs": 0.00037500285543501377, + "semaphoreWaitMs": 0.0003333261702209711, + "smoothedFrameMs": 8.331048612793287, + "streamingRegionMs": 0.0012500095181167126, + "submitMs": 0.010833347914740443, + "updateMs": 0.26904165861196816 + } + }, + "measureSeconds": 15, + "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.99163612499251, + "frameBudgetMs": 12.5, + "frames": 1796, + "framesOverBudget": 5, + "gpuPassMeanMs": { + "Batched Shadow Cascade 0": 0.18360815534521152, + "Batched Shadow Cascade 1": 0.1506465105790647, + "Block Scan Pass": 0.018695265590200433, + "Compact and Stream Pass": 0.01767515423162587, + "Copy Opaque Depth for Gaussian Occlusion": 0.039060067928730464, + "Copy Opaque Depth for HZB": 0.045726726614699294, + "G-Buffer + Light Pass (TBDR)": 1.1641484838530087, + "Gaussian Pass": 0.23502248106904258, + "Grid Pass": 0.1938135373051224, + "HZB Build Mip 0": 0.1858474426503339, + "HZB Build Mip 1": 0.10158815478841879, + "HZB Build Mip 10": 0.010518663697104693, + "HZB Build Mip 2": 0.03798720601336306, + "HZB Build Mip 3": 0.014715400890868573, + "HZB Build Mip 4": 0.011423329064588013, + "HZB Build Mip 5": 0.013088639198218252, + "HZB Build Mip 6": 0.01876569097995545, + "HZB Build Mip 7": 0.020235301781737186, + "HZB Build Mip 8": 0.01825441982182627, + "HZB Build Mip 9": 0.013184029510022262, + "HZB Occlusion Culling pass": 0.034824280623608, + "Local Scan Pass": 0.02468478786191541, + "Mark Visible Pass": 0.021555864699331823, + "Output Transform Pass": 0.36320841369710405, + "Post-Processing Pass": 0.4912179020044542, + "Pre Composite Pass": 0.2857132661469934, + "Shadow Cascade 0": 0.039339065701559095, + "Shadow Cascade 1": 0.031155091870824127, + "Transparency Pass": 0.09773111636971063 + }, + "meanFrameMs": 8.356517791397504, + "meanGPUExecutionMs": 3.0345159839250964, + "missedDeadlines": 0, + "p50FrameMs": 8.332458324730396, + "p95FrameMs": 10.196291841566563, + "p99FrameMs": 10.966208763420582, + "peakGPUAllocatedBytes": 521420800, + "worstFrameMs": 16.650082543492317, + "worstThermalState": 0 + }, + "title": "1024 primitives, static batching, 64 point lights", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/postfx.json b/perf/baselines/Mac16_5/macOS/postfx.json new file mode 100644 index 000000000..1c4a29e4f --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/postfx.json @@ -0,0 +1,402 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "postfx.jsonl", + "id": "postfx", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 1024, + "frustumTested": 1024, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 1024, + "occlusionTested": 1024, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1765, + "gpuPasses": { + "frameIndex": 1766, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.036 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.005416 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.006042 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.004 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.0165 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.055584 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.007417 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.06525 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 0.360375 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.046416 + }, + { + "count": 1, + "label": "SSAO Low-Res Pass", + "ms": 0.074375 + }, + { + "count": 1, + "label": "SSAO Bilateral Blur Horizontal", + "ms": 0.025958 + }, + { + "count": 1, + "label": "SSAO Bilateral Blur Vertical", + "ms": 0.025209 + }, + { + "count": 1, + "label": "SSAO Upsample Pass", + "ms": 0.041667 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.006458 + }, + { + "count": 17, + "label": "Post-Processing Pass", + "ms": 2.2360390000000003 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.026709 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.167 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.193375 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.114833 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.120209 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.083959 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.022958 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.016417 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.014708 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.0155 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.014375 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.014166 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.014458 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.004333 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.004791 + } + ], + "passesSkipped": 0, + "totalMs": 3.8404969999999996 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 119, + "framesOverBudget": 21, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1766, + "histogram": [ + 0, + 576, + 1142, + 48, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 11.291916482150555 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 649543680, + "isUnderPressure": false, + "meshMemoryBytes": 12096992, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 1029, + "utilizationPercent": 0.00026195592363364995 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 1024, + "drawCallsShadow": 995, + "drawCallsTotal": 2045, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 403287, + "visibleInstances": 1024 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 1029, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 132982.40647600003, + "timing": { + "animationMs": 0.0010416842997074127, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0009166542440652847, + "cullingMs": 0.35524999839253724, + "customSystemsMs": 0, + "encodeMs": 7.233083306346089, + "extensionsUpdateMs": 0.0010416842997074127, + "frameTotalMs": 8.83129145950079, + "gameUpdateMs": 0.029624992748722434, + "geometryStreamingMs": 0.0012500095181167126, + "gpuExecutionMs": 3.284666658146307, + "gpuFrameCadenceMs": 8.792208333034068, + "lodMs": 0.0017916900105774403, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.364166684448719, + "renderTotalMs": 7.7799166610930115, + "scenegraphMs": 0.19216665532439947, + "scriptingMs": 0.0001666485331952572, + "semaphoreWaitMs": 0.00037500285543501377, + "smoothedFrameMs": 9.298219438642263, + "streamingRegionMs": 0.0009583309292793274, + "submitMs": 0.016083329683169723, + "updateMs": 0.23141666315495968 + } + }, + "measureSeconds": 15, + "notes": "Same geometry as primitives-1k; the difference is the full-screen pass chain.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.989835000014864, + "frameBudgetMs": 12.5, + "frames": 1766, + "framesOverBudget": 21, + "gpuPassMeanMs": { + "Block Scan Pass": 0.009628169875424726, + "Compact and Stream Pass": 0.008737910532276398, + "Copy Opaque Depth for Gaussian Occlusion": 0.0333434966024915, + "Copy Opaque Depth for HZB": 0.06962422253680617, + "G-Buffer + Light Pass (TBDR)": 0.43896250849377094, + "Gaussian Pass": 0.21271007757644406, + "Grid Pass": 0.14476782106455272, + "HZB Build Mip 0": 0.16945612910532287, + "HZB Build Mip 1": 0.05675881087202716, + "HZB Build Mip 10": 0.013074579275198188, + "HZB Build Mip 2": 0.023787618346545913, + "HZB Build Mip 3": 0.015557006795016991, + "HZB Build Mip 4": 0.01302609399773499, + "HZB Build Mip 5": 0.012724159682899224, + "HZB Build Mip 6": 0.013393057757644366, + "HZB Build Mip 7": 0.012162328425821043, + "HZB Build Mip 8": 0.0130427531143828, + "HZB Build Mip 9": 0.012311932616081534, + "HZB Occlusion Culling pass": 0.02390343318233293, + "Local Scan Pass": 0.010902743488108747, + "Mark Visible Pass": 0.022200477349943376, + "Output Transform Pass": 0.17669609852774565, + "Post-Processing Pass": 1.9406931342015912, + "Pre Composite Pass": 0.22469228935447333, + "SSAO Bilateral Blur Horizontal": 0.08901087429218588, + "SSAO Bilateral Blur Vertical": 0.06422502378255944, + "SSAO Low-Res Pass": 0.13638736296715742, + "SSAO Upsample Pass": 0.07998365855039641, + "Shadow Cascade 0": 0.0393915469988675, + "Shadow Cascade 1": 0.12998300849377106, + "Transparency Pass": 0.030185485277463196 + }, + "meanFrameMs": 8.497975771141366, + "meanGPUExecutionMs": 3.6787869007634377, + "missedDeadlines": 0, + "p50FrameMs": 8.275041356682777, + "p95FrameMs": 10.354208759963512, + "p99FrameMs": 13.015874661505222, + "peakGPUAllocatedBytes": 649543680, + "worstFrameMs": 16.476167365908623, + "worstThermalState": 0 + }, + "title": "1024 primitives with SSAO, bloom, depth of field and SMAA", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/primitives-10k.json b/perf/baselines/Mac16_5/macOS/primitives-10k.json new file mode 100644 index 000000000..c39572e2e --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/primitives-10k.json @@ -0,0 +1,390 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "primitives-10k.jsonl", + "id": "primitives-10k", + "lastSnapshot": { + "batching": { + "batchGroupCount": 1016, + "batchedMeshCount": 9992, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 90, + "frustumPassed": 9910, + "frustumTested": 10000, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 9910, + "occlusionTested": 9910, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 460, + "gpuPasses": { + "frameIndex": 461, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.005917 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.005333 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.006917 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.005083 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.024917 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.061542 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.008167 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.008292 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 0", + "ms": 0.167916 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 1", + "ms": 0.431125 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 1.164542 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.030917 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.036042 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.027458 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.1115 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.226584 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 0.263 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.197792 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.095875 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.128667 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.063458 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.01775 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.005333 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.005625 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.024 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.024625 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.027375 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.011084 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.004917 + } + ], + "passesSkipped": 0, + "totalMs": 3.1917529999999994 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 31, + "framesOverBudget": 461, + "framesOverBudgetLastSecond": 31, + "framesSampled": 461, + "histogram": [ + 0, + 0, + 0, + 0, + 381, + 80, + 0 + ], + "worstFrameMsLastSecond": 51.80862545967102 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 615792640, + "isUnderPressure": false, + "meshMemoryBytes": 118162936, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 10005, + "utilizationPercent": 0.002558775246143341 + }, + "render": { + "drawCallsBatched": 2227, + "drawCallsOpaque": 1024, + "drawCallsShadow": 1211, + "drawCallsTotal": 2242, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 4474285, + "visibleInstances": 9910 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 10005, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 132944.25919933335, + "timing": { + "animationMs": 0.0008333299774676561, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0011666561476886272, + "cullingMs": 3.7482500192709267, + "customSystemsMs": 0.0003332970663905144, + "encodeMs": 18.3108750206884, + "extensionsUpdateMs": 0.0008750066626816988, + "frameTotalMs": 31.404290348291397, + "gameUpdateMs": 0.03745831781998277, + "geometryStreamingMs": 0.001541659003123641, + "gpuExecutionMs": 2.5890833348967135, + "gpuFrameCadenceMs": 27.99512498313561, + "lodMs": 0.003083318006247282, + "physicsMs": 0.0009166542440652847, + "physicsStepCount": 2, + "renderPrepMs": 3.753791650524363, + "renderTotalMs": 22.194250021129847, + "scenegraphMs": 1.6188750159926713, + "scriptingMs": 0.00033335527405142784, + "semaphoreWaitMs": 0.0002916785888373852, + "smoothedFrameMs": 32.8032444541653, + "streamingRegionMs": 0.0008750066626816988, + "submitMs": 0.008333328878507018, + "updateMs": 1.6742916777729988 + } + }, + "measureSeconds": 15, + "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.95815895832493, + "frameBudgetMs": 12.5, + "frames": 461, + "framesOverBudget": 461, + "gpuPassMeanMs": { + "Batched Shadow Cascade 0": 0.1954524967462042, + "Batched Shadow Cascade 1": 0.6238593926247287, + "Block Scan Pass": 0.015771598698481572, + "Compact and Stream Pass": 0.01698825813449024, + "Copy Opaque Depth for Gaussian Occlusion": 0.04886833188720169, + "Copy Opaque Depth for HZB": 0.058826355748373124, + "G-Buffer + Light Pass (TBDR)": 1.772278639913231, + "Gaussian Pass": 0.2771464316702818, + "Grid Pass": 0.25485357049891505, + "HZB Build Mip 0": 0.20087906724511911, + "HZB Build Mip 1": 0.11792911279826461, + "HZB Build Mip 10": 0.011721101952277657, + "HZB Build Mip 2": 0.051800854663774425, + "HZB Build Mip 3": 0.021650924078091067, + "HZB Build Mip 4": 0.012233826464208241, + "HZB Build Mip 5": 0.011373208242950109, + "HZB Build Mip 6": 0.015751086767895887, + "HZB Build Mip 7": 0.021237342733188724, + "HZB Build Mip 8": 0.020523301518438182, + "HZB Build Mip 9": 0.01633655314533623, + "HZB Occlusion Culling pass": 0.041280251626898086, + "Local Scan Pass": 0.024477444685466403, + "Mark Visible Pass": 0.033417225596529265, + "Output Transform Pass": 0.3524678416485901, + "Post-Processing Pass": 0.4936314186550975, + "Pre Composite Pass": 0.2816119804772232, + "Shadow Cascade 0": 0.02275878524945769, + "Shadow Cascade 1": 0.017315700650759203, + "Transparency Pass": 0.12060769631236454 + }, + "meanFrameMs": 32.589466179723075, + "meanGPUExecutionMs": 4.095099150158219, + "missedDeadlines": 0, + "p50FrameMs": 30.894041061401367, + "p95FrameMs": 50.629500299692154, + "p99FrameMs": 52.89120972156525, + "peakGPUAllocatedBytes": 615792640, + "worstFrameMs": 54.943665862083435, + "worstThermalState": 0 + }, + "title": "10000 primitives, static batching, 4 point lights", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/primitives-1k.json b/perf/baselines/Mac16_5/macOS/primitives-1k.json new file mode 100644 index 000000000..0d8bc0991 --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/primitives-1k.json @@ -0,0 +1,378 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "primitives-1k.jsonl", + "id": "primitives-1k", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 1024, + "frustumTested": 1024, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 1024, + "occlusionTested": 1024, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1796, + "gpuPasses": { + "frameIndex": 1796, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.006416 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.007166 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.009875 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.00625 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.024958 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.108834 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.027625 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.594875 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 2.858292 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.306291 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.313584 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.067417 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.522292 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.574416 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 1.076042 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.97025 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.200292 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.034042 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.056791 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.04375 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.0255 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.025583 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.02125 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.021916 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.019167 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.0255 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.021917 + } + ], + "passesSkipped": 0, + "totalMs": 7.970291000000001 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 121, + "framesOverBudget": 3, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1797, + "histogram": [ + 0, + 747, + 1043, + 6, + 1, + 0, + 0 + ], + "worstFrameMsLastSecond": 10.723625309765339 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 486490112, + "isUnderPressure": false, + "meshMemoryBytes": 12098984, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 1030, + "utilizationPercent": 0.0002619990846142173 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 1024, + "drawCallsShadow": 987, + "drawCallsTotal": 2018, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 402025, + "visibleInstances": 1024 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 1030, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 132925.48781458335, + "timing": { + "animationMs": 0.0006666523404419422, + "batchingRebuildMs": 0, + "batchingTickMs": 0.000666681444272399, + "cullingMs": 0.3110416582785547, + "customSystemsMs": 0.000500003807246685, + "encodeMs": 6.1249999853316694, + "extensionsUpdateMs": 0.0008333590812981129, + "frameTotalMs": 7.797374855726957, + "gameUpdateMs": 0.020833336748182774, + "geometryStreamingMs": 0.0009583309292793274, + "gpuExecutionMs": 6.427833315683529, + "gpuFrameCadenceMs": 12.517958326498047, + "lodMs": 0.00200001522898674, + "physicsMs": 0.0008333299774676561, + "physicsStepCount": 1, + "renderPrepMs": 0.3139583277516067, + "renderTotalMs": 8.16037500044331, + "scenegraphMs": 0.1741666637826711, + "scriptingMs": 0.0002500019036233425, + "semaphoreWaitMs": 0, + "smoothedFrameMs": 8.330604123572508, + "streamingRegionMs": 0.000666681444272399, + "submitMs": 0.008125003660097718, + "updateMs": 0.21033332450315356 + } + }, + "measureSeconds": 15, + "notes": "One entity and draw per primitive; measures per-entity CPU cost and encoder overhead.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.98875758331269, + "frameBudgetMs": 12.5, + "frames": 1797, + "framesOverBudget": 3, + "gpuPassMeanMs": { + "Block Scan Pass": 0.016658895322939887, + "Compact and Stream Pass": 0.014880027839643577, + "Copy Opaque Depth for Gaussian Occlusion": 0.060346883630289436, + "Copy Opaque Depth for HZB": 0.1172458079064588, + "G-Buffer + Light Pass (TBDR)": 0.7966720718262786, + "Gaussian Pass": 0.5607875083518942, + "Grid Pass": 0.32361982238307346, + "HZB Build Mip 0": 0.2810054471046767, + "HZB Build Mip 1": 0.10631055623608014, + "HZB Build Mip 10": 0.017910079064587952, + "HZB Build Mip 2": 0.044952980512249344, + "HZB Build Mip 3": 0.02445456904231624, + "HZB Build Mip 4": 0.02146364922048997, + "HZB Build Mip 5": 0.019346375278396458, + "HZB Build Mip 6": 0.01973552227171495, + "HZB Build Mip 7": 0.020053766146993293, + "HZB Build Mip 8": 0.019837365812917573, + "HZB Build Mip 9": 0.019498061247216036, + "HZB Occlusion Culling pass": 0.03181210857461022, + "Local Scan Pass": 0.023309097438752837, + "Mark Visible Pass": 0.018762696547884185, + "Output Transform Pass": 0.46123238195991045, + "Post-Processing Pass": 0.5848721820712705, + "Pre Composite Pass": 0.3320783051224939, + "Shadow Cascade 0": 0.02849255512249453, + "Shadow Cascade 1": 0.30190513585746104, + "Transparency Pass": 0.2327666180400889 + }, + "meanFrameMs": 8.351642091156844, + "meanGPUExecutionMs": 3.2210675343551687, + "missedDeadlines": 0, + "p50FrameMs": 8.214708417654037, + "p95FrameMs": 10.11549960821867, + "p99FrameMs": 10.813833214342594, + "peakGPUAllocatedBytes": 486490112, + "worstFrameMs": 31.519915908575058, + "worstThermalState": 0 + }, + "title": "1024 primitives, 4 point lights", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/stadium.json b/perf/baselines/Mac16_5/macOS/stadium.json new file mode 100644 index 000000000..07bd2f48a --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/stadium.json @@ -0,0 +1,378 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 0226477c", + "render": { + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runID": "20260920-212202-macos", + "scene": { + "file": "stadium.jsonl", + "id": "stadium", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 16, + "frustumTested": 16, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 16, + "occlusionTested": 16, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1796, + "gpuPasses": { + "frameIndex": 1797, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.04825 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.005084 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.006583 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.003875 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.013208 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.063042 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.042583 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.031125 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 0.436083 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.030459 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.468167 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.028417 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.033417 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.224417 + }, + { + "count": 1, + "label": "Post-Processing Pass", + "ms": 0.637875 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.238375 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.262417 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.771625 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.052958 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.006 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.005458 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.005334 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.004917 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.00525 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.005334 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.005292 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.005167 + } + ], + "passesSkipped": 0, + "totalMs": 3.4407119999999995 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 121, + "framesOverBudget": 4, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1797, + "histogram": [ + 1, + 306, + 1485, + 4, + 1, + 0, + 0 + ], + "worstFrameMsLastSecond": 9.398791939020157 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 649609216, + "isUnderPressure": false, + "meshMemoryBytes": 322788, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 378368, + "thermalState": 0, + "trackedEntityCount": 9, + "utilizationPercent": 1.5183276445895899e-05 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 8, + "drawCallsShadow": 6, + "drawCallsTotal": 20, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 4485, + "visibleInstances": 16 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 9, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 133019.94137375004, + "timing": { + "animationMs": 0.0019166618585586548, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0027916685212403536, + "cullingMs": 0.13412500265985727, + "customSystemsMs": 0, + "encodeMs": 0.662375008687377, + "extensionsUpdateMs": 0.0011250085663050413, + "frameTotalMs": 8.594291284680367, + "gameUpdateMs": 0.15462498413398862, + "geometryStreamingMs": 0.0026666675694286823, + "gpuExecutionMs": 1.8342083203606308, + "gpuFrameCadenceMs": 8.855208347085863, + "lodMs": 0.003958324668928981, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.1410833210684359, + "renderTotalMs": 7.687416684348136, + "scenegraphMs": 0.042083353037014604, + "scriptingMs": 0.00037500285543501377, + "semaphoreWaitMs": 0.000500003807246685, + "smoothedFrameMs": 8.325806974122921, + "streamingRegionMs": 0.0016666599549353123, + "submitMs": 0.02166666672565043, + "updateMs": 0.22712501231580973 + } + }, + "measureSeconds": 15, + "notes": "stadium.untold and grass.untold from the render-test resources.", + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.988941250019707, + "frameBudgetMs": 12.5, + "frames": 1797, + "framesOverBudget": 4, + "gpuPassMeanMs": { + "Block Scan Pass": 0.02447151919866441, + "Compact and Stream Pass": 0.017421944351697256, + "Copy Opaque Depth for Gaussian Occlusion": 0.15683896883695064, + "Copy Opaque Depth for HZB": 0.08756588870339448, + "G-Buffer + Light Pass (TBDR)": 1.1261635943238737, + "Gaussian Pass": 1.3359295447968804, + "Grid Pass": 0.315500546466333, + "HZB Build Mip 0": 0.43504627267668333, + "HZB Build Mip 1": 0.07573602949360057, + "HZB Build Mip 10": 0.011759133555926525, + "HZB Build Mip 2": 0.027459703951029504, + "HZB Build Mip 3": 0.015316264329437999, + "HZB Build Mip 4": 0.012862551474680035, + "HZB Build Mip 5": 0.012572338341680596, + "HZB Build Mip 6": 0.012601763494713418, + "HZB Build Mip 7": 0.011303728992765777, + "HZB Build Mip 8": 0.010666003895381223, + "HZB Build Mip 9": 0.010941539788536457, + "HZB Occlusion Culling pass": 0.03034101725097387, + "Local Scan Pass": 0.04325456594323864, + "Mark Visible Pass": 0.061849728436282725, + "Output Transform Pass": 0.34160800166944877, + "Post-Processing Pass": 0.21710856928213695, + "Pre Composite Pass": 0.2995355698386199, + "Shadow Cascade 0": 0.09474082248191434, + "Shadow Cascade 1": 0.13870777239844176, + "Transparency Pass": 0.17956052643294382 + }, + "meanFrameMs": 8.350830966980924, + "meanGPUExecutionMs": 3.0920500142631187, + "missedDeadlines": 0, + "p50FrameMs": 8.332333527505398, + "p95FrameMs": 9.297333657741547, + "p99FrameMs": 9.67891700565815, + "peakGPUAllocatedBytes": 649609216, + "worstFrameMs": 17.7697092294693, + "worstThermalState": 0 + }, + "title": "Stadium and grass (textured assets)", + "warmupSeconds": 3 + } +} \ No newline at end of file From 16d1b19c5aa002b1d75f573d23faf23119376f0b Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 21:32:49 +0200 Subject: [PATCH 09/17] [Performance] Summarise per-system CPU means and per-pass GPU minimums; aggregate repeated benchmark runs Two 15 s runs of the same build on this Mac differed by 20 to 50 % in per-pass GPU and per-system CPU times: the machine idles most of each display-paced frame and its clocks drift, which only ever inflates a time. - EngineStatsRecordingSummary gains timingMeanMs (every EngineTimingStats field and the compositor phases) and gpuPassMinMs. - compare_baseline.py accepts several summaries, aggregates time metrics by their minimum and rates by their mean, compares the per-system CPU means at the frame-time tolerance and the per-pass GPU minimums instead of the means; --update stores the aggregate. - run_bench.sh --repeat N runs the scene set N times and passes every summary to the comparer; the README explains the noise and the remedy. --- Examples/PerfBench/README.md | 11 +- .../Profiling/EngineStatsRecorder.swift | 33 ++++ .../EngineStatsMonitorTests.swift | 3 + scripts/perf/compare_baseline.py | 165 +++++++++++------- scripts/perf/run_bench.sh | 12 +- 5 files changed, 160 insertions(+), 64 deletions(-) diff --git a/Examples/PerfBench/README.md b/Examples/PerfBench/README.md index 47c363537..94cbe27c2 100644 --- a/Examples/PerfBench/README.md +++ b/Examples/PerfBench/README.md @@ -52,8 +52,15 @@ scripts/perf/run_bench.sh visionos --device --xctrace "Metal System Trace Runs land in `perf/results/-/` with `.jsonl`, `summary.json` and the console log. The comparison prints one block per scene and exits non-zero on a regression (10 % -on frame-time and GPU-time metrics, 0.5 points on the over-budget and missed-deadline rates, 20 % -on a per-pass GPU mean). +on frame-time, GPU-time and per-system CPU metrics, 0.5 points on the over-budget and +missed-deadline rates, 20 % on a per-pass GPU minimum). + +Single runs are noisy on a machine that idles most of each frame: CPU and GPU clocks drift between +runs and the same build can differ by 20 to 50 % in a per-pass or per-system time. `--repeat 3` +runs the scene set three times and the comparer aggregates by the minimum of each time metric (a +clock drift only ever inflates a time) and the mean of each rate; record the baseline the same way. +The CPU-bound scenes (`primitives-10k`) are the least noisy; for the others read +`timingMeanMs` and `gpuPassMinMs` rather than frame time. The app can also be opened in Xcode (`xcodegen generate`, then `PerfBench.xcodeproj`) and started from its Start button; it reads its configuration from the environment: diff --git a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift index c00a6be83..a5bf866a6 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift @@ -40,6 +40,12 @@ public struct EngineStatsRecordingSummary: Codable, Equatable, Sendable { public var deadlineSamples: Int = 0 /// Mean GPU time per pass label over the frames in which the pass appeared. public var gpuPassMeanMs: [String: Double] = [:] + /// Minimum GPU time per pass label. GPU clocks drift on a lightly loaded device, which only + /// ever inflates a pass; the minimum is the robust number to compare across runs. + public var gpuPassMinMs: [String: Double] = [:] + /// Mean of the per-frame CPU timing fields (`EngineTimingStats` and the compositor phases), + /// keyed by field name: what a CPU-side optimization changes. + public var timingMeanMs: [String: Double] = [:] public var worstThermalState: Int = 0 public var peakGPUAllocatedBytes: Int = 0 @@ -72,6 +78,9 @@ public final class EngineStatsRecorder: @unchecked Sendable { private var gpuExecutionSum = 0.0 private var gpuExecutionSamples = 0 private var gpuPassSums: [String: (sum: Double, samples: Int)] = [:] + private var gpuPassMins: [String: Double] = [:] + private var timingSums: [String: Double] = [:] + private var timingSamples = 0 private var firstTimestamp: Double? private var lastTimestamp: Double = 0.0 private var lastWrittenSecond: Double = -1.0 @@ -134,7 +143,25 @@ public final class EngineStatsRecorder: @unchecked Sendable { for pass in snapshot.gpuPasses.passes { let entry = gpuPassSums[pass.label] ?? (0.0, 0) gpuPassSums[pass.label] = (entry.sum + pass.totalMs, entry.samples + 1) + gpuPassMins[pass.label] = min(gpuPassMins[pass.label] ?? .greatestFiniteMagnitude, pass.totalMs) } + let t = snapshot.timing + let c = snapshot.compositor + let fields: [(String, Double)] = [ + ("updateMs", t.updateMs), ("renderTotalMs", t.renderTotalMs), ("renderPrepMs", t.renderPrepMs), + ("encodeMs", t.encodeMs), ("submitMs", t.submitMs), ("cullingMs", t.cullingMs), + ("streamingRegionMs", t.streamingRegionMs), ("geometryStreamingMs", t.geometryStreamingMs), + ("batchingTickMs", t.batchingTickMs), ("semaphoreWaitMs", t.semaphoreWaitMs), + ("scenegraphMs", t.scenegraphMs), ("extensionsUpdateMs", t.extensionsUpdateMs), ("lodMs", t.lodMs), + ("animationMs", t.animationMs), ("scriptingMs", t.scriptingMs), ("physicsMs", t.physicsMs), + ("customSystemsMs", t.customSystemsMs), ("gameUpdateMs", t.gameUpdateMs), + ("compositorUpdateMs", c.updateMs), ("compositorInputSlackMs", c.inputSlackMs), + ("compositorSubmissionMs", c.submissionMs), ("compositorDeadlineMarginMs", c.deadlineMarginMs), + ] + for (name, value) in fields { + timingSums[name, default: 0.0] += value + } + timingSamples += 1 if snapshot.timestampSeconds > 0 { if firstTimestamp == nil { firstTimestamp = snapshot.timestampSeconds @@ -170,6 +197,12 @@ public final class EngineStatsRecorder: @unchecked Sendable { for (label, entry) in gpuPassSums where entry.samples > 0 { result.gpuPassMeanMs[label] = entry.sum / Double(entry.samples) } + result.gpuPassMinMs = gpuPassMins + if timingSamples > 0 { + for (name, sum) in timingSums { + result.timingMeanMs[name] = sum / Double(timingSamples) + } + } result.worstThermalState = lastSnapshotMaxThermal result.peakGPUAllocatedBytes = peakGPUAllocated return result diff --git a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift index b3c3fd34c..aa13770d6 100644 --- a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift +++ b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift @@ -491,6 +491,9 @@ final class EngineStatsMonitorTests: XCTestCase { XCTAssertEqual(summary.durationSeconds, 1.0, accuracy: 1e-9) XCTAssertEqual(summary.meanGPUExecutionMs, 10.0, accuracy: 1e-9) XCTAssertEqual(summary.gpuPassMeanMs["Model Pass"] ?? 0, 5.0, accuracy: 1e-9) + XCTAssertEqual(summary.gpuPassMinMs["Model Pass"] ?? 0, 2.5, accuracy: 1e-9) + XCTAssertEqual(summary.timingMeanMs["updateMs"] ?? -1, 0.0, accuracy: 1e-9) + XCTAssertEqual(summary.timingMeanMs["semaphoreWaitMs"] ?? -1, 0.0, accuracy: 1e-9) XCTAssertNil(stopEngineStatsRecording(), "Stopping twice must not report a second recording") let lines = try String(contentsOf: url, encoding: .utf8) diff --git a/scripts/perf/compare_baseline.py b/scripts/perf/compare_baseline.py index 39fba1335..b25d05b52 100755 --- a/scripts/perf/compare_baseline.py +++ b/scripts/perf/compare_baseline.py @@ -1,24 +1,34 @@ #!/usr/bin/env python3 -"""Compare a PerfBench summary.json against the stored baseline for the same device model. +"""Compare PerfBench run summaries against the stored baseline for the same device model. Baselines live in perf/baselines///.json and hold the scene result as the app wrote it, so a baseline is only ever compared with the same platform, layout, foveation and viewport. Exit status 1 means at least one metric regressed past its threshold. - compare_baseline.py perf/results//summary.json [--baselines DIR] [--update] - [--time-tolerance 0.10] [--rate-tolerance 0.005] +Several summaries may be given (repeated runs): time metrics are aggregated by their minimum +across runs, rates by their mean. On a lightly loaded machine CPU and GPU clocks drift between +runs and only ever inflate a time, so the minimum over a few runs is the stable number. + + compare_baseline.py perf/results//summary.json [more summary.json ...] + [--baselines DIR] [--update] + [--time-tolerance 0.10] [--rate-tolerance 0.005] [--pass-tolerance 0.20] """ import argparse import json import os import sys -TIME_METRICS = [ +FRAME_METRICS = [ ("p95FrameMs", "p95 frame ms"), ("p99FrameMs", "p99 frame ms"), ("meanFrameMs", "mean frame ms"), ("meanGPUExecutionMs", "mean GPU ms"), ] +# Per-system CPU means worth a line each; the rest are compared silently. +TIMING_FIELDS = [ + "updateMs", "encodeMs", "cullingMs", "scenegraphMs", "animationMs", "physicsMs", + "scriptingMs", "gameUpdateMs", "semaphoreWaitMs", "compositorUpdateMs", "compositorSubmissionMs", +] def load(path): @@ -34,91 +44,126 @@ def rate(numerator, denominator): return (numerator / denominator) if denominator else 0.0 -def scene_rates(scene): - s = scene["summary"] - frames = s.get("frames", 0) - return { - "overBudgetRate": rate(s.get("framesOverBudget", 0), frames), - "missedDeadlineRate": rate(s.get("missedDeadlines", 0), s.get("deadlineSamples", 0)), - } - - -def config_key(summary): - r = summary["render"] - return (r["platform"], r.get("layout"), r.get("foveation"), r.get("viewportWidth"), r.get("viewportHeight"), r.get("viewCount")) +def config_key(render): + return (render["platform"], render.get("layout"), render.get("foveation"), render.get("viewportWidth"), + render.get("viewportHeight"), render.get("viewCount")) + + +def aggregate(scenes): + """Fold the same scene from several runs into one record: min of times, mean of rates.""" + first = json.loads(json.dumps(scenes[0])) + s = first["summary"] + for key, _ in FRAME_METRICS: + s[key] = min(x["summary"].get(key, 0.0) for x in scenes) + s["worstFrameMs"] = min(x["summary"].get("worstFrameMs", 0.0) for x in scenes) + frames = sum(x["summary"].get("frames", 0) for x in scenes) + s["frames"] = frames + s["framesOverBudget"] = sum(x["summary"].get("framesOverBudget", 0) for x in scenes) + s["missedDeadlines"] = sum(x["summary"].get("missedDeadlines", 0) for x in scenes) + s["deadlineSamples"] = sum(x["summary"].get("deadlineSamples", 0) for x in scenes) + for field in ("gpuPassMeanMs", "gpuPassMinMs", "timingMeanMs"): + merged = {} + for x in scenes: + for label, value in x["summary"].get(field, {}).items(): + merged[label] = min(merged.get(label, value), value) + s[field] = merged + s["worstThermalState"] = max(x["summary"].get("worstThermalState", 0) for x in scenes) + first["runs"] = len(scenes) + return first def main(): ap = argparse.ArgumentParser() - ap.add_argument("summary") + ap.add_argument("summaries", nargs="+") ap.add_argument("--baselines", default=os.path.join(os.path.dirname(__file__), "..", "..", "perf", "baselines")) - ap.add_argument("--update", action="store_true", help="write this run as the baseline") - ap.add_argument("--time-tolerance", type=float, default=0.10, help="relative slack for time metrics") + ap.add_argument("--update", action="store_true", help="write these runs as the baseline") + ap.add_argument("--time-tolerance", type=float, default=0.10, help="relative slack for frame and system time metrics") ap.add_argument("--rate-tolerance", type=float, default=0.005, help="absolute slack for over-budget and missed-deadline rates") - ap.add_argument("--pass-tolerance", type=float, default=0.20, help="relative slack for per-pass GPU means") + ap.add_argument("--pass-tolerance", type=float, default=0.20, help="relative slack for per-pass GPU minimums") args = ap.parse_args() - run = load(args.summary) - model = safe_model(run["device"]["model"]) - platform = run["render"]["platform"] + runs = [load(p) for p in args.summaries] + first = runs[0] + for run in runs[1:]: + if config_key(run["render"]) != config_key(first["render"]) or run["device"]["model"] != first["device"]["model"]: + print("summaries come from different devices or render configurations; refusing to aggregate") + return 2 + scenes_by_id = {} + for run in runs: + for scene in run["scenes"]: + scenes_by_id.setdefault(scene["id"], []).append(scene) + scenes = [aggregate(group) for group in scenes_by_id.values()] + + model = safe_model(first["device"]["model"]) + platform = first["render"]["platform"] base_dir = os.path.join(args.baselines, model, platform) - print(f"run {run['runID']} label={run.get('label','')!r} device={run['device']['model']} platform={platform} " - f"viewport={run['render'].get('viewportWidth')}x{run['render'].get('viewportHeight')} x{run['render'].get('viewCount')}") + r = first["render"] + print(f"{len(runs)} run(s), label={first.get('label','')!r} device={first['device']['model']} platform={platform} " + f"viewport={r.get('viewportWidth')}x{r.get('viewportHeight')} x{r.get('viewCount')} refresh={r.get('displayRefreshHz')}") if args.update: os.makedirs(base_dir, exist_ok=True) - for scene in run["scenes"]: - record = {"render": run["render"], "device": run["device"], "label": run.get("label", ""), - "runID": run["runID"], "scene": scene} + for scene in scenes: + record = {"render": first["render"], "device": first["device"], "label": first.get("label", ""), + "runIDs": [run["runID"] for run in runs], "scene": scene} with open(os.path.join(base_dir, f"{scene['id']}.json"), "w") as f: json.dump(record, f, indent=2, sort_keys=True) - print(f"baseline written to {base_dir} for {len(run['scenes'])} scene(s)") + print(f"baseline written to {base_dir} for {len(scenes)} scene(s) from {len(runs)} run(s)") return 0 regressions = 0 - for scene in run["scenes"]: + for scene in scenes: path = os.path.join(base_dir, f"{scene['id']}.json") s = scene["summary"] - rates = scene_rates(scene) - head = f"{scene['id']}: frames {s.get('frames',0)} p95 {s.get('p95FrameMs',0):.2f} p99 {s.get('p99FrameMs',0):.2f} " \ - f"gpu {s.get('meanGPUExecutionMs',0):.2f} overBudget {rates['overBudgetRate']*100:.2f}% missed {rates['missedDeadlineRate']*100:.2f}%" + over = rate(s.get("framesOverBudget", 0), s.get("frames", 0)) + missed = rate(s.get("missedDeadlines", 0), s.get("deadlineSamples", 0)) + head = (f"{scene['id']}: frames {s.get('frames',0)} p95 {s.get('p95FrameMs',0):.2f} p99 {s.get('p99FrameMs',0):.2f} " + f"gpu {s.get('meanGPUExecutionMs',0):.2f} overBudget {over*100:.2f}% missed {missed*100:.2f}%") if not os.path.exists(path): print(f"{head} [no baseline]") continue base = load(path) - if config_key({"render": base["render"]}) != config_key(run): + if config_key(base["render"]) != config_key(first["render"]): print(f"{head} [baseline has a different render configuration, skipped]") continue b = base["scene"]["summary"] - brates = scene_rates(base["scene"]) + bover = rate(b.get("framesOverBudget", 0), b.get("frames", 0)) + bmissed = rate(b.get("missedDeadlines", 0), b.get("deadlineSamples", 0)) print(head) - for key, label in TIME_METRICS: - now, ref = s.get(key, 0.0), b.get(key, 0.0) - if ref <= 0: - continue - delta = (now - ref) / ref - flag = "REGRESSION" if delta > args.time_tolerance else ("better" if delta < -args.time_tolerance else "ok") - if flag == "REGRESSION": - regressions += 1 - print(f" {label:14s} {now:8.2f} vs {ref:8.2f} {delta*100:+6.1f}% {flag}") - for key in ("overBudgetRate", "missedDeadlineRate"): - now, ref = rates[key], brates[key] - delta = now - ref - flag = "REGRESSION" if delta > args.rate_tolerance else "ok" + + def judge(label, now, ref, tolerance, unit="ms", relative=True): + nonlocal regressions + if relative: + if ref <= 0: + return + delta = (now - ref) / ref + flag = "REGRESSION" if delta > tolerance else ("better" if delta < -tolerance else "ok") + print(f" {label:28s} {now:9.3f} vs {ref:9.3f} {unit} {delta*100:+7.1f}% {flag}") + else: + delta = now - ref + flag = "REGRESSION" if delta > tolerance else "ok" + print(f" {label:28s} {now*100:8.2f}% vs {ref*100:8.2f}% {delta*100:+6.2f}pt {flag}") if flag == "REGRESSION": regressions += 1 - print(f" {key:14s} {now*100:7.2f}% vs {ref*100:7.2f}% {delta*100:+6.2f}pt {flag}") - passes_now = s.get("gpuPassMeanMs", {}) - passes_ref = b.get("gpuPassMeanMs", {}) - for label in sorted(passes_ref): - ref = passes_ref[label] - now = passes_now.get(label) - if now is None or ref < 0.05: - continue - delta = (now - ref) / ref - if delta > args.pass_tolerance: - regressions += 1 - print(f" pass {label!r}: {now:.3f} vs {ref:.3f} ms {delta*100:+6.1f}% REGRESSION") + + for key, label in FRAME_METRICS: + judge(label, s.get(key, 0.0), b.get(key, 0.0), args.time_tolerance) + judge("overBudgetRate", over, bover, args.rate_tolerance, relative=False) + judge("missedDeadlineRate", missed, bmissed, args.rate_tolerance, relative=False) + tnow, tref = s.get("timingMeanMs", {}), b.get("timingMeanMs", {}) + for field in TIMING_FIELDS: + if field in tnow and field in tref and tref[field] >= 0.02: + judge(f"cpu {field}", tnow[field], tref[field], args.time_tolerance) + pnow, pref = s.get("gpuPassMinMs", {}), b.get("gpuPassMinMs", {}) + untimed = sorted(label for label in pref if label not in pnow and pref[label] >= 0.05) + if untimed: + # The baseline knows passes under labels this run never produced (a baseline recorded + # by another timer, or passes renamed since): those are not judged, and say so. + shown = ", ".join(untimed[:4]) + (", ..." if len(untimed) > 4 else "") + print(f" gpu passes not judged: {len(untimed)} baseline label(s) this run did not time ({shown}); re-record the baseline") + for label in sorted(pref): + if label in pnow and pref[label] >= 0.05: + judge(f"gpu min {label[:20]}", pnow[label], pref[label], args.pass_tolerance) if regressions: print(f"{regressions} regression(s)") return 1 diff --git a/scripts/perf/run_bench.sh b/scripts/perf/run_bench.sh index 0190b5f58..d920ab4f2 100755 --- a/scripts/perf/run_bench.sh +++ b/scripts/perf/run_bench.sh @@ -15,6 +15,7 @@ # --out DIR where run folders land (default: perf/results) # --label TEXT stored in summary.json (default: git describe) # --config Debug|Release (default: Release) +# --repeat N run the whole scene set N times and aggregate (min of times, mean of rates) # --xctrace TEMPLATE also record an Instruments trace, e.g. "Metal System Trace" (device runs) # --no-compare skip the baseline comparison # --update-baseline write this run as the new baseline for the device model @@ -41,6 +42,7 @@ DEVICE="" XCTRACE_TEMPLATE="" COMPARE=1 UPDATE_BASELINE=0 +REPEAT=1 while [ $# -gt 0 ]; do case "$1" in @@ -51,6 +53,7 @@ while [ $# -gt 0 ]; do --label) LABEL="$2"; shift 2 ;; --config) CONFIG="$2"; shift 2 ;; --device) DEVICE="$2"; shift 2 ;; + --repeat) REPEAT="$2"; shift 2 ;; --xctrace) XCTRACE_TEMPLATE="$2"; shift 2 ;; --no-compare) COMPARE=0; shift ;; --update-baseline) UPDATE_BASELINE=1; shift ;; @@ -83,6 +86,9 @@ xcodebuild build \ ARCHS=arm64 ONLY_ACTIVE_ARCH=YES \ -quiet +SUMMARIES=() +for ((REP = 1; REP <= REPEAT; REP++)); do +if [ "$REPEAT" -gt 1 ]; then RUN_ID="$(date +%Y%m%d-%H%M%S)-$PLATFORM-r$REP"; echo "== Repeat $REP of $REPEAT"; fi case "$PLATFORM" in macos) APP="$DERIVED/Build/Products/$CONFIG/PerfBench.app" @@ -129,9 +135,11 @@ esac RUN_DIR="$OUT_DIR/$RUN_ID" [ -f "$RUN_DIR/summary.json" ] || { echo "no summary.json in $RUN_DIR"; exit 1; } echo "== Results: $RUN_DIR" +SUMMARIES+=("$RUN_DIR/summary.json") +done if [ "$UPDATE_BASELINE" = 1 ]; then - python3 "$REPO/scripts/perf/compare_baseline.py" "$RUN_DIR/summary.json" --baselines "$BASELINES" --update + python3 "$REPO/scripts/perf/compare_baseline.py" "${SUMMARIES[@]}" --baselines "$BASELINES" --update elif [ "$COMPARE" = 1 ]; then - python3 "$REPO/scripts/perf/compare_baseline.py" "$RUN_DIR/summary.json" --baselines "$BASELINES" + python3 "$REPO/scripts/perf/compare_baseline.py" "${SUMMARIES[@]}" --baselines "$BASELINES" fi From b7d1b5eff5b7df4333f638ca6017f5f87176d1cc Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 21:46:55 +0200 Subject: [PATCH 10/17] [Chores] Replace the Mac16,5 macOS baseline with a three-run aggregate 10 s per scene, three repeats, aggregated by the minimum of each time metric; includes the per-system CPU means and per-pass GPU minimums. --- .../baselines/Mac16_5/macOS/animation-16.json | 266 +++++++++------- perf/baselines/Mac16_5/macOS/gaussian.json | 283 ++++++++++------- perf/baselines/Mac16_5/macOS/lights-64.json | 288 ++++++++++------- perf/baselines/Mac16_5/macOS/postfx.json | 290 ++++++++++------- .../Mac16_5/macOS/primitives-10k.json | 292 +++++++++++------- .../Mac16_5/macOS/primitives-1k.json | 274 +++++++++------- perf/baselines/Mac16_5/macOS/stadium.json | 274 +++++++++------- 7 files changed, 1192 insertions(+), 775 deletions(-) diff --git a/perf/baselines/Mac16_5/macOS/animation-16.json b/perf/baselines/Mac16_5/macOS/animation-16.json index 3ff986c61..3d2896526 100644 --- a/perf/baselines/Mac16_5/macOS/animation-16.json +++ b/perf/baselines/Mac16_5/macOS/animation-16.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "animation-16.jsonl", "id": "animation-16", @@ -70,170 +74,170 @@ ], "usedHZB": true }, - "frameIndex": 1800, + "frameIndex": 1197, "gpuPasses": { - "frameIndex": 1800, + "frameIndex": 1197, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.010875 + "ms": 0.521 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.142291 + "ms": 0.007125 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.01975 + "ms": 0.0135 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.009417 + "ms": 0.114333 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.025542 + "ms": 0.021208 }, { "count": 1, "label": "Grid Pass", - "ms": 0.166 + "ms": 0.438792 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.078333 + "ms": 0.04525 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.07875 + "ms": 0.043 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.490208 + "ms": 0.27875 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.071167 + "ms": 0.040959 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.595708 + "ms": 0.340041 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.064542 + "ms": 0.036208 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.075167 + "ms": 0.042083 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.281209 + "ms": 0.359625 }, { "count": 1, "label": "Post-Processing Pass", - "ms": 0.132833 + "ms": 0.125458 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.201125 + "ms": 0.166959 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.530708 + "ms": 0.547042 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.038583 + "ms": 0.028 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.016834 + "ms": 0.011542 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.011584 + "ms": 0.00775 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.010458 + "ms": 0.006834 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.010333 + "ms": 0.006958 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.010667 + "ms": 0.006667 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.010459 + "ms": 0.006833 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.010458 + "ms": 0.00675 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.0105 + "ms": 0.007167 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.010125 + "ms": 0.006416 } ], "passesSkipped": 0, - "totalMs": 3.113626 + "totalMs": 3.2362500000000005 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 120, - "framesOverBudget": 0, + "framesLastSecond": 121, + "framesOverBudget": 3, "framesOverBudgetLastSecond": 0, - "framesSampled": 1801, + "framesSampled": 1198, "histogram": [ 0, - 355, - 1446, - 0, + 362, + 832, + 4, 0, 0, 0 ], - "worstFrameMsLastSecond": 9.411750361323357 + "worstFrameMsLastSecond": 9.659666568040848 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 649674752, + "gpuAllocatedBytes": 649805824, "isUnderPressure": false, "meshMemoryBytes": 1326600, "textureBudgetBytes": 18471796080, @@ -245,8 +249,8 @@ "render": { "drawCallsBatched": 0, "drawCallsOpaque": 16, - "drawCallsShadow": 24, - "drawCallsTotal": 46, + "drawCallsShadow": 25, + "drawCallsTotal": 47, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -257,7 +261,7 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 17531, + "trianglesTotal": 17969, "visibleInstances": 16 }, "streaming": { @@ -290,86 +294,140 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": false, - "updateWorkMs": 0, + "updateTriggered": true, + "updateWorkMs": 0.06699562072753906, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133001.1765410417, + "timestampSeconds": 133746.49413062504, "timing": { - "animationMs": 0.2791666775010526, + "animationMs": 0.4928333219140768, "batchingRebuildMs": 0, - "batchingTickMs": 0.00120833283290267, - "cullingMs": 0.07700000423938036, + "batchingTickMs": 0.013041659258306026, + "cullingMs": 0.13866668450646102, "customSystemsMs": 0, - "encodeMs": 0.59870834229514, - "extensionsUpdateMs": 0.0011250085663050413, - "frameTotalMs": 7.451375015079975, - "gameUpdateMs": 0.05991666694171727, - "geometryStreamingMs": 0.00200001522898674, - "gpuExecutionMs": 1.7440833325963467, - "gpuFrameCadenceMs": 4.094624979188666, - "lodMs": 0.003374996595084667, + "encodeMs": 0.8497916569467634, + "extensionsUpdateMs": 0.001541659003123641, + "frameTotalMs": 8.87024961411953, + "gameUpdateMs": 0.12612499995157123, + "geometryStreamingMs": 0.07050001295283437, + "gpuExecutionMs": 2.1647083340212703, + "gpuFrameCadenceMs": 6.172791647259146, + "lodMs": 0.003458320861682296, "physicsMs": 0, "physicsStepCount": 0, - "renderPrepMs": 0.0826666655484587, - "renderTotalMs": 0.8604166505392641, - "scenegraphMs": 0.0657916534692049, - "scriptingMs": 0.00016667763702571392, - "semaphoreWaitMs": 0.00020832521840929985, - "smoothedFrameMs": 8.323338984822234, - "streamingRegionMs": 0.00120833283290267, - "submitMs": 0.008083326974883676, - "updateMs": 0.4175000067334622 + "renderPrepMs": 0.14600000577047467, + "renderTotalMs": 6.797750014811754, + "scenegraphMs": 0.10000000474974513, + "scriptingMs": 0.00041667954064905643, + "semaphoreWaitMs": 0.000500003807246685, + "smoothedFrameMs": 8.284629105279842, + "streamingRegionMs": 0.004791683750227094, + "submitMs": 0.014249992091208696, + "updateMs": 0.748166668927297 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "redplayer.untold with the running clip; measures the animation system and GPU skinning.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.990734916675137, + "durationSeconds": 9.988673708343413, "frameBudgetMs": 12.5, - "frames": 1801, - "framesOverBudget": 0, + "frames": 3600, + "framesOverBudget": 3, "gpuPassMeanMs": { - "Block Scan Pass": 0.015838977222222196, - "Compact and Stream Pass": 0.010527443888888906, - "Copy Opaque Depth for Gaussian Occlusion": 0.06240952388888884, - "Copy Opaque Depth for HZB": 0.06055892166666668, - "G-Buffer + Light Pass (TBDR)": 0.39934781777777845, - "Gaussian Pass": 0.5639132361111119, - "Grid Pass": 0.23094499277777808, - "HZB Build Mip 0": 0.34982720444444454, - "HZB Build Mip 1": 0.0657342605555556, - "HZB Build Mip 10": 0.011880018888888878, - "HZB Build Mip 2": 0.023444740555555583, - "HZB Build Mip 3": 0.016003627777777828, - "HZB Build Mip 4": 0.013530129999999994, - "HZB Build Mip 5": 0.012882871111111098, - "HZB Build Mip 6": 0.01187811555555557, - "HZB Build Mip 7": 0.012686277777777805, - "HZB Build Mip 8": 0.01216999722222222, - "HZB Build Mip 9": 0.01132880611111111, - "HZB Occlusion Culling pass": 0.022272708333333363, - "Local Scan Pass": 0.01997584277777772, - "Mark Visible Pass": 0.023161619444444427, - "Output Transform Pass": 0.3741455016666666, - "Post-Processing Pass": 0.23537627000000047, - "Pre Composite Pass": 0.3161726316666667, - "Shadow Cascade 0": 0.17956388499999967, - "Shadow Cascade 1": 0.1690363944444448, - "Transparency Pass": 0.1862558866666666 + "Block Scan Pass": 0.008764747500000003, + "Compact and Stream Pass": 0.005156155833333331, + "Copy Opaque Depth for Gaussian Occlusion": 0.04244916916666665, + "Copy Opaque Depth for HZB": 0.0378395575, + "G-Buffer + Light Pass (TBDR)": 0.2660044399999998, + "Gaussian Pass": 0.34758923166666666, + "Grid Pass": 0.08369763083333334, + "HZB Build Mip 0": 0.35258437499999945, + "HZB Build Mip 1": 0.05212142083333336, + "HZB Build Mip 10": 0.007064885833333325, + "HZB Build Mip 2": 0.018006421666666633, + "HZB Build Mip 3": 0.01162080083333332, + "HZB Build Mip 4": 0.010021489999999982, + "HZB Build Mip 5": 0.00851657916666668, + "HZB Build Mip 6": 0.008395797500000015, + "HZB Build Mip 7": 0.007448001666666661, + "HZB Build Mip 8": 0.007368475833333343, + "HZB Build Mip 9": 0.006942558333333319, + "HZB Occlusion Culling pass": 0.014751043333333304, + "Local Scan Pass": 0.0069991541666666575, + "Mark Visible Pass": 0.008895924166666685, + "Output Transform Pass": 0.2298006041666669, + "Post-Processing Pass": 0.15901843999999998, + "Pre Composite Pass": 0.23769827166666677, + "Shadow Cascade 0": 0.041846876666666706, + "Shadow Cascade 1": 0.043767706666666614, + "Transparency Pass": 0.05802672499999997 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.005333, + "Compact and Stream Pass": 0.002958, + "Copy Opaque Depth for Gaussian Occlusion": 0.02375, + "Copy Opaque Depth for HZB": 0.019, + "G-Buffer + Light Pass (TBDR)": 0.13925, + "Gaussian Pass": 0.094417, + "Grid Pass": 0.048083, + "HZB Build Mip 0": 0.083917, + "HZB Build Mip 1": 0.022416, + "HZB Build Mip 10": 0.003875, + "HZB Build Mip 2": 0.007916, + "HZB Build Mip 3": 0.004792, + "HZB Build Mip 4": 0.004333, + "HZB Build Mip 5": 0.004042, + "HZB Build Mip 6": 0.004, + "HZB Build Mip 7": 0.00425, + "HZB Build Mip 8": 0.004334, + "HZB Build Mip 9": 0.004, + "HZB Occlusion Culling pass": 0.00925, + "Local Scan Pass": 0.003958, + "Mark Visible Pass": 0.003375, + "Output Transform Pass": 0.084292, + "Post-Processing Pass": 0.098208, + "Pre Composite Pass": 0.174458, + "Shadow Cascade 0": 0.024291, + "Shadow Cascade 1": 0.023917, + "Transparency Pass": 0.007708 }, - "meanFrameMs": 8.333283875485069, - "meanGPUExecutionMs": 2.2650290276214946, + "meanFrameMs": 8.331988514366124, + "meanGPUExecutionMs": 1.272507916679994, "missedDeadlines": 0, - "p50FrameMs": 8.327417075634003, - "p95FrameMs": 9.313416667282581, - "p99FrameMs": 9.433208033442497, - "peakGPUAllocatedBytes": 649674752, - "worstFrameMs": 9.82670858502388, + "p50FrameMs": 8.316708728671074, + "p95FrameMs": 9.707249701023102, + "p99FrameMs": 10.15379186719656, + "peakGPUAllocatedBytes": 649805824, + "timingMeanMs": { + "animationMs": 0.4680142585014836, + "batchingTickMs": 0.0029376386857213753, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.12474729472381232, + "customSystemsMs": 0.00021405763099036746, + "encodeMs": 0.6314588538466019, + "extensionsUpdateMs": 0.001295701292956027, + "gameUpdateMs": 0.10586601433279823, + "geometryStreamingMs": 0.013736018542274771, + "lodMs": 0.003851279978817624, + "physicsMs": 0.001089508726853229, + "renderPrepMs": 0.13288186956991865, + "renderTotalMs": 0.9796556337274376, + "scenegraphMs": 0.09701119907641899, + "scriptingMs": 0.0005386751940013695, + "semaphoreWaitMs": 0.0003422843340821533, + "streamingRegionMs": 0.002215393899181214, + "submitMs": 0.014653101153043623, + "updateMs": 0.7047359488249604 + }, + "worstFrameMs": 10.473666712641716, "worstThermalState": 0 }, "title": "16 skinned characters running", diff --git a/perf/baselines/Mac16_5/macOS/gaussian.json b/perf/baselines/Mac16_5/macOS/gaussian.json index 86cfd76af..d722cae6e 100644 --- a/perf/baselines/Mac16_5/macOS/gaussian.json +++ b/perf/baselines/Mac16_5/macOS/gaussian.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "gaussian.jsonl", "id": "gaussian", @@ -70,185 +74,185 @@ ], "usedHZB": true }, - "frameIndex": 1800, + "frameIndex": 1199, "gpuPasses": { - "frameIndex": 1801, + "frameIndex": 1199, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.021417 + "ms": 0.044708 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.060166 + "ms": 0.011917 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.014708 + "ms": 0.175208 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.01525 + "ms": 0.038875 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.028709 + "ms": 0.051542 }, { "count": 1, "label": "Gaussian Frustum Culling", - "ms": 0.096166 + "ms": 0.430334 }, { "count": 1, "label": "Gaussian Preprocess", - "ms": 0.033083 + "ms": 0.139042 }, { "count": 1, "label": "Radix Sort", - "ms": 0.45725 + "ms": 0.683625 }, { "count": 1, "label": "Grid Pass", - "ms": 0.174417 + "ms": 1.115166 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.050667 + "ms": 0.799791 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.111292 + "ms": 0.066041 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 1.0695 + "ms": 0.485625 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.066542 + "ms": 0.05725 }, { "count": 1, "label": "Gaussian Pass", - "ms": 1.265375 + "ms": 0.579125 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.056417 + "ms": 0.076459 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.380416 + "ms": 0.371625 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.343875 + "ms": 0.215167 }, { "count": 1, "label": "Post-Processing Pass", - "ms": 0.111375 + "ms": 0.488 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.146833 + "ms": 0.887375 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.251625 + "ms": 0.288584 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.051958 + "ms": 0.063125 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.01575 + "ms": 0.039917 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.008 + "ms": 0.027625 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.009875 + "ms": 0.03175 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.009291 + "ms": 0.02625 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.0095 + "ms": 0.029042 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.009417 + "ms": 0.01875 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.009667 + "ms": 0.021417 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.009709 + "ms": 0.029584 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.008583 + "ms": 0.019833 } ], "passesSkipped": 0, - "totalMs": 4.896832999999999 + "totalMs": 7.312751999999999 }, "hitches": { "frameBudgetMs": 12.5, "framesLastSecond": 121, "framesOverBudget": 0, "framesOverBudgetLastSecond": 0, - "framesSampled": 1801, + "framesSampled": 1200, "histogram": [ 0, - 365, - 1436, - 0, + 344, + 855, + 1, 0, 0, 0 ], - "worstFrameMsLastSecond": 9.72025003284216 + "worstFrameMsLastSecond": 10.722416453063488 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 648036352, + "gpuAllocatedBytes": 648167424, "isUnderPressure": false, "meshMemoryBytes": 3306392, "textureBudgetBytes": 18471796080, @@ -260,8 +264,8 @@ "render": { "drawCallsBatched": 0, "drawCallsOpaque": 256, - "drawCallsShadow": 366, - "drawCallsTotal": 629, + "drawCallsShadow": 364, + "drawCallsTotal": 627, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -272,7 +276,7 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 124623, + "trianglesTotal": 125163, "visibleInstances": 256 }, "streaming": { @@ -305,89 +309,146 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": true, - "updateWorkMs": 0.1710653305053711, + "updateTriggered": false, + "updateWorkMs": 0, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133038.71068600003, + "timestampSeconds": 133774.0391064167, "timing": { - "animationMs": 0.0019166618585586548, + "animationMs": 0.002083310391753912, "batchingRebuildMs": 0, - "batchingTickMs": 0.0017499842215329409, - "cullingMs": 0.19245833391323686, - "customSystemsMs": 0, - "encodeMs": 1.8227499967906624, - "extensionsUpdateMs": 0.0017499842215329409, - "frameTotalMs": 8.519458584487438, - "gameUpdateMs": 0.054750009439885616, - "geometryStreamingMs": 0.17333333380520344, - "gpuExecutionMs": 2.8509999974630773, - "gpuFrameCadenceMs": 9.638416668167338, - "lodMs": 0.0021250161807984114, - "physicsMs": 0, - "physicsStepCount": 0, - "renderPrepMs": 0.2584166650194675, - "renderTotalMs": 5.631958309095353, - "scenegraphMs": 0.09825002052821219, - "scriptingMs": 0.0009166542440652847, - "semaphoreWaitMs": 0.0003333261702209711, - "smoothedFrameMs": 8.35369162571927, - "streamingRegionMs": 0.001374981366097927, - "submitMs": 0.00845832983031869, - "updateMs": 0.1687916519585997 + "batchingTickMs": 0.0020833394955843687, + "cullingMs": 0.17154167289845645, + "customSystemsMs": 0.0007083290256559849, + "encodeMs": 4.538750014035031, + "extensionsUpdateMs": 0.0017916609067469835, + "frameTotalMs": 8.173458278179169, + "gameUpdateMs": 0.06916667916812003, + "geometryStreamingMs": 0.0022500171326100826, + "gpuExecutionMs": 4.283125017536804, + "gpuFrameCadenceMs": 10.858624998945743, + "lodMs": 0.0025833433028310537, + "physicsMs": 0.00100000761449337, + "physicsStepCount": 1, + "renderPrepMs": 0.43033334077335894, + "renderTotalMs": 7.813041680492461, + "scenegraphMs": 0.07933334563858807, + "scriptingMs": 0.0013333337847143412, + "semaphoreWaitMs": 0.00045832712203264236, + "smoothedFrameMs": 8.38935694967707, + "streamingRegionMs": 0.0018749851733446121, + "submitMs": 0.027916685212403536, + "updateMs": 0.1696250110398978 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "test_gaussians.ply (small) plus a 16x16 grid; exercises the sort pipeline, not splat count.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.992544791661203, + "durationSeconds": 9.983395124989329, "frameBudgetMs": 12.5, - "frames": 1801, - "framesOverBudget": 0, + "frames": 3600, + "framesOverBudget": 2, "gpuPassMeanMs": { - "Block Scan Pass": 0.057722041643531403, - "Compact and Stream Pass": 0.03660256579677954, - "Copy Opaque Depth for Gaussian Occlusion": 0.07394986451971129, - "Copy Opaque Depth for HZB": 0.061220394780677395, - "G-Buffer + Light Pass (TBDR)": 0.5367001871182673, - "Gaussian Frustum Culling": 0.14691374902831747, - "Gaussian Pass": 0.5734694353137145, - "Gaussian Preprocess": 0.061405278734036575, - "Grid Pass": 0.47163356857301436, - "HZB Build Mip 0": 0.37824188561910094, - "HZB Build Mip 1": 0.05621375069405891, - "HZB Build Mip 10": 0.010453197112715168, - "HZB Build Mip 2": 0.02094383120488615, - "HZB Build Mip 3": 0.013187270405330428, - "HZB Build Mip 4": 0.011215890061077195, - "HZB Build Mip 5": 0.010406851193781255, - "HZB Build Mip 6": 0.010334520821765694, - "HZB Build Mip 7": 0.01036684675180458, - "HZB Build Mip 8": 0.010294277068295433, - "HZB Build Mip 9": 0.010563957245974448, - "HZB Occlusion Culling pass": 0.05035325985563576, - "Local Scan Pass": 0.10652645474736276, - "Mark Visible Pass": 0.20184244031093876, - "Output Transform Pass": 0.3012305635757917, - "Post-Processing Pass": 0.1968112109938924, - "Pre Composite Pass": 0.2893822065519154, - "Radix Sort": 0.4973709405885617, - "Shadow Cascade 0": 0.21276293947806782, - "Shadow Cascade 1": 0.19056878067740152, - "Transparency Pass": 0.16417267184897294 + "Block Scan Pass": 0.011558513761467866, + "Compact and Stream Pass": 0.007010944954128434, + "Copy Opaque Depth for Gaussian Occlusion": 0.07104425771476228, + "Copy Opaque Depth for HZB": 0.04869206171809836, + "G-Buffer + Light Pass (TBDR)": 0.4811368457047543, + "Gaussian Frustum Culling": 0.048873914095079206, + "Gaussian Pass": 0.4227533969974974, + "Gaussian Preprocess": 0.03707136113427852, + "Grid Pass": 0.11464033277731439, + "HZB Build Mip 0": 0.3363427989991663, + "HZB Build Mip 1": 0.057526110925771506, + "HZB Build Mip 10": 0.011832924937447892, + "HZB Build Mip 2": 0.02182210425354462, + "HZB Build Mip 3": 0.014309320266889053, + "HZB Build Mip 4": 0.012238431192660543, + "HZB Build Mip 5": 0.011578216013344455, + "HZB Build Mip 6": 0.010445649708090077, + "HZB Build Mip 7": 0.011749016680567133, + "HZB Build Mip 8": 0.011895811509591334, + "HZB Build Mip 9": 0.012171615512927447, + "HZB Occlusion Culling pass": 0.02616073561301084, + "Local Scan Pass": 0.016742093411175926, + "Mark Visible Pass": 0.02112907089241036, + "Output Transform Pass": 0.287302327773144, + "Post-Processing Pass": 0.19227929691409473, + "Pre Composite Pass": 0.2779125037531273, + "Radix Sort": 0.4084149032527101, + "Shadow Cascade 0": 0.0738084178482067, + "Shadow Cascade 1": 0.08641182652210185, + "Transparency Pass": 0.12525412343619677 }, - "meanFrameMs": 8.333416052187468, - "meanGPUExecutionMs": 2.7516523458296045, + "gpuPassMinMs": { + "Block Scan Pass": 0.005375, + "Compact and Stream Pass": 0.00325, + "Copy Opaque Depth for Gaussian Occlusion": 0.023583, + "Copy Opaque Depth for HZB": 0.01825, + "G-Buffer + Light Pass (TBDR)": 0.198334, + "Gaussian Frustum Culling": 0.023667, + "Gaussian Pass": 0.060417, + "Gaussian Preprocess": 0.013166, + "Grid Pass": 0.044334, + "HZB Build Mip 0": 0.07825, + "HZB Build Mip 1": 0.022208, + "HZB Build Mip 10": 0.003791, + "HZB Build Mip 2": 0.00775, + "HZB Build Mip 3": 0.004583, + "HZB Build Mip 4": 0.004375, + "HZB Build Mip 5": 0.003875, + "HZB Build Mip 6": 0.00375, + "HZB Build Mip 7": 0.003875, + "HZB Build Mip 8": 0.003916, + "HZB Build Mip 9": 0.003625, + "HZB Occlusion Culling pass": 0.015166, + "Local Scan Pass": 0.003708, + "Mark Visible Pass": 0.003458, + "Output Transform Pass": 0.099708, + "Post-Processing Pass": 0.095417, + "Pre Composite Pass": 0.169833, + "Radix Sort": 0.1505, + "Shadow Cascade 0": 0.022459, + "Shadow Cascade 1": 0.027667, + "Transparency Pass": 0.0095 + }, + "meanFrameMs": 8.332235859318947, + "meanGPUExecutionMs": 2.2363732969916557, "missedDeadlines": 0, - "p50FrameMs": 8.332625031471252, - "p95FrameMs": 9.311875328421593, - "p99FrameMs": 9.674250148236752, - "peakGPUAllocatedBytes": 648036352, - "worstFrameMs": 10.519708506762981, + "p50FrameMs": 8.327499963343143, + "p95FrameMs": 9.340625256299973, + "p99FrameMs": 10.311874561011791, + "peakGPUAllocatedBytes": 648167424, + "timingMeanMs": { + "animationMs": 0.0011779856625556324, + "batchingTickMs": 0.0018635414744494483, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.1530228476622142, + "customSystemsMs": 0.00017475712714561573, + "encodeMs": 1.787180069416839, + "extensionsUpdateMs": 0.0010474999968816217, + "gameUpdateMs": 0.04783437519411867, + "geometryStreamingMs": 0.029246354218533572, + "lodMs": 0.0023556252805671343, + "physicsMs": 0.00045270833652466536, + "renderPrepMs": 0.22145875040829802, + "renderTotalMs": 2.143913541949587, + "scenegraphMs": 0.06931611084534477, + "scriptingMs": 0.0005127776724596819, + "semaphoreWaitMs": 0.00018826341450524828, + "streamingRegionMs": 0.0015178122945750754, + "submitMs": 0.006856215453202215, + "updateMs": 0.13289055626955815 + }, + "worstFrameMs": 11.064250022172928, "worstThermalState": 0 }, "title": "Gaussian splat with primitives", diff --git a/perf/baselines/Mac16_5/macOS/lights-64.json b/perf/baselines/Mac16_5/macOS/lights-64.json index 849afe4b9..7dbe13c25 100644 --- a/perf/baselines/Mac16_5/macOS/lights-64.json +++ b/perf/baselines/Mac16_5/macOS/lights-64.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "lights-64.jsonl", "id": "lights-64", @@ -54,14 +58,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 0, - "frustumPassed": 1024, + "frustumFailed": 4, + "frustumPassed": 1020, "frustumTested": 1024, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 1024, - "occlusionTested": 1024, + "occlusionPassed": 1020, + "occlusionTested": 1020, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -70,180 +74,180 @@ ], "usedHZB": true }, - "frameIndex": 1795, + "frameIndex": 1200, "gpuPasses": { - "frameIndex": 1796, + "frameIndex": 1201, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.008833 + "ms": 0.021125 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.027541 + "ms": 0.058708 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.008542 + "ms": 0.01075 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.004333 + "ms": 0.006584 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.039708 + "ms": 0.024958 }, { "count": 1, "label": "Grid Pass", - "ms": 0.621958 + "ms": 0.139709 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.027542 + "ms": 0.012667 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.01625 + "ms": 0.009375 }, { "count": 1, "label": "Batched Shadow Cascade 0", - "ms": 0.11375 + "ms": 0.137666 }, { "count": 1, "label": "Batched Shadow Cascade 1", - "ms": 0.227959 + "ms": 0.682416 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 1.296666 + "ms": 1.45075 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.239875 + "ms": 0.051125 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.256583 + "ms": 0.081209 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.047 + "ms": 0.043042 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.342916 + "ms": 0.3405 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.352959 + "ms": 0.186708 }, { "count": 2, "label": "Post-Processing Pass", - "ms": 0.299292 + "ms": 0.322708 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.219416 + "ms": 0.251917 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.121333 + "ms": 0.126459 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.102833 + "ms": 0.035209 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.035333 + "ms": 0.014541 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.016875 + "ms": 0.009292 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.01525 + "ms": 0.008542 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.016667 + "ms": 0.008334 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.015167 + "ms": 0.008916 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.014167 + "ms": 0.007958 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.01225 + "ms": 0.008 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.016125 + "ms": 0.00675 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.016 + "ms": 0.00675 } ], "passesSkipped": 0, - "totalMs": 4.533122999999999 + "totalMs": 4.072668000000001 }, "hitches": { "frameBudgetMs": 12.5, "framesLastSecond": 121, - "framesOverBudget": 5, + "framesOverBudget": 0, "framesOverBudgetLastSecond": 0, - "framesSampled": 1796, + "framesSampled": 1201, "histogram": [ 0, - 444, - 1342, - 10, + 354, + 834, + 13, 0, 0, 0 ], - "worstFrameMsLastSecond": 10.331541299819946 + "worstFrameMsLastSecond": 10.870333760976791 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 521420800, + "gpuAllocatedBytes": 520765440, "isUnderPressure": false, "meshMemoryBytes": 12216512, "textureBudgetBytes": 18471796080, @@ -253,10 +257,10 @@ "utilizationPercent": 0.0002645440981723368 }, "render": { - "drawCallsBatched": 708, + "drawCallsBatched": 585, "drawCallsOpaque": 256, - "drawCallsShadow": 471, - "drawCallsTotal": 734, + "drawCallsShadow": 348, + "drawCallsTotal": 611, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -267,8 +271,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 589609, - "visibleInstances": 1024 + "trianglesTotal": 486457, + "visibleInstances": 1020 }, "streaming": { "activeLoads": 0, @@ -307,81 +311,137 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 132963.6275057917, + "timestampSeconds": 133718.93767395837, "timing": { - "animationMs": 0.0009999785106629133, + "animationMs": 0.00120833283290267, "batchingRebuildMs": 0, - "batchingTickMs": 0.0013750104699283838, - "cullingMs": 0.3834583330899477, - "customSystemsMs": 0, - "encodeMs": 2.8640000091399997, - "extensionsUpdateMs": 0.00100000761449337, - "frameTotalMs": 8.333458565175533, - "gameUpdateMs": 0.03825000021606684, - "geometryStreamingMs": 0.0014583347365260124, - "gpuExecutionMs": 3.4927500237245113, - "gpuFrameCadenceMs": 9.14804165950045, - "lodMs": 0.0019166618585586548, - "physicsMs": 0, - "physicsStepCount": 0, - "renderPrepMs": 0.38691668305546045, - "renderTotalMs": 6.7863333388231695, - "scenegraphMs": 0.22016666480340064, - "scriptingMs": 0.00037500285543501377, - "semaphoreWaitMs": 0.0003333261702209711, - "smoothedFrameMs": 8.331048612793287, - "streamingRegionMs": 0.0012500095181167126, - "submitMs": 0.010833347914740443, - "updateMs": 0.26904165861196816 + "batchingTickMs": 0.002541666617617011, + "cullingMs": 0.4207500023767352, + "customSystemsMs": 0.00037500285543501377, + "encodeMs": 2.5599583168514073, + "extensionsUpdateMs": 0.0014583347365260124, + "frameTotalMs": 6.172166671603918, + "gameUpdateMs": 0.034041673643514514, + "geometryStreamingMs": 0.0022500171326100826, + "gpuExecutionMs": 3.4549999982118607, + "gpuFrameCadenceMs": 9.114833315834403, + "lodMs": 0.002708344254642725, + "physicsMs": 0.001374981366097927, + "physicsStepCount": 1, + "renderPrepMs": 0.4239999980200082, + "renderTotalMs": 7.144874980440363, + "scenegraphMs": 0.21550001110881567, + "scriptingMs": 0.00100000761449337, + "semaphoreWaitMs": 0.0002500019036233425, + "smoothedFrameMs": 8.337481940786043, + "streamingRegionMs": 0.001666689058765769, + "submitMs": 0.007166643626987934, + "updateMs": 0.26775000151246786 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.99163612499251, + "durationSeconds": 9.990087250014767, "frameBudgetMs": 12.5, - "frames": 1796, - "framesOverBudget": 5, + "frames": 3603, + "framesOverBudget": 0, "gpuPassMeanMs": { - "Batched Shadow Cascade 0": 0.18360815534521152, - "Batched Shadow Cascade 1": 0.1506465105790647, - "Block Scan Pass": 0.018695265590200433, - "Compact and Stream Pass": 0.01767515423162587, - "Copy Opaque Depth for Gaussian Occlusion": 0.039060067928730464, - "Copy Opaque Depth for HZB": 0.045726726614699294, - "G-Buffer + Light Pass (TBDR)": 1.1641484838530087, - "Gaussian Pass": 0.23502248106904258, - "Grid Pass": 0.1938135373051224, - "HZB Build Mip 0": 0.1858474426503339, - "HZB Build Mip 1": 0.10158815478841879, - "HZB Build Mip 10": 0.010518663697104693, - "HZB Build Mip 2": 0.03798720601336306, - "HZB Build Mip 3": 0.014715400890868573, - "HZB Build Mip 4": 0.011423329064588013, - "HZB Build Mip 5": 0.013088639198218252, - "HZB Build Mip 6": 0.01876569097995545, - "HZB Build Mip 7": 0.020235301781737186, - "HZB Build Mip 8": 0.01825441982182627, - "HZB Build Mip 9": 0.013184029510022262, - "HZB Occlusion Culling pass": 0.034824280623608, - "Local Scan Pass": 0.02468478786191541, - "Mark Visible Pass": 0.021555864699331823, - "Output Transform Pass": 0.36320841369710405, - "Post-Processing Pass": 0.4912179020044542, - "Pre Composite Pass": 0.2857132661469934, - "Shadow Cascade 0": 0.039339065701559095, - "Shadow Cascade 1": 0.031155091870824127, - "Transparency Pass": 0.09773111636971063 + "Batched Shadow Cascade 0": 0.2142405512073269, + "Batched Shadow Cascade 1": 0.24675282014987468, + "Block Scan Pass": 0.014569315833333315, + "Compact and Stream Pass": 0.01147794250000003, + "Copy Opaque Depth for Gaussian Occlusion": 0.037497634471273876, + "Copy Opaque Depth for HZB": 0.04325083513738555, + "G-Buffer + Light Pass (TBDR)": 1.2959789458784365, + "Gaussian Pass": 0.3319834088259778, + "Grid Pass": 0.23541597249999957, + "HZB Build Mip 0": 0.19810007910074945, + "HZB Build Mip 1": 0.08351252539550363, + "HZB Build Mip 10": 0.011224593671940031, + "HZB Build Mip 2": 0.031147704412989165, + "HZB Build Mip 3": 0.015954043297252276, + "HZB Build Mip 4": 0.01471981598667779, + "HZB Build Mip 5": 0.015279043297252312, + "HZB Build Mip 6": 0.019591934221482102, + "HZB Build Mip 7": 0.016912566194837626, + "HZB Build Mip 8": 0.01712222064945876, + "HZB Build Mip 9": 0.012067846794338068, + "HZB Occlusion Culling pass": 0.028990835833333294, + "Local Scan Pass": 0.020854170833333335, + "Mark Visible Pass": 0.017400186511240683, + "Output Transform Pass": 0.2712267110741049, + "Post-Processing Pass": 0.36746653871773516, + "Pre Composite Pass": 0.2542115570358035, + "Shadow Cascade 0": 0.023040248959200635, + "Shadow Cascade 1": 0.019471407993338918, + "Transparency Pass": 0.1438001665278931 }, - "meanFrameMs": 8.356517791397504, - "meanGPUExecutionMs": 3.0345159839250964, + "gpuPassMinMs": { + "Batched Shadow Cascade 0": 0.043292, + "Batched Shadow Cascade 1": 0.045416, + "Block Scan Pass": 0.00525, + "Compact and Stream Pass": 0.003458, + "Copy Opaque Depth for Gaussian Occlusion": 0.020333, + "Copy Opaque Depth for HZB": 0.023791, + "G-Buffer + Light Pass (TBDR)": 0.752833, + "Gaussian Pass": 0.070708, + "Grid Pass": 0.047667, + "HZB Build Mip 0": 0.071917, + "HZB Build Mip 1": 0.02275, + "HZB Build Mip 10": 0.003708, + "HZB Build Mip 2": 0.008125, + "HZB Build Mip 3": 0.004875, + "HZB Build Mip 4": 0.004208, + "HZB Build Mip 5": 0.00425, + "HZB Build Mip 6": 0.004, + "HZB Build Mip 7": 0.004, + "HZB Build Mip 8": 0.004, + "HZB Build Mip 9": 0.003917, + "HZB Occlusion Culling pass": 0.015209, + "Local Scan Pass": 0.003875, + "Mark Visible Pass": 0.003583, + "Output Transform Pass": 0.192375, + "Post-Processing Pass": 0.257625, + "Pre Composite Pass": 0.173709, + "Shadow Cascade 0": 0.006416, + "Shadow Cascade 1": 0.006292, + "Transparency Pass": 0.016708 + }, + "meanFrameMs": 8.33129754319452, + "meanGPUExecutionMs": 3.340423882918178, "missedDeadlines": 0, - "p50FrameMs": 8.332458324730396, - "p95FrameMs": 10.196291841566563, - "p99FrameMs": 10.966208763420582, - "peakGPUAllocatedBytes": 521420800, - "worstFrameMs": 16.650082543492317, + "p50FrameMs": 8.33266694098711, + "p95FrameMs": 10.276916436851025, + "p99FrameMs": 10.626000352203846, + "peakGPUAllocatedBytes": 520765440, + "timingMeanMs": { + "animationMs": 0.0008935605121123528, + "batchingTickMs": 0.0016668397880125975, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.3852863933165619, + "customSystemsMs": 0.00021090053179325362, + "encodeMs": 2.5767450039910864, + "extensionsUpdateMs": 0.0009832777585803247, + "gameUpdateMs": 0.042672598905873785, + "geometryStreamingMs": 0.09275274133142224, + "lodMs": 0.0022987096099730433, + "physicsMs": 0.00045833320451511085, + "renderPrepMs": 0.38984138249585576, + "renderTotalMs": 3.108962809038484, + "scenegraphMs": 0.20800308761753755, + "scriptingMs": 0.0004569457442213554, + "semaphoreWaitMs": 0.00014883364480202838, + "streamingRegionMs": 0.0013405846127097205, + "submitMs": 0.0062845201690221435, + "updateMs": 0.2650766721707563 + }, + "worstFrameMs": 12.064333073794842, "worstThermalState": 0 }, "title": "1024 primitives, static batching, 64 point lights", diff --git a/perf/baselines/Mac16_5/macOS/postfx.json b/perf/baselines/Mac16_5/macOS/postfx.json index 1c4a29e4f..b72bba666 100644 --- a/perf/baselines/Mac16_5/macOS/postfx.json +++ b/perf/baselines/Mac16_5/macOS/postfx.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "postfx.jsonl", "id": "postfx", @@ -54,14 +58,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 0, - "frustumPassed": 1024, + "frustumFailed": 4, + "frustumPassed": 1020, "frustumTested": 1024, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 1024, - "occlusionTested": 1024, + "occlusionPassed": 1020, + "occlusionTested": 1020, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -70,185 +74,185 @@ ], "usedHZB": true }, - "frameIndex": 1765, + "frameIndex": 1200, "gpuPasses": { - "frameIndex": 1766, + "frameIndex": 1201, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.036 + "ms": 0.005291 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.005416 + "ms": 0.005542 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.006042 + "ms": 0.007917 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.004 + "ms": 0.00525 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.0165 + "ms": 0.020458 }, { "count": 1, "label": "Grid Pass", - "ms": 0.055584 + "ms": 0.070458 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.007417 + "ms": 0.009459 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.06525 + "ms": 0.085667 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.360375 + "ms": 0.454375 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.046416 + "ms": 0.075959 }, { "count": 1, "label": "SSAO Low-Res Pass", - "ms": 0.074375 + "ms": 0.093166 }, { "count": 1, "label": "SSAO Bilateral Blur Horizontal", - "ms": 0.025958 + "ms": 0.033083 }, { "count": 1, "label": "SSAO Bilateral Blur Vertical", - "ms": 0.025209 + "ms": 0.0325 }, { "count": 1, "label": "SSAO Upsample Pass", - "ms": 0.041667 + "ms": 0.062625 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.006458 + "ms": 0.007292 }, { "count": 17, "label": "Post-Processing Pass", - "ms": 2.2360390000000003 + "ms": 2.714502 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.026709 + "ms": 0.038791 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.167 + "ms": 0.208 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.193375 + "ms": 0.19725 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.114833 + "ms": 0.125209 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.120209 + "ms": 0.143042 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.083959 + "ms": 0.04475 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.022958 + "ms": 0.032292 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.016417 + "ms": 0.027875 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.014708 + "ms": 0.024125 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.0155 + "ms": 0.024458 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.014375 + "ms": 0.023833 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.014166 + "ms": 0.024125 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.014458 + "ms": 0.025125 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.004333 + "ms": 0.024125 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.004791 + "ms": 0.024167 } ], "passesSkipped": 0, - "totalMs": 3.8404969999999996 + "totalMs": 4.670710999999999 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 119, - "framesOverBudget": 21, + "framesLastSecond": 121, + "framesOverBudget": 0, "framesOverBudgetLastSecond": 0, - "framesSampled": 1766, + "framesSampled": 1201, "histogram": [ 0, - 576, - 1142, - 48, + 485, + 716, + 0, 0, 0, 0 ], - "worstFrameMsLastSecond": 11.291916482150555 + "worstFrameMsLastSecond": 10.85750013589859 }, "memory": { "availableMemoryBytes": 0, @@ -264,9 +268,9 @@ }, "render": { "drawCallsBatched": 0, - "drawCallsOpaque": 1024, - "drawCallsShadow": 995, - "drawCallsTotal": 2045, + "drawCallsOpaque": 1018, + "drawCallsShadow": 989, + "drawCallsTotal": 2033, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -277,8 +281,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 403287, - "visibleInstances": 1024 + "trianglesTotal": 400887, + "visibleInstances": 1020 }, "streaming": { "activeLoads": 0, @@ -317,83 +321,141 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 132982.40647600003, + "timestampSeconds": 133732.7159945417, "timing": { - "animationMs": 0.0010416842997074127, + "animationMs": 0.0008750066626816988, "batchingRebuildMs": 0, - "batchingTickMs": 0.0009166542440652847, - "cullingMs": 0.35524999839253724, + "batchingTickMs": 0.0009583309292793274, + "cullingMs": 0.3156666934955865, "customSystemsMs": 0, - "encodeMs": 7.233083306346089, - "extensionsUpdateMs": 0.0010416842997074127, - "frameTotalMs": 8.83129145950079, - "gameUpdateMs": 0.029624992748722434, - "geometryStreamingMs": 0.0012500095181167126, - "gpuExecutionMs": 3.284666658146307, - "gpuFrameCadenceMs": 8.792208333034068, - "lodMs": 0.0017916900105774403, + "encodeMs": 6.1264583200681955, + "extensionsUpdateMs": 0.0007916823960840702, + "frameTotalMs": 8.399583399295807, + "gameUpdateMs": 0.025666638975962996, + "geometryStreamingMs": 0.001041655195876956, + "gpuExecutionMs": 4.203249991405755, + "gpuFrameCadenceMs": 6.494916684459895, + "lodMs": 0.0015833356883376837, "physicsMs": 0, "physicsStepCount": 0, - "renderPrepMs": 0.364166684448719, - "renderTotalMs": 7.7799166610930115, - "scenegraphMs": 0.19216665532439947, - "scriptingMs": 0.0001666485331952572, - "semaphoreWaitMs": 0.00037500285543501377, - "smoothedFrameMs": 9.298219438642263, - "streamingRegionMs": 0.0009583309292793274, - "submitMs": 0.016083329683169723, - "updateMs": 0.23141666315495968 + "renderPrepMs": 0.3247916465625167, + "renderTotalMs": 7.489458337659016, + "scenegraphMs": 0.17791669233702123, + "scriptingMs": 0.0003333261702209711, + "semaphoreWaitMs": 8.33242665976286e-05, + "smoothedFrameMs": 8.34235007253786, + "streamingRegionMs": 0.0007083290256559849, + "submitMs": 0.008291652193292975, + "updateMs": 0.21220833878032863 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "Same geometry as primitives-1k; the difference is the full-screen pass chain.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.989835000014864, + "durationSeconds": 9.990820000006352, "frameBudgetMs": 12.5, - "frames": 1766, - "framesOverBudget": 21, + "frames": 3601, + "framesOverBudget": 0, "gpuPassMeanMs": { - "Block Scan Pass": 0.009628169875424726, - "Compact and Stream Pass": 0.008737910532276398, - "Copy Opaque Depth for Gaussian Occlusion": 0.0333434966024915, - "Copy Opaque Depth for HZB": 0.06962422253680617, - "G-Buffer + Light Pass (TBDR)": 0.43896250849377094, - "Gaussian Pass": 0.21271007757644406, - "Grid Pass": 0.14476782106455272, - "HZB Build Mip 0": 0.16945612910532287, - "HZB Build Mip 1": 0.05675881087202716, - "HZB Build Mip 10": 0.013074579275198188, - "HZB Build Mip 2": 0.023787618346545913, - "HZB Build Mip 3": 0.015557006795016991, - "HZB Build Mip 4": 0.01302609399773499, - "HZB Build Mip 5": 0.012724159682899224, - "HZB Build Mip 6": 0.013393057757644366, - "HZB Build Mip 7": 0.012162328425821043, - "HZB Build Mip 8": 0.0130427531143828, - "HZB Build Mip 9": 0.012311932616081534, - "HZB Occlusion Culling pass": 0.02390343318233293, - "Local Scan Pass": 0.010902743488108747, - "Mark Visible Pass": 0.022200477349943376, - "Output Transform Pass": 0.17669609852774565, - "Post-Processing Pass": 1.9406931342015912, - "Pre Composite Pass": 0.22469228935447333, - "SSAO Bilateral Blur Horizontal": 0.08901087429218588, - "SSAO Bilateral Blur Vertical": 0.06422502378255944, - "SSAO Low-Res Pass": 0.13638736296715742, - "SSAO Upsample Pass": 0.07998365855039641, - "Shadow Cascade 0": 0.0393915469988675, - "Shadow Cascade 1": 0.12998300849377106, - "Transparency Pass": 0.030185485277463196 + "Block Scan Pass": 0.007166741451209337, + "Compact and Stream Pass": 0.004883863219349449, + "Copy Opaque Depth for Gaussian Occlusion": 0.030152346121768196, + "Copy Opaque Depth for HZB": 0.04630119933277732, + "G-Buffer + Light Pass (TBDR)": 0.37429505587989964, + "Gaussian Pass": 0.20920801248959198, + "Grid Pass": 0.08188264720600501, + "HZB Build Mip 0": 0.10639147623019186, + "HZB Build Mip 1": 0.04638045287739786, + "HZB Build Mip 10": 0.012676583819849886, + "HZB Build Mip 2": 0.021147976647206038, + "HZB Build Mip 3": 0.012779631359466246, + "HZB Build Mip 4": 0.011618154295246073, + "HZB Build Mip 5": 0.011478815679733126, + "HZB Build Mip 6": 0.011135196830692242, + "HZB Build Mip 7": 0.011818201834862378, + "HZB Build Mip 8": 0.012589997497914915, + "HZB Build Mip 9": 0.012154750625521264, + "HZB Occlusion Culling pass": 0.01920677564637198, + "Local Scan Pass": 0.006157356964136773, + "Mark Visible Pass": 0.009823661384487115, + "Output Transform Pass": 0.1795354862385317, + "Post-Processing Pass": 1.7441400250208527, + "Pre Composite Pass": 0.2175919266055043, + "SSAO Bilateral Blur Horizontal": 0.03973140450375313, + "SSAO Bilateral Blur Vertical": 0.03928342869057547, + "SSAO Low-Res Pass": 0.09367715429524615, + "SSAO Upsample Pass": 0.05927517764804006, + "Shadow Cascade 0": 0.017341626355296066, + "Shadow Cascade 1": 0.08043490241868226, + "Transparency Pass": 0.01330884487072564 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.004291, + "Compact and Stream Pass": 0.002666, + "Copy Opaque Depth for Gaussian Occlusion": 0.020875, + "Copy Opaque Depth for HZB": 0.025792, + "G-Buffer + Light Pass (TBDR)": 0.263166, + "Gaussian Pass": 0.14025, + "Grid Pass": 0.037125, + "HZB Build Mip 0": 0.067875, + "HZB Build Mip 1": 0.018125, + "HZB Build Mip 10": 0.003167, + "HZB Build Mip 2": 0.006833, + "HZB Build Mip 3": 0.004417, + "HZB Build Mip 4": 0.003709, + "HZB Build Mip 5": 0.003542, + "HZB Build Mip 6": 0.003292, + "HZB Build Mip 7": 0.003417, + "HZB Build Mip 8": 0.003458, + "HZB Build Mip 9": 0.003333, + "HZB Occlusion Culling pass": 0.013541, + "Local Scan Pass": 0.00325, + "Mark Visible Pass": 0.003083, + "Output Transform Pass": 0.06625, + "Post-Processing Pass": 1.1594589999999998, + "Pre Composite Pass": 0.170958, + "SSAO Bilateral Blur Horizontal": 0.018083, + "SSAO Bilateral Blur Vertical": 0.0175, + "SSAO Low-Res Pass": 0.052042, + "SSAO Upsample Pass": 0.029167, + "Shadow Cascade 0": 0.005208, + "Shadow Cascade 1": 0.047417, + "Transparency Pass": 0.004833 }, - "meanFrameMs": 8.497975771141366, - "meanGPUExecutionMs": 3.6787869007634377, + "meanFrameMs": 8.332842937302306, + "meanGPUExecutionMs": 3.083998054262869, "missedDeadlines": 0, - "p50FrameMs": 8.275041356682777, - "p95FrameMs": 10.354208759963512, - "p99FrameMs": 13.015874661505222, + "p50FrameMs": 8.223208598792553, + "p95FrameMs": 9.74529143422842, + "p99FrameMs": 10.618583299219608, "peakGPUAllocatedBytes": 649543680, - "worstFrameMs": 16.476167365908623, + "timingMeanMs": { + "animationMs": 0.0007417299310902788, + "batchingTickMs": 0.001215179612179651, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.32980473337013794, + "customSystemsMs": 0.00018956735062166687, + "encodeMs": 6.218851751524969, + "extensionsUpdateMs": 0.000816923535144349, + "gameUpdateMs": 0.03113655313259327, + "geometryStreamingMs": 0.08589491247451142, + "lodMs": 0.0017536775969731917, + "physicsMs": 0.00039935498273497964, + "renderPrepMs": 0.33462614691377407, + "renderTotalMs": 7.421056099144314, + "scenegraphMs": 0.18651228155076138, + "scriptingMs": 0.0004005333122570102, + "semaphoreWaitMs": 7.12261120358375e-05, + "streamingRegionMs": 0.0011239225006785811, + "submitMs": 0.008646752148305223, + "updateMs": 0.2296827641915961 + }, + "worstFrameMs": 11.035249568521976, "worstThermalState": 0 }, "title": "1024 primitives with SSAO, bloom, depth of field and SMAA", diff --git a/perf/baselines/Mac16_5/macOS/primitives-10k.json b/perf/baselines/Mac16_5/macOS/primitives-10k.json index c39572e2e..859bca386 100644 --- a/perf/baselines/Mac16_5/macOS/primitives-10k.json +++ b/perf/baselines/Mac16_5/macOS/primitives-10k.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "primitives-10k.jsonl", "id": "primitives-10k", @@ -54,14 +58,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 90, - "frustumPassed": 9910, + "frustumFailed": 256, + "frustumPassed": 9744, "frustumTested": 10000, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 9910, - "occlusionTested": 9910, + "occlusionPassed": 9744, + "occlusionTested": 9744, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -70,175 +74,175 @@ ], "usedHZB": true }, - "frameIndex": 460, + "frameIndex": 321, "gpuPasses": { - "frameIndex": 461, + "frameIndex": 322, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.005917 + "ms": 0.112708 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.005333 + "ms": 0.043292 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.006917 + "ms": 0.011958 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.005083 + "ms": 0.018667 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.024917 + "ms": 0.035292 }, { "count": 1, "label": "Grid Pass", - "ms": 0.061542 + "ms": 0.212917 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.008167 + "ms": 0.011959 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.008292 + "ms": 0.011625 }, { "count": 1, "label": "Batched Shadow Cascade 0", - "ms": 0.167916 + "ms": 0.041791 }, { "count": 1, "label": "Batched Shadow Cascade 1", - "ms": 0.431125 + "ms": 0.405166 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 1.164542 + "ms": 1.62725 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.030917 + "ms": 0.043333 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.036042 + "ms": 0.04875 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.027458 + "ms": 0.04 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.1115 + "ms": 0.15025 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.226584 + "ms": 0.230292 }, { "count": 2, "label": "Post-Processing Pass", - "ms": 0.263 + "ms": 0.317709 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.197792 + "ms": 0.246333 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.095875 + "ms": 0.117083 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.128667 + "ms": 0.110625 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.063458 + "ms": 0.106666 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.01775 + "ms": 0.008209 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.005333 + "ms": 0.007083 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.005625 + "ms": 0.0205 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.024 + "ms": 0.030875 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.024625 + "ms": 0.02975 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.027375 + "ms": 0.010125 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.011084 + "ms": 0.00625 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.004917 + "ms": 0.006959 } ], "passesSkipped": 0, - "totalMs": 3.1917529999999994 + "totalMs": 4.063417 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 31, - "framesOverBudget": 461, - "framesOverBudgetLastSecond": 31, - "framesSampled": 461, + "framesLastSecond": 34, + "framesOverBudget": 322, + "framesOverBudgetLastSecond": 34, + "framesSampled": 322, "histogram": [ 0, 0, 0, 0, - 381, - 80, + 277, + 45, 0 ], - "worstFrameMsLastSecond": 51.80862545967102 + "worstFrameMsLastSecond": 49.01975020766258 }, "memory": { "availableMemoryBytes": 0, @@ -253,10 +257,10 @@ "utilizationPercent": 0.002558775246143341 }, "render": { - "drawCallsBatched": 2227, - "drawCallsOpaque": 1024, - "drawCallsShadow": 1211, - "drawCallsTotal": 2242, + "drawCallsBatched": 1637, + "drawCallsOpaque": 1020, + "drawCallsShadow": 623, + "drawCallsTotal": 1650, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -267,8 +271,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 4474285, - "visibleInstances": 9910 + "trianglesTotal": 3378685, + "visibleInstances": 9744 }, "streaming": { "activeLoads": 0, @@ -300,88 +304,144 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": false, - "updateWorkMs": 0, + "updateTriggered": true, + "updateWorkMs": 4.662036895751953, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 132944.25919933335, + "timestampSeconds": 133704.5502026667, "timing": { - "animationMs": 0.0008333299774676561, + "animationMs": 0.0021666637621819973, "batchingRebuildMs": 0, - "batchingTickMs": 0.0011666561476886272, - "cullingMs": 3.7482500192709267, - "customSystemsMs": 0.0003332970663905144, - "encodeMs": 18.3108750206884, - "extensionsUpdateMs": 0.0008750066626816988, - "frameTotalMs": 31.404290348291397, - "gameUpdateMs": 0.03745831781998277, - "geometryStreamingMs": 0.001541659003123641, - "gpuExecutionMs": 2.5890833348967135, - "gpuFrameCadenceMs": 27.99512498313561, - "lodMs": 0.003083318006247282, - "physicsMs": 0.0009166542440652847, - "physicsStepCount": 2, - "renderPrepMs": 3.753791650524363, - "renderTotalMs": 22.194250021129847, - "scenegraphMs": 1.6188750159926713, - "scriptingMs": 0.00033335527405142784, - "semaphoreWaitMs": 0.0002916785888373852, - "smoothedFrameMs": 32.8032444541653, - "streamingRegionMs": 0.0008750066626816988, - "submitMs": 0.008333328878507018, - "updateMs": 1.6742916777729988 + "batchingTickMs": 0.006916670827195048, + "cullingMs": 3.9716250030323863, + "customSystemsMs": 0.001166685251519084, + "encodeMs": 18.10358333750628, + "extensionsUpdateMs": 0.0008333299774676561, + "frameTotalMs": 49.43374916911125, + "gameUpdateMs": 0.1885833335109055, + "geometryStreamingMs": 4.666749999159947, + "gpuExecutionMs": 3.1636249914299697, + "gpuFrameCadenceMs": 50.05545832682401, + "lodMs": 0.00420835567638278, + "physicsMs": 0.0023749598767608404, + "physicsStepCount": 3, + "renderPrepMs": 3.9774166652932763, + "renderTotalMs": 22.232083312701434, + "scenegraphMs": 1.7329583351965994, + "scriptingMs": 0.001666689058765769, + "semaphoreWaitMs": 0, + "smoothedFrameMs": 30.790660964945953, + "streamingRegionMs": 0.002541666617617011, + "submitMs": 0.013291661161929369, + "updateMs": 1.975999999558553 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.95815895832493, + "durationSeconds": 9.935214916651603, "frameBudgetMs": 12.5, - "frames": 461, - "framesOverBudget": 461, + "frames": 963, + "framesOverBudget": 963, "gpuPassMeanMs": { - "Batched Shadow Cascade 0": 0.1954524967462042, - "Batched Shadow Cascade 1": 0.6238593926247287, - "Block Scan Pass": 0.015771598698481572, - "Compact and Stream Pass": 0.01698825813449024, - "Copy Opaque Depth for Gaussian Occlusion": 0.04886833188720169, - "Copy Opaque Depth for HZB": 0.058826355748373124, - "G-Buffer + Light Pass (TBDR)": 1.772278639913231, - "Gaussian Pass": 0.2771464316702818, - "Grid Pass": 0.25485357049891505, - "HZB Build Mip 0": 0.20087906724511911, - "HZB Build Mip 1": 0.11792911279826461, - "HZB Build Mip 10": 0.011721101952277657, - "HZB Build Mip 2": 0.051800854663774425, - "HZB Build Mip 3": 0.021650924078091067, - "HZB Build Mip 4": 0.012233826464208241, - "HZB Build Mip 5": 0.011373208242950109, - "HZB Build Mip 6": 0.015751086767895887, - "HZB Build Mip 7": 0.021237342733188724, - "HZB Build Mip 8": 0.020523301518438182, - "HZB Build Mip 9": 0.01633655314533623, - "HZB Occlusion Culling pass": 0.041280251626898086, - "Local Scan Pass": 0.024477444685466403, - "Mark Visible Pass": 0.033417225596529265, - "Output Transform Pass": 0.3524678416485901, - "Post-Processing Pass": 0.4936314186550975, - "Pre Composite Pass": 0.2816119804772232, - "Shadow Cascade 0": 0.02275878524945769, - "Shadow Cascade 1": 0.017315700650759203, - "Transparency Pass": 0.12060769631236454 + "Batched Shadow Cascade 0": 0.13152742236024845, + "Batched Shadow Cascade 1": 0.5024740866873066, + "Block Scan Pass": 0.013532718266253881, + "Compact and Stream Pass": 0.017274773993808043, + "Copy Opaque Depth for Gaussian Occlusion": 0.03643564705882351, + "Copy Opaque Depth for HZB": 0.050453907120743074, + "G-Buffer + Light Pass (TBDR)": 1.531752597523219, + "Gaussian Pass": 0.22193771428571393, + "Grid Pass": 0.21836260681114555, + "HZB Build Mip 0": 0.1743237306501548, + "HZB Build Mip 1": 0.1047644427244582, + "HZB Build Mip 10": 0.00914026182965299, + "HZB Build Mip 2": 0.04195441614906833, + "HZB Build Mip 3": 0.02085507142857145, + "HZB Build Mip 4": 0.011262386996904029, + "HZB Build Mip 5": 0.010589770897832817, + "HZB Build Mip 6": 0.014038819875776393, + "HZB Build Mip 7": 0.019162527950310575, + "HZB Build Mip 8": 0.01982048447204971, + "HZB Build Mip 9": 0.014054154574132493, + "HZB Occlusion Culling pass": 0.035897279503105624, + "Local Scan Pass": 0.022816210526315765, + "Mark Visible Pass": 0.04601806191950465, + "Output Transform Pass": 0.2581778328075709, + "Post-Processing Pass": 0.3476098385093168, + "Pre Composite Pass": 0.23887434161490675, + "Shadow Cascade 0": 0.021264906832298144, + "Shadow Cascade 1": 0.016576721362229097, + "Transparency Pass": 0.08917299690402476 }, - "meanFrameMs": 32.589466179723075, - "meanGPUExecutionMs": 4.095099150158219, + "gpuPassMinMs": { + "Batched Shadow Cascade 0": 0.007834, + "Batched Shadow Cascade 1": 0.26025, + "Block Scan Pass": 0.005875, + "Compact and Stream Pass": 0.004375, + "Copy Opaque Depth for Gaussian Occlusion": 0.022417, + "Copy Opaque Depth for HZB": 0.027209, + "G-Buffer + Light Pass (TBDR)": 0.982875, + "Gaussian Pass": 0.077375, + "Grid Pass": 0.05375, + "HZB Build Mip 0": 0.083291, + "HZB Build Mip 1": 0.025084, + "HZB Build Mip 10": 0.003959, + "HZB Build Mip 2": 0.009333, + "HZB Build Mip 3": 0.005083, + "HZB Build Mip 4": 0.004375, + "HZB Build Mip 5": 0.004417, + "HZB Build Mip 6": 0.004709, + "HZB Build Mip 7": 0.00425, + "HZB Build Mip 8": 0.004542, + "HZB Build Mip 9": 0.004, + "HZB Occlusion Culling pass": 0.020667, + "Local Scan Pass": 0.004333, + "Mark Visible Pass": 0.00525, + "Output Transform Pass": 0.162041, + "Post-Processing Pass": 0.228584, + "Pre Composite Pass": 0.178791, + "Shadow Cascade 0": 0.007084, + "Shadow Cascade 1": 0.006834, + "Transparency Pass": 0.025375 + }, + "meanFrameMs": 31.039583003696272, + "meanGPUExecutionMs": 3.4729653000600935, "missedDeadlines": 0, - "p50FrameMs": 30.894041061401367, - "p95FrameMs": 50.629500299692154, - "p99FrameMs": 52.89120972156525, + "p50FrameMs": 29.310083016753197, + "p95FrameMs": 47.68349975347519, + "p99FrameMs": 49.16954040527344, "peakGPUAllocatedBytes": 615792640, - "worstFrameMs": 54.943665862083435, + "timingMeanMs": { + "animationMs": 0.0008041796529659823, + "batchingTickMs": 0.0016874998340799664, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 3.3322403247247596, + "customSystemsMs": 0.000321558461183254, + "encodeMs": 17.798151916073003, + "extensionsUpdateMs": 0.000936662722032967, + "gameUpdateMs": 0.04606488679431886, + "geometryStreamingMs": 0.7738778391966867, + "lodMs": 0.003309597314592024, + "physicsMs": 0.0011560558457135784, + "renderPrepMs": 3.3375624355163724, + "renderTotalMs": 21.587290764255044, + "scenegraphMs": 1.6750698750628945, + "scriptingMs": 0.00042853444085194046, + "semaphoreWaitMs": 7.036180490241298e-05, + "streamingRegionMs": 0.001263028931956819, + "submitMs": 0.009899763569803967, + "updateMs": 1.7436207427092518 + }, + "worstFrameMs": 50.83279311656952, "worstThermalState": 0 }, "title": "10000 primitives, static batching, 4 point lights", diff --git a/perf/baselines/Mac16_5/macOS/primitives-1k.json b/perf/baselines/Mac16_5/macOS/primitives-1k.json index 0d8bc0991..e1cdb0dbf 100644 --- a/perf/baselines/Mac16_5/macOS/primitives-1k.json +++ b/perf/baselines/Mac16_5/macOS/primitives-1k.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "primitives-1k.jsonl", "id": "primitives-1k", @@ -54,14 +58,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 0, - "frustumPassed": 1024, + "frustumFailed": 4, + "frustumPassed": 1020, "frustumTested": 1024, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 1024, - "occlusionTested": 1024, + "occlusionPassed": 1020, + "occlusionTested": 1020, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -70,165 +74,165 @@ ], "usedHZB": true }, - "frameIndex": 1796, + "frameIndex": 1198, "gpuPasses": { - "frameIndex": 1796, + "frameIndex": 1199, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.006416 + "ms": 0.004625 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.007166 + "ms": 0.005417 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.009875 + "ms": 0.0075 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.00625 + "ms": 0.004959 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.024958 + "ms": 0.019375 }, { "count": 1, "label": "Grid Pass", - "ms": 0.108834 + "ms": 0.069791 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.027625 + "ms": 0.009208 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.594875 + "ms": 0.082709 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 2.858292 + "ms": 0.438417 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.306291 + "ms": 0.034375 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.313584 + "ms": 0.039625 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.067417 + "ms": 0.030209 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.522292 + "ms": 0.121917 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.574416 + "ms": 0.23275 }, { "count": 2, "label": "Post-Processing Pass", - "ms": 1.076042 + "ms": 0.336625 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.97025 + "ms": 0.269542 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.200292 + "ms": 0.175208 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.034042 + "ms": 0.122125 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.056791 + "ms": 0.022083 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.04375 + "ms": 0.006667 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.0255 + "ms": 0.0065 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.025583 + "ms": 0.030583 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.02125 + "ms": 0.032041 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.021916 + "ms": 0.022625 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.019167 + "ms": 0.006666 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.0255 + "ms": 0.005584 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.021917 + "ms": 0.007125 } ], "passesSkipped": 0, - "totalMs": 7.970291000000001 + "totalMs": 2.144251 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 121, - "framesOverBudget": 3, + "framesLastSecond": 120, + "framesOverBudget": 2, "framesOverBudgetLastSecond": 0, - "framesSampled": 1797, + "framesSampled": 1199, "histogram": [ 0, - 747, - 1043, - 6, + 379, + 818, + 1, 1, 0, 0 ], - "worstFrameMsLastSecond": 10.723625309765339 + "worstFrameMsLastSecond": 10.253791697323322 }, "memory": { "availableMemoryBytes": 0, @@ -244,9 +248,9 @@ }, "render": { "drawCallsBatched": 0, - "drawCallsOpaque": 1024, - "drawCallsShadow": 987, - "drawCallsTotal": 2018, + "drawCallsOpaque": 1020, + "drawCallsShadow": 984, + "drawCallsTotal": 2011, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -257,8 +261,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 402025, - "visibleInstances": 1024 + "trianglesTotal": 399685, + "visibleInstances": 1020 }, "streaming": { "activeLoads": 0, @@ -297,79 +301,133 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 132925.48781458335, + "timestampSeconds": 133690.79086183337, "timing": { - "animationMs": 0.0006666523404419422, + "animationMs": 0.0005833571776747704, "batchingRebuildMs": 0, - "batchingTickMs": 0.000666681444272399, - "cullingMs": 0.3110416582785547, - "customSystemsMs": 0.000500003807246685, - "encodeMs": 6.1249999853316694, - "extensionsUpdateMs": 0.0008333590812981129, - "frameTotalMs": 7.797374855726957, - "gameUpdateMs": 0.020833336748182774, - "geometryStreamingMs": 0.0009583309292793274, - "gpuExecutionMs": 6.427833315683529, - "gpuFrameCadenceMs": 12.517958326498047, - "lodMs": 0.00200001522898674, - "physicsMs": 0.0008333299774676561, + "batchingTickMs": 0.001041655195876956, + "cullingMs": 0.33512499067001045, + "customSystemsMs": 0.0002500019036233425, + "encodeMs": 5.965791642665863, + "extensionsUpdateMs": 0.0008333299774676561, + "frameTotalMs": 8.401166647672653, + "gameUpdateMs": 0.02904166467487812, + "geometryStreamingMs": 0.0013333337847143412, + "gpuExecutionMs": 1.4808333362452686, + "gpuFrameCadenceMs": 8.301458350615576, + "lodMs": 0.001374981366097927, + "physicsMs": 0.0006666523404419422, "physicsStepCount": 1, - "renderPrepMs": 0.3139583277516067, - "renderTotalMs": 8.16037500044331, - "scenegraphMs": 0.1741666637826711, - "scriptingMs": 0.0002500019036233425, - "semaphoreWaitMs": 0, - "smoothedFrameMs": 8.330604123572508, - "streamingRegionMs": 0.000666681444272399, - "submitMs": 0.008125003660097718, - "updateMs": 0.21033332450315356 + "renderPrepMs": 0.3439583524595946, + "renderTotalMs": 7.415666652377695, + "scenegraphMs": 0.1763333275448531, + "scriptingMs": 0.00029164948500692844, + "semaphoreWaitMs": 0.00012500095181167126, + "smoothedFrameMs": 8.338668057695031, + "streamingRegionMs": 0.0007500057108700275, + "submitMs": 0.007166672730818391, + "updateMs": 0.2147083287127316 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "One entity and draw per primitive; measures per-entity CPU cost and encoder overhead.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.98875758331269, + "durationSeconds": 9.991228958358988, "frameBudgetMs": 12.5, - "frames": 1797, - "framesOverBudget": 3, + "frames": 3601, + "framesOverBudget": 2, "gpuPassMeanMs": { - "Block Scan Pass": 0.016658895322939887, - "Compact and Stream Pass": 0.014880027839643577, - "Copy Opaque Depth for Gaussian Occlusion": 0.060346883630289436, - "Copy Opaque Depth for HZB": 0.1172458079064588, - "G-Buffer + Light Pass (TBDR)": 0.7966720718262786, - "Gaussian Pass": 0.5607875083518942, - "Grid Pass": 0.32361982238307346, - "HZB Build Mip 0": 0.2810054471046767, - "HZB Build Mip 1": 0.10631055623608014, - "HZB Build Mip 10": 0.017910079064587952, - "HZB Build Mip 2": 0.044952980512249344, - "HZB Build Mip 3": 0.02445456904231624, - "HZB Build Mip 4": 0.02146364922048997, - "HZB Build Mip 5": 0.019346375278396458, - "HZB Build Mip 6": 0.01973552227171495, - "HZB Build Mip 7": 0.020053766146993293, - "HZB Build Mip 8": 0.019837365812917573, - "HZB Build Mip 9": 0.019498061247216036, - "HZB Occlusion Culling pass": 0.03181210857461022, - "Local Scan Pass": 0.023309097438752837, - "Mark Visible Pass": 0.018762696547884185, - "Output Transform Pass": 0.46123238195991045, - "Post-Processing Pass": 0.5848721820712705, - "Pre Composite Pass": 0.3320783051224939, - "Shadow Cascade 0": 0.02849255512249453, - "Shadow Cascade 1": 0.30190513585746104, - "Transparency Pass": 0.2327666180400889 + "Block Scan Pass": 0.009348957535387155, + "Compact and Stream Pass": 0.006914206494587831, + "Copy Opaque Depth for Gaussian Occlusion": 0.03411374979149293, + "Copy Opaque Depth for HZB": 0.03703215820149868, + "G-Buffer + Light Pass (TBDR)": 0.4913213930058284, + "Gaussian Pass": 0.16560686011656967, + "Grid Pass": 0.10912560782681092, + "HZB Build Mip 0": 0.1831707277268944, + "HZB Build Mip 1": 0.0915890491257285, + "HZB Build Mip 10": 0.02156562135112591, + "HZB Build Mip 2": 0.027539036636136587, + "HZB Build Mip 3": 0.01746764196502914, + "HZB Build Mip 4": 0.017508797668609504, + "HZB Build Mip 5": 0.020459615320566185, + "HZB Build Mip 6": 0.017629512927439524, + "HZB Build Mip 7": 0.0171514879065888, + "HZB Build Mip 8": 0.019084470441298903, + "HZB Build Mip 9": 0.024345608006672206, + "HZB Occlusion Culling pass": 0.024133638634471273, + "Local Scan Pass": 0.008437826810990876, + "Mark Visible Pass": 0.009653452123230696, + "Output Transform Pass": 0.30172888426311384, + "Post-Processing Pass": 0.38054572273105725, + "Pre Composite Pass": 0.24445086344712783, + "Shadow Cascade 0": 0.01252478850957529, + "Shadow Cascade 1": 0.11020184346378012, + "Transparency Pass": 0.05877844462947553 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.005834, + "Compact and Stream Pass": 0.003583, + "Copy Opaque Depth for Gaussian Occlusion": 0.019458, + "Copy Opaque Depth for HZB": 0.026375, + "G-Buffer + Light Pass (TBDR)": 0.328833, + "Gaussian Pass": 0.093125, + "Grid Pass": 0.053417, + "HZB Build Mip 0": 0.083416, + "HZB Build Mip 1": 0.0225, + "HZB Build Mip 10": 0.004125, + "HZB Build Mip 2": 0.008625, + "HZB Build Mip 3": 0.005125, + "HZB Build Mip 4": 0.00425, + "HZB Build Mip 5": 0.004292, + "HZB Build Mip 6": 0.004583, + "HZB Build Mip 7": 0.004083, + "HZB Build Mip 8": 0.004209, + "HZB Build Mip 9": 0.004208, + "HZB Occlusion Culling pass": 0.016167, + "Local Scan Pass": 0.004125, + "Mark Visible Pass": 0.003708, + "Output Transform Pass": 0.167084, + "Post-Processing Pass": 0.27475, + "Pre Composite Pass": 0.177666, + "Shadow Cascade 0": 0.007, + "Shadow Cascade 1": 0.063958, + "Transparency Pass": 0.020958 }, - "meanFrameMs": 8.351642091156844, - "meanGPUExecutionMs": 3.2210675343551687, + "meanFrameMs": 8.332067587995262, + "meanGPUExecutionMs": 1.7270092628694618, "missedDeadlines": 0, - "p50FrameMs": 8.214708417654037, - "p95FrameMs": 10.11549960821867, - "p99FrameMs": 10.813833214342594, + "p50FrameMs": 8.264750242233276, + "p95FrameMs": 9.866000153124332, + "p99FrameMs": 10.235041379928589, "peakGPUAllocatedBytes": 486490112, - "worstFrameMs": 31.519915908575058, + "timingMeanMs": { + "animationMs": 0.0007134683598468792, + "batchingTickMs": 0.0011875517771026424, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.32106338461999046, + "customSystemsMs": 0.00013422875624029067, + "encodeMs": 6.018022102190398, + "extensionsUpdateMs": 0.0008297601903166054, + "gameUpdateMs": 0.030450942857480987, + "geometryStreamingMs": 0.08457736565334432, + "lodMs": 0.001704378349576254, + "physicsMs": 0.00039060318821558017, + "renderPrepMs": 0.3268752423947527, + "renderTotalMs": 7.425045274745915, + "scenegraphMs": 0.17717259925953752, + "scriptingMs": 0.00038828711831115565, + "semaphoreWaitMs": 0.00012836507350969423, + "streamingRegionMs": 0.0011410284680791268, + "submitMs": 0.008601859744272189, + "updateMs": 0.21856550118931686 + }, + "worstFrameMs": 10.790250264108181, "worstThermalState": 0 }, "title": "1024 primitives, 4 point lights", diff --git a/perf/baselines/Mac16_5/macOS/stadium.json b/perf/baselines/Mac16_5/macOS/stadium.json index 07bd2f48a..23d4685e2 100644 --- a/perf/baselines/Mac16_5/macOS/stadium.json +++ b/perf/baselines/Mac16_5/macOS/stadium.json @@ -8,7 +8,7 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline 0226477c", + "label": "baseline e172615b", "render": { "displayRefreshHz": 120, "foveation": false, @@ -21,7 +21,11 @@ "viewportWidth": 1920, "vsync": true }, - "runID": "20260920-212202-macos", + "runIDs": [ + "20260920-213454-macos-r1", + "20260920-213632-macos-r2", + "20260920-213810-macos-r3" + ], "scene": { "file": "stadium.jsonl", "id": "stadium", @@ -70,170 +74,170 @@ ], "usedHZB": true }, - "frameIndex": 1796, + "frameIndex": 1197, "gpuPasses": { - "frameIndex": 1797, + "frameIndex": 1198, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.04825 + "ms": 0.017125 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.005084 + "ms": 0.080083 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.006583 + "ms": 0.034792 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.003875 + "ms": 0.020792 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.013208 + "ms": 0.039584 }, { "count": 1, "label": "Grid Pass", - "ms": 0.063042 + "ms": 1.134125 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.042583 + "ms": 0.016417 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.031125 + "ms": 0.015 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.436083 + "ms": 1.951 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.030459 + "ms": 0.078958 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.468167 + "ms": 2.052666 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.028417 + "ms": 0.079625 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.033417 + "ms": 0.784417 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.224417 + "ms": 0.55325 }, { "count": 1, "label": "Post-Processing Pass", - "ms": 0.637875 + "ms": 0.387084 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.238375 + "ms": 0.583834 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.262417 + "ms": 0.411083 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.771625 + "ms": 0.086625 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.052958 + "ms": 0.039209 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.006 + "ms": 0.0255 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.005458 + "ms": 0.023333 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.005334 + "ms": 0.024083 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.004917 + "ms": 0.020916 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.00525 + "ms": 0.025291 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.005334 + "ms": 0.029042 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.005292 + "ms": 0.008875 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.005167 + "ms": 0.005791 } ], "passesSkipped": 0, - "totalMs": 3.4407119999999995 + "totalMs": 8.528499999999998 }, "hitches": { "frameBudgetMs": 12.5, "framesLastSecond": 121, - "framesOverBudget": 4, + "framesOverBudget": 2, "framesOverBudgetLastSecond": 0, - "framesSampled": 1797, + "framesSampled": 1198, "histogram": [ - 1, - 306, - 1485, - 4, - 1, + 0, + 269, + 926, + 3, + 0, 0, 0 ], - "worstFrameMsLastSecond": 9.398791939020157 + "worstFrameMsLastSecond": 10.284333489835262 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 649609216, + "gpuAllocatedBytes": 649740288, "isUnderPressure": false, "meshMemoryBytes": 322788, "textureBudgetBytes": 18471796080, @@ -245,8 +249,8 @@ "render": { "drawCallsBatched": 0, "drawCallsOpaque": 8, - "drawCallsShadow": 6, - "drawCallsTotal": 20, + "drawCallsShadow": 0, + "drawCallsTotal": 14, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -257,7 +261,7 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 4485, + "trianglesTotal": 2277, "visibleInstances": 16 }, "streaming": { @@ -290,86 +294,140 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": false, - "updateWorkMs": 0, + "updateTriggered": true, + "updateWorkMs": 0.054001808166503906, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133019.94137375004, + "timestampSeconds": 133760.26871145837, "timing": { - "animationMs": 0.0019166618585586548, + "animationMs": 0.0016250123735517263, "batchingRebuildMs": 0, - "batchingTickMs": 0.0027916685212403536, - "cullingMs": 0.13412500265985727, - "customSystemsMs": 0, - "encodeMs": 0.662375008687377, - "extensionsUpdateMs": 0.0011250085663050413, - "frameTotalMs": 8.594291284680367, - "gameUpdateMs": 0.15462498413398862, - "geometryStreamingMs": 0.0026666675694286823, - "gpuExecutionMs": 1.8342083203606308, - "gpuFrameCadenceMs": 8.855208347085863, - "lodMs": 0.003958324668928981, - "physicsMs": 0, - "physicsStepCount": 0, - "renderPrepMs": 0.1410833210684359, - "renderTotalMs": 7.687416684348136, - "scenegraphMs": 0.042083353037014604, - "scriptingMs": 0.00037500285543501377, - "semaphoreWaitMs": 0.000500003807246685, - "smoothedFrameMs": 8.325806974122921, - "streamingRegionMs": 0.0016666599549353123, - "submitMs": 0.02166666672565043, - "updateMs": 0.22712501231580973 + "batchingTickMs": 0.0018333375919610262, + "cullingMs": 0.10708335321396589, + "customSystemsMs": 0.0004166504368185997, + "encodeMs": 0.663791666738689, + "extensionsUpdateMs": 0.0011249794624745846, + "frameTotalMs": 8.974291384220123, + "gameUpdateMs": 0.05158333806321025, + "geometryStreamingMs": 0.0565416703466326, + "gpuExecutionMs": 5.34495833562687, + "gpuFrameCadenceMs": 11.122249998152256, + "lodMs": 0.003583321813493967, + "physicsMs": 0.0014166580513119698, + "physicsStepCount": 1, + "renderPrepMs": 0.11358334450051188, + "renderTotalMs": 7.577458338346332, + "scenegraphMs": 0.038916681660339236, + "scriptingMs": 0.00033335527405142784, + "semaphoreWaitMs": 0.0005833571776747704, + "smoothedFrameMs": 8.344984659925103, + "streamingRegionMs": 0.003374996595084667, + "submitMs": 0.014999997802078724, + "updateMs": 0.11037502554245293 } }, - "measureSeconds": 15, + "measureSeconds": 10, "notes": "stadium.untold and grass.untold from the render-test resources.", + "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 14.988941250019707, + "durationSeconds": 9.98754941666266, "frameBudgetMs": 12.5, - "frames": 1797, - "framesOverBudget": 4, + "frames": 3600, + "framesOverBudget": 2, "gpuPassMeanMs": { - "Block Scan Pass": 0.02447151919866441, - "Compact and Stream Pass": 0.017421944351697256, - "Copy Opaque Depth for Gaussian Occlusion": 0.15683896883695064, - "Copy Opaque Depth for HZB": 0.08756588870339448, - "G-Buffer + Light Pass (TBDR)": 1.1261635943238737, - "Gaussian Pass": 1.3359295447968804, - "Grid Pass": 0.315500546466333, - "HZB Build Mip 0": 0.43504627267668333, - "HZB Build Mip 1": 0.07573602949360057, - "HZB Build Mip 10": 0.011759133555926525, - "HZB Build Mip 2": 0.027459703951029504, - "HZB Build Mip 3": 0.015316264329437999, - "HZB Build Mip 4": 0.012862551474680035, - "HZB Build Mip 5": 0.012572338341680596, - "HZB Build Mip 6": 0.012601763494713418, - "HZB Build Mip 7": 0.011303728992765777, - "HZB Build Mip 8": 0.010666003895381223, - "HZB Build Mip 9": 0.010941539788536457, - "HZB Occlusion Culling pass": 0.03034101725097387, - "Local Scan Pass": 0.04325456594323864, - "Mark Visible Pass": 0.061849728436282725, - "Output Transform Pass": 0.34160800166944877, - "Post-Processing Pass": 0.21710856928213695, - "Pre Composite Pass": 0.2995355698386199, - "Shadow Cascade 0": 0.09474082248191434, - "Shadow Cascade 1": 0.13870777239844176, - "Transparency Pass": 0.17956052643294382 + "Block Scan Pass": 0.044396186143572595, + "Compact and Stream Pass": 0.03754259950041626, + "Copy Opaque Depth for Gaussian Occlusion": 0.07857931583333318, + "Copy Opaque Depth for HZB": 0.0589980865945046, + "G-Buffer + Light Pass (TBDR)": 1.1929322916666656, + "Gaussian Pass": 1.3908513150000021, + "Grid Pass": 0.6689245634390657, + "HZB Build Mip 0": 0.3551936111573687, + "HZB Build Mip 1": 0.07696667416666658, + "HZB Build Mip 10": 0.013135387312186983, + "HZB Build Mip 2": 0.027368575833333325, + "HZB Build Mip 3": 0.017368769358867636, + "HZB Build Mip 4": 0.015805035000000005, + "HZB Build Mip 5": 0.01356378434637806, + "HZB Build Mip 6": 0.012482203164029995, + "HZB Build Mip 7": 0.012020679433805172, + "HZB Build Mip 8": 0.013120429641965053, + "HZB Build Mip 9": 0.012669101666666698, + "HZB Occlusion Culling pass": 0.05448790150250413, + "Local Scan Pass": 0.08547215275459108, + "Mark Visible Pass": 0.14195546083333313, + "Output Transform Pass": 0.31994368276436286, + "Post-Processing Pass": 0.22889284512905902, + "Pre Composite Pass": 0.3208551265611993, + "Shadow Cascade 0": 0.11578891402337256, + "Shadow Cascade 1": 0.17332324229808516, + "Transparency Pass": 0.2771254398998334 }, - "meanFrameMs": 8.350830966980924, - "meanGPUExecutionMs": 3.0920500142631187, + "gpuPassMinMs": { + "Block Scan Pass": 0.005459, + "Compact and Stream Pass": 0.002916, + "Copy Opaque Depth for Gaussian Occlusion": 0.023834, + "Copy Opaque Depth for HZB": 0.019083, + "G-Buffer + Light Pass (TBDR)": 0.373084, + "Gaussian Pass": 0.312041, + "Grid Pass": 0.048583, + "HZB Build Mip 0": 0.093167, + "HZB Build Mip 1": 0.019458, + "HZB Build Mip 10": 0.00375, + "HZB Build Mip 2": 0.007625, + "HZB Build Mip 3": 0.004709, + "HZB Build Mip 4": 0.003916, + "HZB Build Mip 5": 0.003959, + "HZB Build Mip 6": 0.003833, + "HZB Build Mip 7": 0.003875, + "HZB Build Mip 8": 0.004041, + "HZB Build Mip 9": 0.003541, + "HZB Occlusion Culling pass": 0.009917, + "Local Scan Pass": 0.00375, + "Mark Visible Pass": 0.003667, + "Output Transform Pass": 0.122333, + "Post-Processing Pass": 0.094666, + "Pre Composite Pass": 0.170166, + "Shadow Cascade 0": 0.0065, + "Shadow Cascade 1": 0.007916, + "Transparency Pass": 0.023292 + }, + "meanFrameMs": 8.33344737534643, + "meanGPUExecutionMs": 3.8109406400004713, "missedDeadlines": 0, - "p50FrameMs": 8.332333527505398, - "p95FrameMs": 9.297333657741547, - "p99FrameMs": 9.67891700565815, - "peakGPUAllocatedBytes": 649609216, - "worstFrameMs": 17.7697092294693, + "p50FrameMs": 8.33504181355238, + "p95FrameMs": 9.209750220179558, + "p99FrameMs": 9.817874990403652, + "peakGPUAllocatedBytes": 649740288, + "timingMeanMs": { + "animationMs": 0.0019218139410662606, + "batchingTickMs": 0.002800501047910342, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.12098785753840337, + "customSystemsMs": 0.0002536173136003054, + "encodeMs": 0.6101803013610558, + "extensionsUpdateMs": 0.0012968486592403686, + "gameUpdateMs": 0.07140764680269991, + "geometryStreamingMs": 0.013353178444528623, + "lodMs": 0.0036916809236409017, + "physicsMs": 0.000845158498116982, + "renderPrepMs": 0.1297185327264509, + "renderTotalMs": 4.215444871905895, + "scenegraphMs": 0.04524646029706043, + "scriptingMs": 0.0004764534395035723, + "semaphoreWaitMs": 0.0004057104332770574, + "streamingRegionMs": 0.0021302518335704337, + "submitMs": 0.015565362390786795, + "updateMs": 0.13981546588601865 + }, + "worstFrameMs": 10.799666866660118, "worstThermalState": 0 }, "title": "Stadium and grass (textured assets)", From 43eaff74e119bb3ac365acad96c2f69352684d1c Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 21:48:09 +0200 Subject: [PATCH 11/17] [Performance] Judge GPU time by the fastest frame, report the mean for information Three repeats of the same build still varied 40 to 70 % in mean GPU time on an idle GPU. The recording summary gains minGPUExecutionMs; the comparer judges that and prints the mean as information only. Existing baselines lack the field and skip the check until re-recorded. --- .../UntoldEngine/Profiling/EngineStatsRecorder.swift | 6 ++++++ .../EngineStatsMonitorTests.swift | 1 + scripts/perf/compare_baseline.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift index a5bf866a6..cf619c097 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsRecorder.swift @@ -35,6 +35,9 @@ public struct EngineStatsRecordingSummary: Codable, Equatable, Sendable { public var worstFrameMs: Double = 0.0 public var framesOverBudget: Int = 0 public var meanGPUExecutionMs: Double = 0.0 + /// Fastest GPU frame of the run. Clock drift on a lightly loaded GPU only inflates frames, + /// so the minimum is the number to compare across runs; the mean tracks the clock state. + public var minGPUExecutionMs: Double = 0.0 /// Compositor deadline misses and samples over the run (visionOS), zero elsewhere. public var missedDeadlines: Int = 0 public var deadlineSamples: Int = 0 @@ -77,6 +80,7 @@ public final class EngineStatsRecorder: @unchecked Sendable { private var frameTimes: [Double] = [] private var gpuExecutionSum = 0.0 private var gpuExecutionSamples = 0 + private var gpuExecutionMin = Double.greatestFiniteMagnitude private var gpuPassSums: [String: (sum: Double, samples: Int)] = [:] private var gpuPassMins: [String: Double] = [:] private var timingSums: [String: Double] = [:] @@ -139,6 +143,7 @@ public final class EngineStatsRecorder: @unchecked Sendable { if snapshot.timing.gpuExecutionMs > 0 { gpuExecutionSum += snapshot.timing.gpuExecutionMs gpuExecutionSamples += 1 + gpuExecutionMin = min(gpuExecutionMin, snapshot.timing.gpuExecutionMs) } for pass in snapshot.gpuPasses.passes { let entry = gpuPassSums[pass.label] ?? (0.0, 0) @@ -193,6 +198,7 @@ public final class EngineStatsRecorder: @unchecked Sendable { } if gpuExecutionSamples > 0 { result.meanGPUExecutionMs = gpuExecutionSum / Double(gpuExecutionSamples) + result.minGPUExecutionMs = gpuExecutionMin } for (label, entry) in gpuPassSums where entry.samples > 0 { result.gpuPassMeanMs[label] = entry.sum / Double(entry.samples) diff --git a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift index aa13770d6..0a7b2f1bf 100644 --- a/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift +++ b/Tests/UntoldEngineRenderTests/EngineStatsMonitorTests.swift @@ -490,6 +490,7 @@ final class EngineStatsMonitorTests: XCTestCase { XCTAssertEqual(summary.frameBudgetMs, 15.0) XCTAssertEqual(summary.durationSeconds, 1.0, accuracy: 1e-9) XCTAssertEqual(summary.meanGPUExecutionMs, 10.0, accuracy: 1e-9) + XCTAssertEqual(summary.minGPUExecutionMs, 5.0, accuracy: 1e-9) XCTAssertEqual(summary.gpuPassMeanMs["Model Pass"] ?? 0, 5.0, accuracy: 1e-9) XCTAssertEqual(summary.gpuPassMinMs["Model Pass"] ?? 0, 2.5, accuracy: 1e-9) XCTAssertEqual(summary.timingMeanMs["updateMs"] ?? -1, 0.0, accuracy: 1e-9) diff --git a/scripts/perf/compare_baseline.py b/scripts/perf/compare_baseline.py index b25d05b52..42b66a7de 100755 --- a/scripts/perf/compare_baseline.py +++ b/scripts/perf/compare_baseline.py @@ -22,8 +22,10 @@ ("p95FrameMs", "p95 frame ms"), ("p99FrameMs", "p99 frame ms"), ("meanFrameMs", "mean frame ms"), - ("meanGPUExecutionMs", "mean GPU ms"), + ("minGPUExecutionMs", "min GPU ms"), ] +# Reported, never judged: it follows the GPU clock state more than the workload. +INFO_METRICS = [("meanGPUExecutionMs", "mean GPU ms")] # Per-system CPU means worth a line each; the rest are compared silently. TIMING_FIELDS = [ "updateMs", "encodeMs", "cullingMs", "scenegraphMs", "animationMs", "physicsMs", @@ -53,7 +55,7 @@ def aggregate(scenes): """Fold the same scene from several runs into one record: min of times, mean of rates.""" first = json.loads(json.dumps(scenes[0])) s = first["summary"] - for key, _ in FRAME_METRICS: + for key, _ in FRAME_METRICS + INFO_METRICS: s[key] = min(x["summary"].get(key, 0.0) for x in scenes) s["worstFrameMs"] = min(x["summary"].get("worstFrameMs", 0.0) for x in scenes) frames = sum(x["summary"].get("frames", 0) for x in scenes) @@ -148,6 +150,10 @@ def judge(label, now, ref, tolerance, unit="ms", relative=True): for key, label in FRAME_METRICS: judge(label, s.get(key, 0.0), b.get(key, 0.0), args.time_tolerance) + for key, label in INFO_METRICS: + now, ref = s.get(key, 0.0), b.get(key, 0.0) + if ref > 0: + print(f" {label:28s} {now:9.3f} vs {ref:9.3f} ms {(now-ref)/ref*100:+7.1f}% (info)") judge("overBudgetRate", over, bover, args.rate_tolerance, relative=False) judge("missedDeadlineRate", missed, bmissed, args.rate_tolerance, relative=False) tnow, tref = s.get("timingMeanMs", {}), b.get("timingMeanMs", {}) From ab2c9b3f04759788d418606701a9f4d178308e34 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 22:10:41 +0200 Subject: [PATCH 12/17] [Bugfix] Run the benchmark state machine on the engine's update thread On visionOS the engine calls the game update from the XR render thread, so MainActor.assumeIsolated in the PerfBench callback trapped on the first frame. BenchRunner is no longer main-actor bound: its state machine runs on whatever thread the engine updates from (main on macOS and iOS, the XR render thread on visionOS, inside the frame's update phase where the engine expects scene mutations), start() only requests the run, and the observable UI properties are published on the main thread. The scenes and the config lose their main-actor annotations for the same reason. --- .../Sources/PerfBenchCore/BenchConfig.swift | 1 - .../Sources/PerfBenchCore/BenchRunner.swift | 95 ++++++++++++++----- .../Sources/PerfBenchCore/BenchScenes.swift | 14 +-- Examples/PerfBench/Sources/iOS/Info.plist | 8 +- .../PerfBench/Sources/visionOS/Info.plist | 8 +- .../Sources/visionOS/PerfBenchApp.swift | 9 +- 6 files changed, 87 insertions(+), 48 deletions(-) diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift index aad7ff208..e8b16ab85 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift @@ -36,7 +36,6 @@ public struct BenchConfig: Sendable { public var immersion: String public var perFrame: Bool - @MainActor public static func fromEnvironment(_ environment: [String: String] = ProcessInfo.processInfo.environment) -> BenchConfig { func double(_ key: String, _ fallback: Double) -> Double { guard let raw = environment[key], let value = Double(raw), value > 0 else { return fallback } diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift index b7c2cf72d..232405fb3 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift @@ -16,8 +16,11 @@ import UntoldEngine /// Drives the scenes one after another from the engine's update callback: /// build → warm up → record → tear down, then writes `summary.json` and reports. -@MainActor -public final class BenchRunner: ObservableObject { +/// +/// `update(deltaTime:)` runs on whatever thread the engine calls its game update from: the main +/// thread on macOS and iOS, the XR render thread on visionOS. The state machine therefore lives +/// on that thread and only the observable UI properties are published on the main thread. +public final class BenchRunner: ObservableObject, @unchecked Sendable { public enum Phase: Equatable { case idle case building(Int) @@ -27,6 +30,7 @@ public final class BenchRunner: ObservableObject { case finished } + /// UI mirrors of the state machine, always set on the main thread. @Published public private(set) var phase: Phase = .idle @Published public private(set) var statusText: String = "Ready" @Published public private(set) var report: String = "" @@ -41,9 +45,15 @@ public final class BenchRunner: ObservableObject { private let scenes: [BenchScene] private var summary: BenchRunSummary + /// The state machine's own state, touched only from the update thread. + private var state: Phase = .idle private var phaseDeadline: Double = 0.0 private var sceneStart: Double = 0.0 private var cameraEntity: EntityID? + private let startLock = NSLock() + private var startRequested = false + private var started = false + private var lastStatus = "" public init(config: BenchConfig, render: BenchRenderSettings) { self.config = config @@ -59,12 +69,24 @@ public final class BenchRunner: ObservableObject { } } - /// Starts the run. Safe to call once; later calls are ignored. + /// Requests the run; the next `update` starts it on the engine's update thread. Safe to call + /// from any thread and more than once. public func start() { - guard phase == .idle else { return } + startLock.lock() + startRequested = true + startLock.unlock() + } + + private func beginRunIfRequested() { + startLock.lock() + let requested = startRequested && !started + if requested { started = true } + startLock.unlock() + guard requested else { return } + guard !scenes.isEmpty else { - statusText = "No scenes match \(config.sceneIDs)" - phase = .finished + setStatus("No scenes match \(config.sceneIDs)") + setState(.finished) return } try? FileManager.default.createDirectory(at: config.runDirectory, withIntermediateDirectories: true) @@ -77,19 +99,20 @@ public final class BenchRunner: ObservableObject { EngineStatsMonitor.shared.frameBudgetMs = 1.5 * 1000.0 / render.displayRefreshHz } gameMode = true - phase = .building(0) + setState(.building(0)) } - /// Call from the engine's game update callback every frame. + /// Call from the engine's game update callback every frame, on the engine's update thread. public func update(deltaTime _: Float) { + beginRunIfRequested() let now = CACurrentMediaTime() - switch phase { + switch state { case .idle, .finished: return case let .building(index): let scene = scenes[index] - statusText = "Building \(scene.id)" + setStatus("Building \(scene.id)") setSceneReady(false) scene.build(origin: sceneOrigin) cameraEntity = CameraSystem.shared.activeCamera @@ -97,37 +120,37 @@ public final class BenchRunner: ObservableObject { EngineStatsMonitor.shared.reset() sceneStart = now phaseDeadline = now + config.warmupSeconds - phase = .warmup(index) + setState(.warmup(index)) case let .warmup(index): driveCamera(scenes[index], elapsed: now - sceneStart) - statusText = String(format: "%@: warming up %.0fs", scenes[index].id, phaseDeadline - now) + setStatus(String(format: "%@: warming up %.0fs", scenes[index].id, phaseDeadline - now)) if now >= phaseDeadline { EngineStatsMonitor.shared.reset() let file = config.runDirectory.appendingPathComponent("\(scenes[index].id).jsonl") do { try startEngineStatsRecording(to: file, interval: config.perFrame ? .perFrame : .perSecond) } catch { - statusText = "Recording failed: \(error)" + setStatus("Recording failed: \(error)") } phaseDeadline = now + config.measureSeconds - phase = .recording(index) + setState(.recording(index)) } case let .recording(index): driveCamera(scenes[index], elapsed: now - sceneStart) - statusText = String(format: "%@: recording %.0fs", scenes[index].id, phaseDeadline - now) + setStatus(String(format: "%@: recording %.0fs", scenes[index].id, phaseDeadline - now)) if now >= phaseDeadline { finishScene(index) phaseDeadline = now + 0.75 - phase = .tearingDown(index) + setState(.tearingDown(index)) } case let .tearingDown(index): if now >= phaseDeadline { let next = index + 1 if next < scenes.count { - phase = .building(next) + setState(.building(next)) } else { finishRun() } @@ -137,6 +160,26 @@ public final class BenchRunner: ObservableObject { // MARK: - Internals + private func setState(_ newState: Phase) { + state = newState + publishOnMain { $0.phase = newState } + } + + private func setStatus(_ text: String) { + guard text != lastStatus else { return } + lastStatus = text + publishOnMain { $0.statusText = text } + } + + /// Runs `body` on the main thread, synchronously when already there, so SwiftUI sees the change. + private func publishOnMain(_ body: @escaping @Sendable (BenchRunner) -> Void) { + if Thread.isMainThread { + body(self) + } else { + DispatchQueue.main.async { body(self) } + } + } + private func driveCamera(_ scene: BenchScene, elapsed: Double) { guard drivesCamera, let cameraEntity else { return } let orbit = scene.orbit @@ -179,23 +222,27 @@ public final class BenchRunner: ObservableObject { private func finishRun() { summary.finishedAt = ISO8601DateFormatter().string(from: Date()) + let text: String do { let data = try summary.write(to: config.runDirectory) - report = summary.consoleReport - print(report) + text = summary.consoleReport + print(text) // One line the driver script can pick out of the console stream. if let json = String(data: data, encoding: .utf8) { print("PERFBENCH_SUMMARY_JSON " + json.replacingOccurrences(of: "\n", with: " ")) } print("PERFBENCH_DONE \(config.runDirectory.path)") } catch { - report = "Failed to write summary: \(error)" - print(report) + text = "Failed to write summary: \(error)" + print(text) print("PERFBENCH_FAILED") } fflush(stdout) - statusText = "Done" - phase = .finished - onFinished?(summary) + publishOnMain { $0.report = text } + setStatus("Done") + setState(.finished) + let finished = summary + let handler = onFinished + publishOnMain { _ in handler?(finished) } } } diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift index 159b1bf4d..10ea6c5b1 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift @@ -35,8 +35,8 @@ public struct BenchCameraOrbit: Sendable { } /// One benchmark scene. `build` creates every entity relative to `origin`; the runner destroys -/// them afterwards, so `teardown` only has to undo global settings the scene changed. -@MainActor +/// them afterwards, so `teardown` only has to undo global settings the scene changed. Scenes are +/// built from the engine's update callback, on the main thread or the XR render thread. public protocol BenchScene { var id: String { get } var title: String { get } @@ -52,7 +52,7 @@ public extension BenchScene { } public enum BenchScenes { - @MainActor public static let all: [BenchScene] = [ + public static let all: [BenchScene] = [ PrimitivesScene(id: "primitives-1k", gridSize: 32, spacing: 1.0, batched: false, pointLights: 4), PrimitivesScene(id: "primitives-10k", gridSize: 100, spacing: 1.0, batched: true, pointLights: 4), PrimitivesScene(id: "lights-64", gridSize: 32, spacing: 1.0, batched: true, pointLights: 64), @@ -62,14 +62,13 @@ public enum BenchScenes { GaussianScene(), ] - @MainActor public static func scenes(withIDs ids: [String]) -> [BenchScene] { + public static func scenes(withIDs ids: [String]) -> [BenchScene] { ids.compactMap { id in all.first { $0.id == id } } } } // MARK: - Shared helpers -@MainActor enum BenchSceneBuilder { static func makeCamera(eye: simd_float3, target: simd_float3) { let camera = createEntity() @@ -162,7 +161,6 @@ enum BenchSceneBuilder { // MARK: - Scenes /// A grid of individual primitives: draw-call and per-entity CPU cost, shadows, a few lights. -@MainActor final class PrimitivesScene: BenchScene { let id: String let gridSize: Int @@ -209,7 +207,6 @@ final class PrimitivesScene: BenchScene { } /// The 1k primitive grid with SSAO, bloom, depth of field and SMAA on: the post-processing chain. -@MainActor final class PostFXScene: BenchScene { let id = "postfx" let title = "1024 primitives with SSAO, bloom, depth of field and SMAA" @@ -240,7 +237,6 @@ final class PostFXScene: BenchScene { } /// Sixteen skinned characters playing a clip: animation sampling, joint updates and the deformation pass. -@MainActor final class AnimationScene: BenchScene { let id = "animation-16" let title = "16 skinned characters running" @@ -265,7 +261,6 @@ final class AnimationScene: BenchScene { } /// A textured, multi-material asset with a ground plane: material binds and texture bandwidth. -@MainActor final class StadiumScene: BenchScene { let id = "stadium" let title = "Stadium and grass (textured assets)" @@ -287,7 +282,6 @@ final class StadiumScene: BenchScene { } /// A Gaussian splat next to primitives: the splat cull, depth-key and radix-sort passes every frame. -@MainActor final class GaussianScene: BenchScene { let id = "gaussian" let title = "Gaussian splat with primitives" diff --git a/Examples/PerfBench/Sources/iOS/Info.plist b/Examples/PerfBench/Sources/iOS/Info.plist index 211e9f2e5..7807bfba1 100644 --- a/Examples/PerfBench/Sources/iOS/Info.plist +++ b/Examples/PerfBench/Sources/iOS/Info.plist @@ -20,11 +20,15 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes + UIFileSharingEnabled + UILaunchScreen UIRequiresFullScreen @@ -34,9 +38,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIFileSharingEnabled - - LSSupportsOpeningDocumentsInPlace - diff --git a/Examples/PerfBench/Sources/visionOS/Info.plist b/Examples/PerfBench/Sources/visionOS/Info.plist index efb00cf56..8dff7794c 100644 --- a/Examples/PerfBench/Sources/visionOS/Info.plist +++ b/Examples/PerfBench/Sources/visionOS/Info.plist @@ -18,6 +18,10 @@ $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) + LSSupportsOpeningDocumentsInPlace + + NSWorldSensingUsageDescription + PerfBench renders the benchmark scenes in your space. UIApplicationSceneManifest UIApplicationPreferredDefaultSceneSessionRole @@ -25,11 +29,7 @@ UIApplicationSupportsMultipleScenes - NSWorldSensingUsageDescription - PerfBench renders the benchmark scenes in your space. UIFileSharingEnabled - LSSupportsOpeningDocumentsInPlace - diff --git a/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift b/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift index 65cdc7d7e..3081d642a 100644 --- a/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift +++ b/Examples/PerfBench/Sources/visionOS/PerfBenchApp.swift @@ -102,16 +102,15 @@ struct PerfBenchApp: App { } XRHolder.shared.xr = xr let host = BenchHost.shared + let runner = host.runner // The XR module's UntoldImmersionMode cannot be named here (the module and its class share // a name), so let the parameter type pick the enum. xr.setImmersionMode(xrImmersionMode: host.isMixedImmersion ? .mixed : .full) xr.setupCallbacks( gameUpdate: { deltaTime in - // The XR loop runs on its own thread; the runner is main-actor bound but the - // engine's callbacks are invoked synchronously from the frame loop, so hop in place. - MainActor.assumeIsolated { - host.runner.update(deltaTime: deltaTime) - } + // Runs on the XR render thread, inside the frame's update phase, which is where the + // engine expects scene mutations from; the runner publishes its UI state to main. + runner.update(deltaTime: deltaTime) }, handleInput: {} ) From 5b9b8a0bfe0c53dddf376e6218654a2a60bde9d6 Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 22:19:45 +0200 Subject: [PATCH 13/17] [Chores] Add the first Apple Vision Pro baseline (RealityDevice17,1, visionOS 27) One 15 s run per scene from the headset: dedicated layout, no foveation, 2048x1984 per eye, full immersion, release build of ac740ee5. Every scene misses the compositor deadline on most frames; this is the reference the visionOS work is measured against. --- .../visionOS/animation-16.json | 442 ++++++++++++++++ .../RealityDevice17_1/visionOS/gaussian.json | 463 +++++++++++++++++ .../RealityDevice17_1/visionOS/lights-64.json | 456 +++++++++++++++++ .../RealityDevice17_1/visionOS/postfx.json | 470 ++++++++++++++++++ .../visionOS/primitives-10k.json | 456 +++++++++++++++++ .../visionOS/primitives-1k.json | 442 ++++++++++++++++ .../RealityDevice17_1/visionOS/stadium.json | 442 ++++++++++++++++ 7 files changed, 3171 insertions(+) create mode 100644 perf/baselines/RealityDevice17_1/visionOS/animation-16.json create mode 100644 perf/baselines/RealityDevice17_1/visionOS/gaussian.json create mode 100644 perf/baselines/RealityDevice17_1/visionOS/lights-64.json create mode 100644 perf/baselines/RealityDevice17_1/visionOS/postfx.json create mode 100644 perf/baselines/RealityDevice17_1/visionOS/primitives-10k.json create mode 100644 perf/baselines/RealityDevice17_1/visionOS/primitives-1k.json create mode 100644 perf/baselines/RealityDevice17_1/visionOS/stadium.json diff --git a/perf/baselines/RealityDevice17_1/visionOS/animation-16.json b/perf/baselines/RealityDevice17_1/visionOS/animation-16.json new file mode 100644 index 000000000..d28372db0 --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/animation-16.json @@ -0,0 +1,442 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "animation-16.jsonl", + "id": "animation-16", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -2.833958332757902, + "deadlineSampleCount": 1382, + "inputSlackMs": 6.817667000177607, + "missedDeadline": true, + "missedDeadlineCount": 1280, + "missingAnchorCount": 0, + "presentationMarginMs": 12.632333000510698, + "submissionMs": 3.73687500041342, + "updateMs": 0.3573750000214204, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 16, + "frustumTested": 16, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 2, + "occlusionPassed": 14, + "occlusionTested": 16, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 1381, + "gpuPasses": { + "frameIndex": 1382, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.005292 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.008584 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.006291 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.00525 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.020209 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 0.870416 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.089917 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.084583 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 4.381167 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.310875 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 4.942875 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 0.230375 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 0.250876 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.8577919999999999 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 1.992334 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 2.439416 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.8035 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.23 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.113167 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.011958 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.007875 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.006417 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.006667 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.006125 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.00625 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.005417 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.0065 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 1.333708 + } + ], + "passesSkipped": 0, + "totalMs": 20.033836000000004 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 91, + "framesOverBudget": 0, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1382, + "histogram": [ + 0, + 0, + 1120, + 262, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 11.89329195767641 + }, + "memory": { + "availableMemoryBytes": 4201002776, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 1219428352, + "isUnderPressure": false, + "meshMemoryBytes": 1326600, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 786432, + "thermalState": 0, + "trackedEntityCount": 17, + "utilizationPercent": 0.0010666975285857916 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 28, + "drawCallsShadow": 32, + "drawCallsTotal": 72, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 28602, + "visibleInstances": 14 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 17, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3216.991029333334, + "timing": { + "animationMs": 0.277416666449426, + "batchingRebuildMs": 0, + "batchingTickMs": 0.000749999799154466, + "cullingMs": 0.21691666688639089, + "customSystemsMs": 0.00016666672308929265, + "encodeMs": 2.9987083335072384, + "extensionsUpdateMs": 0.00029166676540626213, + "frameTotalMs": 11.270499788224697, + "gameUpdateMs": 0.008208333383663557, + "geometryStreamingMs": 0.0006249997568374965, + "gpuExecutionMs": 9.911124999689491, + "gpuFrameCadenceMs": 11.38370833314184, + "lodMs": 0.001541666733828606, + "physicsMs": 0.0008750002962187864, + "physicsStepCount": 1, + "renderPrepMs": 0.2221666668447142, + "renderTotalMs": 3.6008750003020396, + "scenegraphMs": 4.166668077232316e-05, + "scriptingMs": 0.0002083334038616158, + "semaphoreWaitMs": 0.00020833294911426492, + "smoothedFrameMs": 11.137198656797409, + "streamingRegionMs": 0.0006250002115848474, + "submitMs": 0.013875000149710104, + "updateMs": 0.2949999998236308 + } + }, + "measureSeconds": 15, + "notes": "redplayer.untold with the running clip; measures the animation system and GPU skinning.", + "runs": 1, + "summary": { + "deadlineSamples": 1382, + "durationSeconds": 14.981311125000047, + "frameBudgetMs": 16.666666666666668, + "frames": 1382, + "framesOverBudget": 0, + "gpuPassMeanMs": { + "Block Scan Pass": 0.017164214905933464, + "Compact and Stream Pass": 0.005944423299565833, + "Copy Opaque Depth for Gaussian Occlusion": 0.2896682879884227, + "Copy Opaque Depth for HZB": 0.22344759913169335, + "Environment Pass": 0.9002116302460202, + "G-Buffer + Light Pass (TBDR)": 4.284270515918964, + "Gaussian Pass": 4.807984657742393, + "HZB Build Mip 0": 0.8283389934876988, + "HZB Build Mip 1": 0.39028920767004327, + "HZB Build Mip 10": 0.01038294283646891, + "HZB Build Mip 11": 0.010585959479015986, + "HZB Build Mip 2": 0.10569839073806091, + "HZB Build Mip 3": 0.045369081765557165, + "HZB Build Mip 4": 0.018454094066570212, + "HZB Build Mip 5": 0.014787602749638244, + "HZB Build Mip 6": 0.018440398697539814, + "HZB Build Mip 7": 0.011034144717800341, + "HZB Build Mip 8": 0.010573271345875598, + "HZB Build Mip 9": 0.011269972503617973, + "HZB Occlusion Culling pass": 0.02961548769898696, + "Local Scan Pass": 0.017673649059334273, + "Mark Visible Pass": 0.01916184081041967, + "Output Transform Pass": 1.3425493863965257, + "Post-Processing Pass": 0.7685868878437047, + "Pre Composite Pass": 1.693612640376265, + "Shadow Cascade 0": 0.27436969898697533, + "Shadow Cascade 1": 0.07417337192474661, + "Transparency Pass": 0.24281674312590418 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.003083, + "Compact and Stream Pass": 0.00175, + "Copy Opaque Depth for Gaussian Occlusion": 0.21529199999999998, + "Copy Opaque Depth for HZB": 0.164458, + "Environment Pass": 0.533959, + "G-Buffer + Light Pass (TBDR)": 3.249126, + "Gaussian Pass": 3.6644170000000003, + "HZB Build Mip 0": 0.29425, + "HZB Build Mip 1": 0.081166, + "HZB Build Mip 10": 0.003125, + "HZB Build Mip 11": 0.002709, + "HZB Build Mip 2": 0.018583, + "HZB Build Mip 3": 0.00725, + "HZB Build Mip 4": 0.003792, + "HZB Build Mip 5": 0.003334, + "HZB Build Mip 6": 0.0035, + "HZB Build Mip 7": 0.003, + "HZB Build Mip 8": 0.002875, + "HZB Build Mip 9": 0.003125, + "HZB Occlusion Culling pass": 0.007208, + "Local Scan Pass": 0.003209, + "Mark Visible Pass": 0.002083, + "Output Transform Pass": 0.811917, + "Post-Processing Pass": 0.447459, + "Pre Composite Pass": 1.084208, + "Shadow Cascade 0": 0.06275, + "Shadow Cascade 1": 0.049792, + "Transparency Pass": 0.17820799999999998 + }, + "meanFrameMs": 10.85632275277816, + "meanGPUExecutionMs": 8.639747949832195, + "minGPUExecutionMs": 6.31533333353218, + "missedDeadlines": 1280, + "p50FrameMs": 11.059791781008244, + "p95FrameMs": 11.42862532287836, + "p99FrameMs": 11.76329143345356, + "peakGPUAllocatedBytes": 1219428352, + "timingMeanMs": { + "animationMs": 0.2657349855257074, + "batchingTickMs": 0.00069154003150866, + "compositorDeadlineMarginMs": -1.340198615510018, + "compositorInputSlackMs": 7.1115707040599325, + "compositorSubmissionMs": 3.298293807287805, + "compositorUpdateMs": 0.35871050410267413, + "cullingMs": 0.18095218282501652, + "customSystemsMs": 8.728292619489156e-05, + "encodeMs": 2.6229902315516154, + "extensionsUpdateMs": 0.0003238965222429356, + "gameUpdateMs": 0.007762029668395091, + "geometryStreamingMs": 0.009260522186270909, + "lodMs": 0.0012786119162409035, + "physicsMs": 0.0005318982135779939, + "renderPrepMs": 0.18580963579715398, + "renderTotalMs": 3.176967528942377, + "scenegraphMs": 2.0260493634619067e-05, + "scriptingMs": 0.00013663771967678624, + "semaphoreWaitMs": 0.00020094670111833372, + "streamingRegionMs": 0.0006878316461632747, + "submitMs": 0.013461257837007681, + "updateMs": 0.2827730945455516 + }, + "worstFrameMs": 12.327499687671661, + "worstThermalState": 0 + }, + "title": "16 skinned characters running", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/RealityDevice17_1/visionOS/gaussian.json b/perf/baselines/RealityDevice17_1/visionOS/gaussian.json new file mode 100644 index 000000000..e213fdf6b --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/gaussian.json @@ -0,0 +1,463 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "gaussian.jsonl", + "id": "gaussian", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -6.736625665780593, + "deadlineSampleCount": 1384, + "inputSlackMs": 1.788499333542859, + "missedDeadline": true, + "missedDeadlineCount": 1384, + "missingAnchorCount": 0, + "presentationMarginMs": 8.596167000632704, + "submissionMs": 7.8452916663991346, + "updateMs": 0.2030833334174531, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 61, + "frustumPassed": 195, + "frustumTested": 256, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 35, + "occlusionPassed": 160, + "occlusionTested": 195, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 1383, + "gpuPasses": { + "frameIndex": 1384, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.005125 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.009292 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.00875 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.005041 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.033209 + }, + { + "count": 1, + "label": "Gaussian Frustum Culling", + "ms": 0.054416 + }, + { + "count": 1, + "label": "Gaussian Preprocess", + "ms": 0.018875 + }, + { + "count": 1, + "label": "Radix Sort", + "ms": 0.1835 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 1.013166 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.271209 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.119709 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 3.0368329999999997 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.308709 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 4.041083 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 0.29274999999999995 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 0.313916 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.611083 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 1.7698340000000001 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 1.3630840000000002 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.569041 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.328083 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.033625 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.01375 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.008167 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.008333 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.0805 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.004541 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.00725 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.0055 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.005333 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 0.005042 + } + ], + "passesSkipped": 0, + "totalMs": 15.528749000000003 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 101, + "framesOverBudget": 0, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1384, + "histogram": [ + 0, + 0, + 1144, + 240, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 10.492250323295593 + }, + "memory": { + "availableMemoryBytes": 4160485144, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 1218838528, + "isUnderPressure": false, + "meshMemoryBytes": 3306392, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 258, + "utilizationPercent": 0.0016691276105120778 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 318, + "drawCallsShadow": 487, + "drawCallsTotal": 819, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 165062, + "visibleInstances": 160 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 257, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": true, + "updateWorkMs": 0.15795230865478516, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3254.540743333334, + "timing": { + "animationMs": 0.0003333329914312344, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0004166668077232316, + "cullingMs": 0.1537499997539271, + "customSystemsMs": 0.00029166676540626213, + "encodeMs": 7.274541666902223, + "extensionsUpdateMs": 0.00016666672308929265, + "frameTotalMs": 10.301792062819004, + "gameUpdateMs": 0.003000000106112566, + "geometryStreamingMs": 0.15875000008236384, + "gpuExecutionMs": 8.68554166663671, + "gpuFrameCadenceMs": 10.233916666493315, + "lodMs": 0.0005833330760651734, + "physicsMs": 0.0008333336154464632, + "physicsStepCount": 1, + "renderPrepMs": 0.29374999985520844, + "renderTotalMs": 7.739958333331742, + "scenegraphMs": 4.166668077232316e-05, + "scriptingMs": 8.333336154464632e-05, + "semaphoreWaitMs": 0, + "smoothedFrameMs": 9.997920785099268, + "streamingRegionMs": 0.0011249999261053745, + "submitMs": 0.008291666745208204, + "updateMs": 0.008458333013550146 + } + }, + "measureSeconds": 15, + "notes": "test_gaussians.ply (small) plus a 16x16 grid; exercises the sort pipeline, not splat count.", + "runs": 1, + "summary": { + "deadlineSamples": 1384, + "durationSeconds": 14.979403208333224, + "frameBudgetMs": 16.666666666666668, + "frames": 1384, + "framesOverBudget": 0, + "gpuPassMeanMs": { + "Block Scan Pass": 0.011961762283236945, + "Compact and Stream Pass": 0.006527545520231221, + "Copy Opaque Depth for Gaussian Occlusion": 0.3624899963872825, + "Copy Opaque Depth for HZB": 0.38835822615606946, + "Environment Pass": 2.3854589299132978, + "G-Buffer + Light Pass (TBDR)": 3.5958231885838114, + "Gaussian Frustum Culling": 0.05844728395953748, + "Gaussian Pass": 4.564357692919073, + "Gaussian Preprocess": 0.04501878323699417, + "HZB Build Mip 0": 0.6431993236994223, + "HZB Build Mip 1": 0.25817919797687844, + "HZB Build Mip 10": 0.00885002673410406, + "HZB Build Mip 11": 0.007857567919075164, + "HZB Build Mip 2": 0.07612587355491329, + "HZB Build Mip 3": 0.032062777456647436, + "HZB Build Mip 4": 0.016710989161849696, + "HZB Build Mip 5": 0.01336624566473988, + "HZB Build Mip 6": 0.01051318786127168, + "HZB Build Mip 7": 0.011143021676300567, + "HZB Build Mip 8": 0.00879717196531793, + "HZB Build Mip 9": 0.008912185693641633, + "HZB Occlusion Culling pass": 0.03814640679190751, + "Local Scan Pass": 0.011526166184971036, + "Mark Visible Pass": 0.015515843208092487, + "Output Transform Pass": 2.5153854920520216, + "Post-Processing Pass": 1.5623745354046252, + "Pre Composite Pass": 1.7415976719653186, + "Radix Sort": 0.22116919725433468, + "Shadow Cascade 0": 0.2810928923410407, + "Shadow Cascade 1": 0.14455482008670492, + "Transparency Pass": 0.4148671661849711 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.005791, + "Compact and Stream Pass": 0.003458, + "Copy Opaque Depth for Gaussian Occlusion": 0.298334, + "Copy Opaque Depth for HZB": 0.281042, + "Environment Pass": 0.950624, + "G-Buffer + Light Pass (TBDR)": 2.569125, + "Gaussian Frustum Culling": 0.030583, + "Gaussian Pass": 3.600334, + "Gaussian Preprocess": 0.012292, + "HZB Build Mip 0": 0.292583, + "HZB Build Mip 1": 0.072083, + "HZB Build Mip 10": 0.003917, + "HZB Build Mip 11": 0.003958, + "HZB Build Mip 2": 0.0205, + "HZB Build Mip 3": 0.008458, + "HZB Build Mip 4": 0.005208, + "HZB Build Mip 5": 0.004667, + "HZB Build Mip 6": 0.004541, + "HZB Build Mip 7": 0.004125, + "HZB Build Mip 8": 0.004083, + "HZB Build Mip 9": 0.004208, + "HZB Occlusion Culling pass": 0.023125, + "Local Scan Pass": 0.006, + "Mark Visible Pass": 0.003208, + "Output Transform Pass": 1.0965, + "Post-Processing Pass": 0.6399159999999999, + "Pre Composite Pass": 1.187375, + "Radix Sort": 0.162667, + "Shadow Cascade 0": 0.117542, + "Shadow Cascade 1": 0.117291, + "Transparency Pass": 0.30237400000000003 + }, + "meanFrameMs": 10.839754066193164, + "meanGPUExecutionMs": 9.670346098266757, + "minGPUExecutionMs": 8.224291666465433, + "missedDeadlines": 1384, + "p50FrameMs": 11.066083796322346, + "p95FrameMs": 11.42279151827097, + "p99FrameMs": 11.525999754667282, + "peakGPUAllocatedBytes": 1218838528, + "timingMeanMs": { + "animationMs": 0.000380178226277235, + "batchingTickMs": 0.00043337547463571653, + "compositorDeadlineMarginMs": -6.7243956517037695, + "compositorInputSlackMs": 2.9305160144494526, + "compositorSubmissionMs": 7.7148168954715794, + "compositorUpdateMs": 0.08528540462983683, + "cullingMs": 0.15845493135925423, + "customSystemsMs": 5.762283117712675e-05, + "encodeMs": 7.133637885358584, + "extensionsUpdateMs": 0.00020484103850773963, + "gameUpdateMs": 0.003939968686637978, + "geometryStreamingMs": 0.028001053699505705, + "lodMs": 0.0007987415749132218, + "physicsMs": 0.00027899205709959484, + "renderPrepMs": 0.29731123555555083, + "renderTotalMs": 7.602881593213611, + "scenegraphMs": 1.1741330168881882e-05, + "scriptingMs": 8.227962486035338e-05, + "semaphoreWaitMs": 0.00021507706951577168, + "streamingRegionMs": 0.00041323459256375046, + "submitMs": 0.008283146676057044, + "updateMs": 0.009626023605003845 + }, + "worstFrameMs": 11.5880835801363, + "worstThermalState": 0 + }, + "title": "Gaussian splat with primitives", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/RealityDevice17_1/visionOS/lights-64.json b/perf/baselines/RealityDevice17_1/visionOS/lights-64.json new file mode 100644 index 000000000..92b1f9297 --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/lights-64.json @@ -0,0 +1,456 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "lights-64.jsonl", + "id": "lights-64", + "lastSnapshot": { + "batching": { + "batchGroupCount": 160, + "batchedMeshCount": 998, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -15.230249666728923, + "deadlineSampleCount": 1008, + "inputSlackMs": 2.7427083332440816, + "missedDeadline": true, + "missedDeadlineCount": 1008, + "missingAnchorCount": 0, + "presentationMarginMs": 0.23600000031365198, + "submissionMs": 7.797291666520323, + "updateMs": 0.04279166660126066, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 390, + "frustumPassed": 634, + "frustumTested": 1024, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 0, + "occlusionPassed": 634, + "occlusionTested": 634, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 1008, + "gpuPasses": { + "frameIndex": 1008, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.003083 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.005542 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.003709 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.002833 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.47825 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 1.859417 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.432583 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.268209 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 0", + "ms": 1.047167 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 1", + "ms": 0.078333 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 8.159957 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 1.086667 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 1.1079590000000001 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.221541 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 1.7159589999999998 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.142417 + }, + { + "count": 4, + "label": "Post-Processing Pass", + "ms": 1.687875 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 1.431791 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.218458 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.068875 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.020375 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.008042 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.004583 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.003459 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.003209 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.002833 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.002666 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.002708 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.002916 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 0.002875 + } + ], + "passesSkipped": 0, + "totalMs": 21.074290999999995 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 69, + "framesOverBudget": 368, + "framesOverBudgetLastSecond": 23, + "framesSampled": 1009, + "histogram": [ + 0, + 0, + 527, + 114, + 368, + 0, + 0 + ], + "worstFrameMsLastSecond": 24.13112483918667 + }, + "memory": { + "availableMemoryBytes": 4431935472, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 950435840, + "isUnderPressure": false, + "meshMemoryBytes": 12216512, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 1089, + "utilizationPercent": 0.006167120300233364 + }, + "render": { + "drawCallsBatched": 560, + "drawCallsOpaque": 292, + "drawCallsShadow": 320, + "drawCallsTotal": 626, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 676702, + "visibleInstances": 634 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 1089, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3179.435419125001, + "timing": { + "animationMs": 0.00029166676540626213, + "batchingRebuildMs": 0, + "batchingTickMs": 0.00037499967220355757, + "cullingMs": 0.42887500012511737, + "customSystemsMs": 8.333336154464632e-05, + "encodeMs": 7.080291666625271, + "extensionsUpdateMs": 0.00016666672308929265, + "frameTotalMs": 22.563042119145393, + "gameUpdateMs": 0.0034583331398607697, + "geometryStreamingMs": 0.00029166631065891124, + "gpuExecutionMs": 16.381249999994907, + "gpuFrameCadenceMs": 14.512083333556802, + "lodMs": 0.0006250002115848474, + "physicsMs": 0.0003333334461785853, + "physicsStepCount": 1, + "renderPrepMs": 0.4311249999773281, + "renderTotalMs": 7.699708333348099, + "scenegraphMs": 0, + "scriptingMs": 8.333336154464632e-05, + "semaphoreWaitMs": 0.00016666672308929265, + "smoothedFrameMs": 14.815133282293877, + "streamingRegionMs": 0.00025000008463393897, + "submitMs": 0.013750000107393134, + "updateMs": 0.011125000128231477 + } + }, + "measureSeconds": 15, + "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", + "runs": 1, + "summary": { + "deadlineSamples": 1008, + "durationSeconds": 14.987337458333513, + "frameBudgetMs": 16.666666666666668, + "frames": 1009, + "framesOverBudget": 368, + "gpuPassMeanMs": { + "Batched Shadow Cascade 0": 0.2532643700396827, + "Batched Shadow Cascade 1": 0.11571489682539686, + "Block Scan Pass": 0.007390117063492058, + "Compact and Stream Pass": 0.0051341081349206355, + "Copy Opaque Depth for Gaussian Occlusion": 0.33655539583333316, + "Copy Opaque Depth for HZB": 0.40744785218253987, + "Environment Pass": 2.0645481240079353, + "G-Buffer + Light Pass (TBDR)": 8.582363366071426, + "Gaussian Pass": 1.1439629246031748, + "HZB Build Mip 0": 0.30076717857142865, + "HZB Build Mip 1": 0.11410662698412691, + "HZB Build Mip 10": 0.0067944474206349265, + "HZB Build Mip 11": 0.006298321428571432, + "HZB Build Mip 2": 0.032749855158730225, + "HZB Build Mip 3": 0.023960984126984147, + "HZB Build Mip 4": 0.011490265873015921, + "HZB Build Mip 5": 0.012497401785714315, + "HZB Build Mip 6": 0.012387390873015913, + "HZB Build Mip 7": 0.008110161706349237, + "HZB Build Mip 8": 0.007422881944444459, + "HZB Build Mip 9": 0.007170648809523823, + "HZB Occlusion Culling pass": 0.12865727976190466, + "Local Scan Pass": 0.019534911706349203, + "Mark Visible Pass": 0.006209677579365068, + "Output Transform Pass": 1.455160399801587, + "Post-Processing Pass": 1.509264252976193, + "Pre Composite Pass": 1.1407327579365074, + "Shadow Cascade 0": 0.6395881319444449, + "Shadow Cascade 1": 0.04183736210317452, + "Transparency Pass": 0.422881324404762 + }, + "gpuPassMinMs": { + "Batched Shadow Cascade 0": 0.134666, + "Batched Shadow Cascade 1": 0.064958, + "Block Scan Pass": 0.003083, + "Compact and Stream Pass": 0.001916, + "Copy Opaque Depth for Gaussian Occlusion": 0.20175, + "Copy Opaque Depth for HZB": 0.193625, + "Environment Pass": 1.02, + "G-Buffer + Light Pass (TBDR)": 7.120583, + "Gaussian Pass": 0.740166, + "HZB Build Mip 0": 0.1245, + "HZB Build Mip 1": 0.056667, + "HZB Build Mip 10": 0.002292, + "HZB Build Mip 11": 0.002375, + "HZB Build Mip 2": 0.016791, + "HZB Build Mip 3": 0.005875, + "HZB Build Mip 4": 0.003166, + "HZB Build Mip 5": 0.002875, + "HZB Build Mip 6": 0.002667, + "HZB Build Mip 7": 0.002334, + "HZB Build Mip 8": 0.002375, + "HZB Build Mip 9": 0.002417, + "HZB Occlusion Culling pass": 0.021542, + "Local Scan Pass": 0.003333, + "Mark Visible Pass": 0.002166, + "Output Transform Pass": 1.340708, + "Post-Processing Pass": 1.339251, + "Pre Composite Pass": 0.9803740000000001, + "Shadow Cascade 0": 0.15425, + "Shadow Cascade 1": 0.007375, + "Transparency Pass": 0.206666 + }, + "meanFrameMs": 14.886921370444437, + "meanGPUExecutionMs": 15.176517733127838, + "minGPUExecutionMs": 11.818458333436865, + "missedDeadlines": 1008, + "p50FrameMs": 11.178291402757168, + "p95FrameMs": 23.426249623298645, + "p99FrameMs": 24.04904179275036, + "peakGPUAllocatedBytes": 950697984, + "timingMeanMs": { + "animationMs": 0.00031260323789455524, + "batchingTickMs": 0.00040853155158264925, + "compositorDeadlineMarginMs": -16.69341216863311, + "compositorInputSlackMs": 2.590362763797133, + "compositorSubmissionMs": 11.865231830203268, + "compositorUpdateMs": 0.209173769413372, + "cullingMs": 0.42086083581184014, + "customSystemsMs": 6.318136924952527e-05, + "encodeMs": 7.197634002321812, + "extensionsUpdateMs": 0.00018372151004847386, + "gameUpdateMs": 0.004249339268945607, + "geometryStreamingMs": 0.10710922529877034, + "lodMs": 0.0007553270692742724, + "physicsMs": 0.00033448959647444665, + "renderPrepMs": 0.42333576973533354, + "renderTotalMs": 7.812540758176239, + "scenegraphMs": 1.2925337135799393e-05, + "scriptingMs": 6.982986340425586e-05, + "semaphoreWaitMs": 0.0001228939554521032, + "streamingRegionMs": 0.00039461512932691996, + "submitMs": 0.013954781973654455, + "updateMs": 0.009802362078278169 + }, + "worstFrameMs": 24.261249229311943, + "worstThermalState": 0 + }, + "title": "1024 primitives, static batching, 64 point lights", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/RealityDevice17_1/visionOS/postfx.json b/perf/baselines/RealityDevice17_1/visionOS/postfx.json new file mode 100644 index 000000000..9b5a38de6 --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/postfx.json @@ -0,0 +1,470 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "postfx.jsonl", + "id": "postfx", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -38.794958333255636, + "deadlineSampleCount": 480, + "inputSlackMs": 1.2464579999686975, + "missedDeadline": true, + "missedDeadlineCount": 480, + "missingAnchorCount": 0, + "presentationMarginMs": -23.46220833260304, + "submissionMs": 28.190375000122003, + "updateMs": 0.08170833325493732, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 386, + "frustumPassed": 638, + "frustumTested": 1024, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 0, + "occlusionPassed": 638, + "occlusionTested": 638, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 479, + "gpuPasses": { + "frameIndex": 480, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.005583 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.008042 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.007583 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.005167 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.035958 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 0.843041 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.211625 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.137708 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 2.427958 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 1.947625 + }, + { + "count": 2, + "label": "SSAO Low-Res Pass", + "ms": 0.362625 + }, + { + "count": 2, + "label": "SSAO Bilateral Blur Horizontal", + "ms": 0.194542 + }, + { + "count": 2, + "label": "SSAO Bilateral Blur Vertical", + "ms": 0.25312500000000004 + }, + { + "count": 2, + "label": "SSAO Upsample Pass", + "ms": 2.897625 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 0.013416 + }, + { + "count": 34, + "label": "Post-Processing Pass", + "ms": 11.681208999999999 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.218499 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 0.7410829999999999 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.022334 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 0.957416 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.300084 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.299 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.1015 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.01925 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.013666 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.01125 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.010916 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.010542 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.009708 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.010625 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.011542 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 0.010958 + } + ], + "passesSkipped": 0, + "totalMs": 24.781205000000014 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 30, + "framesOverBudget": 480, + "framesOverBudgetLastSecond": 30, + "framesSampled": 480, + "histogram": [ + 0, + 0, + 0, + 0, + 415, + 65, + 0 + ], + "worstFrameMsLastSecond": 42.714498937129974 + }, + "memory": { + "availableMemoryBytes": 4195186456, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 1219690496, + "isUnderPressure": false, + "meshMemoryBytes": 12096992, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 1029, + "utilizationPercent": 0.0061067845672369 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 1276, + "drawCallsShadow": 1406, + "drawCallsTotal": 2734, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 535982, + "visibleInstances": 638 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 1029, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3198.2040140416675, + "timing": { + "animationMs": 0.0003333334461785853, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0005000001692678779, + "cullingMs": 0.4344166663940996, + "customSystemsMs": 0.00029166676540626213, + "encodeMs": 27.494250000017928, + "extensionsUpdateMs": 0.0004583330337482039, + "frameTotalMs": 30.27733415365219, + "gameUpdateMs": 0.006250000296859071, + "geometryStreamingMs": 0.000749999799154466, + "gpuExecutionMs": 20.031125000059546, + "gpuFrameCadenceMs": 30.650583333226677, + "lodMs": 0.0008749998414714355, + "physicsMs": 0.0006666664376098197, + "physicsStepCount": 2, + "renderPrepMs": 0.43741666650021216, + "renderTotalMs": 28.151375000106782, + "scenegraphMs": 0, + "scriptingMs": 0.00012500004231696948, + "semaphoreWaitMs": 0.00029166676540626213, + "smoothedFrameMs": 30.30298613011837, + "streamingRegionMs": 0.0005000001692678779, + "submitMs": 0.022250000256462954, + "updateMs": 0.01316666657658061 + } + }, + "measureSeconds": 15, + "notes": "Same geometry as primitives-1k; the difference is the full-screen pass chain.", + "runs": 1, + "summary": { + "deadlineSamples": 480, + "durationSeconds": 14.949997541667017, + "frameBudgetMs": 16.666666666666668, + "frames": 480, + "framesOverBudget": 480, + "gpuPassMeanMs": { + "Block Scan Pass": 0.00886802916666666, + "Compact and Stream Pass": 0.00561138333333333, + "Copy Opaque Depth for Gaussian Occlusion": 0.23134908333333334, + "Copy Opaque Depth for HZB": 1.0492304541666664, + "Environment Pass": 0.873899308333333, + "G-Buffer + Light Pass (TBDR)": 2.7901557041666654, + "Gaussian Pass": 0.7766141416666662, + "HZB Build Mip 0": 1.0227512312500004, + "HZB Build Mip 1": 0.3360696083333331, + "HZB Build Mip 10": 0.017559981250000006, + "HZB Build Mip 11": 0.01537577916666669, + "HZB Build Mip 2": 0.09242353958333328, + "HZB Build Mip 3": 0.031174102083333356, + "HZB Build Mip 4": 0.02747478749999999, + "HZB Build Mip 5": 0.018948395833333305, + "HZB Build Mip 6": 0.016273160416666686, + "HZB Build Mip 7": 0.019034716666666625, + "HZB Build Mip 8": 0.011546800000000003, + "HZB Build Mip 9": 0.012437656250000003, + "HZB Occlusion Culling pass": 0.04354598125, + "Local Scan Pass": 0.016924583333333333, + "Mark Visible Pass": 0.012942591666666654, + "Output Transform Pass": 1.0484280041666678, + "Post-Processing Pass": 13.109209222916661, + "Pre Composite Pass": 1.1284165895833334, + "SSAO Bilateral Blur Horizontal": 0.28158334999999995, + "SSAO Bilateral Blur Vertical": 0.27868895416666695, + "SSAO Low-Res Pass": 0.46180608750000013, + "SSAO Upsample Pass": 0.5839662395833337, + "Shadow Cascade 0": 0.21624673125, + "Shadow Cascade 1": 0.17070012083333333, + "Transparency Pass": 0.031535918749999954 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.003125, + "Compact and Stream Pass": 0.001875, + "Copy Opaque Depth for Gaussian Occlusion": 0.207959, + "Copy Opaque Depth for HZB": 0.45845800000000003, + "Environment Pass": 0.763833, + "G-Buffer + Light Pass (TBDR)": 2.270375, + "Gaussian Pass": 0.7132499999999999, + "HZB Build Mip 0": 0.25975, + "HZB Build Mip 1": 0.066625, + "HZB Build Mip 10": 0.002833, + "HZB Build Mip 11": 0.002875, + "HZB Build Mip 2": 0.015833, + "HZB Build Mip 3": 0.006209, + "HZB Build Mip 4": 0.003667, + "HZB Build Mip 5": 0.003417, + "HZB Build Mip 6": 0.00325, + "HZB Build Mip 7": 0.002916, + "HZB Build Mip 8": 0.002917, + "HZB Build Mip 9": 0.002916, + "HZB Occlusion Culling pass": 0.025625, + "Local Scan Pass": 0.003292, + "Mark Visible Pass": 0.002542, + "Output Transform Pass": 0.868958, + "Post-Processing Pass": 11.373708999999998, + "Pre Composite Pass": 0.9875419999999999, + "SSAO Bilateral Blur Horizontal": 0.185167, + "SSAO Bilateral Blur Vertical": 0.188, + "SSAO Low-Res Pass": 0.314375, + "SSAO Upsample Pass": 0.445875, + "Shadow Cascade 0": 0.107541, + "Shadow Cascade 1": 0.132375, + "Transparency Pass": 0.011249 + }, + "meanFrameMs": 31.28584201137225, + "meanGPUExecutionMs": 19.981782291662853, + "minGPUExecutionMs": 18.543749999935244, + "missedDeadlines": 480, + "p50FrameMs": 30.11266700923443, + "p95FrameMs": 39.66212645173073, + "p99FrameMs": 40.0385819375515, + "peakGPUAllocatedBytes": 1219690496, + "timingMeanMs": { + "animationMs": 0.0004596354150028977, + "batchingTickMs": 0.0007462673674278145, + "compositorDeadlineMarginMs": -38.396516233518696, + "compositorInputSlackMs": 2.592095411804962, + "compositorSubmissionMs": 28.11446336806303, + "compositorUpdateMs": 0.23982569445119375, + "cullingMs": 0.4409387152795337, + "customSystemsMs": 0.00026085067759140657, + "encodeMs": 27.374460243056358, + "extensionsUpdateMs": 0.0004658854085922333, + "gameUpdateMs": 0.00820182292879205, + "geometryStreamingMs": 0.10402178819693593, + "lodMs": 0.0011569444419971358, + "physicsMs": 0.0007064236162553547, + "renderPrepMs": 0.44416015625188265, + "renderTotalMs": 28.041801128490345, + "scenegraphMs": 5.57291703747372e-05, + "scriptingMs": 0.00022855902273022366, + "semaphoreWaitMs": 0.0003861110978202002, + "streamingRegionMs": 0.0007277777607820705, + "submitMs": 0.02447526041654176, + "updateMs": 0.017672916671547984 + }, + "worstFrameMs": 42.714498937129974, + "worstThermalState": 0 + }, + "title": "1024 primitives with SSAO, bloom, depth of field and SMAA", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/RealityDevice17_1/visionOS/primitives-10k.json b/perf/baselines/RealityDevice17_1/visionOS/primitives-10k.json new file mode 100644 index 000000000..acd4ab57f --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/primitives-10k.json @@ -0,0 +1,456 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "primitives-10k.jsonl", + "id": "primitives-10k", + "lastSnapshot": { + "batching": { + "batchGroupCount": 1394, + "batchedMeshCount": 9736, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -65.67608333261887, + "deadlineSampleCount": 262, + "inputSlackMs": 1.1027496666429215, + "missedDeadline": true, + "missedDeadlineCount": 262, + "missingAnchorCount": 0, + "presentationMarginMs": -50.20983299937143, + "submissionMs": 52.0494583333857, + "updateMs": 0.10137499975826358, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 4619, + "frustumPassed": 5381, + "frustumTested": 10000, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 0, + "occlusionPassed": 5381, + "occlusionTested": 5381, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 261, + "gpuPasses": { + "frameIndex": 262, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.011333 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.011084 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.019625 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.084458 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.082667 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 2.120042 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.382042 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.023542 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 0", + "ms": 1.81975 + }, + { + "count": 1, + "label": "Batched Shadow Cascade 1", + "ms": 3.424 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 12.207208 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 0.535667 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 0.5744579999999999 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.776875 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 2.151042 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.091209 + }, + { + "count": 4, + "label": "Post-Processing Pass", + "ms": 2.984293 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 2.6821669999999997 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.491208 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.119709 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.036708 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.016875 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.01125 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.009292 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.008875 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.007958 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.007375 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.008125 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.007333 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 0.007708 + } + ], + "passesSkipped": 0, + "totalMs": 31.71387799999999 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 18, + "framesOverBudget": 262, + "framesOverBudgetLastSecond": 18, + "framesSampled": 262, + "histogram": [ + 0, + 0, + 0, + 0, + 0, + 262, + 0 + ], + "worstFrameMsLastSecond": 67.50479340553284 + }, + "memory": { + "availableMemoryBytes": 4287215600, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 1040629760, + "isUnderPressure": false, + "meshMemoryBytes": 118162936, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 10005, + "utilizationPercent": 0.05965082719922066 + }, + "render": { + "drawCallsBatched": 4484, + "drawCallsOpaque": 2014, + "drawCallsShadow": 2788, + "drawCallsTotal": 4816, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 6110134, + "visibleInstances": 5381 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 10005, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3160.216490083334, + "timing": { + "animationMs": 0.0006666664376098197, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0006249997568374965, + "cullingMs": 3.5952083330812457, + "customSystemsMs": 0.00029166676540626213, + "encodeMs": 48.08974999968996, + "extensionsUpdateMs": 0.0005833335308125243, + "frameTotalMs": 57.1155846118927, + "gameUpdateMs": 0.010124999789695721, + "geometryStreamingMs": 0.0009166669769911095, + "gpuExecutionMs": 24.658333333263727, + "gpuFrameCadenceMs": 56.06120833317618, + "lodMs": 0.0012083332876500208, + "physicsMs": 0.0011666666068776976, + "physicsStepCount": 3, + "renderPrepMs": 3.598333333229675, + "renderTotalMs": 51.9683749998876, + "scenegraphMs": 4.166668077232316e-05, + "scriptingMs": 0.00012500004231696948, + "semaphoreWaitMs": 0.0010833332453330513, + "smoothedFrameMs": 57.40352335075537, + "streamingRegionMs": 0.0008749998414714355, + "submitMs": 0.08258333309640875, + "updateMs": 0.020666666841862025 + } + }, + "measureSeconds": 15, + "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", + "runs": 1, + "summary": { + "deadlineSamples": 262, + "durationSeconds": 14.942803291666678, + "frameBudgetMs": 16.666666666666668, + "frames": 262, + "framesOverBudget": 262, + "gpuPassMeanMs": { + "Batched Shadow Cascade 0": 1.962467236641222, + "Batched Shadow Cascade 1": 2.4294020000000014, + "Block Scan Pass": 0.04958334732824424, + "Compact and Stream Pass": 0.033677267175572505, + "Copy Opaque Depth for Gaussian Occlusion": 0.6801869961832059, + "Copy Opaque Depth for HZB": 0.5353362099236639, + "Environment Pass": 2.4222603206106865, + "G-Buffer + Light Pass (TBDR)": 11.660146629770999, + "Gaussian Pass": 2.0366230076335885, + "HZB Build Mip 0": 0.5209620000000003, + "HZB Build Mip 1": 0.14610338167938935, + "HZB Build Mip 10": 0.008687969465648851, + "HZB Build Mip 11": 0.00725304580152672, + "HZB Build Mip 2": 0.03748377099236641, + "HZB Build Mip 3": 0.018716595419847312, + "HZB Build Mip 4": 0.013278923664122142, + "HZB Build Mip 5": 0.009485515267175575, + "HZB Build Mip 6": 0.009927312977099242, + "HZB Build Mip 7": 0.011579988549618322, + "HZB Build Mip 8": 0.008711984732824429, + "HZB Build Mip 9": 0.007642805343511454, + "HZB Occlusion Culling pass": 0.07528783969465648, + "Local Scan Pass": 0.05468525954198474, + "Mark Visible Pass": 0.03252848091603053, + "Output Transform Pass": 3.0403080687022914, + "Post-Processing Pass": 3.4833246221374035, + "Pre Composite Pass": 1.286415732824428, + "Shadow Cascade 0": 0.3938967977099238, + "Shadow Cascade 1": 0.02350952671755726, + "Transparency Pass": 0.5657903740458013 + }, + "gpuPassMinMs": { + "Batched Shadow Cascade 0": 0.807583, + "Batched Shadow Cascade 1": 0.663583, + "Block Scan Pass": 0.006375, + "Compact and Stream Pass": 0.006333, + "Copy Opaque Depth for Gaussian Occlusion": 0.275375, + "Copy Opaque Depth for HZB": 0.212167, + "Environment Pass": 1.174458, + "G-Buffer + Light Pass (TBDR)": 4.509124, + "Gaussian Pass": 0.840334, + "HZB Build Mip 0": 0.208084, + "HZB Build Mip 1": 0.066583, + "HZB Build Mip 10": 0.002708, + "HZB Build Mip 11": 0.002666, + "HZB Build Mip 2": 0.0185, + "HZB Build Mip 3": 0.007875, + "HZB Build Mip 4": 0.004708, + "HZB Build Mip 5": 0.00375, + "HZB Build Mip 6": 0.00325, + "HZB Build Mip 7": 0.003, + "HZB Build Mip 8": 0.002625, + "HZB Build Mip 9": 0.002958, + "HZB Occlusion Culling pass": 0.03975, + "Local Scan Pass": 0.007709, + "Mark Visible Pass": 0.010292, + "Output Transform Pass": 1.445333, + "Post-Processing Pass": 1.467748, + "Pre Composite Pass": 0.99525, + "Shadow Cascade 0": 0.185458, + "Shadow Cascade 1": 0.008875, + "Transparency Pass": 0.226416 + }, + "meanFrameMs": 57.45885860999577, + "meanGPUExecutionMs": 23.920441793899315, + "minGPUExecutionMs": 10.405249999621446, + "missedDeadlines": 262, + "p50FrameMs": 55.72950094938278, + "p95FrameMs": 66.43162667751312, + "p99FrameMs": 67.05595552921295, + "peakGPUAllocatedBytes": 1040760832, + "timingMeanMs": { + "animationMs": 0.0006878180490712944, + "batchingTickMs": 0.0009365457749169299, + "compositorDeadlineMarginMs": -66.27768256946342, + "compositorInputSlackMs": 0.3223713371207434, + "compositorSubmissionMs": 51.90651160940794, + "compositorUpdateMs": 1.4903959923869743, + "cullingMs": 3.5855416666724333, + "customSystemsMs": 0.000438931295299971, + "encodeMs": 47.954657283680575, + "extensionsUpdateMs": 0.000649332091702039, + "gameUpdateMs": 0.013623091636241355, + "geometryStreamingMs": 0.8752247137431195, + "lodMs": 0.0014977735527490048, + "physicsMs": 0.0013177481280881485, + "renderPrepMs": 3.5893078880405502, + "renderTotalMs": 51.829421437648605, + "scenegraphMs": 7.617684800850089e-05, + "scriptingMs": 0.00021374046788968587, + "semaphoreWaitMs": 0.0008880407110939446, + "streamingRegionMs": 0.0009414758193879815, + "submitMs": 0.08328053435545496, + "updateMs": 0.027050731541957255 + }, + "worstFrameMs": 67.50479340553284, + "worstThermalState": 0 + }, + "title": "10000 primitives, static batching, 4 point lights", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/RealityDevice17_1/visionOS/primitives-1k.json b/perf/baselines/RealityDevice17_1/visionOS/primitives-1k.json new file mode 100644 index 000000000..3193a8268 --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/primitives-1k.json @@ -0,0 +1,442 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "primitives-1k.jsonl", + "id": "primitives-1k", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -30.46691666577317, + "deadlineSampleCount": 451, + "inputSlackMs": 7.543500666542968, + "missedDeadline": true, + "missedDeadlineCount": 451, + "missingAnchorCount": 0, + "presentationMarginMs": -15.000665999195917, + "submissionMs": 24.73650000001726, + "updateMs": 0.6501666666736128, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 392, + "frustumPassed": 632, + "frustumTested": 1024, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 0, + "occlusionPassed": 632, + "occlusionTested": 632, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 450, + "gpuPasses": { + "frameIndex": 451, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.101375 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.015458 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.013209 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.00825 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.0485 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 2.143375 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.564167 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.353667 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 8.798709 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 0.45899999999999996 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 0.489125 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.6054999999999999 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 1.749084 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.667833 + }, + { + "count": 4, + "label": "Post-Processing Pass", + "ms": 2.887292 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 2.519167 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.488 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.119416 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.036792 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.016458 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.058125 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.01075 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.009792 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.008708 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.008916 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.008667 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.007417 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 0.008 + } + ], + "passesSkipped": 0, + "totalMs": 23.204751999999996 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 30, + "framesOverBudget": 451, + "framesOverBudgetLastSecond": 30, + "framesSampled": 451, + "histogram": [ + 0, + 0, + 0, + 0, + 273, + 178, + 0 + ], + "worstFrameMsLastSecond": 34.597042948007584 + }, + "memory": { + "availableMemoryBytes": 4517214840, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 911851520, + "isUnderPressure": false, + "meshMemoryBytes": 12098984, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 1030, + "utilizationPercent": 0.006107790395617485 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 1264, + "drawCallsShadow": 1407, + "drawCallsTotal": 2685, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 534082, + "visibleInstances": 632 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 1030, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": true, + "updateWorkMs": 0.5769729614257812, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3141.365684208334, + "timing": { + "animationMs": 0.00037500012695090845, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0004583330337482039, + "cullingMs": 0.3672916664072545, + "customSystemsMs": 0.00037500012695090845, + "encodeMs": 24.082291666672972, + "extensionsUpdateMs": 0.0004583334884955548, + "frameTotalMs": 33.99183228611946, + "gameUpdateMs": 0.005833333034388488, + "geometryStreamingMs": 0.577875000089989, + "gpuExecutionMs": 16.580041666657053, + "gpuFrameCadenceMs": 34.097041666427685, + "lodMs": 0.0007916664799267892, + "physicsMs": 0.0005833335308125243, + "physicsStepCount": 2, + "renderPrepMs": 0.36999999974796083, + "renderTotalMs": 24.655666666603793, + "scenegraphMs": 0, + "scriptingMs": 0.00012500004231696948, + "semaphoreWaitMs": 0.00037500012695090845, + "smoothedFrameMs": 33.311411241690315, + "streamingRegionMs": 0.0014583333722839598, + "submitMs": 0.019999999949504854, + "updateMs": 0.015458333109563682 + } + }, + "measureSeconds": 15, + "notes": "One entity and draw per primitive; measures per-entity CPU cost and encoder overhead.", + "runs": 1, + "summary": { + "deadlineSamples": 451, + "durationSeconds": 14.96570225000005, + "frameBudgetMs": 16.666666666666668, + "frames": 451, + "framesOverBudget": 451, + "gpuPassMeanMs": { + "Block Scan Pass": 0.0307488292682926, + "Compact and Stream Pass": 0.011940394678492242, + "Copy Opaque Depth for Gaussian Occlusion": 0.6903882815964524, + "Copy Opaque Depth for HZB": 0.5778049866962304, + "Environment Pass": 2.7257868580931257, + "G-Buffer + Light Pass (TBDR)": 7.964256365853655, + "Gaussian Pass": 2.1016451130820393, + "HZB Build Mip 0": 0.5712761662971174, + "HZB Build Mip 1": 0.27005534589800456, + "HZB Build Mip 10": 0.011621113082039914, + "HZB Build Mip 11": 0.009926467849223939, + "HZB Build Mip 2": 0.05599044124168513, + "HZB Build Mip 3": 0.0468118203991131, + "HZB Build Mip 4": 0.023930226164079804, + "HZB Build Mip 5": 0.013746871396895795, + "HZB Build Mip 6": 0.013092864745011087, + "HZB Build Mip 7": 0.010953414634146327, + "HZB Build Mip 8": 0.014037217294900225, + "HZB Build Mip 9": 0.008771691796008863, + "HZB Occlusion Culling pass": 0.05589048558758313, + "Local Scan Pass": 0.059680725055432426, + "Mark Visible Pass": 0.20499348337028828, + "Output Transform Pass": 2.587873133037694, + "Post-Processing Pass": 2.839885365853662, + "Pre Composite Pass": 1.7759434789356974, + "Shadow Cascade 0": 0.5171597605321507, + "Shadow Cascade 1": 0.3531939356984475, + "Transparency Pass": 0.609366780487805 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.007125, + "Compact and Stream Pass": 0.004084, + "Copy Opaque Depth for Gaussian Occlusion": 0.40425, + "Copy Opaque Depth for HZB": 0.35862499999999997, + "Environment Pass": 1.027459, + "G-Buffer + Light Pass (TBDR)": 4.216417, + "Gaussian Pass": 1.3574169999999999, + "HZB Build Mip 0": 0.314625, + "HZB Build Mip 1": 0.076542, + "HZB Build Mip 10": 0.004125, + "HZB Build Mip 11": 0.004458, + "HZB Build Mip 2": 0.023334, + "HZB Build Mip 3": 0.010833, + "HZB Build Mip 4": 0.006708, + "HZB Build Mip 5": 0.00575, + "HZB Build Mip 6": 0.005125, + "HZB Build Mip 7": 0.00475, + "HZB Build Mip 8": 0.004541, + "HZB Build Mip 9": 0.00475, + "HZB Occlusion Culling pass": 0.03375, + "Local Scan Pass": 0.007417, + "Mark Visible Pass": 0.003958, + "Output Transform Pass": 1.85425, + "Post-Processing Pass": 2.023876, + "Pre Composite Pass": 1.078668, + "Shadow Cascade 0": 0.176542, + "Shadow Cascade 1": 0.224541, + "Transparency Pass": 0.38391600000000004 + }, + "meanFrameMs": 33.33338233590655, + "meanGPUExecutionMs": 16.34904471543319, + "minGPUExecutionMs": 12.308041666983627, + "missedDeadlines": 451, + "p50FrameMs": 33.331625163555145, + "p95FrameMs": 34.56937521696091, + "p99FrameMs": 34.797459840774536, + "peakGPUAllocatedBytes": 911851520, + "timingMeanMs": { + "animationMs": 0.00042322616921868146, + "batchingTickMs": 0.0006124353254934233, + "compositorDeadlineMarginMs": -30.18120756933401, + "compositorInputSlackMs": 8.22262140058785, + "compositorSubmissionMs": 24.644189855882235, + "compositorUpdateMs": 0.21925120101832665, + "cullingMs": 0.3799433665962696, + "customSystemsMs": 0.00023447892836337919, + "encodeMs": 23.977214246117644, + "extensionsUpdateMs": 0.0004326496767431256, + "gameUpdateMs": 0.007540650398179036, + "geometryStreamingMs": 0.09422671841287283, + "lodMs": 0.0011136363635345324, + "physicsMs": 0.0007246858845437782, + "renderPrepMs": 0.38259303400467165, + "renderTotalMs": 24.55664412416974, + "scenegraphMs": 2.8640059748767234e-05, + "scriptingMs": 0.00015169993509704188, + "semaphoreWaitMs": 0.0004522357631179565, + "streamingRegionMs": 0.0006164079842344417, + "submitMs": 0.0193138396232769, + "updateMs": 0.016351626027345325 + }, + "worstFrameMs": 34.94216501712799, + "worstThermalState": 0 + }, + "title": "1024 primitives, 4 point lights", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/RealityDevice17_1/visionOS/stadium.json b/perf/baselines/RealityDevice17_1/visionOS/stadium.json new file mode 100644 index 000000000..c77d402f7 --- /dev/null +++ b/perf/baselines/RealityDevice17_1/visionOS/stadium.json @@ -0,0 +1,442 @@ +{ + "device": { + "gpuName": "Apple M5 GPU", + "isLowPowerMode": false, + "model": "RealityDevice17,1", + "osVersion": "Version 27.0 (Build 24M362)", + "physicalMemoryBytes": 16501948416, + "processorCount": 10, + "thermalStateAtStart": 0 + }, + "label": "", + "render": { + "displayRefreshHz": 90, + "foveation": false, + "immersion": "full", + "layout": "dedicated", + "platform": "visionOS", + "renderQuality": 1, + "viewCount": 2, + "viewportHeight": 1984, + "viewportWidth": 2048, + "vsync": true + }, + "runIDs": [ + "20260920-221408" + ], + "scene": { + "file": "stadium.jsonl", + "id": "stadium", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": -2.1203749997766863, + "deadlineSampleCount": 1350, + "inputSlackMs": 7.856874999561114, + "missedDeadline": true, + "missedDeadlineCount": 1337, + "missingAnchorCount": 0, + "presentationMarginMs": 13.345958333502494, + "submissionMs": 3.7457500002346933, + "updateMs": 0.09112500038099824, + "viewCount": 2, + "viewTextureHeight": 1984, + "viewTextureWidth": 2048 + }, + "culling": { + "frustumFailed": 4, + "frustumPassed": 12, + "frustumTested": 16, + "hzbIsValid": true, + "hzbMipCount": 12, + "occlusionFailed": 0, + "occlusionPassed": 12, + "occlusionTested": 12, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 2048, + 1984 + ], + "usedHZB": true + }, + "frameIndex": 1349, + "gpuPasses": { + "frameIndex": 1350, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.001875 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.003209 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.003083 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.00175 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.006458 + }, + { + "count": 2, + "label": "Environment Pass", + "ms": 0.772667 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.196791 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.055542 + }, + { + "count": 2, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 5.481084 + }, + { + "count": 2, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.26979200000000003 + }, + { + "count": 2, + "label": "Gaussian Pass", + "ms": 5.971333 + }, + { + "count": 2, + "label": "Copy Opaque Depth for HZB", + "ms": 0.230833 + }, + { + "count": 2, + "label": "Transparency Pass", + "ms": 0.246249 + }, + { + "count": 2, + "label": "Pre Composite Pass", + "ms": 1.5205000000000002 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 1.163792 + }, + { + "count": 2, + "label": "Output Transform Pass", + "ms": 1.8846669999999999 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 0.915542 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 1.307125 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.064042 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.021541 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.009666 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.008417 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.004917 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.00475 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.008125 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.00475 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.003875 + }, + { + "count": 1, + "label": "HZB Build Mip 11", + "ms": 0.002833 + } + ], + "passesSkipped": 0, + "totalMs": 20.165208 + }, + "hitches": { + "frameBudgetMs": 16.666666666666668, + "framesLastSecond": 91, + "framesOverBudget": 0, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1350, + "histogram": [ + 0, + 0, + 747, + 603, + 0, + 0, + 0 + ], + "worstFrameMsLastSecond": 12.280167080461979 + }, + "memory": { + "availableMemoryBytes": 4202526488, + "geometryBudgetBytes": 1188546144, + "gpuAllocatedBytes": 1220050944, + "isUnderPressure": false, + "meshMemoryBytes": 322788, + "textureBudgetBytes": 792364096, + "textureMemoryBytes": 1426304, + "thermalState": 0, + "trackedEntityCount": 9, + "utilizationPercent": 0.0008829738944768906 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 14, + "drawCallsShadow": 10, + "drawCallsTotal": 36, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 9574, + "visibleInstances": 12 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 9, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": false, + "updateWorkMs": 0, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 3235.7675083750005, + "timing": { + "animationMs": 0.000749999799154466, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0006666668923571706, + "cullingMs": 0.37191666660874034, + "customSystemsMs": 0, + "encodeMs": 2.7073749997725827, + "extensionsUpdateMs": 0.0004583334884955548, + "frameTotalMs": 11.107166297733784, + "gameUpdateMs": 0.007791666575940326, + "geometryStreamingMs": 0.0022500003069581, + "gpuExecutionMs": 10.007333333305723, + "gpuFrameCadenceMs": 10.78837500017471, + "lodMs": 0.0014583333722839598, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.38037500007703784, + "renderTotalMs": 3.5397500000726723, + "scenegraphMs": 4.166668077232316e-05, + "scriptingMs": 0.00012500004231696948, + "semaphoreWaitMs": 0.00279166670225095, + "smoothedFrameMs": 11.069122143089771, + "streamingRegionMs": 0.003958333309128648, + "submitMs": 0.014875000033498509, + "updateMs": 0.01633333340578247 + } + }, + "measureSeconds": 15, + "notes": "stadium.untold and grass.untold from the render-test resources.", + "runs": 1, + "summary": { + "deadlineSamples": 1350, + "durationSeconds": 14.97752700000001, + "frameBudgetMs": 16.666666666666668, + "frames": 1350, + "framesOverBudget": 0, + "gpuPassMeanMs": { + "Block Scan Pass": 0.016301361481481487, + "Compact and Stream Pass": 0.007529556296296306, + "Copy Opaque Depth for Gaussian Occlusion": 0.30882368592592563, + "Copy Opaque Depth for HZB": 0.2794284755555557, + "Environment Pass": 0.7963780133333337, + "G-Buffer + Light Pass (TBDR)": 5.132751643703702, + "Gaussian Pass": 5.7152756637037045, + "HZB Build Mip 0": 1.0730258325925917, + "HZB Build Mip 1": 0.4230878992592602, + "HZB Build Mip 10": 0.006089136296296281, + "HZB Build Mip 11": 0.005904990370370355, + "HZB Build Mip 2": 0.09143363111111125, + "HZB Build Mip 3": 0.0330848222222222, + "HZB Build Mip 4": 0.01311335037037035, + "HZB Build Mip 5": 0.012206418518518532, + "HZB Build Mip 6": 0.009849549629629658, + "HZB Build Mip 7": 0.0066391866666666586, + "HZB Build Mip 8": 0.0064832918518518525, + "HZB Build Mip 9": 0.006743079259259267, + "HZB Occlusion Culling pass": 0.023170334074074057, + "Local Scan Pass": 0.016142856296296283, + "Mark Visible Pass": 0.008499859259259312, + "Output Transform Pass": 1.1224651266666679, + "Post-Processing Pass": 0.6517526577777771, + "Pre Composite Pass": 1.8236114577777767, + "Shadow Cascade 0": 0.22461109333333293, + "Shadow Cascade 1": 0.06608987925925913, + "Transparency Pass": 0.3017350548148145 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.003, + "Compact and Stream Pass": 0.001583, + "Copy Opaque Depth for Gaussian Occlusion": 0.231874, + "Copy Opaque Depth for HZB": 0.187459, + "Environment Pass": 0.703416, + "G-Buffer + Light Pass (TBDR)": 4.500667, + "Gaussian Pass": 4.914333, + "HZB Build Mip 0": 0.2965, + "HZB Build Mip 1": 0.102458, + "HZB Build Mip 10": 0.002292, + "HZB Build Mip 11": 0.002208, + "HZB Build Mip 2": 0.020584, + "HZB Build Mip 3": 0.006583, + "HZB Build Mip 4": 0.0035, + "HZB Build Mip 5": 0.003125, + "HZB Build Mip 6": 0.002875, + "HZB Build Mip 7": 0.002458, + "HZB Build Mip 8": 0.002417, + "HZB Build Mip 9": 0.002459, + "HZB Occlusion Culling pass": 0.005875, + "Local Scan Pass": 0.003083, + "Mark Visible Pass": 0.001833, + "Output Transform Pass": 0.8092090000000001, + "Post-Processing Pass": 0.435416, + "Pre Composite Pass": 1.1879170000000001, + "Shadow Cascade 0": 0.058541, + "Shadow Cascade 1": 0.051958, + "Transparency Pass": 0.202625 + }, + "meanFrameMs": 11.110879719533303, + "meanGPUExecutionMs": 9.428379753083357, + "minGPUExecutionMs": 7.7004999998280255, + "missedDeadlines": 1337, + "p50FrameMs": 11.1304996535182, + "p95FrameMs": 12.030958198010921, + "p99FrameMs": 12.311374768614769, + "peakGPUAllocatedBytes": 1220050944, + "timingMeanMs": { + "animationMs": 0.0008779320968541368, + "batchingTickMs": 0.000939753085899779, + "compositorDeadlineMarginMs": -2.213074943188918, + "compositorInputSlackMs": 7.239739201973458, + "compositorSubmissionMs": 3.6446945370341752, + "compositorUpdateMs": 0.11379490740317258, + "cullingMs": 0.3425976543224318, + "customSystemsMs": 0.00014083333183036842, + "encodeMs": 2.6439105555541405, + "extensionsUpdateMs": 0.0004915123574594165, + "gameUpdateMs": 0.009909660492566117, + "geometryStreamingMs": 0.008929259251831186, + "lodMs": 0.001771358020387534, + "physicsMs": 0.0006381481428737364, + "renderPrepMs": 0.3516029012391502, + "renderTotalMs": 3.467874907407383, + "scenegraphMs": 4.922839724275508e-05, + "scriptingMs": 0.00019058641858767564, + "semaphoreWaitMs": 0.0016395061801530472, + "streamingRegionMs": 0.0009050925954970471, + "submitMs": 0.018261388887805406, + "updateMs": 0.021978734572527442 + }, + "worstFrameMs": 12.962458655238152, + "worstThermalState": 0 + }, + "title": "Stadium and grass (textured assets)", + "warmupSeconds": 3 + } +} \ No newline at end of file From d771fbf86801796edb126428e6b22c53cfaf9d5d Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 22:21:32 +0200 Subject: [PATCH 14/17] [Bugfix] Restore the previous anti-aliasing mode after the post-FX benchmark scene The scene's teardown set anti-aliasing to .none, so every scene after it ran without the engine's default FXAA: on the headset the stadium lines shimmered. Capture the mode at build time and put it back. --- Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift index 10ea6c5b1..5250e04db 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift @@ -212,12 +212,14 @@ final class PostFXScene: BenchScene { let title = "1024 primitives with SSAO, bloom, depth of field and SMAA" let notes = "Same geometry as primitives-1k; the difference is the full-screen pass chain." let orbit = BenchCameraOrbit(center: .zero, radius: 22.0, height: 11.0, period: 14.0) + private var previousAntiAliasing: AntiAliasingMode = .fxaa func build(origin: simd_float3) { BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin) BenchSceneBuilder.makeSun() _ = BenchSceneBuilder.makeGrid(origin: origin, gridSize: 32, spacing: 1.0, batched: false, namePrefix: id) BenchSceneBuilder.makePointLights(count: 4, around: origin, radius: 12.0, height: 2.0) + previousAntiAliasing = antiAliasingMode setRendering(.postProcessing(.enabled)) setRendering(.antiAliasing(.smaa)) setPostFX(.ssao(.enabled(true))) @@ -232,7 +234,9 @@ final class PostFXScene: BenchScene { setPostFX(.bloomComposite(.enabled(false))) setPostFX(.bloomThreshold(.enabled(false))) setPostFX(.ssao(.enabled(false))) - setRendering(.antiAliasing(.none)) + // Restore the engine's mode (FXAA by default); forcing .none here left every later scene + // without anti-aliasing, which showed as shimmering edges on the headset. + setRendering(.antiAliasing(previousAntiAliasing)) } } From 38dcb10d21210986d1abc493f07aa4a9a9a211ad Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sun, 20 Sep 2026 22:42:51 +0200 Subject: [PATCH 15/17] [Feature] PerfBench: single-cube scene and an anti-aliasing switch A cube on a plane is the frame's fixed cost and the scene to judge image quality on a headset without load. UNTOLD_BENCH_AA selects fxaa, smaa, msaa or none for every scene (the post-FX scene keeps SMAA); the mode is recorded in the run and is part of the baseline configuration key. --- Examples/PerfBench/README.md | 2 ++ .../Sources/PerfBenchCore/BenchConfig.swift | 6 ++++- .../Sources/PerfBenchCore/BenchResults.swift | 2 ++ .../Sources/PerfBenchCore/BenchRunner.swift | 11 ++++++++ .../Sources/PerfBenchCore/BenchScenes.swift | 27 +++++++++++++++++++ scripts/perf/compare_baseline.py | 2 +- 6 files changed, 48 insertions(+), 2 deletions(-) diff --git a/Examples/PerfBench/README.md b/Examples/PerfBench/README.md index 94cbe27c2..0f69a5cd9 100644 --- a/Examples/PerfBench/README.md +++ b/Examples/PerfBench/README.md @@ -14,6 +14,7 @@ compared with a number from after it. | id | What it measures | |---|---| +| `cube` | One lit, shadowed cube on a plane: the fixed per-frame cost of the pass chain, and the scene to judge image quality on a headset. | | `primitives-1k` | 1024 individual cubes and spheres, 4 point lights, cascaded shadows: per-entity CPU cost and encoder overhead. | | `primitives-10k` | 10 000 primitives merged by the static batcher: batched draw cost and light-loop cost at scale. | | `lights-64` | 1024 batched primitives with 64 point lights: the per-pixel light loop. | @@ -77,6 +78,7 @@ from its Start button; it reads its configuration from the environment: | `UNTOLD_BENCH_AUTOSTART` | `0` waits for the Start button | start on launch | | `UNTOLD_BENCH_IMMERSION` | `full` or `mixed` (visionOS) | `full` | | `UNTOLD_BENCH_PER_FRAME` | `0` writes one line per second instead of per frame | per frame | +| `UNTOLD_BENCH_AA` | `fxaa`, `smaa`, `msaa` or `none` for every scene (the post-FX scene keeps SMAA) | engine default, FXAA | On a device the app exits when the run is done so that `devicectl ... --console` returns; the run folder is then pulled from the app's Documents container. With the app opened from Xcode, set diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift index e8b16ab85..8b94110fb 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift @@ -24,6 +24,7 @@ import Foundation /// | `UNTOLD_BENCH_AUTOSTART` | `0` waits for the Start button (visionOS) | start on launch | /// | `UNTOLD_BENCH_IMMERSION` | `full` or `mixed` (visionOS) | `full` | /// | `UNTOLD_BENCH_PER_FRAME` | `0` records one line per second instead of per frame | per frame | +/// | `UNTOLD_BENCH_AA` | `fxaa`, `smaa`, `msaa` or `none`: anti-aliasing for every scene (the post-FX scene keeps SMAA) | engine default (FXAA) | public struct BenchConfig: Sendable { public var sceneIDs: [String] public var warmupSeconds: Double @@ -35,6 +36,8 @@ public struct BenchConfig: Sendable { public var autoStart: Bool public var immersion: String public var perFrame: Bool + /// Anti-aliasing mode name from the environment, or nil to leave the engine's default. + public var antiAliasing: String? public static func fromEnvironment(_ environment: [String: String] = ProcessInfo.processInfo.environment) -> BenchConfig { func double(_ key: String, _ fallback: Double) -> Double { @@ -65,7 +68,8 @@ public struct BenchConfig: Sendable { exitWhenDone: environment["UNTOLD_BENCH_KEEP_OPEN"] != "1", autoStart: environment["UNTOLD_BENCH_AUTOSTART"] != "0", immersion: environment["UNTOLD_BENCH_IMMERSION"] ?? "full", - perFrame: environment["UNTOLD_BENCH_PER_FRAME"] != "0" + perFrame: environment["UNTOLD_BENCH_PER_FRAME"] != "0", + antiAliasing: environment["UNTOLD_BENCH_AA"].map { $0.lowercased() } ) } diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift index df6c667a2..17681026a 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchResults.swift @@ -66,6 +66,8 @@ public struct BenchRenderSettings: Codable, Sendable { public var vsync: Bool = true /// Refresh rate the frame budget is derived from (0 when unknown). public var displayRefreshHz: Double = 0.0 + /// Anti-aliasing mode the scenes ran with (the post-FX scene always uses SMAA). + public var antiAliasing: String = "fxaa" public init(platform: String) { self.platform = platform diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift index 232405fb3..b90c20243 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchRunner.swift @@ -98,6 +98,17 @@ public final class BenchRunner: ObservableObject, @unchecked Sendable { // refresh was skipped: more than one and a half intervals. EngineStatsMonitor.shared.frameBudgetMs = 1.5 * 1000.0 / render.displayRefreshHz } + if let mode = config.antiAliasing { + switch mode { + case "none": setRendering(.antiAliasing(.none)) + case "fxaa": setRendering(.antiAliasing(.fxaa)) + case "smaa": setRendering(.antiAliasing(.smaa)) + case "msaa": setRendering(.antiAliasing(.msaa)) + default: print("PERFBENCH unknown UNTOLD_BENCH_AA value \(mode); keeping the engine default") + } + } + render.antiAliasing = String(describing: antiAliasingMode) + summary.render = render gameMode = true setState(.building(0)) } diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift index 5250e04db..51c217e2b 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchScenes.swift @@ -53,6 +53,7 @@ public extension BenchScene { public enum BenchScenes { public static let all: [BenchScene] = [ + SingleCubeScene(), PrimitivesScene(id: "primitives-1k", gridSize: 32, spacing: 1.0, batched: false, pointLights: 4), PrimitivesScene(id: "primitives-10k", gridSize: 100, spacing: 1.0, batched: true, pointLights: 4), PrimitivesScene(id: "lights-64", gridSize: 32, spacing: 1.0, batched: true, pointLights: 64), @@ -160,6 +161,32 @@ enum BenchSceneBuilder { // MARK: - Scenes +/// One cube and a sun: the fixed cost of a frame with almost nothing in it, and the scene to judge +/// image quality on a headset (edge stability, shadow swim, reprojection) without any load. +final class SingleCubeScene: BenchScene { + let id = "cube" + let title = "A single cube" + let notes = "One lit, shadowed cube in front of the viewer; measures the frame's fixed per-frame cost." + let orbit = BenchCameraOrbit(center: simd_float3(0, 0.5, 0), radius: 4.0, height: 1.5, period: 14.0) + + func build(origin: simd_float3) { + BenchSceneBuilder.makeCamera(eye: origin + orbit.eye(at: 0), target: origin + simd_float3(0, 0.5, 0)) + BenchSceneBuilder.makeSun() + let cube = createEntity() + setEntityName(entityId: cube, name: "Bench Cube") + setEntityMeshDirect(entityId: cube, meshes: BasicPrimitives.createCube(extent: 0.5), assetName: "bench_single_cube") + translateTo(entityId: cube, position: origin + simd_float3(0, 0.5, 0)) + updateMaterialColor(entityId: cube, color: Color(hue: 0.58, saturation: 0.6, brightness: 0.9)) + updateMaterialRoughness(entityId: cube, roughness: 0.6) + let floor = createEntity() + setEntityName(entityId: floor, name: "Bench Floor") + setEntityMeshDirect(entityId: floor, meshes: BasicPrimitives.createPlane(width: 6.0, depth: 6.0), assetName: "bench_floor") + translateTo(entityId: floor, position: origin) + updateMaterialColor(entityId: floor, color: Color(hue: 0.1, saturation: 0.15, brightness: 0.8)) + updateMaterialRoughness(entityId: floor, roughness: 0.9) + } +} + /// A grid of individual primitives: draw-call and per-entity CPU cost, shadows, a few lights. final class PrimitivesScene: BenchScene { let id: String diff --git a/scripts/perf/compare_baseline.py b/scripts/perf/compare_baseline.py index 42b66a7de..950420887 100755 --- a/scripts/perf/compare_baseline.py +++ b/scripts/perf/compare_baseline.py @@ -48,7 +48,7 @@ def rate(numerator, denominator): def config_key(render): return (render["platform"], render.get("layout"), render.get("foveation"), render.get("viewportWidth"), - render.get("viewportHeight"), render.get("viewCount")) + render.get("viewportHeight"), render.get("viewCount"), render.get("antiAliasing", "fxaa")) def aggregate(scenes): From 4f57c33bacb7c0fb29ef53af2805ed64f2077f1d Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sat, 3 Oct 2026 20:25:34 +0200 Subject: [PATCH 16/17] [Feature] PerfBench: choose the display, show the drawable 1:1 and key baselines by refresh rate On a Mac with several displays the bench opened on NSScreen.main, the screen with keyboard focus, so the same command was paced at 60, 100 or 120 Hz depending on where the terminal was. It now runs on the fastest connected display, or on one with the rate given by --refresh / UNTOLD_BENCH_REFRESH, and keeps its window inside that display. The window shows the fixed 1920x1080 drawable one drawable pixel per device pixel. Stretched over a Retina display, the window server rescaled it every frame on the GPU being measured: mean GPU time of the light scenes was 3.3 to 5.6 ms and varied between runs, against 1.2 to 1.8 ms and stable at 1:1. Comparer: - The refresh rate is part of a baseline's configuration (times taken at different rates are not comparable); a mismatch now says which setting differs. - Rates are aggregated by the best run, like times. Other activity on the machine makes the window server skip refreshes in bursts, which only inflates the over-budget rate; a real regression raises it in every run. - GPU times are judged only when the baseline shows the GPU busy for at least half the frame, as on a headset, and printed as information otherwise: with the GPU mostly idle they follow its clock state (one frame's minimum GPU time measured 0.8 and 2.2 ms in consecutive runs of the same build). - A GPU or CPU time also has to move by more than an absolute slack to count. --- Examples/PerfBench/README.md | 30 +++++-- .../Sources/PerfBenchCore/BenchConfig.swift | 6 +- .../PerfBench/Sources/macOS/AppDelegate.swift | 50 ++++++++++- scripts/perf/compare_baseline.py | 89 +++++++++++++++---- scripts/perf/run_bench.sh | 8 +- 5 files changed, 154 insertions(+), 29 deletions(-) diff --git a/Examples/PerfBench/README.md b/Examples/PerfBench/README.md index 0f69a5cd9..0e50f9e82 100644 --- a/Examples/PerfBench/README.md +++ b/Examples/PerfBench/README.md @@ -37,6 +37,14 @@ per-system CPU times in the snapshots; frame time and the over-budget count beco when a scene no longer fits. A frame counts as over budget when it took more than one and a half refresh intervals, that is when a refresh was skipped. +On a Mac with several displays the bench opens on the fastest one, whichever screen has the +keyboard focus, or on one running at the rate given with `--refresh`. Times taken at different +refresh rates are not comparable (the GPU stretches its work over the slack a slower display +leaves), so the refresh rate is part of a baseline's configuration and the comparer skips a +baseline recorded at another rate. The Mac16,5 baseline is recorded at 120 Hz, on the built-in +display. The window shows the fixed 1920 by 1080 drawable one drawable pixel per device pixel, so +the window server does not rescale it on the GPU that is being measured. + ## Running ```bash @@ -54,14 +62,23 @@ scripts/perf/run_bench.sh visionos --device --xctrace "Metal System Trace Runs land in `perf/results/-/` with `.jsonl`, `summary.json` and the console log. The comparison prints one block per scene and exits non-zero on a regression (10 % on frame-time, GPU-time and per-system CPU metrics, 0.5 points on the over-budget and -missed-deadline rates, 20 % on a per-pass GPU minimum). +missed-deadline rates, 20 % on a per-pass GPU minimum). A GPU or CPU time also has to move by +more than an absolute slack to count (0.5 ms for a frame's minimum GPU time, 0.25 ms for a pass, +0.1 ms for a per-system CPU mean). Single runs are noisy on a machine that idles most of each frame: CPU and GPU clocks drift between -runs and the same build can differ by 20 to 50 % in a per-pass or per-system time. `--repeat 3` -runs the scene set three times and the comparer aggregates by the minimum of each time metric (a -clock drift only ever inflates a time) and the mean of each rate; record the baseline the same way. -The CPU-bound scenes (`primitives-10k`) are the least noisy; for the others read -`timingMeanMs` and `gpuPassMinMs` rather than frame time. +runs, other activity makes the window server skip refreshes, and the same build can differ by 20 +to 50 % in a per-pass or per-system time. `--repeat 3` runs the scene set three times and the +comparer aggregates by the minimum of each time metric and of each rate (interference only ever +inflates them, and a real regression raises them in every run); record the baseline the same way. + +What to trust on a Mac: the per-system CPU means (`timingMeanMs`) repeat within a few percent, and +so does everything in the CPU-bound scenes (`primitives-10k`). GPU times of the light scenes do +not: with the GPU idle most of the frame they follow its clock state, and one frame's minimum GPU +time measured 0.8 and 2.2 ms in consecutive runs of the same build. The comparer therefore judges +GPU times only when the baseline shows the GPU busy for at least half the frame, and prints them +as information otherwise; judge GPU cost on the headset, where the GPU is busy and run-to-run +noise is under 10 %. The app can also be opened in Xcode (`xcodegen generate`, then `PerfBench.xcodeproj`) and started from its Start button; it reads its configuration from the environment: @@ -79,6 +96,7 @@ from its Start button; it reads its configuration from the environment: | `UNTOLD_BENCH_IMMERSION` | `full` or `mixed` (visionOS) | `full` | | `UNTOLD_BENCH_PER_FRAME` | `0` writes one line per second instead of per frame | per frame | | `UNTOLD_BENCH_AA` | `fxaa`, `smaa`, `msaa` or `none` for every scene (the post-FX scene keeps SMAA) | engine default, FXAA | +| `UNTOLD_BENCH_REFRESH` | macOS: run on a connected display with this maximum refresh rate (`run_bench.sh --refresh`) | the fastest connected display | On a device the app exits when the run is done so that `devicectl ... --console` returns; the run folder is then pulled from the app's Documents container. With the app opened from Xcode, set diff --git a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift index 8b94110fb..055d18e34 100644 --- a/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift +++ b/Examples/PerfBench/Sources/PerfBenchCore/BenchConfig.swift @@ -25,6 +25,7 @@ import Foundation /// | `UNTOLD_BENCH_IMMERSION` | `full` or `mixed` (visionOS) | `full` | /// | `UNTOLD_BENCH_PER_FRAME` | `0` records one line per second instead of per frame | per frame | /// | `UNTOLD_BENCH_AA` | `fxaa`, `smaa`, `msaa` or `none`: anti-aliasing for every scene (the post-FX scene keeps SMAA) | engine default (FXAA) | +/// | `UNTOLD_BENCH_REFRESH` | macOS: run on a connected display with this maximum refresh rate, for example `120` | the fastest connected display | public struct BenchConfig: Sendable { public var sceneIDs: [String] public var warmupSeconds: Double @@ -38,6 +39,8 @@ public struct BenchConfig: Sendable { public var perFrame: Bool /// Anti-aliasing mode name from the environment, or nil to leave the engine's default. public var antiAliasing: String? + /// Refresh rate of the display to run on (macOS), or nil for the fastest connected display. + public var refreshHz: Int? public static func fromEnvironment(_ environment: [String: String] = ProcessInfo.processInfo.environment) -> BenchConfig { func double(_ key: String, _ fallback: Double) -> Double { @@ -69,7 +72,8 @@ public struct BenchConfig: Sendable { autoStart: environment["UNTOLD_BENCH_AUTOSTART"] != "0", immersion: environment["UNTOLD_BENCH_IMMERSION"] ?? "full", perFrame: environment["UNTOLD_BENCH_PER_FRAME"] != "0", - antiAliasing: environment["UNTOLD_BENCH_AA"].map { $0.lowercased() } + antiAliasing: environment["UNTOLD_BENCH_AA"].map { $0.lowercased() }, + refreshHz: environment["UNTOLD_BENCH_REFRESH"].flatMap { Int($0) }.flatMap { $0 > 0 ? $0 : nil } ) } diff --git a/Examples/PerfBench/Sources/macOS/AppDelegate.swift b/Examples/PerfBench/Sources/macOS/AppDelegate.swift index 05b692236..849aa21fa 100644 --- a/Examples/PerfBench/Sources/macOS/AppDelegate.swift +++ b/Examples/PerfBench/Sources/macOS/AppDelegate.swift @@ -25,14 +25,30 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var renderer: UntoldRenderer! private var runner: BenchRunner! + /// The display the bench runs on: one whose maximum refresh rate is the requested one, else + /// the fastest connected display. Not `NSScreen.main`, which is the screen with keyboard + /// focus and so depends on where the terminal that launched the bench happens to be. + private static func benchScreen(requestedHz: Int?) -> NSScreen? { + let screens = NSScreen.screens + if let requestedHz { + if let match = screens.first(where: { $0.maximumFramesPerSecond == requestedHz }) { + return match + } + let rates = screens.map { String($0.maximumFramesPerSecond) }.joined(separator: ", ") + print("PERFBENCH_WARNING no connected display runs at \(requestedHz) Hz (connected: \(rates) Hz); using the fastest one") + } + return screens.max { $0.maximumFramesPerSecond < $1.maximumFramesPerSecond } + } + func applicationDidFinishLaunching(_: Notification) { let config = BenchConfig.fromEnvironment() + let screen = Self.benchScreen(requestedHz: config.refreshHz) var render = BenchRenderSettings(platform: "macOS") render.viewportWidth = Int(Constants.drawableSize.width) render.viewportHeight = Int(Constants.drawableSize.height) render.layout = "single" render.vsync = true - render.displayRefreshHz = Double(NSScreen.main?.maximumFramesPerSecond ?? 60) + render.displayRefreshHz = Double(screen?.maximumFramesPerSecond ?? 60) runner = BenchRunner(config: config, render: render) guard let renderer = UntoldRenderer.create() else { @@ -52,9 +68,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate { (view.layer as? CAMetalLayer)?.contentsScale = 1.0 renderInfo.viewPort = simd_float2(Float(Constants.drawableSize.width), Float(Constants.drawableSize.height)) + // The drawable keeps its fixed pixel size. The window shows it one drawable pixel per + // device pixel, so the window server presents it without rescaling, and is scaled down + // further only if the chosen display is smaller than that, so it never hangs over onto a + // neighbouring display that refreshes at another rate. + let styleMask: NSWindow.StyleMask = [.titled, .closable] + let backingScale = max(1.0, screen?.backingScaleFactor ?? 1.0) + var contentSize = CGSize( + width: Constants.drawableSize.width / backingScale, + height: Constants.drawableSize.height / backingScale + ) + if let screen { + let visible = screen.visibleFrame + let framed = NSWindow.frameRect(forContentRect: NSRect(origin: .zero, size: contentSize), styleMask: styleMask) + let chromeHeight = framed.height - contentSize.height + let fit = min(1.0, visible.width / contentSize.width, (visible.height - chromeHeight) / contentSize.height) + contentSize = CGSize(width: floor(contentSize.width * fit), height: floor(contentSize.height * fit)) + } window = NSWindow( - contentRect: NSRect(origin: .zero, size: Constants.drawableSize), - styleMask: [.titled, .closable], + contentRect: NSRect(origin: .zero, size: contentSize), + styleMask: styleMask, backing: .buffered, defer: false ) @@ -64,7 +97,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate { var viewOptions = UntoldViewOptions.default viewOptions.preferredFramesPerSecond = Int(render.displayRefreshHz) window.contentView = NSHostingView(rootView: BenchView(renderer: renderer, runner: runner, viewOptions: viewOptions)) - window.center() + if let screen { + // Centre the window on the chosen display; the display it sits on paces its frames. + let visible = screen.visibleFrame + window.setFrameOrigin(NSPoint( + x: visible.midX - window.frame.width / 2, + y: visible.midY - window.frame.height / 2 + )) + } else { + window.center() + } window.makeKeyAndOrderFront(nil) NSApp.setActivationPolicy(.regular) NSApp.activate(ignoringOtherApps: true) diff --git a/scripts/perf/compare_baseline.py b/scripts/perf/compare_baseline.py index 950420887..462a91f55 100755 --- a/scripts/perf/compare_baseline.py +++ b/scripts/perf/compare_baseline.py @@ -5,13 +5,22 @@ app wrote it, so a baseline is only ever compared with the same platform, layout, foveation and viewport. Exit status 1 means at least one metric regressed past its threshold. -Several summaries may be given (repeated runs): time metrics are aggregated by their minimum -across runs, rates by their mean. On a lightly loaded machine CPU and GPU clocks drift between -runs and only ever inflate a time, so the minimum over a few runs is the stable number. +Several summaries may be given (repeated runs): every time metric and every rate is aggregated by +its minimum across runs. On a lightly loaded machine CPU and GPU clocks drift between runs, and +other activity on the machine makes it skip refreshes; both only ever inflate a time or a rate, so +the minimum over a few runs is the stable number, and a real regression raises it in every run. + +GPU times of a scene that leaves the GPU mostly idle follow the GPU's clock state more than the +workload (the same build measured 0.8 and 2.2 ms for one frame in consecutive runs). They are +judged only when the baseline shows the GPU busy for at least --gpu-busy-fraction of the frame, as +it is on a headset, and reported as information otherwise. A GPU or CPU time also only counts as +changed when it moves by more than the relative tolerance and by more than an absolute slack. compare_baseline.py perf/results//summary.json [more summary.json ...] [--baselines DIR] [--update] [--time-tolerance 0.10] [--rate-tolerance 0.005] [--pass-tolerance 0.20] + [--gpu-slack-ms 0.5] [--pass-slack-ms 0.25] [--cpu-slack-ms 0.1] + [--gpu-busy-fraction 0.5] """ import argparse import json @@ -46,13 +55,44 @@ def rate(numerator, denominator): return (numerator / denominator) if denominator else 0.0 +# Render settings a baseline must share with a run to be compared with it, with the value assumed +# for records written before the field existed. +CONFIG_FIELDS = [ + ("platform", None), ("layout", None), ("foveation", None), ("viewportWidth", None), + ("viewportHeight", None), ("viewCount", None), ("antiAliasing", "fxaa"), + # Frames are display-paced, and the GPU stretches its work over the slack a slower display + # leaves, so times taken at different refresh rates are not comparable. + ("displayRefreshHz", 0.0), +] + + def config_key(render): - return (render["platform"], render.get("layout"), render.get("foveation"), render.get("viewportWidth"), - render.get("viewportHeight"), render.get("viewCount"), render.get("antiAliasing", "fxaa")) + return tuple(render.get(name, default) for name, default in CONFIG_FIELDS) + + +def config_difference(a, b): + """The render settings in which two records differ, as text.""" + parts = [] + for name, default in CONFIG_FIELDS: + left, right = a.get(name, default), b.get(name, default) + if left != right: + parts.append(f"{name} {left} vs {right}") + return ", ".join(parts) + + +def scene_rates(summary): + """(over-budget rate, missed-deadline rate) of a scene summary, single run or aggregate.""" + over = summary.get("overBudgetRate") + if over is None: + over = rate(summary.get("framesOverBudget", 0), summary.get("frames", 0)) + missed = summary.get("missedDeadlineRate") + if missed is None: + missed = rate(summary.get("missedDeadlines", 0), summary.get("deadlineSamples", 0)) + return over, missed def aggregate(scenes): - """Fold the same scene from several runs into one record: min of times, mean of rates.""" + """Fold the same scene from several runs into one record: min of times and of rates.""" first = json.loads(json.dumps(scenes[0])) s = first["summary"] for key, _ in FRAME_METRICS + INFO_METRICS: @@ -63,6 +103,9 @@ def aggregate(scenes): s["framesOverBudget"] = sum(x["summary"].get("framesOverBudget", 0) for x in scenes) s["missedDeadlines"] = sum(x["summary"].get("missedDeadlines", 0) for x in scenes) s["deadlineSamples"] = sum(x["summary"].get("deadlineSamples", 0) for x in scenes) + # The counts above are totals, for the record; the rates that are judged are the best run's. + s["overBudgetRate"] = min(scene_rates(x["summary"])[0] for x in scenes) + s["missedDeadlineRate"] = min(scene_rates(x["summary"])[1] for x in scenes) for field in ("gpuPassMeanMs", "gpuPassMinMs", "timingMeanMs"): merged = {} for x in scenes: @@ -82,6 +125,11 @@ def main(): ap.add_argument("--time-tolerance", type=float, default=0.10, help="relative slack for frame and system time metrics") ap.add_argument("--rate-tolerance", type=float, default=0.005, help="absolute slack for over-budget and missed-deadline rates") ap.add_argument("--pass-tolerance", type=float, default=0.20, help="relative slack for per-pass GPU minimums") + ap.add_argument("--gpu-slack-ms", type=float, default=0.5, help="a frame's minimum GPU time must also move by more than this") + ap.add_argument("--pass-slack-ms", type=float, default=0.25, help="a per-pass GPU minimum must also move by more than this") + ap.add_argument("--cpu-slack-ms", type=float, default=0.1, help="a per-system CPU mean must also move by more than this") + ap.add_argument("--gpu-busy-fraction", type=float, default=0.5, + help="judge GPU times only when the baseline's mean GPU time is at least this fraction of its mean frame time") args = ap.parse_args() runs = [load(p) for p in args.summaries] @@ -117,8 +165,7 @@ def main(): for scene in scenes: path = os.path.join(base_dir, f"{scene['id']}.json") s = scene["summary"] - over = rate(s.get("framesOverBudget", 0), s.get("frames", 0)) - missed = rate(s.get("missedDeadlines", 0), s.get("deadlineSamples", 0)) + over, missed = scene_rates(s) head = (f"{scene['id']}: frames {s.get('frames',0)} p95 {s.get('p95FrameMs',0):.2f} p99 {s.get('p99FrameMs',0):.2f} " f"gpu {s.get('meanGPUExecutionMs',0):.2f} overBudget {over*100:.2f}% missed {missed*100:.2f}%") if not os.path.exists(path): @@ -126,20 +173,27 @@ def main(): continue base = load(path) if config_key(base["render"]) != config_key(first["render"]): - print(f"{head} [baseline has a different render configuration, skipped]") + difference = config_difference(base["render"], first["render"]) + print(f"{head} [baseline has a different render configuration ({difference}), skipped]") continue b = base["scene"]["summary"] - bover = rate(b.get("framesOverBudget", 0), b.get("frames", 0)) - bmissed = rate(b.get("missedDeadlines", 0), b.get("deadlineSamples", 0)) + bover, bmissed = scene_rates(b) print(head) - def judge(label, now, ref, tolerance, unit="ms", relative=True): + # With the GPU idle most of the frame its times follow the clock state, not the workload. + gpu_busy = rate(b.get("meanGPUExecutionMs", 0.0), b.get("meanFrameMs", 0.0)) >= args.gpu_busy_fraction + + def judge(label, now, ref, tolerance, unit="ms", relative=True, slack=0.0, informational=False): nonlocal regressions if relative: if ref <= 0: return delta = (now - ref) / ref - flag = "REGRESSION" if delta > tolerance else ("better" if delta < -tolerance else "ok") + if informational: + print(f" {label:28s} {now:9.3f} vs {ref:9.3f} {unit} {delta*100:+7.1f}% (info, GPU mostly idle)") + return + moved = abs(now - ref) > slack + flag = "REGRESSION" if delta > tolerance and moved else ("better" if delta < -tolerance and moved else "ok") print(f" {label:28s} {now:9.3f} vs {ref:9.3f} {unit} {delta*100:+7.1f}% {flag}") else: delta = now - ref @@ -149,7 +203,9 @@ def judge(label, now, ref, tolerance, unit="ms", relative=True): regressions += 1 for key, label in FRAME_METRICS: - judge(label, s.get(key, 0.0), b.get(key, 0.0), args.time_tolerance) + is_gpu = key == "minGPUExecutionMs" + judge(label, s.get(key, 0.0), b.get(key, 0.0), args.time_tolerance, + slack=args.gpu_slack_ms if is_gpu else 0.0, informational=is_gpu and not gpu_busy) for key, label in INFO_METRICS: now, ref = s.get(key, 0.0), b.get(key, 0.0) if ref > 0: @@ -159,7 +215,7 @@ def judge(label, now, ref, tolerance, unit="ms", relative=True): tnow, tref = s.get("timingMeanMs", {}), b.get("timingMeanMs", {}) for field in TIMING_FIELDS: if field in tnow and field in tref and tref[field] >= 0.02: - judge(f"cpu {field}", tnow[field], tref[field], args.time_tolerance) + judge(f"cpu {field}", tnow[field], tref[field], args.time_tolerance, slack=args.cpu_slack_ms) pnow, pref = s.get("gpuPassMinMs", {}), b.get("gpuPassMinMs", {}) untimed = sorted(label for label in pref if label not in pnow and pref[label] >= 0.05) if untimed: @@ -169,7 +225,8 @@ def judge(label, now, ref, tolerance, unit="ms", relative=True): print(f" gpu passes not judged: {len(untimed)} baseline label(s) this run did not time ({shown}); re-record the baseline") for label in sorted(pref): if label in pnow and pref[label] >= 0.05: - judge(f"gpu min {label[:20]}", pnow[label], pref[label], args.pass_tolerance) + judge(f"gpu min {label[:20]}", pnow[label], pref[label], args.pass_tolerance, + slack=args.pass_slack_ms, informational=not gpu_busy) if regressions: print(f"{regressions} regression(s)") return 1 diff --git a/scripts/perf/run_bench.sh b/scripts/perf/run_bench.sh index d920ab4f2..1d2f5e480 100755 --- a/scripts/perf/run_bench.sh +++ b/scripts/perf/run_bench.sh @@ -15,7 +15,9 @@ # --out DIR where run folders land (default: perf/results) # --label TEXT stored in summary.json (default: git describe) # --config Debug|Release (default: Release) -# --repeat N run the whole scene set N times and aggregate (min of times, mean of rates) +# --repeat N run the whole scene set N times and aggregate (minimum of each time and rate) +# --refresh N macOS: run on a connected display whose maximum refresh rate is N Hz +# (default: the fastest connected display). Baselines are keyed by the rate. # --xctrace TEMPLATE also record an Instruments trace, e.g. "Metal System Trace" (device runs) # --no-compare skip the baseline comparison # --update-baseline write this run as the new baseline for the device model @@ -43,6 +45,7 @@ XCTRACE_TEMPLATE="" COMPARE=1 UPDATE_BASELINE=0 REPEAT=1 +REFRESH="" while [ $# -gt 0 ]; do case "$1" in @@ -54,6 +57,7 @@ while [ $# -gt 0 ]; do --config) CONFIG="$2"; shift 2 ;; --device) DEVICE="$2"; shift 2 ;; --repeat) REPEAT="$2"; shift 2 ;; + --refresh) REFRESH="$2"; shift 2 ;; --xctrace) XCTRACE_TEMPLATE="$2"; shift 2 ;; --no-compare) COMPARE=0; shift ;; --update-baseline) UPDATE_BASELINE=1; shift ;; @@ -95,7 +99,7 @@ case "$PLATFORM" in echo "== Running $APP" UNTOLD_BENCH_SCENES="$SCENES" UNTOLD_BENCH_SECONDS="$SECONDS_PER_SCENE" UNTOLD_BENCH_WARMUP="$WARMUP" \ UNTOLD_BENCH_OUTPUT="$OUT_DIR" UNTOLD_BENCH_RUN_ID="$RUN_ID" UNTOLD_BENCH_LABEL="$LABEL" \ - UNTOLD_STATS=1 \ + UNTOLD_STATS=1 UNTOLD_BENCH_REFRESH="$REFRESH" \ "$APP/Contents/MacOS/PerfBench" | tee "$OUT_DIR/$RUN_ID.log" | grep -v "^PERFBENCH_SUMMARY_JSON" ;; From 51873ca5592b91d80e861c4510e9423f2aa7c17a Mon Sep 17 00:00:00 2001 From: Javier Segura Date: Sat, 3 Oct 2026 20:25:34 +0200 Subject: [PATCH 17/17] [Chores] Re-record the Mac16,5 baseline at 120 Hz Three-run aggregate of all eight scenes (the single-cube scene had none) at 88737f37, on the built-in display at 120 Hz with the 1:1 window. The earlier baseline predates the merge of develop and the window change. Per-system CPU means repeat within a few percent across the three runs (encode 6.32 / 6.39 / 6.34 ms in primitives-1k, 17.5 / 18.0 / 18.1 ms in primitives-10k). One of the three runs was free of skipped refreshes; the other two had bursts from other activity on the machine, which the best-run aggregation leaves out. --- .../baselines/Mac16_5/macOS/animation-16.json | 322 ++++++------- perf/baselines/Mac16_5/macOS/cube.json | 440 ++++++++++++++++++ perf/baselines/Mac16_5/macOS/gaussian.json | 338 +++++++------- perf/baselines/Mac16_5/macOS/lights-64.json | 334 ++++++------- perf/baselines/Mac16_5/macOS/postfx.json | 344 +++++++------- .../Mac16_5/macOS/primitives-10k.json | 334 ++++++------- .../Mac16_5/macOS/primitives-1k.json | 324 ++++++------- perf/baselines/Mac16_5/macOS/stadium.json | 324 ++++++------- 8 files changed, 1614 insertions(+), 1146 deletions(-) create mode 100644 perf/baselines/Mac16_5/macOS/cube.json diff --git a/perf/baselines/Mac16_5/macOS/animation-16.json b/perf/baselines/Mac16_5/macOS/animation-16.json index 3d2896526..c8f063e75 100644 --- a/perf/baselines/Mac16_5/macOS/animation-16.json +++ b/perf/baselines/Mac16_5/macOS/animation-16.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "animation-16.jsonl", @@ -74,165 +75,165 @@ ], "usedHZB": true }, - "frameIndex": 1197, + "frameIndex": 1799, "gpuPasses": { - "frameIndex": 1197, + "frameIndex": 1799, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.521 + "ms": 0.012083 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.007125 + "ms": 0.012708 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.0135 + "ms": 0.102 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.114333 + "ms": 0.127083 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.021208 + "ms": 0.058375 }, { "count": 1, "label": "Grid Pass", - "ms": 0.438792 + "ms": 0.270292 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.04525 + "ms": 0.169583 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.043 + "ms": 0.076709 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.27875 + "ms": 0.473792 }, { "count": 1, - "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.040959 + "label": "Copy Opaque Depth for HZB", + "ms": 0.073833 }, { "count": 1, - "label": "Gaussian Pass", - "ms": 0.340041 + "label": "Transparency Pass", + "ms": 0.084708 }, { "count": 1, - "label": "Copy Opaque Depth for HZB", - "ms": 0.036208 + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.0705 }, { "count": 1, - "label": "Transparency Pass", - "ms": 0.042083 + "label": "Gaussian Pass", + "ms": 0.2585 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.359625 + "ms": 0.285833 }, { - "count": 1, + "count": 2, "label": "Post-Processing Pass", - "ms": 0.125458 + "ms": 0.601083 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.166959 + "ms": 0.501208 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.547042 + "ms": 0.496625 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.028 + "ms": 0.08475 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.011542 + "ms": 0.036083 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.00775 + "ms": 0.0195 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.006834 + "ms": 0.010625 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.006958 + "ms": 0.010541 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.006667 + "ms": 0.042125 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.006833 + "ms": 0.348167 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.00675 + "ms": 0.011375 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.007167 + "ms": 0.00975 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.006416 + "ms": 0.010459 } ], "passesSkipped": 0, - "totalMs": 3.2362500000000005 + "totalMs": 4.25829 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 121, - "framesOverBudget": 3, - "framesOverBudgetLastSecond": 0, - "framesSampled": 1198, + "framesLastSecond": 120, + "framesOverBudget": 1, + "framesOverBudgetLastSecond": 1, + "framesSampled": 1800, "histogram": [ 0, - 362, - 832, - 4, + 85, + 1714, + 1, 0, 0, 0 ], - "worstFrameMsLastSecond": 9.659666568040848 + "worstFrameMsLastSecond": 13.796666637063026 }, "memory": { "availableMemoryBytes": 0, @@ -249,8 +250,8 @@ "render": { "drawCallsBatched": 0, "drawCallsOpaque": 16, - "drawCallsShadow": 25, - "drawCallsTotal": 47, + "drawCallsShadow": 32, + "drawCallsTotal": 55, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -261,7 +262,7 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 17969, + "trianglesTotal": 21037, "visibleInstances": 16 }, "streaming": { @@ -294,141 +295,144 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": true, - "updateWorkMs": 0.06699562072753906, + "updateTriggered": false, + "updateWorkMs": 0, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133746.49413062504, + "timestampSeconds": 844262.2853940418, "timing": { - "animationMs": 0.4928333219140768, + "animationMs": 0.17341668717563152, "batchingRebuildMs": 0, - "batchingTickMs": 0.013041659258306026, - "cullingMs": 0.13866668450646102, - "customSystemsMs": 0, - "encodeMs": 0.8497916569467634, - "extensionsUpdateMs": 0.001541659003123641, - "frameTotalMs": 8.87024961411953, - "gameUpdateMs": 0.12612499995157123, - "geometryStreamingMs": 0.07050001295283437, - "gpuExecutionMs": 2.1647083340212703, - "gpuFrameCadenceMs": 6.172791647259146, - "lodMs": 0.003458320861682296, - "physicsMs": 0, - "physicsStepCount": 0, - "renderPrepMs": 0.14600000577047467, - "renderTotalMs": 6.797750014811754, - "scenegraphMs": 0.10000000474974513, - "scriptingMs": 0.00041667954064905643, - "semaphoreWaitMs": 0.000500003807246685, - "smoothedFrameMs": 8.284629105279842, - "streamingRegionMs": 0.004791683750227094, - "submitMs": 0.014249992091208696, - "updateMs": 0.748166668927297 + "batchingTickMs": 0.0008749775588512421, + "cullingMs": 0.04299997817724943, + "customSystemsMs": 0.0003332970663905144, + "encodeMs": 0.4212500061839819, + "extensionsUpdateMs": 0.000625033862888813, + "frameTotalMs": 8.460583165287971, + "gameUpdateMs": 0.01241674181073904, + "geometryStreamingMs": 0.0011665979400277138, + "gpuExecutionMs": 2.2618749644607306, + "gpuFrameCadenceMs": 8.281083428300917, + "lodMs": 0.001249951310455799, + "physicsMs": 0.00100000761449337, + "physicsStepCount": 1, + "renderPrepMs": 0.04670827183872461, + "renderTotalMs": 7.536666584201157, + "scenegraphMs": 0.013083335943520069, + "scriptingMs": 0.00037497375160455704, + "semaphoreWaitMs": 0.0003332970663905144, + "smoothedFrameMs": 8.354476342598597, + "streamingRegionMs": 0.00100000761449337, + "submitMs": 0.008499948307871819, + "updateMs": 0.20879169460386038 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "redplayer.untold with the running clip; measures the animation system and GPU skinning.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.988673708343413, + "durationSeconds": 14.98854420823045, "frameBudgetMs": 12.5, - "frames": 3600, - "framesOverBudget": 3, + "frames": 5396, + "framesOverBudget": 8, "gpuPassMeanMs": { - "Block Scan Pass": 0.008764747500000003, - "Compact and Stream Pass": 0.005156155833333331, - "Copy Opaque Depth for Gaussian Occlusion": 0.04244916916666665, - "Copy Opaque Depth for HZB": 0.0378395575, - "G-Buffer + Light Pass (TBDR)": 0.2660044399999998, - "Gaussian Pass": 0.34758923166666666, - "Grid Pass": 0.08369763083333334, - "HZB Build Mip 0": 0.35258437499999945, - "HZB Build Mip 1": 0.05212142083333336, - "HZB Build Mip 10": 0.007064885833333325, - "HZB Build Mip 2": 0.018006421666666633, - "HZB Build Mip 3": 0.01162080083333332, - "HZB Build Mip 4": 0.010021489999999982, - "HZB Build Mip 5": 0.00851657916666668, - "HZB Build Mip 6": 0.008395797500000015, - "HZB Build Mip 7": 0.007448001666666661, - "HZB Build Mip 8": 0.007368475833333343, - "HZB Build Mip 9": 0.006942558333333319, - "HZB Occlusion Culling pass": 0.014751043333333304, - "Local Scan Pass": 0.0069991541666666575, - "Mark Visible Pass": 0.008895924166666685, - "Output Transform Pass": 0.2298006041666669, - "Post-Processing Pass": 0.15901843999999998, - "Pre Composite Pass": 0.23769827166666677, - "Shadow Cascade 0": 0.041846876666666706, - "Shadow Cascade 1": 0.043767706666666614, - "Transparency Pass": 0.05802672499999997 + "Block Scan Pass": 0.022452563959955506, + "Compact and Stream Pass": 0.01394364738598441, + "Copy Opaque Depth for Gaussian Occlusion": 0.032583199331848646, + "Copy Opaque Depth for HZB": 0.03617071269487763, + "G-Buffer + Light Pass (TBDR)": 0.3598435540089086, + "Gaussian Pass": 0.11289490367483279, + "Grid Pass": 0.07198865979955452, + "HZB Build Mip 0": 0.11781574053452108, + "HZB Build Mip 1": 0.0965010857461025, + "HZB Build Mip 10": 0.028331292940522515, + "HZB Build Mip 2": 0.04045328309232477, + "HZB Build Mip 3": 0.036473774749721945, + "HZB Build Mip 4": 0.02983165072302554, + "HZB Build Mip 5": 0.026809998888271253, + "HZB Build Mip 6": 0.030125085094549555, + "HZB Build Mip 7": 0.024716972191323697, + "HZB Build Mip 8": 0.02379029342984408, + "HZB Build Mip 9": 0.021600150890868603, + "HZB Occlusion Culling pass": 0.02651586748329624, + "Local Scan Pass": 0.021684521714922113, + "Mark Visible Pass": 0.0301986987750557, + "Output Transform Pass": 0.22674751002227123, + "Post-Processing Pass": 0.3289736664810697, + "Pre Composite Pass": 0.11232453674832976, + "Shadow Cascade 0": 0.1105933868816008, + "Shadow Cascade 1": 0.0794014713729853, + "Transparency Pass": 0.042753628619153716 }, "gpuPassMinMs": { - "Block Scan Pass": 0.005333, - "Compact and Stream Pass": 0.002958, - "Copy Opaque Depth for Gaussian Occlusion": 0.02375, - "Copy Opaque Depth for HZB": 0.019, - "G-Buffer + Light Pass (TBDR)": 0.13925, - "Gaussian Pass": 0.094417, - "Grid Pass": 0.048083, - "HZB Build Mip 0": 0.083917, - "HZB Build Mip 1": 0.022416, - "HZB Build Mip 10": 0.003875, - "HZB Build Mip 2": 0.007916, - "HZB Build Mip 3": 0.004792, - "HZB Build Mip 4": 0.004333, - "HZB Build Mip 5": 0.004042, - "HZB Build Mip 6": 0.004, - "HZB Build Mip 7": 0.00425, - "HZB Build Mip 8": 0.004334, - "HZB Build Mip 9": 0.004, - "HZB Occlusion Culling pass": 0.00925, - "Local Scan Pass": 0.003958, - "Mark Visible Pass": 0.003375, - "Output Transform Pass": 0.084292, - "Post-Processing Pass": 0.098208, - "Pre Composite Pass": 0.174458, - "Shadow Cascade 0": 0.024291, - "Shadow Cascade 1": 0.023917, - "Transparency Pass": 0.007708 + "Block Scan Pass": 0.016166, + "Compact and Stream Pass": 0.007959, + "Copy Opaque Depth for Gaussian Occlusion": 0.021625, + "Copy Opaque Depth for HZB": 0.025083, + "G-Buffer + Light Pass (TBDR)": 0.295875, + "Gaussian Pass": 0.088041, + "Grid Pass": 0.055667, + "HZB Build Mip 0": 0.056417, + "HZB Build Mip 1": 0.02125, + "HZB Build Mip 10": 0.009083, + "HZB Build Mip 2": 0.011333, + "HZB Build Mip 3": 0.010333, + "HZB Build Mip 4": 0.009958, + "HZB Build Mip 5": 0.009666, + "HZB Build Mip 6": 0.01, + "HZB Build Mip 7": 0.009292, + "HZB Build Mip 8": 0.00925, + "HZB Build Mip 9": 0.009167, + "HZB Occlusion Culling pass": 0.020333, + "Local Scan Pass": 0.010917, + "Mark Visible Pass": 0.008209, + "Output Transform Pass": 0.171833, + "Post-Processing Pass": 0.20979199999999998, + "Pre Composite Pass": 0.063917, + "Shadow Cascade 0": 0.075292, + "Shadow Cascade 1": 0.072667, + "Transparency Pass": 0.018375 }, - "meanFrameMs": 8.331988514366124, - "meanGPUExecutionMs": 1.272507916679994, + "meanFrameMs": 8.336217815522105, + "meanGPUExecutionMs": 1.3271008954198829, + "minGPUExecutionMs": 1.0357083519920707, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 8.316708728671074, - "p95FrameMs": 9.707249701023102, - "p99FrameMs": 10.15379186719656, + "overBudgetRate": 0.0005555555555555556, + "p50FrameMs": 8.332124911248684, + "p95FrameMs": 8.65854136645794, + "p99FrameMs": 9.313291870057583, "peakGPUAllocatedBytes": 649805824, "timingMeanMs": { - "animationMs": 0.4680142585014836, - "batchingTickMs": 0.0029376386857213753, + "animationMs": 0.15608755300007035, + "batchingTickMs": 0.0005866262650259283, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 0.12474729472381232, - "customSystemsMs": 0.00021405763099036746, - "encodeMs": 0.6314588538466019, - "extensionsUpdateMs": 0.001295701292956027, - "gameUpdateMs": 0.10586601433279823, - "geometryStreamingMs": 0.013736018542274771, - "lodMs": 0.003851279978817624, - "physicsMs": 0.001089508726853229, - "renderPrepMs": 0.13288186956991865, - "renderTotalMs": 0.9796556337274376, - "scenegraphMs": 0.09701119907641899, - "scriptingMs": 0.0005386751940013695, - "semaphoreWaitMs": 0.0003422843340821533, - "streamingRegionMs": 0.002215393899181214, - "submitMs": 0.014653101153043623, - "updateMs": 0.7047359488249604 + "cullingMs": 0.03389899771034502, + "customSystemsMs": 8.899067063661967e-05, + "encodeMs": 0.28558319239396734, + "extensionsUpdateMs": 0.00033027311848164535, + "gameUpdateMs": 0.007790945828972622, + "geometryStreamingMs": 0.007614091578653582, + "lodMs": 0.0010145839529695547, + "physicsMs": 0.00036931876559587873, + "renderPrepMs": 0.03821946137403871, + "renderTotalMs": 2.553726041029828, + "scenegraphMs": 0.012343069569569995, + "scriptingMs": 0.0001886686176459658, + "semaphoreWaitMs": 7.111291801707241e-05, + "streamingRegionMs": 0.0005917752298444062, + "submitMs": 0.004427610784064217, + "updateMs": 0.18600326663211741 }, - "worstFrameMs": 10.473666712641716, - "worstThermalState": 0 + "worstFrameMs": 13.796666637063026, + "worstThermalState": 1 }, "title": "16 skinned characters running", "warmupSeconds": 3 diff --git a/perf/baselines/Mac16_5/macOS/cube.json b/perf/baselines/Mac16_5/macOS/cube.json new file mode 100644 index 000000000..1bb98a371 --- /dev/null +++ b/perf/baselines/Mac16_5/macOS/cube.json @@ -0,0 +1,440 @@ +{ + "device": { + "gpuName": "Apple M4 Max", + "isLowPowerMode": false, + "model": "Mac16,5", + "osVersion": "Version 27.0 (Build 26A428)", + "physicalMemoryBytes": 137438953472, + "processorCount": 16, + "thermalStateAtStart": 0 + }, + "label": "baseline 88737f37+120hz", + "render": { + "antiAliasing": "fxaa", + "displayRefreshHz": 120, + "foveation": false, + "immersion": "none", + "layout": "single", + "platform": "macOS", + "renderQuality": 1, + "viewCount": 1, + "viewportHeight": 1080, + "viewportWidth": 1920, + "vsync": true + }, + "runIDs": [ + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" + ], + "scene": { + "file": "cube.jsonl", + "id": "cube", + "lastSnapshot": { + "batching": { + "batchGroupCount": 0, + "batchedMeshCount": 0, + "deferredByWorkBudget": 0, + "dirtyCellsAfterPrune": 0, + "dirtyCellsBeforePrune": 0, + "dispatchedBuilds": 0, + "lastRebuildCostMs": 0, + "lastRebuildInputMeshCount": 0, + "lastRebuildOutputBatchCount": 0, + "rebuildsThisSecond": 0, + "skippedByComplexityGuard": 0 + }, + "compositor": { + "deadlineMarginMs": 0, + "deadlineSampleCount": 0, + "inputSlackMs": 0, + "missedDeadline": false, + "missedDeadlineCount": 0, + "missingAnchorCount": 0, + "presentationMarginMs": 0, + "submissionMs": 0, + "updateMs": 0, + "viewCount": 0, + "viewTextureHeight": 0, + "viewTextureWidth": 0 + }, + "culling": { + "frustumFailed": 0, + "frustumPassed": 2, + "frustumTested": 2, + "hzbIsValid": true, + "hzbMipCount": 11, + "occlusionFailed": 0, + "occlusionPassed": 2, + "occlusionTested": 2, + "optimizedFrustumPath": true, + "selectedHZBMipLevel": 0, + "selectedHZBMipSize": [ + 1920, + 1080 + ], + "usedHZB": true + }, + "frameIndex": 1763, + "gpuPasses": { + "frameIndex": 1764, + "passes": [ + { + "count": 1, + "label": "Mark Visible Pass", + "ms": 0.009916 + }, + { + "count": 1, + "label": "Local Scan Pass", + "ms": 0.138834 + }, + { + "count": 1, + "label": "Block Scan Pass", + "ms": 0.017333 + }, + { + "count": 1, + "label": "Compact and Stream Pass", + "ms": 0.0095 + }, + { + "count": 1, + "label": "HZB Occlusion Culling pass", + "ms": 0.025166 + }, + { + "count": 1, + "label": "Grid Pass", + "ms": 0.163625 + }, + { + "count": 1, + "label": "Shadow Cascade 0", + "ms": 0.06575 + }, + { + "count": 1, + "label": "Shadow Cascade 1", + "ms": 0.064834 + }, + { + "count": 1, + "label": "G-Buffer + Light Pass (TBDR)", + "ms": 0.743542 + }, + { + "count": 1, + "label": "Copy Opaque Depth for HZB", + "ms": 0.067709 + }, + { + "count": 1, + "label": "Transparency Pass", + "ms": 0.0775 + }, + { + "count": 1, + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.069959 + }, + { + "count": 1, + "label": "Gaussian Pass", + "ms": 0.354875 + }, + { + "count": 1, + "label": "Pre Composite Pass", + "ms": 0.420458 + }, + { + "count": 2, + "label": "Post-Processing Pass", + "ms": 0.585459 + }, + { + "count": 1, + "label": "Output Transform Pass", + "ms": 0.467958 + }, + { + "count": 1, + "label": "HZB Build Mip 0", + "ms": 1.036667 + }, + { + "count": 1, + "label": "HZB Build Mip 1", + "ms": 0.2185 + }, + { + "count": 1, + "label": "HZB Build Mip 2", + "ms": 0.035875 + }, + { + "count": 1, + "label": "HZB Build Mip 3", + "ms": 0.011042 + }, + { + "count": 1, + "label": "HZB Build Mip 4", + "ms": 0.010208 + }, + { + "count": 1, + "label": "HZB Build Mip 5", + "ms": 0.014875 + }, + { + "count": 1, + "label": "HZB Build Mip 6", + "ms": 0.01525 + }, + { + "count": 1, + "label": "HZB Build Mip 7", + "ms": 0.015125 + }, + { + "count": 1, + "label": "HZB Build Mip 8", + "ms": 0.013125 + }, + { + "count": 1, + "label": "HZB Build Mip 9", + "ms": 0.0105 + }, + { + "count": 1, + "label": "HZB Build Mip 10", + "ms": 0.009875 + } + ], + "passesSkipped": 0, + "totalMs": 4.67346 + }, + "hitches": { + "frameBudgetMs": 12.5, + "framesLastSecond": 121, + "framesOverBudget": 41, + "framesOverBudgetLastSecond": 0, + "framesSampled": 1764, + "histogram": [ + 0, + 347, + 1308, + 101, + 8, + 0, + 0 + ], + "worstFrameMsLastSecond": 9.72854159772396 + }, + "memory": { + "availableMemoryBytes": 0, + "geometryBudgetBytes": 27707694120, + "gpuAllocatedBytes": 484409344, + "isUnderPressure": false, + "meshMemoryBytes": 6308, + "textureBudgetBytes": 18471796080, + "textureMemoryBytes": 0, + "thermalState": 0, + "trackedEntityCount": 4, + "utilizationPercent": 1.3659743558491755e-07 + }, + "render": { + "drawCallsBatched": 0, + "drawCallsOpaque": 2, + "drawCallsShadow": 4, + "drawCallsTotal": 13, + "drawCallsTransparent": 0, + "tileFullDrawsEstimate": 0, + "tileFullTrianglesEstimate": 0, + "tileFullVisibleInstances": 0, + "tileHLODDrawsEstimate": 0, + "tileHLODTrianglesEstimate": 0, + "tileHLODVisibleInstances": 0, + "tileLODDrawsEstimate": 0, + "tileLODTrianglesEstimate": 0, + "tileLODVisibleInstances": 0, + "trianglesTotal": 55, + "visibleInstances": 2 + }, + "streaming": { + "activeLoads": 0, + "activeTileRepresentationFades": 0, + "availableLoadSlots": 3, + "averageAsyncLoadMs": 0, + "blockedByGateMs": 0, + "cachedMeshResources": 0, + "evictionsPerformed": 0, + "fullAndFallbackResidentOverlapTiles": 0, + "fullAndHLODVisibleOverlapTiles": 0, + "fullAndLODVisibleOverlapTiles": 0, + "lastApplyLoadedMeshMs": 0, + "loadCandidates": 0, + "loadedStreamingEntities": 0, + "loadingStreamingEntities": 0, + "lod0VisibilityWarnings": 0, + "lod0VisibilityWarningsNoFallback": 0, + "lod0VisibilityWarningsWithFallback": 0, + "lodAndHLODVisibleOverlapTiles": 0, + "nearbyEntitiesQueried": 4, + "pendingLoadBacklog": 0, + "pendingUploadCount": 0, + "residentFullTileRepresentations": 0, + "residentHLODRepresentations": 0, + "residentLODRepresentations": 0, + "residentMeshEntities": 0, + "tileRepresentationGapWarnings": 0, + "tileSwapWarnings": 0, + "tilesSkippedByHierarchyGate": 0, + "unloadedStreamingEntities": 0, + "updateTriggered": true, + "updateWorkMs": 0.012993812561035156, + "visibleFullTileRepresentations": 0, + "visibleHLODRepresentations": 0, + "visibleLODRepresentations": 0, + "waitingTileRepresentationFades": 0 + }, + "timestampSeconds": 844167.8094951252, + "timing": { + "animationMs": 0.000500003807246685, + "batchingRebuildMs": 0, + "batchingTickMs": 0.0007083872333168983, + "cullingMs": 0.029291724786162376, + "customSystemsMs": 0, + "encodeMs": 0.2549584023654461, + "extensionsUpdateMs": 0.0003334134817123413, + "frameTotalMs": 8.392499759793282, + "gameUpdateMs": 0.0076249707490205765, + "geometryStreamingMs": 0.013416633009910583, + "gpuExecutionMs": 2.504000090993941, + "gpuFrameCadenceMs": 4.919999977573752, + "lodMs": 0.0015416881069540977, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.03175006713718176, + "renderTotalMs": 8.35104170255363, + "scenegraphMs": 0.0033333199098706245, + "scriptingMs": 0.0003334134817123413, + "semaphoreWaitMs": 8.323695510625839e-05, + "smoothedFrameMs": 8.443654235452414, + "streamingRegionMs": 0.00045832712203264236, + "submitMs": 0.006125075742602348, + "updateMs": 0.01750001683831215 + } + }, + "measureSeconds": 15, + "notes": "One lit, shadowed cube in front of the viewer; measures the frame's fixed per-frame cost.", + "runs": 3, + "summary": { + "deadlineSamples": 0, + "durationSeconds": 14.983347166678868, + "frameBudgetMs": 12.5, + "frames": 5360, + "framesOverBudget": 44, + "gpuPassMeanMs": { + "Block Scan Pass": 0.024624993885491855, + "Compact and Stream Pass": 0.01679879321845464, + "Copy Opaque Depth for Gaussian Occlusion": 0.039220551417454175, + "Copy Opaque Depth for HZB": 0.04220511228460256, + "G-Buffer + Light Pass (TBDR)": 0.2972883885491937, + "Gaussian Pass": 0.138653429683157, + "Grid Pass": 0.09281012311977704, + "HZB Build Mip 0": 0.16089371428571395, + "HZB Build Mip 1": 0.10892386325736508, + "HZB Build Mip 10": 0.019424422335600863, + "HZB Build Mip 2": 0.05274613896609231, + "HZB Build Mip 3": 0.03989399319727893, + "HZB Build Mip 4": 0.03195254365079368, + "HZB Build Mip 5": 0.028550215986394626, + "HZB Build Mip 6": 0.02662751195108395, + "HZB Build Mip 7": 0.022798389455782345, + "HZB Build Mip 8": 0.020674705782312962, + "HZB Build Mip 9": 0.021773750972762636, + "HZB Occlusion Culling pass": 0.027827942618384442, + "Local Scan Pass": 0.019224730362117082, + "Mark Visible Pass": 0.015428300453514707, + "Output Transform Pass": 0.2240532551417458, + "Post-Processing Pass": 0.2881363451917733, + "Pre Composite Pass": 0.11820964146748203, + "Shadow Cascade 0": 0.08338528293496386, + "Shadow Cascade 1": 0.08078163201778764, + "Transparency Pass": 0.05468355753196216 + }, + "gpuPassMinMs": { + "Block Scan Pass": 0.004125, + "Compact and Stream Pass": 0.0025, + "Copy Opaque Depth for Gaussian Occlusion": 0.008583, + "Copy Opaque Depth for HZB": 0.01075, + "G-Buffer + Light Pass (TBDR)": 0.097917, + "Gaussian Pass": 0.033333, + "Grid Pass": 0.020958, + "HZB Build Mip 0": 0.027542, + "HZB Build Mip 1": 0.009458, + "HZB Build Mip 10": 0.003041, + "HZB Build Mip 2": 0.004917, + "HZB Build Mip 3": 0.004084, + "HZB Build Mip 4": 0.003667, + "HZB Build Mip 5": 0.0035, + "HZB Build Mip 6": 0.003708, + "HZB Build Mip 7": 0.003125, + "HZB Build Mip 8": 0.00325, + "HZB Build Mip 9": 0.003167, + "HZB Occlusion Culling pass": 0.007709, + "Local Scan Pass": 0.00325, + "Mark Visible Pass": 0.00275, + "Output Transform Pass": 0.058625, + "Post-Processing Pass": 0.07483300000000001, + "Pre Composite Pass": 0.030917, + "Shadow Cascade 0": 0.015916, + "Shadow Cascade 1": 0.015416, + "Transparency Pass": 0.013416 + }, + "meanFrameMs": 8.333307047012365, + "meanGPUExecutionMs": 1.303349569048515, + "minGPUExecutionMs": 0.381499994546175, + "missedDeadlineRate": 0.0, + "missedDeadlines": 0, + "overBudgetRate": 0.0, + "p50FrameMs": 8.333041332662106, + "p95FrameMs": 8.883541449904442, + "p99FrameMs": 9.314791299402714, + "peakGPUAllocatedBytes": 484409344, + "timingMeanMs": { + "animationMs": 0.0007290052922649516, + "batchingTickMs": 0.001079623749513441, + "compositorDeadlineMarginMs": 0, + "compositorInputSlackMs": 0, + "compositorSubmissionMs": 0, + "compositorUpdateMs": 0, + "cullingMs": 0.038593961275960494, + "customSystemsMs": 0.0001386184979620118, + "encodeMs": 0.20409198898252928, + "extensionsUpdateMs": 0.000575330159740983, + "gameUpdateMs": 0.011471163155822153, + "geometryStreamingMs": 0.003770089649672066, + "lodMs": 0.0014400517620884695, + "physicsMs": 0.000369235769742065, + "renderPrepMs": 0.04223693835804185, + "renderTotalMs": 1.8123834715354152, + "scenegraphMs": 0.003838760753417334, + "scriptingMs": 0.0002957248639207508, + "semaphoreWaitMs": 7.805472705513239e-05, + "streamingRegionMs": 0.0009835297905555812, + "submitMs": 0.005416226081757082, + "updateMs": 0.02529062325537271 + }, + "worstFrameMs": 9.37245786190033, + "worstThermalState": 1 + }, + "title": "A single cube", + "warmupSeconds": 3 + } +} \ No newline at end of file diff --git a/perf/baselines/Mac16_5/macOS/gaussian.json b/perf/baselines/Mac16_5/macOS/gaussian.json index d722cae6e..c2c86f862 100644 --- a/perf/baselines/Mac16_5/macOS/gaussian.json +++ b/perf/baselines/Mac16_5/macOS/gaussian.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "gaussian.jsonl", @@ -74,185 +75,185 @@ ], "usedHZB": true }, - "frameIndex": 1199, + "frameIndex": 1790, "gpuPasses": { - "frameIndex": 1199, + "frameIndex": 1791, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.044708 + "ms": 0.010625 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.011917 + "ms": 0.012291 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.175208 + "ms": 0.017667 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.038875 + "ms": 0.011041 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.051542 + "ms": 0.045083 }, { "count": 1, "label": "Gaussian Frustum Culling", - "ms": 0.430334 + "ms": 0.170667 }, { "count": 1, "label": "Gaussian Preprocess", - "ms": 0.139042 + "ms": 0.037666 }, { "count": 1, "label": "Radix Sort", - "ms": 0.683625 + "ms": 0.778042 }, { "count": 1, "label": "Grid Pass", - "ms": 1.115166 + "ms": 0.161292 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.799791 + "ms": 0.102083 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.066041 + "ms": 0.099041 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.485625 + "ms": 0.720042 }, { "count": 1, - "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.05725 + "label": "Copy Opaque Depth for HZB", + "ms": 0.069458 }, { "count": 1, - "label": "Gaussian Pass", - "ms": 0.579125 + "label": "Transparency Pass", + "ms": 0.078958 }, { "count": 1, - "label": "Copy Opaque Depth for HZB", - "ms": 0.076459 + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.059875 }, { "count": 1, - "label": "Transparency Pass", - "ms": 0.371625 + "label": "Gaussian Pass", + "ms": 0.254791 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.215167 + "ms": 0.276541 }, { - "count": 1, + "count": 2, "label": "Post-Processing Pass", - "ms": 0.488 + "ms": 0.587833 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.887375 + "ms": 0.520333 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.288584 + "ms": 0.841042 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.063125 + "ms": 0.124292 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.039917 + "ms": 0.017875 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.027625 + "ms": 0.012375 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.03175 + "ms": 0.014041 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.02625 + "ms": 0.015292 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.029042 + "ms": 0.013666 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.01875 + "ms": 0.018625 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.021417 + "ms": 0.012083 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.029584 + "ms": 0.011791 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.019833 + "ms": 0.012333 } ], "passesSkipped": 0, - "totalMs": 7.312751999999999 + "totalMs": 5.106743999999997 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 121, - "framesOverBudget": 0, + "framesLastSecond": 120, + "framesOverBudget": 8, "framesOverBudgetLastSecond": 0, - "framesSampled": 1200, + "framesSampled": 1791, "histogram": [ - 0, - 344, - 855, + 2, + 223, + 1547, + 18, 1, 0, - 0, 0 ], - "worstFrameMsLastSecond": 10.722416453063488 + "worstFrameMsLastSecond": 9.170708246529102 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 648167424, + "gpuAllocatedBytes": 648036352, "isUnderPressure": false, "meshMemoryBytes": 3306392, "textureBudgetBytes": 18471796080, @@ -264,8 +265,8 @@ "render": { "drawCallsBatched": 0, "drawCallsOpaque": 256, - "drawCallsShadow": 364, - "drawCallsTotal": 627, + "drawCallsShadow": 512, + "drawCallsTotal": 776, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -276,7 +277,7 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 125163, + "trianglesTotal": 155141, "visibleInstances": 256 }, "streaming": { @@ -316,140 +317,143 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133774.0391064167, + "timestampSeconds": 844299.8383583336, "timing": { - "animationMs": 0.002083310391753912, + "animationMs": 0.001249951310455799, "batchingRebuildMs": 0, - "batchingTickMs": 0.0020833394955843687, - "cullingMs": 0.17154167289845645, - "customSystemsMs": 0.0007083290256559849, - "encodeMs": 4.538750014035031, - "extensionsUpdateMs": 0.0017916609067469835, - "frameTotalMs": 8.173458278179169, - "gameUpdateMs": 0.06916667916812003, - "geometryStreamingMs": 0.0022500171326100826, - "gpuExecutionMs": 4.283125017536804, - "gpuFrameCadenceMs": 10.858624998945743, - "lodMs": 0.0025833433028310537, - "physicsMs": 0.00100000761449337, + "batchingTickMs": 0.0017499551177024841, + "cullingMs": 0.1647083554416895, + "customSystemsMs": 0.000250060111284256, + "encodeMs": 1.9362083403393626, + "extensionsUpdateMs": 0.0014167744666337967, + "frameTotalMs": 7.903208024799824, + "gameUpdateMs": 0.025833374820649624, + "geometryStreamingMs": 0.0017082784324884415, + "gpuExecutionMs": 2.427999977953732, + "gpuFrameCadenceMs": 8.486666716635227, + "lodMs": 0.0031249364838004112, + "physicsMs": 0.0011667143553495407, "physicsStepCount": 1, - "renderPrepMs": 0.43033334077335894, - "renderTotalMs": 7.813041680492461, - "scenegraphMs": 0.07933334563858807, - "scriptingMs": 0.0013333337847143412, - "semaphoreWaitMs": 0.00045832712203264236, - "smoothedFrameMs": 8.38935694967707, - "streamingRegionMs": 0.0018749851733446121, - "submitMs": 0.027916685212403536, - "updateMs": 0.1696250110398978 + "renderPrepMs": 0.24391664192080498, + "renderTotalMs": 5.851083318702877, + "scenegraphMs": 0.0642499653622508, + "scriptingMs": 0.00041676685214042664, + "semaphoreWaitMs": 0.00029173679649829865, + "smoothedFrameMs": 8.333355514332652, + "streamingRegionMs": 0.0014166580513119698, + "submitMs": 0.009249895811080933, + "updateMs": 0.11195836123079062 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "test_gaussians.ply (small) plus a 16x16 grid; exercises the sort pipeline, not splat count.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.983395124989329, + "durationSeconds": 14.991625000024214, "frameBudgetMs": 12.5, - "frames": 3600, - "framesOverBudget": 2, + "frames": 5385, + "framesOverBudget": 15, "gpuPassMeanMs": { - "Block Scan Pass": 0.011558513761467866, - "Compact and Stream Pass": 0.007010944954128434, - "Copy Opaque Depth for Gaussian Occlusion": 0.07104425771476228, - "Copy Opaque Depth for HZB": 0.04869206171809836, - "G-Buffer + Light Pass (TBDR)": 0.4811368457047543, - "Gaussian Frustum Culling": 0.048873914095079206, - "Gaussian Pass": 0.4227533969974974, - "Gaussian Preprocess": 0.03707136113427852, - "Grid Pass": 0.11464033277731439, - "HZB Build Mip 0": 0.3363427989991663, - "HZB Build Mip 1": 0.057526110925771506, - "HZB Build Mip 10": 0.011832924937447892, - "HZB Build Mip 2": 0.02182210425354462, - "HZB Build Mip 3": 0.014309320266889053, - "HZB Build Mip 4": 0.012238431192660543, - "HZB Build Mip 5": 0.011578216013344455, - "HZB Build Mip 6": 0.010445649708090077, - "HZB Build Mip 7": 0.011749016680567133, - "HZB Build Mip 8": 0.011895811509591334, - "HZB Build Mip 9": 0.012171615512927447, - "HZB Occlusion Culling pass": 0.02616073561301084, - "Local Scan Pass": 0.016742093411175926, - "Mark Visible Pass": 0.02112907089241036, - "Output Transform Pass": 0.287302327773144, - "Post-Processing Pass": 0.19227929691409473, - "Pre Composite Pass": 0.2779125037531273, - "Radix Sort": 0.4084149032527101, - "Shadow Cascade 0": 0.0738084178482067, - "Shadow Cascade 1": 0.08641182652210185, - "Transparency Pass": 0.12525412343619677 + "Block Scan Pass": 0.02421204355108874, + "Compact and Stream Pass": 0.029281818537130095, + "Copy Opaque Depth for Gaussian Occlusion": 0.03593344394868936, + "Copy Opaque Depth for HZB": 0.05135692749581711, + "G-Buffer + Light Pass (TBDR)": 0.438270947016174, + "Gaussian Frustum Culling": 0.171407202453988, + "Gaussian Pass": 0.1537357412158393, + "Gaussian Preprocess": 0.06619478447794515, + "Grid Pass": 0.12522488510875632, + "HZB Build Mip 0": 0.0902804397556912, + "HZB Build Mip 1": 0.041502462520821806, + "HZB Build Mip 10": 0.014925597989949769, + "HZB Build Mip 2": 0.026688952248750685, + "HZB Build Mip 3": 0.031467893947806774, + "HZB Build Mip 4": 0.026024952248750746, + "HZB Build Mip 5": 0.023056857621440618, + "HZB Build Mip 6": 0.01982140591848129, + "HZB Build Mip 7": 0.01747851814628703, + "HZB Build Mip 8": 0.01600489447236184, + "HZB Build Mip 9": 0.015302365717476271, + "HZB Occlusion Culling pass": 0.05977950977107777, + "Local Scan Pass": 0.03221377746793091, + "Mark Visible Pass": 0.018610533780011217, + "Output Transform Pass": 0.1990434280955025, + "Post-Processing Pass": 0.2481776529705715, + "Pre Composite Pass": 0.10816299832682676, + "Radix Sort": 0.7867843714445051, + "Shadow Cascade 0": 0.1690453305415969, + "Shadow Cascade 1": 0.15621918704634266, + "Transparency Pass": 0.066608662576687 }, "gpuPassMinMs": { - "Block Scan Pass": 0.005375, - "Compact and Stream Pass": 0.00325, - "Copy Opaque Depth for Gaussian Occlusion": 0.023583, - "Copy Opaque Depth for HZB": 0.01825, - "G-Buffer + Light Pass (TBDR)": 0.198334, - "Gaussian Frustum Culling": 0.023667, - "Gaussian Pass": 0.060417, - "Gaussian Preprocess": 0.013166, - "Grid Pass": 0.044334, - "HZB Build Mip 0": 0.07825, - "HZB Build Mip 1": 0.022208, - "HZB Build Mip 10": 0.003791, - "HZB Build Mip 2": 0.00775, - "HZB Build Mip 3": 0.004583, - "HZB Build Mip 4": 0.004375, - "HZB Build Mip 5": 0.003875, - "HZB Build Mip 6": 0.00375, - "HZB Build Mip 7": 0.003875, - "HZB Build Mip 8": 0.003916, - "HZB Build Mip 9": 0.003625, - "HZB Occlusion Culling pass": 0.015166, - "Local Scan Pass": 0.003708, - "Mark Visible Pass": 0.003458, - "Output Transform Pass": 0.099708, - "Post-Processing Pass": 0.095417, - "Pre Composite Pass": 0.169833, - "Radix Sort": 0.1505, - "Shadow Cascade 0": 0.022459, - "Shadow Cascade 1": 0.027667, - "Transparency Pass": 0.0095 + "Block Scan Pass": 0.01625, + "Compact and Stream Pass": 0.010084, + "Copy Opaque Depth for Gaussian Occlusion": 0.021125, + "Copy Opaque Depth for HZB": 0.024583, + "G-Buffer + Light Pass (TBDR)": 0.318834, + "Gaussian Frustum Culling": 0.058959, + "Gaussian Pass": 0.081958, + "Gaussian Preprocess": 0.0325, + "Grid Pass": 0.059958, + "HZB Build Mip 0": 0.048, + "HZB Build Mip 1": 0.015792, + "HZB Build Mip 10": 0.009166, + "HZB Build Mip 2": 0.010833, + "HZB Build Mip 3": 0.010334, + "HZB Build Mip 4": 0.009875, + "HZB Build Mip 5": 0.00975, + "HZB Build Mip 6": 0.00975, + "HZB Build Mip 7": 0.009333, + "HZB Build Mip 8": 0.009292, + "HZB Build Mip 9": 0.009125, + "HZB Occlusion Culling pass": 0.037042, + "Local Scan Pass": 0.011917, + "Mark Visible Pass": 0.009708, + "Output Transform Pass": 0.167334, + "Post-Processing Pass": 0.200375, + "Pre Composite Pass": 0.062, + "Radix Sort": 0.533541, + "Shadow Cascade 0": 0.097667, + "Shadow Cascade 1": 0.093667, + "Transparency Pass": 0.011375 }, - "meanFrameMs": 8.332235859318947, - "meanGPUExecutionMs": 2.2363732969916557, + "meanFrameMs": 8.333323082303849, + "meanGPUExecutionMs": 1.8180134096268588, + "minGPUExecutionMs": 1.2316666543483734, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 8.327499963343143, - "p95FrameMs": 9.340625256299973, - "p99FrameMs": 10.311874561011791, - "peakGPUAllocatedBytes": 648167424, + "overBudgetRate": 0.0, + "p50FrameMs": 8.333167061209679, + "p95FrameMs": 8.930874988436699, + "p99FrameMs": 9.434250183403492, + "peakGPUAllocatedBytes": 648036352, "timingMeanMs": { - "animationMs": 0.0011779856625556324, - "batchingTickMs": 0.0018635414744494483, + "animationMs": 0.0009103042116468314, + "batchingTickMs": 0.001127441097892368, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 0.1530228476622142, - "customSystemsMs": 0.00017475712714561573, - "encodeMs": 1.787180069416839, - "extensionsUpdateMs": 0.0010474999968816217, - "gameUpdateMs": 0.04783437519411867, - "geometryStreamingMs": 0.029246354218533572, - "lodMs": 0.0023556252805671343, - "physicsMs": 0.00045270833652466536, - "renderPrepMs": 0.22145875040829802, - "renderTotalMs": 2.143913541949587, - "scenegraphMs": 0.06931611084534477, - "scriptingMs": 0.0005127776724596819, - "semaphoreWaitMs": 0.00018826341450524828, - "streamingRegionMs": 0.0015178122945750754, - "submitMs": 0.006856215453202215, - "updateMs": 0.13289055626955815 + "cullingMs": 0.11790775978882931, + "customSystemsMs": 0.00012860055836265603, + "encodeMs": 1.859141663168762, + "extensionsUpdateMs": 0.0006795734392560767, + "gameUpdateMs": 0.01293486251708222, + "geometryStreamingMs": 0.02553174058870019, + "lodMs": 0.0015418950818905162, + "physicsMs": 0.0004083394429241532, + "renderPrepMs": 0.16663679606159698, + "renderTotalMs": 6.152122420307126, + "scenegraphMs": 0.05520608989487732, + "scriptingMs": 0.00031436396621039415, + "semaphoreWaitMs": 6.0568416262586405e-05, + "streamingRegionMs": 0.0010959262431407533, + "submitMs": 0.006463444699277209, + "updateMs": 0.07882539661490523 }, - "worstFrameMs": 11.064250022172928, - "worstThermalState": 0 + "worstFrameMs": 11.251416988670826, + "worstThermalState": 1 }, "title": "Gaussian splat with primitives", "warmupSeconds": 3 diff --git a/perf/baselines/Mac16_5/macOS/lights-64.json b/perf/baselines/Mac16_5/macOS/lights-64.json index 7dbe13c25..0d4d6c6e7 100644 --- a/perf/baselines/Mac16_5/macOS/lights-64.json +++ b/perf/baselines/Mac16_5/macOS/lights-64.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "lights-64.jsonl", @@ -58,14 +59,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 4, - "frustumPassed": 1020, + "frustumFailed": 0, + "frustumPassed": 1024, "frustumTested": 1024, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 1020, - "occlusionTested": 1020, + "occlusionPassed": 1024, + "occlusionTested": 1024, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -74,180 +75,180 @@ ], "usedHZB": true }, - "frameIndex": 1200, + "frameIndex": 1791, "gpuPasses": { - "frameIndex": 1201, + "frameIndex": 1792, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.021125 + "ms": 0.010833 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.058708 + "ms": 0.1365 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.01075 + "ms": 0.018791 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.006584 + "ms": 0.011167 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.024958 + "ms": 0.044834 }, { "count": 1, "label": "Grid Pass", - "ms": 0.139709 + "ms": 0.164084 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.012667 + "ms": 0.021083 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.009375 + "ms": 0.020917 }, { "count": 1, "label": "Batched Shadow Cascade 0", - "ms": 0.137666 + "ms": 0.235167 }, { "count": 1, "label": "Batched Shadow Cascade 1", - "ms": 0.682416 + "ms": 0.141167 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 1.45075 + "ms": 2.955583 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.051125 + "ms": 0.074792 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.081209 + "ms": 0.085458 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.043042 + "ms": 0.073584 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.3405 + "ms": 0.268458 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.186708 + "ms": 0.308291 }, { "count": 2, "label": "Post-Processing Pass", - "ms": 0.322708 + "ms": 0.574833 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.251917 + "ms": 0.497791 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.126459 + "ms": 0.411583 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.035209 + "ms": 0.094542 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.014541 + "ms": 0.016792 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.009292 + "ms": 0.031916 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.008542 + "ms": 0.02575 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.008334 + "ms": 0.025834 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.008916 + "ms": 0.020208 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.007958 + "ms": 0.010709 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.008 + "ms": 0.009834 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.00675 + "ms": 0.00975 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.00675 + "ms": 0.044 } ], "passesSkipped": 0, - "totalMs": 4.072668000000001 + "totalMs": 6.344250999999999 }, "hitches": { "frameBudgetMs": 12.5, "framesLastSecond": 121, - "framesOverBudget": 0, + "framesOverBudget": 21, "framesOverBudgetLastSecond": 0, - "framesSampled": 1201, + "framesSampled": 1792, "histogram": [ - 0, - 354, - 834, - 13, - 0, + 12, + 377, + 1381, + 20, + 2, 0, 0 ], - "worstFrameMsLastSecond": 10.870333760976791 + "worstFrameMsLastSecond": 10.48029214143753 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 520765440, + "gpuAllocatedBytes": 521289728, "isUnderPressure": false, "meshMemoryBytes": 12216512, "textureBudgetBytes": 18471796080, @@ -257,10 +258,10 @@ "utilizationPercent": 0.0002645440981723368 }, "render": { - "drawCallsBatched": 585, + "drawCallsBatched": 708, "drawCallsOpaque": 256, - "drawCallsShadow": 348, - "drawCallsTotal": 611, + "drawCallsShadow": 471, + "drawCallsTotal": 734, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -271,8 +272,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 486457, - "visibleInstances": 1020 + "trianglesTotal": 589609, + "visibleInstances": 1024 }, "streaming": { "activeLoads": 0, @@ -311,138 +312,141 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133718.93767395837, + "timestampSeconds": 844224.7386120835, "timing": { - "animationMs": 0.00120833283290267, + "animationMs": 0.0009583309292793274, "batchingRebuildMs": 0, - "batchingTickMs": 0.002541666617617011, - "cullingMs": 0.4207500023767352, - "customSystemsMs": 0.00037500285543501377, - "encodeMs": 2.5599583168514073, - "extensionsUpdateMs": 0.0014583347365260124, - "frameTotalMs": 6.172166671603918, - "gameUpdateMs": 0.034041673643514514, - "geometryStreamingMs": 0.0022500171326100826, - "gpuExecutionMs": 3.4549999982118607, - "gpuFrameCadenceMs": 9.114833315834403, - "lodMs": 0.002708344254642725, - "physicsMs": 0.001374981366097927, + "batchingTickMs": 0.0012082746252417564, + "cullingMs": 0.4218750400468707, + "customSystemsMs": 0.0002916203811764717, + "encodeMs": 2.7517499402165413, + "extensionsUpdateMs": 0.0010833609849214554, + "frameTotalMs": 8.37116688489914, + "gameUpdateMs": 0.024583423510193825, + "geometryStreamingMs": 0.001249951310455799, + "gpuExecutionMs": 4.813999985344708, + "gpuFrameCadenceMs": 7.537500001490116, + "lodMs": 0.002541695721447468, + "physicsMs": 0.0009167706593871117, "physicsStepCount": 1, - "renderPrepMs": 0.4239999980200082, - "renderTotalMs": 7.144874980440363, - "scenegraphMs": 0.21550001110881567, - "scriptingMs": 0.00100000761449337, - "semaphoreWaitMs": 0.0002500019036233425, - "smoothedFrameMs": 8.337481940786043, - "streamingRegionMs": 0.001666689058765769, - "submitMs": 0.007166643626987934, - "updateMs": 0.26775000151246786 + "renderPrepMs": 0.4271250218153, + "renderTotalMs": 6.866625044494867, + "scenegraphMs": 0.21483330056071281, + "scriptingMs": 0.00037497375160455704, + "semaphoreWaitMs": 0.00024994369596242905, + "smoothedFrameMs": 8.33468739874661, + "streamingRegionMs": 0.0014583347365260124, + "submitMs": 0.011499971151351929, + "updateMs": 0.2521249698475003 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.990087250014767, + "durationSeconds": 14.98351312498562, "frameBudgetMs": 12.5, - "frames": 3603, - "framesOverBudget": 0, + "frames": 5354, + "framesOverBudget": 79, "gpuPassMeanMs": { - "Batched Shadow Cascade 0": 0.2142405512073269, - "Batched Shadow Cascade 1": 0.24675282014987468, - "Block Scan Pass": 0.014569315833333315, - "Compact and Stream Pass": 0.01147794250000003, - "Copy Opaque Depth for Gaussian Occlusion": 0.037497634471273876, - "Copy Opaque Depth for HZB": 0.04325083513738555, - "G-Buffer + Light Pass (TBDR)": 1.2959789458784365, - "Gaussian Pass": 0.3319834088259778, - "Grid Pass": 0.23541597249999957, - "HZB Build Mip 0": 0.19810007910074945, - "HZB Build Mip 1": 0.08351252539550363, - "HZB Build Mip 10": 0.011224593671940031, - "HZB Build Mip 2": 0.031147704412989165, - "HZB Build Mip 3": 0.015954043297252276, - "HZB Build Mip 4": 0.01471981598667779, - "HZB Build Mip 5": 0.015279043297252312, - "HZB Build Mip 6": 0.019591934221482102, - "HZB Build Mip 7": 0.016912566194837626, - "HZB Build Mip 8": 0.01712222064945876, - "HZB Build Mip 9": 0.012067846794338068, - "HZB Occlusion Culling pass": 0.028990835833333294, - "Local Scan Pass": 0.020854170833333335, - "Mark Visible Pass": 0.017400186511240683, - "Output Transform Pass": 0.2712267110741049, - "Post-Processing Pass": 0.36746653871773516, - "Pre Composite Pass": 0.2542115570358035, - "Shadow Cascade 0": 0.023040248959200635, - "Shadow Cascade 1": 0.019471407993338918, - "Transparency Pass": 0.1438001665278931 + "Batched Shadow Cascade 0": 0.1531333002232144, + "Batched Shadow Cascade 1": 0.13491696037946424, + "Block Scan Pass": 0.01826797104677051, + "Compact and Stream Pass": 0.011982729398663657, + "Copy Opaque Depth for Gaussian Occlusion": 0.02700119603399443, + "Copy Opaque Depth for HZB": 0.02822664475920684, + "G-Buffer + Light Pass (TBDR)": 1.1334411456090647, + "Gaussian Pass": 0.10325921529745041, + "Grid Pass": 0.0631845918708241, + "HZB Build Mip 0": 0.09173902946175637, + "HZB Build Mip 1": 0.052637156373937614, + "HZB Build Mip 10": 0.04649044419642854, + "HZB Build Mip 2": 0.02736995100222724, + "HZB Build Mip 3": 0.02466683172804537, + "HZB Build Mip 4": 0.012903492917847047, + "HZB Build Mip 5": 0.020387609065155816, + "HZB Build Mip 6": 0.021847905382436264, + "HZB Build Mip 7": 0.012499878186968854, + "HZB Build Mip 8": 0.01175424419263455, + "HZB Build Mip 9": 0.01678505779036827, + "HZB Occlusion Culling pass": 0.03757376503340754, + "Local Scan Pass": 0.013360756681514492, + "Mark Visible Pass": 0.011910181514476632, + "Output Transform Pass": 0.15240274900849865, + "Post-Processing Pass": 0.19064210254957492, + "Pre Composite Pass": 0.08491572917847044, + "Shadow Cascade 0": 0.020235757812500112, + "Shadow Cascade 1": 0.019511370535714216, + "Transparency Pass": 0.04635344872521249 }, "gpuPassMinMs": { - "Batched Shadow Cascade 0": 0.043292, - "Batched Shadow Cascade 1": 0.045416, - "Block Scan Pass": 0.00525, - "Compact and Stream Pass": 0.003458, - "Copy Opaque Depth for Gaussian Occlusion": 0.020333, - "Copy Opaque Depth for HZB": 0.023791, - "G-Buffer + Light Pass (TBDR)": 0.752833, - "Gaussian Pass": 0.070708, - "Grid Pass": 0.047667, - "HZB Build Mip 0": 0.071917, - "HZB Build Mip 1": 0.02275, - "HZB Build Mip 10": 0.003708, - "HZB Build Mip 2": 0.008125, - "HZB Build Mip 3": 0.004875, - "HZB Build Mip 4": 0.004208, - "HZB Build Mip 5": 0.00425, - "HZB Build Mip 6": 0.004, - "HZB Build Mip 7": 0.004, - "HZB Build Mip 8": 0.004, - "HZB Build Mip 9": 0.003917, - "HZB Occlusion Culling pass": 0.015209, - "Local Scan Pass": 0.003875, - "Mark Visible Pass": 0.003583, - "Output Transform Pass": 0.192375, - "Post-Processing Pass": 0.257625, - "Pre Composite Pass": 0.173709, - "Shadow Cascade 0": 0.006416, - "Shadow Cascade 1": 0.006292, - "Transparency Pass": 0.016708 + "Batched Shadow Cascade 0": 0.047542, + "Batched Shadow Cascade 1": 0.047625, + "Block Scan Pass": 0.005958, + "Compact and Stream Pass": 0.003792, + "Copy Opaque Depth for Gaussian Occlusion": 0.009167, + "Copy Opaque Depth for HZB": 0.010542, + "G-Buffer + Light Pass (TBDR)": 0.489167, + "Gaussian Pass": 0.033916, + "Grid Pass": 0.021083, + "HZB Build Mip 0": 0.025875, + "HZB Build Mip 1": 0.008958, + "HZB Build Mip 10": 0.004, + "HZB Build Mip 2": 0.005583, + "HZB Build Mip 3": 0.004959, + "HZB Build Mip 4": 0.004375, + "HZB Build Mip 5": 0.004667, + "HZB Build Mip 6": 0.004209, + "HZB Build Mip 7": 0.004166, + "HZB Build Mip 8": 0.004125, + "HZB Build Mip 9": 0.004, + "HZB Occlusion Culling pass": 0.01325, + "Local Scan Pass": 0.004291, + "Mark Visible Pass": 0.004041, + "Output Transform Pass": 0.064292, + "Post-Processing Pass": 0.080584, + "Pre Composite Pass": 0.037583, + "Shadow Cascade 0": 0.007291, + "Shadow Cascade 1": 0.007083, + "Transparency Pass": 0.006541 }, - "meanFrameMs": 8.33129754319452, - "meanGPUExecutionMs": 3.340423882918178, + "meanFrameMs": 8.35187965550532, + "meanGPUExecutionMs": 2.681186688594097, + "minGPUExecutionMs": 0.8315000450238585, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 8.33266694098711, - "p95FrameMs": 10.276916436851025, - "p99FrameMs": 10.626000352203846, - "peakGPUAllocatedBytes": 520765440, + "overBudgetRate": 0.004451864218141347, + "p50FrameMs": 8.332333527505398, + "p95FrameMs": 10.111291892826557, + "p99FrameMs": 10.926375165581703, + "peakGPUAllocatedBytes": 521289728, "timingMeanMs": { - "animationMs": 0.0008935605121123528, - "batchingTickMs": 0.0016668397880125975, + "animationMs": 0.0008185886848939745, + "batchingTickMs": 0.0009953640230335856, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 0.3852863933165619, - "customSystemsMs": 0.00021090053179325362, - "encodeMs": 2.5767450039910864, - "extensionsUpdateMs": 0.0009832777585803247, - "gameUpdateMs": 0.042672598905873785, - "geometryStreamingMs": 0.09275274133142224, - "lodMs": 0.0022987096099730433, - "physicsMs": 0.00045833320451511085, - "renderPrepMs": 0.38984138249585576, - "renderTotalMs": 3.108962809038484, - "scenegraphMs": 0.20800308761753755, - "scriptingMs": 0.0004569457442213554, - "semaphoreWaitMs": 0.00014883364480202838, - "streamingRegionMs": 0.0013405846127097205, - "submitMs": 0.0062845201690221435, - "updateMs": 0.2650766721707563 + "cullingMs": 0.3587278798139619, + "customSystemsMs": 0.00014062815397132021, + "encodeMs": 2.513197367079556, + "extensionsUpdateMs": 0.0006384489633885104, + "gameUpdateMs": 0.01211711695302707, + "geometryStreamingMs": 0.08937721978345353, + "lodMs": 0.001560981500982973, + "physicsMs": 0.00039004755989504843, + "renderPrepMs": 0.3622730954332017, + "renderTotalMs": 6.440203588421615, + "scenegraphMs": 0.1957935311438426, + "scriptingMs": 0.000292684703559496, + "semaphoreWaitMs": 7.57543850299657e-05, + "streamingRegionMs": 0.0009685822798309223, + "submitMs": 0.006461370519725694, + "updateMs": 0.22005491036520716 }, - "worstFrameMs": 12.064333073794842, - "worstThermalState": 0 + "worstFrameMs": 15.957541763782501, + "worstThermalState": 1 }, "title": "1024 primitives, static batching, 64 point lights", "warmupSeconds": 3 diff --git a/perf/baselines/Mac16_5/macOS/postfx.json b/perf/baselines/Mac16_5/macOS/postfx.json index b72bba666..368d4abc2 100644 --- a/perf/baselines/Mac16_5/macOS/postfx.json +++ b/perf/baselines/Mac16_5/macOS/postfx.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "postfx.jsonl", @@ -58,14 +59,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 4, - "frustumPassed": 1020, + "frustumFailed": 0, + "frustumPassed": 1024, "frustumTested": 1024, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 1020, - "occlusionTested": 1020, + "occlusionPassed": 1024, + "occlusionTested": 1024, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -74,185 +75,185 @@ ], "usedHZB": true }, - "frameIndex": 1200, + "frameIndex": 1566, "gpuPasses": { - "frameIndex": 1201, + "frameIndex": 1567, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.005291 + "ms": 0.011125 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.005542 + "ms": 0.158416 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.007917 + "ms": 0.10575 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.00525 + "ms": 0.011542 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.020458 + "ms": 0.041125 }, { "count": 1, "label": "Grid Pass", - "ms": 0.070458 + "ms": 0.16525 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.009459 + "ms": 0.294834 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.085667 + "ms": 0.288542 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.454375 + "ms": 1.038916 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.075959 + "ms": 0.12 }, { "count": 1, "label": "SSAO Low-Res Pass", - "ms": 0.093166 + "ms": 0.21425 }, { "count": 1, "label": "SSAO Bilateral Blur Horizontal", - "ms": 0.033083 + "ms": 0.075333 }, { "count": 1, "label": "SSAO Bilateral Blur Vertical", - "ms": 0.0325 + "ms": 0.073375 }, { "count": 1, "label": "SSAO Upsample Pass", - "ms": 0.062625 + "ms": 0.153666 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.007292 + "ms": 0.013708 }, { "count": 17, "label": "Post-Processing Pass", - "ms": 2.714502 + "ms": 3.493875 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.038791 + "ms": 0.063958 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.208 + "ms": 0.309834 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.19725 + "ms": 0.210958 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.125209 + "ms": 0.204542 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.143042 + "ms": 0.24675 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.04475 + "ms": 0.076791 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.032292 + "ms": 0.029708 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.027875 + "ms": 0.0185 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.024125 + "ms": 0.01825 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.024458 + "ms": 0.013209 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.023833 + "ms": 0.012167 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.024125 + "ms": 0.01225 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.025125 + "ms": 0.026542 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.024125 + "ms": 0.036083 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.024167 + "ms": 0.046083 } ], "passesSkipped": 0, - "totalMs": 4.670710999999999 + "totalMs": 7.585332 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 121, - "framesOverBudget": 0, - "framesOverBudgetLastSecond": 0, - "framesSampled": 1201, + "framesLastSecond": 104, + "framesOverBudget": 180, + "framesOverBudgetLastSecond": 17, + "framesSampled": 1567, "histogram": [ 0, - 485, - 716, - 0, - 0, + 163, + 1162, + 239, + 3, 0, 0 ], - "worstFrameMsLastSecond": 10.85750013589859 + "worstFrameMsLastSecond": 16.42637513577938 }, "memory": { "availableMemoryBytes": 0, @@ -268,9 +269,9 @@ }, "render": { "drawCallsBatched": 0, - "drawCallsOpaque": 1018, - "drawCallsShadow": 989, - "drawCallsTotal": 2033, + "drawCallsOpaque": 1024, + "drawCallsShadow": 1771, + "drawCallsTotal": 2821, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -281,8 +282,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 400887, - "visibleInstances": 1020 + "trianglesTotal": 555291, + "visibleInstances": 1024 }, "streaming": { "activeLoads": 0, @@ -321,141 +322,144 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133732.7159945417, + "timestampSeconds": 844243.5180470002, "timing": { - "animationMs": 0.0008750066626816988, + "animationMs": 0.0008333008736371994, "batchingRebuildMs": 0, - "batchingTickMs": 0.0009583309292793274, - "cullingMs": 0.3156666934955865, - "customSystemsMs": 0, - "encodeMs": 6.1264583200681955, - "extensionsUpdateMs": 0.0007916823960840702, - "frameTotalMs": 8.399583399295807, - "gameUpdateMs": 0.025666638975962996, - "geometryStreamingMs": 0.001041655195876956, - "gpuExecutionMs": 4.203249991405755, - "gpuFrameCadenceMs": 6.494916684459895, - "lodMs": 0.0015833356883376837, - "physicsMs": 0, - "physicsStepCount": 0, - "renderPrepMs": 0.3247916465625167, - "renderTotalMs": 7.489458337659016, - "scenegraphMs": 0.17791669233702123, - "scriptingMs": 0.0003333261702209711, - "semaphoreWaitMs": 8.33242665976286e-05, - "smoothedFrameMs": 8.34235007253786, - "streamingRegionMs": 0.0007083290256559849, - "submitMs": 0.008291652193292975, - "updateMs": 0.21220833878032863 + "batchingTickMs": 0.00100000761449337, + "cullingMs": 0.3377916291356087, + "customSystemsMs": 0.00016670674085617065, + "encodeMs": 6.973666721023619, + "extensionsUpdateMs": 0.0007499475032091141, + "frameTotalMs": 15.14795795083046, + "gameUpdateMs": 0.012999982573091984, + "geometryStreamingMs": 0.001625041477382183, + "gpuExecutionMs": 6.401333375833929, + "gpuFrameCadenceMs": 15.771374921314418, + "lodMs": 0.0014583347365260124, + "physicsMs": 0.0009166542440652847, + "physicsStepCount": 1, + "renderPrepMs": 0.34129165578633547, + "renderTotalMs": 7.4769583297893405, + "scenegraphMs": 0.18804159481078386, + "scriptingMs": 0.00029173679649829865, + "semaphoreWaitMs": 0.00020838342607021332, + "smoothedFrameMs": 9.689130509893099, + "streamingRegionMs": 0.0010416842997074127, + "submitMs": 0.006375019438564777, + "updateMs": 0.21154165733605623 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "Same geometry as primitives-1k; the difference is the full-screen pass chain.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.990820000006352, + "durationSeconds": 14.988691041595303, "frameBudgetMs": 12.5, - "frames": 3601, - "framesOverBudget": 0, + "frames": 5078, + "framesOverBudget": 268, "gpuPassMeanMs": { - "Block Scan Pass": 0.007166741451209337, - "Compact and Stream Pass": 0.004883863219349449, - "Copy Opaque Depth for Gaussian Occlusion": 0.030152346121768196, - "Copy Opaque Depth for HZB": 0.04630119933277732, - "G-Buffer + Light Pass (TBDR)": 0.37429505587989964, - "Gaussian Pass": 0.20920801248959198, - "Grid Pass": 0.08188264720600501, - "HZB Build Mip 0": 0.10639147623019186, - "HZB Build Mip 1": 0.04638045287739786, - "HZB Build Mip 10": 0.012676583819849886, - "HZB Build Mip 2": 0.021147976647206038, - "HZB Build Mip 3": 0.012779631359466246, - "HZB Build Mip 4": 0.011618154295246073, - "HZB Build Mip 5": 0.011478815679733126, - "HZB Build Mip 6": 0.011135196830692242, - "HZB Build Mip 7": 0.011818201834862378, - "HZB Build Mip 8": 0.012589997497914915, - "HZB Build Mip 9": 0.012154750625521264, - "HZB Occlusion Culling pass": 0.01920677564637198, - "Local Scan Pass": 0.006157356964136773, - "Mark Visible Pass": 0.009823661384487115, - "Output Transform Pass": 0.1795354862385317, - "Post-Processing Pass": 1.7441400250208527, - "Pre Composite Pass": 0.2175919266055043, - "SSAO Bilateral Blur Horizontal": 0.03973140450375313, - "SSAO Bilateral Blur Vertical": 0.03928342869057547, - "SSAO Low-Res Pass": 0.09367715429524615, - "SSAO Upsample Pass": 0.05927517764804006, - "Shadow Cascade 0": 0.017341626355296066, - "Shadow Cascade 1": 0.08043490241868226, - "Transparency Pass": 0.01330884487072564 + "Block Scan Pass": 0.016791121666666617, + "Compact and Stream Pass": 0.010696342222222164, + "Copy Opaque Depth for Gaussian Occlusion": 0.03043023728813562, + "Copy Opaque Depth for HZB": 0.0704777583333334, + "G-Buffer + Light Pass (TBDR)": 0.6823227672222218, + "Gaussian Pass": 0.10395511864406802, + "Grid Pass": 0.0628181477777777, + "HZB Build Mip 0": 0.08223004944444451, + "HZB Build Mip 1": 0.03629073833333339, + "HZB Build Mip 10": 0.035733034460753016, + "HZB Build Mip 2": 0.025889020455873824, + "HZB Build Mip 3": 0.017737188888888922, + "HZB Build Mip 4": 0.021801633333333372, + "HZB Build Mip 5": 0.021231206312098197, + "HZB Build Mip 6": 0.01984189888953832, + "HZB Build Mip 7": 0.01701800055555558, + "HZB Build Mip 8": 0.019563307422559902, + "HZB Build Mip 9": 0.016972367222222246, + "HZB Occlusion Culling pass": 0.03396250444444437, + "Local Scan Pass": 0.012392883333333242, + "Mark Visible Pass": 0.028384498404594817, + "Output Transform Pass": 0.06262132144944468, + "Post-Processing Pass": 1.2024988947983626, + "Pre Composite Pass": 0.08031273056691998, + "SSAO Bilateral Blur Horizontal": 0.04136317111111118, + "SSAO Bilateral Blur Vertical": 0.039189195555555494, + "SSAO Low-Res Pass": 0.12646419777777784, + "SSAO Upsample Pass": 0.05283953722222209, + "Shadow Cascade 0": 0.13434482111111065, + "Shadow Cascade 1": 0.18597818953414122, + "Transparency Pass": 0.012661851111111068 }, "gpuPassMinMs": { - "Block Scan Pass": 0.004291, - "Compact and Stream Pass": 0.002666, - "Copy Opaque Depth for Gaussian Occlusion": 0.020875, - "Copy Opaque Depth for HZB": 0.025792, - "G-Buffer + Light Pass (TBDR)": 0.263166, - "Gaussian Pass": 0.14025, - "Grid Pass": 0.037125, - "HZB Build Mip 0": 0.067875, - "HZB Build Mip 1": 0.018125, - "HZB Build Mip 10": 0.003167, - "HZB Build Mip 2": 0.006833, - "HZB Build Mip 3": 0.004417, - "HZB Build Mip 4": 0.003709, - "HZB Build Mip 5": 0.003542, - "HZB Build Mip 6": 0.003292, - "HZB Build Mip 7": 0.003417, - "HZB Build Mip 8": 0.003458, - "HZB Build Mip 9": 0.003333, - "HZB Occlusion Culling pass": 0.013541, - "Local Scan Pass": 0.00325, - "Mark Visible Pass": 0.003083, - "Output Transform Pass": 0.06625, - "Post-Processing Pass": 1.1594589999999998, - "Pre Composite Pass": 0.170958, - "SSAO Bilateral Blur Horizontal": 0.018083, - "SSAO Bilateral Blur Vertical": 0.0175, - "SSAO Low-Res Pass": 0.052042, - "SSAO Upsample Pass": 0.029167, - "Shadow Cascade 0": 0.005208, - "Shadow Cascade 1": 0.047417, - "Transparency Pass": 0.004833 + "Block Scan Pass": 0.00575, + "Compact and Stream Pass": 0.003583, + "Copy Opaque Depth for Gaussian Occlusion": 0.009416, + "Copy Opaque Depth for HZB": 0.011041, + "G-Buffer + Light Pass (TBDR)": 0.214834, + "Gaussian Pass": 0.038708, + "Grid Pass": 0.022166, + "HZB Build Mip 0": 0.021958, + "HZB Build Mip 1": 0.008375, + "HZB Build Mip 10": 0.004084, + "HZB Build Mip 2": 0.005417, + "HZB Build Mip 3": 0.004333, + "HZB Build Mip 4": 0.004333, + "HZB Build Mip 5": 0.004291, + "HZB Build Mip 6": 0.00425, + "HZB Build Mip 7": 0.004459, + "HZB Build Mip 8": 0.004125, + "HZB Build Mip 9": 0.004125, + "HZB Occlusion Culling pass": 0.01275, + "Local Scan Pass": 0.00425, + "Mark Visible Pass": 0.003625, + "Output Transform Pass": 0.028208, + "Post-Processing Pass": 0.43987499999999996, + "Pre Composite Pass": 0.043583, + "SSAO Bilateral Blur Horizontal": 0.013375, + "SSAO Bilateral Blur Vertical": 0.01275, + "SSAO Low-Res Pass": 0.039833, + "SSAO Upsample Pass": 0.017166, + "Shadow Cascade 0": 0.041375, + "Shadow Cascade 1": 0.065083, + "Transparency Pass": 0.004209 }, - "meanFrameMs": 8.332842937302306, - "meanGPUExecutionMs": 3.083998054262869, + "meanFrameMs": 8.333933495450765, + "meanGPUExecutionMs": 3.5483235186095246, + "minGPUExecutionMs": 1.0989999864250422, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 8.223208598792553, - "p95FrameMs": 9.74529143422842, - "p99FrameMs": 10.618583299219608, + "overBudgetRate": 0.0, + "p50FrameMs": 8.92999954521656, + "p95FrameMs": 9.83795803040266, + "p99FrameMs": 10.318499989807606, "peakGPUAllocatedBytes": 649543680, "timingMeanMs": { - "animationMs": 0.0007417299310902788, - "batchingTickMs": 0.001215179612179651, + "animationMs": 0.0008530315244570374, + "batchingTickMs": 0.0009025456125123634, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 0.32980473337013794, - "customSystemsMs": 0.00018956735062166687, - "encodeMs": 6.218851751524969, - "extensionsUpdateMs": 0.000816923535144349, - "gameUpdateMs": 0.03113655313259327, - "geometryStreamingMs": 0.08589491247451142, - "lodMs": 0.0017536775969731917, - "physicsMs": 0.00039935498273497964, - "renderPrepMs": 0.33462614691377407, - "renderTotalMs": 7.421056099144314, - "scenegraphMs": 0.18651228155076138, - "scriptingMs": 0.0004005333122570102, - "semaphoreWaitMs": 7.12261120358375e-05, - "streamingRegionMs": 0.0011239225006785811, - "submitMs": 0.008646752148305223, - "updateMs": 0.2296827641915961 + "cullingMs": 0.31976046098861843, + "customSystemsMs": 0.0001238672606026133, + "encodeMs": 6.7039999065066995, + "extensionsUpdateMs": 0.0006735873951887091, + "gameUpdateMs": 0.00968270902780609, + "geometryStreamingMs": 0.08635819378671133, + "lodMs": 0.001383241679933336, + "physicsMs": 0.0003575232242130571, + "renderPrepMs": 0.323188425326306, + "renderTotalMs": 7.674766087244886, + "scenegraphMs": 0.18948585077769692, + "scriptingMs": 0.00030275531268368167, + "semaphoreWaitMs": 5.1406107642614374e-05, + "streamingRegionMs": 0.000903566122158534, + "submitMs": 0.008056688756268058, + "updateMs": 0.2094099783183386 }, - "worstFrameMs": 11.035249568521976, + "worstFrameMs": 12.461916543543339, "worstThermalState": 0 }, "title": "1024 primitives with SSAO, bloom, depth of field and SMAA", diff --git a/perf/baselines/Mac16_5/macOS/primitives-10k.json b/perf/baselines/Mac16_5/macOS/primitives-10k.json index 859bca386..699ed7a2d 100644 --- a/perf/baselines/Mac16_5/macOS/primitives-10k.json +++ b/perf/baselines/Mac16_5/macOS/primitives-10k.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "primitives-10k.jsonl", @@ -58,14 +59,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 256, - "frustumPassed": 9744, + "frustumFailed": 98, + "frustumPassed": 9902, "frustumTested": 10000, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 9744, - "occlusionTested": 9744, + "occlusionPassed": 9902, + "occlusionTested": 9902, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -74,175 +75,175 @@ ], "usedHZB": true }, - "frameIndex": 321, + "frameIndex": 511, "gpuPasses": { - "frameIndex": 322, + "frameIndex": 511, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.112708 + "ms": 0.074667 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.043292 + "ms": 0.0105 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.011958 + "ms": 0.013542 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.018667 + "ms": 0.007417 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.035292 + "ms": 0.031166 }, { "count": 1, "label": "Grid Pass", - "ms": 0.212917 + "ms": 0.090166 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.011959 + "ms": 0.012083 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.011625 + "ms": 0.011833 }, { "count": 1, "label": "Batched Shadow Cascade 0", - "ms": 0.041791 + "ms": 0.243333 }, { "count": 1, "label": "Batched Shadow Cascade 1", - "ms": 0.405166 + "ms": 1.0925 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 1.62725 + "ms": 2.880375 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.043333 + "ms": 0.076709 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.04875 + "ms": 0.086625 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.04 + "ms": 0.073708 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.15025 + "ms": 0.274541 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.230292 + "ms": 0.32125 }, { "count": 2, "label": "Post-Processing Pass", - "ms": 0.317709 + "ms": 0.338334 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.246333 + "ms": 0.258125 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.117083 + "ms": 0.266708 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.110625 + "ms": 0.143375 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.106666 + "ms": 0.076291 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.008209 + "ms": 0.029041 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.007083 + "ms": 0.009791 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.0205 + "ms": 0.009292 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.030875 + "ms": 0.013959 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.02975 + "ms": 0.024458 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.010125 + "ms": 0.023375 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.00625 + "ms": 0.028416 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.006959 + "ms": 0.008083 } ], "passesSkipped": 0, - "totalMs": 4.063417 + "totalMs": 6.529663 }, "hitches": { "frameBudgetMs": 12.5, - "framesLastSecond": 34, - "framesOverBudget": 322, - "framesOverBudgetLastSecond": 34, - "framesSampled": 322, + "framesLastSecond": 33, + "framesOverBudget": 512, + "framesOverBudgetLastSecond": 33, + "framesSampled": 512, "histogram": [ 0, 0, 0, 0, - 277, - 45, + 459, + 53, 0 ], - "worstFrameMsLastSecond": 49.01975020766258 + "worstFrameMsLastSecond": 49.29812625050545 }, "memory": { "availableMemoryBytes": 0, @@ -257,10 +258,10 @@ "utilizationPercent": 0.002558775246143341 }, "render": { - "drawCallsBatched": 1637, - "drawCallsOpaque": 1020, - "drawCallsShadow": 623, - "drawCallsTotal": 1650, + "drawCallsBatched": 2203, + "drawCallsOpaque": 1024, + "drawCallsShadow": 1187, + "drawCallsTotal": 2218, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -271,8 +272,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 3378685, - "visibleInstances": 9744 + "trianglesTotal": 4404421, + "visibleInstances": 9902 }, "streaming": { "activeLoads": 0, @@ -304,145 +305,148 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": true, - "updateWorkMs": 4.662036895751953, + "updateTriggered": false, + "updateWorkMs": 0, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133704.5502026667, + "timestampSeconds": 844205.3797718752, "timing": { - "animationMs": 0.0021666637621819973, + "animationMs": 0.0007499475032091141, "batchingRebuildMs": 0, - "batchingTickMs": 0.006916670827195048, - "cullingMs": 3.9716250030323863, - "customSystemsMs": 0.001166685251519084, - "encodeMs": 18.10358333750628, - "extensionsUpdateMs": 0.0008333299774676561, - "frameTotalMs": 49.43374916911125, - "gameUpdateMs": 0.1885833335109055, - "geometryStreamingMs": 4.666749999159947, - "gpuExecutionMs": 3.1636249914299697, - "gpuFrameCadenceMs": 50.05545832682401, - "lodMs": 0.00420835567638278, - "physicsMs": 0.0023749598767608404, - "physicsStepCount": 3, - "renderPrepMs": 3.9774166652932763, - "renderTotalMs": 22.232083312701434, - "scenegraphMs": 1.7329583351965994, - "scriptingMs": 0.001666689058765769, - "semaphoreWaitMs": 0, - "smoothedFrameMs": 30.790660964945953, - "streamingRegionMs": 0.002541666617617011, - "submitMs": 0.013291661161929369, - "updateMs": 1.975999999558553 + "batchingTickMs": 0.0009583309292793274, + "cullingMs": 3.3223750069737434, + "customSystemsMs": 0.00020838342607021332, + "encodeMs": 17.848874907940626, + "extensionsUpdateMs": 0.0005415640771389008, + "frameTotalMs": 28.73341739177704, + "gameUpdateMs": 0.01779163721948862, + "geometryStreamingMs": 0.009624985978007317, + "gpuExecutionMs": 5.3587500005960464, + "gpuFrameCadenceMs": 35.51700001116842, + "lodMs": 0.002374988980591297, + "physicsMs": 0.0007083872333168983, + "physicsStepCount": 1, + "renderPrepMs": 3.3261249773204327, + "renderTotalMs": 21.287124953232706, + "scenegraphMs": 1.6666667070239782, + "scriptingMs": 0.00029173679649829865, + "semaphoreWaitMs": 8.335337042808533e-05, + "smoothedFrameMs": 32.94579014182091, + "streamingRegionMs": 0.0011249212548136711, + "submitMs": 0.007999944500625134, + "updateMs": 1.6991666052490473 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "Cubes and spheres merged by the static batcher; measures batched draw cost and light-loop cost.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.935214916651603, + "durationSeconds": 14.9517183334101, "frameBudgetMs": 12.5, - "frames": 963, - "framesOverBudget": 963, + "frames": 1487, + "framesOverBudget": 1487, "gpuPassMeanMs": { - "Batched Shadow Cascade 0": 0.13152742236024845, - "Batched Shadow Cascade 1": 0.5024740866873066, - "Block Scan Pass": 0.013532718266253881, - "Compact and Stream Pass": 0.017274773993808043, - "Copy Opaque Depth for Gaussian Occlusion": 0.03643564705882351, - "Copy Opaque Depth for HZB": 0.050453907120743074, - "G-Buffer + Light Pass (TBDR)": 1.531752597523219, - "Gaussian Pass": 0.22193771428571393, - "Grid Pass": 0.21836260681114555, - "HZB Build Mip 0": 0.1743237306501548, - "HZB Build Mip 1": 0.1047644427244582, - "HZB Build Mip 10": 0.00914026182965299, - "HZB Build Mip 2": 0.04195441614906833, - "HZB Build Mip 3": 0.02085507142857145, - "HZB Build Mip 4": 0.011262386996904029, - "HZB Build Mip 5": 0.010589770897832817, - "HZB Build Mip 6": 0.014038819875776393, - "HZB Build Mip 7": 0.019162527950310575, - "HZB Build Mip 8": 0.01982048447204971, - "HZB Build Mip 9": 0.014054154574132493, - "HZB Occlusion Culling pass": 0.035897279503105624, - "Local Scan Pass": 0.022816210526315765, - "Mark Visible Pass": 0.04601806191950465, - "Output Transform Pass": 0.2581778328075709, - "Post-Processing Pass": 0.3476098385093168, - "Pre Composite Pass": 0.23887434161490675, - "Shadow Cascade 0": 0.021264906832298144, - "Shadow Cascade 1": 0.016576721362229097, - "Transparency Pass": 0.08917299690402476 + "Batched Shadow Cascade 0": 0.17825651663405093, + "Batched Shadow Cascade 1": 0.7603268336798328, + "Block Scan Pass": 0.024165120582120586, + "Compact and Stream Pass": 0.020559439271255038, + "Copy Opaque Depth for Gaussian Occlusion": 0.030378459459459467, + "Copy Opaque Depth for HZB": 0.050253981288981245, + "G-Buffer + Light Pass (TBDR)": 1.9149631372141385, + "Gaussian Pass": 0.13682269028340088, + "Grid Pass": 0.09452987044534406, + "HZB Build Mip 0": 0.07958764864864873, + "HZB Build Mip 1": 0.026129937629937598, + "HZB Build Mip 10": 0.01702957534246576, + "HZB Build Mip 2": 0.0432766777546777, + "HZB Build Mip 3": 0.01601565280665279, + "HZB Build Mip 4": 0.014263883575883578, + "HZB Build Mip 5": 0.013355647749510776, + "HZB Build Mip 6": 0.019155825831702524, + "HZB Build Mip 7": 0.011564436590436588, + "HZB Build Mip 8": 0.012598835758835757, + "HZB Build Mip 9": 0.01913976516634052, + "HZB Occlusion Culling pass": 0.0431023970893971, + "Local Scan Pass": 0.0296052085020243, + "Mark Visible Pass": 0.02274509589041089, + "Output Transform Pass": 0.17996928274428262, + "Post-Processing Pass": 0.24296467206477754, + "Pre Composite Pass": 0.10154883198380561, + "Shadow Cascade 0": 0.02623490998043056, + "Shadow Cascade 1": 0.022121970645792598, + "Transparency Pass": 0.06293870850202417 }, "gpuPassMinMs": { - "Batched Shadow Cascade 0": 0.007834, - "Batched Shadow Cascade 1": 0.26025, - "Block Scan Pass": 0.005875, - "Compact and Stream Pass": 0.004375, - "Copy Opaque Depth for Gaussian Occlusion": 0.022417, - "Copy Opaque Depth for HZB": 0.027209, - "G-Buffer + Light Pass (TBDR)": 0.982875, - "Gaussian Pass": 0.077375, - "Grid Pass": 0.05375, - "HZB Build Mip 0": 0.083291, - "HZB Build Mip 1": 0.025084, - "HZB Build Mip 10": 0.003959, - "HZB Build Mip 2": 0.009333, - "HZB Build Mip 3": 0.005083, - "HZB Build Mip 4": 0.004375, - "HZB Build Mip 5": 0.004417, - "HZB Build Mip 6": 0.004709, - "HZB Build Mip 7": 0.00425, - "HZB Build Mip 8": 0.004542, - "HZB Build Mip 9": 0.004, - "HZB Occlusion Culling pass": 0.020667, - "Local Scan Pass": 0.004333, + "Batched Shadow Cascade 0": 0.007916, + "Batched Shadow Cascade 1": 0.252417, + "Block Scan Pass": 0.005958, + "Compact and Stream Pass": 0.0045, + "Copy Opaque Depth for Gaussian Occlusion": 0.00875, + "Copy Opaque Depth for HZB": 0.010709, + "G-Buffer + Light Pass (TBDR)": 0.791625, + "Gaussian Pass": 0.0345, + "Grid Pass": 0.021, + "HZB Build Mip 0": 0.0245, + "HZB Build Mip 1": 0.008416, + "HZB Build Mip 10": 0.003958, + "HZB Build Mip 2": 0.00525, + "HZB Build Mip 3": 0.0045, + "HZB Build Mip 4": 0.004291, + "HZB Build Mip 5": 0.00425, + "HZB Build Mip 6": 0.004833, + "HZB Build Mip 7": 0.003958, + "HZB Build Mip 8": 0.003916, + "HZB Build Mip 9": 0.00425, + "HZB Occlusion Culling pass": 0.014291, + "Local Scan Pass": 0.004459, "Mark Visible Pass": 0.00525, - "Output Transform Pass": 0.162041, - "Post-Processing Pass": 0.228584, - "Pre Composite Pass": 0.178791, - "Shadow Cascade 0": 0.007084, - "Shadow Cascade 1": 0.006834, - "Transparency Pass": 0.025375 + "Output Transform Pass": 0.052166, + "Post-Processing Pass": 0.06725, + "Pre Composite Pass": 0.037041, + "Shadow Cascade 0": 0.00725, + "Shadow Cascade 1": 0.007083, + "Transparency Pass": 0.013584 }, - "meanFrameMs": 31.039583003696272, - "meanGPUExecutionMs": 3.4729653000600935, + "meanFrameMs": 29.341946090426063, + "meanGPUExecutionMs": 3.7823439841478166, + "minGPUExecutionMs": 1.3440833427011967, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 29.310083016753197, - "p95FrameMs": 47.68349975347519, - "p99FrameMs": 49.16954040527344, + "overBudgetRate": 1.0, + "p50FrameMs": 27.334416285157204, + "p95FrameMs": 44.51800137758255, + "p99FrameMs": 46.47258296608925, "peakGPUAllocatedBytes": 615792640, "timingMeanMs": { - "animationMs": 0.0008041796529659823, - "batchingTickMs": 0.0016874998340799664, + "animationMs": 0.0008555071567826112, + "batchingTickMs": 0.0010858603794625414, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 3.3322403247247596, - "customSystemsMs": 0.000321558461183254, - "encodeMs": 17.798151916073003, - "extensionsUpdateMs": 0.000936662722032967, - "gameUpdateMs": 0.04606488679431886, - "geometryStreamingMs": 0.7738778391966867, - "lodMs": 0.003309597314592024, - "physicsMs": 0.0011560558457135784, - "renderPrepMs": 3.3375624355163724, - "renderTotalMs": 21.587290764255044, - "scenegraphMs": 1.6750698750628945, - "scriptingMs": 0.00042853444085194046, - "semaphoreWaitMs": 7.036180490241298e-05, - "streamingRegionMs": 0.001263028931956819, - "submitMs": 0.009899763569803967, - "updateMs": 1.7436207427092518 + "cullingMs": 3.318322593713674, + "customSystemsMs": 0.00025379577055875105, + "encodeMs": 17.50744474406929, + "extensionsUpdateMs": 0.0006876681339342584, + "gameUpdateMs": 0.01776172319495002, + "geometryStreamingMs": 0.7852661931337934, + "lodMs": 0.0023582122130281773, + "physicsMs": 0.0010090370778925717, + "renderPrepMs": 3.322330646142291, + "renderTotalMs": 20.946014973560523, + "scenegraphMs": 1.6524055140507699, + "scriptingMs": 0.00030895919437229873, + "semaphoreWaitMs": 6.131764674777927e-05, + "streamingRegionMs": 0.0009366543492234429, + "submitMs": 0.008077683541848351, + "updateMs": 1.6883045266240515 }, - "worstFrameMs": 50.83279311656952, - "worstThermalState": 0 + "worstFrameMs": 52.26687341928482, + "worstThermalState": 1 }, "title": "10000 primitives, static batching, 4 point lights", "warmupSeconds": 3 diff --git a/perf/baselines/Mac16_5/macOS/primitives-1k.json b/perf/baselines/Mac16_5/macOS/primitives-1k.json index e1cdb0dbf..0f9b4e6ba 100644 --- a/perf/baselines/Mac16_5/macOS/primitives-1k.json +++ b/perf/baselines/Mac16_5/macOS/primitives-1k.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "primitives-1k.jsonl", @@ -58,14 +59,14 @@ "viewTextureWidth": 0 }, "culling": { - "frustumFailed": 4, - "frustumPassed": 1020, + "frustumFailed": 0, + "frustumPassed": 1024, "frustumTested": 1024, "hzbIsValid": true, "hzbMipCount": 11, "occlusionFailed": 0, - "occlusionPassed": 1020, - "occlusionTested": 1020, + "occlusionPassed": 1024, + "occlusionTested": 1024, "optimizedFrustumPath": true, "selectedHZBMipLevel": 0, "selectedHZBMipSize": [ @@ -74,183 +75,183 @@ ], "usedHZB": true }, - "frameIndex": 1198, + "frameIndex": 1763, "gpuPasses": { - "frameIndex": 1199, + "frameIndex": 1763, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.004625 + "ms": 0.010625 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.005417 + "ms": 0.013583 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.0075 + "ms": 0.017875 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.004959 + "ms": 0.011125 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.019375 + "ms": 0.044916 }, { "count": 1, "label": "Grid Pass", - "ms": 0.069791 + "ms": 0.253416 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.009208 + "ms": 0.173542 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.082709 + "ms": 0.196125 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 0.438417 + "ms": 1.042625 }, { "count": 1, "label": "Copy Opaque Depth for HZB", - "ms": 0.034375 + "ms": 0.073792 }, { "count": 1, "label": "Transparency Pass", - "ms": 0.039625 + "ms": 0.083791 }, { "count": 1, "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.030209 + "ms": 0.070292 }, { "count": 1, "label": "Gaussian Pass", - "ms": 0.121917 + "ms": 0.263917 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.23275 + "ms": 0.281417 }, { "count": 2, "label": "Post-Processing Pass", - "ms": 0.336625 + "ms": 0.5992919999999999 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.269542 + "ms": 0.509625 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.175208 + "ms": 0.472958 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.122125 + "ms": 0.127 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.022083 + "ms": 0.025416 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.006667 + "ms": 0.011209 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.0065 + "ms": 0.011417 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.030583 + "ms": 0.047292 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.032041 + "ms": 0.035583 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.022625 + "ms": 0.135458 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.006666 + "ms": 0.119042 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.005584 + "ms": 0.026167 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.007125 + "ms": 0.027791 } ], "passesSkipped": 0, - "totalMs": 2.144251 + "totalMs": 4.6852909999999985 }, "hitches": { "frameBudgetMs": 12.5, "framesLastSecond": 120, - "framesOverBudget": 2, + "framesOverBudget": 54, "framesOverBudgetLastSecond": 0, - "framesSampled": 1199, + "framesSampled": 1764, "histogram": [ 0, - 379, - 818, - 1, - 1, + 899, + 742, + 116, + 7, 0, 0 ], - "worstFrameMsLastSecond": 10.253791697323322 + "worstFrameMsLastSecond": 12.467624619603157 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 486490112, + "gpuAllocatedBytes": 486244352, "isUnderPressure": false, - "meshMemoryBytes": 12098984, + "meshMemoryBytes": 12096992, "textureBudgetBytes": 18471796080, "textureMemoryBytes": 0, "thermalState": 0, - "trackedEntityCount": 1030, - "utilizationPercent": 0.0002619990846142173 + "trackedEntityCount": 1029, + "utilizationPercent": 0.00026195592363364995 }, "render": { "drawCallsBatched": 0, - "drawCallsOpaque": 1020, - "drawCallsShadow": 984, - "drawCallsTotal": 2011, + "drawCallsOpaque": 1024, + "drawCallsShadow": 1752, + "drawCallsTotal": 2783, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -261,8 +262,8 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 399685, - "visibleInstances": 1020 + "trianglesTotal": 555025, + "visibleInstances": 1024 }, "streaming": { "activeLoads": 0, @@ -283,7 +284,7 @@ "lod0VisibilityWarningsNoFallback": 0, "lod0VisibilityWarningsWithFallback": 0, "lodAndHLODVisibleOverlapTiles": 0, - "nearbyEntitiesQueried": 1030, + "nearbyEntitiesQueried": 1029, "pendingLoadBacklog": 0, "pendingUploadCount": 0, "residentFullTileRepresentations": 0, @@ -301,134 +302,137 @@ "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133690.79086183337, + "timestampSeconds": 844186.5905053335, "timing": { - "animationMs": 0.0005833571776747704, + "animationMs": 0.00045832712203264236, "batchingRebuildMs": 0, - "batchingTickMs": 0.001041655195876956, - "cullingMs": 0.33512499067001045, - "customSystemsMs": 0.0002500019036233425, - "encodeMs": 5.965791642665863, - "extensionsUpdateMs": 0.0008333299774676561, - "frameTotalMs": 8.401166647672653, - "gameUpdateMs": 0.02904166467487812, - "geometryStreamingMs": 0.0013333337847143412, - "gpuExecutionMs": 1.4808333362452686, - "gpuFrameCadenceMs": 8.301458350615576, - "lodMs": 0.001374981366097927, - "physicsMs": 0.0006666523404419422, + "batchingTickMs": 0.0003334134817123413, + "cullingMs": 0.292458338662982, + "customSystemsMs": 0.00020826701074838638, + "encodeMs": 6.116041564382613, + "extensionsUpdateMs": 0.0004584435373544693, + "frameTotalMs": 10.521125048398972, + "gameUpdateMs": 0.006875023245811462, + "geometryStreamingMs": 0.00045832712203264236, + "gpuExecutionMs": 3.0760832596570253, + "gpuFrameCadenceMs": 9.218958322890103, + "lodMs": 0.0009583309292793274, + "physicsMs": 0.0005832407623529434, "physicsStepCount": 1, - "renderPrepMs": 0.3439583524595946, - "renderTotalMs": 7.415666652377695, - "scenegraphMs": 0.1763333275448531, - "scriptingMs": 0.00029164948500692844, - "semaphoreWaitMs": 0.00012500095181167126, - "smoothedFrameMs": 8.338668057695031, - "streamingRegionMs": 0.0007500057108700275, - "submitMs": 0.007166672730818391, - "updateMs": 0.2147083287127316 + "renderPrepMs": 0.2948334440588951, + "renderTotalMs": 6.492000073194504, + "scenegraphMs": 0.1805000938475132, + "scriptingMs": 0.00012491364032030106, + "semaphoreWaitMs": 4.1676685214042664e-05, + "smoothedFrameMs": 8.327631931751966, + "streamingRegionMs": 0.000500003807246685, + "submitMs": 0.00500003807246685, + "updateMs": 0.1990000018849969 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "One entity and draw per primitive; measures per-entity CPU cost and encoder overhead.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.991228958358988, + "durationSeconds": 14.989601250039414, "frameBudgetMs": 12.5, - "frames": 3601, - "framesOverBudget": 2, + "frames": 5352, + "framesOverBudget": 68, "gpuPassMeanMs": { - "Block Scan Pass": 0.009348957535387155, - "Compact and Stream Pass": 0.006914206494587831, - "Copy Opaque Depth for Gaussian Occlusion": 0.03411374979149293, - "Copy Opaque Depth for HZB": 0.03703215820149868, - "G-Buffer + Light Pass (TBDR)": 0.4913213930058284, - "Gaussian Pass": 0.16560686011656967, - "Grid Pass": 0.10912560782681092, - "HZB Build Mip 0": 0.1831707277268944, - "HZB Build Mip 1": 0.0915890491257285, - "HZB Build Mip 10": 0.02156562135112591, - "HZB Build Mip 2": 0.027539036636136587, - "HZB Build Mip 3": 0.01746764196502914, - "HZB Build Mip 4": 0.017508797668609504, - "HZB Build Mip 5": 0.020459615320566185, - "HZB Build Mip 6": 0.017629512927439524, - "HZB Build Mip 7": 0.0171514879065888, - "HZB Build Mip 8": 0.019084470441298903, - "HZB Build Mip 9": 0.024345608006672206, - "HZB Occlusion Culling pass": 0.024133638634471273, - "Local Scan Pass": 0.008437826810990876, - "Mark Visible Pass": 0.009653452123230696, - "Output Transform Pass": 0.30172888426311384, - "Post-Processing Pass": 0.38054572273105725, - "Pre Composite Pass": 0.24445086344712783, - "Shadow Cascade 0": 0.01252478850957529, - "Shadow Cascade 1": 0.11020184346378012, - "Transparency Pass": 0.05877844462947553 + "Block Scan Pass": 0.018543396111110992, + "Compact and Stream Pass": 0.011531038333333288, + "Copy Opaque Depth for Gaussian Occlusion": 0.028812248322147755, + "Copy Opaque Depth for HZB": 0.03326566888888886, + "G-Buffer + Light Pass (TBDR)": 0.7018186250000001, + "Gaussian Pass": 0.10534677069351235, + "Grid Pass": 0.06810618000000003, + "HZB Build Mip 0": 0.08437727181208061, + "HZB Build Mip 1": 0.049286751118568256, + "HZB Build Mip 10": 0.059105730005672194, + "HZB Build Mip 2": 0.044664948545861345, + "HZB Build Mip 3": 0.02510751106069204, + "HZB Build Mip 4": 0.022463375496313123, + "HZB Build Mip 5": 0.025950102098695416, + "HZB Build Mip 6": 0.022216137024608527, + "HZB Build Mip 7": 0.013755887583892628, + "HZB Build Mip 8": 0.01628909055555556, + "HZB Build Mip 9": 0.0297931118568233, + "HZB Occlusion Culling pass": 0.035576116666666595, + "Local Scan Pass": 0.013333217777777748, + "Mark Visible Pass": 0.020517254679523575, + "Output Transform Pass": 0.20398793959731595, + "Post-Processing Pass": 0.2552045027964206, + "Pre Composite Pass": 0.09516925055928413, + "Shadow Cascade 0": 0.1383836977777775, + "Shadow Cascade 1": 0.20502874333333335, + "Transparency Pass": 0.04059532333333333 }, "gpuPassMinMs": { - "Block Scan Pass": 0.005834, - "Compact and Stream Pass": 0.003583, - "Copy Opaque Depth for Gaussian Occlusion": 0.019458, - "Copy Opaque Depth for HZB": 0.026375, - "G-Buffer + Light Pass (TBDR)": 0.328833, - "Gaussian Pass": 0.093125, - "Grid Pass": 0.053417, - "HZB Build Mip 0": 0.083416, - "HZB Build Mip 1": 0.0225, - "HZB Build Mip 10": 0.004125, + "Block Scan Pass": 0.006166, + "Compact and Stream Pass": 0.004208, + "Copy Opaque Depth for Gaussian Occlusion": 0.014958, + "Copy Opaque Depth for HZB": 0.016167, + "G-Buffer + Light Pass (TBDR)": 0.351958, + "Gaussian Pass": 0.055417, + "Grid Pass": 0.036083, + "HZB Build Mip 0": 0.04175, + "HZB Build Mip 1": 0.014375, + "HZB Build Mip 10": 0.004541, "HZB Build Mip 2": 0.008625, - "HZB Build Mip 3": 0.005125, - "HZB Build Mip 4": 0.00425, - "HZB Build Mip 5": 0.004292, + "HZB Build Mip 3": 0.00575, + "HZB Build Mip 4": 0.004792, + "HZB Build Mip 5": 0.005, "HZB Build Mip 6": 0.004583, - "HZB Build Mip 7": 0.004083, - "HZB Build Mip 8": 0.004209, - "HZB Build Mip 9": 0.004208, - "HZB Occlusion Culling pass": 0.016167, - "Local Scan Pass": 0.004125, - "Mark Visible Pass": 0.003708, - "Output Transform Pass": 0.167084, - "Post-Processing Pass": 0.27475, - "Pre Composite Pass": 0.177666, - "Shadow Cascade 0": 0.007, - "Shadow Cascade 1": 0.063958, - "Transparency Pass": 0.020958 + "HZB Build Mip 7": 0.004333, + "HZB Build Mip 8": 0.004375, + "HZB Build Mip 9": 0.004458, + "HZB Occlusion Culling pass": 0.016708, + "Local Scan Pass": 0.004375, + "Mark Visible Pass": 0.004167, + "Output Transform Pass": 0.109625, + "Post-Processing Pass": 0.137542, + "Pre Composite Pass": 0.052542, + "Shadow Cascade 0": 0.041333, + "Shadow Cascade 1": 0.069208, + "Transparency Pass": 0.020458 }, - "meanFrameMs": 8.332067587995262, - "meanGPUExecutionMs": 1.7270092628694618, + "meanFrameMs": 8.337625573234012, + "meanGPUExecutionMs": 1.7444122379142541, + "minGPUExecutionMs": 1.1410833103582263, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 8.264750242233276, - "p95FrameMs": 9.866000153124332, - "p99FrameMs": 10.235041379928589, - "peakGPUAllocatedBytes": 486490112, + "overBudgetRate": 0.0005555555555555556, + "p50FrameMs": 7.939125411212444, + "p95FrameMs": 9.882958605885506, + "p99FrameMs": 10.568708181381226, + "peakGPUAllocatedBytes": 486244352, "timingMeanMs": { - "animationMs": 0.0007134683598468792, - "batchingTickMs": 0.0011875517771026424, + "animationMs": 0.0006732389865280598, + "batchingTickMs": 0.0008415963060946928, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 0.32106338461999046, - "customSystemsMs": 0.00013422875624029067, - "encodeMs": 6.018022102190398, - "extensionsUpdateMs": 0.0008297601903166054, - "gameUpdateMs": 0.030450942857480987, - "geometryStreamingMs": 0.08457736565334432, - "lodMs": 0.001704378349576254, - "physicsMs": 0.00039060318821558017, - "renderPrepMs": 0.3268752423947527, - "renderTotalMs": 7.425045274745915, - "scenegraphMs": 0.17717259925953752, - "scriptingMs": 0.00038828711831115565, - "semaphoreWaitMs": 0.00012836507350969423, - "streamingRegionMs": 0.0011410284680791268, - "submitMs": 0.008601859744272189, - "updateMs": 0.21856550118931686 + "cullingMs": 0.3064088379496488, + "customSystemsMs": 0.00012618211460196308, + "encodeMs": 6.3185848223358025, + "extensionsUpdateMs": 0.0005421625205279748, + "gameUpdateMs": 0.009831282262267416, + "geometryStreamingMs": 0.08381067341866179, + "lodMs": 0.001257362369743803, + "physicsMs": 0.0003589665797441131, + "renderPrepMs": 0.30966892793422496, + "renderTotalMs": 7.690429835218108, + "scenegraphMs": 0.17636950204348903, + "scriptingMs": 0.0002504671742552079, + "semaphoreWaitMs": 7.613308121443994e-05, + "streamingRegionMs": 0.0008686811068198747, + "submitMs": 0.007038572056075377, + "updateMs": 0.19522061420685843 }, - "worstFrameMs": 10.790250264108181, - "worstThermalState": 0 + "worstFrameMs": 14.32604156434536, + "worstThermalState": 1 }, "title": "1024 primitives, 4 point lights", "warmupSeconds": 3 diff --git a/perf/baselines/Mac16_5/macOS/stadium.json b/perf/baselines/Mac16_5/macOS/stadium.json index 23d4685e2..4056e40df 100644 --- a/perf/baselines/Mac16_5/macOS/stadium.json +++ b/perf/baselines/Mac16_5/macOS/stadium.json @@ -8,8 +8,9 @@ "processorCount": 16, "thermalStateAtStart": 0 }, - "label": "baseline e172615b", + "label": "baseline 88737f37+120hz", "render": { + "antiAliasing": "fxaa", "displayRefreshHz": 120, "foveation": false, "immersion": "none", @@ -22,9 +23,9 @@ "vsync": true }, "runIDs": [ - "20260920-213454-macos-r1", - "20260920-213632-macos-r2", - "20260920-213810-macos-r3" + "20261003-195536-macos-r1", + "20261003-195808-macos-r2", + "20261003-200040-macos-r3" ], "scene": { "file": "stadium.jsonl", @@ -74,170 +75,170 @@ ], "usedHZB": true }, - "frameIndex": 1197, + "frameIndex": 1788, "gpuPasses": { - "frameIndex": 1198, + "frameIndex": 1788, "passes": [ { "count": 1, "label": "Mark Visible Pass", - "ms": 0.017125 + "ms": 0.010291 }, { "count": 1, "label": "Local Scan Pass", - "ms": 0.080083 + "ms": 0.012291 }, { "count": 1, "label": "Block Scan Pass", - "ms": 0.034792 + "ms": 0.071542 }, { "count": 1, "label": "Compact and Stream Pass", - "ms": 0.020792 + "ms": 0.144958 }, { "count": 1, "label": "HZB Occlusion Culling pass", - "ms": 0.039584 + "ms": 0.02675 }, { "count": 1, "label": "Grid Pass", - "ms": 1.134125 + "ms": 0.306458 }, { "count": 1, "label": "Shadow Cascade 0", - "ms": 0.016417 + "ms": 0.159833 }, { "count": 1, "label": "Shadow Cascade 1", - "ms": 0.015 + "ms": 0.089625 }, { "count": 1, "label": "G-Buffer + Light Pass (TBDR)", - "ms": 1.951 + "ms": 1.173125 }, { "count": 1, - "label": "Copy Opaque Depth for Gaussian Occlusion", - "ms": 0.078958 + "label": "Copy Opaque Depth for HZB", + "ms": 0.074 }, { "count": 1, - "label": "Gaussian Pass", - "ms": 2.052666 + "label": "Transparency Pass", + "ms": 0.084 }, { "count": 1, - "label": "Copy Opaque Depth for HZB", - "ms": 0.079625 + "label": "Copy Opaque Depth for Gaussian Occlusion", + "ms": 0.071 }, { "count": 1, - "label": "Transparency Pass", - "ms": 0.784417 + "label": "Gaussian Pass", + "ms": 0.258875 }, { "count": 1, "label": "Pre Composite Pass", - "ms": 0.55325 + "ms": 0.29075 }, { - "count": 1, + "count": 2, "label": "Post-Processing Pass", - "ms": 0.387084 + "ms": 0.384459 }, { "count": 1, "label": "Output Transform Pass", - "ms": 0.583834 + "ms": 0.291833 }, { "count": 1, "label": "HZB Build Mip 0", - "ms": 0.411083 + "ms": 0.478792 }, { "count": 1, "label": "HZB Build Mip 1", - "ms": 0.086625 + "ms": 0.069708 }, { "count": 1, "label": "HZB Build Mip 2", - "ms": 0.039209 + "ms": 0.031791 }, { "count": 1, "label": "HZB Build Mip 3", - "ms": 0.0255 + "ms": 0.025042 }, { "count": 1, "label": "HZB Build Mip 4", - "ms": 0.023333 + "ms": 0.020917 }, { "count": 1, "label": "HZB Build Mip 5", - "ms": 0.024083 + "ms": 0.01025 }, { "count": 1, "label": "HZB Build Mip 6", - "ms": 0.020916 + "ms": 0.011167 }, { "count": 1, "label": "HZB Build Mip 7", - "ms": 0.025291 + "ms": 0.19175 }, { "count": 1, "label": "HZB Build Mip 8", - "ms": 0.029042 + "ms": 0.010125 }, { "count": 1, "label": "HZB Build Mip 9", - "ms": 0.008875 + "ms": 0.010083 }, { "count": 1, "label": "HZB Build Mip 10", - "ms": 0.005791 + "ms": 0.014458 } ], "passesSkipped": 0, - "totalMs": 8.528499999999998 + "totalMs": 4.323873000000001 }, "hitches": { "frameBudgetMs": 12.5, "framesLastSecond": 121, - "framesOverBudget": 2, + "framesOverBudget": 10, "framesOverBudgetLastSecond": 0, - "framesSampled": 1198, + "framesSampled": 1789, "histogram": [ 0, - 269, - 926, - 3, - 0, + 96, + 1675, + 17, + 1, 0, 0 ], - "worstFrameMsLastSecond": 10.284333489835262 + "worstFrameMsLastSecond": 9.337208233773708 }, "memory": { "availableMemoryBytes": 0, "geometryBudgetBytes": 27707694120, - "gpuAllocatedBytes": 649740288, + "gpuAllocatedBytes": 649609216, "isUnderPressure": false, "meshMemoryBytes": 322788, "textureBudgetBytes": 18471796080, @@ -249,8 +250,8 @@ "render": { "drawCallsBatched": 0, "drawCallsOpaque": 8, - "drawCallsShadow": 0, - "drawCallsTotal": 14, + "drawCallsShadow": 8, + "drawCallsTotal": 23, "drawCallsTransparent": 0, "tileFullDrawsEstimate": 0, "tileFullTrianglesEstimate": 0, @@ -261,7 +262,7 @@ "tileLODDrawsEstimate": 0, "tileLODTrianglesEstimate": 0, "tileLODVisibleInstances": 0, - "trianglesTotal": 2277, + "trianglesTotal": 4997, "visibleInstances": 16 }, "streaming": { @@ -294,141 +295,144 @@ "tileSwapWarnings": 0, "tilesSkippedByHierarchyGate": 0, "unloadedStreamingEntities": 0, - "updateTriggered": true, - "updateWorkMs": 0.054001808166503906, + "updateTriggered": false, + "updateWorkMs": 0, "visibleFullTileRepresentations": 0, "visibleHLODRepresentations": 0, "visibleLODRepresentations": 0, "waitingTileRepresentationFades": 0 }, - "timestampSeconds": 133760.26871145837, + "timestampSeconds": 844281.0595357502, "timing": { - "animationMs": 0.0016250123735517263, + "animationMs": 0.0009166542440652847, "batchingRebuildMs": 0, - "batchingTickMs": 0.0018333375919610262, - "cullingMs": 0.10708335321396589, - "customSystemsMs": 0.0004166504368185997, - "encodeMs": 0.663791666738689, - "extensionsUpdateMs": 0.0011249794624745846, - "frameTotalMs": 8.974291384220123, - "gameUpdateMs": 0.05158333806321025, - "geometryStreamingMs": 0.0565416703466326, - "gpuExecutionMs": 5.34495833562687, - "gpuFrameCadenceMs": 11.122249998152256, - "lodMs": 0.003583321813493967, - "physicsMs": 0.0014166580513119698, - "physicsStepCount": 1, - "renderPrepMs": 0.11358334450051188, - "renderTotalMs": 7.577458338346332, - "scenegraphMs": 0.038916681660339236, - "scriptingMs": 0.00033335527405142784, - "semaphoreWaitMs": 0.0005833571776747704, - "smoothedFrameMs": 8.344984659925103, - "streamingRegionMs": 0.003374996595084667, - "submitMs": 0.014999997802078724, - "updateMs": 0.11037502554245293 + "batchingTickMs": 0.005666748620569706, + "cullingMs": 0.038041616789996624, + "customSystemsMs": 0, + "encodeMs": 0.351916765794158, + "extensionsUpdateMs": 0.000500003807246685, + "frameTotalMs": 8.311250247061253, + "gameUpdateMs": 0.008208444342017174, + "geometryStreamingMs": 0.0009166542440652847, + "gpuExecutionMs": 2.7520000003278255, + "gpuFrameCadenceMs": 8.237000089138746, + "lodMs": 0.0014583347365260124, + "physicsMs": 0, + "physicsStepCount": 0, + "renderPrepMs": 0.04100007936358452, + "renderTotalMs": 8.400749997235835, + "scenegraphMs": 0.015041674487292767, + "scriptingMs": 0.00016659032553434372, + "semaphoreWaitMs": 0, + "smoothedFrameMs": 8.329130616039038, + "streamingRegionMs": 0.0007083872333168983, + "submitMs": 0.007999944500625134, + "updateMs": 0.036375015042722225 } }, - "measureSeconds": 10, + "measureSeconds": 15, "notes": "stadium.untold and grass.untold from the render-test resources.", "runs": 3, "summary": { "deadlineSamples": 0, - "durationSeconds": 9.98754941666266, + "durationSeconds": 14.987728500040248, "frameBudgetMs": 12.5, - "frames": 3600, - "framesOverBudget": 2, + "frames": 5386, + "framesOverBudget": 16, "gpuPassMeanMs": { - "Block Scan Pass": 0.044396186143572595, - "Compact and Stream Pass": 0.03754259950041626, - "Copy Opaque Depth for Gaussian Occlusion": 0.07857931583333318, - "Copy Opaque Depth for HZB": 0.0589980865945046, - "G-Buffer + Light Pass (TBDR)": 1.1929322916666656, - "Gaussian Pass": 1.3908513150000021, - "Grid Pass": 0.6689245634390657, - "HZB Build Mip 0": 0.3551936111573687, - "HZB Build Mip 1": 0.07696667416666658, - "HZB Build Mip 10": 0.013135387312186983, - "HZB Build Mip 2": 0.027368575833333325, - "HZB Build Mip 3": 0.017368769358867636, - "HZB Build Mip 4": 0.015805035000000005, - "HZB Build Mip 5": 0.01356378434637806, - "HZB Build Mip 6": 0.012482203164029995, - "HZB Build Mip 7": 0.012020679433805172, - "HZB Build Mip 8": 0.013120429641965053, - "HZB Build Mip 9": 0.012669101666666698, - "HZB Occlusion Culling pass": 0.05448790150250413, - "Local Scan Pass": 0.08547215275459108, - "Mark Visible Pass": 0.14195546083333313, - "Output Transform Pass": 0.31994368276436286, - "Post-Processing Pass": 0.22889284512905902, - "Pre Composite Pass": 0.3208551265611993, - "Shadow Cascade 0": 0.11578891402337256, - "Shadow Cascade 1": 0.17332324229808516, - "Transparency Pass": 0.2771254398998334 + "Block Scan Pass": 0.019228991662034418, + "Compact and Stream Pass": 0.012779119510839354, + "Copy Opaque Depth for Gaussian Occlusion": 0.032403011117287356, + "Copy Opaque Depth for HZB": 0.04107674485825462, + "G-Buffer + Light Pass (TBDR)": 0.5617387384529767, + "Gaussian Pass": 0.16160004391328503, + "Grid Pass": 0.07212819810795754, + "HZB Build Mip 0": 0.11587203835464151, + "HZB Build Mip 1": 0.0813373188647747, + "HZB Build Mip 10": 0.014886748881431799, + "HZB Build Mip 2": 0.02622177987770991, + "HZB Build Mip 3": 0.024026843400447445, + "HZB Build Mip 4": 0.022640781302170305, + "HZB Build Mip 5": 0.020364775727069375, + "HZB Build Mip 6": 0.02049250528658881, + "HZB Build Mip 7": 0.015425479688369518, + "HZB Build Mip 8": 0.014543933222036736, + "HZB Build Mip 9": 0.013502186978297191, + "HZB Occlusion Culling pass": 0.028939623679822134, + "Local Scan Pass": 0.021952868113522574, + "Mark Visible Pass": 0.013879614653243852, + "Output Transform Pass": 0.13940085754034504, + "Post-Processing Pass": 0.19658906566499748, + "Pre Composite Pass": 0.11626767648693709, + "Shadow Cascade 0": 0.10606010394663712, + "Shadow Cascade 1": 0.09222167505592856, + "Transparency Pass": 0.06527798554752631 }, "gpuPassMinMs": { - "Block Scan Pass": 0.005459, - "Compact and Stream Pass": 0.002916, - "Copy Opaque Depth for Gaussian Occlusion": 0.023834, - "Copy Opaque Depth for HZB": 0.019083, - "G-Buffer + Light Pass (TBDR)": 0.373084, - "Gaussian Pass": 0.312041, - "Grid Pass": 0.048583, - "HZB Build Mip 0": 0.093167, - "HZB Build Mip 1": 0.019458, - "HZB Build Mip 10": 0.00375, - "HZB Build Mip 2": 0.007625, - "HZB Build Mip 3": 0.004709, - "HZB Build Mip 4": 0.003916, - "HZB Build Mip 5": 0.003959, - "HZB Build Mip 6": 0.003833, - "HZB Build Mip 7": 0.003875, - "HZB Build Mip 8": 0.004041, - "HZB Build Mip 9": 0.003541, - "HZB Occlusion Culling pass": 0.009917, - "Local Scan Pass": 0.00375, - "Mark Visible Pass": 0.003667, - "Output Transform Pass": 0.122333, - "Post-Processing Pass": 0.094666, - "Pre Composite Pass": 0.170166, - "Shadow Cascade 0": 0.0065, - "Shadow Cascade 1": 0.007916, - "Transparency Pass": 0.023292 + "Block Scan Pass": 0.005916, + "Compact and Stream Pass": 0.00325, + "Copy Opaque Depth for Gaussian Occlusion": 0.022292, + "Copy Opaque Depth for HZB": 0.025167, + "G-Buffer + Light Pass (TBDR)": 0.455708, + "Gaussian Pass": 0.089042, + "Grid Pass": 0.053209, + "HZB Build Mip 0": 0.05525, + "HZB Build Mip 1": 0.022125, + "HZB Build Mip 10": 0.005166, + "HZB Build Mip 2": 0.008042, + "HZB Build Mip 3": 0.007042, + "HZB Build Mip 4": 0.00625, + "HZB Build Mip 5": 0.005542, + "HZB Build Mip 6": 0.00475, + "HZB Build Mip 7": 0.004875, + "HZB Build Mip 8": 0.004292, + "HZB Build Mip 9": 0.004375, + "HZB Occlusion Culling pass": 0.01025, + "Local Scan Pass": 0.004292, + "Mark Visible Pass": 0.003792, + "Output Transform Pass": 0.077292, + "Post-Processing Pass": 0.131458, + "Pre Composite Pass": 0.064708, + "Shadow Cascade 0": 0.019209, + "Shadow Cascade 1": 0.030833, + "Transparency Pass": 0.031084 }, - "meanFrameMs": 8.33344737534643, - "meanGPUExecutionMs": 3.8109406400004713, + "meanFrameMs": 8.333272981043491, + "meanGPUExecutionMs": 1.4801193189998476, + "minGPUExecutionMs": 1.1152916122227907, + "missedDeadlineRate": 0.0, "missedDeadlines": 0, - "p50FrameMs": 8.33504181355238, - "p95FrameMs": 9.209750220179558, - "p99FrameMs": 9.817874990403652, - "peakGPUAllocatedBytes": 649740288, + "overBudgetRate": 0.0, + "p50FrameMs": 8.332042023539543, + "p95FrameMs": 8.808458223938942, + "p99FrameMs": 9.345541708171368, + "peakGPUAllocatedBytes": 649609216, "timingMeanMs": { - "animationMs": 0.0019218139410662606, - "batchingTickMs": 0.002800501047910342, + "animationMs": 0.0008147364844032706, + "batchingTickMs": 0.0008376465921700166, "compositorDeadlineMarginMs": 0, "compositorInputSlackMs": 0, "compositorSubmissionMs": 0, "compositorUpdateMs": 0, - "cullingMs": 0.12098785753840337, - "customSystemsMs": 0.0002536173136003054, - "encodeMs": 0.6101803013610558, - "extensionsUpdateMs": 0.0012968486592403686, - "gameUpdateMs": 0.07140764680269991, - "geometryStreamingMs": 0.013353178444528623, - "lodMs": 0.0036916809236409017, - "physicsMs": 0.000845158498116982, - "renderPrepMs": 0.1297185327264509, - "renderTotalMs": 4.215444871905895, - "scenegraphMs": 0.04524646029706043, - "scriptingMs": 0.0004764534395035723, - "semaphoreWaitMs": 0.0004057104332770574, - "streamingRegionMs": 0.0021302518335704337, - "submitMs": 0.015565362390786795, - "updateMs": 0.13981546588601865 + "cullingMs": 0.03751553562674495, + "customSystemsMs": 0.00010619590936864027, + "encodeMs": 0.2550494674101679, + "extensionsUpdateMs": 0.0004566858409093895, + "gameUpdateMs": 0.009244901030161146, + "geometryStreamingMs": 0.006028660080713335, + "lodMs": 0.001345088088166634, + "physicsMs": 0.00037187186956538315, + "renderPrepMs": 0.04072039139093743, + "renderTotalMs": 0.9207651158794761, + "scenegraphMs": 0.01515303173506167, + "scriptingMs": 0.0002624515732195753, + "semaphoreWaitMs": 9.726613509154147e-05, + "streamingRegionMs": 0.000850423967334119, + "submitMs": 0.005688510123074354, + "updateMs": 0.03357507755153781 }, - "worstFrameMs": 10.799666866660118, - "worstThermalState": 0 + "worstFrameMs": 9.437541477382183, + "worstThermalState": 1 }, "title": "Stadium and grass (textured assets)", "warmupSeconds": 3