Skip to content

Commit 51cfe8c

Browse files
mattleibowCopilot
andcommitted
Fix cache rebuild losing image, fix unbounded memIndex, update README
- Save TileSource BEFORE RebuildControllerCache so image is preserved after cache type/size changes - Fix double-dispose: _delayCache.Dispose() cascades to inner cache; _browserCache no longer disposed separately - Add _innerCache field to clearly track ownership in cache chain - Cap BrowserStorageTileCache._memIndex at 512 entries (tiles beyond the cap remain in sessionStorage, accessible via TryGetAsync) - PutAsync only populates memIndex on successful sessionStorage write - Remove ShowDebugStats reference from README (feature removed) - Rename SKDeepZoomTileCache → SKDeepZoomMemoryTileCache in README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 64779af commit 51cfe8c

File tree

3 files changed

+50
-30
lines changed

3 files changed

+50
-30
lines changed

samples/SkiaSharpDemo.Blazor/BrowserStorageTileCache.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ namespace SkiaSharpDemo;
2121
public sealed class BrowserStorageTileCache : ISKDeepZoomTileCache
2222
{
2323
private readonly IJSRuntime _js;
24-
// In-memory index so Contains/TryGet (sync) can return fast on hits without round-tripping JS.
24+
// In-memory index so Contains/TryGet (sync) can return fast without JS interop round-trips.
25+
// Capped to prevent unbounded growth (tiles still persist in sessionStorage beyond this limit).
26+
private const int MaxMemoryEntries = 512;
2527
private readonly ConcurrentDictionary<SKDeepZoomTileId, SKBitmap> _memIndex = new();
2628
private int _storageCount;
2729

@@ -59,7 +61,7 @@ public bool TryGet(SKDeepZoomTileId id, out SKBitmap? bitmap)
5961

6062
var bytes = Convert.FromBase64String(base64);
6163
var bitmap = SKBitmap.Decode(bytes);
62-
if (bitmap is not null)
64+
if (bitmap is not null && _memIndex.Count < MaxMemoryEntries)
6365
_memIndex.TryAdd(id, bitmap);
6466
return bitmap;
6567
}
@@ -71,19 +73,22 @@ public bool TryGet(SKDeepZoomTileId id, out SKBitmap? bitmap)
7173
public void Put(SKDeepZoomTileId id, SKBitmap? bitmap)
7274
{
7375
if (bitmap is null) return;
74-
_memIndex[id] = bitmap;
76+
if (_memIndex.Count < MaxMemoryEntries)
77+
_memIndex[id] = bitmap;
7578
// Fire-and-forget write to storage (best-effort, no await)
7679
_ = WriteToBrowserAsync(id, bitmap, CancellationToken.None);
7780
}
7881

7982
public async Task PutAsync(SKDeepZoomTileId id, SKBitmap? bitmap, CancellationToken ct = default)
8083
{
8184
if (bitmap is null) return;
82-
_memIndex[id] = bitmap;
83-
await WriteToBrowserAsync(id, bitmap, ct).ConfigureAwait(false);
85+
bool stored = await WriteToBrowserAsync(id, bitmap, ct).ConfigureAwait(false);
86+
// Only cache in memory if the storage write succeeded and we have room.
87+
if (stored && _memIndex.Count < MaxMemoryEntries)
88+
_memIndex.TryAdd(id, bitmap);
8489
}
8590

