Can I use the Job System/Task Graph to procedureally generate a texture with good perfomance?

Here is the context. I have been trying to learn Flax Engine by taking Unity tutorials and replicating them in Flax. The current tutorial I am learning uses Unity’s Job System and Burst Compiler to generate a procedure texture with Perlin noise

While I believe Flax’s Job System is different conceptually, I do believe this task is doable but requires some tweaking. However my attempt failed to be performant so I am here to see if there was something I could change to get this to work.

Currently Flax crashed when I try to update per frame with my texture function so I use the stats version to run the same calculations without _tex.SetPixels(colors);

Any ideas or even ways to improve the code in general would be helpful

``using System;
using System.Collections.Generic;
using FlaxEngine;
using FlaxEngine.Utilities;

namespace Game
{
///


/// Perlin Noise Script.
///

///

public class Perlin_DOT : Script
{
    [Header("Inscribed")]
    public int textureSize = 1080;
    public float test = 0.5f;

    [Header("Inscribed / Dynamic")]
    public float noiseScale = 10;
    public Vector2 noiseOffset = Vector2.Zero;
    public Vector2 offsetVel = Vector2.UnitX;
    MaterialBase _mat;
    Texture _tex;
    private SpriteRender currentActor;
    private Float2 perNoise = new Float2();
    public PerlinVars perlinVars = new PerlinVars();
    private Color32[] colors;
    
    /// <inheritdoc/>
    public override void OnStart()
    {
        currentActor = Actor.As<SpriteRender>();
        _mat = Actor.As<SpriteRender>().Material;
        InitTex();
        _tex.GetPixels(out colors, 0, 0);
        UpdateText();
        currentActor.Image = _tex;
        //SpriteRender
        // Here you can add code that needs to be called when script is created, just before the first game update
    }
    public void Execute(int ndx)
    {
        float noiseMult = noiseScale / textureSize;
        float minusHalf = -(textureSize * test);
        int h = ndx % textureSize;
         int v = ndx / textureSize;
         Float2 loc;
         loc.X = (( h) * noiseMult) + noiseOffset.X;
         loc.Y = (( v) * noiseMult) + noiseOffset.Y;

        float u = Noise.PerlinNoise(loc);

        byte b = (byte)(255 * u );

        colors[ndx] = new Color32(b, b, b, 255);
    }
    private void UpdateText()
    {

       JobSystem.Execute(Execute, colors.Length);
       // Debug.Log($"Length: {jobData.pixels.Length}");
        
        _tex.SetPixels(colors);
    }


    private void UpdateTextStats()
    {

        //Precalulate some math


        JobSystem.Execute(Execute, colors.Length);

        //_tex.SetPixels(jobData.pixels);
    }

    public float PerlinOctaves(PerlinVars pVars, Float2 xy)
    {
        float u = 0, lacu = 1, pers = 1, noiseAmt;
        for ( int octave = 0; octave < pVars.numOctaves; octave++)
        {
            if ( octave != 0 )
            {
                lacu *= pVars.lacunarity;
                pers *= pVars.persistence;
            }
            xy.X *= lacu;
            xy.Y *= lacu;
            noiseAmt = (Noise.PerlinNoise(xy) - 0.5f) * pers;
            u += noiseAmt;
        }
        return u + 0.5f;
    }
    

    private  unsafe void InitTex()
    {


        // Create new texture asset
        var texture = Content.CreateVirtualAsset<Texture>();
        _tex = texture;
        var initData = new TextureBase.InitData();
        initData.Width = textureSize;
        initData.Height = textureSize;
        initData.ArraySize = 1;
        initData.Format = PixelFormat.R8G8B8A8_UNorm;
        var data = new byte[initData.Width * initData.Height * PixelFormatExtensions.SizeInBytes(initData.Format)];
        fixed (byte* dataPtr = data)
        {
            // Generate pixels data (linear gradient)
            var colorsPtr = (Color32*)dataPtr;
            for (int y = 0; y < initData.Height; y++)
            {
                float t1 = (float)y / initData.Height;
                var c1 = Color32.Lerp(Color.Red, Color.Blue, t1);
                var c2 = Color32.Lerp(Color.Yellow, Color.Green, t1);
                for (int x = 0; x < initData.Width; x++)
                {
                    float t2 = (float)x / initData.Width;
                    colorsPtr[y * initData.Width + x] = Color32.Lerp(c1, c2, t2);
                }
            }
        }
        initData.Mips = new[]
        {
        // Initialize mip maps data container description
        new TextureBase.InitData.MipData
        {
            Data = data,
            RowPitch = data.Length / initData.Height,
            SlicePitch = data.Length
        },
    };
        texture.Init(ref initData);
    }

    /// <inheritdoc/>
    public override void OnEnable()
    {
        // Here you can add code that needs to be called when script is enabled (eg. register for events)
    }

    /// <inheritdoc/>
    public override void OnDisable()
    {
        // Here you can add code that needs to be called when script is disabled (eg. unregister from events)
    }

    /// <inheritdoc/>
    public override void OnUpdate()
    {
        // Here you can add code that needs to be called every frame
        //noiseOffset += offsetVel * Time.DeltaTime;
        UpdateTextStats();

    }

    public override void OnDestroy()
    {
        // Ensure to cleanup resources
        FlaxEngine.Object.Destroy(ref _tex);

    }
}

}

Fixed crash here: Fix crash when updating GPU texture residency to 0 · FlaxEngine/FlaxEngine@6aaa583 · GitHub

Also, Job System executed for every texture pixel (1080x1080 in this case) is terribly slow. I would advise running a single Job per chunk of for example 64x64 pixels block to make it run faster.

image

Another way would be to use GPUTexture like here HOWTO: Use dynamic texture | Flax Documentation to upload data more directly without using Texture asset layer - you would need to use custom material with GPUTexture parameter that would be set at runtime via virtual Material Instance, more info: Instanced Materials | Flax Documentation

Finally, updating large texture on CPU every frame and sending to GPU is usually slow so it can be batched to be executed once a few frames or even moved to GPU side to run in shader.

Understood, will implement your suggestions when I have time to return to my project.
Thanks for the feedback and crash fix