From cd7b46f257aa40743e0a02e2869f272ac864ab62 Mon Sep 17 00:00:00 2001 From: sour Date: Wed, 5 Mar 2025 02:56:26 -0500 Subject: [PATCH 1/4] removed redundant busy check --- src/Graph/change.luau | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Graph/change.luau b/src/Graph/change.luau index 2095efed8..b4a644708 100644 --- a/src/Graph/change.luau +++ b/src/Graph/change.luau @@ -49,8 +49,6 @@ local function change( done = false table.insert(invalidateList, dependent) table.insert(searchInNext, dependent) - elseif dependent.validity == "busy" then - return External.logError("infiniteLoop") end end end From 5caac741982f3acb8669021801d25546fef60b2d Mon Sep 17 00:00:00 2001 From: sour Date: Wed, 5 Mar 2025 03:02:26 -0500 Subject: [PATCH 2/4] added handling for state objects --- src/Graph/depend.luau | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/Graph/depend.luau b/src/Graph/depend.luau index 542dc81c0..44567381e 100644 --- a/src/Graph/depend.luau +++ b/src/Graph/depend.luau @@ -11,23 +11,35 @@ local Package = script.Parent.Parent local Types = require(Package.Types) local External = require(Package.External) local evaluate = require(Package.Graph.evaluate) +local castToGraph = require(Package.Graph.castToGraph) +local castToState = require(Package.State.castToState) local nameOf = require(Package.Utility.nameOf) -local function depend( +local depend = function( dependent: Types.GraphObject, - dependency: Types.GraphObject -): () - -- Ensure dependencies are evaluated and up-to-date - -- when they are depended on. Also, newly created objects - -- might not have any transitive dependencies captured yet, - -- so ensure that they're present. - evaluate(dependency, false) + dependency: Types.UsedAs | Types.GraphObject +): any + if castToGraph(dependency) then + local dependency = dependency :: Types.GraphObject + -- Ensure dependencies are evaluated and up-to-date + -- when they are depended on. Also, newly created objects + -- might not have any transitive dependencies captured yet, + -- so ensure that they're present. + evaluate(dependency, false) - if table.isfrozen(dependent.dependencySet) or table.isfrozen(dependency.dependentSet) then - External.logError("cannotDepend", nil, nameOf(dependent, "Dependent"), nameOf(dependency, "dependency")) + if table.isfrozen(dependent.dependencySet) or table.isfrozen(dependency.dependentSet) then + External.logError("cannotDepend", nil, nameOf(dependent, "Dependent"), nameOf(dependency, "dependency")) + end + dependency.dependentSet[dependent] = true + dependent.dependencySet[dependency] = true + + if castToState(dependency) then + return (dependency :: Types.StateObject)._EXTREMELY_DANGEROUS_usedAsValue :: T + else + return nil + end end - dependency.dependentSet[dependent] = true - dependent.dependencySet[dependency] = true -end + return dependency :: T +end :: ((Types.GraphObject, Types.UsedAs) -> T) & ((Types.GraphObject, Types.GraphObject) -> ()) return depend \ No newline at end of file From 31c96b9cd5592f3ce5431b52a1de16da0fffdfff Mon Sep 17 00:00:00 2001 From: sour Date: Wed, 5 Mar 2025 03:02:53 -0500 Subject: [PATCH 3/4] reintroduce spring sleeping --- src/Animation/ExternalTime.luau | 8 +++---- src/Animation/Spring.luau | 37 ++++++++++++++------------------- src/Animation/Stopwatch.luau | 36 ++++++++++++++++++++------------ 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/Animation/ExternalTime.luau b/src/Animation/ExternalTime.luau index 8e18ce204..ce636a216 100644 --- a/src/Animation/ExternalTime.luau +++ b/src/Animation/ExternalTime.luau @@ -24,7 +24,6 @@ class.type = "State" class.kind = "ExternalTime" class.timeliness = "lazy" class.dependencySet = table.freeze {} -class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep() local METATABLE = table.freeze {__index = class} @@ -40,8 +39,9 @@ local function ExternalTime( dependentSet = {}, lastChange = nil, scope = scope, - validity = "invalid" - }, + validity = "invalid", + _EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep(), + }, METATABLE ) :: any local destroy = function() @@ -66,13 +66,13 @@ function class._evaluate( -- the external update step runs, it's safe enough to assume that the result -- has always meaningfully changed. The worst that can happen is unexpected -- refreshing for people doing unorthodox shenanigans, which is an OK trade. + self._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep() return true end External.bindToUpdateStep(function( externalNow: number ): () - class._EXTREMELY_DANGEROUS_usedAsValue = External.lastUpdateStep() for _, timer in allTimers do change(timer) end diff --git a/src/Animation/Spring.luau b/src/Animation/Spring.luau index 4b332aa49..ae3ebfdf8 100644 --- a/src/Animation/Spring.luau +++ b/src/Animation/Spring.luau @@ -53,7 +53,7 @@ type Self = Types.Spring & { local class = {} class.type = "State" class.kind = "Spring" -class.timeliness = "eager" +class.timeliness = "lazy" local METATABLE = table.freeze {__index = class} @@ -110,7 +110,7 @@ local function Spring( self.oldestTask = destroy nicknames[self.oldestTask] = "Spring" table.insert(scope, destroy) - + if goalState ~= nil then checkLifetime.bOutlivesA( scope, self.oldestTask, @@ -203,8 +203,12 @@ function class._evaluate( self._EXTREMELY_DANGEROUS_usedAsValue = self._goal :: T return false end - -- depend(self, goal) - local nextFrameGoal = peek(goal) + + local nextFrameGoal = depend(self, goal) + + local stopwatch = self._stopwatch :: Stopwatch.Stopwatch + local elapsed = depend(self, stopwatch) + -- Protect against NaN goals. if nextFrameGoal ~= nextFrameGoal then External.logWarn("springNanGoal") @@ -213,11 +217,6 @@ function class._evaluate( local nextFrameGoalType = typeof(nextFrameGoal) local discontinuous = nextFrameGoalType ~= self._activeType - local stopwatch = self._stopwatch :: Stopwatch.Stopwatch - local elapsed = peek(stopwatch) - depend(self, stopwatch) - - local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue local newValue: T if discontinuous then @@ -225,16 +224,14 @@ function class._evaluate( -- graph, even if simulation is logically one frame behind, because it -- makes the whole graph behave more consistently. newValue = nextFrameGoal - elseif elapsed <= 0 then - newValue = oldValue else -- Calculate spring motion. -- IMPORTANT: use the parameters from last frame, not this frame. We're -- integrating the motion that happened over the last frame, after all. -- The stopwatch will have captured the length of time needed correctly. local posPos, posVel, velPos, velVel = springCoefficients( - elapsed, - self._activeDamping, + elapsed, + self._activeDamping, self._activeSpeed ) local isMoving = false @@ -257,14 +254,11 @@ function class._evaluate( self._activeLatestV[index] = latestV end -- Sleep and snap to goal if the motion has decayed to a negligible amount. - if not isMoving then - for index = 1, self._activeNumSprings do - self._activeLatestP[index] = self._activeTargetP[index] - end - -- TODO: figure out how to do sleeping correctly for single frame - -- changes - -- stopwatch:pause() - -- stopwatch:zero() + if not isMoving and stopwatch:isPlaying() then + self._activeLatestP = table.clone(self._activeTargetP) + self._activeLatestV = table.create(self._activeNumSprings, 0) + stopwatch:pause() + stopwatch:zero() end -- Pack springs into final value. newValue = packType(self._activeLatestP, self._activeType) :: any @@ -305,6 +299,7 @@ function class._evaluate( -- Don't need to use the similarity test here because this code doesn't -- deal with tables, and NaN is already guarded against, so the similarity -- test doesn't actually add any new safety here. + local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue self._EXTREMELY_DANGEROUS_usedAsValue = newValue return oldValue ~= newValue end diff --git a/src/Animation/Stopwatch.luau b/src/Animation/Stopwatch.luau index b2e330c9b..10e0e74d9 100644 --- a/src/Animation/Stopwatch.luau +++ b/src/Animation/Stopwatch.luau @@ -25,12 +25,14 @@ local nicknames = require(Package.Utility.nicknames) export type Stopwatch = Types.StateObject & { zero: (Stopwatch) -> (), pause: (Stopwatch) -> (), - unpause: (Stopwatch) -> () + unpause: (Stopwatch) -> (), + isPlaying: (Stopwatch) -> boolean, } type Self = Stopwatch & { _measureTimeSince: number, _playing: boolean, + _zeroFlag: boolean, _timer: Types.StateObject } @@ -58,8 +60,9 @@ local function Stopwatch( _EXTREMELY_DANGEROUS_usedAsValue = 0, _measureTimeSince = 0, -- this should be set on unpause _playing = false, - _timer = timer - }, + _zeroFlag = true, + _timer = timer, + }, METATABLE ) :: any local destroy = function() @@ -81,12 +84,9 @@ end function class.zero( self: Self ): () - local newTimepoint = peek(self._timer) - if newTimepoint ~= self._measureTimeSince then - self._measureTimeSince = newTimepoint - self._EXTREMELY_DANGEROUS_usedAsValue = 0 - change(self) - end + self._zeroFlag = true + self._measureTimeSince = peek(self._timer) + change(self) end function class.pause( @@ -103,17 +103,28 @@ function class.unpause( ): () if self._playing == false then self._playing = true - self._measureTimeSince = peek(self._timer) - self._EXTREMELY_DANGEROUS_usedAsValue + self._measureTimeSince = peek(self._timer) - ( + if self._zeroFlag then + 0 + else + self._EXTREMELY_DANGEROUS_usedAsValue + ) change(self) end end +function class.isPlaying( + self: Self +): boolean + return self._playing +end + function class._evaluate( self: Self ): boolean if self._playing then - depend(self, self._timer) - local currentTime = peek(self._timer) + self._zeroFlag = false + local currentTime = depend(self, self._timer) local oldValue = self._EXTREMELY_DANGEROUS_usedAsValue local newValue = currentTime - self._measureTimeSince self._EXTREMELY_DANGEROUS_usedAsValue = newValue @@ -121,7 +132,6 @@ function class._evaluate( else return false end - end table.freeze(class) From e8286c645901be9335a9907bf77eb656a3c5dfe1 Mon Sep 17 00:00:00 2001 From: sour <80003255+plainsour@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:44:35 -0500 Subject: [PATCH 4/4] revert timeliness to "eager" "the original motivation for springs being eager was to be able to do timekeeping" --- src/Animation/Spring.luau | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Animation/Spring.luau b/src/Animation/Spring.luau index ae3ebfdf8..a94eae00a 100644 --- a/src/Animation/Spring.luau +++ b/src/Animation/Spring.luau @@ -53,7 +53,7 @@ type Self = Types.Spring & { local class = {} class.type = "State" class.kind = "Spring" -class.timeliness = "lazy" +class.timeliness = "eager" local METATABLE = table.freeze {__index = class} @@ -305,4 +305,4 @@ function class._evaluate( end table.freeze(class) -return Spring :: Types.SpringConstructor \ No newline at end of file +return Spring :: Types.SpringConstructor