86-
private async Task WriteToBrowserAsync(SKDeepZoomTileId id, SKBitmap bitmap, CancellationToken ct)
91+
private async Task<bool> WriteToBrowserAsync(SKDeepZoomTileId id, SKBitmap bitmap, CancellationToken ct)
8792
{
8893
try
8994
{
@@ -93,8 +98,9 @@ private async Task WriteToBrowserAsync(SKDeepZoomTileId id, SKBitmap bitmap, Can
9398
await _js.InvokeVoidAsync("deepZoomCacheSet", ct, TileKey(id), base64)
9499
.ConfigureAwait(false);
95100
Interlocked.Increment(ref _storageCount);
101+
return true;
96102
}
97-
catch { /* quota exceeded or interop unavailable */ }
103+
catch { return false; /* quota exceeded or interop unavailable */ }
98104
}
99105

100106
public bool Remove(SKDeepZoomTileId id)

samples/SkiaSharpDemo.Blazor/Pages/DeepZoom.razor

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,9 @@
367367
private bool _useBrowserCache;
368368
private int _cacheCapacity = 1024;
369369
private DelayTileCache? _delayCache;
370-
private BrowserStorageTileCache? _browserCache;
370+
// _innerCache is the cache directly wrapped by _delayCache (owned; dispose via _delayCache)
371+
private ISKDeepZoomTileCache? _innerCache;
372+
private BrowserStorageTileCache? _browserCache; // same ref as _innerCache when _useBrowserCache=true
371373
372374
// URL input
373375
private string _urlInput = DefaultDziUrl;
@@ -402,8 +404,8 @@
402404
_thisRef = DotNetObjectReference.Create(this);
403405
await JS.InvokeVoidAsync("deepZoomRegisterResize", _thisRef);
404406

405-
_browserCache = new BrowserStorageTileCache(JS);
406-
_delayCache = new DelayTileCache(new SkiaSharp.Extended.DeepZoom.SKDeepZoomMemoryTileCache(_cacheCapacity));
407+
_innerCache = new SkiaSharp.Extended.DeepZoom.SKDeepZoomMemoryTileCache(_cacheCapacity);
408+
_delayCache = new DelayTileCache(_innerCache);
407409
_controller = new SKDeepZoomController(_delayCache);
408410
_controller.InvalidateRequired += OnInvalidateRequired;
409411

@@ -603,18 +605,20 @@
603605
private void OnCacheTypeChanged()
604606
{
605607
if (_controller == null) return;
606-
// Reload with new cache type
608+
var source = _controller.TileSource; // save BEFORE dispose
607609
RebuildControllerCache();
608-
var source = _controller.TileSource;
609-
var fetcher = new SkiaSharp.Extended.DeepZoom.SKDeepZoomHttpTileFetcher();
610610
if (source != null)
611-
_controller.Load(source, fetcher);
611+
_controller.Load(source, new SkiaSharp.Extended.DeepZoom.SKDeepZoomHttpTileFetcher());
612612
InvokeAsync(() => _canvas?.Invalidate());
613613
}
614614

615615
private void OnCacheSizeChanged()
616616
{
617+
if (_controller == null) return;
618+
var source = _controller.TileSource; // save BEFORE dispose
617619
RebuildControllerCache();
620+
if (source != null)
621+
_controller.Load(source, new SkiaSharp.Extended.DeepZoom.SKDeepZoomHttpTileFetcher());
618622
InvokeAsync(() => _canvas?.Invalidate());
619623
}
620624

@@ -627,17 +631,26 @@
627631
private void RebuildControllerCache()
628632
{
629633
if (_controller == null) return;
630-
// Dispose old caches
631-
_delayCache?.Dispose();
632-
_browserCache?.Dispose();
633634

634-
_browserCache = new BrowserStorageTileCache(JS);
635+
// _delayCache.Dispose() cascades to _innerCache (and transitively _browserCache)
636+
// so we must NOT dispose _innerCache or _browserCache separately.
637+
_delayCache?.Dispose();
638+
_delayCache = null;
639+
_innerCache = null; // already disposed via _delayCache
640+
_browserCache = null; // same object as _innerCache when useBrowserCache=true
635641
636-
ISKDeepZoomTileCache inner = _useBrowserCache
637-
? _browserCache
638-
: new SkiaSharp.Extended.DeepZoom.SKDeepZoomMemoryTileCache(_cacheCapacity);
642+
// Build new cache chain
643+
if (_useBrowserCache)
644+
{
645+
_browserCache = new BrowserStorageTileCache(JS);
646+
_innerCache = _browserCache;
647+
}
648+
else
649+
{
650+
_innerCache = new SkiaSharp.Extended.DeepZoom.SKDeepZoomMemoryTileCache(_cacheCapacity);
651+
}
639652

640-
_delayCache = new DelayTileCache(inner)
653+
_delayCache = new DelayTileCache(_innerCache)
641654
{
642655
IsEnabled = _cacheDelayEnabled,
643656
MinDelayMs = _cacheDelayMin,
@@ -665,7 +678,8 @@
665678
_controller.InvalidateRequired -= OnInvalidateRequired;
666679
_controller.Dispose();
667680
}
668-
_browserCache?.Dispose();
681+
// _delayCache owns _innerCache (and transitively _browserCache); dispose cascades.
682+
_delayCache?.Dispose();
669683
}
670684
}
671685

