From 85bf771ea6d8d584f1c735cc963e17571ca0a0d1 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Mon, 17 Apr 2023 11:54:07 +0100
Subject: First cut of instance vertices and proxy

Untested outside unit test, allows the use of a glBuffer as a storage container. To be combined
with a vertex array and/or mesh etc for massing drawing with glDrawElementsInstanced
---
 test/Jamfile.jam         |  1 +
 test/test-instancing.cpp | 76 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 77 insertions(+)
 create mode 100644 test/test-instancing.cpp

(limited to 'test')

diff --git a/test/Jamfile.jam b/test/Jamfile.jam
index b0eed5e..9cdcef4 100644
--- a/test/Jamfile.jam
+++ b/test/Jamfile.jam
@@ -57,6 +57,7 @@ run test-assetFactory.cpp : -- : [ sequence.insertion-sort [ glob-tree $(res) :
 run perf-assetFactory.cpp : -- : test-assetFactory : <library>benchmark <library>test ;
 run perf-persistence.cpp : -- : test-persistence : <library>benchmark <library>test ;
 run test-worker.cpp ;
+run test-instancing.cpp : : : <library>test ;
 compile test-static-enumDetails.cpp ;
 compile test-static-stream_support.cpp ;
 explicit perf-assetFactory ;
diff --git a/test/test-instancing.cpp b/test/test-instancing.cpp
new file mode 100644
index 0000000..7191567
--- /dev/null
+++ b/test/test-instancing.cpp
@@ -0,0 +1,76 @@
+#define BOOST_TEST_MODULE instancing
+
+#include "testHelpers.h"
+#include "testMainWindow.h"
+#include "ui/applicationBase.h"
+#include <boost/test/data/test_case.hpp>
+#include <boost/test/unit_test.hpp>
+
+#include <gfx/gl/instanceVertices.h>
+
+BOOST_GLOBAL_FIXTURE(ApplicationBase);
+BOOST_GLOBAL_FIXTURE(TestMainWindow);
+
+BOOST_FIXTURE_TEST_SUITE(i, InstanceVertices<int>)
+
+BOOST_AUTO_TEST_CASE(createDestroy)
+{
+	BOOST_REQUIRE(data.data());
+	BOOST_CHECK_EQUAL(0, next);
+	BOOST_CHECK(unused.empty());
+}
+
+BOOST_AUTO_TEST_CASE(storeRetreive)
+{ // Read write raw buffer, not normally allowed
+	std::vector<int> test(data.size());
+	std::copy(test.begin(), test.end(), data.begin());
+	BOOST_CHECK_EQUAL_COLLECTIONS(test.begin(), test.end(), data.begin(), data.end());
+}
+
+BOOST_AUTO_TEST_CASE(acquireRelease)
+{
+	{
+		auto proxy = acquire();
+		*proxy = 20;
+		BOOST_CHECK_EQUAL(1, next);
+	}
+	BOOST_CHECK_EQUAL(1, next);
+	BOOST_CHECK_EQUAL(1, unused.size());
+	BOOST_CHECK_EQUAL(0, unused.front());
+}
+
+BOOST_AUTO_TEST_CASE(acquireReleaseMove)
+{
+	{
+		auto proxy1 = acquire();
+		*proxy1 = 20;
+		BOOST_CHECK_EQUAL(1, next);
+		auto proxy2 = std::move(proxy1);
+		*proxy2 = 40;
+		BOOST_CHECK_EQUAL(1, next);
+	}
+	BOOST_CHECK_EQUAL(1, next);
+	BOOST_CHECK_EQUAL(1, unused.size());
+	BOOST_CHECK_EQUAL(0, unused.front());
+}
+
+BOOST_AUTO_TEST_CASE(initialize)
+{
+	auto proxy = acquire(5);
+	BOOST_CHECK_EQUAL(*proxy, 5);
+}
+
+BOOST_AUTO_TEST_CASE(resize)
+{
+	constexpr auto COUNT = 500;
+	std::vector<decltype(acquire())> proxies;
+	std::vector<int> expected;
+	for (auto n = 0; n < COUNT; n++) {
+		proxies.push_back(acquire(n));
+		expected.emplace_back(n);
+	}
+	BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), data.begin(), data.begin() + COUNT);
+	BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), proxies.begin(), proxies.end());
+}
+
+BOOST_AUTO_TEST_SUITE_END()
-- 
cgit v1.2.3


From 93f11a006b94ccecd7a1fd1dfba0adff56ae7c38 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Wed, 19 Apr 2023 00:51:35 +0100
Subject: Drop performance debug to warning and stop disabling error checking
 for texture save

---
 test/test-assetFactory.cpp | 1 -
 test/test-render.cpp       | 3 ---
 test/testMainWindow.cpp    | 3 ++-
 3 files changed, 2 insertions(+), 5 deletions(-)

(limited to 'test')

diff --git a/test/test-assetFactory.cpp b/test/test-assetFactory.cpp
index 3d79213..82543f3 100644
--- a/test/test-assetFactory.cpp
+++ b/test/test-assetFactory.cpp
@@ -29,7 +29,6 @@ public:
 	FactoryFixture() : sceneRenderer {size, output} { }
 	~FactoryFixture()
 	{
-		glDisable(GL_DEBUG_OUTPUT);
 		auto outpath = (TMP / boost::unit_test::framework::current_test_case().full_name()).replace_extension(".tga");
 		std::filesystem::create_directories(outpath.parent_path());
 		Texture::save(outImage, outpath.c_str());
diff --git a/test/test-render.cpp b/test/test-render.cpp
index 45acab5..1643068 100644
--- a/test/test-render.cpp
+++ b/test/test-render.cpp
@@ -86,7 +86,6 @@ BOOST_AUTO_TEST_CASE(basic)
 	ss.camera.setView({-10, -10, 60}, glm::normalize(glm::vec3 {1, 1, -0.5F}));
 	const TestScene scene;
 	ss.render(scene);
-	glDisable(GL_DEBUG_OUTPUT);
 	Texture::save(outImage, "/tmp/basic.tga");
 }
 
