Skip to content

Commit c894878

Browse files
committed
Improve frame timing
1 parent 1efe19e commit c894878

File tree

4 files changed

+105
-36
lines changed

4 files changed

+105
-36
lines changed

Sources/GateEngine/Game.swift

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,43 +101,19 @@ public final class Game {
101101
self.insertSystem(CacheSystem.self)
102102
self.insertSystem(DeferredDelaySystem.self)
103103
}
104-
105-
private var deltaTimeAccumulator: Double = 0
106-
private var previousTime: Double = 0
107104

108-
@MainActor
109-
internal static func getNextDeltaTime(accumulator: inout Double, previous: inout Double) -> Double? {
110-
// 240fps
111-
let stepDuration: Double = /* 1/240 */ 0.004166666667
112-
let now: Double = Platform.current.systemTime()
113-
let newDeltaTimeAccumulator: Double = accumulator + (now - previous)
114-
if newDeltaTimeAccumulator < stepDuration {
115-
return nil
116-
}
117-
118-
accumulator = newDeltaTimeAccumulator
119-
previous = now
120-
let deltaTime = stepDuration * (accumulator / stepDuration)
121-
accumulator -= deltaTime
122-
// Discard times larger then 12 fps. This will cause slow down but will also reduce
123-
// of the chance of the simulation from breaking
124-
if deltaTime > /* 1/12 */ 0.08333333333 {
125-
return nil
126-
}
127-
128-
return deltaTime
129-
}
105+
private var deltaTimeHelper = DeltaTimeHelper(name: "System")
130106

