sharpemu/tests/SharpEmu.Libs.Tests/AvPlayer/AvPlayerFrameReuseTests.cs

147 lines
5.9 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.Libs.AvPlayer;
using Xunit;
using static SharpEmu.Libs.AvPlayer.AvPlayerExports;
namespace SharpEmu.Libs.Tests.AvPlayer;
public sealed class AvPlayerFrameReuseTests
{
[Theory]
[InlineData(2)]
[InlineData(6)]
[InlineData(16)]
public void FramesKeepTheirContentsAndReuseBoundedStorage(int capacity)
{
const int frameCount = 600;
const int frameByteCount = 384;
using var source = new PatternStream(frameByteCount, frameCount);
using var queue = new VideoFrameQueue(source, frameByteCount, capacity);
var storage = new HashSet<byte[]>(ReferenceEqualityComparer.Instance);
byte[]? lastFrame = null;
for (var frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
lastFrame = ReadReadyFrame(queue);
Assert.Equal(frameByteCount, lastFrame.Length);
Assert.All(lastFrame, value => Assert.Equal(unchecked((byte)frameIndex), value));
storage.Add(lastFrame);
}
WaitForWorker(queue);
Assert.Equal(VideoFrameReadResult.End, queue.TryRead(out _));
Assert.All(lastFrame!, value => Assert.Equal(unchecked((byte)(frameCount - 1)), value));
Assert.InRange(storage.Count, 2, capacity + 3);
}
[Fact]
public void PendingReadDoesNotRecycleTheCurrentFrame()
{
using var source = new PatternStream(384, 3, pauseBeforeFrame: 1);
using var queue = new VideoFrameQueue(source, 384, 2);
var firstFrame = ReadReadyFrame(queue);
Assert.True(source.Paused.Wait(TimeSpan.FromSeconds(5)));
Assert.Equal(VideoFrameReadResult.Pending, queue.TryRead(out var pendingFrame));
Assert.Null(pendingFrame);
source.Resume.Set();
WaitForWorker(queue);
Assert.All(firstFrame, value => Assert.Equal((byte)0, value));
Assert.All(ReadReadyFrame(queue), value => Assert.Equal((byte)1, value));
Assert.All(ReadReadyFrame(queue), value => Assert.Equal((byte)2, value));
}
[Fact]
public void DisposingAFullQueueStopsTheWorkerAndPreservesTheDeliveredFrame()
{
using var source = new PatternStream(384, 100);
using var queue = new VideoFrameQueue(source, 384, 2);
var firstFrame = ReadReadyFrame(queue);
Assert.True(SpinWait.SpinUntil(() => source.CompletedFrames >= 4, TimeSpan.FromSeconds(5)));
queue.Dispose();
queue.Dispose();
WaitForWorker(queue);
using var replacementSource = new PatternStream(384, 2);
using var replacementQueue = new VideoFrameQueue(replacementSource, 384, 2);
Assert.NotSame(firstFrame, ReadReadyFrame(replacementQueue));
Assert.NotSame(firstFrame, ReadReadyFrame(replacementQueue));
Assert.All(firstFrame, value => Assert.Equal((byte)0, value));
}
[Fact]
public void IncompleteFinalFrameIsNotPublished()
{
using var source = new MemoryStream(new byte[384 + 17]);
using var queue = new VideoFrameQueue(source, 384, 2);
WaitForWorker(queue);
Assert.Equal(384, ReadReadyFrame(queue).Length);
Assert.Equal(VideoFrameReadResult.End, queue.TryRead(out _));
}
private static byte[] ReadReadyFrame(VideoFrameQueue queue)
{
byte[]? frame = null;
var result = VideoFrameReadResult.Pending;
Assert.True(SpinWait.SpinUntil(() =>
{
result = queue.TryRead(out frame);
return result != VideoFrameReadResult.Pending;
}, TimeSpan.FromSeconds(5)), "The decoder did not supply a frame.");
Assert.Equal(VideoFrameReadResult.Ready, result);
return Assert.IsType<byte[]>(frame);
}
private static void WaitForWorker(VideoFrameQueue queue)
{
var worker = (Thread)typeof(VideoFrameQueue)
.GetField("_worker", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(queue)!;
Assert.True(worker.Join(TimeSpan.FromSeconds(5)), "The decoder worker did not finish.");
}
private sealed class PatternStream(int frameByteCount, int frameCount, int pauseBeforeFrame = -1) : Stream
{
private int _position;
private int _completedFrames;
public ManualResetEventSlim Paused { get; } = new();
public ManualResetEventSlim Resume { get; } = new();
public int CompletedFrames => Volatile.Read(ref _completedFrames);
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => (long)frameByteCount * frameCount;
public override long Position { get => _position; set => throw new NotSupportedException(); }
public override int Read(byte[] buffer, int offset, int count)
{
if (_position == Length)
{
return 0;
}
var frameIndex = _position / frameByteCount;
if (frameIndex == pauseBeforeFrame)
{
Paused.Set();
Resume.Wait();
}
var byteCount = Math.Min(Math.Min(count, 37), frameByteCount - _position % frameByteCount);
buffer.AsSpan(offset, byteCount).Fill(unchecked((byte)frameIndex));
_position += byteCount;
Volatile.Write(ref _completedFrames, _position / frameByteCount);
return byteCount;
}
protected override void Dispose(bool disposing)
{
Resume.Set();
base.Dispose(disposing);
}
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}
}