@@ -114,7 +113,6 @@ BOOST_AUTO_TEST_CASE(pointlight)
 	};
 	const PointLightScene scene;
 	ss.render(scene);
-	glDisable(GL_DEBUG_OUTPUT);
 	Texture::save(outImage, "/tmp/pointlight.tga");
 }
 
@@ -141,7 +139,6 @@ BOOST_AUTO_TEST_CASE(spotlight)
 	};
 	const PointLightScene scene;
 	ss.render(scene);
-	glDisable(GL_DEBUG_OUTPUT);
 	Texture::save(outImage, "/tmp/spotlight.tga");
 }
 
diff --git a/test/testMainWindow.cpp b/test/testMainWindow.cpp
index 49e18f1..57e3473 100644
--- a/test/testMainWindow.cpp
+++ b/test/testMainWindow.cpp
@@ -12,11 +12,12 @@ TestMainWindow::TestMainWindow() : Window {1, 1, __FILE__, SDL_WINDOW_OPENGL | S
 						(type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""), type, severity, message);
 				switch (type) {
 					case GL_DEBUG_TYPE_ERROR:
-					case GL_DEBUG_TYPE_PERFORMANCE:
 					case GL_DEBUG_TYPE_PORTABILITY:
 					case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR:
 					case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR:
 						BOOST_TEST_ERROR(msg.get());
+					case GL_DEBUG_TYPE_PERFORMANCE:
+						BOOST_TEST_WARN(msg.get());
 					default:
 						BOOST_TEST_MESSAGE(msg.get());
 				}
-- 
cgit v1.2.3


From ac05fbbc71282b059164b51efd68ee6e372870cb Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Thu, 20 Apr 2023 20:27:43 +0100
Subject: Switch to render trees in bulk through foliage asset rendering

---
 game/scenary/foliage.cpp   | 22 ++++++++++++++--------
 game/scenary/foliage.h     | 10 +++++++---
 game/scenary/plant.cpp     | 12 +++---------
 game/scenary/plant.h       | 12 +++++-------
 test/test-assetFactory.cpp |  2 +-
 ui/gameMainWindow.cpp      | 10 ++++++++++
 6 files changed, 40 insertions(+), 28 deletions(-)

(limited to 'test')

diff --git a/game/scenary/foliage.cpp b/game/scenary/foliage.cpp
index d39d500..35be051 100644
--- a/game/scenary/foliage.cpp
+++ b/game/scenary/foliage.cpp
@@ -1,7 +1,9 @@
 #include "foliage.h"
 #include "gfx/gl/sceneShader.h"
 #include "gfx/gl/shadowMapper.h"
+#include "gfx/gl/vertexArrayObject.hpp"
 #include "gfx/models/texture.h"
+#include "location.hpp"
 
 bool
 Foliage::persist(Persistence::PersistenceStore & store)
@@ -13,21 +15,25 @@ void
 Foliage::postLoad()
 {
 	texture = getTexture();
+	bodyMesh->configureVAO(instanceVAO).addAttribs<glm::mat4>(instances.bufferName(), 1);
 }
 
 void
