diff --git a/Program.cs b/Program.cs index 3ef9be1..7ce50ac 100644 --- a/Program.cs +++ b/Program.cs @@ -172,6 +172,7 @@ void main() { public class Photo { public string Filename; public bool Loaded = false; + public long LastTouch = 0; public Vector2i Size; public DateTime DateTimeOriginal; public string CameraModel = ""; @@ -183,6 +184,7 @@ public class Photo { public int Rating = 0; public ushort Orientation = 1; + private static long touchCounter = 0; private Texture texture; private Texture placeholder; private Image? image = null; @@ -204,6 +206,7 @@ public class Photo { // 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 // a texture for it until that's already happened. + LastTouch = touchCounter++; Image tmp = await Image.LoadAsync(Filename); Util.RotateImageFromExif(tmp, Orientation); image = tmp; @@ -348,6 +351,7 @@ public class Photo { } public Texture Texture() { + LastTouch = touchCounter++; if (texture == placeholder && image != null) { // 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 @@ -371,6 +375,9 @@ public class Texture : IDisposable { public int Handle; public Vector2i Size; + private static int maxHandle = -1; + private bool disposedValue = false; + public Texture(Image image) { Size = new Vector2i(image.Width, image.Height); byte[] pixelBytes = new byte[Size.X * Size.Y * Unsafe.SizeOf()]; @@ -395,9 +402,6 @@ public class Texture : IDisposable { //GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); } - private static int maxHandle = -1; - private bool disposedValue = false; - protected virtual void Dispose(bool disposing) { if (!disposedValue) { GL.DeleteTexture(Handle); @@ -565,7 +569,7 @@ public class Game : GameWindow { int VertexArrayObject; List allPhotos = new(); List photos = new(); - HashSet loadedImages = new(); + HashSet loadedImages = new(); int photoIndex = 0; int ribbonIndex = 0; Shader shader = new(); @@ -711,6 +715,7 @@ public class Game : GameWindow { void FilterByRating(int rating) { Console.WriteLine("filter to " + rating); photos = allPhotos.Where(p => p.Rating >= rating).ToList(); + // TODO: put this closest to wherever the previously active photo was. photoIndex = 0; } @@ -752,10 +757,10 @@ public class Game : GameWindow { // Load photos from a directory. // 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(@"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\"); for (int i = 0; i < files.Count(); i++) { @@ -785,28 +790,31 @@ public class Game : GameWindow { } private async void LoadAndUnloadImagesAsync() { - int minUnloadedImage = Math.Max(0, photoIndex - 20); - int maxUnloadedImage = Math.Min(photoIndex + 30, photos.Count - 1); - int minLoadedImage = Math.Max(0, photoIndex - 10); + int minLoadedImage = Math.Max(0, photoIndex - 20); int maxLoadedImage = Math.Min(photoIndex + 20, photos.Count - 1); - // First, unload images that are far outside our window. - // FIXME: also cancel any in-progress loading tasks that have moved outside our window. + // First, unload images that haven't been touched in a while. + // 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: turn unloading back on, using an LRU cache for evicting images. - /* - foreach (int i in loadedImages) { - if (i < minUnloadedImage || i > maxUnloadedImage) { - // Console.WriteLine("unloading " + i); - loadedImages.Remove(i); - photos[i].UnloadAsync(); + while (loadedImages.Count > 60) { + long earliestTime = long.MaxValue; + Photo? earliest = null; + foreach (Photo photo in loadedImages) { + if (photo.LastTouch < earliestTime) { + earliest = photo; + earliestTime = photo.LastTouch; + } + } + if (earliest != null) { + Console.WriteLine($"loadedImages.Count: {loadedImages.Count}, evicting {earliest.Filename} @ {earliestTime}"); + earliest.UnloadAsync(); + loadedImages.Remove(earliest); } } - */ - // Then, start loading any images that aren't in our window. + // Then, start loading any images that are in our window but not yet loaded. for (int i = minLoadedImage; i <= maxLoadedImage; i++) { - if (!loadedImages.Contains(photos[i].Filename)) { - // Console.WriteLine("loading " + i); - loadedImages.Add(photos[i].Filename); + if (!loadedImages.Contains(photos[i])) { + Console.WriteLine("loading " + i); + loadedImages.Add(photos[i]); await Task.Run( () => { photos[i].LoadAsync(); }); } }