Skip to content

Commit dd2288c

Browse files
committed
StaticImageRenderer: mipmap pyramid for smooth zoom; add Anisotropic scaling option (#177)
Replace the full-res-every-frame draw path with a pre-generated mipmap pyramid (0.5x ... 1/32x source) built on a background Task at load time. Draw() selects the smallest level whose pixel dimensions still exceed the display size, so zoom animations sample a level close to screen size instead of the full source - eliminating frame drops on large images at high interpolation quality. Remove the old offscreen buffer entirely; the mip chain serves both the animating and static states. Add Anisotropic as a new interpolation option between Bilinear and High Quality Cubic (Settings -> Image Scaling Quality). Mip generation snapshots the user's configured scaling quality so NearestNeighbor pixel art is built and rendered correctly at all zoom levels. Quality changes cancel and rebuild the mip chain immediately. IRenderer changes: - Remove the three offscreen methods - Rename OnDeviceRecreated -> HandleScalingMethodChange (the old name implied full device-loss recovery, which is not done; cached CanvasBitmaps in the photo cache remain tied to the old device) - MultiPageRenderer.HandleScalingMethodChange triggers an immediate redraw so the new quality takes effect without waiting for the next animation frame
1 parent 4b89b0d commit dd2288c

11 files changed

Lines changed: 183 additions & 198 deletions

File tree

Src/FlyPhotos/Core/Model/Enums.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public enum Setting
6868
CaptionButtonsAutoHideToggle,
6969
CtrlDragToMoveWindowToggle,
7070
AutoFadeToggle,
71-
AutoHideMouseToggle
71+
AutoHideMouseToggle,
72+
ImageScalingQualityChange
7273
}
7374

7475
public enum ScrollDirection
@@ -100,5 +101,6 @@ public enum ImageInterpolation
100101
{
101102
NearestNeighbor,
102103
Linear,
104+
Anisotropic,
103105
HighQualityCubic
104106
}

Src/FlyPhotos/Display/Controllers/CanvasController.cs

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,11 @@ internal class CanvasController : ICanvasController
7575
private bool _realImageDisplayedForCurrentPhoto;
7676
private bool _isMultiPageActive;
7777

78-
// W2D-owned: set inside the ZoomOutOnExit action, read in the AnimationCompleted handler.
78+
// W2D-owned: set inside the ZoomOutOnExit action, read in WaitForPanZoomAnimationAsync.
7979
private bool _isGoingToExit;
8080

8181
// W2D-owned: true while ZoomAtPointPrecision ticks are arriving (right-click continuous zoom).
82-
// Cleared 700 ms after the last tick — just after the 650 ms offscreen timer fires — so Draw
83-
// skips the offscreen and uses source-bitmap quality during the zoom burst.
82+
// Treated as animating so Draw uses mip-based quality. Cleared 700 ms after the last tick.
8483
private bool _continuousZoomActive;
8584
private CancellationTokenSource _continuousZoomCts;
8685

@@ -109,7 +108,6 @@ public CanvasController(CanvasAnimatedControl d2dCanvas, IThumbnailController th
109108
_d2dCanvas.DispatcherQueue.TryEnqueue(() => OnOneToOneStateChanged?.Invoke(isOneToOne));
110109
_canvasViewManager.ZoomChanged += RequestZoomUpdate;
111110
_canvasViewManager.ViewChanged += RequestInvalidate;
112-
_canvasViewManager.AnimationCompleted += CanvasViewManager_AnimationCompleted;
113111
}
114112