-Foliage::render(const SceneShader & shader, const Location & loc) const
+Foliage::render(const SceneShader & shader) const
 {
-	shader.basic.use(loc);
-	if (texture) {
-		texture->bind();
+	if (const auto count = instances.count()) {
+		shader.basicInst.use();
+		if (texture) {
+			texture->bind();
+		}
+		glBindVertexArray(instanceVAO);
+		glDrawElementsInstanced(
+				bodyMesh->type(), bodyMesh->count(), GL_UNSIGNED_INT, nullptr, static_cast<GLsizei>(count));
+		glBindVertexArray(0);
 	}
-	bodyMesh->Draw();
 }
 
 void
-Foliage::shadows(const ShadowMapper & mapper, const Location & loc) const
+Foliage::shadows(const ShadowMapper &) const
 {
-	mapper.dynamicPoint.use(loc);
-	bodyMesh->Draw();
 }
diff --git a/game/scenary/foliage.h b/game/scenary/foliage.h
index b85aab2..b72a9c2 100644
--- a/game/scenary/foliage.h
+++ b/game/scenary/foliage.h
@@ -1,19 +1,23 @@
 #pragma once
 
 #include "assetFactory/asset.h"
+#include "gfx/gl/instanceVertices.h"
+#include "gfx/renderable.h"
 
 class SceneShader;
 class ShadowMapper;
 class Location;
 class Texture;
 
-class Foliage : public Asset, public StdTypeDefs<Foliage> {
+class Foliage : public Asset, public Renderable, public StdTypeDefs<Foliage> {
 	Mesh::Ptr bodyMesh;
 	std::shared_ptr<Texture> texture;
+	glVertexArray instanceVAO;
 
 public:
-	void render(const SceneShader &, const Location &) const;
-	void shadows(const ShadowMapper &, const Location &) const;
+	mutable InstanceVertices<glm::mat4> instances;
+	void render(const SceneShader &) const override;
+	void shadows(const ShadowMapper &) const override;
 
 protected:
 	friend Persistence::SelectionPtrBase<std::shared_ptr<Foliage>>;
diff --git a/game/scenary/plant.cpp b/game/scenary/plant.cpp
index 2b01bee..678d4a7 100644
--- a/game/scenary/plant.cpp
+++ b/game/scenary/plant.cpp
@@ -1,13 +1,7 @@
 #include "plant.h"
 
-void
-Plant::render(const SceneShader & shader) const
+Plant::Plant(std::shared_ptr<const Foliage> type, Location position) :
+	type {std::move(type)},
+	location {this->type->instances.acquire(glm::translate(position.pos) * rotate_ypr(position.rot))}
 {
-	type->render(shader, position);
-}
-
-void
-Plant::shadows(const ShadowMapper & mapper) const
-{
-	type->shadows(mapper, position);
 }
diff --git a/game/scenary/plant.h b/game/scenary/plant.h
index 55acca1..77c5979 100644
--- a/game/scenary/plant.h
+++ b/game/scenary/plant.h
@@ -2,15 +2,13 @@
 
 #include "foliage.h"
 #include "game/worldobject.h"
-#include "gfx/renderable.h"
 #include "location.hpp"
+#include "maths.h"
+#include <glm/gtx/transform.hpp>
 
-class Plant : public Renderable, public WorldObject {
+class Plant : public WorldObject {
 	std::shared_ptr<const Foliage> type;
-	Location position;
-
-	void render(const SceneShader & shader) const override;
-	void shadows(const ShadowMapper & shadowMapper) const override;
+	InstanceVertices<glm::mat4>::InstanceProxy location;
 
 	void
 	tick(TickDuration) override
@@ -18,5 +16,5 @@ class Plant : public Renderable, public WorldObject {
 	}
 
 public:
-	Plant(std::shared_ptr<const Foliage> type, Location position) : type(std::move(type)), position(position) { }
+	Plant(std::shared_ptr<const Foliage> type, Location position);
 };
diff --git a/test/test-assetFactory.cpp b/test/test-assetFactory.cpp
index 82543f3..817654b 100644
--- a/test/test-assetFactory.cpp
+++ b/test/test-assetFactory.cpp
@@ -108,7 +108,7 @@ BOOST_AUTO_TEST_CASE(foliage, *boost::unit_test::timeout(5))
 	BOOST_REQUIRE(tree_01_1_f);
 
 	auto plant = std::make_shared<Plant>(tree_01_1_f, Location {{-2, 2, 0}, {}});
-	objects.objects.push_back(plant);
+	objects.objects.push_back(tree_01_1_f);
 
 	render(5);
 }
diff --git a/ui/gameMainWindow.cpp b/ui/gameMainWindow.cpp
index 0b30cad..3f85a4f 100644
--- a/ui/gameMainWindow.cpp
+++ b/ui/gameMainWindow.cpp
@@ -51,6 +51,11 @@ GameMainWindow::render() const
 void
 GameMainWindow::content(const SceneShader & shader) const
 {
+	for (const auto & [id, asset] : gameState->assets) {
+		if (const auto r = std::dynamic_pointer_cast<const Renderable>(asset)) {
+			r->render(shader);
+		}
+	}
 	gameState->world.apply<Renderable>(&Renderable::render, shader);
 	uiComponents.apply<WorldOverlay>(&WorldOverlay::render, shader);
 }
@@ -68,5 +73,10 @@ GameMainWindow::lights(const SceneShader & shader) const
 void
 GameMainWindow::shadows(const ShadowMapper & shadowMapper) const
 {
+	for (const auto & [id, asset] : gameState->assets) {
+		if (const auto r = std::dynamic_pointer_cast<const Renderable>(asset)) {
+			r->shadows(shadowMapper);
+		}
+	}
 	gameState->world.apply<Renderable>(&Renderable::shadows, shadowMapper);
 }
-- 
cgit v1.2.3


From ab5b46dbe58ae0401c24ead55a2627b9a577fc1a Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Fri, 21 Apr 2023 23:52:56 +0100
Subject: Instances buffer data needs to be unmapped before use

Here we change quite a bit to support mapping and unmapping the buffer as required. The instance/free
referencing is still broken though. We add several instances of tree in the render.
---
 gfx/gl/instanceVertices.h  | 66 ++++++++++++++++++++++++++++++++--------------
 test/Jamfile.jam           |  2 +-
 test/test-assetFactory.cpp |  5 +++-
 test/test-instancing.cpp   | 15 +++++++----
 4 files changed, 61 insertions(+), 27 deletions(-)

(limited to 'test')

diff --git a/gfx/gl/instanceVertices.h b/gfx/gl/instanceVertices.h
index dc28a4d..c2ddb1d 100644
--- a/gfx/gl/instanceVertices.h
+++ b/gfx/gl/instanceVertices.h
@@ -9,11 +9,9 @@
 
 template<typename T> class InstanceVertices {
 public:
-	InstanceVertices(size_t initialSize = 16) : data {allocBuffer(buffer, initialSize)}, next {} { }
-
-	~InstanceVertices()
+	InstanceVertices(size_t initialSize = 16)
 	{
-		glUnmapNamedBuffer(buffer);
+		allocBuffer(initialSize);
 	}
 
 	class InstanceProxy {
@@ -50,17 +48,20 @@ public:
 		T &
 		operator=(U && v)
 		{
+			instances->map();
 			return instances->data[index] = std::forward<U>(v);
 		}
 
 		T *
 		get()
 		{
+			instances->map();
 			return &instances->data[index];
 		}
 		const T *
 		get() const
 		{
+			instances->map();
 			return &instances->data[index];
 		}
 		T *
@@ -95,22 +96,24 @@ public:
 	InstanceProxy
 	acquire(Params &&... params)
 	{
+		map();
 		if (!unused.empty()) {
 			auto idx = unused.back();
 			unused.pop_back();
-			new (&data[idx]) T(std::forward<Params>(params)...);
+			new (data + idx) T(std::forward<Params>(params)...);
 			return InstanceProxy {this, idx};
 		}
-		if (next >= data.size()) {
-			resize(data.size() * 2);
+		if (next >= capacity) {
+			resize(capacity * 2);
 		}
-		new (&data[next]) T(std::forward<Params>(params)...);
+		new (data + next) T(std::forward<Params>(params)...);
 		return InstanceProxy {this, next++};
 	}
 
 	void
 	release(const InstanceProxy & p)
 	{
+		map();
 		data[p.index].~T();
 		unused.push_back(p.index);
 	}
@@ -118,42 +121,65 @@ public:
 	const auto &
 	bufferName() const
 	{
+		unmap();
 		return buffer;
 	}
 
 	auto
 	count() const
 	{
+		unmap();
 		return next;
 	}
 
 protected:
 	friend InstanceProxy;
 
-	static std::span<T>
-	allocBuffer(const glBuffer & buffer, std::size_t count)
+	void
+	allocBuffer(std::size_t newCapacity)
 	{
 		glBindBuffer(GL_ARRAY_BUFFER, buffer);
-		glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(sizeof(T) * count), nullptr, GL_DYNAMIC_DRAW);
-		auto data = static_cast<T *>(glMapNamedBuffer(buffer, GL_READ_WRITE));
+		glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(sizeof(T) * newCapacity), nullptr, GL_DYNAMIC_DRAW);
 		glBindBuffer(GL_ARRAY_BUFFER, 0);
-		return {data, count};
+		capacity = newCapacity;
+		data = nullptr;
 	}
 
 	void
 	resize(size_t newCapacity)
 	{
-		const auto maintain = std::min(newCapacity, data.size());
-		const auto maintaind = static_cast<typename decltype(data)::difference_type>(maintain);
+		const auto maintain = std::min(newCapacity, capacity);
 		std::vector<T> existing;
+		const auto maintaind = static_cast<typename decltype(existing)::difference_type>(maintain);
 		existing.reserve(maintain);
-		std::move(data.begin(), data.begin() + maintaind, std::back_inserter(existing));
-		data = allocBuffer(buffer, newCapacity);
-		std::move(existing.begin(), existing.begin() + maintaind, data.begin());
+		map();
+		std::move(data, data + maintain, std::back_inserter(existing));
+		allocBuffer(newCapacity);
+		map();
+		std::move(existing.begin(), existing.begin() + maintaind, data);
+		capacity = newCapacity;
+	}
+
+	void
+	map() const
+	{
+		if (!data) {
+			data = static_cast<T *>(glMapNamedBuffer(buffer, GL_READ_WRITE));
+		}
+	}
+
+	void
+	unmap() const
+	{
+		if (data) {
+			glUnmapNamedBuffer(buffer);
+			data = nullptr;
+		}
 	}
 
 	glBuffer buffer;
-	std::span<T> data;
-	std::size_t next;
+	mutable T * data {};
+	std::size_t capacity {};
+	std::size_t next {};
 	std::vector<size_t> unused;
 };
diff --git a/test/Jamfile.jam b/test/Jamfile.jam
index 9cdcef4..1ce73b7 100644
--- a/test/Jamfile.jam
+++ b/test/Jamfile.jam
@@ -53,7 +53,7 @@ run test-text.cpp ;
 run test-enumDetails.cpp ;
 run test-render.cpp : -- : test-assetFactory : <library>test ;
 run test-glContextBhvr.cpp ;
-run test-assetFactory.cpp : -- : [ sequence.insertion-sort [ glob-tree $(res) : *.* ] fixtures/rgb.txt ] : <library>test ;
+run test-assetFactory.cpp : -- : [ sequence.insertion-sort [ glob-tree $(res) : *.* ] fixtures/rgb.txt test-instancing ] : <library>test ;
 run perf-assetFactory.cpp : -- : test-assetFactory : <library>benchmark <library>test ;
 run perf-persistence.cpp : -- : test-persistence : <library>benchmark <library>test ;
 run test-worker.cpp ;
diff --git a/test/test-assetFactory.cpp b/test/test-assetFactory.cpp
index 817654b..82a1825 100644
--- a/test/test-assetFactory.cpp
+++ b/test/test-assetFactory.cpp
@@ -107,7 +107,10 @@ BOOST_AUTO_TEST_CASE(foliage, *boost::unit_test::timeout(5))
 	auto tree_01_1_f = std::dynamic_pointer_cast<Foliage>(tree_01_1);
 	BOOST_REQUIRE(tree_01_1_f);
 
-	auto plant = std::make_shared<Plant>(tree_01_1_f, Location {{-2, 2, 0}, {}});
+	auto plant1 = std::make_shared<Plant>(tree_01_1_f, Location {{-2, 2, 0}, {0, 0, 0}});
+	auto plant2 = std::make_shared<Plant>(tree_01_1_f, Location {{3, -4, 0}, {0, 1, 0}});
+	auto plant3 = std::make_shared<Plant>(tree_01_1_f, Location {{-2, -4, 0}, {0, 2, 0}});
+	auto plant4 = std::make_shared<Plant>(tree_01_1_f, Location {{3, 2, 0}, {0, 3, 0}});
 	objects.objects.push_back(tree_01_1_f);
 
 	render(5);
diff --git a/test/test-instancing.cpp b/test/test-instancing.cpp
index 7191567..2eb3647 100644
--- a/test/test-instancing.cpp
+++ b/test/test-instancing.cpp
@@ -15,16 +15,21 @@ BOOST_FIXTURE_TEST_SUITE(i, InstanceVertices<int>)
 
 BOOST_AUTO_TEST_CASE(createDestroy)
 {
-	BOOST_REQUIRE(data.data());
+	BOOST_CHECK(!data);
+	map();
+	BOOST_REQUIRE(data);
 	BOOST_CHECK_EQUAL(0, next);
 	BOOST_CHECK(unused.empty());
+	unmap();
+	BOOST_CHECK(!data);
 }
 
 BOOST_AUTO_TEST_CASE(storeRetreive)
 { // Read write raw buffer, not normally allowed
-	std::vector<int> test(data.size());
-	std::copy(test.begin(), test.end(), data.begin());
-	BOOST_CHECK_EQUAL_COLLECTIONS(test.begin(), test.end(), data.begin(), data.end());
+	std::vector<int> test(capacity);
+	map();
+	std::copy(test.begin(), test.end(), data);
+	BOOST_CHECK_EQUAL_COLLECTIONS(test.begin(), test.end(), data, data + capacity);
 }
 
 BOOST_AUTO_TEST_CASE(acquireRelease)
@@ -69,7 +74,7 @@ BOOST_AUTO_TEST_CASE(resize)
 		proxies.push_back(acquire(n));
 		expected.emplace_back(n);
 	}
-	BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), data.begin(), data.begin() + COUNT);
+	BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), data, data + COUNT);
 	BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), proxies.begin(), proxies.end());
 }
 
