OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / AvaloniaShell.Tests / FrameAccumulationTests.cs

109 lines · csharp ref:
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);
    }
}