source/SkiaSharp.Extended/DeepZoom/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ flowchart TD
2525
Controller["SKDeepZoomController\n(orchestrator)"]
2626
Viewport["SKDeepZoomViewport\n(geometry)"]
2727
Scheduler["SKDeepZoomTileScheduler\n(tile selection)"]
28-
Cache["SKDeepZoomTileCache\n(LRU bitmap store)"]
28+
Cache["SKDeepZoomMemoryTileCache\n(LRU bitmap store)"]
2929
Renderer["SKDeepZoomRenderer\n(draws tiles)"]
3030
Fetcher["ISKDeepZoomTileFetcher\n(HTTP or file)"]
3131
TileSource["SKDeepZoomImageSource\n(DZI metadata)"]
@@ -145,7 +145,7 @@ flowchart LR
145145
end
146146
147147
subgraph "Caching"
148-
C["SKDeepZoomTileCache\n(LRU, configurable capacity)"]
148+
C["SKDeepZoomMemoryTileCache\n(LRU, configurable capacity)"]
149149
end
150150
151151
IF --> C
@@ -154,7 +154,7 @@ flowchart LR
154154

155155
**`SKDeepZoomTileId`** — uniquely identifies a tile as `(level, col, row)`.
156156
**`SKDeepZoomTileRequest`** — adds fallback context: if the exact tile isn't cached, a parent tile at a lower level is used to fill the space while the tile loads.
157-
**`SKDeepZoomTileCache`** — thread-safe LRU cache of `SKBitmap` objects. Evicts least-recently-used tiles when capacity is reached.
157+
**`SKDeepZoomMemoryTileCache`** — thread-safe LRU cache of `SKBitmap` objects. Evicts least-recently-used tiles when capacity is reached.
158158

159159
### 3 — Viewport & Geometry
160160

@@ -207,7 +207,7 @@ classDiagram
207207
```mermaid
208208
flowchart TD
209209
Renderer["SKDeepZoomRenderer"]
210-
Cache["SKDeepZoomTileCache"]
210+
Cache["SKDeepZoomMemoryTileCache"]
211211
Viewport["SKDeepZoomViewport"]
212212
TileSource["SKDeepZoomImageSource"]
213213
Scheduler["SKDeepZoomTileScheduler"]
@@ -223,7 +223,7 @@ flowchart TD
223223
Cache --> note1
224224
```
225225

226-
The renderer always falls back to a lower-level tile while the correct tile is loading, so the display is never blank. Tile borders and a debug overlay can be enabled via `ShowTileBorders` / `ShowDebugStats`.
226+
The renderer always falls back to a lower-level tile while the correct tile is loading, so the display is never blank. Tile borders and a debug overlay can be enabled via `ShowTileBorders`.
227227

228228
---
229229

@@ -233,7 +233,7 @@ The renderer always falls back to a lower-level tile while the correct tile is l
233233
classDiagram
234234
class SKDeepZoomController {
235235
+SKDeepZoomViewport Viewport
236-
+SKDeepZoomTileCache Cache
236+
+SKDeepZoomMemoryTileCache Cache
237237
+SKDeepZoomTileScheduler Scheduler
238238
+SKDeepZoomRenderer Renderer
239239
+SKDeepZoomImageSource? TileSource
@@ -341,7 +341,7 @@ public partial class MyPage : ContentPage
341341
| `SKDeepZoomViewportState.cs` | Snapshot struct for viewport state |
342342
| `SKDeepZoomRenderer.cs` | Draws tiles onto `SKCanvas` with fallback support |
343343
| `SKDeepZoomTileScheduler.cs` | Selects visible tiles at the optimal pyramid level |
344-
| `SKDeepZoomTileCache.cs` | Thread-safe LRU cache of decoded tile bitmaps |
344+
| `SKDeepZoomMemoryTileCache.cs` | Thread-safe LRU cache of decoded tile bitmaps |
345345
| `SKDeepZoomTileId.cs` | Identifies a tile: `(level, col, row)` |
346346
| `SKDeepZoomTileRequest.cs` | Tile + optional fallback parent tile |
347347
| `SKDeepZoomTileFailedEventArgs.cs` | Event args for tile load failures |

0 commit comments

Comments
 (0)