function FindScheduleEnd(Schedule: TSchedule; MinTotal, MaxTotal, MaxDuration: Int64; out Entry: PScheduleEntry): Int64; type TWaitArray = array of Int32; THeap = record Data: array of Integer; // indices into Schedule / NextTimes Count: Integer; end; TStateRec = record Hash: UInt64; TimeValue: Int64; TotalValue: Int64; Waits: TWaitArray; // normalized state: NextTime[i] - CurrentTime NextInBucket: Integer; // chained hash table end; var N: Integer; NextTimes: array of Int64; Heap: THeap; States: array of TStateRec; StateCount: Integer; Buckets: array of Integer; // stores state index, -1 = empty CurrentTotal: Int64; function MaxI(A, B: Integer): Integer; inline; begin if A > B then Result := A else Result := B; end; function EventLess(IndexA, IndexB: Integer): Boolean; inline; begin if NextTimes[IndexA] <> NextTimes[IndexB] then Result := NextTimes[IndexA] < NextTimes[IndexB] else Result := IndexA < IndexB; // schedule order tiebreak end; procedure HeapSwap(I, J: Integer); inline; var T: Integer; begin T := Heap.Data[I]; Heap.Data[I] := Heap.Data[J]; Heap.Data[J] := T; end; procedure HeapSiftUp(Pos: Integer); var Parent: Integer; begin while Pos > 0 do begin Parent := (Pos - 1) shr 1; if not EventLess(Heap.Data[Pos], Heap.Data[Parent]) then Break; HeapSwap(Pos, Parent); Pos := Parent; end; end; procedure HeapSiftDown(Pos: Integer); var L, R, Best: Integer; begin while True do begin L := (Pos shl 1) + 1; if L >= Heap.Count then Break; R := L + 1; Best := L; if (R < Heap.Count) and EventLess(Heap.Data[R], Heap.Data[L]) then Best := R; if not EventLess(Heap.Data[Best], Heap.Data[Pos]) then Break; HeapSwap(Pos, Best); Pos := Best; end; end; procedure HeapPush(Value: Integer); begin if Length(Heap.Data) = Heap.Count then SetLength(Heap.Data, MaxI(16, Heap.Count * 2)); Heap.Data[Heap.Count] := Value; Inc(Heap.Count); HeapSiftUp(Heap.Count - 1); end; function HeapPop: Integer; begin Result := Heap.Data[0]; Dec(Heap.Count); if Heap.Count > 0 then begin Heap.Data[0] := Heap.Data[Heap.Count]; HeapSiftDown(0); end; end; function HeapPeek: Integer; inline; begin Result := Heap.Data[0]; end; function MakeStateHash(const Waits: TWaitArray): UInt64; var I: Integer; begin // 64-bit FNV-1a Result := UInt64($CBF29CE484222325); for I := 0 to High(Waits) do begin Result := Result xor UInt32(Waits[I]); Result := Result * UInt64($100000001B3); end; end; function SameWaits(const A, B: TWaitArray): Boolean; var I: Integer; begin if Length(A) <> Length(B) then Exit(False); for I := 0 to High(A) do if A[I] <> B[I] then Exit(False); Result := True; end; procedure Rehash(NewBucketCount: Integer); var I, B: Integer; begin SetLength(Buckets, NewBucketCount); for I := 0 to High(Buckets) do Buckets[I] := -1; for I := 0 to StateCount - 1 do begin B := Integer(States[I].Hash and UInt64(NewBucketCount - 1)); States[I].NextInBucket := Buckets[B]; Buckets[B] := I; end; end; function FindExistingState(const Waits: TWaitArray; Hash: UInt64): Integer; var B, S: Integer; begin if Length(Buckets) = 0 then Exit(-1); B := Integer(Hash and UInt64(Length(Buckets) - 1)); S := Buckets[B]; while S <> -1 do begin if (States[S].Hash = Hash) and SameWaits(States[S].Waits, Waits) then Exit(S); S := States[S].NextInBucket; end; Result := -1; end; procedure AddState(const Waits: TWaitArray; Hash: UInt64; TimeValue, TotalValue: Int64); var B: Integer; begin if (Length(Buckets) = 0) or (StateCount * 4 >= Length(Buckets) * 3) then Rehash(MaxI(16, Length(Buckets) * 2)); if Length(States) = StateCount then SetLength(States, MaxI(16, StateCount * 2)); States[StateCount].Hash := Hash; States[StateCount].TimeValue := TimeValue; States[StateCount].TotalValue := TotalValue; States[StateCount].Waits := Copy(Waits); B := Integer(Hash and UInt64(Length(Buckets) - 1)); States[StateCount].NextInBucket := Buckets[B]; Buckets[B] := StateCount; Inc(StateCount); end; function BuildNormalizedWaits(CurrentTime: Int64): TWaitArray; var I: Integer; begin SetLength(Result, N); for I := 0 to N - 1 do Result[I] := Int32(NextTimes[I] - CurrentTime); end; procedure RecordStateAndCheckStable(CurrentTime, TotalValue: Int64; out Stable: Boolean); var Waits: TWaitArray; H: UInt64; Prev: Integer; begin Stable := False; Waits := BuildNormalizedWaits(CurrentTime); H := MakeStateHash(Waits); Prev := FindExistingState(Waits, H); if Prev >= 0 then begin // Same normalized state means the future event pattern repeats from here. // If the total is also the same, future evolution repeats exactly forever. if States[Prev].TotalValue = TotalValue then begin Stable := True; Exit; end; end else AddState(Waits, H, CurrentTime, TotalValue); end; var I: Integer; T: Int64; Idx: Integer; BestPosIdx, BestNegIdx: Integer; BestPosDelta, BestNegDelta: Int32; Stable: Boolean; InitialWaits: TWaitArray; InitialHash: UInt64; begin Entry := nil; CurrentTotal := 0; N := Length(Schedule); if MaxDuration <= 0 then begin Result := MaxDuration; Exit; end; if N = 0 then begin Result := MaxDuration; Exit; end; SetLength(NextTimes, N); Heap.Count := 0; SetLength(Heap.Data, N); for I := 0 to N - 1 do begin // First activation is after TimeOrigin + Period, always > 0. NextTimes[I] := Int64(Schedule[I].TimeOrigin) + Int64(Schedule[I].Period); HeapPush(I); end; // Record initial normalized state at time 0. InitialWaits := BuildNormalizedWaits(0); InitialHash := MakeStateHash(InitialWaits); AddState(InitialWaits, InitialHash, 0, 0); while Heap.Count > 0 do begin Idx := HeapPeek; T := NextTimes[Idx]; // No more events at or before MaxDuration. if T > MaxDuration then begin Result := MaxDuration; Entry := nil; Exit; end; BestPosIdx := -1; BestNegIdx := -1; BestPosDelta := Low(Int32); BestNegDelta := High(Int32); // Apply all entries that fire at this exact timestep. repeat Idx := HeapPop; Inc(CurrentTotal, Schedule[Idx].Delta); if Schedule[Idx].Delta > 0 then begin if (BestPosIdx < 0) or (Schedule[Idx].Delta > BestPosDelta) or ((Schedule[Idx].Delta = BestPosDelta) and (Idx < BestPosIdx)) then begin BestPosIdx := Idx; BestPosDelta := Schedule[Idx].Delta; end; end else if Schedule[Idx].Delta < 0 then begin if (BestNegIdx < 0) or (Schedule[Idx].Delta < BestNegDelta) or ((Schedule[Idx].Delta = BestNegDelta) and (Idx < BestNegIdx)) then begin BestNegIdx := Idx; BestNegDelta := Schedule[Idx].Delta; end; end; NextTimes[Idx] := NextTimes[Idx] + Int64(Schedule[Idx].Period); HeapPush(Idx); until (Heap.Count = 0) or (NextTimes[HeapPeek] <> T); // Threshold checks happen after all entries for the timestep were applied. if CurrentTotal > MaxTotal then begin Result := T; if BestPosIdx >= 0 then Entry := @Schedule[BestPosIdx] else Entry := nil; Exit; end; if CurrentTotal < MinTotal then begin Result := T; if BestNegIdx >= 0 then Entry := @Schedule[BestNegIdx] else Entry := nil; Exit; end; // Reached the horizon exactly without crossing. if T >= MaxDuration then begin Result := MaxDuration; Entry := nil; Exit; end; // Stable-cycle detection: // once the normalized "next waits" vector repeats with the same total, // the whole future repeats exactly and no threshold can ever be crossed. RecordStateAndCheckStable(T, CurrentTotal, Stable); if Stable then begin Result := MaxDuration; Entry := nil; Exit; end; end; Result := MaxDuration; Entry := nil; end;