Code/CaptureLanes.cs
using System;

namespace HumanoidMocapX;

/// <summary>Chooses how many frames local capture processes at once. Each extra lane lets
/// one frame's CPU work overlap another's GPU work, and costs a little more video memory.</summary>
public static class CaptureLanes
{
    /// <summary>Largest lane count offered; requests are also capped by the GPU memory budget.</summary>
    public const int Maximum = 6;
    /// <summary>Requested value meaning "as many as this GPU allows".</summary>
    public const int Auto = 0;
    // Measured: shared SAM weights plus one lane used ~3.2 GB and each extra lane ~0.15 GB,
    // so these reserves leave headroom for the editor and other applications.
    const ulong FirstLaneBytes = 4UL << 30, ExtraLaneBytes = 512UL << 20;

    /// <summary>Lanes this GPU can safely run, from the OS video memory budget and the
    /// memory already in use. An unknown budget (0) allows a single lane.</summary>
    public static int Capacity(ulong budgetBytes, ulong usedBytes)
    {
        if (budgetBytes <= usedBytes) return 1;
        var free = budgetBytes - usedBytes;
        if (free <= FirstLaneBytes) return 1;
        return (int)Math.Min(Maximum, 1 + (free - FirstLaneBytes) / ExtraLaneBytes);
    }

    /// <summary>Applies the user's choice: Auto takes the capacity; a fixed count is capped
    /// to it. Rejects requests outside Auto or 1 to <see cref="Maximum"/>.</summary>
    public static int Resolve(int requested, ulong budgetBytes, ulong usedBytes)
    {
        Validate(requested);
        var capacity = Capacity(budgetBytes, usedBytes);
        return requested == Auto ? capacity : Math.Min(requested, capacity);
    }

    internal static void Validate(int requested)
    {
        if (requested is < Auto or > Maximum)
            throw new ArgumentOutOfRangeException(nameof(requested), $"Lanes must be Auto (0) or 1 to {Maximum}.");
    }
}