Repository: zeux/meshoptimizer (C++)

Issue:
Add dequantization functions
There do not appear to be any dequantization functions? There is `meshopt_quantizeHalf` for example, but there is no `meshopt_dequantizeHalf` that turns the quantized value back into a float value. 

I'm not sure if I might be missing something, but quantization functions without dequantization functions seem a bit limited in helpfulness, since any quantized value also needs to be turned back into a regular value once it should actually be used - and if the library doesn't offer a function for that, that seems to be an issue. 

If a way to do this exists already in this library, then an explanation of this is missing from the readme, which explains how to quantize a normal or position, but never mentions anything about how to dequantize it.


Discussion: The expectation currently is that the quantized values will be dequantized on the GPU using fixed function vertex configuration / using the shader math, which is why the dequantization functions are indeed missing.
Ok, but even if the values are expected be dequantized on the GPU, then someone still needs to write the code for the dequantization there that matches the quantization code of this library. And for that, having a reference implementation of the dequantization in C/C++ in this library would be very helpful, since porting some existing C/C++ code to HLSL is definitely easier than manually having to come up with the correct dequantization code.

In my case, I do not have control over the GPU-side of things, so I only use this library with quantization to reduce the size of a saved mesh on disk. So after loading it from disk I do need to dequantize the mesh in C++ before sending it to the GPU, so I'd just use dequantization functions of the library directly in C++ if they would exist.

For now, I let GPT-4 generate a "opposite of `meshopt_quantizeHalf`" function, it seems to work well for me so far:

```cpp
inline float meshopt_dequantizeHalf(unsigned short h)
{
	union { float f; unsigned int ui; } u;
	unsigned int s = (h & 0x8000) << 16;
	unsigned int e = (h >> 10) & 0x1F;
	unsigned int m = h & 0x3FF;

	// Handle zero, denormals, infinity, and NaN
	unsigned int exp;
	if (e == 0)
	{
		exp = (m != 0) ? ((127 - 14) << 23) : 0;
		m = (m != 0) ? (m << 13) : 0;
	}
	else if (e == 31)
	{
		exp = (m != 0) ? 0x7F800000 : 0x7F800000;  // NaN or infinity
		m = m << 13;
	}
	else
	{
		exp = (e + 127 - 15) << 23;  // normalized number
		m = m << 13;
	}

	u.ui = s | exp | m;
	return u.f;
}
```
> Ok, but even if the values are expected be dequantized on the GPU, then someone still needs to write the code for the dequantization there that matches the quantization code of this library.

Not really - the half-precision format is standardized and all GPUs support it natively. The integer formats (unorm/snorm) are also supported natively through normalized formats by every graphics API, but of course for those the dequantization function is just a multiplication/division - which you can find in the comments. This should probably be spelled out in README.

It looks like for your specific case of encoding & decoding halfs on CPU it might indeed be useful to add a dequantization function, I'll take a look at adding that..
Ah, thanks! I did not know that it's actually "the" half-precision format, I thought it would be "some" half precision quantized format that you specifically created for this library. I know that some people spend a lot of time with coming up with new quantization algorithms for neural networks to get them down to just 2 or 3 or 4 bit, so I thought it would be similar here, that you came up with some fancy quantization algorithm that's especially good for mesh data.

It would definitely be good to mention then in the Readme and the comment of the functions that the quantization functions are just for the standardized format. That actually makes it much easier for me as well, I really don't need to use that function at all and can just use the native Unreal Engine FFloat16 type, since I'm using Unreal Engine and it already has that native type for half-precision floats, if this library doesn't end up creating anything else than a "regular" half-precision float.

For people who aren't using an engine/framework that already offers a native half-precision float type, having dequantization functions in this library would still be very useful though.