-- 
cgit v1.2.3


From b8889129c6c2b747ff771b711cfbbf91bca93bb6 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Sat, 22 Apr 2023 11:44:38 +0100
Subject: Fix the instancing maintenance

---
 gfx/gl/instanceVertices.h |  83 ++++++++++++++++++++++++------------
 test/test-instancing.cpp  | 105 +++++++++++++++++++++++++++++++++++++++++-----
 2 files changed, 152 insertions(+), 36 deletions(-)

(limited to 'test')

diff --git a/gfx/gl/instanceVertices.h b/gfx/gl/instanceVertices.h
index c2ddb1d..036b48e 100644
--- a/gfx/gl/instanceVertices.h
+++ b/gfx/gl/instanceVertices.h
@@ -32,37 +32,38 @@ public:
 		InstanceProxy &
 		operator=(InstanceProxy && other)
 		{
+			if (instances) {
+				instances->release(*this);
+			}
 			instances = std::exchange(other.instances, nullptr);
 			index = other.index;
+			return *this;
+		}
+		template<typename U>
+		T &
+		operator=(U && v)
+		{
+			return instances->at(index) = std::forward<U>(v);
 		}
 
 		operator T &()
 		{
-			return *get();
+			return instances->at(index);
 		}
 		operator const T &() const
 		{
-			return *get();
-		}
-		template<typename U>
-		T &
-		operator=(U && v)
-		{
-			instances->map();
-			return instances->data[index] = std::forward<U>(v);
+			return instances->at(index);
 		}
 
 		T *
 		get()
 		{
-			instances->map();
-			return &instances->data[index];
+			return &instances->at(index);
 		}
 		const T *
 		get() const
 		{
-			instances->map();
-			return &instances->data[index];
+			return &instances->at(index);
 		}
 		T *
 		operator->()
