IJobParallelFor Run Slower than main thread solution ?

Hi, my current job does not run faster than solution for main thread. What am i wrong ?
This is my job and the way i use it on main thread.

 public struct ImagePixel
    {
        public Vector2 Position;
        public Color Color;
    }

    [BurstCompile]
    public struct PaintingJob : IJobParallelFor
    {
        public int BrushScale;
        public int BrushWidth;
        public int BrushHeight;
        public int TargetWidth;
        public int TargetHeight;
        public int PaintPositionX;
        public int PaintPositionY;
        public Color BrushColor;
        public NativeArray<ImagePixel> ImagePixelColors;

        public void Execute(int index)
        {
            int i = index % BrushHeight;
            int j = index / BrushWidth;
            int x = PaintPositionX + i - BrushHeight * BrushScale / 2;
            int y = PaintPositionY + j - BrushWidth * BrushScale / 2;
            if (x < 0 || x >= TargetWidth || y < 0 || y >= TargetHeight) return;

            ImagePixelColors[index] = new ImagePixel
            {
                Color = BrushColor,
                Position = new Vector2(x, y)
            };
        }
    }

This is Function when i execute my job on Update:

void PaintingWithBurst()
        {
            _imagePixels = new NativeArray<Vector2>(_textureTarget.width * _textureTarget.height, Allocator.TempJob);
            _imagePixelColors =
                new NativeArray<ImagePixel>(_textureTarget.width * _textureTarget.height, Allocator.TempJob);
            Vector3 screenPosition = Input.mousePosition;
            screenPosClicked = _camera.ScreenToViewportPoint(screenPosition);
            _paintingJob = new PaintingJob
            {
                BrushScale = brushScale,
                BrushWidth = _textureBrush.width,
                BrushHeight = _textureBrush.height,
                TargetWidth = _textureTarget.width,
                TargetHeight = _textureTarget.height,
                PaintPositionX = (int)(screenPosClicked.x * _textureTarget.width),
                PaintPositionY = (int)(screenPosClicked.y * _textureTarget.height),
                BrushColor = brushColor,
                ImagePixelColors = _imagePixelColors,
            };
            _jobHandle = _paintingJob.Schedule(_textureBrush.width * _textureBrush.height, 64);

            _jobHandle.Complete();
            _imagePixelColors = _paintingJob.ImagePixelColors;

            for (int i = 0; i < _imagePixelColors.Length; i++)
            {
                int x = (int)_imagePixelColors[i].Position.x;
                int y = (int)_imagePixelColors[i].Position.y;
                _textureTarget.SetPixel(x, y, _imagePixelColors[i].Color);
            }

            _textureTarget.Apply();

            _imagePixels.Dispose();
            _imagePixelColors.Dispose();
        }

And this is my old code version for main thread(not using job burst), and it run faster:

 void Painting()
        {
            // Get the position of the mouse click in screen space
            Vector3 screenPosition = Input.mousePosition;

            // Convert the screen space position to img space
            screenPosClicked = _camera.ScreenToViewportPoint(screenPosition);
            // Convert the screen space position to world space
            worldPosClicked = _camera.ScreenToWorldPoint(screenPosition);

            // Calculate the x and y indices of the pixel within the target image
            int xAxisPixel = (int)(screenPosClicked.x * _textureTarget.width);
            int yAxisPixel = (int)(screenPosClicked.y * _textureTarget.height);


            for (int i = 0; i < _textureBrush.height * brushScale; i++)
            {
                for (int j = 0; j < _textureBrush.width * brushScale; j++)
                {
                    int x = xAxisPixel + i - _textureBrush.height * brushScale / 2;
                    int y = yAxisPixel + j - _textureBrush.width * brushScale / 2;
                    if (x < 0 || x >= _textureTarget.width || y < 0 || y >= _textureTarget.height) continue;

                    Color eraseColor = _textureBrush.GetPixel(i % _textureBrush.width, j % _textureBrush.height);
                    Color currentColor = _textureTarget.GetPixel(x, y);
                    Color newColor = Color.Lerp(currentColor, brushColor, brushStrength * eraseColor.a);
                    _textureTarget.SetPixel(x, y, newColor);
                }
            }


         
            _textureTarget.Apply();
        }

This is the way i call my job and normal solution on main thread:

void Update()
        {
            if (Input.GetMouseButton(0))
            {
                PaintingWithBurst();
                // Painting();
            }
        }

This is profiler while i run with burst - PaintingWithBurst():

This is profiler while i run without burst - Painting():

Note: I enabled Burst Compilation, Safety Check On.
Noy have any Log Error. And it run Slower than my old Solution. What am i wrong ?

Your job is no multithreaded.
Your job is not bursted.
Your job uses vectors instead floats. Like Vector3 should be float3.
NativeArray inside job is not marked NativeDisbaleSafetyRestrictions

Possibly other things too. But try to correct these first.

1 Like

None of this is true. Look at the profile capture closely. There’s a green parallel sliver. The issue isn’t the job as much as it is what is outside the job.

Issue 1) You are allocating two NativeArrays, one that contains positions, and one that contains colors and positions. The positions are completely unnecessary. What you actually want is a single NativeArray.
Issue 2) You are clearing the NativeArrays on the main thread. Use the optional fourth constructor argument NativeArrayOptions.UninitializedMemory.
Issue 3) You are not taking advantage of native APIs for setting the texture. Use LoadRawTextureData() and then Apply().

1 Like

Or better - GetRawTextureData → modify texture data directly without allocating additional arrays at all → Apply()

1 Like

Or even better → use compute shader :slight_smile:

If you know the internal texture format, then 100% this. But if not, what I proposed is a good stepping stone.

1 Like

Well, that will require you to use RenderTexture and swapping render target with Blit and ReadPixels back and forth, in common case burst compiled job with writing directly to raw texture data will show you pretty good performance, probably even better in good half of cases (well depend of course on size, format, target API, hardware, operations you’re doing in your logic for every pixel, [put more circumstances]):smile:

1 Like

Thank you.
DreamingImLatios.
I fixed it. My problem is that i set wrong size for NativeArray of job.
I refactored and used deep profiler and check that:
My Texture.SetPixel() is called 2073600 times. It equals 1920x1080(same as my target texture size).
So i change size of NativeArr to size of my Brush Texture and everything work correctly.
Thanks everyone !

1 Like