In this article, we are going to use Half data type from the Android platform and also explore the caveats of it.
This post is add on to a previous 3 part article series.
- How to draw 2.6 million polygons on Android at 60 FPS: The Problem Statement
- How to draw 2.6 million polygons on Android at 60 FPS: The First Render
- How to draw 2.6 million polygons on Android at 60 FPS: The Optimizations
- Bonus How to draw 2.6 million polygons on Android at 60 FPS: Half the data with Half Float
Full working sample for this article series can be found on Github
In last article we rendered the full L2 dataset at 60 FPS, however, Romain Guy suggested a very tempting optimization on this reddit thread. This was pretty interesting to me as I wasn’t aware of the existence of such a class in Android Framework.
A half-precision float can be created from or converted to single-precision floats, and is stored in a short data type.
What this means is that
Half is not a primitive data type like
Short its like a wrapper/utility class over
Short type which does some bit magic to store
significand in 16 bits space. This also means that it offers less precision, the same document also lists available precision ranges for numbers ranging between 1/16,384 to 32,768.
In theory, we can save 50% of GPU memory if we can store our vertices in a
HalfBuffer or more practically speaking a
ShortBuffer. This should also reduce GPU data transfer times. Before we weigh in on the pros and cons of using
Half, let’s implement this to see the output.
As stated before,
Half is stored in
Short natively, hence we need to store all vertex coordinates in a
ShortArray. Since mathematical operations are not supported yet we do all calculations in
Float and convert the final value to
toHalf() extension method. Then Half provides a convenient method
halfValue() which does this as per documentation
Returns the half-precision value of this Half as a short containing the bit representation described in Half.
This means that we now have a
half-precision Float value stored in a Short data type. This
ShortArray can be converted to
ByteBuffer just like we were converting
FloatArray before. Here is new implementation of
generateVertexData() method. I will be talking about performance in the later part of this article.
As for drawing
OpenGL ES added support for half-precision floats in
OpenGL ES Version 3. I had to bump usage of
GLES30. Rest of the
draw() method remains the same except for we now specify the data type as
GL_HALF_FLOAT where ever it was
That’s it! Here are before and after images of L2 rendering
Does this look good? We will take a closer look at that, but, here is the complete implementation of Reflectivity Layer with Half data type for now.
Performance was the primary reason for this exercise. I found that while data transfer takes half the time preprocessing(generating vertices) is taking 4 times the time. Here are the result of 5 runs
Reduced GPU transfer times are because there is only 50% data to transfer as compared to
Float. Preprocessing time got increased because of extra boxing and un-boxing I believe. In a production app preprocessing will be done off UI thread so this may be okay depending upon the use case. We are still getting 60 FPS which is expected. So far so good,
Half is looking promising. Let’s have a look at its support.
Half was introduced in API level 26 and hence can only support Android devices running Android Oreo and above. As most of the Android developers don’t have the luxury of having
minSdk=26(yet) so one needs to maintain two implementations of the same Layer/Object. That’s exactly what I have done. I created two layers
ReflectivityLayerHalfFloat and based on the Android version I use the appropriate class.
OpenGL ES 3.0 and above which most of the devices should be able to support at this point. It’s again up-to-the developer to perform that check and use appropriate implementation.
As I have mentioned before 16bit Float comes at the price of precision. A precision table is given in documentation which maps what ranges of values will have what precision. In our case latitude and longitudes vary between [-180, 180]. As per the table storing a value of 128 will give a precision of 1/8 or 0.125. This can introduce error of multiple kilometers when converted. This is evident when zoomed in on half float render. Here is a comparison
The left image represents full 32bit
Float render, while the right image represents 16bit
Half render. You can see how lines are zig-zagged in case of half float. This is significant for this use case since some people rely on these renders in realtime.
Because of the above-stated reasons and primarily precision issue, I concluded that this optimization may not be a good idea for my case. In the interest of research and experimentation here is full set of changes.