@@ -77,12 +78,12 @@ public:
 		T &
 		operator*()
 		{
-			return *get();
+			return instances->at(index);
 		}
 		const T &
 		operator*() const
 		{
-			return *get();
+			return instances->at(index);
 		}
 
 	private:
@@ -100,22 +101,16 @@ public:
 		if (!unused.empty()) {
 			auto idx = unused.back();
 			unused.pop_back();
-			new (data + idx) T(std::forward<Params>(params)...);
+			index[idx] = next++;
+			new (&at(idx)) T(std::forward<Params>(params)...);
 			return InstanceProxy {this, idx};
 		}
 		if (next >= capacity) {
 			resize(capacity * 2);
 		}
-		new (data + next) T(std::forward<Params>(params)...);
-		return InstanceProxy {this, next++};
-	}
-
-	void
-	release(const InstanceProxy & p)
-	{
-		map();
-		data[p.index].~T();
-		unused.push_back(p.index);
+		index.emplace_back(next++);
+		new (data + index.back()) T(std::forward<Params>(params)...);
+		return InstanceProxy {this, index.size() - 1};
 	}
 
 	const auto &
@@ -135,6 +130,30 @@ public:
 protected:
 	friend InstanceProxy;
 
+	void
+	release(const InstanceProxy & p)
+	{
+		// Destroy p's object
+		at(p.index).~T();
+		if (next-- > index[p.index]) {
+			// Remember p.index is free index now
+			unused.push_back(p.index);
+			// Move last object into p's slot
+			new (&at(p.index)) T {std::move(data[next])};
+			(data[next]).~T();
+			for (auto & i : index) {
+				if (i == next) {
+					i = index[p.index];
+					break;
+				}
+			}
+			index[p.index] = 100000;
+		}
+		else {
+			index.pop_back();
+		}
+	}
+
 	void
 	allocBuffer(std::size_t newCapacity)
 	{
@@ -160,6 +179,13 @@ protected:
 		capacity = newCapacity;
 	}
 