115113
public void Dispose()
@@ -207,8 +205,8 @@ private async Task HandleHqAnimatedDisplayItemAsync(int currentOperationId, Phot
207205
{
208206
// For animated images, first display the static first frame immediately for responsiveness.
209207
InstallRenderer(
210-
new StaticImageRenderer(_d2dCanvas, _canvasViewState, animDispItem.Bitmap,
211-
photo.SupportsTransparency, RequestInvalidate, createOffScreen: false),
208+
new StaticImageRenderer(_d2dCanvas, animDispItem.Bitmap,
209+
photo.SupportsTransparency, RequestInvalidate, generateMipChain: false),
212210
_imageSize, animDispItem.Rotation, ctx, forceThumbNailRedraw: true);
213211

214212
// Asynchronously create the appropriate animator (GIF, WebP, APNG, or AVIF).
@@ -247,7 +245,7 @@ private async Task HandleHqAnimatedDisplayItemAsync(int currentOperationId, Phot
247245
private void HandleHqStaticDisplayItem(Photo photo, HqDisplayItem hqDispItem, PhotoInstallContext ctx)
248246
{
249247
InstallRenderer(
250-
new StaticImageRenderer(_d2dCanvas, _canvasViewState, hqDispItem.Bitmap,
248+
new StaticImageRenderer(_d2dCanvas, hqDispItem.Bitmap,
251249
photo.SupportsTransparency, RequestInvalidate),
252250
_imageSize, hqDispItem.Rotation, ctx, forceThumbNailRedraw: true);
253251
}
@@ -269,8 +267,8 @@ private void HandlePreviewDisplayItem(Photo photo, PreviewDisplayItem previewDis
269267
var correctedWidth = _imageSize.Height * previewAspectRatio;
270268

271269
InstallRenderer(
272-
new StaticImageRenderer(_d2dCanvas, _canvasViewState, previewDispItem.Bitmap,
273-
photo.SupportsTransparency, RequestInvalidate, createOffScreen: false),
270+
new StaticImageRenderer(_d2dCanvas, previewDispItem.Bitmap,
271+
photo.SupportsTransparency, RequestInvalidate, generateMipChain: false),
274272
new Size(correctedWidth, _imageSize.Height), previewDispItem.Rotation, ctx, forceThumbNailRedraw: true);
275273
}
276274

@@ -328,12 +326,6 @@ private void InstallRenderer(IRenderer newRenderer, Size imageSize,
328326

329327
if (forceThumbNailRedraw)
330328
_thumbNailController.CreateThumbnailRibbonOffScreen(); // self-marshals to the UI thread
331-
332-
// Skip the offscreen timer when an animation is starting — AnimationCompleted will call
333-
// TryRedrawOffScreen(false) at the correct final scale. Starting the timer here while
334-
// Scale is near-zero (startup zoom) would bake a tiny offscreen mid-animation.
335-
if (!_canvasViewManager.PanZoomAnimationOnGoing)
336-
_currentRenderer.RestartOffScreenDrawTimer();
337329
});
338330
}
339331

@@ -344,7 +336,6 @@ public void FitToScreen(bool animateChange)
344336
EnqueueW2dAction(() =>
345337
{
346338
if (_currentRenderer == null) return;
347-
if (animateChange) _currentRenderer.CancelOffScreenTimer();
348339
_canvasViewManager.ZoomPanToFit(animateChange, imageSize, canvasSize);
349340
});
350341
}
@@ -355,7 +346,7 @@ public void ZoomToHundred()
355346
EnqueueW2dAction(() =>
356347
{
357348
if (_currentRenderer == null) return;
358-
_currentRenderer.CancelOffScreenTimer();
349+
359350
_canvasViewManager.ZoomToHundred(canvasSize);
360351
});
361352
}
@@ -366,7 +357,7 @@ public void ZoomToHundred(Point anchor)
366357
EnqueueW2dAction(() =>
367358
{
368359
if (_currentRenderer == null) return;
369-
_currentRenderer.CancelOffScreenTimer();
360+
370361
_canvasViewManager.ZoomToHundred(canvasSize, anchor);
371362
});
372363
}
@@ -387,7 +378,7 @@ public void ZoomByKeyboard(ZoomDirection zoomDirection)
387378
EnqueueW2dAction(() =>
388379
{
389380
if (_currentRenderer == null) return;
390-
_currentRenderer.CancelOffScreenTimer();
381+
391382
_canvasViewManager.ZoomAtCenter(zoomDirection, canvasSize);
392383
});
393384
}
@@ -398,7 +389,7 @@ public void StepZoom(ZoomDirection zoomDirection, Point? zoomAnchor = null)
398389
EnqueueW2dAction(() =>
399390
{
400391
if (_currentRenderer == null) return;
401-
_currentRenderer.CancelOffScreenTimer();
392+
402393
_canvasViewManager.StepZoom(zoomDirection, canvasSize, zoomAnchor);
403394
});
404395
}
@@ -408,7 +399,7 @@ public void ZoomAtPoint(ZoomDirection zoomDirection, Point zoomAnchor)
408399
EnqueueW2dAction(() =>
409400
{
410401
if (_currentRenderer == null) return;
411-
_currentRenderer.CancelOffScreenTimer();
402+
412403
_canvasViewManager.ZoomAtPoint(zoomDirection, zoomAnchor);
413404
});
414405
}
@@ -420,13 +411,9 @@ public void ZoomAtPointPrecision(int delta, Point zoomAnchor)
420411
if (_currentRenderer == null) return;
421412
_canvasViewManager.ZoomAtPointPrecision(delta, zoomAnchor);
422413

