Editor/Gpu/OmegaGpuModel.cs
// C# adaptation of VGGT-Omega's image and alternating encoders. See LICENSE-VGGT and THIRD_PARTY_NOTICES.md.
// Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Sandbox;
using HumanoidMocapX.TensorInference;

namespace HumanoidMocapX.Inference;

/// <summary>Camera-only Omega inference: 24 image blocks, 24 alternating frame/global
/// pairs and the four-block camera head. Loads one RGB image at a time and retains
/// features on the GPU. Create, run and dispose on the main thread.</summary>
internal sealed class OmegaGpuModel : IDisposable
{
    readonly FlowGpuStorage storage = new();
    readonly OmegaShape shape;
    readonly OmegaGpuWorkspace workspace;
    readonly OmegaGpuBlock[] frames = new OmegaGpuBlock[OmegaShape.Depth], interFrames = new OmegaGpuBlock[OmegaShape.Depth];
    readonly TensorBatch batch;
    readonly GpuBuffer<float> image, patches, features, registers, finalFrame, finalTokens;
    // Set by the build steps, so a model can be built part by part (LoadAsync).
    DinoGpuEncoder encoder;
    OmegaGpuCameraHead camera;
    GpuBuffer<float> framePositions, cameraToken, registerToken;
    readonly GpuBuffer<uint> frameRanges, globalRanges, registerRanges;
    readonly float[] input;
    readonly float[] fence = new float[1];
    readonly Action normalizeImage, recordImage, recordLayer;
    CancellationToken cancellation;
    int frame, layer;
    bool running, disposed;

    const string FrameBlocks = "aggregator.frame_blocks.", InterFrameBlocks = "aggregator.inter_frame_blocks.";
    static readonly string[] ImageParts = ["aggregator.patch_embed.", "aggregator.camera_token", "aggregator.register_token", "aggregator.rope_embed."];

    /// <summary>Builds the whole model from fully loaded weights.</summary>
    internal OmegaGpuModel(FlowWeights weights, OmegaShape shape) : this(shape)
    {
        try { BuildImage(weights); BuildBlocks(weights, frames, FrameBlocks); BuildBlocks(weights, interFrames, InterFrameBlocks); BuildHead(weights); }
        catch { Dispose(); throw; }
    }

    /// <summary>Builds the same model in four parts (image encoder, frame blocks, global blocks,
    /// camera head). <paramref name="load"/> reads one part's weights by name prefix and runs off
    /// the editor thread; each part is dropped once uploaded, so only one part's CPU weights is live
    /// at a time instead of the whole checkpoint (when they are collected is up to the GC).
    /// Failures and cancellation release everything built.</summary>
    internal static async Task<OmegaGpuModel> LoadAsync(Func<string[], FlowWeights> load, OmegaShape shape, CancellationToken token)
    {
        ThreadSafe.AssertIsMainThread(); ArgumentNullException.ThrowIfNull(load);
        token.ThrowIfCancellationRequested();
        var model = new OmegaGpuModel(shape);
        try
        {
            model.BuildImage(await Part(ImageParts));
            model.BuildBlocks(await Part([FrameBlocks]), model.frames, FrameBlocks);
            model.BuildBlocks(await Part([InterFrameBlocks]), model.interFrames, InterFrameBlocks);
            model.BuildHead(await Part(["camera_head."]));
            return model;
        }
        catch { await GameTask.MainThread(); model.Dispose(); throw; }

        async Task<FlowWeights> Part(string[] prefixes)
        {
            var part = await Task.Run(() => load(prefixes), token);
            await GameTask.MainThread(); token.ThrowIfCancellationRequested();
            return part;
        }
    }

    // Everything that needs no weights: shared GPU buffers, attention ranges and callbacks.
    OmegaGpuModel(OmegaShape shape)
    {
        ThreadSafe.AssertIsMainThread();
        if (shape.Frames == 0 || shape.Channels / shape.Heads != 64) throw new ArgumentException("GPU Omega image heads must have 64 channels.");
        this.shape = shape; var width = shape.Channels;
        input = new float[shape.ImageElements];
        try
        {
            workspace = new(storage, shape.Tokens, width);
            image = storage.Create<float>(input.Length); patches = storage.Create<float>(shape.Patches * width);
            features = storage.Create<float>(shape.Tokens * width);
            registers = storage.Create<float>(shape.Frames * OmegaShape.FramePrefix * width);
            finalFrame = storage.Create<float>(registers.ElementCount); finalTokens = storage.Create<float>(shape.CameraFeatures);
            frameRanges = Ranges(shape.Frames, shape.FrameTokens, true);
            globalRanges = Ranges(shape.Frames, shape.FrameTokens, false); registerRanges = Ranges(shape.Frames, OmegaShape.FramePrefix, false);
            normalizeImage = NormalizeImage; recordImage = RecordImage; recordLayer = RecordLayer;
            batch = new();
        }
        catch { storage.Dispose(); throw; }
    }

