using System.Text.Json; using AcDream.App.Rendering; using AcDream.App.Rendering.Residency; using AcDream.App.Rendering.Scene; using AcDream.App.Streaming; using AcDream.App.UI.Testing; using AcDream.Core.Physics; using AcDream.Runtime; using AcDream.Runtime.World; namespace AcDream.App.Diagnostics; internal static class AutomationArtifactName { public static bool TryValidate(string? name, out string error) { if (string.IsNullOrWhiteSpace(name) || name.Length > 80) { error = "artifact name must contain 1-80 characters"; return false; } foreach (char c in name) { if (!char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_') { error = $"artifact name '{name}' may contain only letters, digits, '-' and '_'"; return false; } } error = string.Empty; return true; } } internal sealed record WorldLifecycleResourceSnapshot( int LoadedLandblocks, int WorldEntities, int AnimatedEntities, int VisibleLandblocks, int TotalLandblocks, int LiveEntities, int MaterializedLiveEntities, CurrentRenderSceneOracleSnapshot RenderSceneOracle, RenderSceneShadowComparisonSnapshot RenderSceneShadow, RenderFrameProductComparisonSnapshot RenderFrameProduct, int PendingLiveTeardowns, int PendingLandblockRetirements, int ParticleEmitters, int Particles, int ParticleBindings, int ParticleOwners, int EffectOwners, int LightOwners, int ScriptOwners, int ActiveScripts, int MeshRenderData, int MeshAtlasArrays, long MeshEstimatedBytes, int StagedMeshUploads, long StagedMeshBytes, long TrackedGpuBytes, long ResidencyGpuBytes, long GpuTrackerMinusResidencyBytes, int TrackedGpuBuffers, int TrackedGpuTextures, int OwnedCompositeTextures, int CompositeTextureOwners, int ActiveParticleTextures, int ParticleTextureOwners, int CompositeWarmupPending, long ManagedBytes, long ManagedCommittedBytes, long LohSizeBytes, long LohFragmentationBytes, long ProcessTotalAllocatedBytes, long CpuMeshCacheHits, long CpuMeshCacheMisses, long CpuMeshCacheEvictions, long DecodedTextureCacheHits, long DecodedTextureCacheMisses, long DecodedTextureCacheEvictions, long DatObjectCacheHits, long DatObjectCacheMisses, long DatObjectCacheEvictions, int PhysicsGraphGfxObjs, int PhysicsGraphSetups, int PhysicsGraphCells, int PhysicsFlatGfxObjs, int PhysicsFlatSetups, int PhysicsFlatCells, int PhysicsFlatEnvCells, CollisionShadowStats CollisionShadow, StreamingWorkDiagnostics StreamingWork, ResidencySnapshot Residency, double Fps, double FrameMilliseconds, string? LastFrameProfile); internal sealed record WorldLifecycleCheckpoint( int Sequence, string Name, DateTime TimestampUtc, int ProcessId, RuntimePortalSnapshot Reveal, RuntimeWorldEnvironmentOwnershipSnapshot EnvironmentOwnership, RuntimeWorldTransitOwnershipSnapshot TransitOwnership, RenderFrameOutcome Render, WorldLifecycleResourceSnapshot Resources); internal sealed class WorldLifecycleCheckpointRequest : IRetailUiAutomationCheckpoint { private readonly object _sync = new(); private RetailUiAutomationCheckpointStatus _status = RetailUiAutomationCheckpointStatus.Pending; private string? _error; public WorldLifecycleCheckpointRequest(object owner, int sequence, string name) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); Sequence = sequence; Name = name ?? throw new ArgumentNullException(nameof(name)); } internal object Owner { get; } public int Sequence { get; } public string Name { get; } public RetailUiAutomationCheckpointStatus Status { get { lock (_sync) return _status; } } public string? Error { get { lock (_sync) return _error; } } internal bool TrySetTerminal( RetailUiAutomationCheckpointStatus status, string? error) { if (status == RetailUiAutomationCheckpointStatus.Pending) throw new ArgumentOutOfRangeException(nameof(status)); lock (_sync) { if (_status != RetailUiAutomationCheckpointStatus.Pending) return false; _status = status; _error = error; return true; } } } /// /// Diagnostic-only runtime seam used by production retained-UI scripts. It /// writes one structured checkpoint at explicit script edges and delegates GL /// capture to the render-thread screenshot owner. /// internal sealed class WorldLifecycleAutomationController : IRetailUiAutomationRuntime, IRenderFramePostDiagnosticsPhase, IDisposable { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; private readonly Func _getReveal; private readonly Func _getEnvironmentOwnership; private readonly Func _getTransitOwnership; private readonly Func _getPortalMaterializationCount; private readonly Func _captureResources; private readonly FrameScreenshotController _screenshots; private readonly string _artifactDirectory; private readonly Action _log; private readonly object _requestOwner = new(); private readonly object _sync = new(); private readonly Queue _requests = []; private int _sequence; private bool _disposed; public WorldLifecycleAutomationController( Func getReveal, Func getEnvironmentOwnership, Func getTransitOwnership, Func getPortalMaterializationCount, Func captureResources, FrameScreenshotController screenshots, string artifactDirectory, Action? log = null) { _getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal)); _getEnvironmentOwnership = getEnvironmentOwnership ?? throw new ArgumentNullException( nameof(getEnvironmentOwnership)); _getTransitOwnership = getTransitOwnership ?? throw new ArgumentNullException(nameof(getTransitOwnership)); _getPortalMaterializationCount = getPortalMaterializationCount ?? throw new ArgumentNullException(nameof(getPortalMaterializationCount)); _captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources)); _screenshots = screenshots ?? throw new ArgumentNullException(nameof(screenshots)); _artifactDirectory = string.IsNullOrWhiteSpace(artifactDirectory) ? throw new ArgumentException("An automation artifact directory is required.", nameof(artifactDirectory)) : Path.GetFullPath(artifactDirectory); _log = log ?? (_ => { }); } public bool IsWorldReady => _getReveal().IsReady; public bool IsWorldViewportVisible => _getReveal().WorldViewportObserved; public int PortalMaterializationCount => _getPortalMaterializationCount(); public bool TryRequestCheckpoint( string name, out IRetailUiAutomationCheckpoint? checkpoint, out string error) { checkpoint = null; if (!AutomationArtifactName.TryValidate(name, out error)) return false; lock (_sync) { if (_disposed) { error = $"checkpoint '{name}' was rejected because world lifecycle automation is shutting down"; return false; } var request = new WorldLifecycleCheckpointRequest( _requestOwner, checked(++_sequence), name); _requests.Enqueue(request); checkpoint = request; error = string.Empty; return true; } } public void CancelCheckpoint(IRetailUiAutomationCheckpoint checkpoint) { ArgumentNullException.ThrowIfNull(checkpoint); if (checkpoint is not WorldLifecycleCheckpointRequest request || !ReferenceEquals(request.Owner, _requestOwner)) { throw new ArgumentException( "The checkpoint acknowledgement is not owned by this automation runtime.", nameof(checkpoint)); } lock (_sync) { request.TrySetTerminal( RetailUiAutomationCheckpointStatus.Cancelled, $"checkpoint '{request.Name}' was cancelled before render-frame capture"); } } public void Process(RenderFrameInput input, RenderFrameOutcome outcome) { lock (_sync) { if (_disposed) return; while (_requests.Count != 0) { WorldLifecycleCheckpointRequest request = _requests.Dequeue(); if (request.Status != RetailUiAutomationCheckpointStatus.Pending) continue; WriteCheckpoint(request, outcome); } } } private void WriteCheckpoint( WorldLifecycleCheckpointRequest request, RenderFrameOutcome outcome) { try { Directory.CreateDirectory(_artifactDirectory); var checkpoint = new WorldLifecycleCheckpoint( Sequence: request.Sequence, Name: request.Name, TimestampUtc: DateTime.UtcNow, ProcessId: Environment.ProcessId, Reveal: _getReveal(), EnvironmentOwnership: _getEnvironmentOwnership(), TransitOwnership: _getTransitOwnership(), Render: outcome, Resources: _captureResources(outcome)); string json = JsonSerializer.Serialize(checkpoint, JsonOptions); string timelinePath = Path.Combine( _artifactDirectory, "world-lifecycle.checkpoints.jsonl"); File.WriteAllText( Path.Combine( _artifactDirectory, $"checkpoint-{request.Name}.json"), json + Environment.NewLine); // Timeline publication is the commit edge: a visible row always // has its complete named checkpoint artifact beside it. File.AppendAllText(timelinePath, json + Environment.NewLine); request.TrySetTerminal( RetailUiAutomationCheckpointStatus.Succeeded, error: null); _log( $"[world-gate] checkpoint name={request.Name} " + $"sequence={request.Sequence} path={timelinePath}"); } catch (Exception exception) { request.TrySetTerminal( RetailUiAutomationCheckpointStatus.Failed, $"checkpoint '{request.Name}' sequence {request.Sequence} failed: " + exception.Message); } } public void Dispose() { lock (_sync) { if (_disposed) return; _disposed = true; while (_requests.Count != 0) { WorldLifecycleCheckpointRequest request = _requests.Dequeue(); request.TrySetTerminal( RetailUiAutomationCheckpointStatus.Cancelled, $"checkpoint '{request.Name}' was cancelled during world lifecycle automation shutdown"); } } } public bool TryRequestScreenshot(string name, out string error) => _screenshots.TryRequest(name, out error); public bool IsScreenshotComplete(string name) => _screenshots.IsComplete(name); }