OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / IFSEngine / Rendering / RendererGL.cs

1170 lines · csharp ref:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

using IFSEngine.Model;
using IFSEngine.Rendering.GpuStructs;
using IFSEngine.Utility;

using Nito.AsyncEx;

using OpenTK.Graphics.OpenGL;
using OpenTK.Windowing.Common;

namespace IFSEngine.Rendering;

public sealed class RendererGL : IAsyncDisposable
{
    /// <summary>
    /// Invoked by the rendering thread. The frequency of updates is influenced by <see cref="EnablePerceptualUpdates"/>.
    /// </summary>
    public event EventHandler DisplayFramebufferUpdated;

    /// <summary>
    /// Invoked by the rendering thread when <see cref="TotalIterations"/> reaches the limit defined by <see cref="IFS.TargetIterationLevel"/>.
    /// </summary>
    public event EventHandler TargetIterationReached;

    public bool IsInitialized { get; private set; } = false;
    public bool IsRendering { get; private set; } = false;

    public bool UpdateDisplayOnRender { get; set; } = true;

    /// <summary>
    /// Enable perceptually equal difference between updates.
    /// </summary>
    public bool EnablePerceptualUpdates { get; set; } = true;

    /// <summary>
    /// Enable Density Estimation.
    /// </summary>
    public bool EnableDE { get; set; } = false;
    public int DEMaxRadius { get; set; } = 9;
    public double DEPower { get; set; } = 0.2;
    public double DEThreshold { get; set; } = 0.4;

    /// <summary>
    /// Number of dispatches since accumulation reset.
    /// This is needed for random generation.
    /// </summary>
    private int _dispatchCnt = 0;

    public int HistogramWidth { get; private set; } = 1920;
    public int HistogramHeight { get; private set; } = 1080;
    public int DisplayWidth { get; private set; } = 1280;
    public int DisplayHeight { get; private set; } = 720;

    private string _includesSource;
    private List<TransformPlugin> _registeredTransforms;
    private List<EffectPlugin> _registeredPostfxs;

    public IFS LoadedParams { get; private set; } = new IFS();
    private bool _invalidHistogramResolution = false;
    private bool _invalidHistogramBuffer = false;
    private bool _invalidParamsBuffer = false;
    private bool _invalidPaletteBuffer = false;
    private bool _invalidPointsStateBuffer = false;

    /// <summary>
    /// Number of workgroups to be dispatched. Each workgroup consists of 64 kernel invocations. Default value is 256.
    /// </summary>
    public int WorkgroupCount { get; private set; } = 256;
    public async Task SetWorkgroupCount(int s)
    {
        WorkgroupCount = s;
        await WithContext(() =>
        {
            GL.NamedBufferData(_pointsBufferHandle, InvocationCount * (4 * sizeof(float) + 2 * sizeof(float) + 2 * sizeof(int)), IntPtr.Zero, BufferUsageHint.StaticCopy);

            InvalidatePointsStateBuffer();
        });
    }

    private const int WorkgroupSize = 64;//nv:32, amd:64. Optimal is 64.

    public int InvocationCount => WorkgroupCount * WorkgroupSize;

    /// <summary>
    /// Number of iterations a single invocation performs.
    /// This value is adjusted after each dispatch in order to approach <see cref="TargetFramerate"/>
    /// </summary>
    public int InvocationIters { get; private set; } = 500;

    /// <summary>
    /// The render loop aims to reach the specified framerate by measuring compute kernel execution time and adjusting the workload size. Measured in Fps.
    /// </summary>
    public int TargetFramerate { get; set; } = 60;

    /// <summary>
    /// Total iterations since accumulation reset
    /// </summary>
    public ulong TotalIterations { get; private set; } = 0;

    /// <summary>
    /// Maximum radius of the spatial filter.
    /// Higher values are slow to render.
    /// </summary>
    public int MaxFilterRadius { get; set; } = 0;

    public double HistogramScale { get; private set; } = 1.0;

    /// <summary>
    /// Seed value used to generate random numbers. Recommended to change this for each animation frame.
    /// </summary>
    public uint Seed { get; set; } = 0;

    /// <summary>
    /// True when the level of <see cref="TotalIterations"/> is beyond <see cref="IFS.TargetIterationLevel"/>.
    /// Reset by <see cref="InvalidateHistogramBuffer"/>.
    /// </summary>
    public bool IsTargetIterationReached { get; private set; } = false;

    /// <summary>
    /// Whether to mark the area in focus with red. This is used as a visual aid when the user is changing the focus.
    /// Should be false when doing final render.
    /// </summary>
    public bool MarkAreaInFocus { get; set; } = false;

    /// <summary>
    /// Size of the color gradient sample buffer.
    /// </summary>
    public int GradientResolution { get; init; } = 256;

    private bool _updateDisplayNow = false;
    private readonly IGraphicsContext _ctx;

    private int _vertexShaderHandle;
    private int _vao;
    //compute shader handles
    private int _computeProgramHandle;
    private int _histogramBufferHandle;
    private int _settingsBufferHandle;
    private int _iteratorsBufferHandle;
    private int _aliasBufferHandle;
    private int _pointsBufferHandle;
    private int _colorGradientBufferHandle;
    private int _realParametersBufferHandle;
    private int _vec3ParametersBufferHandle;
    //fragment shader handles
    private int _tonemapProgramHandle;
    private int _deProgramHandle;
    private int _offscreenFBOHandle;
    private int _renderTextureHandle;
    //ping-pong FBOs for post-fx chaining
    private int _postFxFboA;
    private int _postFxTextureA;
    private int _postFxFboB;
    private int _postFxTextureB;
    private readonly List<int> _postFxProgramHandles = [];

    private readonly AsyncAutoResetEvent _stopRender = new(false);

    private readonly float[] _bufferClearColor = [0.0f, 0.0f, 0.0f, 1.0f]; //RGBA
    private readonly string _shadersPath = "IFSEngine.Rendering.Shaders.";
    private readonly bool _debugFlag = true;

