Editor/Server/EditorHost.cs
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Editor;
using HumanoidMocapX.EditorTools.Video;
using HumanoidMocapX;
using HumanoidMocapX.EditorTools;
using Sandbox;
namespace HumanoidMocapHost;
/// <summary>Opt-in processing host inside the s&box editor. Operator configuration
/// comes from MOCAP_* environment variables; see docs/editor-host.md.</summary>
public static class EditorHost
{
// The engine exposes static console/event entry points; session state stays here.
static readonly SemaphoreSlim Changes = new(1, 1);
static EditorHostLifetime active;
static CancellationTokenSource starting;
/// <summary>Verifies installed Base/Latest weights, then listens on loopback. Requires
/// Windows Media Foundation and a GPU budget. Startup failures propagate to the console.</summary>
[ConCmd("mocap_x_host_start", ConVarFlags.Protected, Help = "Start the configured in-editor Humanoid Mocap X processing host")]
public static async Task StartAsync()
{
await Changes.WaitAsync();
using var cancellation = new CancellationTokenSource();
starting = cancellation;
try
{
if (active is { IsRunning: true }) { Log.Info("Humanoid Mocap X host is already running."); return; }
if (active is not null) { await active.StopAsync(); active = null; }
if (LocalCapture.Availability is { } reason) throw new PlatformNotSupportedException(reason);
var config = HostConfiguration.Read(Environment.GetEnvironmentVariable,
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "HumanoidMocapX"));
await ModelStore.InstalledAsync(config.Models, FlowModel.Base, message => Log.Info(message), cancellation.Token);
await ModelStore.InstalledAsync(config.Models, FlowModel.Latest, message => Log.Info(message), cancellation.Token);
await GameTask.MainThread(); cancellation.Token.ThrowIfCancellationRequested();
if (!Graphics.IsAvailable || Graphics.VideoMemoryBudget == 0 && Sandbox.Engine.SystemInfo.GpuMemory == 0)
throw new InvalidOperationException("The host needs an initialized GPU with a reported video-memory budget.");
active = Start(config);
Log.Info($"Humanoid Mocap X host listening on http://127.0.0.1:{config.Port}/. Stop with mocap_x_host_stop.");
}
finally { starting = null; Changes.Release(); }
}
static EditorHostLifetime Start(HostConfiguration config)
{
var lanes = config.Lanes == CaptureLanes.Auto ? CaptureLanes.Maximum : config.Lanes;
var admission = new GpuAdmission(lanes);
admission.Update(Graphics.VideoMemoryBudget, Graphics.VideoMemoryUsed, Sandbox.Engine.SystemInfo.GpuMemory);
var storage = new FileJobStorage(config.Data);
try
{
var processor = new CaptureProcessor(storage.CaptureDirectory, config.UploadBytes,
(selected, workspace, progress) => new LocalCapture(new CaptureOptions
{ Model = selected.Model, CameraInterval = selected.CameraInterval, Lanes = lanes }, config.Models, workspace, progress),
ReadDurationAsync);
var queue = new CaptureQueue(storage, processor, TimeProvider.System, config.ActivePerOwner,
maximumUploadBytes: config.UploadBytes, maximumClipSeconds: config.ClipSeconds,
concurrentJobs: config.ConcurrentJobs, canStartJob: admission.CanStart);
var session = new HostSession($"http://127.0.0.1:{config.Port}/", config.Keys, storage, queue, ReportError);
return new EditorHostLifetime(session, admission);
}
catch { storage.Dispose(); throw; }
}
static Task<double> ReadDurationAsync(string video, CancellationToken token) => Task.Run(() =>
{
token.ThrowIfCancellationRequested();
using var decoder = IVideoDecoder.Open(video);
return decoder.Duration;
}, token);
/// <summary>Cancels startup, uploads and active captures, then waits for GPU cleanup.
/// Repeated calls are safe; completed job IDs are invalidated by the next start.</summary>
[ConCmd("mocap_x_host_stop", ConVarFlags.Protected, Help = "Stop and drain the in-editor Humanoid Mocap X processing host")]
public static async Task StopAsync()
{
try { starting?.Cancel(); } catch (ObjectDisposedException) { } // Startup just finished.
await Changes.WaitAsync();
try
{
await GameTask.MainThread();
if (active is not null) { await active.StopAsync(); active = null; }
}
finally { Changes.Release(); }
}
[EditorEvent.Hotload]
static void Hotloaded() => _ = StopAfterHotloadAsync();
static async Task StopAfterHotloadAsync()
{
try { await StopAsync(); }
catch (Exception error) { ReportError(error); }
}
static void ReportError(Exception error) => Log.Error($"Humanoid Mocap X host: {error}");
}