LANCommander/LANCommander.Launcher/Converters/FilePathToBitmapConverter.cs
Pat Hartl 99cf4cdc51 Launcher game details performance optimizations
- Changed scroll transition on video/screenshot carousel to use RenderTransform instead of margin to cut down on layout passes for each frame. This should result in a significant drop in CPU usage when scrolling.
- Pause playback of videos when scrolled out of view or window is no longer active.
- Removed additional InlineVideoPlayer controls from being created for each item in the media carousel, and instead only created for videos. This should result in a significant drop in RAM allocation when scrolling.
- Rework caching mechanism for covers to have an upper limit of allocated RAM.
- Only decode screenshots / covers at Hi DPI size in which they are displayed at. This should ensure crisp display while only allocating the amount of RAM needed for display.
2026-06-15 19:37:46 -05:00

74 lines
3.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
namespace LANCommander.Launcher.Converters;
/// <summary>
/// Converts a file path string to a thumbnail Bitmap.
/// Decodes at a fixed width so full-resolution source images don't consume
/// excessive memory, and caches bitmaps via WeakReference so they are freed
/// by the GC once no control is displaying them (e.g. after virtualization
/// scrolls them out of view) and reloaded on next access.
/// </summary>
public class FilePathToBitmapConverter : IValueConverter
{
// Covers are displayed at 140220 px wide. 320 px gives enough headroom
// for high-DPI screens while keeping per-image memory around 400 KB
// instead of several MB for full-resolution sources.
private const int DecodeWidth = 320;
// WeakReference cache: keeps bitmaps alive while on-screen, lets the GC
// collect them once nothing holds a strong reference (i.e. off-screen).
// Key is "{path}|{decodeWidth}" so full-res and thumbnail don't collide.
private static readonly Dictionary<string, WeakReference<Bitmap>> _cache = new();
public static readonly FilePathToBitmapConverter Instance = new();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string path || string.IsNullOrEmpty(path))
return null;
// ConverterParameter="full" → full resolution
// ConverterParameter="h{N}" → DecodeToHeight(N)
// ConverterParameter="w{N}" → DecodeToWidth(N) (e.g. game detail background)
// default → DecodeToWidth(320)
bool fullRes = parameter is string p && p == "full";
int? decodeHeight = parameter is string hp && hp.StartsWith("h") && int.TryParse(hp.AsSpan(1), out var h) ? h : null;
int? decodeWidth = parameter is string wp && wp.StartsWith("w") && int.TryParse(wp.AsSpan(1), out var w) ? w : null;
var cacheKey = fullRes ? $"{path}|full"
: decodeHeight.HasValue ? $"{path}|h{decodeHeight}"
: decodeWidth.HasValue ? $"{path}|w{decodeWidth}"
: $"{path}|{DecodeWidth}";
if (_cache.TryGetValue(cacheKey, out var weakRef) && weakRef.TryGetTarget(out var cached))
return cached;
try
{
if (!File.Exists(path))
return null;
using var stream = File.OpenRead(path);
var bitmap = fullRes
? new Bitmap(stream)
: decodeHeight.HasValue
? Bitmap.DecodeToHeight(stream, decodeHeight.Value, BitmapInterpolationMode.HighQuality)
: Bitmap.DecodeToWidth(stream, decodeWidth ?? DecodeWidth, BitmapInterpolationMode.HighQuality);
_cache[cacheKey] = new WeakReference<Bitmap>(bitmap);
return bitmap;
}
catch
{
return null;
}
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException();
}