Repository: zeux/meshoptimizer (language: cpp)

Issue:
Pass vertex and index versions as arguments instead of having them as globals.
Having gEncodeVertexVersion and gEncodeIndexVersion as globals makes the meshoptimizer library difficult to use correctly in a multithreaded program, when some threads may be want to write with different versions.


Discussion / hints:
Could you elaborate on why you need to encode multiple versions in different threads? The assumption so far has been that the versions are either never changed or changed at the start of the program.

(as to "why not just add this", due to binary compatibility requirements this can't be added as an argument to the existing `encode*Buffer` functions. It may be possible to expand `encodeVertexBufferLevel` with an extra version argument as it's still experimental, and add `encodeIndexBufferLevel` counterpart, and/or rename them somehow, but I was hoping to not have to do that)
Hi,
It's for backwards compatibility - for older clients connecting to my metaverse server, they will not have the code to decode vertex version 1.  So meshes need to be encoded for them with version 0.   But for newer clients that support version 1, I want to be able to encode meshes with version 1.
How long is that window of compatibility? As in is there a point where you will know for sure that all clients support version 1 and would be able to use it unconditionally?
And, just to confirm, this is specifically for the vertex decoder?

I understand the use case but want to make sure it's solved with least amount of new APIs that would have to be kept long term...
There's no specific window, just long enough that no one is likely to be using the old client any more.  maybe a year or so.
And yeah it's just for the vertex version, I think I introduced meshopt encoded meshes after the index version was 1.

