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);
+}