diff --git a/include/igeometrystore.h b/include/igeometrystore.h index d625e9fca5..bbb2bb2b80 100644 --- a/include/igeometrystore.h +++ b/include/igeometrystore.h @@ -60,6 +60,21 @@ class IGeometryStore */ virtual Slot allocateSlot(std::size_t numVertices, std::size_t numIndices) = 0; + /** + * Allocate memory to store an alternative set of indices referencing + * an existing set of vertices. When rendering this re-mapped geometry, it will + * re-use the vertices of that other slot with the indices defined in this slot. + * + * With the returned handle, user code can invoke the update[Sub]Data() methods + * to upload the indices, but the passed vertex set has to be empty. + * + * Use the regular deallocate() method to release this slot. + * + * The index remap slot is depending on the one containing the vertex data, if the latter + * is removed, this slot becomes invalid and behaviour is undefined. + */ + virtual Slot allocateIndexSlot(Slot slotContainingVertexData, std::size_t numIndices) = 0; + /** * Load vertex and index data into the specified block. The given vertex and * index arrays must not be larger than what has been allocated earlier, @@ -68,6 +83,15 @@ class IGeometryStore virtual void updateData(Slot slot, const std::vector& vertices, const std::vector& indices) = 0; + /** + * Updates the data of an index slot. Equivalent to calling updateData() with + * an empty set of vertices. + */ + virtual void updateIndexData(Slot slot, const std::vector& indices) + { + updateData(slot, {}, indices); + } + /** * Load a chunk of vertex and index data into the specified range, starting * from vertexOffset/indexOffset respectively. The affected range must not be out of bounds @@ -76,11 +100,24 @@ class IGeometryStore virtual void updateSubData(Slot slot, std::size_t vertexOffset, const std::vector& vertices, std::size_t indexOffset, const std::vector& indices) = 0; + /** + * Updates a portion of index data in an index slot. Equivalent to calling updateSubData() with + * an empty set of vertices. + */ + virtual void updateIndexSubData(Slot slot, std::size_t indexOffset, const std::vector& indices) + { + updateSubData(slot, 0, {}, indexOffset, indices); + } + /** * Called in case the stored data in the given slot should just be cut off at the end. */ virtual void resizeData(Slot slot, std::size_t vertexSize, std::size_t indexSize) = 0; + /** + * Releases the memory allocated by the given slot. + * The Slot ID is invalidated by this operation and should no longer be used. + */ virtual void deallocateSlot(Slot slot) = 0; // The render parameters suitable for rendering surfaces using gl(Multi)DrawElements diff --git a/libs/render/GeometryStore.h b/libs/render/GeometryStore.h index f7c1df1568..fcb09cf5b3 100644 --- a/libs/render/GeometryStore.h +++ b/libs/render/GeometryStore.h @@ -97,6 +97,11 @@ class GeometryStore : return slot; } + Slot allocateIndexSlot(Slot slotContainingVertexData, std::size_t numIndices) override + { + return std::numeric_limits::max(); + } + void updateData(Slot slot, const std::vector& vertices, const std::vector& indices) override { diff --git a/test/GeometryStore.cpp b/test/GeometryStore.cpp index de21f0c7f4..ebc882b1ed 100644 --- a/test/GeometryStore.cpp +++ b/test/GeometryStore.cpp @@ -1,6 +1,7 @@ #include "gtest/gtest.h" #include +#include #include #include "render/GeometryStore.h" @@ -465,4 +466,127 @@ TEST(GeometryStore, SyncObjectAcquisition) "GeometryStore should have performed 5 frame buffer switches"; } +TEST(GeometryStore, AllocateIndexRemap) +{ + render::GeometryStore store(NullSyncObjectProvider::Instance()); + + // Allocate a slot to hold indexed vertices + auto vertices = generateVertices(3, 15 * 20); + auto indices = generateIndices(vertices); + + auto primarySlot = store.allocateSlot(vertices.size(), indices.size()); + EXPECT_NE(primarySlot, std::numeric_limits::max()) << "Invalid slot"; + + // Uploading the data should succeed + EXPECT_NO_THROW(store.updateData(primarySlot, vertices, indices)); + + auto secondarySlot = store.allocateIndexSlot(primarySlot, 20); + EXPECT_NE(secondarySlot, std::numeric_limits::max()) << "Invalid slot"; + + // Deallocation through the regular method should succeed + EXPECT_NO_THROW(store.deallocateSlot(secondarySlot)); +} + +TEST(GeometryStore, AllocateInvalidIndexRemap) +{ + render::GeometryStore store(NullSyncObjectProvider::Instance()); + + // Allocate a slot to hold indexed vertices + auto vertices = generateVertices(3, 15 * 20); + auto indices = generateIndices(vertices); + + auto primarySlot = store.allocateSlot(vertices.size(), indices.size()); + EXPECT_NE(primarySlot, std::numeric_limits::max()) << "Invalid slot"; + + // This allocation is valid and will be a remap type + auto secondarySlot = store.allocateIndexSlot(primarySlot, 20); + EXPECT_NE(secondarySlot, std::numeric_limits::max()) << "Invalid slot"; + + // This call is not valid and should throw, since the secondary slot cannot be re-used + EXPECT_THROW(store.allocateIndexSlot(secondarySlot, 10), std::logic_error); + + // Trying to reference a non-existing slot should throw as well + EXPECT_THROW(store.allocateIndexSlot(56756756, 10), std::logic_error); +} + +TEST(GeometryStore, UpdateIndexRemapData) +{ + render::GeometryStore store(NullSyncObjectProvider::Instance()); + + // Allocate a slot to hold indexed vertices + auto vertices = generateVertices(3, 15 * 20); + auto indices = generateIndices(vertices); + + auto primarySlot = store.allocateSlot(vertices.size(), indices.size()); + EXPECT_NE(primarySlot, std::numeric_limits::max()) << "Invalid slot"; + EXPECT_NO_THROW(store.updateData(primarySlot, vertices, indices)); + + // Now allocate an index remapping slot, containing a straight, sequential set of indices 0..n-1 + std::vector remap; + remap.resize(vertices.size()); + std::iota(remap.begin(), remap.end(), 0); + + auto secondarySlot = store.allocateIndexSlot(primarySlot, remap.size()); + EXPECT_NE(secondarySlot, std::numeric_limits::max()) << "Invalid slot"; + + // Update the index data through updateData() + EXPECT_NO_THROW(store.updateData(secondarySlot, {}, remap)); + + // The render params should effectively point us the re-used vertices, in remapped order + verifyAllocation(store, secondarySlot, vertices, remap); + + // Reverse the index order for testing the second way of uploading data + std::reverse(remap.begin(), remap.end()); + EXPECT_NO_THROW(store.updateIndexData(secondarySlot, remap)); + + verifyAllocation(store, secondarySlot, vertices, remap); + + // We expect an exception when trying to store vertex data + EXPECT_THROW(store.updateData(secondarySlot, vertices, remap), std::logic_error); +} + +TEST(GeometryStore, UpdateIndexRemapSubData) +{ + render::GeometryStore store(NullSyncObjectProvider::Instance()); + + // Allocate a slot to hold indexed vertices + auto vertices = generateVertices(3, 15 * 20); + auto indices = generateIndices(vertices); + + auto primarySlot = store.allocateSlot(vertices.size(), indices.size()); + EXPECT_NE(primarySlot, std::numeric_limits::max()) << "Invalid slot"; + EXPECT_NO_THROW(store.updateData(primarySlot, vertices, indices)); + + // Now allocate an index remapping slot, containing a straight, sequential set of indices 0..n-1 + std::vector remap; + remap.resize(vertices.size()); + std::iota(remap.begin(), remap.end(), 0); + + auto secondarySlot = store.allocateIndexSlot(primarySlot, remap.size()); + EXPECT_NE(secondarySlot, std::numeric_limits::max()) << "Invalid slot"; + + // Upload the sequential set of indices + EXPECT_NO_THROW(store.updateIndexData(secondarySlot, remap)); + verifyAllocation(store, secondarySlot, vertices, remap); + + // Generate a new set of indices, and make it half as large than the original remap + std::vector indexSubset; + indexSubset.resize(remap.size() / 2); + std::iota(indexSubset.begin(), indexSubset.end(), 0); // [0..N-1] + std::reverse(indexSubset.begin(), indexSubset.end()); // make it [N-1...0] + + // Apply the subset to the local remap copy + auto offset = indexSubset.size() / 4; + std::copy(indexSubset.begin(), indexSubset.end(), remap.begin() + offset); + + // Apply the subset to the data in the store + EXPECT_NO_THROW(store.updateIndexSubData(secondarySlot, offset, indexSubset)); + + // The new subset should now be used when effective + verifyAllocation(store, secondarySlot, vertices, remap); + + // We expect boundaries to be respected, this should be out of range + EXPECT_THROW(store.updateIndexSubData(secondarySlot, remap.size() - 1, indexSubset), std::logic_error); +} + }