From bd29bcdb378c89663e5a638cbab16f7c5fdc2679 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:35:47 +0200 Subject: [PATCH] Add SkiaPlugin support and benchmark plugins for SkiaSharp and GDI+ --- .../BuiltInPlugins/BenchmarkPlugin.cs | 128 ++++++++++++++++++ .../BuiltInPlugins/SkiaBenchmarkPlugin.cs | 123 +++++++++++++++++ src/DesktopMagic/DesktopMagic.csproj | 1 + src/DesktopMagic/Manager.cs | 2 + src/DesktopMagic/Plugins/PluginWindow.xaml.cs | 107 ++++++++++++--- .../Resources/Strings/StringResources.de.xaml | 2 + .../Resources/Strings/StringResources.en.xaml | 2 + .../DesktopMagic.Api.csproj | 1 + src/DesktopMagicPluginAPI/SkiaAsyncPlugin.cs | 31 +++++ src/DesktopMagicPluginAPI/SkiaPlugin.cs | 29 ++++ 10 files changed, 407 insertions(+), 19 deletions(-) create mode 100644 src/DesktopMagic/BuiltInPlugins/BenchmarkPlugin.cs create mode 100644 src/DesktopMagic/BuiltInPlugins/SkiaBenchmarkPlugin.cs create mode 100644 src/DesktopMagicPluginAPI/SkiaAsyncPlugin.cs create mode 100644 src/DesktopMagicPluginAPI/SkiaPlugin.cs diff --git a/src/DesktopMagic/BuiltInPlugins/BenchmarkPlugin.cs b/src/DesktopMagic/BuiltInPlugins/BenchmarkPlugin.cs new file mode 100644 index 0000000..fee899b --- /dev/null +++ b/src/DesktopMagic/BuiltInPlugins/BenchmarkPlugin.cs @@ -0,0 +1,128 @@ +using DesktopMagic.Api; +using DesktopMagic.Api.Settings; + +using System; +using System.Drawing; +using System.Drawing.Drawing2D; + +namespace DesktopMagic.BuiltInPlugins; + +internal class BenchmarkPlugin : Plugin +{ + private const int BitmapWidth = 400; + private const int BitmapHeight = 300; + + private readonly Random rng = new(42); + private readonly object rngLock = new(); + + private readonly System.Diagnostics.Stopwatch frameTimer = System.Diagnostics.Stopwatch.StartNew(); + private double fps; + private long lastFrameMs; + + [Setting("shape-count", "Shapes")] + private readonly IntegerUpDown shapeCount = new(10, 10000, 1000); + + [Setting("shape-type", "Shape Type")] + private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed"); + + [Setting("fps-counter", "Fps")] + private readonly Label fpsCounter = new(""); + + public override int UpdateInterval => 16; + + public override Bitmap? Main() + { + long now = frameTimer.ElapsedMilliseconds; + if (lastFrameMs > 0) + { + double elapsed = now - lastFrameMs; + fps = (fps * 0.9) + (1000.0 / Math.Max(elapsed, 1) * 0.1); + } + lastFrameMs = now; + + int w = BitmapWidth; + int h = BitmapHeight; + + Bitmap bmp = new Bitmap(w, h); + using Graphics g = Graphics.FromImage(bmp); + g.Clear(Color.Transparent); + g.SmoothingMode = SmoothingMode.HighSpeed; + g.CompositingQuality = CompositingQuality.HighSpeed; + g.InterpolationMode = InterpolationMode.Low; + + Color color = Application.Theme.PrimaryColor; + int count = shapeCount.Value; + string type = shapeType.Value; + + for (int i = 0; i < count; i++) + { + int x, y, rw, rh; + lock (rngLock) + { + x = rng.Next(0, w); + y = rng.Next(0, h); + rw = rng.Next(5, 60); + rh = rng.Next(5, 60); + } + + using Brush brush = new SolidBrush(Color.FromArgb(rng.Next(100, 255), color)); + + switch (type) + { + case "Rectangles": + g.FillRectangle(brush, x, y, rw, rh); + break; + case "Ellipses": + g.FillEllipse(brush, x, y, rw, rh); + break; + case "Lines": + { + int thickness; + lock (rngLock) { thickness = rng.Next(1, 5); } + g.DrawLine(new Pen(brush, thickness), x, y, x + rw, y + rh); + } + break; + case "Text": + { + int fontSize; + lock (rngLock) { fontSize = rng.Next(8, 24); } + using Font font = new("Arial", fontSize); + g.DrawString("Mg", font, brush, x, y); + } + break; + default: + { + int shape, thickness, fontSize; + lock (rngLock) + { + shape = rng.Next(0, 4); + thickness = rng.Next(1, 5); + fontSize = rng.Next(8, 24); + } + if (shape == 0) + { + g.FillRectangle(brush, x, y, rw, rh); + } + else if (shape == 1) + { + g.FillEllipse(brush, x, y, rw, rh); + } + else if (shape == 2) + { + g.DrawLine(new Pen(brush, thickness), x, y, x + rw, y + rh); + } + else + { + using Font font = new("Arial", fontSize); + g.DrawString("Mg", font, brush, x, y); + } + } + break; + } + } + + fpsCounter.Value = fps.ToString(); + + return bmp; + } +} diff --git a/src/DesktopMagic/BuiltInPlugins/SkiaBenchmarkPlugin.cs b/src/DesktopMagic/BuiltInPlugins/SkiaBenchmarkPlugin.cs new file mode 100644 index 0000000..814e2c5 --- /dev/null +++ b/src/DesktopMagic/BuiltInPlugins/SkiaBenchmarkPlugin.cs @@ -0,0 +1,123 @@ +using DesktopMagic.Api; +using DesktopMagic.Api.Settings; + +using SkiaSharp; + +using System; + +namespace DesktopMagic.BuiltInPlugins; + +internal class SkiaBenchmarkPlugin : SkiaPlugin +{ + private readonly Random rng = new(42); + private readonly object rngLock = new(); + + private readonly System.Diagnostics.Stopwatch frameTimer = System.Diagnostics.Stopwatch.StartNew(); + private double fps; + private long lastFrameMs; + + [Setting("skia-shape-count", "Shapes")] + private readonly IntegerUpDown shapeCount = new(10, 10000, 1000); + + [Setting("skia-shape-type", "Shape Type")] + private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed"); + + public override int UpdateInterval => 16; + + public override void Main(SKCanvas canvas) + { + long now = frameTimer.ElapsedMilliseconds; + if (lastFrameMs > 0) + { + double elapsed = now - lastFrameMs; + fps = fps * 0.9 + (1000.0 / Math.Max(elapsed, 1)) * 0.1; + } + lastFrameMs = now; + + int w = (int)canvas.DeviceClipBounds.Width; + int h = (int)canvas.DeviceClipBounds.Height; + + canvas.Clear(SKColors.Transparent); + + SKColor color = new( + Application.Theme.PrimaryColor.R, + Application.Theme.PrimaryColor.G, + Application.Theme.PrimaryColor.B, + Application.Theme.PrimaryColor.A); + + int count = shapeCount.Value; + string type = shapeType.Value; + + for (int i = 0; i < count; i++) + { + int x, y, rw, rh; + lock (rngLock) + { + x = rng.Next(0, w); + y = rng.Next(0, h); + rw = rng.Next(5, 60); + rh = rng.Next(5, 60); + } + + byte alpha = (byte)rng.Next(100, 255); + using var paint = new SKPaint + { + Color = color.WithAlpha(alpha), + IsAntialias = false, + Style = SKPaintStyle.Fill + }; + + switch (type) + { + case "Rectangles": + canvas.DrawRect(x, y, rw, rh, paint); + break; + case "Ellipses": + canvas.DrawOval(x + rw / 2f, y + rh / 2f, rw / 2f, rh / 2f, paint); + break; + case "Lines": + paint.Style = SKPaintStyle.Stroke; + paint.StrokeWidth = 2; + canvas.DrawLine(x, y, x + rw, y + rh, paint); + break; + case "Text": + using (var textFont = new SKFont(SKTypeface.Default, 16)) + { + canvas.DrawText("Mg", x, y + 16, SKTextAlign.Left, textFont, paint); + } + break; + default: + int shape; + lock (rngLock) { shape = rng.Next(0, 4); } + if (shape == 0) + { + canvas.DrawRect(x, y, rw, rh, paint); + } + else if (shape == 1) + { + canvas.DrawOval(x + rw / 2f, y + rh / 2f, rw / 2f, rh / 2f, paint); + } + else if (shape == 2) + { + paint.Style = SKPaintStyle.Stroke; + paint.StrokeWidth = 2; + canvas.DrawLine(x, y, x + rw, y + rh, paint); + } + else + { + using var textFont2 = new SKFont(SKTypeface.Default, 16); + canvas.DrawText("Mg", x, y + 16, SKTextAlign.Left, textFont2, paint); + } + break; + } + } + + using var fpsFont = new SKFont(SKTypeface.Default, 14); + using var fpsPaint = new SKPaint + { + Color = color, + IsAntialias = false + }; + canvas.DrawText($"FPS: {fps:F1} | Shapes: {count} | Type: {type}", 8, 18, SKTextAlign.Left, fpsFont, fpsPaint); + } +} diff --git a/src/DesktopMagic/DesktopMagic.csproj b/src/DesktopMagic/DesktopMagic.csproj index 9141158..3ed9fc5 100644 --- a/src/DesktopMagic/DesktopMagic.csproj +++ b/src/DesktopMagic/DesktopMagic.csproj @@ -51,6 +51,7 @@ + diff --git a/src/DesktopMagic/Manager.cs b/src/DesktopMagic/Manager.cs index c7bac72..f67caf0 100644 --- a/src/DesktopMagic/Manager.cs +++ b/src/DesktopMagic/Manager.cs @@ -46,6 +46,8 @@ public sealed class Manager {new((string)App.LanguageDictionary["weather"], 5) { Author = "Stone_Red" }, typeof(WeatherPlugin)}, {new((string)App.LanguageDictionary["nextMeetingCountdown"], 6) { Author = "Stone_Red" }, typeof(NextMeetingCountdownPlugin)}, {new((string)App.LanguageDictionary["agenda"], 7) { Author = "Stone_Red" }, typeof(AgendaPlugin)}, + {new((string)App.LanguageDictionary["benchmark"], 8) { Author = "Stone_Red" }, typeof(BenchmarkPlugin)}, + {new((string)App.LanguageDictionary["skiaBenchmark"], 9) { Author = "Stone_Red" }, typeof(SkiaBenchmarkPlugin)}, }; // Window management diff --git a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs index abc9aaf..ab73d77 100644 --- a/src/DesktopMagic/Plugins/PluginWindow.xaml.cs +++ b/src/DesktopMagic/Plugins/PluginWindow.xaml.cs @@ -6,6 +6,8 @@ using DesktopMagic.Helpers; using DesktopMagic.Plugins; using DesktopMagic.Settings; +using SkiaSharp; + using System; using System.Collections.Generic; using System.Drawing; @@ -834,16 +836,36 @@ public partial class PluginWindow : Window, IPluginWindow { if (IsRunning && pluginClassInstance is not null) { - Bitmap? result; - - if (pluginClassInstance is AsyncPlugin asyncPlugin) + if (pluginClassInstance is SkiaPlugin or SkiaAsyncPlugin) { - CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None; - result = await asyncPlugin.MainAsync(token); + await RenderSkiaFrame(); } else { - result = pluginClassInstance.Main(); + Bitmap? result; + + if (pluginClassInstance is AsyncPlugin asyncPlugin) + { + CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None; + result = await asyncPlugin.MainAsync(token); + } + else + { + result = pluginClassInstance.Main(); + } + + if (result is not null) + { + BitmapScalingMode renderOptions = pluginClassInstance.RenderQuality switch + { + RenderQuality.High => BitmapScalingMode.HighQuality, + RenderQuality.Low => BitmapScalingMode.LowQuality, + RenderQuality.Performance => BitmapScalingMode.NearestNeighbor, + _ => BitmapScalingMode.Unspecified + }; + + UpdateImageFromBitmap(result, renderOptions); + } } if (pluginClassInstance.UpdateInterval > 0) @@ -854,19 +876,6 @@ public partial class PluginWindow : Window, IPluginWindow { updateTimer!.Stop(); } - - if (result is not null) - { - BitmapScalingMode renderOptions = pluginClassInstance.RenderQuality switch - { - RenderQuality.High => BitmapScalingMode.HighQuality, - RenderQuality.Low => BitmapScalingMode.LowQuality, - RenderQuality.Performance => BitmapScalingMode.NearestNeighbor, - _ => BitmapScalingMode.Unspecified - }; - - UpdateImageFromBitmap(result, renderOptions); - } } } catch (Exception ex) @@ -893,6 +902,66 @@ public partial class PluginWindow : Window, IPluginWindow } } + private async Task RenderSkiaFrame() + { + int width = 0; + int height = 0; + + await Dispatcher.InvokeAsync(() => + { + width = Math.Max(1, (int)ActualWidth); + height = Math.Max(1, (int)ActualHeight); + }); + + using SKSurface surface = SKSurface.Create(new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul)); + surface.Canvas.Clear(SKColors.Transparent); + + if (pluginClassInstance is SkiaAsyncPlugin skiaAsyncPlugin) + { + CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None; + await skiaAsyncPlugin.MainAsync(surface.Canvas, token); + } + else if (pluginClassInstance is SkiaPlugin skiaPlugin) + { + skiaPlugin.Main(surface.Canvas); + } + + SKPixmap? pixmap = surface.PeekPixels(); + + if (pixmap is null) + { + return; + } + + await Dispatcher.InvokeAsync(() => + { + if (writeableBitmap is null || writeableBitmap.PixelWidth != width || writeableBitmap.PixelHeight != height) + { + writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null); + image.Source = writeableBitmap; + } + + writeableBitmap.Lock(); + try + { + unsafe + { + Buffer.MemoryCopy( + pixmap.GetPixels().ToPointer(), + writeableBitmap.BackBuffer.ToPointer(), + writeableBitmap.BackBufferStride * height, + pixmap.RowBytes * pixmap.Height); + } + + writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); + } + finally + { + writeableBitmap.Unlock(); + } + }); + } + private void Border_SizeChanged(object sender, SizeChangedEventArgs e) { rectangleGeometry.Rect = new Rect(-settings.Theme.Margin, -settings.Theme.Margin, e.NewSize.Width, e.NewSize.Height); diff --git a/src/DesktopMagic/Resources/Strings/StringResources.de.xaml b/src/DesktopMagic/Resources/Strings/StringResources.de.xaml index 96d9736..be372a9 100644 --- a/src/DesktopMagic/Resources/Strings/StringResources.de.xaml +++ b/src/DesktopMagic/Resources/Strings/StringResources.de.xaml @@ -40,6 +40,8 @@ Wetter Nächstes Meeting Countdown Agenda + Performance Benchmark + SkiaSharp Benchmark Signalverstärkung: Neues Layout Layout Löschen diff --git a/src/DesktopMagic/Resources/Strings/StringResources.en.xaml b/src/DesktopMagic/Resources/Strings/StringResources.en.xaml index a048d3d..6b9b75b 100644 --- a/src/DesktopMagic/Resources/Strings/StringResources.en.xaml +++ b/src/DesktopMagic/Resources/Strings/StringResources.en.xaml @@ -40,6 +40,8 @@ Weather Next Meeting Countdown Agenda + Performance Benchmark + SkiaSharp Benchmark Signal Amplification: New Layout Delete Layout diff --git a/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj b/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj index 1cd85a0..fda12f5 100644 --- a/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj +++ b/src/DesktopMagicPluginAPI/DesktopMagic.Api.csproj @@ -49,6 +49,7 @@ + diff --git a/src/DesktopMagicPluginAPI/SkiaAsyncPlugin.cs b/src/DesktopMagicPluginAPI/SkiaAsyncPlugin.cs new file mode 100644 index 0000000..b46b0a4 --- /dev/null +++ b/src/DesktopMagicPluginAPI/SkiaAsyncPlugin.cs @@ -0,0 +1,31 @@ +using SkiaSharp; + +using System.Drawing; +using System.Threading; +using System.Threading.Tasks; + +namespace DesktopMagic.Api; + +/// +/// Provides an abstract base class for creating asynchronous plugins using SkiaSharp rendering. +/// Plugins override instead of . +/// All existing , , and code is unaffected. +/// +public abstract class SkiaAsyncPlugin : AsyncPlugin +{ + /// + /// This method is sealed and returns null for async Skia plugins. + /// Override instead. + /// + public sealed override Task MainAsync(CancellationToken cancellationToken) + { + return Task.FromResult(null); + } + + /// + /// Called every milliseconds to render the plugin asynchronously. + /// + /// The SkiaSharp canvas to draw on. + /// Token signaled when the host requests cancellation. + public abstract Task MainAsync(SKCanvas canvas, CancellationToken cancellationToken); +} diff --git a/src/DesktopMagicPluginAPI/SkiaPlugin.cs b/src/DesktopMagicPluginAPI/SkiaPlugin.cs new file mode 100644 index 0000000..a53f79c --- /dev/null +++ b/src/DesktopMagicPluginAPI/SkiaPlugin.cs @@ -0,0 +1,29 @@ +using SkiaSharp; + +using System.Drawing; + +namespace DesktopMagic.Api; + +/// +/// Provides an abstract base class for creating plugins using SkiaSharp rendering. +/// Plugins override instead of . +/// All existing and code is unaffected. +/// +public abstract class SkiaPlugin : Plugin +{ + /// + /// This method is sealed and returns null for Skia plugins. + /// Override instead. + /// + /// Always null. + public sealed override Bitmap? Main() + { + return null; + } + + /// + /// Called every milliseconds to render the plugin. + /// + /// The SkiaSharp canvas to draw on. + public abstract void Main(SKCanvas canvas); +}