OMRVisualFoundation.AdaptivePixelation.cs
using Sandbox;
using System;

/// <summary>
/// Phase 8F3F2 depth-adaptive + edge-aware world pixelation controls.
///
/// Adaptive pixel sizes are authored as multipliers of the existing world
/// PixelateScale. This keeps Native vs Adaptive comparisons meaningful:
/// a multiplier of 1.0 matches the legacy/native-style world pixel block,
/// values above 1.0 are chunkier, and values below 1.0 are finer.
///
/// The profile supports 2-6 distance anchors. Only the first Point Count
/// anchors are evaluated; the remaining authored values are preserved so the
/// profile can be expanded/collapsed without losing tuning.
/// </summary>
public sealed partial class OMRVisualFoundation
{
    public enum AdaptivePixelationDebugMode
    {
        Off = 0,
        Depth = 1,
        PixelBands = 2,
        EdgeProtection = 3,
        Unprotected = 4,
        PassCheck = 5
    }

    public enum AdaptivePixelationInterpolationMode
    {
        Step = 0,
        Linear = 1,
        Smooth = 2
    }

    [Property, Group( "World Geometry/Adaptive Pixelation" ), Title( "Enable Adaptive Pixelation" )]
    [Description( "Depth-adaptive world-only pixelation with depth/normal edge protection. Runs before Edge Cues and does not replace the existing Pixelate path." )]
    public bool EnableAdaptivePixelation { get; set; } = false;

