using XFESystem.Desktop; using Xunit; using Xunit.Abstractions; namespace XFESystem.Desktop.Tests; public sealed class DesktopWebFrameCacheTests(ITestOutputHelper output) { private static DesktopWebFrameKey Key(long page = 1, long revision = 2, long layout = 3, int width = 4, int height = 3, int scale = 200, double scrollX = 0, double scrollY = 0) => new(page, revision, layout, width, height, scale, scrollX, scrollY); [Fact] public void BackgroundRestoreCannotOverwriteProtectedForegroundMotionPixels() { var cache=new DesktopWebFrameCache();var pixels=Enumerable.Range(0,120).ToArray();var original=(int[])pixels.Clone(); Assert.True(cache.Store(Key(),pixels,12,10,1,2,new(0,0,12,10)));Array.Fill(pixels,-1); Assert.True(cache.TryRestore(Key(),pixels,12,10,7,6,new(0,0,12,10),new(8,7,2,1))); for(int y=0;y<10;y++)for(int x=0;x<12;x++) { bool viewport=x>=7&&x<11&&y>=6&&y<9,protectedPixel=x>=8&&x<10&&y==7; Assert.Equal(viewport&&!protectedPixel?original[(y-6+2)*12+x-7+1]:-1,pixels[y*12+x]); } Assert.Equal(10,cache.PixelsRestored); Assert.True(cache.TryRestore(Key(),pixels,12,10,7,6,new(0,0,12,10),new(0,0,12,10))); Assert.Equal(10,cache.PixelsRestored); } [Fact] public void CaptureRejectsAnyForegroundPreservationIntersectionWithoutEvictingGoodFrames() { var cache=new DesktopWebFrameCache(maximumBytes:48,maximumEntries:1);var pixels=Enumerable.Range(0,48).ToArray(); Assert.True(cache.Store(Key(),pixels,8,6,1,1,new(0,0,8,6))); Assert.False(cache.Store(Key(page:2),pixels,8,6,1,1,new(0,0,8,6),new(4,2,1,1))); Assert.Equal(1,cache.Count);Assert.Equal(0,cache.Evictions);Assert.Equal(1,cache.Captures); Assert.True(cache.TryRestore(Key(),pixels,8,6,1,1,new(0,0,8,6))); Assert.True(cache.Store(Key(page:2),pixels,8,6,1,1,new(0,0,8,6),new(5,2,1,1))); } [Fact] public void ProtectedRestoreKeepsExactPixelsAndAccountingForClippedAndOffscreenDestinations() { var cache=new DesktopWebFrameCache();var original=Enumerable.Range(0,120).ToArray();var pixels=new int[120]; Assert.True(cache.Store(Key(),original,12,10,1,2,new(0,0,12,10))); foreach(int destinationX in new[]{-3,0,7,10})foreach(int destinationY in new[]{-2,0,6,9}) foreach(var clip in new[]{new DesktopBounds(0,0,12,10),new DesktopBounds(2,1,7,5)}) foreach(var preserved in new[]{new DesktopBounds(8,7,2,1),new DesktopBounds(-2,-2,4,5),new DesktopBounds(11,9,100,100), new DesktopBounds(0,0,0,0),new DesktopBounds(int.MinValue,0,int.MaxValue,10),new DesktopBounds(0,int.MinValue,12,int.MaxValue)}) { Array.Fill(pixels,-1);long before=cache.PixelsRestored;int copied=0; Assert.True(cache.TryRestore(Key(),pixels,12,10,destinationX,destinationY,clip,preserved)); for(int y=0;y<10;y++)for(int x=0;x<12;x++) { bool viewport=x>=destinationX&&x=destinationY&&y=clip.X&&x=clip.Y&&y=preserved.X&&(long)x<(long)preserved.X+preserved.Width&&y>=preserved.Y&&(long)y<(long)preserved.Y+preserved.Height; bool copy=viewport&&inClip&&!protect;if(copy)copied++; Assert.Equal(copy?original[(2+y-destinationY)*12+1+x-destinationX]:-1,pixels[y*12+x]); } Assert.Equal(copied,cache.PixelsRestored-before); } } [Fact] public void RestoreCopiesOnlyCurrentDamageAndReusesPixelsAtAnotherPosition() { var cache = new DesktopWebFrameCache(); var pixels = Enumerable.Range(0, 120).ToArray(); Assert.True(cache.Store(Key(), pixels, 12, 10, 1, 2, new(1, 2, 4, 3))); var stored = (int[])pixels.Clone(); Array.Fill(pixels, -1); Assert.True(cache.TryRestore(Key(), pixels, 12, 10, 7, 6, new(8, 7, 2, 1))); for (int y = 0; y < 10; y++) for (int x = 0; x < 12; x++) Assert.Equal(y == 7 && x is >= 8 and < 10 ? stored[(2 + y - 6) * 12 + 1 + x - 7] : -1, pixels[y * 12 + x]); Assert.Equal(2, cache.PixelsRestored); Assert.Equal(1, cache.RestoreHits); Assert.Equal(48, cache.CachedBytes); } [Fact] public void RestoreClipsNegativeDestinationToTheRealFramebuffer() { var cache = new DesktopWebFrameCache(); var pixels = Enumerable.Range(0, 48).ToArray(); Assert.True(cache.Store(Key(), pixels, 8, 6, 2, 1, new(0, 0, 8, 6))); var stored = (int[])pixels.Clone(); Array.Fill(pixels, -1); Assert.True(cache.TryRestore(Key(), pixels, 8, 6, -2, -1, new(0, 0, 8, 6))); for (int y = 0; y < 6; y++) for (int x = 0; x < 8; x++) Assert.Equal(y < 2 && x < 2 ? stored[(1 + y + 1) * 8 + 2 + x + 2] : -1, pixels[y * 8 + x]); } [Fact] public void PartialOrOffscreenCaptureCannotReplaceAnExistingFullFrame() { var cache = new DesktopWebFrameCache(); var pixels = Enumerable.Range(0, 48).ToArray(); Assert.True(cache.Store(Key(), pixels, 8, 6, 2, 1, new(0, 0, 8, 6))); var stored = (int[])pixels.Clone(); Array.Fill(pixels, -1); Assert.False(cache.Store(Key(), pixels, 8, 6, 2, 1, new(2, 1, 3, 3))); Assert.False(cache.Store(Key(page: 2), pixels, 8, 6, -1, 0, new(0, 0, 8, 6))); Assert.False(cache.Store(Key(page: 3), pixels, 8, 6, 5, 4, new(0, 0, 8, 6))); Assert.Equal(1, cache.Count); Assert.Equal(1, cache.Captures); Assert.True(cache.TryRestore(Key(), pixels, 8, 6, 2, 1, new(0, 0, 8, 6))); for (int y = 1; y < 4; y++) for (int x = 2; x < 6; x++) Assert.Equal(stored[y * 8 + x], pixels[y * 8 + x]); } [Fact] public void EveryContentGeometryAndScaleKeyFieldInvalidatesTheFrame() { var cache = new DesktopWebFrameCache(); var pixels = new int[48]; Assert.True(cache.Store(Key(), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); foreach (var key in new[] { Key(page: 2), Key(revision: 4), Key(layout: 5), Key(width: 5), Key(height: 4), Key(scale: 100), Key(scrollX: .25), Key(scrollY: 1) }) Assert.False(cache.TryRestore(key, pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.True(cache.TryRestore(Key(), pixels, 8, 6, 100, 100, new(0, 0, 8, 6))); Assert.Equal(0, cache.PixelsRestored); } [Fact] public void EntryBoundUsesLeastRecentlyRestoredAndStoredOrder() { var cache = new DesktopWebFrameCache(maximumBytes: 96, maximumEntries: 2); var pixels = new int[12]; Assert.True(cache.Store(Key(page: 1), pixels, 4, 3, 0, 0, new(0, 0, 4, 3))); Assert.True(cache.Store(Key(page: 2), pixels, 4, 3, 0, 0, new(0, 0, 4, 3))); Assert.True(cache.TryRestore(Key(page: 1), pixels, 4, 3, 0, 0, new(0, 0, 4, 3))); Assert.True(cache.Store(Key(page: 3), pixels, 4, 3, 0, 0, new(0, 0, 4, 3))); Assert.False(cache.TryRestore(Key(page: 2), pixels, 4, 3, 0, 0, new(0, 0, 4, 3))); Assert.True(cache.TryRestore(Key(page: 1), pixels, 4, 3, 0, 0, new(0, 0, 4, 3))); Assert.Equal(2, cache.Count); Assert.Equal(96, cache.CachedBytes); Assert.Equal(1, cache.Evictions); } [Fact] public void RepeatedRevisionsReuseEvictedBuffersWithoutChangingPixelsOrLruOrder() { const int width = 128, height = 64; var cache = new DesktopWebFrameCache(maximumBytes: width * height * sizeof(int) * 3, maximumEntries: 3); var pixels = new int[width * height]; var restored = new int[pixels.Length]; var clip = new DesktopBounds(0, 0, width, height); DesktopWebFrameKey Revision(int revision) => Key(revision: revision, width: width, height: height); for (int revision = 1; revision <= 3; revision++) { Array.Fill(pixels, revision); Assert.True(cache.Store(Revision(revision), pixels, width, height, 0, 0, clip)); } // Restoring revision 1 makes revision 2 the first normal eviction. Assert.True(cache.TryRestore(Revision(1), restored, width, height, 0, 0, clip)); Assert.All(restored, pixel => Assert.Equal(1, pixel)); long allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); bool storedAll = true; for (int revision = 4; revision <= 1003; revision++) { // Non-uniform values catch stale rows and partial overwrites when // the old revision's storage is assigned to the new key. for (int pixel = 0; pixel < pixels.Length; pixel++) pixels[pixel] = revision * 10000 + pixel; storedAll &= cache.Store(Revision(revision), pixels, width, height, 0, 0, clip); } long allocated = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore; output.WriteLine("RepeatedRevisionUpdates=1000; PixelBufferBytes=32768; AllocatedBytes=" + allocated); Assert.True(storedAll); // Entry records are still allocated; 1,000 new 32 KiB pixel arrays // would exceed 32 MiB. No buffer pool or host GC is required for reuse. Assert.InRange(allocated, 1, 256 * 1024); Assert.Equal(3, cache.Count); Assert.Equal(3L * pixels.Length * sizeof(int), cache.CachedBytes); Assert.Equal(1000, cache.Evictions); Assert.Equal(1003, cache.Captures); Assert.Equal(3, cache.PixelChunkAllocations); Assert.Equal(1000, cache.PixelStorageReuses); Assert.False(cache.TryRestore(Revision(1000), restored, width, height, 0, 0, clip)); for (int revision = 1001; revision <= 1003; revision++) { Array.Fill(restored, -1); Assert.True(cache.TryRestore(Revision(revision), restored, width, height, 0, 0, clip)); for (int pixel = 0; pixel < restored.Length; pixel++) Assert.Equal(revision * 10000 + pixel, restored[pixel]); } } [Fact] public void EvictedBuffersAreReusedOnlyAtTheExactPixelLength() { var cache = new DesktopWebFrameCache(maximumBytes: 96, maximumEntries: 1); var pixels = Enumerable.Range(0, 48).ToArray(); var clip = new DesktopBounds(0, 0, 8, 6); Assert.True(cache.Store(Key(width: 4, height: 3), pixels, 8, 6, 1, 1, clip)); Assert.True(cache.Store(Key(page: 2, width: 6, height: 4), pixels, 8, 6, 0, 0, clip)); var restored = Enumerable.Repeat(-1, pixels.Length).ToArray(); Assert.True(cache.TryRestore(Key(page: 2, width: 6, height: 4), restored, 8, 6, 0, 0, clip)); for (int row = 0; row < 6; row++) for (int column = 0; column < 8; column++) Assert.Equal(row < 4 && column < 6 ? pixels[row * 8 + column] : -1, restored[row * 8 + column]); Assert.Equal(96, cache.CachedBytes); Assert.Equal(1, cache.Count); } [Fact] public void ChunkedCaptureAndProtectedClippedRestorePreserveEveryNonUniformPixelAcrossChunkSeams() { const int frameWidth = 389, frameHeight = 150, width = 353, height = 113, sourceX = 13, sourceY = 7; var cache = new DesktopWebFrameCache(); var original = Enumerable.Range(0, frameWidth * frameHeight).Select(value => unchecked(value * 7919 + 131)).ToArray(); var restored = new int[original.Length]; var key = Key(width: width, height: height); Assert.True(cache.Store(key, original, frameWidth, frameHeight, sourceX, sourceY, new(0, 0, frameWidth, frameHeight))); Assert.Equal(3, cache.PixelChunkAllocations); Assert.Equal(64 * 1024, cache.LargestPixelChunkAllocatedBytes); foreach (int targetX in new[] { -17, 7, 90 }) foreach (int targetY in new[] { -6, 20, 110 }) foreach (var clip in new[] { new DesktopBounds(0, 0, frameWidth, frameHeight), new DesktopBounds(11, 13, 351, 117) }) { var preserved = new DesktopBounds(141, 66, 28, 6); Array.Fill(restored, -1); long before = cache.PixelsRestored; Assert.True(cache.TryRestore(key, restored, frameWidth, frameHeight, targetX, targetY, clip, preserved)); int copied = 0; for (int y = 0; y < frameHeight; y++) for (int x = 0; x < frameWidth; x++) { bool copy = x >= targetX && x < targetX + width && y >= targetY && y < targetY + height && clip.Contains(x, y) && !preserved.Contains(x, y); if (copy) copied++; Assert.Equal(copy ? original[(sourceY + y - targetY) * frameWidth + sourceX + x - targetX] : -1, restored[y * frameWidth + x]); } Assert.Equal(copied, cache.PixelsRestored - before); } } [Fact] public void NativeFourKScaledViewportNeedsOnlyBoundedChunkAllocationsAndReusesThemForLaterRevisions() { // This exact geometry previously requested 5,145,368 bytes including // the native managed-array header and failed with 960 free pages in // the largest physical run, despite 48,830 free pages in total. const int width = 1592, height = 808; var key = Key(width: width, height: height); var cache = new DesktopWebFrameCache(maximumBytes: (long)width * height * sizeof(int), maximumEntries: 1); var pixels = Enumerable.Range(0, width * height).Select(value => unchecked(value * 1009 + 77)).ToArray(); var restored = new int[pixels.Length]; var clip = new DesktopBounds(0, 0, width, height); Assert.True(cache.Store(key, pixels, width, height, 0, 0, clip)); Assert.Equal(79, cache.PixelChunkAllocations); Assert.Equal(64 * 1024, cache.LargestPixelChunkAllocatedBytes); Assert.Equal(5_145_344, cache.CachedBytes); Assert.True(cache.TryRestore(key, restored, width, height, 0, 0, clip)); Assert.Equal(pixels, restored); for (int pixel = 0; pixel < pixels.Length; pixel++) pixels[pixel] = unchecked(pixels[pixel] ^ 0x13572468); var next = key with { Revision = key.Revision + 1 }; long allocatedBefore = GC.GetAllocatedBytesForCurrentThread(); Assert.True(cache.Store(next, pixels, width, height, 0, 0, clip)); long allocated = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore; Assert.InRange(allocated, 1, 1024); Assert.Equal(79, cache.PixelChunkAllocations); Assert.Equal(1, cache.PixelStorageReuses); Assert.False(cache.TryRestore(key, restored, width, height, 0, 0, clip)); Assert.True(cache.TryRestore(next, restored, width, height, 0, 0, clip)); Assert.Equal(pixels, restored); output.WriteLine("NativeViewport=1592x808; PixelBytes={0}; Chunks={1}; LargestChunkPayloadBytes={2}; RevisionAllocatedBytes={3}", cache.CachedBytes, cache.PixelChunkAllocations, cache.LargestPixelChunkAllocatedBytes, allocated); } [Fact] public void GlobalByteBoundIsThirtyTwoMiBAndEntryBoundIsSix() { var cache = new DesktopWebFrameCache(); var pixels = new int[2048 * 1024]; for (int page = 1; page <= 7; page++) { Assert.True(cache.Store(Key(page: page, width: 2048, height: 1024), pixels, 2048, 1024, 0, 0, new(0, 0, 2048, 1024))); Assert.InRange(cache.Count, 1, 6); Assert.InRange(cache.CachedBytes, 1, 32L * 1024 * 1024); } Assert.Equal(4, cache.Count); Assert.Equal(32L * 1024 * 1024, cache.CachedBytes); Assert.Equal(3, cache.Evictions); var tiny = new DesktopWebFrameCache(); var one = new int[1]; for (int page = 1; page <= 7; page++) Assert.True(tiny.Store(Key(page: page, width: 1, height: 1), one, 1, 1, 0, 0, new(0, 0, 1, 1))); Assert.Equal(6, tiny.Count); Assert.False(tiny.TryRestore(Key(page: 1, width: 1, height: 1), one, 1, 1, 0, 0, new(0, 0, 1, 1))); } [Fact] public void InvalidAndOversizedCapturesDoNotEvictGoodFrames() { var cache = new DesktopWebFrameCache(maximumBytes: 48); var pixels = new int[48]; Assert.True(cache.Store(Key(), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.False(cache.Store(Key(width: 8, height: 6), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.False(cache.Store(Key(scrollY: double.NaN), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.False(cache.Store(Key(scrollX: double.PositiveInfinity), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.False(cache.Store(Key(width: int.MaxValue, height: int.MaxValue), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.False(cache.Store(Key(), pixels, 8, 6, 0, 0, new(int.MinValue, 0, int.MaxValue, 6))); Assert.Equal(1, cache.Count); Assert.Equal(0, cache.Evictions); Assert.True(cache.TryRestore(Key(), pixels, 8, 6, 0, 0, new(0, 0, 8, 6))); Assert.Throws(() => new DesktopWebFrameCache(maximumBytes: 32L * 1024 * 1024 + 1)); Assert.Throws(() => new DesktopWebFrameCache(maximumEntries: 7)); } }