Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[CoherentNoise] [ThreadNinja] Strange random bug while multithreading

Discussion in 'Scripting' started by TheGabelle, Feb 2, 2017.

  1. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Quick Background:
    I’m a self-taught hobbyist (tinkering since Unity 3.5) who is working on my first long term project. I typically can overcome any problem or bug by taking a stroll through google results for a few days, maybe even a week or two. Tons of online resources. One particular issue, though, has become an outlier this month. The heart of the issue lies in a subject way over my head. Multithreading. Before I dive head first into research for several weeks, if not months, I’d like to throw the problem your way and see if anyone has any quick and dirty solutions. I’d rather not halt progress.

    The Bug:
    My goal is to generate height maps using the ‘CoherentNoise’ library on several threads to speed up world generation process. The height maps are for individual terrains which are tiled together to emulate a massive finite world. Everything works perfectly when done on a single background ‘ThreadNinja’ thread (by Ciela Spike). When I dispatch several threads to handle a portion of the total terrains, the results aren’t consistent. The failure appears to caused by the ‘Function’ generator since that’s the only generator acting up. Or perhaps I’m using ThreadNinja poorly.

    Note: I’m generating textures which are applied to tiled quads to make investigation easier.


    Single Background Thread Code:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEditor;
    5. using CoherentNoise;
    6. using CoherentNoise.Generation;
    7. using CoherentNoise.Generation.Displacement;
    8. using CoherentNoise.Generation.Fractal;
    9. using CoherentNoise.Generation.Modification;
    10. using CoherentNoise.Generation.Patterns;
    11. using CoherentNoise.Generation.Voronoi;
    12. using CoherentNoise.Texturing;
    13. using CielaSpike;
    14. using System.Threading;
    15.  
    16. public class FuncTestSingleThread : MonoBehaviour {
    17.  
    18.     [Header("World Settings")]
    19.     [Space(10)]
    20.     public GameObject prefab;
    21.     public int seed = 1;
    22.     public int size = 64;
    23.     public int count = 5;
    24.  
    25.     [Header("Mask Settings")]
    26.     [Space(10)]
    27.     public float freq = 3f;
    28.     public float power = 0.1f;
    29.     public int octaves = 3;
    30.     public float iscale = 1.1f;
    31.     int xindex = 0;
    32.     int yindex = 0;
    33.     public AnimationCurve curve;
    34.  
    35.     [Header("Mountainous Terrain Settings")]
    36.     [Space(10)]
    37.     public float ridgeExp = 0.4f;
    38.     public float ridgeOffset = .75f;
    39.     public float ridgeGain = 13.0f;
    40.     public float ridgeFreq = 0.5f;
    41.     public int ridgeOctaves = 8;
    42.  
    43.     // other stuff
    44.     Vector3 mypos;
    45.     Quaternion myq;
    46.  
    47.     void Start(){
    48.         mypos = transform.position;
    49.         myq = Quaternion.identity;
    50.         StartCoroutine(StartAsync());
    51.  
    52.     }
    53.  
    54.  
    55.     IEnumerator StartAsync(){
    56.         Debug.Log("StartAsync()");
    57.         Task task;
    58.         yield return this.StartCoroutineAsync(FuncToThread(), out task);
    59.         Debug.Log ("DONE!");
    60.     }
    61.  
    62.  
    63.     IEnumerator FuncToThread(){
    64.         LogAsync ("FuncToThread Started");
    65.         //
    66.         Function opt = new Function((x,y,z) => { return curve.Evaluate(Vector2.Distance (new Vector2(x,y), new Vector2(count*0.5f,count*0.5f))/(count/2 * iscale));});
    67.  
    68.         Turbulence trb = new Turbulence (opt, seed);
    69.         trb.Power = power;
    70.         trb.Frequency = freq;
    71.         trb.OctaveCount = octaves;
    72.  
    73.         RidgeNoise mts = new RidgeNoise (seed);
    74.         mts.Exponent = ridgeExp;
    75.         mts.Offset = ridgeOffset;
    76.         mts.Gain = ridgeGain;
    77.         mts.Frequency = ridgeFreq;
    78.         mts.OctaveCount = ridgeOctaves;
    79.  
    80.         Generator hmap = (mts * 0.5f + 0.25f) * trb;
    81.  
    82.         for (int tileX = 0; tileX < count; tileX++) {
    83.             for (int tileY = 0; tileY < count; tileY++) {
    84.                 int i = 0;
    85.                 //float highest = -10f;
    86.                 //float lowest = 10f;
    87.                 yield return Ninja.JumpToUnity;
    88.                 Texture2D img = new Texture2D(size,size);
    89.                 Color[] pixels = new Color[size*size];
    90.                 GameObject tile = Instantiate(prefab, mypos + new Vector3(tileX, tileY, 0), myq, transform);
    91.                 yield return Ninja.JumpBack;
    92.  
    93.                 for (int x = 0; x < size; x++) {
    94.                     for (int y = 0; y < size; y++) {
    95.                         float sx = (float)x / (size - 1f) + (float)tileY;
    96.                         float sy = (float)y / (size - 1f) + (float)tileX;
    97.                         float num = hmap.GetValue (sx, sy, 0f);
    98.                         pixels[i] = new Color (num, num, num, 1);
    99.                         i++;
    100.                     }
    101.                 }
    102.                 yield return Ninja.JumpToUnity;
    103.                 img.SetPixels (pixels);
    104.                 img.Apply (true);
    105.                 tile.GetComponent<Renderer> ().material.SetTexture("_EmissionMap", (Texture)img);
    106.                 yield return Ninja.JumpBack;
    107.             }
    108.         }
    109.         yield return Ninja.JumpToUnity;
    110.     }
    111.  
    112.  
    113.     private void LogAsync(string msg)
    114.     {
    115.         Debug.Log("[LogAsync]   " + msg);
    116.     }
    117. }

    Single Background Results:




    Multiple background Threads Code:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEditor;
    5. using CoherentNoise;
    6. using CoherentNoise.Generation;
    7. using CoherentNoise.Generation.Displacement;
    8. using CoherentNoise.Generation.Fractal;
    9. using CoherentNoise.Generation.Modification;
    10. using CoherentNoise.Generation.Patterns;
    11. using CoherentNoise.Generation.Voronoi;
    12. using CoherentNoise.Texturing;
    13. using CielaSpike;
    14. using System.Threading;
    15.  
    16. public class FuncTestMultiThread : MonoBehaviour {
    17.  
    18.     [Header("World Settings")]
    19.     [Space(10)]
    20.     public GameObject prefab;
    21.     public int seed = 1;
    22.     public int size = 64;
    23.     public int chunkGridRoot = 5;
    24.  
    25.     [Header("Mask Settings")]
    26.     [Space(10)]
    27.     public float freq = 3f;
    28.     public float power = 0.1f;
    29.     public int octaves = 3;
    30.     public float iscale = 1.1f;
    31.     int xindex = 0;
    32.     int yindex = 0;
    33.     public AnimationCurve curve;
    34.  
    35.     [Header("Mountainous Terrain Settings")]
    36.     [Space(10)]
    37.     public float ridgeExp = 0.4f;
    38.     public float ridgeOffset = .75f;
    39.     public float ridgeGain = 13.0f;
    40.     public float ridgeFreq = 0.5f;
    41.     public int ridgeOctaves = 8;
    42.  
    43.     // other stuff
    44.     Vector3 mypos;
    45.     Quaternion myq;
    46.     int coreCount;
    47.     int chunkCount;
    48.  
    49.  
    50.     void Start(){
    51.         mypos = transform.position;
    52.         myq = Quaternion.identity;
    53.         coreCount = SystemInfo.processorCount;
    54.         chunkCount = chunkGridRoot * chunkGridRoot;
    55.         StartCoroutine(StartAsync());
    56.     }
    57.  
    58.  
    59.     IEnumerator StartAsync(){
    60.         int cc = coreCount > 1 ? coreCount - 1 : 1;        // reserve one core, hopefully this helps the main thread run quicker
    61.         Task[] tasks;                                    // array of tasks -- ThreadNinja
    62.         int num = Mathf.CeilToInt(chunkCount / cc);        // max chunks for all threads except the last
    63.         int sc = 0;                                        // start chunk
    64.         int ec = 0;                                        // end chunk
    65.         Debug.Log ("START");
    66.  
    67.         if (cc >= chunkCount) {                            // if there less chunks (or same number of chunks) than targeted threads
    68.             tasks = new Task[chunkCount];
    69.             for (int i = 0; i < chunkCount; i++) {
    70.                 // there is no 'yeild return' here because it prevents the threads from running at the same time
    71.                 this.StartCoroutineAsync (FuncToThread (i, i, i), out tasks [i]);
    72.             }
    73.         } else {                                        // There are more chunks than targeted threads.
    74.             tasks = new Task[cc];
    75.             for (int c = 0; c < cc; c++) {                // For each targeted thread...
    76.                 if (c == cc - 1) {                        // if this is the last thread...
    77.                     sc = ec;
    78.                     ec = chunkCount;
    79.                 }else if(c==0){                            // if this is the first thread...
    80.                     sc = 0;
    81.                     ec = num;
    82.                 } else {                                // if this is not the first or last thread...
    83.                     sc += num;
    84.                     ec += num;
    85.                 }
    86.                 Debug.Log ("Thread " + c+ "  start chunk: " + sc + "  end chunk: " +ec);
    87.                 // there is no 'yeild return' here because it prevents the threads from running at the same time
    88.                 this.StartCoroutineAsync (FuncToThread (c, sc, ec), out tasks [c]);        // start thread
    89.             }
    90.         }
    91.         Debug.Log ("GenChunk() FINISHED.");
    92.         yield return null;
    93.     }
    94.  
    95.  
    96.     IEnumerator FuncToThread(int threadnumber, int startchunk, int endchunk){
    97.         LogAsync ("FuncToThread Started");
    98.  
    99.         // opt might be the source of the error
    100.         Function opt = new Function((x,y,z) => { return curve.Evaluate(Vector2.Distance (new Vector2(x,y), new Vector2(chunkGridRoot*0.5f,chunkGridRoot*0.5f))/(chunkGridRoot/2 * iscale));});
    101.  
    102.         Turbulence trb = new Turbulence (opt, seed);
    103.         trb.Power = power;
    104.         trb.Frequency = freq;
    105.         trb.OctaveCount = octaves;
    106.  
    107.         RidgeNoise mts = new RidgeNoise (seed);
    108.         mts.Exponent = ridgeExp;
    109.         mts.Offset = ridgeOffset;
    110.         mts.Gain = ridgeGain;
    111.         mts.Frequency = ridgeFreq;
    112.         mts.OctaveCount = ridgeOctaves;
    113.  
    114.         Generator hmap = (mts * 0.5f + 0.25f) * trb;
    115.  
    116.         for (int chunk = startchunk; chunk < endchunk; chunk++) {
    117.             int i = 0;                                                // used to track 1d index during a 2d loop
    118.             float tileX = chunk % chunkGridRoot;                    // breaking a 1d into a 2d array, x index
    119.             float tileY = Mathf.Floor(chunk / chunkGridRoot);        // breaking a 1d into a 2d array, y index
    120.             yield return Ninja.JumpToUnity;                            // ThreadNinja starts working on the main thread
    121.             Texture2D img = new Texture2D(size,size);        // Our chunk image ('tile' == 'chunk')
    122.             Color[] pixels = new Color[size*size];            // img pixels. Our 'heightmap', which is texture2D at this time for demonstration
    123.             GameObject tile = Instantiate(prefab, mypos + new Vector3(tileX, tileY, 0), myq, transform); // Our quad (prefab set in inspector)
    124.             tile.name = "Chunk("+chunk+")_X"+tileX+"_Y"+tileY;
    125.             yield return Ninja.JumpBack;                            // ThreadNinja returns to this background thread
    126.  
    127.             for (int x = 0; x < size; x++) {                            // 2d loop to fill pixels
    128.                 for (int y = 0; y < size; y++) {                        // ^
    129.                     float sx = (float)x / (size - 1) + (float)tileY;    // 'CoherentNoise'operates on 0..1, increments of 1 shift the generator by a full tile
    130.                     float sy = (float)y / (size - 1) + (float)tileX;    // ^
    131.                     float num = hmap.GetValue(sx,sy, 0);                    // change 'hmap' to 'trb' or 'opt' to view precursors
    132.                     pixels[i] = new Color (num, num, num, 1);                // grayscale image, so rgb are all the same value. alpha is 1.
    133.                     i++;                                                    // increment pixel indexor
    134.                 }
    135.             }
    136.  
    137.             // generate terrain and chunk component data
    138.             yield return Ninja.JumpToUnity;                        // ThreadNinja starts working on the main thread again.
    139.             // apply component data
    140.             img.SetPixels (pixels);
    141.             img.Apply (true);
    142.             tile.GetComponent<Renderer> ().material.SetTexture("_EmissionMap", (Texture)img); // emission, not, albedo, so lighting doesn't screw with our inspection
    143.             yield return Ninja.JumpBack;                        // ThreadNinja returns to this background thread        
    144.         }
    145.         yield return Ninja.JumpToUnity;        // chunks loop is finished.                                    // The loop is finished. ThreadNinja goes back to the main thread again.
    146.     }
    147.      
    148.  
    149.     private void LogAsync(string msg)                        // used as a debugger while outside Unity's main loop
    150.     {
    151.         Debug.Log("[LogAsync]   " + msg);
    152.     }
    153. }

    Multiple background Threads Result:
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,515
    Without looking at anything too deeply, Unity3D is single-threaded for the purposes of you interacting with the Unity API. That means you need to provide marshaling that uses only C# primitives that marshal delegates back to some other in-thread queue to apply any changes or request data from Unity objects.

    If your code needs a "multi-threaded feel" to it, then use Unity3D's coroutines, which are basically poor-man's threads that are guaranteed to execute serially and all in the main thread, where you have full access to all Unity objects.

    In short, don't use System.Threading threads in Unity unless you really know WTF you're trying to accomplish and where the benefits will be.
     
  3. Dameon_

    Dameon_

    Joined:
    Apr 11, 2014
    Posts:
    542
    Actually, there are ways to communicate with the main thread that are relatively simple. I tend to have a script that maintains a list of objects inheriting from an Interface that it runs every Update. Marshaling is definitely not needed.

    OP:
    Threading problems can be difficult to trace, and your description of your problem is too vague to be much use. "Not consistent" doesn't tell us enough...not consistent with what? Does the same seed result in different noise?

    The problem with scripts that do multi-threading for you is that there are pitfalls in multi-threading that you need to be aware of when using it. You need to know how to make those scripts in the first place, and THEN they're useful. So learn multi-threading. It's actually pretty simple, once you know how to do it, and will likely only take you a few days to a week to get to know. Once you know it, you have a powerful tool under your belt.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    I wouldn't mind taking a look at it (not right now, at the shop on my linux machine, and it really doesn't like the linux build of unity).

    What is this 'Turbulence' class you're using? Is this part of the CoherrentNoise library you reference in the script? Where'd you get that?

    From the looks of your results in the video, I think it's a maths issue. But just reading code here in the browser is a nuisance... so I'd like more hands on to see what it probably is. If you have a small demo project of it already done up that you could zip up and share, I'll certainly look at it in an hour or so when I get home.
     
  5. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Unity 5.5.1f1 Project: https://drive.google.com/open?id=0B4z6sM8WThTYdVRIdTNYQ3FzUnc


    You're right. Spike's ThreadNinja library is supposed to provide a high level approach to threading / multithreading. Without a solid foundation on threading it is possible I'm using the library wrong. I may link this forum thread in Spike's asset forum thread later on.


    I don't think I can dodge it any longer, so I've started digging into C# threading. Any discrepancies unique to Unity I should be aware of right away? I've read it can be a bit tricky to work with Unity's main thread efficiently.


    I've linked the project, but I'll try explaining it better. I'm chaining CoherentNoise functions to generate terrain heightmaps. Currently the heightmaps are rendered as grayscale images on tiled quads. This process takes a while to complete, so I've implemented ThreadNinja to retain a tolerable frame rate during the generation process. My implementation of the process works wonderfully when performed on a single ThreadNinja thread. My bug occurs when the process is performed over several ThreadNinja threads. Sometimes The multithreaded version of my process works perfectly, and other times there are large sections of the heightmaps with values of 0f. The bug results aren't consistent and I am getting no errors in the unity console. The bug appears to be a math issue that occurs during the chained progression of the CoherentNoise generators.

    I have a suspicion:
    One of the generators 'Function' takes in a function that accepts three floats (x,y,z) and returns a single float value. When this generator is isolated and generates the heightmaps, the results have math errors (black and white dots, as well as large black blocks, where they theoretically shouldn't be). The method the 'Function' generator works with uses Unity's Vector2 and Vector2.Distance, and AnimationCurve and AnimationCurve.Evaluate. Perhaps there is an issue with multiple threads accessing these things.

    I think so too, detailed above.
     
  6. Dameon_

    Dameon_

    Joined:
    Apr 11, 2014
    Posts:
    542
    The only discrepancy is that Unity's main thread won't let you interact with it from another thread...but there are quite safe ways of interacting.
    I can tell you certainly that none of those are the problem. I've used all of those with multi-threading, without any problems. Unless there was some new multi-threading breaking bug in the very latest update (unlikely).

    Running the code on my machine, it looks like your threads are deadlocking at some point. My guess is that it's due to how Thread Ninja uses a single lock object to "jump" between the main thread and the background thread. If multiple threads attempt to call either of the Ninja.Jump methods, then you'll wind up with deadlock. I'm not familiar with that library, but this whole coroutine way of dealing with threads is confusing and makes the whole thing harder to sort out than it should be.

    Rather than "jumping" between processing on the main thread and your own thread, you should do whatever preliminary work you can on the main thread (create all the textures and GameObjects at once), then dispatch your threads to do their own processing, and finally gather all their results when the threads have finished their work. If you can structure things so that you aren't doing those yield return statements, I'm willing to bet that your deadlock problem will go away.
     
  7. AndyGainey

    AndyGainey

    Joined:
    Dec 2, 2015
    Posts:
    216
    Adding to the speculation, it's possible that your math-heavy calculations are working just fine, and the failure is when doing the img.SetPixels() or img.Apply(). Within the generation loops, try keeping a sum of all the pixel values. Just before calling img.SetPixels(), do a Debug.Log(pixelSum). If that value is always some large number for every single tile, but the height map still comes out as all black for some tiles, then you know that your generation is fine, and the issue just lies with transferring the data over from the raw pixel array into a texture. On the other hand, if the sum is actually zero, then you know that the generator is failing to produce the proper pixel values for whatever reason.
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    So I ran some tests with it earlier today, but then I ended up going to Ft. Laudy Daudy to play an escape room... now I'm back, but gotta eat fried chicken... long story short, I'm quickly typing out my findings.

    Anyways...

    Cleaned up the code a little bit because some of the unnecessary stuff in it was confusing me.

    Code (csharp):
    1.  
    2. using System.Collections;
    3. using UnityEngine;
    4. using CoherentNoise;
    5. using CoherentNoise.Generation;
    6. using CoherentNoise.Generation.Displacement;
    7. using CoherentNoise.Generation.Fractal;
    8. using CielaSpike;
    9.  
    10. public class FuncTestMultiThread : MonoBehaviour {
    11.  
    12.     [Header("World Settings")]
    13.     [Space(10)]
    14.     public GameObject prefab;
    15.     public int seed = 1;
    16.     public int size = 64;
    17.     public int chunkGridRoot = 5;
    18.  
    19.     [Header("Mask Settings")]
    20.     [Space(10)]
    21.     public float freq = 3f;
    22.     public float power = 0.1f;
    23.     public int octaves = 3;
    24.     public float iscale = 1.1f;
    25.     public AnimationCurve curve;
    26.  
    27.     [Header("Mountainous Terrain Settings")]
    28.     [Space(10)]
    29.     public float ridgeExp = 0.4f;
    30.     public float ridgeOffset = .75f;
    31.     public float ridgeGain = 13.0f;
    32.     public float ridgeFreq = 0.5f;
    33.     public int ridgeOctaves = 8;
    34.  
    35.     // other stuff
    36.     Vector3 mypos;
    37.  
    38.  
    39.  
    40.     void Start(){
    41.         mypos = transform.position;
    42.         StartCoroutine(StartAsync());
    43.     }
    44.  
    45.  
    46.     IEnumerator StartAsync()
    47.     {
    48.         int chunkCount = chunkGridRoot * chunkGridRoot;
    49.         int cc = Mathf.Max(SystemInfo.processorCount - 1, 1); // reserve one core, hopefully this helps the main thread run quicker
    50.         Debug.Log ("START");
    51.        
    52.         int subcnt = Mathf.CeilToInt((float)chunkCount / cc);
    53.         for(int i = 0; i < cc; i++)
    54.         {
    55.             int sc = i * subcnt;
    56.             int ec = Mathf.Min(sc + subcnt, chunkCount);
    57.             this.StartCoroutineAsync(FuncToThread(i, sc, ec));
    58.         }
    59.  
    60.  
    61.         Debug.Log ("GenChunk() FINISHED.");
    62.         yield return null;
    63.     }
    64.  
    65.  
    66.     IEnumerator FuncToThread(int threadnumber, int startchunk, int endchunk){
    67.         LogAsync ("FuncToThread Started");
    68.  
    69.         // opt might be the source of the error
    70.         Function opt = new Function((x, y, z) =>
    71.         {
    72.             float v = 0f;
    73.             v = Vector2.Distance(new Vector2(x, y), new Vector2(chunkGridRoot / 2f, chunkGridRoot / 2f));
    74.             v = v / (iscale * chunkGridRoot * 0.5f);
    75.             //v = curve.Evaluate(v);
    76.             return v;
    77.         });
    78.  
    79.         Turbulence trb = new Turbulence(opt, seed);
    80.         trb.Power = power;
    81.         trb.Frequency = freq;
    82.         trb.OctaveCount = octaves;
    83.  
    84.         RidgeNoise mts = new RidgeNoise(seed);
    85.         mts.Exponent = ridgeExp;
    86.         mts.Offset = ridgeOffset;
    87.         mts.Gain = ridgeGain;
    88.         mts.Frequency = ridgeFreq;
    89.         mts.OctaveCount = ridgeOctaves;
    90.  
    91.         Generator hmap = (mts * 0.5f + 0.25f) * trb;
    92.  
    93.         for (int chunk = startchunk; chunk < endchunk; chunk++) {
    94.             float tileX = chunk % chunkGridRoot;                    // breaking a 1d into a 2d array, x index
    95.             float tileY = Mathf.Floor(chunk / chunkGridRoot);        // breaking a 1d into a 2d array, y index
    96.             Color[] pixels = new Color[size * size];            // img pixels. Our 'heightmap', which is texture2D at this time for demonstration
    97.  
    98.             for (int x = 0; x < size; x++) {                            // 2d loop to fill pixels
    99.                 for (int y = 0; y < size; y++) {                        // ^
    100.                     float sx = ((float)x / size) + tileX;
    101.                     float sy = ((float)y / size) + tileY;
    102.                     float num = hmap.GetValue(sx,sy, 0);                    // change 'hmap' to 'trb' or 'opt' to view precursors
    103.                     pixels[(x + y * size)] = new Color (num, num, num, 1);                // grayscale image, so rgb are all the same value. alpha is 1.                                                    // increment pixel indexor
    104.                 }
    105.             }
    106.  
    107.             // generate terrain and chunk component data
    108.             yield return Ninja.JumpToUnity;                     // ThreadNinja starts working on the main thread again.
    109.             Texture2D img = new Texture2D(size, size);      // Our chunk image ('tile' == 'chunk')
    110.             GameObject tile = Instantiate(prefab, mypos + new Vector3(tileX, tileY, 0), Quaternion.identity, transform); // Our quad (prefab set in inspector)
    111.             tile.name = "Chunk(" + chunk + ")_X" + tileX + "_Y" + tileY;
    112.             img.SetPixels (pixels);
    113.             img.Apply (true);
    114.             tile.GetComponent<Renderer> ().material.SetTexture("_EmissionMap", (Texture)img); // emission, not, albedo, so lighting doesn't screw with our inspection
    115.             yield return Ninja.JumpBack;                        // ThreadNinja returns to this background thread          
    116.         }
    117.     }
    118.        
    119.  
    120.     private void LogAsync(string msg)                        // used as a debugger while outside Unity's main loop
    121.     {
    122.         Debug.Log("[LogAsync]   " + msg);
    123.     }
    124. }
    125.  
    A few issues arised.

    1) your pixels indexing was rotated weird ending up with odd results. Fixed that.

    2) same goes for your sx,sy, probably because you noticed the rotation issue and attempted to fix it, but it just added in more oddness

    3) and the BIG culprit.... AnimationCurve.Evaluate

    Turns out that the AnimationCurve is not thread safe. Removing it from the scenario makes it work no matter what. But including it makes everything break. There must be stateful field inside the AnimationCurve implementation that doesn't allow it to be called at the same time.

    There is also a stateful issue with the Generator Functions, but it only occurs if you define 1 Generator used for all threads. But your code has one for each thread, so it doesn't occur. So you're good there.

    Anyways... this all sort of makes sense, Unity outright says all of its library should not be considered thread safe. It just happens some is by coincidence (like the Vector2 methods).

    Maybe find an alternative for the AnimationCurve that is thread safe?
     
    AndyGainey likes this.
  9. Dameon_

    Dameon_

    Joined:
    Apr 11, 2014
    Posts:
    542
    This really surprises me...I have multi-threaded code that uses AnimationCurves without any issue.

    An easy work-around, since AnimationCurves are structs, is just to create a local reference so you get a local copy, rather than using your global reference.[/code][/QUOTE]
     
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,515
    10/10 would definitely debug with @lordofduct again
     
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    They're not structs (or at least not in my version)... but they are clonable.

    And yep... fixed the problem:
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using UnityEngine;
    4. using CoherentNoise;
    5. using CoherentNoise.Generation;
    6. using CoherentNoise.Generation.Displacement;
    7. using CoherentNoise.Generation.Fractal;
    8. using CielaSpike;
    9.  
    10. public class FuncTestMultiThread : MonoBehaviour {
    11.  
    12.     [Header("World Settings")]
    13.     [Space(10)]
    14.     public GameObject prefab;
    15.     public int seed = 1;
    16.     public int size = 64;
    17.     public int chunkGridRoot = 5;
    18.  
    19.     [Header("Mask Settings")]
    20.     [Space(10)]
    21.     public float freq = 3f;
    22.     public float power = 0.1f;
    23.     public int octaves = 3;
    24.     public float iscale = 1.1f;
    25.     public AnimationCurve curve;
    26.  
    27.     [Header("Mountainous Terrain Settings")]
    28.     [Space(10)]
    29.     public float ridgeExp = 0.4f;
    30.     public float ridgeOffset = .75f;
    31.     public float ridgeGain = 13.0f;
    32.     public float ridgeFreq = 0.5f;
    33.     public int ridgeOctaves = 8;
    34.  
    35.     // other stuff
    36.     Vector3 mypos;
    37.  
    38.  
    39.  
    40.     void Start(){
    41.         mypos = transform.position;
    42.         StartCoroutine(StartAsync());
    43.     }
    44.  
    45.  
    46.     IEnumerator StartAsync()
    47.     {
    48.         int chunkCount = chunkGridRoot * chunkGridRoot;
    49.         int cc = Mathf.Max(SystemInfo.processorCount - 1, 1); // reserve one core, hopefully this helps the main thread run quicker
    50.         Debug.Log ("START");
    51.      
    52.         int subcnt = Mathf.CeilToInt((float)chunkCount / cc);
    53.         for(int i = 0; i < cc; i++)
    54.         {
    55.             int sc = i * subcnt;
    56.             int ec = Mathf.Min(sc + subcnt, chunkCount);
    57.             this.StartCoroutineAsync(FuncToThread(i, sc, ec));
    58.         }
    59.  
    60.  
    61.         Debug.Log ("GenChunk() FINISHED.");
    62.         yield return null;
    63.     }
    64.  
    65.  
    66.     IEnumerator FuncToThread(int threadnumber, int startchunk, int endchunk){
    67.         LogAsync ("FuncToThread Started");
    68.  
    69.         // opt might be the source of the error
    70.         var crv = new AnimationCurve(curve.keys); //clone for posterity
    71.         Function opt = new Function((x, y, z) =>
    72.         {
    73.             float v = 0f;
    74.             v = Vector2.Distance(new Vector2(x, y), new Vector2(chunkGridRoot / 2f, chunkGridRoot / 2f));
    75.             v = v / (iscale * chunkGridRoot * 0.5f);
    76.             v = crv.Evaluate(v);
    77.             return v;
    78.         });
    79.  
    80.         Turbulence trb = new Turbulence(opt, seed);
    81.         trb.Power = power;
    82.         trb.Frequency = freq;
    83.         trb.OctaveCount = octaves;
    84.  
    85.         RidgeNoise mts = new RidgeNoise(seed);
    86.         mts.Exponent = ridgeExp;
    87.         mts.Offset = ridgeOffset;
    88.         mts.Gain = ridgeGain;
    89.         mts.Frequency = ridgeFreq;
    90.         mts.OctaveCount = ridgeOctaves;
    91.  
    92.         Generator hmap = (mts * 0.5f + 0.25f) * trb;
    93.  
    94.         for (int chunk = startchunk; chunk < endchunk; chunk++) {
    95.             float tileX = chunk % chunkGridRoot;                    // breaking a 1d into a 2d array, x index
    96.             float tileY = Mathf.Floor(chunk / chunkGridRoot);        // breaking a 1d into a 2d array, y index
    97.             Color[] pixels = new Color[size * size];            // img pixels. Our 'heightmap', which is texture2D at this time for demonstration
    98.  
    99.             for (int x = 0; x < size; x++) {                            // 2d loop to fill pixels
    100.                 for (int y = 0; y < size; y++) {                        // ^
    101.                     float sx = ((float)x / size) + tileX;
    102.                     float sy = ((float)y / size) + tileY;
    103.                     float num = hmap.GetValue(sx,sy, 0);                    // change 'hmap' to 'trb' or 'opt' to view precursors
    104.                     pixels[(x + y * size)] = new Color (num, num, num, 1);                // grayscale image, so rgb are all the same value. alpha is 1.                                                    // increment pixel indexor
    105.                 }
    106.             }
    107.  
    108.             // generate terrain and chunk component data
    109.             yield return Ninja.JumpToUnity;                     // ThreadNinja starts working on the main thread again.
    110.             Texture2D img = new Texture2D(size, size);      // Our chunk image ('tile' == 'chunk')
    111.             GameObject tile = Instantiate(prefab, mypos + new Vector3(tileX, tileY, 0), Quaternion.identity, transform); // Our quad (prefab set in inspector)
    112.             tile.name = "Chunk(" + chunk + ")_X" + tileX + "_Y" + tileY;
    113.             img.SetPixels (pixels);
    114.             img.Apply (true);
    115.             tile.GetComponent<Renderer> ().material.SetTexture("_EmissionMap", (Texture)img); // emission, not, albedo, so lighting doesn't screw with our inspection
    116.             yield return Ninja.JumpBack;                        // ThreadNinja returns to this background thread        
    117.         }
    118.     }
    119.      
    120.  
    121.     private void LogAsync(string msg)                        // used as a debugger while outside Unity's main loop
    122.     {
    123.         Debug.Log("[LogAsync]   " + msg);
    124.     }
    125. }
    126.  
     
    TheGabelle likes this.
  12. Dameon_

    Dameon_

    Joined:
    Apr 11, 2014
    Posts:
    542
    Whoops! Been dealing with the equivalent in ParticleSystems, and assumed it was the same. That would explain why I haven't experienced this...I had forgotten that the curves a ParticleSystem uses are different. Very embarrassing.
     
  13. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    I've yet to do this. Sounds like a blast!

    I didn't know about this.

    Huh. Here I am thinking a fix was going to super complicated. Ran your script version many times with expected and weird variables and not one issue. Same goes for testing Unity's terrain objects. The fix seems so glaringly obvious now. Lesson learned. I have a strong feeling I'll be doing this a lot moving forward.

    You guys are amazing and I thank you all for taking the time. This script has been credited towards lordofduct and all in this thread.
     
    Last edited: Feb 5, 2017
  14. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    Oh man, it's so much fun.

    I met up with this couple who are building out a new location here in town, and my buddy and I are going to be doing the labour. After helping them out at a promotional spot at the science museum, basically having to repair all their electrical equipment on site, they decided to treat us to an escape room and dinner as a thankyou gesture.

    First time doing it.

    The timing worked out great too, it gave me a ton of inspiration for our gamejam this weekend.


    So going to do it again next week with some buddies.
     
    Kurt-Dekker likes this.
  15. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Got me dreaming about an escape room or haunted house implementing AR / VR for a group or teams. I really hope someone is working on something similar!
     
    lordofduct likes this.
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    So my plans are to position ourselves to design some games (they have to change out the rooms on a regular basis). And yeah, that actually was one of the ideas we were tossing around. I want to come up with modular/adaptive games that can be catered to a team when they come in.

    Often you go in and a game is designed with X teammates being there, which puts you in a pickle if you are just a duo. Vice versa if a big team goes. Having scaleable and modular game mechanics could easily fix that. AR/VR is a definite way to get some of that.
     
    TheGabelle likes this.
  17. TheGabelle

    TheGabelle

    Joined:
    Aug 23, 2013
    Posts:
    242
    Oh wow. If this becomes a reality, I beg you to document it to some degree. AR/VR is still fairly new territory and whatever you guys would implement would likely be ground-breaking to some extent. Wouldn't dare ask for tutorials, I know they can become quite a burden while exploring new ideas. I'm pretty excited for you and hope you get the opportunity.