Skip to content

Commit 1736b26

Browse files
committed
perf: optimize ImageDifferenceControl with RenderTargetBitmap caching
- Add intelligent caching to reduce memory allocations during alpha slider dragging - Implement proper disposal in OnUnloaded to prevent memory leaks - Add bounds checking with 8K max dimensions to prevent integer overflow - Invalidate cache when images change via OnPropertyChanged - Fix missing Avalonia using directive in DoubleConverters.cs These optimizations significantly improve performance for large image diffs while maintaining proper resource management and safety.
1 parent 78fc646 commit 1736b26

File tree

2 files changed

+68
-10
lines changed

2 files changed

+68
-10
lines changed

src/Converters/DoubleConverters.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Avalonia.Data.Converters;
1+
using Avalonia;
2+
using Avalonia.Data.Converters;
23

34
namespace SourceGit.Converters
45
{

src/Views/ImageContainer.cs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,12 @@ static ImageDifferenceControl()
397397
AffectsRender<ImageDifferenceControl>(AlphaProperty);
398398
}
399399

400+
// Cached render target for performance optimization
401+
private RenderTargetBitmap _cachedRenderTarget;
402+
private Size _cachedSize;
403+
private Bitmap _cachedOldImage;
404+
private Bitmap _cachedNewImage;
405+
400406
public override void Render(DrawingContext context)
401407
{
402408
base.Render(context);
@@ -409,19 +415,45 @@ public override void Render(DrawingContext context)
409415

410416
if (drawLeft && drawRight)
411417
{
412-
using (var rt = new RenderTargetBitmap(new PixelSize((int)Bounds.Width, (int)Bounds.Height), right.Dpi))
418+
// Bounds checking for safe integer casting
419+
var width = Math.Min(Math.Max(1, Bounds.Width), 8192); // Max 8K width
420+
var height = Math.Min(Math.Max(1, Bounds.Height), 8192); // Max 8K height
421+
var pixelWidth = (int)Math.Ceiling(width);
422+
var pixelHeight = (int)Math.Ceiling(height);
423+
424+
// Check if we need to recreate the cached render target
425+
var currentSize = new Size(pixelWidth, pixelHeight);
426+
var needsRecreate = _cachedRenderTarget == null ||
427+
_cachedSize != currentSize ||
428+
_cachedOldImage != left ||
429+
_cachedNewImage != right;
430+
431+
if (needsRecreate)
413432
{
414-
using (var dc = rt.CreateDrawingContext())
415-
{
416-
using (dc.PushRenderOptions(RO_SRC))
417-
RenderSingleSide(dc, left, rt.Size.Width, rt.Size.Height, Math.Min(1.0, 2.0 - 2.0 * alpha));
433+
// Dispose old cached render target
434+
_cachedRenderTarget?.Dispose();
435+
436+
// Create new render target with bounds checking
437+
_cachedRenderTarget = new RenderTargetBitmap(new PixelSize(pixelWidth, pixelHeight), right.Dpi);
438+
_cachedSize = currentSize;
439+
_cachedOldImage = left;
440+
_cachedNewImage = right;
441+
}
418442

419-
using (dc.PushRenderOptions(RO_DST))
420-
RenderSingleSide(dc, right, rt.Size.Width, rt.Size.Height, Math.Min(1.0, 2.0 * alpha));
421-
}
443+
// Render to the cached target
444+
using (var dc = _cachedRenderTarget.CreateDrawingContext())
445+
{
446+
// Clear the render target first
447+
dc.DrawRectangle(Brushes.Transparent, null, new Rect(0, 0, pixelWidth, pixelHeight));
422448

423-
context.DrawImage(rt, new Rect(0, 0, Bounds.Width, Bounds.Height));
449+
using (dc.PushRenderOptions(RO_SRC))
450+
RenderSingleSide(dc, left, pixelWidth, pixelHeight, Math.Min(1.0, 2.0 - 2.0 * alpha));
451+
452+
using (dc.PushRenderOptions(RO_DST))
453+
RenderSingleSide(dc, right, pixelWidth, pixelHeight, Math.Min(1.0, 2.0 * alpha));
424454
}
455+
456+
context.DrawImage(_cachedRenderTarget, new Rect(0, 0, Bounds.Width, Bounds.Height));
425457
}
426458
else if (drawLeft)
427459
{
@@ -475,5 +507,30 @@ private void RenderSingleSide(DrawingContext context, Bitmap img, double w, doub
475507

476508
private static readonly RenderOptions RO_SRC = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Source, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
477509
private static readonly RenderOptions RO_DST = new RenderOptions() { BitmapBlendingMode = BitmapBlendingMode.Difference, BitmapInterpolationMode = BitmapInterpolationMode.HighQuality };
510+
511+
protected override void OnUnloaded(Avalonia.Interactivity.RoutedEventArgs e)
512+
{
513+
base.OnUnloaded(e);
514+
515+
// Clean up cached render target to prevent memory leaks
516+
_cachedRenderTarget?.Dispose();
517+
_cachedRenderTarget = null;
518+
_cachedOldImage = null;
519+
_cachedNewImage = null;
520+
}
521+
522+
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
523+
{
524+
base.OnPropertyChanged(change);
525+
526+
// Invalidate cache when images change
527+
if (change.Property == OldImageProperty || change.Property == NewImageProperty)
528+
{
529+
_cachedRenderTarget?.Dispose();
530+
_cachedRenderTarget = null;
531+
_cachedOldImage = null;
532+
_cachedNewImage = null;
533+
}
534+
}
478535
}
479536
}

0 commit comments

Comments
 (0)