+	T &
+	at(size_t pindex)
+	{
+		map();
+		return data[index[pindex]];
+	}
+
 	void
 	map() const
 	{
@@ -179,7 +205,12 @@ protected:
 
 	glBuffer buffer;
 	mutable T * data {};
+	// Size of buffer
 	std::size_t capacity {};
+	// # used of capacity
 	std::size_t next {};
+	// Index into buffer given to nth proxy
+	std::vector<size_t> index;
+	// List of free spaces in index
 	std::vector<size_t> unused;
 };
diff --git a/test/test-instancing.cpp b/test/test-instancing.cpp
index 2eb3647..8837071 100644
--- a/test/test-instancing.cpp
+++ b/test/test-instancing.cpp
@@ -1,10 +1,12 @@
 #define BOOST_TEST_MODULE instancing
 
+#include "stream_support.hpp"
 #include "testHelpers.h"
 #include "testMainWindow.h"
 #include "ui/applicationBase.h"
 #include <boost/test/data/test_case.hpp>
 #include <boost/test/unit_test.hpp>
+#include <set>
 
 #include <gfx/gl/instanceVertices.h>
 
@@ -20,28 +22,25 @@ BOOST_AUTO_TEST_CASE(createDestroy)
 	BOOST_REQUIRE(data);
 	BOOST_CHECK_EQUAL(0, next);
 	BOOST_CHECK(unused.empty());
+	BOOST_CHECK(index.empty());
 	unmap();
 	BOOST_CHECK(!data);
 }
 
-BOOST_AUTO_TEST_CASE(storeRetreive)
-{ // Read write raw buffer, not normally allowed
-	std::vector<int> test(capacity);
-	map();
-	std::copy(test.begin(), test.end(), data);
-	BOOST_CHECK_EQUAL_COLLECTIONS(test.begin(), test.end(), data, data + capacity);
-}
-
 BOOST_AUTO_TEST_CASE(acquireRelease)
 {
 	{
 		auto proxy = acquire();
 		*proxy = 20;
 		BOOST_CHECK_EQUAL(1, next);
+		BOOST_REQUIRE_EQUAL(1, index.size());
+		BOOST_CHECK_EQUAL(0, index.front());
+		BOOST_CHECK(unused.empty());
 	}
-	BOOST_CHECK_EQUAL(1, next);
+	BOOST_CHECK_EQUAL(0, next);
 	BOOST_CHECK_EQUAL(1, unused.size());
 	BOOST_CHECK_EQUAL(0, unused.front());
+	BOOST_CHECK_EQUAL(1, index.size());
 }
 
 BOOST_AUTO_TEST_CASE(acquireReleaseMove)
@@ -54,9 +53,10 @@ BOOST_AUTO_TEST_CASE(acquireReleaseMove)
 		*proxy2 = 40;
 		BOOST_CHECK_EQUAL(1, next);
 	}
-	BOOST_CHECK_EQUAL(1, next);
+	BOOST_CHECK_EQUAL(0, next);
 	BOOST_CHECK_EQUAL(1, unused.size());
 	BOOST_CHECK_EQUAL(0, unused.front());
+	BOOST_CHECK_EQUAL(1, index.size());
 }
 
 BOOST_AUTO_TEST_CASE(initialize)
@@ -78,4 +78,89 @@ BOOST_AUTO_TEST_CASE(resize)
 	BOOST_CHECK_EQUAL_COLLECTIONS(expected.begin(), expected.end(), proxies.begin(), proxies.end());
 }
 
