new simplified timer. and little other ops

This commit is contained in:
Youmu Liu
2025-11-18 20:39:03 +08:00
parent 81a60d1cfe
commit ece12f2ea7
28 changed files with 2254 additions and 853 deletions
@@ -26,6 +26,7 @@
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace StgSharp.Collections
@@ -33,9 +34,13 @@ namespace StgSharp.Collections
public static class LinearBagBuilder
{
/// <summary>
/// Build a <see cref="LinearBag{T}" /> from a read-only span. Capacity will exactly fit
/// the input.
/// </summary>
public static LinearBag<T> FromSpan<T>(ReadOnlySpan<T> values)
{
if (values.IsEmpty || values.Length == 0) {
if (values.IsEmpty) {
return new LinearBag<T>(4);
}
LinearBag<T> bag = new(values.Length);
@@ -47,12 +52,24 @@ namespace StgSharp.Collections
}
/// <summary>
/// A very lightweight contiguous storage structure: unordered, not enumerable by design.
/// Provides O(1) Add and O(1) RemoveAt (by swapping with the tail). Intended for high-
/// performance scenarios where element ordering does not matter. NOT thread-safe.
/// </summary>
/// <typeparam name="T">
/// Element type.
/// </typeparam>
[CollectionBuilder(typeof(LinearBagBuilder), nameof(LinearBagBuilder.FromSpan))]
public class LinearBag<T>(int capacity = 4)
{
private T[] _items = new T[capacity];
// Invariant: valid elements occupy indexes [0, Count-1]. Count is the number of live elements.
private T[] _items = new T[Math.Max(0, capacity)];
/// <summary>
/// Access element by index (0..Count-1). Throws if out of range.
/// </summary>
public T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -68,10 +85,19 @@ namespace StgSharp.Collections
}
}
/// <summary>
/// Total allocated slots.
/// </summary>
public int Capacity => _items.Length;
/// <summary>
/// Number of currently stored elements (valid index range 0..Count-1).
/// </summary>
public int Count { get; private set; }
/// <summary>
/// Adds an item (unordered). Expands internal array if needed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(T item)
{
@@ -79,15 +105,19 @@ namespace StgSharp.Collections
EnsureCapacity(Count + 1);
}
_items[Count++] = item;
_items[Count] = item;
Count++;
}
/// <summary>
/// Returns a span over the live portion of the bag (0..Count-1).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<T> AsSpan()
{
return new Span<T>(_items, 0, Count);
}
public Span<T> AsSpan() => new Span<T>(_items, 0, Count);
/// <summary>
/// Clears all elements and zeroes out references to avoid retention. Count becomes 0.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Clear()
{
@@ -95,18 +125,22 @@ namespace StgSharp.Collections
Count = 0;
}
/// <summary>
/// Fast clear: only resets Count (leaves underlying array unchanged). References are NOT
/// released. Use only when you are certain old references can remain.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void FastClear()
{
// 只重置size,不清理数组内容
Count = 0;
}
public void FastClear() => Count = 0;
/// <summary>
/// Removes the first occurrence of value (unordered). Does nothing if not found.
/// </summary>
public void Remove(T value)
{
EqualityComparer<T> comparer = EqualityComparer<T>.Default;
for (int i = 0; i < Count; i++)
{
if (_items[i]?.Equals(value) ?? false)
if (comparer.Equals(_items[i], value))
{
RemoveAt(i);
return;
@@ -114,6 +148,10 @@ namespace StgSharp.Collections
}
}
/// <summary>
/// Removes element at index by swapping the last element into its slot (unless already
/// last). Preserves O(1) removal at cost of ordering.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveAt(int index)
{
@@ -122,27 +160,30 @@ namespace StgSharp.Collections
}
int last = Count - 1;
if (index != last)
{
_items[index] = _items[last]; // swap back from tail
if (index != last) {
_items[index] = _items[last];
}
_items[last] = default!; // clear last slot to avoid ref retention
_items[last] = default!; // release reference / reset value
Count = last;
}
/// <summary>
/// Ensures capacity is at least <paramref name="min" />. Doubles size when growing.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureCapacity(int min)
{
if (_items.Length < min)
{
int newCapacity = _items.Length == 0 ? 4 : _items.Length * 2;
if (newCapacity < min) {
newCapacity = min;
}
Array.Resize(ref _items, newCapacity);
if (_items.Length >= min) {
return;
}
int newCapacity = _items.Length == 0 ? 4 : _items.Length * 2;
if (newCapacity < min) {
newCapacity = min;
}
Array.Resize(ref _items, newCapacity);
}
}
@@ -45,14 +45,11 @@ namespace StgSharp.Controls
private PlainGeometryMesh marker;
private TimeFunction _movement;
private Vec2 _currentPosition, _movementVec;
public MarkingLabel(PlainGeometryMesh marker, TimeFunction movement)
public MarkingLabel(PlainGeometryMesh marker)
{
this.marker = marker;
_movement = movement;
}
public bool IsEntity => true;
@@ -74,9 +71,7 @@ namespace StgSharp.Controls
public void Move(Vec2 movement)
{
if (!_movement.IsComplete) {
Position = _currentPosition + (_movement.Calculate() * _movementVec);
}
throw new NotImplementedException();
}
public void MoveTo(Vec2 target)
+1 -1
View File
@@ -186,7 +186,7 @@ namespace StgSharp
internal static unsafe void InternalInitialize()
{
mainTimeProvider = new StgSharpTime();
// Initialize time provider and start providing time
MainTimeProvider.StartProvidingTime();
GraphicFramework.LoadGlfw();
if (GraphicFramework.glfwInit() == 0) {
+3 -3
View File
@@ -120,7 +120,7 @@ namespace StgSharp.Graphics
protected float TimePassed
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _timeProvider.CurrentSecond;
get => (float)_timeProvider.CurrentSecond;
}
protected TimeSpanProvider TimeProvider
@@ -147,8 +147,8 @@ namespace StgSharp.Graphics
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public abstract void SetSizeLimit(int minWidth, int minHeight, int maxWidth, int maxHeight);
public static TTarget ShareContextFrom<TSource, TTarget>([NotNull]TSource source) where TSource: RenderStream
where TTarget: RenderStream, new()
public static TTarget ShareContextFrom<TSource, TTarget>([NotNull]TSource source) where TSource : RenderStream
where TTarget : RenderStream, new()
{
TTarget target = new TTarget();
target.Initialize(source.BindedViewPortContext, source._timeProvider);
+13 -2
View File
@@ -230,9 +230,18 @@ namespace StgSharp.Internal
IntPtr monitor,
IntPtr share)
{
nint handle;
fixed (byte* titlePtr = title) {
return _glfw.glfwCreateWindow(width, height, titlePtr, monitor, share);
handle = _glfw.glfwCreateWindow(width, height, titlePtr, monitor, share);
}
if (handle == 0)
{
byte* desc;
int code = _glfw.glfwGetError(&desc);
string msg = $"{code}: {Marshal.PtrToStringUTF8((nint)desc)}";
throw new InvalidOperationException(msg);
}
return handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -813,7 +822,9 @@ namespace StgSharp.Internal
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe IntPtr glfwGetProcAddress(ReadOnlySpan<byte> procName)
{
return glfwGetProcAddress(procName);
fixed (byte* sPtr = procName) {
return _glfw.glfwGetProcAddress(sPtr);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="glfwFunction"
// file="glfw.function"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
+17 -27
View File
@@ -57,9 +57,8 @@ namespace StgSharp.MVVM.ViewModel
{
CustomizeInitialize();
UseViewInNextFrame(viewEntryName);
TimeSpanAwaitingToken fpsCountingToken = fpsCountProvider.ParticipantSpanAwaiting(1);
fpsCountingToken.TimeSpanRefreshed += CountFps;
fpsCountingToken.MissTimeSpanRefreshed += CountFps;
fpsCountProvider.TimeSpanRefreshed += CountFps;
fpsCountProvider.MissTimeSpanRefreshed += CountFps;
if (_nextView != _currentView) {
_ = Interlocked.Exchange(ref _currentView, _nextView);
}
@@ -78,24 +77,19 @@ namespace StgSharp.MVVM.ViewModel
});
renderThread.Start();
/**/
using (TimeSpanAwaitingToken eventAwaitingToken = frameTimeProvider.ParticipantSpanAwaiting(
1))
while (_closed)
{
while (_closed)
{
if (_nextView != _currentView) {
_ = Interlocked.Exchange(ref _currentView, _nextView);
}
GraphicFramework.glfwPollEvents();
eventAwaitingToken.WaitNextSpan();
_closed = !ShouldClose;
if (_nextView != _currentView) {
_ = Interlocked.Exchange(ref _currentView, _nextView);
}
GraphicFramework.glfwPollEvents();
frameTimeProvider.WaitNextSpan();
_closed = !ShouldClose;
}
/**/
renderThread.Join();
fpsCountingToken.Dispose();
fpsCountProvider.StopSpanProviding();
frameTimeProvider.StopSpanProviding();
fpsCountProvider.Dispose();
frameTimeProvider.Dispose();
}
protected void UseViewInNextFrame(string name)
@@ -124,19 +118,15 @@ namespace StgSharp.MVVM.ViewModel
private void ProcessFrameWithoutEvent()
{
using (TimeSpanAwaitingToken renderAwaitingToken = frameTimeProvider.ParticipantSpanAwaiting(
1))
frameTimeProvider.TimeSpanRefreshed += (_, _) => { };
while (_closed)
{
renderAwaitingToken.TimeSpanRefreshed += (_, _) => { };
while (_closed)
{
foreach (ViewPort context in CurrentView.Render) {
context.FlushSize();
}
CurrentView.Use();
renderAwaitingToken.WaitNextSpan();
Interlocked.Add(ref _frameCount, 1);
foreach (ViewPort context in CurrentView.Render) {
context.FlushSize();
}
CurrentView.Use();
frameTimeProvider.WaitNextSpan();
Interlocked.Add(ref _frameCount, 1);
}
}
+16 -18
View File
@@ -25,6 +25,7 @@
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.Common.Timing;
using StgSharp.Controlling.UsrActivity;
using StgSharp.Graphics;
using StgSharp.Graphics.OpenGL;
@@ -65,24 +66,23 @@ namespace StgSharp.MVVM.ViewModel
{
viewPortDisplay = new ViewPort();
allView = [];
fpsCountProvider = new TimeSpanProvider(1.0, TimeSpanProvider.DefaultProvider);
fpsCountProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, 1.0 / frameSpeed), TimeSpanProvider.DefaultProvider);
_frameSpeed = 0;
_frameCount = 0;
long frameLength = (FrameSpeed > 0) ? (1000L * 1000L / FrameSpeed) : 0;
frameTimeProvider = new TimeSpanProvider(frameLength, TimeSpanProvider.DefaultProvider);
frameTimeProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, 1.0 / frameSpeed), TimeSpanProvider.DefaultProvider);
SetFrameSizeCallback(DefaultFrameResizeCallback);
}
protected ViewModelBase(float frameLength)
protected ViewModelBase(double frameLength)
{
viewPortDisplay = new ViewPort();
allView = [];
fpsCountProvider = new TimeSpanProvider(1.0, TimeSpanProvider.DefaultProvider);
fpsCountProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, frameLength), TimeSpanProvider.DefaultProvider);
_frameSpeed = 0;
_frameCount = 0;
long _frameLength = (FrameSpeed > 0) ? ((long)(1000 * frameLength)) : 0;
frameTimeProvider = new TimeSpanProvider(_frameLength,
TimeSpanProvider.DefaultProvider);
frameTimeProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, frameLength), TimeSpanProvider.DefaultProvider);
SetFrameSizeCallback(DefaultFrameResizeCallback);
}
@@ -90,24 +90,23 @@ namespace StgSharp.MVVM.ViewModel
{
viewPortDisplay = vBinding;
allView = [];
fpsCountProvider = new TimeSpanProvider(1.0, TimeSpanProvider.DefaultProvider);
fpsCountProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, 1.0), TimeSpanProvider.DefaultProvider);
_frameSpeed = 0;
_frameCount = 0;
long frameLength = (frameSpeed > 0) ? (1000L * 1000L / frameSpeed) : 0;
frameTimeProvider = new TimeSpanProvider(frameLength, TimeSpanProvider.DefaultProvider);
frameTimeProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, 1.0 / frameSpeed), TimeSpanProvider.DefaultProvider);
SetFrameSizeCallback(DefaultFrameResizeCallback);
}
public ViewModelBase(ViewPort vBinding, float frameLength)
public ViewModelBase(ViewPort vBinding, double frameLength)
{
viewPortDisplay = vBinding;
allView = [];
fpsCountProvider = new TimeSpanProvider(1.0, TimeSpanProvider.DefaultProvider);
fpsCountProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, 1.0), TimeSpanProvider.DefaultProvider);
_frameSpeed = 0;
_frameCount = 0;
long _frameLength = (frameLength > 0) ? ((long)(1000 * frameLength)) : 0;
frameTimeProvider = new TimeSpanProvider(_frameLength,
TimeSpanProvider.DefaultProvider);
long _frameLength = (FrameSpeed > 0) ? ((long)(1000 * frameLength)) : 0;
frameTimeProvider = new TimeSpanProvider(TimeSequence.CreateLinear(double.MaxValue, frameLength), TimeSpanProvider.DefaultProvider);
SetFrameSizeCallback(DefaultFrameResizeCallback);
}
@@ -193,9 +192,8 @@ namespace StgSharp.MVVM.ViewModel
public void SetSizeLimit(Vec2 minSize, Vec2 maxSize)
{
ThreadHelper.MainThreadAssert();
GraphicFramework.glfwSetWindowSizeLimits(
this.viewPortDisplay.ViewPortHandle, (int)minSize.X, (int)minSize.Y, (int)maxSize.X,
(int)maxSize.Y);
GraphicFramework.glfwSetWindowSizeLimits(this.viewPortDisplay.ViewPortHandle, (int)minSize.X,
(int)minSize.Y, (int)maxSize.X, (int)maxSize.Y);
}
/// <summary>
@@ -217,8 +215,8 @@ namespace StgSharp.MVVM.ViewModel
public void SetSizeLimit(int minWidth, int minHeight, int maxWidth, int maxHeight)
{
ThreadHelper.MainThreadAssert();
GraphicFramework.glfwSetWindowSizeLimits(
this.viewPortDisplay.ViewPortHandle, minWidth, minHeight, maxWidth, maxHeight);
GraphicFramework.glfwSetWindowSizeLimits(this.viewPortDisplay.ViewPortHandle, minWidth, minHeight, maxWidth,
maxHeight);
}
public abstract bool TryGetObjectReference(string name, out DataBindingEntry entry);
+4
View File
@@ -84,6 +84,10 @@
</PropertyGroup>
<ItemGroup>
<None Remove=".nuspec" />
<None Remove="StgSharp.Common.deps.json" />
<None Remove="StgSharp.Common.dll" />
<None Remove="StgSharp.Common.pdb" />
<None Remove="StgSharp.Common.xml" />
</ItemGroup>
<ItemGroup>
+23
View File
@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"StgSharp.Common/1.0.0": {
"runtime": {
"StgSharp.Common.dll": {}
}
}
}
},
"libraries": {
"StgSharp.Common/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,316 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="VariableCapacitySemaphore"
// Project: StgSharp.Common
// AuthorGroup: Nitload
// License: MIT
// -----------------------------------------------------------------------
using System;
using System.Runtime.CompilerServices;
using System.Threading;
namespace StgSharp.Threading
{
/// <summary>
/// A counting semaphore with adjustable maximum capacity at runtime. Implemented using one or
/// more ManualResetEventSlim (MRES) gates and atomic counters to avoid kernel semaphore
/// overhead while supporting high-frequency Release/Wait patterns. Notes: - This is a
/// counting primitive (not a barrier). Permits accumulate up to <see cref="MaxPermits" />. -
/// Reducing <see cref="MaxPermits" /> does not revoke already issued permits; it only clamps
/// future releases. - Striped MRES can reduce contention in highly parallel waits.
/// </summary>
public sealed class VariableCapacitySemaphore : IDisposable
{
// MRES stripes (one gate per stripe). All stripes are set when transitioning 0 -> >0 and reset at >0 -> 0.
private readonly ManualResetEventSlim[] _events;
private volatile int _current;
private volatile int _max;
private readonly int _stripeMask; // for power-of-two stripe counts; otherwise -1
/// <summary>
/// Create a new variable-capacity semaphore.
/// </summary>
/// <param name="initialCount">
/// Initial available permits (clamped to [0, maxPermits]).
/// </param>
/// <param name="maxPermits">
/// Logical maximum permits (>= 1).
/// </param>
/// <param name="stripes">
/// Optional stripe count for MRES (<= 0 to auto choose by CPU).
/// </param>
public VariableCapacitySemaphore(int initialCount = 0, int maxPermits = 1, int stripes = 0)
{
ArgumentOutOfRangeException.ThrowIfLessThan(maxPermits, 1);
ArgumentOutOfRangeException.ThrowIfNegative(initialCount);
_max = maxPermits;
_current = Math.Min(initialCount, maxPermits);
int stripeCount = stripes > 0 ? stripes : ClampToPow2(Math.Min(Environment.ProcessorCount, 8));
_events = new ManualResetEventSlim[stripeCount];
bool set = _current > 0;
for (int i = 0; i < stripeCount; i++) {
_events[i] = new ManualResetEventSlim(set, 0);
}
_stripeMask = IsPow2(stripeCount) ? (stripeCount - 1) : -1;
}
/// <summary>
/// Current number of available permits.
/// </summary>
public int CurrentCount => Volatile.Read(ref _current);
/// <summary>
/// Logical maximum number of permits. Can be changed at runtime.
/// </summary>
public int MaxPermits
{
get => Volatile.Read(ref _max);
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1);
Volatile.Write(ref _max, value);
// Clamp current to not exceed the new max (do not block; let Wait() consume down).
while (true)
{
int cur = Volatile.Read(ref _current);
if (cur <= value)
{
break;
}
if (Interlocked.CompareExchange(ref _current, value, cur) == cur)
{
break;
}
}
// Reflect availability to gates
if (Volatile.Read(ref _current) > 0)
{
SetAll();
} else
{
ResetAll();
}
}
}
public void Dispose()
{
foreach (ManualResetEventSlim e in _events) {
e.Dispose();
}
}
/// <summary>
/// Release a single permit if under <see cref="MaxPermits" />. Returns the previous
/// count.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Release()
{
while (true)
{
int cur = Volatile.Read(ref _current);
int max = Volatile.Read(ref _max);
if (cur >= max) {
return cur;
}
if (Interlocked.CompareExchange(ref _current, cur + 1, cur) == cur)
{
if (cur == 0) {
SetAll();
}
return cur;
}
}
}
/// <summary>
/// Release multiple permits (capped by <see cref="MaxPermits" />). Returns the previous
/// count.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Release(int permits)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(permits);
while (true)
{
int cur = Volatile.Read(ref _current);
int max = Volatile.Read(ref _max);
if (cur >= max) {
return cur;
}
long target = (long)cur + permits;
if (target > max) {
target = max;
}
if (Interlocked.CompareExchange(ref _current, (int)target, cur) == cur)
{
if (cur == 0) {
SetAll();
}
return cur;
}
}
}
/// <summary>
/// Try to take a permit without blocking. Returns true if acquired.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryWait()
{
while (true)
{
int cur = Volatile.Read(ref _current);
if (cur == 0) {
return false;
}
if (Interlocked.CompareExchange(ref _current, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
ResetAll();
}
return true;
}
}
}
/// <summary>
/// Block until a permit is available, then consume one.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Wait()
{
ManualResetEventSlim gate = GetStripeEvent();
SpinWait spinner = new SpinWait();
while (true)
{
int cur = Volatile.Read(ref _current);
if (cur > 0 && Interlocked.CompareExchange(ref _current, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
ResetAll();
}
return;
}
if (!spinner.NextSpinWillYield)
{
spinner.SpinOnce();
} else
{
gate.Wait();
}
}
}
/// <summary>
/// Block until a permit is available or canceled, then consume one.
/// </summary>
public bool Wait(int millisecondsTimeout, CancellationToken cancellationToken = default)
{
ManualResetEventSlim gate = GetStripeEvent();
SpinWait spinner = new SpinWait();
int start = Environment.TickCount;
int remaining = millisecondsTimeout;
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
int cur = Volatile.Read(ref _current);
if (cur > 0 && Interlocked.CompareExchange(ref _current, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
ResetAll();
}
return true;
}
if (!spinner.NextSpinWillYield)
{
spinner.SpinOnce();
continue;
}
if (millisecondsTimeout < 0)
{
gate.Wait(cancellationToken);
continue;
}
if (remaining <= 0) {
return false;
}
if (!gate.Wait(remaining, cancellationToken)) {
return false;
}
remaining = millisecondsTimeout - (Environment.TickCount - start);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ClampToPow2(int x)
{
if (x < 1) {
x = 1;
}
if (x > 8) {
x = 8;
}
int p = 1;
while ((p << 1) <= x) {
p <<= 1;
}
return p;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ManualResetEventSlim GetStripeEvent()
{
int id = Environment.CurrentManagedThreadId & 0x7fffffff;
if (_stripeMask != -1) {
return _events[id & _stripeMask];
}
int idx = id % _events.Length;
return _events[idx];
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPow2(int x) => (x & (x - 1)) == 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ResetAll()
{
foreach (ManualResetEventSlim e in _events) {
e.Reset();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetAll()
{
foreach (ManualResetEventSlim e in _events) {
e.Set();
}
}
}
}
@@ -1,6 +1,6 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeProvider"
// file="StgSharpTime"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
@@ -27,12 +27,8 @@
// -----------------------------------------------------------------------
using StgSharp.Collections;
using StgSharp.Timing;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
namespace StgSharp
@@ -40,9 +36,7 @@ namespace StgSharp
public static partial class World
{
internal static StgSharpTime mainTimeProvider = new StgSharpTime();
internal static TimeSourceProviderBase MainTimeProvider => mainTimeProvider;
public static TimeSourceProviderBase MainTimeProvider => StgSharpTime.OnlyInstance;
}
@@ -50,40 +44,30 @@ namespace StgSharp
{
private static readonly LinearBag<TimeSpanProvider> _subscribers = new();
private static long accuracy;
private static readonly Stopwatch mainProvider = new();
private static Thread timeProvideThread;
internal StgSharpTime() { }
private StgSharpTime() { }
public static TimeSourceProviderBase OnlyInstance => World.MainTimeProvider;
// Use Stopwatch frequency as base tick frequency (ticks per second).
public override long Frequency => Stopwatch.Frequency;
public static StgSharpTime OnlyInstance { get; } = new StgSharpTime();
public override long GetCurrentTimeSpanTick() => mainProvider.ElapsedTicks;
public sealed override void StartProvidingTime()
{
if (mainProvider.IsRunning) {
return;
}
mainProvider.Start();
// speed test
long totalMS, internalFrequency = Stopwatch.Frequency;
for (int i = 0; i < 100; i++)
mainProvider.Start();
timeProvideThread = new Thread(ProvideTime)
{
totalMS = mainProvider.ElapsedTicks / (internalFrequency / (1000L * 1000L));
for (int t = 0; t < 10; t++)
{
if (totalMS < 1000) {
_subscribers.Add(
new TimeSpanProvider(100L, TimeSpanProvider.DefaultProvider));
}
}
}
mainProvider.Stop();
_subscribers.Clear();
totalMS = mainProvider.ElapsedTicks / (internalFrequency / (1000L * 1000L)) / 100L;
accuracy = totalMS * 2 / 3;
mainProvider.Reset();
timeProvideThread = new Thread(new ThreadStart(ProvideTime));
IsBackground = true,
Name = "StgSharpTime"
};
timeProvideThread.Start();
}
@@ -113,29 +97,36 @@ namespace StgSharp
private void ProvideTime()
{
long totalMS, internalFrequency = Stopwatch.Frequency;
long internalFrequency = Stopwatch.Frequency; // ticks per second
lock (mainProvider) {
mainProvider.Restart();
}
while (mainProvider.IsRunning)
{
totalMS = mainProvider.ElapsedTicks / (internalFrequency / (1000L * 1000L));
long elapsedTicks;
lock (mainProvider) {
elapsedTicks = mainProvider.ElapsedTicks;
}
if (_subscribers.Count == 0)
{
Thread.SpinWait(50);
continue;
}
lock (SubscribeLock)
{
for (int i = 0; i < _subscribers.Count; i++)
{
if (!_subscribers[i].CheckTime(totalMS))
if (!_subscribers[i].OnSpanTick(elapsedTicks))
{
_subscribers.RemoveAt(i);
DefaultLog.InternalWriteLog(
$"Time subscriber {_subscribers[i]} is to be removed.", LogType.Info);
i--; // adjust index after removal
}
}
}
// Optional: brief spin to avoid tight loop if frame lengths are large.
Thread.SpinWait(20);
}
}
-143
View File
@@ -1,143 +0,0 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeFunction"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace StgSharp.Timing
{
public class TimeFunction
{
private float _beginning, _ending;
private TimeSpanProvider _timeProvider;
internal TimeFunction() { }
public TimeFunction(TimeSpanProvider timeSpanProvider, Func<float, float> timeRelatedCalculation, float range)
{
_timeProvider = timeSpanProvider;
TimeRelatedCalculation = timeRelatedCalculation;
if (timeSpanProvider == null) {
return;
}
_beginning = _timeProvider.CurrentSecond;
_ending = _timeProvider.CurrentSecond + range;
}
public virtual bool IsComplete
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _timeProvider.CurrentSecond > _ending;
}
public Func<float, float> TimeRelatedCalculation { get; set; }
public static TimeFunction One
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new TimeFunctionOne();
}
public TimeSpanProvider TimeSpanProvider
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _timeProvider;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => _timeProvider = value;
}
public virtual float Calculate()
{
if (_timeProvider.CurrentSecond < _ending) {
return 0;
}
return TimeRelatedCalculation(_timeProvider.CurrentSecond - _beginning);
}
public static TimeFunction Proportional(TimeSpanProvider time, float begin, float range)
{
return new TimeFunction(time, (float t) => t + begin, range);
}
public virtual TimeFunction Renew((Func<float, float> func, float range) funcPair)
{
return new TimeFunction(_timeProvider, funcPair.func, funcPair.range);
}
public virtual TimeFunction Renew(Func<float, float> func, float range)
{
return new TimeFunction(_timeProvider, func, range);
}
public virtual void Reset()
{
float range = _ending - _beginning;
_beginning = _timeProvider.CurrentSecond;
_ending = _beginning + range;
}
public virtual TimeFunction Reverse()
{
float range = _ending - _beginning;
return new TimeFunction(
_timeProvider, p => TimeRelatedCalculation(range - p), range);
}
}
public sealed class TimeFunctionOne : TimeFunction
{
internal TimeFunctionOne() : base() { }
public override bool IsComplete => true;
public override float Calculate()
{
return 1;
}
public override TimeFunction Renew((Func<float, float> func, float range) funcPair)
{
throw new NotSupportedException();
}
public override TimeFunction Renew(Func<float, float> func, float range)
{
throw new NotSupportedException();
}
#pragma warning disable CA1822
public override void Reset()
{
}
#pragma warning restore CA1822
}
}
+160 -36
View File
@@ -25,69 +25,193 @@
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.Mathematics;
using StgSharp.Timing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace StgSharp.Common.Timing
{
public abstract class TimeSequence
{
public ulong BeginningTick { get; protected set; }
public long BeginningTick { get; protected set; }
public ulong EndingTick { get; protected set; }
public long EndingTick { get; protected set; }
public ulong FrameLength { get; protected set; }
public long PreviousFrameTick { get; protected set; }
public static TimeSequence CreateLinear(ulong timeout, ulong length)
{
return new LinearTimeSequence(timeout, timeout + length, length);
}
public static TimeSequence CreateLinear(double totalSeconds, double frameSeconds) => new LinearTimeSequence(totalSeconds, frameSeconds);
public abstract bool IsNextFrameReady(ulong currentTick);
public static TimeSequence CreateLogarithmic(
double initialSliceSeconds,
double totalSeconds,
double growthMultiplier,
double minSliceSeconds) => new LogTimeSequence(initialSliceSeconds, totalSeconds,
growthMultiplier, minSliceSeconds);
}
public abstract bool EndsAt(long currentTick);
internal sealed class LogTimeSequence : TimeSequence
{
public abstract bool IsNextFrameReady(long currentTick);
private float _rate ;
public LogTimeSequence(ulong beginningTick, ulong endingTick, ulong tickPerDecade)
{
BeginningTick = beginningTick;
EndingTick = endingTick;
_rate = Scalar.Pow(10, 1 / tickPerDecade);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(ulong currentTick)
{
return (ulong)Math.Log(currentTick - BeginningTick + 1) >= FrameLength;
}
public abstract void StartFrom(TimeSourceProviderBase source);
}
internal sealed class LinearTimeSequence : TimeSequence
{
public LinearTimeSequence(ulong beginningTick, ulong endingTick, ulong tickPerFrame)
private readonly double _frameSeconds;
private readonly double _totalSeconds;
private long _frameLengthTicks;
public LinearTimeSequence(double totalSeconds, double frameSeconds)
{
BeginningTick = beginningTick;
EndingTick = endingTick;
FrameLength = tickPerFrame;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalSeconds);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(frameSeconds);
if (frameSeconds > totalSeconds) {
throw new ArgumentException("Frame length greater than total length.");
}
_totalSeconds = totalSeconds;
_frameSeconds = frameSeconds;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(ulong currentTick)
public override bool EndsAt(long currentTick) => currentTick >= EndingTick;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(long currentTick)
{
return currentTick - BeginningTick >= FrameLength;
long span = currentTick - PreviousFrameTick;
if (span >= _frameLengthTicks && currentTick < EndingTick)
{
PreviousFrameTick += _frameLengthTicks; // advance start of next frame
return true;
}
return false;
}
public override void StartFrom(TimeSourceProviderBase source)
{
long f = source?.Frequency ?? throw new ArgumentNullException(nameof(source));
_frameLengthTicks = (long)Math.Max(1, _frameSeconds * f);
long totalTicks = (long)Math.Max(_frameLengthTicks, Math.Round(_totalSeconds * f, MidpointRounding.AwayFromZero));
BeginningTick = source.GetCurrentTimeSpanTick();
EndingTick = BeginningTick + totalTicks;
EndingTick = EndingTick < 0 ? long.MaxValue : EndingTick;
PreviousFrameTick = BeginningTick;
}
}
/// <summary>
/// Logarithmic time sequence with growth factor. Boundaries (cumulative times since start)
/// follow initial * multiplier^k, but each slice duration (difference between adjusted
/// boundaries) is at least minSliceSeconds. First slice uses the exact initial length even if
/// below minSliceSeconds, subsequent slices enforce minimum. Example: initial=2,
/// multiplier=2, total>=8, min=3 => boundaries: 2,5,8 (durations:2,3,3).
/// </summary>
internal sealed class LogTimeSequence : TimeSequence
{
private readonly double _initialSeconds;
private double _lastBoundarySecondsAdjusted; // adjusted cumulative seconds of last emitted boundary (0 at start)
private readonly double _minSliceSeconds;
private readonly double _multiplier;
private double _nextBoundarySecondsAdjusted; // adjusted cumulative seconds for next boundary
private readonly double _totalSeconds;
private int _sliceIndex; // index of next boundary (0 => first boundary at initialSeconds)
private long _freq;
private long _nextBoundaryTick; // absolute tick for next boundary
private long _totalTicks;
public LogTimeSequence(
double initialSliceSeconds,
double totalSeconds,
double growthMultiplier,
double minSliceSeconds)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialSliceSeconds);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(totalSeconds);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(growthMultiplier);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minSliceSeconds);
if (growthMultiplier < 1.0) {
throw new ArgumentException("growthMultiplier must be >= 1");
}
if (initialSliceSeconds > totalSeconds) {
throw new ArgumentException("Initial slice length greater than total length.");
}
_initialSeconds = initialSliceSeconds;
_totalSeconds = totalSeconds;
_multiplier = growthMultiplier;
_minSliceSeconds = minSliceSeconds;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool EndsAt(long currentTick) => currentTick >= EndingTick ||
_nextBoundarySecondsAdjusted >= _totalSeconds;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool IsNextFrameReady(long currentTick)
{
if (currentTick < _nextBoundaryTick || currentTick >= EndingTick) {
return false;
}
// Emit frame: update previous frame tick and prepare next boundary.
PreviousFrameTick = _nextBoundaryTick; // boundary just reached
_lastBoundarySecondsAdjusted = _nextBoundarySecondsAdjusted;
_sliceIndex++;
// Compute original (unadjusted) next cumulative boundary in seconds.
double originalNext = _initialSeconds * Math.Pow(_multiplier, _sliceIndex);
// Enforce total cap.
if (originalNext > _totalSeconds)
{
originalNext = _totalSeconds;
}
// Compute slice duration based on adjusted previous boundary.
double delta = originalNext - _lastBoundarySecondsAdjusted;
if (_sliceIndex > 0 && delta < _minSliceSeconds)
{
// Enforce minimum (except for first slice which uses initial value)
originalNext = _lastBoundarySecondsAdjusted + _minSliceSeconds;
if (originalNext > _totalSeconds) {
originalNext = _totalSeconds;
}
}
_nextBoundarySecondsAdjusted = originalNext;
if (_nextBoundarySecondsAdjusted >= _totalSeconds)
{
_nextBoundaryTick = EndingTick; // sequence will end after this boundary
} else
{
_nextBoundaryTick = BeginningTick + SecondsToTicks(_nextBoundarySecondsAdjusted);
}
return true;
}
public override void StartFrom(TimeSourceProviderBase source)
{
_freq = source?.Frequency ?? throw new ArgumentNullException(nameof(source));
BeginningTick = source.GetCurrentTimeSpanTick();
_totalTicks = SecondsToTicks(_totalSeconds);
EndingTick = BeginningTick + _totalTicks;
_sliceIndex = 0;
_lastBoundarySecondsAdjusted = 0.0;
_nextBoundarySecondsAdjusted = _initialSeconds; // first boundary uses exact initial length
_nextBoundaryTick = BeginningTick + SecondsToTicks(_nextBoundarySecondsAdjusted);
PreviousFrameTick = BeginningTick; // start point
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private long SecondsToTicks(double seconds) => (long)Math.Max(1L, Math.Round(seconds * _freq, MidpointRounding.AwayFromZero));
}
}
@@ -36,6 +36,8 @@ namespace StgSharp.Timing
public abstract class TimeSourceProviderBase
{
public abstract long Frequency { get; }
protected object SubscribeLock { get; } = new();
public void AddSubscriber(TimeSpanProvider subscriber)
@@ -45,6 +47,8 @@ namespace StgSharp.Timing
}
}
public abstract long GetCurrentTimeSpanTick();
public void RemoveSubscriber(TimeSpanProvider subscriber)
{
lock (SubscribeLock) {
@@ -1,147 +0,0 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeSpanAwaitingToken"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.HighPerformance.Memory;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace StgSharp.Timing
{
public sealed class TimeSpanAwaitingToken : IDisposable
{
private static readonly ConcurrentBufferStack<int> s_unusedID = new(1);
private static int s_maxID;
private EventHandler? _refreshHandler,_missHandler;
private int _size, _id;
private readonly object _participantObject = new();
private SemaphoreSlim _awaitingSemaphore;
private TimeSpanProvider _provider;
internal TimeSpanAwaitingToken(TimeSpanProvider provider)
{
if (!s_unusedID.TryPop(out int id)) {
id = Interlocked.Increment(ref s_maxID);
}
TokenID = id;
_provider = provider;
_size = 1;
_awaitingSemaphore = new SemaphoreSlim(0, 1);
}
internal TimeSpanAwaitingToken(TimeSpanProvider provider, int count)
{
if (!s_unusedID.TryPop(out int id)) {
id = Interlocked.Increment(ref s_maxID);
}
_provider = provider;
_size = count;
_awaitingSemaphore = new SemaphoreSlim(0, _size);
}
public event EventHandler MissTimeSpanRefreshed
{
add => _missHandler += value;
remove => _missHandler -= value;
}
public event EventHandler TimeSpanRefreshed
{
add => _refreshHandler += value;
remove => _refreshHandler -= value;
}
public int TokenID
{
get => _id;
private set => _id = value;
}
internal SemaphoreSlim AwaitingSemaphoreSlim
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _awaitingSemaphore;
}
public void Dispose()
{
_provider.QuitSpanAwaiting(this);
s_unusedID.Push(TokenID);
_awaitingSemaphore.Dispose();
GC.SuppressFinalize(this);
}
public override int GetHashCode()
{
return _id;
}
public void Participant()
{
lock (_participantObject)
{
_size++;
SemaphoreSlim newSemaphore = new(_awaitingSemaphore.CurrentCount, _size);
Interlocked.Exchange(ref _awaitingSemaphore, newSemaphore).Dispose();
}
}
public void WaitNextSpan()
{
_awaitingSemaphore.Wait();
}
internal void MissRefresh()
{
_missHandler?.Invoke(this, EventArgs.Empty);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Refresh()
{
_refreshHandler?.Invoke(this, EventArgs.Empty);
}
~TimeSpanAwaitingToken()
{
Dispose();
}
}
}
@@ -0,0 +1,198 @@
//-----------------------------------------------------------------------
// -----------------------------------------------------------------------
// file="TimeSpanProvider.Awaiting"
// Project: StgSharp
// AuthorGroup: Nitload
// Copyright (c) Nitload. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.HighPerformance.Memory;
using System;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace StgSharp.Timing
{
public partial class TimeSpanProvider
{
private static readonly ConcurrentBufferStack<int> s_unusedID = new ConcurrentBufferStack<int>(8);
private static int s_maxID;
private EventHandler? _refreshHandler, _missHandler;
private int _pendingSpans; // Number of unconsumed span ticks since last Wait.
private int _size, _id;
private readonly ManualResetEventSlim _event = new();
private readonly object _participantObject = new();
public event EventHandler MissTimeSpanRefreshed
{
add => _missHandler += value;
remove => _missHandler -= value;
}
public event EventHandler TimeSpanRefreshed
{
add => _refreshHandler += value;
remove => _refreshHandler -= value;
}
/// <summary>
/// True if there is at least one unconsumed span tick (PendingSpanCount &gt; 0).
/// </summary>
public bool HasPendingSpan => PendingSpanCount > 0;
public int TokenID
{
get => _id;
private set => _id = value;
}
/// <summary>
/// Number of span ticks not yet consumed by Wait ( &gt; 0 means last tick not waited).
/// </summary>
public int PendingSpanCount => Volatile.Read(ref _pendingSpans);
internal ManualResetEventSlim AwaitingEvent
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _event;
}
public void Dispose()
{
s_unusedID.Push(TokenID);
_event.Dispose();
GC.SuppressFinalize(this);
}
public override int GetHashCode() => _id;
public void Participant()
{
lock (_participantObject)
{
// Increase the maximum concurrent consumers for the next tick.
_size++;
}
}
public void WaitNextSpan()
{
_event.Wait();
// Consume one span slot (clamp at 0 so multiple subscribers won't make it negative).
while (true)
{
int cur = Volatile.Read(ref _pendingSpans);
if (cur == 0) {
return;
}
if (Interlocked.CompareExchange(ref _pendingSpans, cur - 1, cur) == cur)
{
if (cur - 1 == 0)
{
// Last consumer resets the gate for the next tick.
_event.Reset();
}
return;
}
}
}
public async Task WaitNextSpanAsync()
{
await Task.Run(() => _event.Wait()).ConfigureAwait(false);
while (true)
{
int cur = Volatile.Read(ref _pendingSpans);
if (cur == 0)
{
break;
}
if (Interlocked.CompareExchange(ref _pendingSpans, cur - 1, cur) == cur)
{
if (cur - 1 == 0) {
_event.Reset();
}
break;
}
}
}
internal void MissRefresh() => _missHandler?.Invoke(this, EventArgs.Empty); // Legacy compatibility.
/// <summary>
///
/// </summary>
/// <param name="currentTick">
/// Tick passed since binded time source.
/// </param>
/// <returns>
/// True if the span provider is working. False if time is over (the sequence ended).
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool OnSpanTick(long currentTick)
{
if (_sequence.EndsAt(currentTick))
{
Dispose();
return false;
}
if (!_sequence.IsNextFrameReady(currentTick)) {
return true;
}
Console.WriteLine("Frame hit!");
// Fast path: if no pending consumers, initialize the counter to current capacity (_size).
if (Volatile.Read(ref _pendingSpans) == 0)
{
int toIssue = Math.Max(1, Volatile.Read(ref _size));
if (Interlocked.CompareExchange(ref _pendingSpans, toIssue, 0) == 0)
{
// Open the gate for all current waiters (up to `_size`).
_event.Set();
_refreshHandler?.Invoke(this, EventArgs.Empty);
}
} else
{
// A tick is already pending (or was just set by another thread): mark as miss.
_missHandler?.Invoke(this, EventArgs.Empty);
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Refresh() => _refreshHandler?.Invoke(this, EventArgs.Empty); // Legacy compatibility.
~TimeSpanProvider() => Dispose();
}
}
+21 -175
View File
@@ -25,207 +25,53 @@
//
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
using StgSharp.Common.Timing;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace StgSharp.Timing
{
/// <summary>
/// A microsecond-accuracy timer. It will refresh every a certain time span.
/// TimeSpanProvider: uses a bound TimeSequence and TimeSourceProviderBase to publish span
/// ticks. Each OnSpanTick advances the sequence; when sequence ends provider unsubscribes.
/// </summary>
public class TimeSpanProvider
public partial class TimeSpanProvider
{
private bool _subscribed;
private readonly ConcurrentQueue<TimeSpanAwaitingToken> _tokenToAdd = new();
private readonly ConcurrentQueue<TimeSpanAwaitingToken> _tokenToQuit;
private Counter<int> timeSpanCount = new(-1, 1, 1);
private readonly HashSet<TimeSpanAwaitingToken> _awaitingTokens;
private long _maxSpanCount;
private long _spanBegin;
private readonly long _spanLength;
private int _emittedSpanCount; // legacy counter
private readonly TimeSequence _sequence;
private readonly TimeSourceProviderBase _timeSource;
/// <summary>
/// Create a new <see cref="TimeSpanProvider" /> and define the length of time span.
/// Length of a single time span is defined in microseconds.
/// </summary>
/// <param _label="spanMicroSeconds">
///
/// </param>
/// <param _label="provider">
///
/// </param>
public TimeSpanProvider(long spanMicroSeconds, TimeSourceProviderBase provider)
public TimeSpanProvider(TimeSequence sequence, TimeSourceProviderBase provider, int maxWaitCount = 1)
{
ArgumentNullException.ThrowIfNull(provider);
ArgumentNullException.ThrowIfNull(sequence);
_timeSource = provider;
_spanLength = spanMicroSeconds;
_maxSpanCount = int.MaxValue;
_sequence = sequence;
_sequence.StartFrom(provider);
provider.AddSubscriber(this);
_subscribed = true;
_awaitingTokens = [];
_tokenToQuit = new ConcurrentQueue<TimeSpanAwaitingToken>();
}
/// <summary>
/// Create a new <see cref="TimeSpanProvider" /> and define the length of time span.
/// Length of a single time span is defined in seconds.
/// </summary>
/// <param _label="spanSeconds">
///
/// </param>
/// <param _label="provider">
///
/// </param>
public TimeSpanProvider(double spanSeconds, TimeSourceProviderBase provider)
{
ArgumentNullException.ThrowIfNull(provider);
_timeSource = provider;
_spanLength = (long)(spanSeconds * 1000L * 1000L);
_spanBegin = -_spanLength;
_maxSpanCount = long.MaxValue;
provider.AddSubscriber(this);
_subscribed = true;
_awaitingTokens = [];
_tokenToQuit = new ConcurrentQueue<TimeSpanAwaitingToken>();
}
/// <summary>
/// Create a new <see cref="TimeSpanProvider" /> and define the length of time span.
/// Length of a single time span is defined in microseconds.
/// </summary>
/// <param _label="spanLength">
///
/// </param>
/// <param _label="maxSpan">
///
/// </param>
/// <param _label="provider">
///
/// </param>
public TimeSpanProvider(long spanLength, long maxSpan, TimeSourceProviderBase provider)
{
ArgumentNullException.ThrowIfNull(provider);
if (maxSpan <= 0) {
throw new ArgumentException("Sapn count cannot be smaller than zero");
if (!s_unusedID.TryPop(out int id)) {
id = Interlocked.Increment(ref s_maxID);
}
_timeSource = provider;
_spanLength = spanLength;
_spanBegin = -_spanLength;
provider.AddSubscriber(this);
_subscribed = true;
_awaitingTokens = new HashSet<TimeSpanAwaitingToken>();
_tokenToQuit = new ConcurrentQueue<TimeSpanAwaitingToken>();
TokenID = id;
_size = maxWaitCount > 0 ? maxWaitCount : 1;
_event = new ManualResetEventSlim(initialState:false, spinCount:0);
_pendingSpans = 0;
}
public double SpanLength
public double CurrentSecond
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _spanLength * 1.0 / (1000L * 1000L);
get => (double)_sequence.PreviousFrameTick / _timeSource.Frequency;
}
public float CurrentSecond
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => timeSpanCount.Value * SpanLengthLowPrecession;
}
public int CurrentSpan => _emittedSpanCount;
public float SpanLengthLowPrecession
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _spanLength * 1.0f / (1000 * 1000);
}
public static TimeSourceProviderBase DefaultProvider => World.MainTimeProvider;
public int CurrentSpan
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => timeSpanCount.Value;
}
/// <summary>
/// Time source provider provided in <see cref="World" /> internal framework.
/// </summary>
public static TimeSourceProviderBase DefaultProvider => StgSharpTime.OnlyInstance;
public TimeSpanAwaitingToken ParticipantSpanAwaiting(int count)
{
TimeSpanAwaitingToken token = new(this, count);
_tokenToAdd.Enqueue(token);
return token;
}
public void QuitSpanAwaiting(TimeSpanAwaitingToken token)
{
_tokenToQuit.Enqueue(token);
}
public void StopSpanProviding()
{
_ = Interlocked.Exchange(ref _maxSpanCount, 0);
}
public void SubscribeToTimeSource(TimeSourceProviderBase serviceHandler)
{
ArgumentNullException.ThrowIfNull(serviceHandler);
if (_subscribed) {
return;
}
_subscribed = true;
serviceHandler.AddSubscriber(this);
}
public int ToInt32()
{
return timeSpanCount.Value;
}
internal bool CheckTime(long microseconds)
{
if (microseconds - _spanBegin < _spanLength) {
return true;
}
foreach (TimeSpanAwaitingToken tokenAdd in _tokenToAdd) {
_ = _awaitingTokens.Add(tokenAdd);
}
_tokenToAdd.Clear();
foreach (TimeSpanAwaitingToken tokenQuit in _tokenToQuit)
{
_ = _awaitingTokens.Remove(tokenQuit);
DefaultLog.InternalWriteLog(
$"Awaiting token {tokenQuit!.TokenID} has been removed.", LogType.Info);
}
_tokenToQuit.Clear();
_spanBegin = microseconds;
timeSpanCount++;
foreach (TimeSpanAwaitingToken item in _awaitingTokens)
{
if (item.AwaitingSemaphoreSlim.CurrentCount == 0)
{
_ = item.AwaitingSemaphoreSlim.Release();
item.Refresh();
} else
{
item.MissRefresh();
}
}
return timeSpanCount < _maxSpanCount;
}
public static explicit operator int([NotNull]TimeSpanProvider provider)
{
return provider.ToInt32();
}
public static explicit operator int(TimeSpanProvider provider) => provider?._emittedSpanCount ?? 0;
}
}