131107
#if GATEENGINE_PLATFORM_EVENT_DRIVEN
132108
@MainActor internal func eventLoop(completion: @escaping () -> Void) {
133-
guard let deltaTime = Game.getNextDeltaTime(accumulator: &deltaTimeAccumulator, previous: &previousTime) else {
109+
guard let highPrecisionDeltaTime = deltaTimeHelper.getDeltaTime() else {
134110
completion()
135111
return
136112
}
137113

138114
// Add a high priority Task so we can jump the line if other Tasks were started
139115
Task(priority: .high) { @MainActor in
140-
let deltaTime = Float(deltaTime)
116+
let deltaTime = Float(highPrecisionDeltaTime)
141117
self.resourceManager.update(withTimePassed: deltaTime)
142118
await windowManager.updateWindows(deltaTime: deltaTime)
143119
self.windowManager.drawWindows()
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
internal struct DeltaTimeHelper {
9+
internal let name: String
10+
private var accumulator: Double = 0
11+
private var previousTime: Double = .nan
12+
13+
private static let preferredFrameRate: Int = Platform.current.prefferedFrameRate()
14+
private static let simulationStepDuration: Double = 1 / Double(preferredFrameRate) / 2
15+
private static let maxSimulationSteps: Int = 4
16+
17+
internal init(name: String) {
18+
self.name = name
19+
self.reset()
20+
}
21+
22+
mutating func reset() {
23+
self.accumulator = 0
24+
self.previousTime = .nan
25+
}
26+
27+
@MainActor
28+
@inlinable
29+
mutating func getDeltaTime() -> Double? {
30+
let currentTime = Platform.current.systemTime()
31+
32+
// Determine the new accumulator value
33+
let newAccumulator: Double = self.accumulator + (currentTime - self.previousTime)
34+
35+
// If our accumulated time is less than a single step, bail
36+
if newAccumulator < Self.simulationStepDuration {
37+
return nil
38+
}
39+
40+
// Update previousTime if we get this far
41+
self.previousTime = currentTime
42+
43+
// If our accumulated time is not a number, bail
44+
if newAccumulator.isFinite == false {
45+
return nil
46+
}
47+
48+
// Update the accumulator
49+
self.accumulator = newAccumulator
50+
51+
// The number of steps that currently fit in the accumulator
52+
let stepsRequired = Int(accumulator / Self.simulationStepDuration)
53+
54+
// If stepsRequired is greater than maxSimulationSteps, return only that
55+
// Erase the accumulator as this is likely a spike from a CPU hang
56+
if stepsRequired > Self.maxSimulationSteps {
57+
// Anytime we return from here, the simulation will run slower.
58+
// This is similar in severity to a dropped frame and will happen if the simulation is lagging.
59+
// To reduce occurances of this, do less work.
60+
// Try spreading complex tasks out across multiple updates.
61+
let message = "\(name) deltaTime attempted to use \(stepsRequired) steps, and was truncated to \(Self.maxSimulationSteps)."
62+
if stepsRequired > 15 {
63+
Log.warn(message, "This is a \(String(format: "%0.3f", Self.simulationStepDuration * Double(stepsRequired))) second hang!")
64+
}else{
65+
Log.debug(message, "This is a minor performance issue.")
66+
}
67+
68+
let maxSimulationSteps = Double(Self.maxSimulationSteps)
69+
70+
// Zero the accumulator to give the next update the maximum time, becuase we just had a hang
71+
self.accumulator = 0
72+
73+
// Return the maximum allowed deltaTime
74+
return Self.simulationStepDuration * maxSimulationSteps
75+
}
76+
77+
// Calculate fixed step deltaTime
78+
let deltaTime = Self.simulationStepDuration * Double(stepsRequired)
79+
80+
// Remove the used time from the accumulator
81+
self.accumulator -= deltaTime
82+
83+
return deltaTime
84+
}
85+
}

Sources/GateEngine/System/Platforms/PlatformProtocol.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ internal protocol InternalPlatformProtocol: PlatformProtocol {
5656
func setCursorStyle(_ style: Mouse.Style)
5757

5858
func systemTime() -> Double
59+
func prefferedFrameRate() -> Int
5960
@MainActor func main()
6061

6162
#if GATEENGINE_PLATFORM_HAS_FILESYSTEM
@@ -248,6 +249,10 @@ extension InternalPlatformProtocol {
248249
return URL(fileURLWithPath: try fileSystem.pathForSearchPath(.persistent, in: .currentUser)).appendingPathComponent(name).path
249250
}
250251

252+
func prefferedFrameRate() -> Int {
253+
return 60
254+
}
255+
251256
#if os(macOS) || os(iOS) || os(tvOS) || os(Windows) || os(Linux)
252257
func systemTime() -> Double {
253258
var time: timespec = timespec()

Sources/GateEngine/UI/GameViewController.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,7 @@ public final class GameView: View {
7979
}
8080
var mode: Mode = .screen
8181

82-
private var deltaTimeAccumulator: Double = 0
83-
private var previousTime: Double = 0
82+
private var deltaTimeHelper = DeltaTimeHelper(name: "Rendering")
8483

8584
override final func draw(_ rect: Rect, into canvas: inout UICanvas) {
8685
var frame = frame
@@ -91,14 +90,9 @@ public final class GameView: View {
9190
}
9291

9392
if let gameViewController {
94-
guard let _deltaTime = Game.getNextDeltaTime(
95-
accumulator: &deltaTimeAccumulator,
96-
previous: &previousTime
97-
) else {
98-
return
99-
}
93+
let highPrecisionDeltaTime = self.deltaTimeHelper.getDeltaTime() ?? .leastNonzeroMagnitude
10094

101-
let deltaTime = Float(_deltaTime)
95+
let deltaTime = Float(highPrecisionDeltaTime)
10296
gameViewController.render(context: gameViewController.context, into: self, withTimePassed: deltaTime)
10397
gameViewController.context.updateRendering(into: self, deltaTime: deltaTime)
10498

@@ -140,11 +134,19 @@ public final class GameView: View {
140134
return _renderTarget?.texture ?? self.window!.texture
141135
}
142136

137+
public override func didLayout() {
138+
super.didLayout()
139+
self.deltaTimeHelper.reset()
140+
}
141+
143142
public override func didChangeSuperview() {
143+
super.didChangeSuperview()
144+
144145
if self.superView == nil {
145146
_renderTarget = nil
146147
return
147148
}
149+
148150
if _viewController?.isRootViewController == true {
149151
self.mode = .screen
150152
_renderTarget = nil
@@ -159,6 +161,7 @@ public final class GameView: View {
159161
_renderTarget = RenderTarget(backgroundColor: self.backgroundColor ?? .clear)
160162
Game.shared.attributes.remove(.renderingIsPermitted)
161163
}
164+
162165
self.pendingBackgroundColor = nil
163166
}
164167
}

0 commit comments

Comments
 (0)