    //https://gist.github.com/Vassalware/d47ff5e60580caf2cbbf0f31aa20af5d
    private static void DebugCallback(DebugSource source,
        DebugType type,
        int id,
        DebugSeverity severity,
        int length,
        IntPtr message,
        IntPtr userParam)
    {
        string messageString = Marshal.PtrToStringAnsi(message, length);

        System.Diagnostics.Debug.WriteLine($"{severity} {type} | {messageString}");

        if (type == DebugType.DebugTypeError)
        {
            throw new Exception(messageString);
        }
    }
    private static readonly DebugProc _debugProcCallback = DebugCallback;
    private static GCHandle _debugProcCallbackHandle;
    private int _timerQueryHandle;

    /// <summary>
    /// Number of iterator stucts in the buffer.
    /// </summary>
    private int _iteratorsBufferSize = 0;

    /// <summary>
    /// Number of real values in the buffer.
    /// </summary>
    private int _realParametersBufferSize = 0;

    /// <summary>
    /// Number of vec3 values in the buffer.
    /// </summary>
    private int _vec3ParametersBufferSize = 0;

    /// <summary>
    /// Number of iterators in the xaos matrix.
    /// </summary>
    private int _aliasBufferSize = 0;

    /// <summary>
    /// Creates a new renderer instance.
    /// <see cref="Initialize"/> must be called before starting the render loop.
    /// </summary>
    /// <param name="ctx"></param>
    public RendererGL(IGraphicsContext ctx)
    {
        _ctx = ctx;
    }

    public async Task Initialize(IEnumerable<string> includeSources, IEnumerable<TransformPlugin> transforms, IEnumerable<EffectPlugin> postFxs)
    {
        if (IsInitialized)
            throw new InvalidOperationException("Renderer is already initialized.");

        if (_debugFlag)
        {
            _debugProcCallbackHandle = GCHandle.Alloc(_debugProcCallback);
            GL.DebugMessageCallback(_debugProcCallback, IntPtr.Zero);
            GL.Enable(EnableCap.DebugOutput);
            GL.Enable(EnableCap.DebugOutputSynchronous);
        }

        _includesSource = string.Join(Environment.NewLine, includeSources);
        _registeredTransforms = transforms.ToList();
        _registeredPostfxs = postFxs.ToList();

        //attributeless rendering
        _vao = GL.GenVertexArray();
        GL.BindVertexArray(_vao);
        //empty vao

        InitBuffers();
        InitVertexShader();
        InitTonemapPass();
        InitDEPass();
        InitComputeProgram(_registeredTransforms);
        InitPostFxPasses(_registeredPostfxs);
        InitPostFxPingPongFramebuffers();
        GL.DeleteShader(_vertexShaderHandle);

        _timerQueryHandle = GL.GenQuery();

        SetHistogramScaleToDisplay();

        IsInitialized = true;
        await SetWorkgroupCount(WorkgroupCount);

        InvalidateParamsBuffer();
    }

