- Home /
Worker thread blocking main thread?
I am writing my own custom terrain engine, as I want to have spherical terrain with LOD support. I have a (relatively) low poly sphere that I subdivide and deform based off of noise generated on the fly, and desired LOD.
Deforming the highest LOD chunks was causing significant lag spikes (a little over a second per chunk being processed), so I decided to put the slowest part of it (noise generation) in it's own thread (using code found here: http://answers.unity3d.com/questions/357033/unity3d-and-c-coroutines-vs-threading.html). I expected this to solve the problem, but it didn't. For some reason, even though the processing is now on it;s own thread, the main thread still has a lag spike. Here is the relevant code:
public class ThreadedJob {
private bool m_IsDone = false;
private object m_Handle = new object();
private System.Threading.Thread m_Thread = null;
public bool IsDone {
get {
bool tmp;
lock (m_Handle) {
tmp = m_IsDone;
}
return tmp;
}
set {
lock (m_Handle) {
m_IsDone = value;
}
}
}
public virtual void Start() {
m_Thread = new System.Threading.Thread(Run);
m_Thread.Start();
}
public virtual void Abort() {
m_Thread.Abort();
}
protected virtual void ThreadFunction() { }
protected virtual void OnFinished() { }
public virtual bool Update() {
if (IsDone) {
OnFinished();
return true;
}
return false;
}
private void Run() {
ThreadFunction();
IsDone = true;
}
}
public class ChunkDeform : ThreadedJob {
public int chunkID;
public int LOD;
public HeightMap heightMap;
public GameObject chunk;
public Mesh tapeMesh;
public int[] manifoldIndices;
private Mesh chunkMesh;
private Vector3[] chunkVerts;
private Vector3[] tapeVerts;
private Vector2[] chunkUVs;
private Vector2[] tapeUVs;
// for profiling
private float timeSpent;
private int threadID;
public override void Start() {
chunkMesh = chunk.GetComponent<MeshFilter>().mesh;
chunkVerts = chunkMesh.vertices;
tapeVerts = tapeMesh.vertices;
chunkUVs = chunkMesh.uv;
tapeUVs = tapeMesh.uv;
heightMap = new HeightMap();
heightMap.mapHeight = 16384;
heightMap.mapWidth = 16384;
heightMap.seed = 42;
timeSpent = Time.realtimeSinceStartup;
base.Start();
}
protected override void ThreadFunction() {
// Do your threaded task. DON'T use the Unity API here
threadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
for (int v = 0; v < chunkVerts.Length; v++) {
chunkVerts[v] = chunkVerts[v] * ((heightMap.GetValue(chunkUVs[v].x, chunkUVs[v].y) * 0.05f) + 1);
}
for (int v = 0; v < tapeVerts.Length; v++) {
tapeVerts[v] = tapeVerts[v] * ((heightMap.GetValue(tapeUVs[v].x, tapeUVs[v].y) * 0.05f) + 1);
}
}
protected override void OnFinished() {
// This is executed by the Unity main thread when the job is finished
chunkMesh.vertices = chunkVerts;
chunkMesh.RecalculateNormals();
tapeMesh.vertices = tapeVerts;
tapeMesh.RecalculateNormals();
Vector3[] chunkNormals = chunkMesh.normals;
Vector3[] tapeNormals = tapeMesh.normals;
// fix the normals
for (int tv = 0; tv < tapeVerts.Length; tv++) {
for (int i = 0; i < manifoldIndices.Length; i++) {
Vector3 sample = chunkVerts[manifoldIndices[i]];
float distance = Vector3.Distance(sample, tapeVerts[tv]);
if (distance < 0.1) {
// these match, use tape vertex normal
chunkNormals[manifoldIndices[i]] = tapeNormals[tv];
}
}
}
chunkMesh.normals = chunkNormals;
chunkMesh.RecalculateBounds();
chunk.GetComponent<MeshCollider>().sharedMesh = chunkMesh;
timeSpent = Time.realtimeSinceStartup - timeSpent;
Debug.Log("ChunkDeform tid "+threadID+": deformed in " + timeSpent.ToString() + " seconds");
}
}
In another class that runs on the main thread, the following code is run for any chunk that has yet to be deformed:
// it has not, so set up a chunk deform job
// for this LOD
GameObject chunk = transform.FindChild("Planet_LOD" + currentLOD + "." + chunkID.ToString("D3")).gameObject;
int[] mvIndices = MeshHelper.BuildManifoldVertices(chunk.GetComponent<MeshFilter>().mesh);
// store these manifold indicies for later use
chunkTree.manifoldIndices[chunkID][currentLOD] = mvIndices;
ChunkDeform deformJob = new ChunkDeform();
deformJob.chunkID = chunkID;
deformJob.LOD = currentLOD;
//deformJob.heightMap = (HeightMap)heightMap.Clone();
deformJob.chunk = chunk;
deformJob.tapeMesh = chunkTree.tape[chunkID, currentLOD].GetComponent<MeshFilter>().mesh;
deformJob.manifoldIndices = chunkTree.manifoldIndices[chunkID][currentLOD];
deformJobs[currentLOD] = deformJob;
deformJobs[currentLOD].Start();
Finally, in the Update() function of that class, I run this to check for completion:
// check on our threaded jobs
foreach (ChunkDeform cd in deformJobs) {
if (cd != null) {
if (cd.Update()) {
deformed[cd.LOD] = true;
deformJobs[cd.LOD] = null;
}
}
}
You can see I added code to check on the thread ID to ensure it is really in it's own thread, which it is. So, I am confused about why this would still cause lag even though the heavy processing is on its own thread now. Is there something I am missing? I feel like maybe I have a misunderstanding
I assume you have tested to confirm that the delay is not cause by some function you call that is NOT in the thread, like .. $$anonymous$$eshHelper.Build$$anonymous$$anifoldVertices
@Glurth: I'm glad you brought that up. While I had did some testing of speed outside of the thread, I did not test it well enough it seems!
The slow points are the processing of the manifold vertices (either gathering them in the first place, or looping through them; unsure yet); and, surprisingly enough, assignment of the meshcollider.
I can easily fix the issue with the manifold vertex lookup by altering it to be thread safe. How in the hell do I fix the issue of assigning the meshcollider though, considering Unity API is not thread safe!?
you mean here?
chunk$$anonymous$$esh.RecalculateBounds();
chunk.GetComponent<$$anonymous$$eshCollider>().shared$$anonymous$$esh = chunk$$anonymous$$esh;
I would suspect it's the recalculatebounds function, rather than the assignment of the collider. Or have you eli$$anonymous$$ated that?
If you construct the mesh properly, I don't think you will need to call recalculateBounds. That's been my experience with procedural meshes, at least DRAWING them. I have not had to implement colliders on them though, so that may be different.
@Glurth: I have eli$$anonymous$$ated RecalculateBounds() as the problem. The biggest slowdown comes from : chunk.GetComponent<$$anonymous$$eshCollider>().shared$$anonymous$$esh = chunk$$anonymous$$esh;
Ok, that IS odd. perhaps it has to do some processing internally when you make the assignment? $$anonymous$$gest sanity check: breaking it up into two lines, to further narrow it down: One line for the getcomponent call, a second line just for the assignment.
If it DOES do lots of processing during the assignment, you may need to cache your unassigned meshes. Then in a co-routine (which should have access to the API), assign everything in the cache to the appropriate collider. (I have only used threads, but not co-routines in unity so far; so take this suggestion with a grain of salt.)
Edit: another thought- have your meshes created and attached, before you even launch your thread to recompute the mesh. rather than create a new mesh that you will need to assign- modify the mesh that is ALREADY assigned.