+BOOST_AUTO_TEST_CASE(shuffle)
+{
+	std::vector<decltype(acquire())> proxies;
+	BOOST_CHECK_EQUAL(0, proxies.emplace_back(acquire(0)));
+	BOOST_CHECK_EQUAL(1, proxies.emplace_back(acquire(1)));
+	BOOST_CHECK_EQUAL(2, proxies.emplace_back(acquire(2)));
+	BOOST_CHECK_EQUAL(3, proxies.emplace_back(acquire(3)));
+	BOOST_CHECK_EQUAL(4, next);
+	BOOST_CHECK_EQUAL(data + 0, proxies[0].get());
+	BOOST_CHECK_EQUAL(data + 1, proxies[1].get());
+	BOOST_CHECK_EQUAL(data + 2, proxies[2].get());
+	BOOST_CHECK_EQUAL(data + 3, proxies[3].get());
+	BOOST_CHECK(unused.empty());
+	BOOST_REQUIRE_EQUAL(4, index.size());
+	BOOST_CHECK_EQUAL(0, index[0]);
+	BOOST_CHECK_EQUAL(1, index[1]);
+	BOOST_CHECK_EQUAL(2, index[2]);
+	BOOST_CHECK_EQUAL(3, index[3]);
+	// Remove 1, 3 moves to [1]
+	proxies.erase(proxies.begin() + 1);
+	BOOST_REQUIRE_EQUAL(4, index.size());
+	BOOST_REQUIRE_EQUAL(1, unused.size());
+	BOOST_CHECK_EQUAL(1, unused[0]);
+	BOOST_CHECK_EQUAL(data + 0, proxies[0].get());
+	BOOST_CHECK_EQUAL(data + 2, proxies[1].get());
+	BOOST_CHECK_EQUAL(data + 1, proxies[2].get());
+	// Remove 1, 2 moves to [1]
+	proxies.erase(proxies.begin() + 1);
+	BOOST_REQUIRE_EQUAL(4, index.size());
+	BOOST_REQUIRE_EQUAL(2, unused.size());
+	BOOST_CHECK_EQUAL(1, unused[0]);
+	BOOST_CHECK_EQUAL(2, unused[1]);
+	BOOST_CHECK_EQUAL(data + 0, proxies[0].get());
+	BOOST_CHECK_EQUAL(data + 1, proxies[1].get());
+	// Add new, takes 2 at [2]
+	BOOST_CHECK_EQUAL(4, proxies.emplace_back(acquire(4)));
+	BOOST_REQUIRE_EQUAL(4, index.size());
+	BOOST_REQUIRE_EQUAL(1, unused.size());
+	BOOST_CHECK_EQUAL(1, unused[0]);
+	BOOST_CHECK_EQUAL(data + 0, proxies[0].get());
+	BOOST_CHECK_EQUAL(data + 1, proxies[1].get());
+	BOOST_CHECK_EQUAL(data + 2, proxies[2].get());
+}
+
+BOOST_DATA_TEST_CASE(shuffle_random, boost::unit_test::data::xrange(0, 10), x)
+{
+	std::ignore = x;
+	std::mt19937 gen(std::random_device {}());
+	std::map<int, InstanceVertices<int>::InstanceProxy> proxies;
+	const std::string_view actions = "aaaaaaaarararrraarrrararararaarrrarararararararararraarrrraaaarararaararar";
+	int n {};
+	for (const auto action : actions) {
+		switch (action) {
+			case 'a':
+				BOOST_REQUIRE_EQUAL(n, proxies.emplace(n, acquire(n)).first->second);
+				n++;
+				break;
+			case 'r':
+				BOOST_REQUIRE(!proxies.empty());
+				auto e = std::next(proxies.begin(),
+						std::uniform_int_distribution<> {0, static_cast<int>(proxies.size() - 1)}(gen));
+				proxies.erase(e);
+				break;
+		}
+
+		BOOST_REQUIRE_EQUAL(next, proxies.size());
+		for (const auto & [n, p] : proxies) {
+			BOOST_REQUIRE_EQUAL(n, p);
+		}
+		std::set<size_t> iused;
+		for (size_t i {}; i < index.size(); i++) {
+			if (std::find(unused.begin(), unused.end(), i) == unused.end()) {
+				iused.emplace(index[i]);
+			}
+		}
+		BOOST_TEST_CONTEXT(index) {
+			BOOST_REQUIRE_EQUAL(iused.size(), next);
+			if (!iused.empty()) {
+				BOOST_REQUIRE_EQUAL(*iused.begin(), 0);
+				BOOST_REQUIRE_EQUAL(*iused.rbegin(), next - 1);
+			}
+		}
+	}
+}
+
 BOOST_AUTO_TEST_SUITE_END()
-- 
cgit v1.2.3


From bcd0e98739974248e6d29f72e8b281e21fd59657 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Sat, 22 Apr 2023 13:17:05 +0100
Subject: Test instancing automatic unmap when count is called, add some
 nodiscard

---
 gfx/gl/instanceVertices.h | 27 +++++++++++++--------------
 test/test-instancing.cpp  | 19 ++++++++++++++++++-
 2 files changed, 31 insertions(+), 15 deletions(-)

(limited to 'test')

diff --git a/gfx/gl/instanceVertices.h b/gfx/gl/instanceVertices.h
index cf4f0ae..edcaef4 100644
--- a/gfx/gl/instanceVertices.h
+++ b/gfx/gl/instanceVertices.h
@@ -14,7 +14,7 @@ public:
 		allocBuffer(initialSize);
 	}
 
-	class InstanceProxy {
+	class [[nodiscard]] InstanceProxy {
 	public:
 		InstanceProxy(InstanceVertices * iv, std::size_t idx) : instances {iv}, index {idx} { }
 		InstanceProxy(InstanceProxy && other) : instances {std::exchange(other.instances, nullptr)}, index {other.index}
@@ -46,41 +46,41 @@ public:
 			return instances->at(index) = std::forward<U>(v);
 		}
 
+		[[nodiscard]]
 		operator T &()
 		{
 			return instances->at(index);
 		}
-		operator const T &() const
+		[[nodiscard]] operator const T &() const
 		{
 			return instances->at(index);
 		}
-
-		T *
+		[[nodiscard]] T *
 		get()
 		{
 			return &instances->at(index);
 		}
-		const T *
+		[[nodiscard]] const T *
 		get() const
 		{
 			return &instances->at(index);
 		}
-		T *
+		[[nodiscard]] T *
 		operator->()
 		{
 			return get();
 		}
-		const T *
+		[[nodiscard]] const T *
 		operator->() const
 		{
 			return get();
 		}
-		T &
+		[[nodiscard]] T &
 		operator*()
 		{
 			return instances->at(index);
 		}
-		const T &
+		[[nodiscard]] const T &
 		operator*() const
 		{
 			return instances->at(index);
@@ -92,7 +92,7 @@ public:
 	};
 
 	template<typename... Params>
-	InstanceProxy
+	[[nodiscard]] InstanceProxy
 	acquire(Params &&... params)
 	{
 		map();
@@ -111,14 +111,13 @@ public:
 		return InstanceProxy {this, index.size() - 1};
 	}
 
-	const auto &
+	[[nodiscard]] const auto &
 	bufferName() const
 	{
-		unmap();
 		return buffer;
 	}
 
-	auto
+	[[nodiscard]] auto
 	count() const
 	{
 		unmap();
@@ -173,7 +172,7 @@ protected:
 		capacity = newCapacity;
 	}
 
-	T &
+	[[nodiscard]] T &
 	at(size_t pindex)
 	{
 		map();
diff --git a/test/test-instancing.cpp b/test/test-instancing.cpp
index 8837071..70b20fa 100644
--- a/test/test-instancing.cpp
+++ b/test/test-instancing.cpp
@@ -50,8 +50,9 @@ BOOST_AUTO_TEST_CASE(acquireReleaseMove)
 		*proxy1 = 20;
 		BOOST_CHECK_EQUAL(1, next);
 		auto proxy2 = std::move(proxy1);
-		*proxy2 = 40;
+		proxy2 = 40;
 		BOOST_CHECK_EQUAL(1, next);
+		BOOST_CHECK_EQUAL(data[0], 40);
 	}
 	BOOST_CHECK_EQUAL(0, next);
 	BOOST_CHECK_EQUAL(1, unused.size());
@@ -59,10 +60,26 @@ BOOST_AUTO_TEST_CASE(acquireReleaseMove)
 	BOOST_CHECK_EQUAL(1, index.size());
 }
 