    public async Task LoadPlugins(IEnumerable<string> includeSources, IEnumerable<TransformPlugin> transforms, IEnumerable<EffectPlugin> postFxs)
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        await WithContext(() =>
        {
            _includesSource = string.Join(Environment.NewLine, includeSources);
            _registeredTransforms = transforms.ToList();
            _registeredPostfxs = postFxs.ToList();
            InitVertexShader();
            InitPostFxPasses(_registeredPostfxs);
            InitComputeProgram(_registeredTransforms);
            GL.DeleteShader(_vertexShaderHandle);
            InvalidateParamsBuffer();
        });
    }

    public void LoadParams(IFS p)
    {
        LoadedParams = p;
        InvocationIters = 500;
        InvalidateParamsBuffer();
        InvalidatePaletteBuffer();
        SetHistogramScaleToDisplay();
    }

    /// <summary>
    /// Invalidates the data in the parameter buffers, which causes the render thread to update them.
    /// Also invalidates the accumulation and state buffers.
    /// Usually called whenever the loaded <see cref="IFS"/> structure or a parameter changes.
    /// </summary>
    public void InvalidateParamsBuffer()
    {
        InvalidateHistogramBuffer();
        _invalidParamsBuffer = true;
    }

    /// <summary>
    /// Invalidates the data in the histogram buffer, which causes the render thread to empty it.
    /// Also invalidates the state buffer.
    /// Usually called whenever the <see cref="Camera"/> parameters are changed.
    /// </summary>
    public void InvalidateHistogramBuffer()
    {
        InvalidatePointsStateBuffer();
        IsTargetIterationReached = false;
        _invalidHistogramBuffer = true;
    }

    public void InvalidateDisplay() => _updateDisplayNow = true;

    /// <summary>
    /// Invalidates the palette gradient buffer, which causes the render thread to recompute it.
    /// Usually called whenever the loaded palette's keys or interpolation mode change.
    /// </summary>
    public void InvalidatePaletteBuffer() => _invalidPaletteBuffer = true;

    private void InvalidatePointsStateBuffer() => _invalidPointsStateBuffer = true;

    private void UpdatePaletteGradientBuffer()
    {
        var palette = LoadedParams.Palette;
        var colorArray = new Vector4[GradientResolution];
        for (int i = 0; i < GradientResolution; i++)
        {
            double position = (double)i / (GradientResolution - 1);
            colorArray[i] = palette.SampleGradient(position);
        }
        GL.NamedBufferSubData(_colorGradientBufferHandle, 0, GradientResolution * sizeof(float) * 4, colorArray);
    }

    public void SetHistogramScale(double scale)
    {
        HistogramScale = scale;
        int newWidth = (int)(LoadedParams.ImageResolution.Width * HistogramScale);
        int newHeight = (int)(LoadedParams.ImageResolution.Height * HistogramScale);
        if (newWidth != HistogramWidth || newHeight != HistogramHeight)
        {
            HistogramWidth = newWidth;
            HistogramHeight = newHeight;
            _invalidHistogramResolution = true;
            InvalidateHistogramBuffer();
        }
    }

    public void SetHistogramScaleToDisplay()
    {
        double rw = DisplayWidth / (double)LoadedParams.ImageResolution.Width;
        double rh = DisplayHeight / (double)LoadedParams.ImageResolution.Height;
        double rr = Math.Min(rw, rh) * .98;
        SetHistogramScale(rr);
    }

    private void UpdateHistogramResolution()
    {
        // resize histogram buffer
        GL.NamedBufferData(_histogramBufferHandle, HistogramWidth * HistogramHeight * 4 * sizeof(float), IntPtr.Zero, BufferUsageHint.StaticCopy);

        // set new resolution uniforms
        GL.UseProgram(_computeProgramHandle);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "width"), HistogramWidth);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "height"), HistogramHeight);
        GL.UseProgram(_tonemapProgramHandle);
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "width"), HistogramWidth);
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "height"), HistogramHeight);
        GL.UseProgram(_deProgramHandle);
        GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "width"), HistogramWidth);
        GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "height"), HistogramHeight);
        foreach (var handle in _postFxProgramHandles)
        {
            GL.UseProgram(handle);
            GL.Uniform1(GL.GetUniformLocation(handle, "width"), HistogramWidth);
            GL.Uniform1(GL.GetUniformLocation(handle, "height"), HistogramHeight);
        }

        //resize post-fx pingpong textures
        GL.ActiveTexture(TextureUnit.Texture0);
        GL.BindTexture(TextureTarget.Texture2D, _postFxTextureA);
        GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32f, HistogramWidth, HistogramHeight, 0, PixelFormat.Rgba, PixelType.Float, new IntPtr(0));
        GL.BindTexture(TextureTarget.Texture2D, _postFxTextureB);
        GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32f, HistogramWidth, HistogramHeight, 0, PixelFormat.Rgba, PixelType.Float, new IntPtr(0));
        //resize display texture
        GL.ActiveTexture(TextureUnit.Texture0);
        GL.BindTexture(TextureTarget.Texture2D, _renderTextureHandle);
        GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32f, HistogramWidth, HistogramHeight, 0, PixelFormat.Rgba, PixelType.Float, new IntPtr(0));

        GL.Viewport(0, 0, HistogramWidth, HistogramHeight);

        //must clear the display framebuffer since we're only blitting to it so garbage from previous resolution could remain visible.
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
        GL.ClearBuffer(ClearBuffer.Color, 0, _bufferClearColor);
        _ctx.SwapBuffers();//clear back buffer
        GL.ClearBuffer(ClearBuffer.Color, 0, _bufferClearColor);
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, _offscreenFBOHandle);

        DisplayFramebufferUpdated?.Invoke(this, null);

        _invalidHistogramResolution = false;
    }

    public void SetDisplayResolution(int displayWidth, int displayHeight)
    {
        DisplayWidth = displayWidth;
        DisplayHeight = displayHeight;
        InvalidateDisplay();
    }

    public void DispatchCompute()
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        GL.UseProgram(_computeProgramHandle);

        if (_invalidHistogramBuffer)
        {
            if (_invalidHistogramResolution)
            {
                UpdateHistogramResolution();
                GL.UseProgram(_computeProgramHandle);
            }

            //reset accumulation
            GL.ClearNamedBufferData(_histogramBufferHandle, PixelInternalFormat.R32f, PixelFormat.Red, PixelType.Float, IntPtr.Zero);
            _invalidHistogramBuffer = false;
            _dispatchCnt = 0;
            TotalIterations = 0;
            InvalidateDisplay();
            InvalidatePointsStateBuffer();//needed when IterationDepth is high

            //update settings struct
            var settings = new SettingsStruct
            {
                camera_params = LoadedParams.Camera.GetCameraParameters(),
                itnum = LoadedParams.Iterators.Count,
                fog_effect = (float)LoadedParams.FogEffect,
                gradient_resolution = GradientResolution,
                entropy = (float)LoadedParams.Entropy,
                warmup = LoadedParams.Warmup,
                max_filter_radius = MaxFilterRadius,
                mark_area_in_focus = MarkAreaInFocus ? 1 : 0
            };
            GL.NamedBufferSubData(_settingsBufferHandle, 0, Marshal.SizeOf(typeof(SettingsStruct)), ref settings);

        }

        if (_invalidParamsBuffer)
        {
            //generate iterator and transform structs
            var its = new List<IteratorStruct>();
            var allRealParams = new List<float>();
            var allVec3Params = new List<Vector4>();
            var currentIterators = LoadedParams.Iterators.ToList();
            //start weights -> alias method tables
            double sumStartWeights = currentIterators.Sum(i => i.StartWeight);
            if (sumStartWeights == 0.0)
            {
                //TODO: throw new InvalidOperationException("Invalid params: No input iterator found.");
                return;
            }
            var normalizedStartWeights = currentIterators.Select(i => i.StartWeight / sumStartWeights).ToList();
            var aliasTables = AliasMethod.GenerateAliasTable(normalizedStartWeights).ToList();
            for (int iti = 0; iti < currentIterators.Count; iti++)
            {
                var it = currentIterators[iti];
                //iterators
                its.Add(new IteratorStruct
                {
                    tfId = _registeredTransforms.IndexOf(it.Transform),
                    real_params_index = allRealParams.Count,
                    vec3_params_index = allVec3Params.Count,
                    color_speed = (float)it.ColorSpeed,
                    color_index = (float)it.ColorIndex,
                    opacity = (float)it.Opacity,
                    shading_mode = (int)it.ShadingMode,
                    tf_mix = (float)it.Mix,
                    tf_add = (float)it.Add,
                    reset_alias = aliasTables[iti].k,
                    reset_prob = (float)aliasTables[iti].u
                });
                //collect transform params
                allRealParams.AddRange(it.RealParams.Values.Select(p => (float)p).ToList());
                allVec3Params.AddRange(it.Vec3Params.Values.Select(p => new Vector4(p, 0.0f)).ToList());
            }

            if (its.Count == _iteratorsBufferSize)
                GL.NamedBufferSubData(_iteratorsBufferHandle, 0, _iteratorsBufferSize * Marshal.SizeOf(typeof(IteratorStruct)), its.ToArray());
            else
            {//resize buffer when number of iterators change
                _iteratorsBufferSize = its.Count;
                GL.NamedBufferData(_iteratorsBufferHandle, _iteratorsBufferSize * Marshal.SizeOf(typeof(IteratorStruct)), its.ToArray(), BufferUsageHint.DynamicDraw);
            }

            if (allRealParams.Count == _realParametersBufferSize)
                GL.NamedBufferSubData(_realParametersBufferHandle, 0, _realParametersBufferSize * 4 * sizeof(float), allRealParams.Select(f => new Vector4(f)).ToArray());
            else
            {//resize buffer when number of real parameters change
                _realParametersBufferSize = allRealParams.Count;
                GL.NamedBufferData(_realParametersBufferHandle, _realParametersBufferSize * 4 * sizeof(float), allRealParams.Select(f => new Vector4(f)).ToArray(), BufferUsageHint.DynamicDraw);
            }

            if (allVec3Params.Count == _vec3ParametersBufferSize)
                GL.NamedBufferSubData(_vec3ParametersBufferHandle, 0, _vec3ParametersBufferSize * 4 * sizeof(float), allVec3Params.ToArray());
            else
            {//resize buffer when number of vec3 parameters change
                _vec3ParametersBufferSize = allVec3Params.Count;
                GL.NamedBufferData(_vec3ParametersBufferHandle, _vec3ParametersBufferSize * 4 * sizeof(float), allVec3Params.ToArray(), BufferUsageHint.DynamicDraw);
            }

            //normalize base weights
            double SumWeights = currentIterators.Sum(i => i.BaseWeight);
            var normalizedBaseWeights = currentIterators.ToDictionary(i => i, i => i.BaseWeight / SumWeights);
            var xaosAliasTables = new List<(double u, int k)>();
            foreach (var it in currentIterators)
            {
                var itWeights = new List<double>(currentIterators.Count);
                foreach (var toIt in currentIterators)
                {
                    if (it.WeightTo.TryGetValue(toIt, out var value))//multiply with base weights
                        itWeights.Add(value * normalizedBaseWeights[toIt]);
                    else//fill missing transitions with 0
                        itWeights.Add(0);
                }
                double sumw = itWeights.Sum();
                if (sumw > 0)
                {
                    itWeights = itWeights.Select(w => w / sumw).ToList();//normalize xaos weights
                    xaosAliasTables.AddRange(AliasMethod.GenerateAliasTable(itWeights));
                }
                else
                {//iteration resets here because there are no outgoing weights. Mark this with -1
                    xaosAliasTables.AddRange(Enumerable.Repeat((-1.0, -1), currentIterators.Count));
                }
            }

            //update xaos alias tables
            var xaosAliasBufferData = xaosAliasTables.Select(t => new Vector4((float)t.u, t.k, 0f, 0f)).ToArray();
            if (currentIterators.Count == _aliasBufferSize)
                GL.NamedBufferSubData(_aliasBufferHandle, 0, _aliasBufferSize * _aliasBufferSize * sizeof(float) * 4, xaosAliasBufferData);
            else
            {//resize buffer when number of iterators change
                _aliasBufferSize = currentIterators.Count;
                GL.NamedBufferData(_aliasBufferHandle, _aliasBufferSize * _aliasBufferSize * sizeof(float) * 4, xaosAliasBufferData, BufferUsageHint.DynamicDraw);
            }


            _invalidParamsBuffer = false;
        }

        if (_invalidPaletteBuffer)
        {
            UpdatePaletteGradientBuffer();
            _invalidPaletteBuffer = false;
        }

        //these values can change every dispatch
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "reset_points_state"), _invalidPointsStateBuffer ? 1 : 0);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "dispatch_cnt"), _dispatchCnt);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "invocation_iters"), InvocationIters);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "seed"), Seed);

        GL.Finish(); //blocking call
        GL.DispatchCompute(WorkgroupCount, 1, 1);

        _invalidPointsStateBuffer = false;
        TotalIterations += Convert.ToUInt64(InvocationIters * InvocationCount);
        _dispatchCnt++;

    }

    public void RenderImage()
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        GL.UseProgram(_tonemapProgramHandle);
        GL.BindVertexArray(_vao);
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "max_density"), 1 + (uint)(TotalIterations / (uint)(HistogramWidth * HistogramHeight)));//apo:*0.001//draw quad
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "brightness"), (float)LoadedParams.Brightness);
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "inv_gamma"), (float)(1.0f / LoadedParams.Gamma));
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "gamma_threshold"), (float)LoadedParams.GammaThreshold);
        GL.Uniform1(GL.GetUniformLocation(_tonemapProgramHandle, "vibrancy"), (float)LoadedParams.Vibrancy);
        GL.Uniform3(GL.GetUniformLocation(_tonemapProgramHandle, "bg_color"), LoadedParams.BackgroundColor.R / 255.0f, LoadedParams.BackgroundColor.G / 255.0f, LoadedParams.BackgroundColor.B / 255.0f);
        GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);

        if (EnableDE)
        {
            GL.UseProgram(_deProgramHandle);
            GL.BindVertexArray(_vao);
            GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "de_max_radius"), (float)DEMaxRadius);
            GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "de_power"), (float)DEPower);
            GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "de_threshold"), (float)DEThreshold);
            GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "max_density"), 1 + (uint)(TotalIterations / (uint)(HistogramWidth * HistogramHeight)));//apo:*0.001
            GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
        }

        ApplyPostFxPasses();

    }

    /// <summary>
    /// Fast write finished image from offscreen framebuffer to the center of the display framebuffer (0) with a small margin.
    /// </summary>
    private void BlitToDisplayFramebuffer()
    {
        float rw = DisplayWidth / (float)HistogramWidth;
        float rh = DisplayHeight / (float)HistogramHeight;
        float rr = (rw < rh ? rw : rh) * .98f;//
        GL.BlitNamedFramebuffer(_offscreenFBOHandle,
            0, 0, 0, HistogramWidth, HistogramHeight,
            (int)(DisplayWidth / 2 - HistogramWidth / 2 * rr),
            (int)(DisplayHeight / 2 - HistogramHeight / 2 * rr),
            (int)(DisplayWidth / 2 + HistogramWidth / 2 * rr),
            (int)(DisplayHeight / 2 + HistogramHeight / 2 * rr),
            ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Nearest);
        // Workaround: On AMD the AlphaBits=0 is ignored for some reason,
        // so we need to clear the alpha channel before diplaying to avoid "white" background problem.
        GL.BindFramebuffer(FramebufferTarget.DrawFramebuffer, 0);
        GL.ColorMask(false, false, false, true);
        GL.ClearBuffer(ClearBuffer.Color, 0, _bufferClearColor);
        GL.ColorMask(true, true, true, true);
        GL.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _offscreenFBOHandle);
    }

    private void AdjustWorkloadSize()
    {
        GL.GetQueryObject(_timerQueryHandle, GetQueryObjectParam.QueryResult, out int timerQueryResultNs); //blocking call
        int actualMs = timerQueryResultNs / 1000000;//ns to ms
        int targetMs = 1000 / TargetFramerate;
        double adjustment = (actualMs > targetMs) ? 0.66 : 1.1;
        int adjustedInvocationIters = (int)(InvocationIters * adjustment);
        InvocationIters = Math.Clamp(adjustedInvocationIters, 50, 20000);
    }

    public void StartRenderLoop()
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        if (!IsRendering)
        {
            IsRendering = true;

            if (_ctx.IsCurrent)
                _ctx.MakeNoneCurrent();

            ThreadPool.QueueUserWorkItem((_) =>
            {
                _ctx.MakeCurrent();
                while (IsRendering)
                {
                    GL.BeginQuery(QueryTarget.TimeElapsed, _timerQueryHandle);
                    DispatchCompute();//compute the histogram
                    GL.EndQuery(QueryTarget.TimeElapsed);

                    AdjustWorkloadSize();

                    bool target = BitOperations.Log2(1 + TotalIterations / (ulong)(HistogramWidth * HistogramHeight)) >= LoadedParams.TargetIterationLevel;
                    bool isTargetIterationReachedByCurrentDispatch = !IsTargetIterationReached && target;
                    IsTargetIterationReached = target;

                    bool isPerceptuallyEqualFrame = BitOperations.IsPow2(_dispatchCnt);

                    if (_updateDisplayNow || isTargetIterationReachedByCurrentDispatch || (UpdateDisplayOnRender && (!EnablePerceptualUpdates || (EnablePerceptualUpdates && isPerceptuallyEqualFrame))))
                    {
                        //render image from histogram
                        RenderImage();
                        //display the image
                        BlitToDisplayFramebuffer();
                        _ctx.SwapBuffers();
                        _updateDisplayNow = false;
                        DisplayFramebufferUpdated?.Invoke(this, null);
                    }

                    if (isTargetIterationReachedByCurrentDispatch)
                        TargetIterationReached?.Invoke(this, null);
                }
                GL.Finish();
                _ctx.MakeNoneCurrent();
                _stopRender.Set();
            });
        }
    }

    /// <summary>
    /// Wait for the render thread to stop.
    /// </summary>
    public async Task StopRenderLoop()
    {
        if (!IsRendering)
            throw new InvalidOperationException("The render loop is not running.");
        IsRendering = false;
        await _stopRender.WaitAsync();
    }

    /// <summary>
    /// Helper to call opengl from current thread with context. Stops the render thread if context is not current.
    /// </summary>
    /// <param name="action"></param>
    /// <returns></returns>
    private async Task WithContext(Action action)
    {
        bool renderingPaused = IsRendering && !_ctx.IsCurrent;
        if (renderingPaused)
            await StopRenderLoop();
        bool wasCurrentContext = _ctx.IsCurrent;
        if (!_ctx.IsCurrent)
            _ctx.MakeCurrent();//acquire
        action();
        if (!wasCurrentContext)
            _ctx.MakeNoneCurrent();//release
        if (renderingPaused)
            StartRenderLoop();//restart render thread if it was running
    }

    /// <summary>
    /// Writes the pixel data to the specified pointer.
    /// The format is ubyte, bgra, which is ideal for filling a bitmap buffer quickly.
    /// </summary>
    /// <param name="ptr">BitmapData.Scan0 or WriteableBitmap.BackBuffer</param>
    /// <remarks>        
    /// The resulting bitmap requires further transformations: 
    /// <list type="bullet">
    /// <item> Image must be flipped vertically </item>
    /// <item> Alpha channel may be removed </item>
    /// </list>
    /// </remarks>
    public async Task CopyPixelDataToBitmap(IntPtr ptr)
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        InvalidateDisplay();
        await WithContext(() =>
        {
            GL.ReadPixels(0, 0, HistogramWidth, HistogramHeight, PixelFormat.Bgra, PixelType.UnsignedByte, ptr);
        });
    }

    /// <summary>
    /// Format: float[y, x, rgba]
    /// </summary>
    /// 
    /// <remarks>
    /// For large images, GC configuration is required in App.config to avoid <see cref="OutOfMemoryException"/>:
    /// <code>gcAllowVeryLargeObjects</code>
    /// The LargeObjectHeap may be collected manually:
    /// <code>
    /// GCSettings.LargeObjectHeapCompactionMode = CompactOnce;
    /// GC.Collect();
    /// </code>
    /// 
    /// The resulting bitmap requires further transformations: 
    /// <list type="bullet">
    /// <item> Image must be flipped vertically </item>
    /// <item> Alpha channel may be removed </item>
    /// </list>
    /// </remarks>
    public async Task<float[,,]> ReadPixelData()
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        InvalidateDisplay();
        float[,,] o = new float[HistogramHeight, HistogramWidth, 4];
        await WithContext(() =>
        {
            GL.ReadPixels(0, 0, HistogramWidth, HistogramHeight, PixelFormat.Rgba, PixelType.Float, o);
        });

        return o;
    }

    public async Task<float[][][]> ReadHistogramData()
    {
        if (!IsInitialized)
            throw NewNotInitializedException();

        InvalidateDisplay();
        float[] o2 = new float[HistogramWidth * HistogramHeight * 4];
        await WithContext(() =>
        {
            GL.GetNamedBufferSubData<float>(_histogramBufferHandle, IntPtr.Zero, HistogramWidth * HistogramHeight * 4 * sizeof(float), o2);
            GL.Finish();
        });

        var div = LoadedParams.Brightness * (1 + (TotalIterations / (ulong)(HistogramWidth * HistogramHeight)));
        for (int i = 0; i < o2.Length; i++)
            o2[i] = (float)(o2[i] / div);

        var histogram = o2
            .Chunk(4)
            .Chunk(HistogramWidth)
            .Chunk(HistogramHeight)
            .First().Reverse().ToArray();

        return histogram;
    }

    private void InitDEPass()
    {

        var resource = typeof(RendererGL).GetTypeInfo().Assembly.GetManifestResourceStream(_shadersPath + "de.frag.shader");
        string deShaderSource = new StreamReader(resource).ReadToEnd();
        //compile de shader
        int deShaderH = GL.CreateShader(ShaderType.FragmentShader);
        GL.ShaderSource(deShaderH, deShaderSource);
        GL.CompileShader(deShaderH);
        GL.GetShader(deShaderH, ShaderParameter.CompileStatus, out int status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error compiling {0} shader: {1}", ShaderType.FragmentShader.ToString(), GL.GetShaderInfoLog(deShaderH)));
        }

        _deProgramHandle = GL.CreateProgram();
        GL.AttachShader(_deProgramHandle, _vertexShaderHandle);
        GL.AttachShader(_deProgramHandle, deShaderH);
        GL.LinkProgram(_deProgramHandle);
        GL.GetProgram(_deProgramHandle, GetProgramParameterName.LinkStatus, out status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error linking de program: {0}", GL.GetProgramInfoLog(_deProgramHandle)));
        }

        GL.DetachShader(_deProgramHandle, _vertexShaderHandle);
        GL.DetachShader(_deProgramHandle, deShaderH);
        GL.DeleteShader(deShaderH);

        GL.UseProgram(_deProgramHandle);
        GL.Uniform1(GL.GetUniformLocation(_deProgramHandle, "histogram_tex"), 0);

    }

    private void InitVertexShader()
    {
        var assembly = typeof(RendererGL).GetTypeInfo().Assembly;

        _vertexShaderHandle = GL.CreateShader(ShaderType.VertexShader);
        GL.ShaderSource(_vertexShaderHandle, new StreamReader(assembly.GetManifestResourceStream(_shadersPath + "quad.vert.shader")).ReadToEnd());
        GL.CompileShader(_vertexShaderHandle);
        GL.GetShader(_vertexShaderHandle, ShaderParameter.CompileStatus, out int status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error compiling {0} shader: {1}", ShaderType.VertexShader.ToString(), GL.GetShaderInfoLog(_vertexShaderHandle)));
        }
    }

    private void InitTonemapPass()
    {
        var assembly = typeof(RendererGL).GetTypeInfo().Assembly;

        var fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
        GL.ShaderSource(fragmentShader, new StreamReader(assembly.GetManifestResourceStream(_shadersPath + "tonemap.frag.shader")).ReadToEnd());
        GL.CompileShader(fragmentShader);
        GL.GetShader(fragmentShader, ShaderParameter.CompileStatus, out int status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error compiling {0} shader: {1}", ShaderType.FragmentShader.ToString(), GL.GetShaderInfoLog(fragmentShader)));
        }

        //init display image texture
        _renderTextureHandle = GL.GenTexture();
        GL.ActiveTexture(TextureUnit.Texture0);
        GL.BindTexture(TextureTarget.Texture2D, _renderTextureHandle);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Nearest);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToBorder);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToBorder);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBorderColor, _bufferClearColor);
        //TODO: display resolution?
        GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32f, HistogramWidth, HistogramHeight, 0, PixelFormat.Rgba, PixelType.Float, new IntPtr(0));

        _offscreenFBOHandle = GL.GenFramebuffer();
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, _offscreenFBOHandle);//offscreen
        GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _renderTextureHandle, 0);
        if (GL.CheckFramebufferStatus(FramebufferTarget.Framebuffer) != FramebufferErrorCode.FramebufferComplete)
            throw new Exception("Frame Buffer Error");

        _tonemapProgramHandle = GL.CreateProgram();
        GL.AttachShader(_tonemapProgramHandle, _vertexShaderHandle);
        GL.AttachShader(_tonemapProgramHandle, fragmentShader);
        GL.LinkProgram(_tonemapProgramHandle);
        GL.GetProgram(_tonemapProgramHandle, GetProgramParameterName.LinkStatus, out status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error linking program: {0}", GL.GetProgramInfoLog(_tonemapProgramHandle)));
        }

        GL.DetachShader(_tonemapProgramHandle, _vertexShaderHandle);
        GL.DetachShader(_tonemapProgramHandle, fragmentShader);
        GL.DeleteShader(fragmentShader);

    }

    /// <summary>
    /// Creates ping-pong framebuffers and textures for post-fx chaining.
    /// </summary>
    private void InitPostFxPingPongFramebuffers()
    {
        _postFxFboA = GL.GenFramebuffer();
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, _postFxFboA);
        _postFxTextureA = GL.GenTexture();
        GL.ActiveTexture(TextureUnit.Texture0);
        GL.BindTexture(TextureTarget.Texture2D, _postFxTextureA);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Linear);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToBorder);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToBorder);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBorderColor, _bufferClearColor);
        GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32f, HistogramWidth, HistogramHeight, 0, PixelFormat.Rgba, PixelType.Float, new IntPtr(0));
        GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _postFxTextureA, 0);

        _postFxFboB = GL.GenFramebuffer();
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, _postFxFboB);
        _postFxTextureB = GL.GenTexture();
        GL.ActiveTexture(TextureUnit.Texture0);
        GL.BindTexture(TextureTarget.Texture2D, _postFxTextureB);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Linear);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToBorder);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToBorder);
        GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBorderColor, _bufferClearColor);
        GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba32f, HistogramWidth, HistogramHeight, 0, PixelFormat.Rgba, PixelType.Float, new IntPtr(0));
        GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _postFxTextureB, 0);

        GL.BindFramebuffer(FramebufferTarget.Framebuffer, _offscreenFBOHandle);
    }

    /// <summary>
    /// Creates/recreates opengl programs for all registered postfx plugins.
    /// </summary>
    private void InitPostFxPasses(IEnumerable<EffectPlugin> postFxs)
    {
        //delete old programs
        foreach (var handle in _postFxProgramHandles)
            if (handle != 0)
                GL.DeleteProgram(handle);
        _postFxProgramHandles.Clear();

        foreach (var postfx in postFxs)
            _postFxProgramHandles.Add(InitPostFxPass(postfx));
    }

    /// <summary>
    /// Creates the opengl program for the specified post-processing effect.
    /// </summary>
    /// <returns>The handle of the created program.</returns>
    private int InitPostFxPass(EffectPlugin postfx)
    {
        var assembly = typeof(RendererGL).GetTypeInfo().Assembly;
        string shaderTemplate = new StreamReader(assembly.GetManifestResourceStream(_shadersPath + "postfx.frag.shader")).ReadToEnd();
        //insert includes
        string shaderSource = shaderTemplate.Replace("@includes", _includesSource);
        //replace the snippet placeholder with the plugin's source code
        shaderSource = shaderSource.Replace("@PostFxSnippet", postfx.SourceCode);

        int fragmentShader = GL.CreateShader(ShaderType.FragmentShader);
        GL.ShaderSource(fragmentShader, shaderSource);
        GL.CompileShader(fragmentShader);
        GL.GetShader(fragmentShader, ShaderParameter.CompileStatus, out int status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error compiling postfx '{0}' shader: {1}", postfx.Name, GL.GetShaderInfoLog(fragmentShader)));
        }

        int programHandle = GL.CreateProgram();
        GL.AttachShader(programHandle, _vertexShaderHandle);
        GL.AttachShader(programHandle, fragmentShader);
        GL.LinkProgram(programHandle);
        GL.GetProgram(programHandle, GetProgramParameterName.LinkStatus, out status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error linking postfx '{0}' program: {1}", postfx.Name, GL.GetProgramInfoLog(programHandle)));
        }

        GL.DetachShader(programHandle, _vertexShaderHandle);
        GL.DetachShader(programHandle, fragmentShader);
        GL.DeleteShader(fragmentShader);

        GL.UseProgram(programHandle);
        GL.Uniform1(GL.GetUniformLocation(programHandle, "width"), HistogramWidth);
        GL.Uniform1(GL.GetUniformLocation(programHandle, "height"), HistogramHeight);

        return programHandle;
    }

    /// <summary>
    /// Applies each enabled post-processing effect, chaining them via ping-pong textures.
    /// </summary>
    private void ApplyPostFxPasses()
    {
        var enabledLayers = LoadedParams.PostEffects.Where(i => i.Enabled).ToList();
        if (enabledLayers.Count == 0)
            return;

        //blit the tonemap/DE result into the first ping-pong texture
        GL.BlitNamedFramebuffer(_offscreenFBOHandle, _postFxFboA,
            0, 0, HistogramWidth, HistogramHeight,
            0, 0, HistogramWidth, HistogramHeight,
            ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Linear);

        //apply each layer in order
        for (int i = 0; i < enabledLayers.Count; i++)
        {
            var layer = enabledLayers[i];
            int programIndex = _registeredPostfxs.IndexOf(layer.Effect);
            if (programIndex < 0 || programIndex >= _postFxProgramHandles.Count)
                throw new Exception($"Postfx program not found for effect '{layer.Effect.Name}'.");

            int programHandle = _postFxProgramHandles[programIndex];
            GL.UseProgram(programHandle);

            //bind the source texture
            GL.ActiveTexture(TextureUnit.Texture0);
            GL.BindTexture(TextureTarget.Texture2D, (i % 2 == 0) ? _postFxTextureA : _postFxTextureB);
            GL.Uniform1(GL.GetUniformLocation(programHandle, "tex"), 0);
            GL.Uniform1(GL.GetUniformLocation(programHandle, "palette_colors_count"), LoadedParams.Palette.KeyColors.Count);

            //set up param arrays for this postfx
            float[] realParams = layer.RealParams.Values.Select(v => (float)v).ToArray();
            if (realParams.Length > 0)
                GL.Uniform1(GL.GetUniformLocation(programHandle, "postfx_real_params"), realParams.Length, ref realParams[0]);
            float[] vec3Data = layer.Vec3Params.Values.SelectMany(v => new float[] { v.X, v.Y, v.Z }).ToArray();
            if (vec3Data.Length > 0)
                GL.Uniform3(GL.GetUniformLocation(programHandle, "postfx_vec3_params"), vec3Data.Length / 3, ref vec3Data[0]);

            //render to the other FBO
            GL.BindFramebuffer(FramebufferTarget.Framebuffer, (i % 2 == 0) ? _postFxFboB : _postFxFboA);
            GL.DrawArrays(PrimitiveType.TriangleStrip, 0, 4);
        }

        //copy final result back to the offscreen FBO
        int finalFbo = (enabledLayers.Count % 2 == 0) ? _postFxFboA : _postFxFboB;
        GL.BlitNamedFramebuffer(finalFbo, _offscreenFBOHandle,
            0, 0, HistogramWidth, HistogramHeight,
            0, 0, HistogramWidth, HistogramHeight,
            ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Linear);

        //restore offscreen FBO as active
        GL.BindFramebuffer(FramebufferTarget.Framebuffer, _offscreenFBOHandle);
    }

    private void InitComputeProgram(IEnumerable<TransformPlugin> transforms)
    {
        //load functions
        string transformsSource = "";
        for (int tfIndex = 0; tfIndex < transforms.Count(); tfIndex++)
        {
            var tf = transforms.ElementAt(tfIndex);
            transformsSource += $@"
if (iter.tfId == {tfIndex})
{{
{tf.SourceCode}
}}
";
        }

        //assemble source string
        var resource = typeof(RendererGL).GetTypeInfo().Assembly.GetManifestResourceStream(_shadersPath + "ifs_kernel.comp.shader");
        string computeShaderSource = new StreamReader(resource).ReadToEnd();

        //insert includes
        computeShaderSource = computeShaderSource.Replace("@includes", _includesSource);
        //insert transforms
        computeShaderSource = computeShaderSource.Replace("@transforms", transformsSource);

        //compile compute shader
        int computeShaderH = GL.CreateShader(ShaderType.ComputeShader);
        GL.ShaderSource(computeShaderH, computeShaderSource);
        GL.CompileShader(computeShaderH);
        GL.GetShader(computeShaderH, ShaderParameter.CompileStatus, out int status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error compiling {0} shader: {1}", ShaderType.ComputeShader.ToString(), GL.GetShaderInfoLog(computeShaderH)));
        }

        //free previous resources
        if (_computeProgramHandle != 0)
            GL.DeleteProgram(_computeProgramHandle);
        //build shader program
        _computeProgramHandle = GL.CreateProgram();
        GL.AttachShader(_computeProgramHandle, computeShaderH);
        GL.LinkProgram(_computeProgramHandle);
        GL.GetProgram(_computeProgramHandle, GetProgramParameterName.LinkStatus, out status);
        if (status == 0)
        {
            throw new Exception(
                string.Format("Error linking de program: {0}", GL.GetProgramInfoLog(_computeProgramHandle)));
        }
        GL.DetachShader(_computeProgramHandle, computeShaderH);
        GL.DeleteShader(computeShaderH);

        GL.UseProgram(_computeProgramHandle);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "width"), HistogramWidth);
        GL.Uniform1(GL.GetUniformLocation(_computeProgramHandle, "height"), HistogramHeight);

    }

    private void InitBuffers()
    {
        //create buffers
        _settingsBufferHandle = GL.GenBuffer();
        _histogramBufferHandle = GL.GenBuffer();
        _pointsBufferHandle = GL.GenBuffer();
        _iteratorsBufferHandle = GL.GenBuffer();
        _aliasBufferHandle = GL.GenBuffer();
        _colorGradientBufferHandle = GL.GenBuffer();
        _realParametersBufferHandle = GL.GenBuffer();
        _vec3ParametersBufferHandle = GL.GenBuffer();

        //bind and allocate fixed-size buffers
        GL.BindBuffer(BufferTarget.UniformBuffer, _settingsBufferHandle);
        GL.BufferData(BufferTarget.UniformBuffer, Marshal.SizeOf(typeof(SettingsStruct)), IntPtr.Zero, BufferUsageHint.StreamDraw);
        GL.BindBuffer(BufferTarget.UniformBuffer, _colorGradientBufferHandle);
        GL.BufferData(BufferTarget.UniformBuffer, GradientResolution * sizeof(float) * 4, IntPtr.Zero, BufferUsageHint.StreamDraw);

        //bind varying-size buffers to targets, without allocation
        GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _histogramBufferHandle);
        GL.BindBuffer(BufferTarget.ShaderStorageBuffer, _pointsBufferHandle);
        GL.BindBuffer(BufferTarget.UniformBuffer, _iteratorsBufferHandle);
        GL.BindBuffer(BufferTarget.UniformBuffer, _aliasBufferHandle);
        GL.BindBuffer(BufferTarget.UniformBuffer, _colorGradientBufferHandle);
        GL.BindBuffer(BufferTarget.UniformBuffer, _realParametersBufferHandle);
        GL.BindBuffer(BufferTarget.UniformBuffer, _vec3ParametersBufferHandle);

        //bind buffers to layout:
        GL.BindBufferBase(BufferRangeTarget.ShaderStorageBuffer, 0, _histogramBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.ShaderStorageBuffer, 1, _pointsBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.UniformBuffer, 2, _settingsBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.UniformBuffer, 3, _iteratorsBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.UniformBuffer, 4, _aliasBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.UniformBuffer, 5, _colorGradientBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.UniformBuffer, 6, _realParametersBufferHandle);
        GL.BindBufferBase(BufferRangeTarget.UniformBuffer, 7, _vec3ParametersBufferHandle);

    }

    private static InvalidOperationException NewNotInitializedException() => new("Renderer is not initialized.");

    public async ValueTask DisposeAsync()
    {
        DisplayFramebufferUpdated = null;
        if (IsInitialized)
        {
            if (IsRendering)
                await StopRenderLoop();
            if (_debugFlag)
                _debugProcCallbackHandle.Free();
            //TODO: dispose buffers

            GL.DeleteQuery(_timerQueryHandle);
        }
        IsInitialized = false;
    }

}