    void BuildImage(FlowWeights weights)
    {
        var width = shape.Channels;
        encoder = new(storage, weights, DinoShape.Omega(shape));
        cameraToken = storage.Weight(weights, "aggregator.camera_token", width, 1, 2, 1);
        registerToken = storage.Weight(weights, "aggregator.register_token", width, 16, 2, 1);
        framePositions = storage.Upload<float>(OmegaPosition.Factors(shape.Height / 16, shape.Width / 16, OmegaShape.FramePrefix,
            weights.Get("aggregator.rope_embed.periods", width / shape.Heads / 4).Data));
    }

    void BuildBlocks(FlowWeights weights, OmegaGpuBlock[] blocks, string prefix)
    {
        for (var i = 0; i < OmegaShape.Depth; i++) blocks[i] = new(storage, weights, prefix + i, shape.Channels, shape.Heads, true);
    }

    void BuildHead(FlowWeights weights) => camera = new(weights, shape.Frames, shape.Channels * 2, shape.Heads);

    /// <summary>Calls readImage for each sampled frame with a reusable HWC RGB [0,1]
    /// destination. The callback must fill the entire image before returning. Writes
    /// [frames,9] camera poses. Discard output on failure; instances allow one call at a time.</summary>
    internal async Task RunAsync(Func<int, Memory<float>, CancellationToken, Task> readImage, Memory<float> poses, CancellationToken token = default)
    {
        ThreadSafe.AssertIsMainThread(); ObjectDisposedException.ThrowIf(disposed, this);
        ArgumentNullException.ThrowIfNull(readImage);
        if (running) throw new InvalidOperationException("Await the current Omega inference before starting another.");
        if (poses.Length != shape.Frames * 9) throw new ArgumentException("Omega output needs nine camera values per frame.", nameof(poses));
        token.ThrowIfCancellationRequested(); running = true; cancellation = token;
        try
        {
            for (frame = 0; frame < shape.Frames; frame++)
            {
                token.ThrowIfCancellationRequested();
                // A partial reader must fail instead of reusing pixels from the preceding frame.
                input.AsSpan().Fill(float.NaN);
                await readImage(frame, input.AsMemory(), token);
                await Task.Run(normalizeImage, token);
                await GameTask.MainThread();
                image.SetData<float>(input);
                await batch.RunAsync(recordImage, features, fence, token);
                await GameTask.MainThread();
            }
            for (layer = 0; layer < OmegaShape.Depth; layer++)
            {
                token.ThrowIfCancellationRequested();
                await batch.RunAsync(recordLayer, features, fence, token);
                await GameTask.MainThread();
            }
            await camera.RunGpuAsync(finalTokens, poses, token);
        }
        finally { await GameTask.MainThread(); running = false; cancellation = default; }
    }

    void NormalizeImage() => DinoImageEncoder.NormalizeImage(input, input);

    void RecordImage()
    {
        var width = shape.Channels;
        encoder.Run(workspace, image, patches, cancellation);
        var start = frame * shape.FrameTokens * width;
        cameraToken.CopyTo(features, (frame == 0 ? 0 : 1) * width, start, width);
        registerToken.CopyTo(features, (frame == 0 ? 0 : 1) * 16 * width, start + width, 16 * width);
        patches.CopyTo(features, 0, start + OmegaShape.FramePrefix * width, shape.Patches * width);
    }

    void RecordLayer()
    {
        cancellation.ThrowIfCancellationRequested();
        frames[layer].Run(workspace, features, frameRanges, shape.Tokens, framePositions, shape.FrameTokens);
        if (layer == OmegaShape.Depth - 1) Gather(finalFrame);
        if (OmegaShape.RegistersOnly(layer))
        {
            Gather(registers); interFrames[layer].Run(workspace, registers, registerRanges, shape.Frames * OmegaShape.FramePrefix, sharedKeyRanges: true);
            for (var f = 0; f < shape.Frames; f++)
                registers.CopyTo(features, f * OmegaShape.FramePrefix * shape.Channels, f * shape.FrameTokens * shape.Channels, OmegaShape.FramePrefix * shape.Channels);
        }
        else interFrames[layer].Run(workspace, features, globalRanges, shape.Tokens, sharedKeyRanges: true);
        if (layer != OmegaShape.Depth - 1) return;
        Gather(registers);
        workspace.Kernels.ConcatColumns(finalFrame, registers, finalTokens, shape.Frames * OmegaShape.FramePrefix, shape.Channels, shape.Channels);
    }

    void Gather(GpuBuffer<float> destination)
    {
        for (var f = 0; f < shape.Frames; f++)
            features.CopyTo(destination, f * shape.FrameTokens * shape.Channels, f * OmegaShape.FramePrefix * shape.Channels, OmegaShape.FramePrefix * shape.Channels);
    }

    GpuBuffer<uint> Ranges(int count, int tokens, bool withinFrame)
    {
        var values = new uint[count * tokens * 4];
        for (var row = 0; row < count * tokens; row++)
        { values[row * 4] = withinFrame ? (uint)(row / tokens * tokens) : 0; values[row * 4 + 1] = (uint)(withinFrame ? tokens : count * tokens); }
        return storage.Upload<uint>(values);
    }

    public void Dispose()
    {
        ThreadSafe.AssertIsMainThread();
        if (disposed) return;
        if (running) throw new InvalidOperationException("Await Omega inference, including cancelled GPU work, before disposal.");
        disposed = true; batch?.Dispose(); camera?.Dispose(); storage.Dispose();
    }
}