using System.Collections.Generic;
using IFSRenderer.AvaloniaShell;
using Xunit;
namespace IFSRenderer.AvaloniaShell.Tests;
/// <summary>
/// The frame-completion gate used by both export paths.
/// </summary>
/// <remarks>
/// <para>
/// These exist because a clip export produced N identical frames: every frame after the
/// first was a byte-for-byte copy of frame 0, so an animated document exported as a still.
/// </para>
/// <para>
/// The cause is entirely in the gate's shape. <c>RendererGL.TotalIterations</c> is reset
/// INSIDE <c>DispatchCompute</c> — the invalidation flags raised by <c>LoadParams</c> only
/// take effect once a dispatch actually runs (RendererGL.cs:420-432). A gate that tests the
/// level BEFORE dispatching therefore reads the previous frame's exhausted counter, skips
/// the loop entirely, and tonemaps the stale histogram. At least one dispatch per frame is
/// what performs the reset, so the gate must be do-while, not while.
/// </para>
/// <para>
/// <see cref="FakeEngine"/> models exactly that contract and nothing else.
/// </para>
/// </remarks>
public class FrameAccumulationTests
{
/// <summary>
/// Models RendererGL's accumulation contract: an invalidated histogram is only cleared
/// (and the counter only zeroed) by a dispatch, never by the invalidation itself.
/// </summary>
private sealed class FakeEngine
{
private const int PerDispatch = 1;
private bool _invalidated = true;
public int TotalIterations { get; private set; }
public int Dispatches { get; private set; }
/// <summary>Stands in for LoadParams / SetHistogramScale — raises the flag only.</summary>
public void Invalidate() => _invalidated = true;
public void DispatchCompute()
{
if (_invalidated)
{
_invalidated = false;
TotalIterations = 0;
}
TotalIterations += PerDispatch;
Dispatches++;
}
public int Level() => TotalIterations;
}
[Fact]
public void EveryFrameDispatchesAfterInvalidation()
{
const int target = 3;
var engine = new FakeEngine();
var perFrame = new List<int>();
for (int frame = 0; frame < 4; frame++)
{
engine.Invalidate();
int before = engine.Dispatches;
RenderService.AccumulateToLevel(target, engine.Level, engine.DispatchCompute);
perFrame.Add(engine.Dispatches - before);
}
// Frame 0 accumulates; every later frame must do the same work, not zero.
Assert.Equal(new[] { target, target, target, target }, perFrame);
}
[Fact]
public void FrameIsAccumulatedFromScratchNotInheritedFromThePrevious()
{
const int target = 3;
var engine = new FakeEngine();
engine.Invalidate();
RenderService.AccumulateToLevel(target, engine.Level, engine.DispatchCompute);
engine.Invalidate();
RenderService.AccumulateToLevel(target, engine.Level, engine.DispatchCompute);
// The second frame's histogram was cleared and refilled, so the counter reflects
// one frame's worth of samples - not two frames stacked, and not zero dispatches.
Assert.Equal(target, engine.TotalIterations);
Assert.Equal(target * 2, engine.Dispatches);
}
[Fact]
public void ProgressIsReportedForEachLevelReached()
{
var engine = new FakeEngine();
var reported = new List<int>();
engine.Invalidate();
RenderService.AccumulateToLevel(3, engine.Level, engine.DispatchCompute, reported.Add);
Assert.Equal(new[] { 1, 2, 3 }, reported);
}
}