Add SkiaPlugin support and benchmark plugins for SkiaSharp and GDI+

This commit is contained in:
Stone_Red
2026-07-01 14:35:47 +02:00
parent 2e219e66c9
commit bd29bcdb37
10 changed files with 407 additions and 19 deletions
@@ -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;
}
}
@@ -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);
}
}
+1
View File
@@ -51,6 +51,7 @@
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
<PackageReference Include="Modio" Version="1.0.0" /> <PackageReference Include="Modio" Version="1.0.0" />
<PackageReference Include="NAudio" Version="2.2.1" /> <PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="SkiaSharp" Version="4.148.0" />
<PackageReference Include="System.Management" Version="8.0.0" /> <PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="WPF-UI" Version="4.2.0" /> <PackageReference Include="WPF-UI" Version="4.2.0" />
<PackageReference Include="WPF-UI.Markdown" Version="4.0.2" /> <PackageReference Include="WPF-UI.Markdown" Version="4.0.2" />
+2
View File
@@ -46,6 +46,8 @@ public sealed class Manager
{new((string)App.LanguageDictionary["weather"], 5) { Author = "Stone_Red" }, typeof(WeatherPlugin)}, {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["nextMeetingCountdown"], 6) { Author = "Stone_Red" }, typeof(NextMeetingCountdownPlugin)},
{new((string)App.LanguageDictionary["agenda"], 7) { Author = "Stone_Red" }, typeof(AgendaPlugin)}, {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 // Window management
+88 -19
View File
@@ -6,6 +6,8 @@ using DesktopMagic.Helpers;
using DesktopMagic.Plugins; using DesktopMagic.Plugins;
using DesktopMagic.Settings; using DesktopMagic.Settings;
using SkiaSharp;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
@@ -834,16 +836,36 @@ public partial class PluginWindow : Window, IPluginWindow
{ {
if (IsRunning && pluginClassInstance is not null) if (IsRunning && pluginClassInstance is not null)
{ {
Bitmap? result; if (pluginClassInstance is SkiaPlugin or SkiaAsyncPlugin)
if (pluginClassInstance is AsyncPlugin asyncPlugin)
{ {
CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None; await RenderSkiaFrame();
result = await asyncPlugin.MainAsync(token);
} }
else 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) if (pluginClassInstance.UpdateInterval > 0)
@@ -854,19 +876,6 @@ public partial class PluginWindow : Window, IPluginWindow
{ {
updateTimer!.Stop(); 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) 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) private void Border_SizeChanged(object sender, SizeChangedEventArgs e)
{ {
rectangleGeometry.Rect = new Rect(-settings.Theme.Margin, -settings.Theme.Margin, e.NewSize.Width, e.NewSize.Height); rectangleGeometry.Rect = new Rect(-settings.Theme.Margin, -settings.Theme.Margin, e.NewSize.Width, e.NewSize.Height);
@@ -40,6 +40,8 @@
<system:String x:Key="weather">Wetter</system:String> <system:String x:Key="weather">Wetter</system:String>
<system:String x:Key="nextMeetingCountdown">Nächstes Meeting Countdown</system:String> <system:String x:Key="nextMeetingCountdown">Nächstes Meeting Countdown</system:String>
<system:String x:Key="agenda">Agenda</system:String> <system:String x:Key="agenda">Agenda</system:String>
<system:String x:Key="benchmark">Performance Benchmark</system:String>
<system:String x:Key="skiaBenchmark">SkiaSharp Benchmark</system:String>
<system:String x:Key="amplifier">Signalverstärkung:</system:String> <system:String x:Key="amplifier">Signalverstärkung:</system:String>
<system:String x:Key="newLayout">Neues Layout</system:String> <system:String x:Key="newLayout">Neues Layout</system:String>
<system:String x:Key="deleteLayout">Layout Löschen</system:String> <system:String x:Key="deleteLayout">Layout Löschen</system:String>
@@ -40,6 +40,8 @@
<system:String x:Key="weather">Weather</system:String> <system:String x:Key="weather">Weather</system:String>
<system:String x:Key="nextMeetingCountdown">Next Meeting Countdown</system:String> <system:String x:Key="nextMeetingCountdown">Next Meeting Countdown</system:String>
<system:String x:Key="agenda">Agenda</system:String> <system:String x:Key="agenda">Agenda</system:String>
<system:String x:Key="benchmark">Performance Benchmark</system:String>
<system:String x:Key="skiaBenchmark">SkiaSharp Benchmark</system:String>
<system:String x:Key="amplifier">Signal Amplification:</system:String> <system:String x:Key="amplifier">Signal Amplification:</system:String>
<system:String x:Key="newLayout">New Layout</system:String> <system:String x:Key="newLayout">New Layout</system:String>
<system:String x:Key="deleteLayout">Delete Layout</system:String> <system:String x:Key="deleteLayout">Delete Layout</system:String>
@@ -49,6 +49,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="4.148.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" /> <PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,31 @@
using SkiaSharp;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.Api;
/// <summary>
/// Provides an abstract base class for creating asynchronous plugins using SkiaSharp rendering.
/// Plugins override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead of <see cref="Plugin.Main()"/>.
/// All existing <see cref="Plugin"/>, <see cref="AsyncPlugin"/>, and <see cref="SkiaPlugin"/> code is unaffected.
/// </summary>
public abstract class SkiaAsyncPlugin : AsyncPlugin
{
/// <summary>
/// This method is sealed and returns null for async Skia plugins.
/// Override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead.
/// </summary>
public sealed override Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
return Task.FromResult<Bitmap?>(null);
}
/// <summary>
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin asynchronously.
/// </summary>
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public abstract Task MainAsync(SKCanvas canvas, CancellationToken cancellationToken);
}
+29
View File
@@ -0,0 +1,29 @@
using SkiaSharp;
using System.Drawing;
namespace DesktopMagic.Api;
/// <summary>
/// Provides an abstract base class for creating plugins using SkiaSharp rendering.
/// Plugins override <see cref="Main(SKCanvas)"/> instead of <see cref="Plugin.Main()"/>.
/// All existing <see cref="Plugin"/> and <see cref="AsyncPlugin"/> code is unaffected.
/// </summary>
public abstract class SkiaPlugin : Plugin
{
/// <summary>
/// This method is sealed and returns null for Skia plugins.
/// Override <see cref="Main(SKCanvas)"/> instead.
/// </summary>
/// <returns>Always null.</returns>
public sealed override Bitmap? Main()
{
return null;
}
/// <summary>
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin.
/// </summary>
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
public abstract void Main(SKCanvas canvas);
}