EDIT: unlike a lot of computer games, I don't force clients to update to a specific version.
Alright, thanks. I think it would make the most sense to preserve the global versioning by default but provide a way to override that for vertex encoder (index encoder currently does not need it in practice, and similar functions could be added there in the future if there's ever a v2 index codec). The existing meshopt_encodeVertexBufferLevel function could be changed to something like

```c++
MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeVertexBufferLevel(
    unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size,
    int level, int version);
```

... with `version = -1` used to select the currently active global version, but values 0/1 (and other values in the future if there's ever a vertex codec v2) used to override that. `meshopt_encodeVertexBufferLevel` name is a little less clear with that change, maybe it should be changed (`-Ex`?), not sure - I'll think about alternatives. All of this is currently ok to change because `meshopt_encodeVertexBufferLevel` is experimental (and as such is subject to change). So in your case you'd need to switch to this function to be able to simultaneously encode multiple streams at different versions from multiple threads.

Fix patch:
diff --git a/README.md b/README.md
index 9ea6ed923..985d14eff 100644
--- a/README.md
+++ b/README.md
@@ -253,7 +253,7 @@ For optimal compression results, the values must be quantized to small integers.
 For single-precision floating-point data, it's recommended to use `meshopt_quantizeFloat` to remove entropy from the lower bits of the mantissa. Due to current limitations of the codec, the bit count needs to be 15 (23-8) for good results (7 can be used for more extreme compression).
 For normal or tangent vectors, using octahedral encoding is recommended over three components as it reduces redundancy. Similarly to other quantized values, consider using 10-12 bits per component instead of 16.
 
-When data is bit packed, using v1 vertex codec (via `meshopt_encodeVertexVersion(1)`) and specifying compression level 3 (`meshopt_encodeVertexBufferLevel`) can improve the compression further by redistributing bits between components. Note that v1 vertex codec is recommended regardless, as it improves compression ratios and decoding performance even absent bit packing.
+When data is bit packed, using v1 vertex codec and specifying compression level 3 (`meshopt_encodeVertexBufferLevel` with level 3 and version 1) can improve the compression further by redistributing bits between components. Note that v1 vertex codec (`meshopt_encodeVertexVersion(1)`) is recommended regardless, as it improves compression ratios and decoding performance even absent bit packing.
 
 To further leverage the inherent structure of some data, the preparation stage can use filters that encode and decode the data in a lossy manner. This is similar to quantization but can be used without having to change the shader code. After decoding, the filter transformation needs to be reversed. For native game engine pipelines, it is usually more optimal to carefully prequantize and pretransform the vertex data, but sometimes (for example when serializing data in glTF format) this is not a practical option and filters are more practical. This library provides three filters:
 
@@ -618,7 +618,7 @@ Currently, the following APIs are experimental, with the functions marked with `
 - `meshopt_buildMeshletsFlex`
 - `meshopt_buildMeshletsSplit`
 - `meshopt_computeSphereBounds`*
-- `meshopt_encodeVertexBufferLevel`*
+- `meshopt_encodeVertexBufferLevel`
 - `meshopt_generateProvokingIndexBuffer`*
 - `meshopt_generateVertexRemapCustom`*
 - `meshopt_partitionClusters`
diff --git a/demo/main.cpp b/demo/main.cpp
index a98c69461..a06a54f04 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -893,7 +893,7 @@ void encodeVertex(const Mesh& mesh, const char* pvn, int level = 2)
 	double start = timestamp();
 
 	std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(mesh.vertices.size(), sizeof(PV)));
-	vbuf.resize(meshopt_encodeVertexBufferLevel(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV), level));
+	vbuf.resize(meshopt_encodeVertexBufferLevel(&vbuf[0], vbuf.size(), &pv[0], mesh.vertices.size(), sizeof(PV), level, -1));
 
 	double middle = timestamp();
 
diff --git a/src/meshoptimizer.h b/src/meshoptimizer.h
index 84ba1a7ab..ef6c4a93f 100644
--- a/src/meshoptimizer.h
+++ b/src/meshoptimizer.h
@@ -304,8 +304,9 @@ MESHOPTIMIZER_API size_t meshopt_encodeVertexBufferBound(size_t vertex_count, si
  * The default compression level implied by meshopt_encodeVertexBuffer is 2.
  *
  * level should be in the range [0, 3] with 0 being the fastest and 3 being the slowest and producing the best compression ratio.
+ * version should be -1 to use the default version (specified via meshopt_encodeVertexVersion), or 0/1 to override the version; per above, level won't take effect if version is 0.
  */
-MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level);
+MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level, int version);
 
 /**
  * Set vertex encoder format version
diff --git a/src/vertexcodec.cpp b/src/vertexcodec.cpp
index 53cf9d753..847589b3d 100644
--- a/src/vertexcodec.cpp
+++ b/src/vertexcodec.cpp
@@ -1643,13 +1643,16 @@ static unsigned int cpuid = getCpuFeatures();
 
 } // namespace meshopt
 
-size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level)
+size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size, int level, int version)
 {
 	using namespace meshopt;
 
 	assert(vertex_size > 0 && vertex_size <= 256);
 	assert(vertex_size % 4 == 0);
 	assert(level >= 0 && level <= 9); // only a subset of this range is used right now
+	assert(version < 0 || unsigned(version) <= kDecodeVertexVersion);
+
+	version = version < 0 ? gEncodeVertexVersion : version;
 
 #if TRACE
 	memset(vertexstats, 0, sizeof(vertexstats));
@@ -1663,8 +1666,6 @@ size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size
 	if (size_t(data_end - data) < 1)
 		return 0;
 
-	int version = gEncodeVertexVersion;
-
 	*data++ = (unsigned char)(kVertexHeader | version);
 
 	unsigned char first_vertex[256] = {};
@@ -1777,7 +1778,7 @@ size_t meshopt_encodeVertexBufferLevel(unsigned char* buffer, size_t buffer_size
 
 size_t meshopt_encodeVertexBuffer(unsigned char* buffer, size_t buffer_size, const void* vertices, size_t vertex_count, size_t vertex_size)
 {
-	return meshopt_encodeVertexBufferLevel(buffer, buffer_size, vertices, vertex_count, vertex_size, meshopt::kEncodeDefaultLevel);
+	return meshopt_encodeVertexBufferLevel(buffer, buffer_size, vertices, vertex_count, vertex_size, meshopt::kEncodeDefaultLevel, meshopt::gEncodeVertexVersion);
 }
 
 size_t meshopt_encodeVertexBufferBound(size_t vertex_count, size_t vertex_size)
diff --git a/tools/codecfuzz.cpp b/tools/codecfuzz.cpp
index 89bce2409..4992857e3 100644
--- a/tools/codecfuzz.cpp
+++ b/tools/codecfuzz.cpp
@@ -26,12 +26,12 @@ void fuzzRoundtrip(const uint8_t* data, size_t size, size_t stride, int level)
 	void* decoded = malloc(count * stride);
 	assert(encoded && decoded);
 
-	size_t res = meshopt_encodeVertexBufferLevel(static_cast<unsigned char*>(encoded), bound, data, count, stride, level);
+	size_t res = meshopt_encodeVertexBufferLevel(static_cast<unsigned char*>(encoded), bound, data, count, stride, level, -1);
 	assert(res > 0 && res <= bound);
 
 	// encode again at the boundary to check for memory safety
 	// this should produce the same output because encoder is deterministic
-	size_t rese = meshopt_encodeVertexBufferLevel(static_cast<unsigned char*>(encoded) + bound - res, res, data, count, stride, level);
+	size_t rese = meshopt_encodeVertexBufferLevel(static_cast<unsigned char*>(encoded) + bound - res, res, data, count, stride, level, -1);
 	assert(rese == res);
 
 	int rc = meshopt_decodeVertexBuffer(decoded, count, stride, static_cast<unsigned char*>(encoded) + bound - res, res);


Test patch:
diff --git a/demo/tests.cpp b/demo/tests.cpp
index 1636756ca..8e9e4a9c1 100644
--- a/demo/tests.cpp
+++ b/demo/tests.cpp
@@ -616,7 +616,7 @@ static void decodeVertexDeltas()
 	}
 
 	std::vector<unsigned char> buffer(meshopt_encodeVertexBufferBound(16, 8));
-	buffer.resize(meshopt_encodeVertexBufferLevel(&buffer[0], buffer.size(), data, 16, 8, 2));
+	buffer.resize(meshopt_encodeVertexBufferLevel(&buffer[0], buffer.size(), data, 16, 8, 2, -1));
 
 	unsigned short decoded[16 * 4];
 	assert(meshopt_decodeVertexBuffer(decoded, 16, 8, &buffer[0], buffer.size()) == 0);
@@ -637,7 +637,7 @@ static void decodeVertexBitXor()
 	}
 
 	std::vector<unsigned char> buffer(meshopt_encodeVertexBufferBound(16, 16));
-	buffer.resize(meshopt_encodeVertexBufferLevel(&buffer[0], buffer.size(), data, 16, 16, 3));
+	buffer.resize(meshopt_encodeVertexBufferLevel(&buffer[0], buffer.size(), data, 16, 16, 3, -1));
 
 	unsigned int decoded[16 * 4];
 	assert(meshopt_decodeVertexBuffer(decoded, 16, 16, &buffer[0], buffer.size()) == 0);
diff --git a/tools/codectest.cpp b/tools/codectest.cpp
index 28203ecd4..04ba873b1 100644
--- a/tools/codectest.cpp
+++ b/tools/codectest.cpp
@@ -91,7 +91,7 @@ void testFile(FILE* file, size_t count, size_t stride, int level, Stats* stats =
 
 	std::vector<unsigned char> output(meshopt_encodeVertexBufferBound(count, stride));
 	meshopt_encodeVertexVersion(1);
-	output.resize(meshopt_encodeVertexBufferLevel(output.data(), output.size(), decoded.data(), count, stride, level));
+	output.resize(meshopt_encodeVertexBufferLevel(output.data(), output.size(), decoded.data(), count, stride, level, -1));
 
 	printf(" raw %zu KB\t", decoded.size() / 1024);
 	printf(" v0 %.3f", double(input.size()) / double(decoded.size()));