unload Photos by least-recently-used

This commit is contained in:
Colin McMillen 2023-07-26 21:29:26 -04:00
parent dc909a80f4
commit ca5b2d94f5

View File

@ -172,6 +172,7 @@ void main() {
public class Photo { public class Photo {
public string Filename; public string Filename;
public bool Loaded = false; public bool Loaded = false;
public long LastTouch = 0;
public Vector2i Size; public Vector2i Size;
public DateTime DateTimeOriginal; public DateTime DateTimeOriginal;
public string CameraModel = ""; public string CameraModel = "";
@ -183,6 +184,7 @@ public class Photo {
public int Rating = 0; public int Rating = 0;
public ushort Orientation = 1; public ushort Orientation = 1;
private static long touchCounter = 0;
private Texture texture; private Texture texture;
private Texture placeholder; private Texture placeholder;
private Image<Rgba32>? image = null; private Image<Rgba32>? image = null;
@ -204,6 +206,7 @@ public class Photo {
// We don't assign to this.image until Load() is done, because we might // We don't assign to this.image until Load() is done, because we might
// edit the image due to rotation (etc) and don't want to try generating // edit the image due to rotation (etc) and don't want to try generating
// a texture for it until that's already happened. // a texture for it until that's already happened.
LastTouch = touchCounter++;
Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename); Image<Rgba32> tmp = await Image.LoadAsync<Rgba32>(Filename);
Util.RotateImageFromExif(tmp, Orientation); Util.RotateImageFromExif(tmp, Orientation);
image = tmp; image = tmp;
@ -348,6 +351,7 @@ public class Photo {
} }
public Texture Texture() { public Texture Texture() {
LastTouch = touchCounter++;
if (texture == placeholder && image != null) { if (texture == placeholder && image != null) {
// The texture needs to be created on the GL thread, so we instantiate // The texture needs to be created on the GL thread, so we instantiate
// it here (since this is called from OnRenderFrame), as long as the // it here (since this is called from OnRenderFrame), as long as the
@ -371,6 +375,9 @@ public class Texture : IDisposable {
public int Handle; public int Handle;
public Vector2i Size; public Vector2i Size;
private static int maxHandle = -1;
private bool disposedValue = false;
public Texture(Image<Rgba32> image) { public Texture(Image<Rgba32> image) {
Size = new Vector2i(image.Width, image.Height); Size = new Vector2i(image.Width, image.Height);
byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf<Rgba32>()]; byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf<Rgba32>()];
@ -395,9 +402,6 @@ public class Texture : IDisposable {
//GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); //GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
} }
private static int maxHandle = -1;
private bool disposedValue = false;
protected virtual void Dispose(bool disposing) { protected virtual void Dispose(bool disposing) {
if (!disposedValue) { if (!disposedValue) {
GL.DeleteTexture(Handle); GL.DeleteTexture(Handle);
@ -565,7 +569,7 @@ public class Game : GameWindow {
int VertexArrayObject; int VertexArrayObject;
List<Photo> allPhotos = new(); List<Photo> allPhotos = new();
List<Photo> photos = new(); List<Photo> photos = new();
HashSet<string> loadedImages = new(); HashSet<Photo> loadedImages = new();
int photoIndex = 0; int photoIndex = 0;
int ribbonIndex = 0; int ribbonIndex = 0;
Shader shader = new(); Shader shader = new();
@ -711,6 +715,7 @@ public class Game : GameWindow {
void FilterByRating(int rating) { void FilterByRating(int rating) {
Console.WriteLine("filter to " + rating); Console.WriteLine("filter to " + rating);
photos = allPhotos.Where(p => p.Rating >= rating).ToList(); photos = allPhotos.Where(p => p.Rating >= rating).ToList();
// TODO: put this closest to wherever the previously active photo was.
photoIndex = 0; photoIndex = 0;
} }
@ -752,10 +757,10 @@ public class Game : GameWindow {
// Load photos from a directory. // Load photos from a directory.
// string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\"); // string[] files = Directory.GetFiles(@"c:\users\colin\desktop\photos-test\");
string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\"); // string[] files = Directory.GetFiles(@"c:\users\colin\pictures\photos\2023\07\14\");
// string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\"); // string[] files = Directory.GetFiles(@"G:\DCIM\100EOSR6\");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23"); // string[] files = Directory.GetFiles(@"C:\Users\colin\Pictures\photos\2018\06\23");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000"); string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\Germany all\104D7000");
// string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\"); // string[] files = Directory.GetFiles(@"C:\Users\colin\Desktop\many-birds\");
for (int i = 0; i < files.Count(); i++) { for (int i = 0; i < files.Count(); i++) {
@ -785,28 +790,31 @@ public class Game : GameWindow {
} }
private async void LoadAndUnloadImagesAsync() { private async void LoadAndUnloadImagesAsync() {
int minUnloadedImage = Math.Max(0, photoIndex - 20); int minLoadedImage = Math.Max(0, photoIndex - 20);
int maxUnloadedImage = Math.Min(photoIndex + 30, photos.Count - 1);
int minLoadedImage = Math.Max(0, photoIndex - 10);
int maxLoadedImage = Math.Min(photoIndex + 20, photos.Count - 1); int maxLoadedImage = Math.Min(photoIndex + 20, photos.Count - 1);
// First, unload images that are far outside our window. // First, unload images that haven't been touched in a while.
// FIXME: also cancel any in-progress loading tasks that have moved outside our window. // FIXME: also cancel any of these if they still have an in-progress loading task -- I suspect this is the source of a memory leak.
// FIXME: keep around thumbnail-sized textures? // FIXME: keep around thumbnail-sized textures?
// FIXME: turn unloading back on, using an LRU cache for evicting images. while (loadedImages.Count > 60) {
/* long earliestTime = long.MaxValue;
foreach (int i in loadedImages) { Photo? earliest = null;
if (i < minUnloadedImage || i > maxUnloadedImage) { foreach (Photo photo in loadedImages) {
// Console.WriteLine("unloading " + i); if (photo.LastTouch < earliestTime) {
loadedImages.Remove(i); earliest = photo;
photos[i].UnloadAsync(); earliestTime = photo.LastTouch;
} }
} }
*/ if (earliest != null) {
// Then, start loading any images that aren't in our window. Console.WriteLine($"loadedImages.Count: {loadedImages.Count}, evicting {earliest.Filename} @ {earliestTime}");
earliest.UnloadAsync();
loadedImages.Remove(earliest);
}
}
// Then, start loading any images that are in our window but not yet loaded.
for (int i = minLoadedImage; i <= maxLoadedImage; i++) { for (int i = minLoadedImage; i <= maxLoadedImage; i++) {
if (!loadedImages.Contains(photos[i].Filename)) { if (!loadedImages.Contains(photos[i])) {
// Console.WriteLine("loading " + i); Console.WriteLine("loading " + i);
loadedImages.Add(photos[i].Filename); loadedImages.Add(photos[i]);
await Task.Run( () => { photos[i].LoadAsync(); }); await Task.Run( () => { photos[i].LoadAsync(); });
} }
} }