    [Property, Group( "World Geometry/Adaptive Pixelation" ), Range( 0f, 1f ), Title( "Blend" )]
    public float AdaptivePixelationBlend { get; set; } = 1f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile" ), Range( 2, 6 ), Title( "Point Count" )]
    [Description( "How many distance/size anchors are active. The first 2-6 profile points below are used in order." )]
    public int AdaptivePixelationPointCount { get; set; } = 3;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile" ), Title( "Interpolation" )]
    [Description( "Step keeps hard distance zones. Linear blends directly. Smooth eases between anchors and is the recommended default." )]
    public AdaptivePixelationInterpolationMode AdaptivePixelationInterpolation { get; set; } = AdaptivePixelationInterpolationMode.Smooth;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 1" ), Range( 0f, 8192f ), Title( "Distance" )]
    public float AdaptivePixelationPoint1Distance { get; set; } = 0f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 1" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
    [Description( "Multiplier relative to the existing World Pixelate Scale. 2.0 = twice as chunky as Native at this distance." )]
    public float AdaptivePixelationPoint1Multiplier { get; set; } = 2f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 2" ), Range( 1f, 8192f ), Title( "Distance" )]
    public float AdaptivePixelationPoint2Distance { get; set; } = 560f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 2" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
    [Description( "1.0 matches the existing Native/legacy world pixel block at this distance." )]
    public float AdaptivePixelationPoint2Multiplier { get; set; } = 1f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 3" ), Range( 2f, 8192f ), Title( "Distance" )]
    public float AdaptivePixelationPoint3Distance { get; set; } = 1000f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 3" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
    [Description( "0.5 targets half the Native block size. The final screen-space block cannot become smaller than one physical framebuffer pixel." )]
    public float AdaptivePixelationPoint3Multiplier { get; set; } = 0.5f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 4" ), Range( 3f, 8192f ), Title( "Distance" )]
    public float AdaptivePixelationPoint4Distance { get; set; } = 1400f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 4" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
    public float AdaptivePixelationPoint4Multiplier { get; set; } = 0.4f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 5" ), Range( 4f, 8192f ), Title( "Distance" )]
    public float AdaptivePixelationPoint5Distance { get; set; } = 2000f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 5" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
    public float AdaptivePixelationPoint5Multiplier { get; set; } = 0.3f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 6" ), Range( 5f, 8192f ), Title( "Distance" )]
    public float AdaptivePixelationPoint6Distance { get; set; } = 3000f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Profile/Point 6" ), Range( 0.1f, 4f ), Title( "Size Multiplier" )]
    public float AdaptivePixelationPoint6Multiplier { get; set; } = 0.25f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0.001f, 0.5f ), Title( "Depth Threshold" )]
    [Description( "Relative depth difference that starts protecting a pixel block from sampling across another depth layer." )]
    public float AdaptivePixelationDepthThreshold { get; set; } = 0.035f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Title( "Use Normal Protection" )]
    [Description( "Also rejects coarse samples that cross a strong G-buffer normal discontinuity." )]
    public bool AdaptivePixelationUseNormalProtection { get; set; } = true;

    [Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0.001f, 1f ), Title( "Normal Threshold" )]
    public float AdaptivePixelationNormalThreshold { get; set; } = 0.16f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0f, 1f ), Title( "Protection Strength" )]
    [Description( "0 ignores detected boundaries. 1 fully prefers the safer fallback sample around protected edges." )]
    public float AdaptivePixelationProtectionStrength { get; set; } = 1f;

    [Property, Group( "World Geometry/Adaptive Pixelation/Edge Protection" ), Range( 0.1f, 1f ), Title( "Fallback Block Scale" )]
    [Description( "When a coarse block crosses an edge, retry using this fraction of the current block size before falling back to the original pixel." )]
    public float AdaptivePixelationFallbackScale { get; set; } = 0.5f;

    [Property, Group( "World Geometry/Adaptive Pixelation" ), Title( "Pixelate Sky" )]
    [Description( "When disabled, pixels without opaque scene depth keep the clean sky. Recommended for Standard." )]
    public bool AdaptivePixelationPixelateSky { get; set; } = false;

    [Property, Group( "World Geometry/Adaptive Pixelation" ), Title( "Debug View" )]
    public AdaptivePixelationDebugMode AdaptivePixelationDebug { get; set; } = AdaptivePixelationDebugMode.Off;

    [Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Use Native Pixelate" )]
    public void UseNativePixelationForComparison()
    {
        EnablePixelate = true;
        EnableAdaptivePixelation = false;
        RefreshPixelationComparison();
    }

    [Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Use Adaptive Pixelate" )]
    public void UseAdaptivePixelationForComparison()
    {
        EnablePixelate = false;
        EnableAdaptivePixelation = true;
        RefreshPixelationComparison();
    }

    [Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Use Both" )]
    public void UseBothPixelationForComparison()
    {
        EnablePixelate = true;
        EnableAdaptivePixelation = true;
        RefreshPixelationComparison();
    }

    [Button, Group( "World Geometry/Adaptive Pixelation/A-B" ), Title( "Disable Both" )]
    public void DisableBothPixelationForComparison()
    {
        EnablePixelate = false;
        EnableAdaptivePixelation = false;
        RefreshPixelationComparison();
    }

    [Button, Group( "World Geometry/Adaptive Pixelation" ), Title( "Reset Adaptive Pixelation" )]
    public void ResetAdaptivePixelation()
    {
        EnableAdaptivePixelation = false;
        AdaptivePixelationBlend = 1f;
        AdaptivePixelationPointCount = 3;
        AdaptivePixelationInterpolation = AdaptivePixelationInterpolationMode.Smooth;

        AdaptivePixelationPoint1Distance = 0f;
        AdaptivePixelationPoint1Multiplier = 2f;
        AdaptivePixelationPoint2Distance = 560f;
        AdaptivePixelationPoint2Multiplier = 1f;
        AdaptivePixelationPoint3Distance = 1000f;
        AdaptivePixelationPoint3Multiplier = 0.5f;
        AdaptivePixelationPoint4Distance = 1400f;
        AdaptivePixelationPoint4Multiplier = 0.4f;
        AdaptivePixelationPoint5Distance = 2000f;
        AdaptivePixelationPoint5Multiplier = 0.3f;
        AdaptivePixelationPoint6Distance = 3000f;
        AdaptivePixelationPoint6Multiplier = 0.25f;

        AdaptivePixelationDepthThreshold = 0.035f;
        AdaptivePixelationUseNormalProtection = true;
        AdaptivePixelationNormalThreshold = 0.16f;
        AdaptivePixelationProtectionStrength = 1f;
        AdaptivePixelationFallbackScale = 0.5f;
        AdaptivePixelationPixelateSky = false;
        AdaptivePixelationDebug = AdaptivePixelationDebugMode.Off;

        RebuildAdaptivePixelation();
    }

    private OMRAdaptivePixelationPostProcess _adaptivePixelationPostProcess;
    private bool _adaptivePixelationPostProcessCreatedByFoundation;
    private bool _adaptivePixelationReadyLogged;

    private void RefreshPixelationComparison()
    {
        ResolveNativeEffects();
        ApplySettings();
        RebuildAdaptivePixelation();
        RebuildRuntimeCommands();
    }

    private void RebuildAdaptivePixelation()
    {
        // Match Edge Cues: keep the authored scene clean in edit mode and create
        // only the runtime helper while playing.
        if ( !Game.IsPlaying )
        {
            DisableRuntimeAdaptivePixelation();
            return;
        }

        CameraComponent camera = GetComponent<CameraComponent>( true );
        if ( camera is null || !camera.IsValid )
            return;

        if ( _adaptivePixelationPostProcess is null || !_adaptivePixelationPostProcess.IsValid )
        {
            _adaptivePixelationPostProcess = GetComponent<OMRAdaptivePixelationPostProcess>( true );

            if ( _adaptivePixelationPostProcess is null || !_adaptivePixelationPostProcess.IsValid )
            {
                _adaptivePixelationPostProcess = GameObject.AddComponent<OMRAdaptivePixelationPostProcess>( false );
                _adaptivePixelationPostProcessCreatedByFoundation = true;
            }
        }

        if ( _adaptivePixelationPostProcess is null || !_adaptivePixelationPostProcess.IsValid )
            return;

        int pointCount = Math.Clamp( AdaptivePixelationPointCount, 2, 6 );

        float d1 = MathF.Max( 0f, AdaptivePixelationPoint1Distance );
        float d2 = MathF.Max( d1 + 1f, AdaptivePixelationPoint2Distance );
        float d3 = MathF.Max( d2 + 1f, AdaptivePixelationPoint3Distance );
        float d4 = MathF.Max( d3 + 1f, AdaptivePixelationPoint4Distance );
        float d5 = MathF.Max( d4 + 1f, AdaptivePixelationPoint5Distance );
        float d6 = MathF.Max( d5 + 1f, AdaptivePixelationPoint6Distance );

        _adaptivePixelationPostProcess.Blend = MathX.Clamp( AdaptivePixelationBlend, 0f, 1f );
        _adaptivePixelationPostProcess.BasePixelateScale = MathX.Clamp( PixelateScale, 0f, 1f );
        _adaptivePixelationPostProcess.PointCount = pointCount;
        _adaptivePixelationPostProcess.InterpolationMode = (int)AdaptivePixelationInterpolation;

        _adaptivePixelationPostProcess.Point1Distance = d1;
        _adaptivePixelationPostProcess.Point1Multiplier = MathX.Clamp( AdaptivePixelationPoint1Multiplier, 0.1f, 4f );
        _adaptivePixelationPostProcess.Point2Distance = d2;
        _adaptivePixelationPostProcess.Point2Multiplier = MathX.Clamp( AdaptivePixelationPoint2Multiplier, 0.1f, 4f );
        _adaptivePixelationPostProcess.Point3Distance = d3;
        _adaptivePixelationPostProcess.Point3Multiplier = MathX.Clamp( AdaptivePixelationPoint3Multiplier, 0.1f, 4f );
        _adaptivePixelationPostProcess.Point4Distance = d4;
        _adaptivePixelationPostProcess.Point4Multiplier = MathX.Clamp( AdaptivePixelationPoint4Multiplier, 0.1f, 4f );
        _adaptivePixelationPostProcess.Point5Distance = d5;
        _adaptivePixelationPostProcess.Point5Multiplier = MathX.Clamp( AdaptivePixelationPoint5Multiplier, 0.1f, 4f );
        _adaptivePixelationPostProcess.Point6Distance = d6;
        _adaptivePixelationPostProcess.Point6Multiplier = MathX.Clamp( AdaptivePixelationPoint6Multiplier, 0.1f, 4f );

        _adaptivePixelationPostProcess.DepthThreshold = MathX.Clamp( AdaptivePixelationDepthThreshold, 0.001f, 0.5f );
        _adaptivePixelationPostProcess.UseNormalProtection = AdaptivePixelationUseNormalProtection;
        _adaptivePixelationPostProcess.NormalThreshold = MathX.Clamp( AdaptivePixelationNormalThreshold, 0.001f, 1f );
        _adaptivePixelationPostProcess.ProtectionStrength = MathX.Clamp( AdaptivePixelationProtectionStrength, 0f, 1f );
        _adaptivePixelationPostProcess.FallbackScale = MathX.Clamp( AdaptivePixelationFallbackScale, 0.1f, 1f );
        _adaptivePixelationPostProcess.PixelateSky = AdaptivePixelationPixelateSky;
        _adaptivePixelationPostProcess.DebugMode = (int)AdaptivePixelationDebug;

        bool debugActive = AdaptivePixelationDebug != AdaptivePixelationDebugMode.Off;
        bool shouldRun = EnableAdaptivePixelation &&
            (_adaptivePixelationPostProcess.Blend > 0.0001f || debugActive);

        _adaptivePixelationPostProcess.Enabled = shouldRun;

        if ( shouldRun && LogSetup && !_adaptivePixelationReadyLogged )
        {
            _adaptivePixelationReadyLogged = true;

            float nativeBlock = 1f + 23f * _adaptivePixelationPostProcess.BasePixelateScale;
            Log.Info(
                $"[OMR VISUAL] Adaptive pixelation ready on '{GameObject.Name}' | " +
                $"CameraPostProcessing:{camera.EnablePostProcessing} | Debug:{AdaptivePixelationDebug} | " +
                $"Points:{pointCount} | NativeBase:{nativeBlock:0.##}px | " +
                $"Multipliers:{_adaptivePixelationPostProcess.Point1Multiplier:0.##}/" +
                $"{_adaptivePixelationPostProcess.Point2Multiplier:0.##}/" +
                $"{_adaptivePixelationPostProcess.Point3Multiplier:0.##}"
            );
        }

        if ( !shouldRun )
            _adaptivePixelationReadyLogged = false;
    }

    private void DisableRuntimeAdaptivePixelation()
    {
        if ( _adaptivePixelationPostProcess is not null && _adaptivePixelationPostProcess.IsValid )
            _adaptivePixelationPostProcess.Enabled = false;

        _adaptivePixelationReadyLogged = false;
    }

    private void ReleaseAdaptivePixelation()
    {
        if ( _adaptivePixelationPostProcess is not null && _adaptivePixelationPostProcess.IsValid )
        {
            _adaptivePixelationPostProcess.Enabled = false;

            if ( _adaptivePixelationPostProcessCreatedByFoundation )
                _adaptivePixelationPostProcess.Destroy();
        }

        _adaptivePixelationPostProcess = null;
        _adaptivePixelationPostProcessCreatedByFoundation = false;
        _adaptivePixelationReadyLogged = false;
    }
}