Fix (patch):
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 01909c140..a21445796 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,6 +28,7 @@ set(SOURCES
     src/indexgenerator.cpp
     src/overdrawanalyzer.cpp
     src/overdrawoptimizer.cpp
+    src/quantization.cpp
     src/simplifier.cpp
     src/spatialorder.cpp
     src/stripifier.cpp
diff --git a/README.md b/README.md
index d96ffa836..7fe171322 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,8 @@ unsigned short py = meshopt_quantizeHalf(v.y);
 unsigned short pz = meshopt_quantizeHalf(v.z);
 ```
 
+Since quantized vertex attributes often need to remain in their compact representations for efficient transfer and storage, they are usually dequantized during vertex processing by configuring the GPU vertex input correctly to expect normalized integers or half precision floats, which often needs no or minimal changes to the shader code. When CPU dequantization is required instead, `meshopt_dequantizeHalf` can be used to convert half precision values back to single precision; for normalized integer formats, the dequantization just requires dividing by 2^N-1 for unorm and 2^(N-1)-1 for snorm variants, for example manually reversing `meshopt_quantizeUnorm(v, 10)` can be done by dividing by 1023.
+
 ## Vertex/index buffer compression
 
 In case storage size or transmission bandwidth is of importance, you might want to additionally compress vertex and index data. While several mesh compression libraries, like Google Draco, are available, they typically are designed to maximize the compression ratio at the cost of disturbing the vertex/index order (which makes the meshes inefficient to render on GPU) or decompression performance. They also frequently don't support custom game-ready quantized vertex formats and thus require to re-quantize the data after loading it, introducing extra quantization errors and making decoding slower.
diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h
index aeeab9de6..b763148a2 100644
--- a/src/meshoptimizer.h
+++ b/src/meshoptimizer.h
@@ -577,19 +577,25 @@ inline int meshopt_quantizeUnorm(float v, int N);
 inline int meshopt_quantizeSnorm(float v, int N);
 
 /**
- * Quantize a float into half-precision floating point value
+ * Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value
  * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest
  * Representable magnitude range: [6e-5; 65504]
  * Maximum relative reconstruction error: 5e-4
  */
-inline unsigned short meshopt_quantizeHalf(float v);
+MESHOPTIMIZER_API unsigned short meshopt_quantizeHalf(float v);
 
 /**
- * Quantize a float into a floating point value with a limited number of significant mantissa bits
+ * Quantize a float into a floating point value with a limited number of significant mantissa bits, preserving the IEEE-754 fp32 binary representation
  * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest
  * Assumes N is in a valid mantissa precision range, which is 1..23
  */
-inline float meshopt_quantizeFloat(float v, int N);
+MESHOPTIMIZER_API float meshopt_quantizeFloat(float v, int N);
+
+/**
+ * Reverse quantization of a half-precision (as defined by IEEE-754 fp16) floating point value
+ * Preserves Inf/NaN, flushes denormals to zero
+ */
+MESHOPTIMIZER_API float meshopt_dequantizeHalf(unsigned short h);
 #endif
 
 /**
@@ -684,50 +690,6 @@ inline int meshopt_quantizeSnorm(float v, int N)
 
 	return int(v * scale + round);
 }
-
-inline unsigned short meshopt_quantizeHalf(float v)
-{
-	union { float f; unsigned int ui; } u = {v};
-	unsigned int ui = u.ui;
-
-	int s = (ui >> 16) & 0x8000;
-	int em = ui & 0x7fffffff;
-
-	/* bias exponent and round to nearest; 112 is relative exponent bias (127-15) */
-	int h = (em - (112 << 23) + (1 << 12)) >> 13;
-
-	/* underflow: flush to zero; 113 encodes exponent -14 */
-	h = (em < (113 << 23)) ? 0 : h;
-
-	/* overflow: infinity; 143 encodes exponent 16 */
-	h = (em >= (143 << 23)) ? 0x7c00 : h;
-
-	/* NaN; note that we convert all types of NaN to qNaN */
-	h = (em > (255 << 23)) ? 0x7e00 : h;
-
-	return (unsigned short)(s | h);
-}
-
-inline float meshopt_quantizeFloat(float v, int N)
-{
-	union { float f; unsigned int ui; } u = {v};
-	unsigned int ui = u.ui;
-
-	const int mask = (1 << (23 - N)) - 1;
-	const int round = (1 << (23 - N)) >> 1;
-
-	int e = ui & 0x7f800000;
-	unsigned int rui = (ui + round) & ~mask;
-
-	/* round all numbers except inf/nan; this is important to make sure nan doesn't overflow into -0 */
-	ui = e == 0x7f800000 ? ui : rui;
-
-	/* flush denormals to zero */
-	ui = e == 0 ? 0 : ui;
-
-	u.ui = ui;
-	return u.f;
-}
 #endif
 
 /* Internal implementation helpers */
diff --git a/src/quantization.cpp b/src/quantization.cpp
new file mode 100644
index 000000000..09a314d60
--- /dev/null
+++ b/src/quantization.cpp
@@ -0,0 +1,70 @@
+// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details
+#include "meshoptimizer.h"
+
+#include <assert.h>
+
+unsigned short meshopt_quantizeHalf(float v)
+{
+	union { float f; unsigned int ui; } u = {v};
+	unsigned int ui = u.ui;
+
+	int s = (ui >> 16) & 0x8000;
+	int em = ui & 0x7fffffff;
+
+	// bias exponent and round to nearest; 112 is relative exponent bias (127-15)
+	int h = (em - (112 << 23) + (1 << 12)) >> 13;
+
+	// underflow: flush to zero; 113 encodes exponent -14
+	h = (em < (113 << 23)) ? 0 : h;
+
+	// overflow: infinity; 143 encodes exponent 16
+	h = (em >= (143 << 23)) ? 0x7c00 : h;
+
+	// NaN; note that we convert all types of NaN to qNaN
+	h = (em > (255 << 23)) ? 0x7e00 : h;
+
+	return (unsigned short)(s | h);
+}
+
+float meshopt_quantizeFloat(float v, int N)
+{
+	assert(N >= 0 && N <= 23);
+
+	union { float f; unsigned int ui; } u = {v};
+	unsigned int ui = u.ui;
+
+	const int mask = (1 << (23 - N)) - 1;
+	const int round = (1 << (23 - N)) >> 1;
+
+	int e = ui & 0x7f800000;
+	unsigned int rui = (ui + round) & ~mask;
+
+	// round all numbers except inf/nan; this is important to make sure nan doesn't overflow into -0
+	ui = e == 0x7f800000 ? ui : rui;
+
+	// flush denormals to zero
+	ui = e == 0 ? 0 : ui;
+
+	u.ui = ui;
+	return u.f;
+}
+
+float meshopt_dequantizeHalf(unsigned short h)
+{
+	unsigned int s = unsigned(h & 0x8000) << 16;
+	int em = h & 0x7fff;
+
+	// bias exponent and pad mantissa with 0; 112 is relative exponent bias (127-15)
+	int r = (em + (112 << 10)) << 13;
+
+	// denormal: flush to zero
+	r = (em < (1 << 10)) ? 0 : r;
+
+	// infinity/NaN; note that we preserve NaN payload as a byproduct of unifying inf/nan cases
+	// 112 is an exponent bias fixup; since we already applied it once, applying it twice converts 31 to 255
+	r += (em >= (31 << 10)) ? (112 << 23) : 0;
+
+	union { float f; unsigned int ui; } u;
+	u.ui = s | r;
+	return u.f;
+}