diff --git a/R3.sln b/R3.sln index 2e17bb81..994a7d1d 100644 --- a/R3.sln +++ b/R3.sln @@ -22,7 +22,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C7327A31-4 README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "R3.WPF", "src\R3.WPF\R3.WPF.csproj", "{57AC0130-0D5F-489A-A565-B41EF9928085}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "R3.WPF", "src\R3.WPF\R3.WPF.csproj", "{57AC0130-0D5F-489A-A565-B41EF9928085}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfApp1", "sandbox\WpfApp1\WpfApp1.csproj", "{BA40E541-3BCD-438A-B966-2FC7BE80AB80}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -46,6 +48,10 @@ Global {57AC0130-0D5F-489A-A565-B41EF9928085}.Debug|Any CPU.Build.0 = Debug|Any CPU {57AC0130-0D5F-489A-A565-B41EF9928085}.Release|Any CPU.ActiveCfg = Release|Any CPU {57AC0130-0D5F-489A-A565-B41EF9928085}.Release|Any CPU.Build.0 = Release|Any CPU + {BA40E541-3BCD-438A-B966-2FC7BE80AB80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BA40E541-3BCD-438A-B966-2FC7BE80AB80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BA40E541-3BCD-438A-B966-2FC7BE80AB80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA40E541-3BCD-438A-B966-2FC7BE80AB80}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,6 +61,7 @@ Global {72DE3CB9-195E-4740-9416-5960D75ED795} = {FAB2137C-1DBA-4F2F-8E22-DF3521C9B365} {42F7C4F7-3BB3-4DC8-8285-9DFF7E93BC4B} = {0544806B-3BB4-43CF-8277-BC612F32208D} {57AC0130-0D5F-489A-A565-B41EF9928085} = {9FA6D327-728B-4436-AE3A-9E46D8FEF591} + {BA40E541-3BCD-438A-B966-2FC7BE80AB80} = {FAB2137C-1DBA-4F2F-8E22-DF3521C9B365} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84B77761-6B9E-46BA-B132-6C77B0B6E4FA} diff --git a/sandbox/WpfApp1/App.xaml b/sandbox/WpfApp1/App.xaml new file mode 100644 index 00000000..2e70522d --- /dev/null +++ b/sandbox/WpfApp1/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/sandbox/WpfApp1/App.xaml.cs b/sandbox/WpfApp1/App.xaml.cs new file mode 100644 index 00000000..56a3e205 --- /dev/null +++ b/sandbox/WpfApp1/App.xaml.cs @@ -0,0 +1,12 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace WpfApp1; +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ +} + diff --git a/sandbox/WpfApp1/AssemblyInfo.cs b/sandbox/WpfApp1/AssemblyInfo.cs new file mode 100644 index 00000000..b0ec8275 --- /dev/null +++ b/sandbox/WpfApp1/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/sandbox/WpfApp1/MainWindow.xaml b/sandbox/WpfApp1/MainWindow.xaml new file mode 100644 index 00000000..8b0a0e9f --- /dev/null +++ b/sandbox/WpfApp1/MainWindow.xaml @@ -0,0 +1,12 @@ + + + + + diff --git a/sandbox/WpfApp1/MainWindow.xaml.cs b/sandbox/WpfApp1/MainWindow.xaml.cs new file mode 100644 index 00000000..31e0ec7b --- /dev/null +++ b/sandbox/WpfApp1/MainWindow.xaml.cs @@ -0,0 +1,49 @@ +using R3; +using R3.WPF; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Windows.Threading; + +namespace WpfApp1; +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + + + + //Dispatcher.Yield(DispatcherPriority.Input); + + + + R3.WPF.WpfProviderInitializer.SetDefaultProviders(); + + + + + //var sw = Stopwatch.StartNew(); + //Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)).Subscribe(_ => + //{ + // textBlock.Text = "Hello World:" + sw.Elapsed; + //}); + + Observable.TimerFrame(50, 100).Subscribe(_ => + { + textBlock.Text = "Hello World:" + ObservableSystem.DefaultFrameProvider.GetFrameCount(); + }); + } +} diff --git a/sandbox/WpfApp1/WpfApp1.csproj b/sandbox/WpfApp1/WpfApp1.csproj new file mode 100644 index 00000000..0c280836 --- /dev/null +++ b/sandbox/WpfApp1/WpfApp1.csproj @@ -0,0 +1,16 @@ + + + + WinExe + net8.0-windows + enable + enable + true + + + + + + + + diff --git a/src/R3.WPF/DispatcherFrameProvider.cs b/src/R3.WPF/DispatcherFrameProvider.cs deleted file mode 100644 index 42ddce39..00000000 --- a/src/R3.WPF/DispatcherFrameProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Windows.Threading; - -namespace R3.WPF; - -public sealed class DispatcherFrameProvider : FrameProvider -{ - - public DispatcherFrameProvider(Dispatcher dispatcher) - { - - var foo = dispatcher.InvokeAsync(() => - { - }); - - - } - - public override long GetFrameCount() - { - throw new NotImplementedException(); - } - - public override void Register(IFrameRunnerWorkItem callback) - { - throw new NotImplementedException(); - } -} diff --git a/src/R3.WPF/DispatcherTimerProvider.cs b/src/R3.WPF/DispatcherTimerProvider.cs deleted file mode 100644 index d699a8d6..00000000 --- a/src/R3.WPF/DispatcherTimerProvider.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Windows.Threading; - -namespace R3.WPF; - -public sealed class DispatcherTimerProvider : TimeProvider -{ - public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) - { - return base.CreateTimer(callback, state, dueTime, period); - } -} - -internal sealed class DispatcherTimerProviderTimer : ITimer -{ - DispatcherTimer? timer; - TimerCallback callback; - object? state; - EventHandler timerTick; - TimeSpan? period; - - public DispatcherTimerProviderTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) - { - this.timerTick = Timer_Tick; - this.callback = callback; - this.state = state; - this.timer = new DispatcherTimer(); - timer.Tick += timerTick; - - Change(dueTime, period); - } - - public bool Change(TimeSpan dueTime, TimeSpan period) - { - if (timer != null) - { - timer.Stop(); - - this.period = period; - timer.Interval = dueTime; - - timer.Start(); - return true; - } - return false; - } - - void Timer_Tick(object? sender, EventArgs e) - { - callback(state); - - if (timer != null && period != null) - { - timer.Stop(); - - timer.Interval = period.Value; - period = null; - - timer.Start(); - } - } - - public void Dispose() - { - if (timer != null) - { - timer.Stop(); - timer.Tick -= timerTick; - timer = null; - } - } - - public ValueTask DisposeAsync() - { - Dispose(); - return default; - } -} diff --git a/src/R3.WPF/WpfDispatcherTimerProvider.cs b/src/R3.WPF/WpfDispatcherTimerProvider.cs new file mode 100644 index 00000000..5a03c280 --- /dev/null +++ b/src/R3.WPF/WpfDispatcherTimerProvider.cs @@ -0,0 +1,119 @@ +using System.Windows.Threading; + +namespace R3.WPF; + +public sealed class WpfDispatcherTimerProvider : TimeProvider +{ + readonly DispatcherPriority? priority; + readonly Dispatcher? dispatcher; + + public WpfDispatcherTimerProvider() + { + this.priority = null; + this.dispatcher = null; + } + + public WpfDispatcherTimerProvider(DispatcherPriority priority) + { + this.priority = priority; + this.dispatcher = null; + } + + public WpfDispatcherTimerProvider(DispatcherPriority priority, Dispatcher dispatcher) + { + this.priority = priority; + this.dispatcher = dispatcher; + } + + public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + return new WpfDispatcherTimerProviderTimer(priority, dispatcher, callback, state, dueTime, period); + } +} + +internal sealed class WpfDispatcherTimerProviderTimer : ITimer +{ + DispatcherTimer? timer; + TimerCallback callback; + object? state; + EventHandler timerTick; + TimeSpan? period; + + public WpfDispatcherTimerProviderTimer(DispatcherPriority? priority, Dispatcher? dispatcher, TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) + { + this.timerTick = Timer_Tick; + this.callback = callback; + this.state = state; + if (priority == null && dispatcher == null) + { + this.timer = new DispatcherTimer(); + } + if (dispatcher == null) // priority is not null + { + this.timer = new DispatcherTimer(priority!.Value); + } + else + { + this.timer = new DispatcherTimer(priority!.Value, dispatcher); + } + + timer.Tick += timerTick; + + if (dueTime != Timeout.InfiniteTimeSpan) + { + Change(dueTime, period); + } + } + + public bool Change(TimeSpan dueTime, TimeSpan period) + { + if (timer != null) + { + timer.Stop(); + + this.period = period; + timer.Interval = dueTime; + + timer.Start(); + return true; + } + return false; + } + + void Timer_Tick(object? sender, EventArgs e) + { + callback(state); + + if (timer != null && period != null) + { + timer.Stop(); + + if (period.Value == Timeout.InfiniteTimeSpan) + { + period = null; + } + else + { + timer.Interval = period.Value; + period = null; + timer.Start(); + } + } + } + + public void Dispose() + { + if (timer != null) + { + timer.Stop(); + timer.Tick -= timerTick; + timer = null; + } + } + + public ValueTask DisposeAsync() + { + Dispose(); + return default; + } +} diff --git a/src/R3.WPF/WpfProviderInitializer.cs b/src/R3.WPF/WpfProviderInitializer.cs new file mode 100644 index 00000000..0399393b --- /dev/null +++ b/src/R3.WPF/WpfProviderInitializer.cs @@ -0,0 +1,24 @@ +using System.Windows.Threading; + +namespace R3.WPF; + +public static class WpfProviderInitializer +{ + public static void SetDefaultProviders() + { + ObservableSystem.DefaultTimeProvider = new WpfDispatcherTimerProvider(); + ObservableSystem.DefaultFrameProvider = new WpfRenderingFrameProvider(); + } + + public static void SetDefaultProviders(DispatcherPriority priority) + { + ObservableSystem.DefaultTimeProvider = new WpfDispatcherTimerProvider(priority); + ObservableSystem.DefaultFrameProvider = new WpfRenderingFrameProvider(); + } + + public static void SetDefaultProviders(DispatcherPriority priority, Dispatcher dispatcher) + { + ObservableSystem.DefaultTimeProvider = new WpfDispatcherTimerProvider(priority, dispatcher); + ObservableSystem.DefaultFrameProvider = new WpfRenderingFrameProvider(); + } +} diff --git a/src/R3.WPF/WpfRenderingFrameProvider.cs b/src/R3.WPF/WpfRenderingFrameProvider.cs new file mode 100644 index 00000000..ae45cccb --- /dev/null +++ b/src/R3.WPF/WpfRenderingFrameProvider.cs @@ -0,0 +1,80 @@ +using System.Diagnostics.CodeAnalysis; + +namespace R3.WPF; + +public sealed class WpfRenderingFrameProvider : FrameProvider +{ + bool disposed; + long frameCount; + FreeListCore list; + readonly object gate = new object(); + + EventHandler messageLoop; + + public WpfRenderingFrameProvider() + { + this.messageLoop = Run; + this.list = new FreeListCore(gate); + System.Windows.Media.CompositionTarget.Rendering += messageLoop; + } + + public override long GetFrameCount() + { + ThrowObjectDisposedIf(disposed, typeof(NewThreadSleepFrameProvider)); + return frameCount; + } + + public override void Register(IFrameRunnerWorkItem callback) + { + ThrowObjectDisposedIf(disposed, typeof(NewThreadSleepFrameProvider)); + list.Add(callback, out _); + } + + public void Dispose() + { + disposed = true; + System.Windows.Media.CompositionTarget.Rendering -= messageLoop; + list.Dispose(); + } + + void Run(object? sender, EventArgs e) + { + frameCount++; + + var span = list.AsSpan(); + for (int i = 0; i < span.Length; i++) + { + ref readonly var item = ref span[i]; + if (item != null) + { + try + { + if (!item.MoveNext(frameCount)) + { + list.Remove(i); + } + } + catch (Exception ex) + { + list.Remove(i); + try + { + ObservableSystem.GetUnhandledExceptionHandler().Invoke(ex); + } + catch { } + } + } + } + } + + static void ThrowObjectDisposedIf([DoesNotReturnIf(true)] bool condition, Type type) + { + if (condition) + { + ThrowObjectDisposedException(type); + } + } + + [DoesNotReturn] + internal static void ThrowObjectDisposedException(Type? type) => throw new ObjectDisposedException(type?.FullName); +} diff --git a/src/R3/Internal/FreeListCore.cs b/src/R3/FreeListCore.cs similarity index 97% rename from src/R3/Internal/FreeListCore.cs rename to src/R3/FreeListCore.cs index fec82027..021e7089 100644 --- a/src/R3/Internal/FreeListCore.cs +++ b/src/R3/FreeListCore.cs @@ -1,10 +1,10 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace R3.Internal; +namespace R3; [StructLayout(LayoutKind.Auto)] -internal struct FreeListCore +public struct FreeListCore where T : class { readonly object gate; @@ -44,7 +44,7 @@ public void Add(T item, out int removeKey) { // full, 1, 4, 6,...resize(x1.5) var len = values.Length; - var newValues = len == 1 ? new T[4] : new T[len + (len / 2)]; + var newValues = len == 1 ? new T[4] : new T[len + len / 2]; Array.Copy(values, newValues, len); Volatile.Write(ref values, newValues); index = len; @@ -125,42 +125,42 @@ public void Dispose() values = null; lastIndex = -2; // -2 is disposed. } - } - + } + #if NET6_0_OR_GREATER - + static int FindNullIndex(T?[] target) { var span = MemoryMarshal.CreateReadOnlySpan( ref Unsafe.As(ref MemoryMarshal.GetArrayDataReference(target)), target.Length); return span.IndexOf(IntPtr.Zero); - } - -#else - + } + +#else + static unsafe int FindNullIndex(T?[] target) - { - ref var head = ref Unsafe.As(ref MemoryMarshal.GetReference(target.AsSpan())); - fixed (void* p = &head) - { - var span = new ReadOnlySpan(p, target.Length); - -#if NETSTANDARD2_1 - return span.IndexOf(IntPtr.Zero); -#else - for (int i = 0; i < span.Length; i++) - { - if (span[i] == IntPtr.Zero) return i; - } - return -1; -#endif - } - } - + { + ref var head = ref Unsafe.As(ref MemoryMarshal.GetReference(target.AsSpan())); + fixed (void* p = &head) + { + var span = new ReadOnlySpan(p, target.Length); + +#if NETSTANDARD2_1 + return span.IndexOf(IntPtr.Zero); +#else + for (int i = 0; i < span.Length; i++) + { + if (span[i] == IntPtr.Zero) return i; + } + return -1; +#endif + } + } + #endif - + #if NET8_0_OR_GREATER - + static int FindLastNonNullIndex(T?[] target, int lastIndex) { var span = MemoryMarshal.CreateReadOnlySpan( @@ -170,20 +170,20 @@ static int FindLastNonNullIndex(T?[] target, int lastIndex) } #else - + static unsafe int FindLastNonNullIndex(T?[] target, int lastIndex) { - ref var head = ref Unsafe.As(ref MemoryMarshal.GetReference(target.AsSpan())); - fixed (void* p = &head) - { - var span = new ReadOnlySpan(p, lastIndex); // without lastIndexed value. - + ref var head = ref Unsafe.As(ref MemoryMarshal.GetReference(target.AsSpan())); + fixed (void* p = &head) + { + var span = new ReadOnlySpan(p, lastIndex); // without lastIndexed value. + for (var i = span.Length - 1; i >= 0; i--) { if (span[i] != IntPtr.Zero) return i; - } - - return -1; + } + + return -1; } } diff --git a/src/R3/NewThreadSleepFrameProvider.cs b/src/R3/NewThreadSleepFrameProvider.cs index ed2cb730..78533074 100644 --- a/src/R3/NewThreadSleepFrameProvider.cs +++ b/src/R3/NewThreadSleepFrameProvider.cs @@ -43,6 +43,8 @@ void Run() { while (!disposed) { + frameCount++; + var span = list.AsSpan(); for (int i = 0; i < span.Length; i++) { @@ -69,7 +71,6 @@ void Run() } Thread.Sleep(sleepMilliseconds); - frameCount++; } list.Dispose(); } diff --git a/src/R3/TimerFrameProvider.cs b/src/R3/TimerFrameProvider.cs index 833c70f5..0eb8fc68 100644 --- a/src/R3/TimerFrameProvider.cs +++ b/src/R3/TimerFrameProvider.cs @@ -64,6 +64,8 @@ static void Run(object? state) lock (self.gate) { + self.frameCount++; + var span = self.list.AsSpan(); for (int i = 0; i < span.Length; i++) { @@ -88,8 +90,6 @@ static void Run(object? state) } } } - - self.frameCount++; } } }