{"id":3922,"date":"2019-12-07T22:44:23","date_gmt":"2019-12-07T21:44:23","guid":{"rendered":"http:\/\/t-machine.org\/?p=3922"},"modified":"2019-12-07T22:45:45","modified_gmt":"2019-12-07T21:45:45","slug":"speed-up-unity2019-terrains-by-a-factor-of-20x-using-buffer-blockcopy","status":"publish","type":"post","link":"http:\/\/new.t-machine.org\/index.php\/2019\/12\/07\/speed-up-unity2019-terrains-by-a-factor-of-20x-using-buffer-blockcopy\/","title":{"rendered":"Speed-up Unity2019 Terrains by a factor of 20x using Buffer.BlockCopy"},"content":{"rendered":"<p>Unity Terrain is a good Terrain renderer, but the API&#8217;s behind it are famously badly documented and rather clunky (most of the documentation still hasn&#8217;t been written, almost 10 years after it was launched). At Unite this year they were showing-off some of the &#8220;new Terrain&#8221; features\/tools, all of which were aimed at artists, and look great.<\/p>\n<p>But what about the Terrain itself? Unity 2019.2 and 2019.3 still have the ponderous old API and it seems we&#8217;re stuck with it for at least another few years. Today I found and fixed an issue in my custom Terrain-tools that took our Editor rendering from &lt; 4 FPS back to normal realtime speeds.<\/p>\n<p>The secret is to use Unity&#8217;s required float[,,] arrays (which Microsoft only partially supports in C#) but plug the gap in C# which causes them to be slow when interacting with Unity&#8217;s Serializer (which fires 6 times per frame in the Editor, magnifiying any slowdown considerably!)<\/p>\n<p>NB: As far as I can tell, you cannot fix the Unity &#8220;serialize 6 times even if it&#8217;s not needed, where only 1 would have been fine&#8221; issue, because the methods to do that only exist on Custom EditorWindows, and not on Custom Inspectors. But it&#8217;s bad practice to be slowing-down the serializer anyway, so I&#8217;m happy with fixing MY code to run fast, and then stop worrying about the Unity Serialization layers being inefficient.<\/p>\n<h2>The problem: float[,,] isn&#8217;t supported by Unity<\/h2>\n<p>Unity requires you to use float[,,] for textures\/splats\/alphamaps on their terrain.<\/p>\n<p>However, Unity has never supported multi-dimensional arrays in their engine (this is finally getting fixed sometime in 2020, I believe, with the new Serializer). So your data gets wiped every frame. Thats a pain when making Terrain-editing scripts.<\/p>\n<p>The workaround is to implement Unity&#8217;s ISerializationCallbackReceiver interface, and provide the missing code that Unity doesn&#8217;t (i.e. serialize a float[,,]). The standard way of doing this is something like:<\/p>\n<blockquote><p>\nNB: I&#8217;m only showing half of the serialize\/deserialize here, just to illustrate the point<\/p><\/blockquote>\n<p>[code language=&#8221;csharp&#8221;]<br \/>\nvoid ISerializationCallbackReceiver.OnAfterDeserialize()<br \/>\n{<br \/>\ndeltas = new float[_Serialize_2D_Length0, _Serialize_2D_Length1, _Serialize_2D_Length2];<\/p>\n<p>\/** NB: iterate in C#&#8217;s internal storage order for [,,] *\/<br \/>\nfor (int i0 = 0; i0 &amp;lt; _Serialize_2D_Length0; i0++)<br \/>\nfor (int i1 = 0; i1 &amp;lt; _Serialize_2D_Length1; i1++)<br \/>\nfor( int i2 = 0; i2 &amp;lt; _Serialize_2D_Length2; i2++ )<br \/>\ndeltas[i0,i1,i2] = _Serialize_1DArray[i0 * _Serialize_2D_Length1 * _Serialize_2D_Length2<br \/>\n+ i1 * _Serialize_2D_Length2<br \/>\n+ i2];<br \/>\n}<br \/>\n}<br \/>\n[\/code]<\/p>\n<p>&#8230;which retrieves every cell in the float[,,] from a cell in a private float[] (which Unity DOES support and will auto-serialize for you).<\/p>\n<p>The problem is that C# for-loops are extremely slow when used like this, simply because of the scale of the operation. For a typical Unity terrain, you&#8217;re copying up to 4096 x 4096 samples (your splatmap) with anywhere from 5 to 10 values for each. Each value is a 4-byte 32-bit float.<\/p>\n<p>i.e. 4k x 4k x 10 x 4 == 640 MB of data<\/p>\n<p>&#8230;which destroys your 100+ FPS frame-time, taking it to 1 FPS or worse.<\/p>\n<p>You need to copy this data in a single call, not in 640,000,000 separate method calls.<\/p>\n<p>But &#8230; how?<\/p>\n<h2>Array.Copy() to the rescue!<\/h2>\n<p>It doesn&#8217;t work. You can compile your C# class, and then the C# runtime will cry when you try to execute it:<\/p>\n<blockquote><p>\nRankException: Only single dimension arrays are supported here.<\/p><\/blockquote>\n<p>Bummer. In theory, Array.Copy() would have solved the problem &#8211; this is literally what it was designed for: bulk copying of large arrays without the overhead of doing millions of tiny copy-calls.<\/p>\n<h2>Try again &#8230; Buffer.BlockCopy()<\/h2>\n<p>Fortunately there&#8217;s another method in C# core that steps-in and saves us. I often find that when C# ties your hands behind your back, the reason it hasn&#8217;t been changed\/updated\/improved is that there&#8217;s a lesser-known behind-the-scenes low-level method that you can (ab)use to achieve what you need, and the language maintainers recommend you do that instead of them updating the mainstream stuff. Fair enough!<\/p>\n<p>The one caveat with BlockCopy is that you need to tell it the size in bytes that you&#8217;re copying NOT the number of array items.<\/p>\n<p>i.e.: [code language=&#8221;csharp&#8221;]Array.Copy( from, 0, to, 0, length )[\/code]<br \/>\nbecomes: [code language=&#8221;csharp&#8221;]Buffer.BlockCopy( from, 0, to, 0, 4 * length ) \/\/ if copying float, or int, or any of the other 32bit primitives[\/code]<\/p>\n<h2>20x faster Terrain data handling<\/h2>\n<p>The modified serialization callback becomes:<\/p>\n<p>[code language=&#8221;csharp&#8221;]<br \/>\nvoid ISerializationCallbackReceiver.OnAfterDeserialize()<br \/>\n{<br \/>\ndeltas = new float[_Serialize_2D_Length0, _Serialize_2D_Length1, _Serialize_2D_Length2];<\/p>\n<p>int bytesPerFloat = 4;<br \/>\nBuffer.BlockCopy( _Serialize_2D, 0, deltas, 0, bytesPerFloat * deltas.GetLength( 0 ) * deltas.GetLength( 1 )*deltas.GetLength( 2 ) );<br \/>\n}<br \/>\n}<br \/>\n[\/code]<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Unity Terrain is a good Terrain renderer, but the API&#8217;s behind it are famously badly documented and rather clunky (most of the documentation still hasn&#8217;t been written, almost 10 years after it was launched). At Unite this year they were showing-off some of the &#8220;new Terrain&#8221; features\/tools, all of which were aimed at artists, and [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"_links":{"self":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/3922"}],"collection":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/comments?post=3922"}],"version-history":[{"count":6,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/3922\/revisions"}],"predecessor-version":[{"id":3928,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/posts\/3922\/revisions\/3928"}],"wp:attachment":[{"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/media?parent=3922"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/categories?post=3922"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/new.t-machine.org\/index.php\/wp-json\/wp\/v2\/tags?post=3922"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}