+BOOST_AUTO_TEST_CASE(autoMapUnmap)
+{
+	{
+		auto proxy = acquire();
+		BOOST_CHECK(data);
+		std::ignore = bufferName();
+		BOOST_CHECK(data);
+		BOOST_CHECK_EQUAL(1, count());
+		BOOST_CHECK(!data);
+	}
+	BOOST_CHECK_EQUAL(0, count());
+}
+
 BOOST_AUTO_TEST_CASE(initialize)
 {
 	auto proxy = acquire(5);
+	const auto & constProxy = proxy;
 	BOOST_CHECK_EQUAL(*proxy, 5);
+	BOOST_CHECK_EQUAL(*constProxy, 5);
+	BOOST_CHECK_EQUAL(constProxy.get(), constProxy.get());
 }
 
 BOOST_AUTO_TEST_CASE(resize)
-- 
cgit v1.2.3


From 59d85f9b87fc82b52f2c7438e38768aeddc3cc87 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Sat, 22 Apr 2023 14:56:08 +0100
Subject: Don't fill the instances unused vector unnecessarily

---
 gfx/gl/instanceVertices.h | 17 ++++++++++-------
 test/test-instancing.cpp  | 10 ++++------
 2 files changed, 14 insertions(+), 13 deletions(-)

(limited to 'test')

diff --git a/gfx/gl/instanceVertices.h b/gfx/gl/instanceVertices.h
index edcaef4..51bf4bc 100644
--- a/gfx/gl/instanceVertices.h
+++ b/gfx/gl/instanceVertices.h
@@ -132,19 +132,22 @@ protected:
 	{
 		// Destroy p's object
 		at(pidx).~T();
-		if (next-- > index[pidx]) {
-			// Remember p.index is free index now
-			unused.push_back(pidx);
+		if (--next != index[pidx]) {
 			// Move last object into p's slot
 			new (&at(pidx)) T {std::move(data[next])};
 			(data[next]).~T();
-			*std::find(index.begin(), index.end(), next) = index[pidx];
-			// Not strictly required, but needed for uniqueness unit test assertion
-			index[pidx] = static_cast<size_t>(-1);
+			*std::find_if(index.begin(), index.end(), [this](const auto & i) {
+				const auto n = &i - index.data();
+				return i == next && std::find(unused.begin(), unused.end(), n) == unused.end();
+			}) = index[pidx];
 		}
-		else {
+		if (pidx == index.size() - 1) {
 			index.pop_back();
 		}
+		else {
+			// Remember p.index is free index now
+			unused.push_back(pidx);
+		}
 	}
 
 	void
diff --git a/test/test-instancing.cpp b/test/test-instancing.cpp
index 70b20fa..c743ce0 100644
--- a/test/test-instancing.cpp
+++ b/test/test-instancing.cpp
@@ -38,9 +38,8 @@ BOOST_AUTO_TEST_CASE(acquireRelease)
 		BOOST_CHECK(unused.empty());
 	}
 	BOOST_CHECK_EQUAL(0, next);
-	BOOST_CHECK_EQUAL(1, unused.size());
-	BOOST_CHECK_EQUAL(0, unused.front());
-	BOOST_CHECK_EQUAL(1, index.size());
+	BOOST_CHECK(unused.empty());
+	BOOST_CHECK(index.empty());
 }
 
 BOOST_AUTO_TEST_CASE(acquireReleaseMove)
@@ -55,9 +54,8 @@ BOOST_AUTO_TEST_CASE(acquireReleaseMove)
 		BOOST_CHECK_EQUAL(data[0], 40);
 	}
 	BOOST_CHECK_EQUAL(0, next);
-	BOOST_CHECK_EQUAL(1, unused.size());
-	BOOST_CHECK_EQUAL(0, unused.front());
-	BOOST_CHECK_EQUAL(1, index.size());
+	BOOST_CHECK(unused.empty());
+	BOOST_CHECK(index.empty());
 }
 
 BOOST_AUTO_TEST_CASE(autoMapUnmap)
-- 
cgit v1.2.3