423-
// No animation → no AnimationCompleted → must restart the offscreen timer manually
424-
// so the offscreen is recreated 650ms after the last zoom tick.
425-
_currentRenderer.RestartOffScreenDrawTimer();
426-
427-
// Mark the burst as active so D2dCanvas_Draw skips the offscreen and uses
428-
// source-bitmap quality. Debounce the clear to 700ms (just after the 650ms
429-
// offscreen timer) so Draw switches back to the fresh offscreen once zoom stops.
414+
// Mark the burst as active so the render path uses mip-based quality instead of
415+
// source-bitmap quality. Debounced 700ms after the last tick so Draw reverts to
416+
// non-animating mip selection once the zoom burst ends.
430417
_continuousZoomActive = true;
431418
_continuousZoomCts?.Cancel();
432419
_continuousZoomCts?.Dispose();
@@ -488,11 +475,12 @@ private void D2dCanvas_CreateResources(CanvasAnimatedControl sender,
488475
Microsoft.Graphics.Canvas.UI.CanvasCreateResourcesEventArgs args)
489476
{
490477
// Device (re)created (first load or device loss). Drop the device-bound brush so it is
491-
// rebuilt lazily, and ask the current renderer to rebuild its off-screen surface.
478+
// rebuilt lazily, and rebuild the mip chain with the current scaling quality.
479+
// Note: CanvasBitmaps held in the photo cache are NOT re-created here (accepted risk).
492480
// Runs on the W2D thread.
493481
_checkeredBrush?.Dispose();
494482
_checkeredBrush = null;
495-
_currentRenderer?.TryRedrawOffScreen(true);
483+
_currentRenderer?.HandleScalingMethodChange();
496484
}
497485

498486
/// <summary>
@@ -554,16 +542,9 @@ private void D2dCanvas_SizeChanged(object sender, SizeChangedEventArgs args)
554542
{
555543
if (_currentRenderer == null) return;
556544
_canvasViewManager.HandleSizeChange(newSize, previousSize);
557-
_currentRenderer.RestartOffScreenDrawTimer();
558545
});
559546
}
560547

561-
private void CanvasViewManager_AnimationCompleted()
562-
{
563-
if (!_isGoingToExit)
564-
_currentRenderer?.TryRedrawOffScreen(false);
565-
}
566-
567548
/// <summary>
568549
/// Returns a Task that completes when the next pan/zoom animation finishes (via the W2D-thread
569550
/// <see cref="CanvasViewManager.AnimationCompleted"/> event), or after <paramref name="timeoutMs"/>
@@ -613,7 +594,12 @@ public bool IsPressedOnImage(Point position)
613594

614595
public void HandleCheckeredBackgroundChange()
615596
{
616-
EnqueueW2dAction(() => _currentRenderer?.TryRedrawOffScreen(true));
597+
EnqueueW2dAction(() => { }); // wake the canvas; Draw() reads the setting live
598+
}
599+
600+
public void HandleImageScalingQualityChange()
601+
{
602+
EnqueueW2dAction(() => _currentRenderer?.HandleScalingMethodChange());
617603
}
618604

619605
/// <summary>

Src/FlyPhotos/Display/ImageRendering/AnimatedImageRenderer.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,7 @@ private async Task UpdateFrameAsync()
9292
}
9393
}
9494

95-
public void RestartOffScreenDrawTimer() { }
96-
public void CancelOffScreenTimer() { }
97-
public void TryRedrawOffScreen(bool forceCreate) { }
95+
public void HandleScalingMethodChange() { }
9896

9997
public void Dispose()
10098
{

Src/FlyPhotos/Display/ImageRendering/IRenderer.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ internal interface IRenderer : IDisposable
1414
/// </summary>
1515
void Draw(CanvasDrawingSession session, CanvasViewState viewState, CanvasImageInterpolation quality, bool isAnimating);
1616

17-
void RestartOffScreenDrawTimer();
18-
19-
void CancelOffScreenTimer();
20-
21-
void TryRedrawOffScreen(bool forceCreate);
17+
/// <summary>
18+
/// Called when the image scaling quality setting changes. Renderers that cache scaled
19+
/// GPU resources (e.g. mip chains) should rebuild them with the new quality.
20+
/// </summary>
21+
void HandleScalingMethodChange();
2222
}

Src/FlyPhotos/Display/ImageRendering/MultiPageRenderer.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ public void Draw(CanvasDrawingSession session, CanvasViewState viewState, Canvas
9393
}
9494
}
9595

96-
public void RestartOffScreenDrawTimer() { }
97-
public void CancelOffScreenTimer() { }
98-
public void TryRedrawOffScreen(bool forceCreate) { }
96+
public void HandleScalingMethodChange() => _invalidate();
9997

10098
public void Dispose()
10199
{

0 commit comments

Comments
 (0)