mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 00:46:12 +02:00
Add SkiaPlugin support and benchmark plugins for SkiaSharp and GDI+
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
||||
<PackageReference Include="Modio" Version="1.0.0" />
|
||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
<PackageReference Include="WPF-UI" Version="4.2.0" />
|
||||
<PackageReference Include="WPF-UI.Markdown" Version="4.0.2" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
<system:String x:Key="weather">Wetter</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="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="newLayout">Neues Layout</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="nextMeetingCountdown">Next Meeting Countdown</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="newLayout">New Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Delete Layout</system:String>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
</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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user