Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/polyscope/render/engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class AttributeBuffer {
virtual void setData(const std::vector<std::array<glm::vec3, 3>>& data) = 0;
virtual void setData(const std::vector<std::array<glm::vec3, 4>>& data) = 0;

// Pre-allocate GPU memory for n elements without uploading any data. Subsequent setData()
// calls with size <= n will not need to reallocate the underlying buffer.
virtual void reserveCapacity(size_t n) = 0;

virtual uint32_t getNativeBufferID() = 0; // used to interop with external things, e.g. ImGui

// == Getters
Expand Down
55 changes: 55 additions & 0 deletions include/polyscope/render/managed_buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,56 @@ class ManagedBuffer : public virtual WeakReferrable {
void setTextureSize(uint32_t sizeX, uint32_t sizeY, uint32_t sizeZ);
std::array<uint32_t, 3> getTextureSize() const;

// == Resize / capacity API

// The .size() of the buffer is the number of data elements it holds.
//
// The .capacity() of the buffer is the number of data elements it has capacity for without needing to reallocate
// (similar to std::vector).
//
// In a simple flow where we put data in a buffer once, the capacity is the same as the size and neither ever changes.
// But in settings where we e.g. incrementally add elements or change the number of data elements on each frame, we
// may want to allocate a larger capacity to avoid expensive re-allocation each time.

// Resize the buffer to newSize elements.
//
// If newSize <= capacity(), this is a cheap constant-time operation which just updates metadata.
//
// If newSize > capacity(), this triggers a full reallocation. The new capacity is set to at least
// 2*oldCapacity (amortized doubling). Any data that was GPU-resident is first copied back to the
// host, then the GPU buffer is reallocated in-place (same buffer object, new backing memory).
// ShaderPrograms holding a reference to the GPU buffer remain valid.
//
// Returns true if a reallocation occurred, false if the resize stayed within capacity.
//
// Valid for attributes and 1D textures only; call the 2D/3D variants below for multidimensional
// textures.
bool resize(size_t newSize);

// Resize a 2D texture. No-op (returns false) if dimensions are unchanged. Otherwise always
// triggers a reallocation (2D/3D textures have no capacity slack — capacity always equals size),
// reallocates the GPU buffer in-place, and returns true.
bool resizeTexture2D(uint32_t newSizeX, uint32_t newSizeY);

// Resize a 3D texture. No-op (returns false) if dimensions are unchanged. Otherwise always
// triggers a reallocation (2D/3D textures have no capacity slack — capacity always equals size),
// reallocates the GPU buffer in-place, and returns true.
bool resizeTexture3D(uint32_t newSizeX, uint32_t newSizeY, uint32_t newSizeZ);

// The maximum size the buffer can be resized to without triggering a reallocation. Always >= size().
size_t capacity();

// Set the managed capacity to newCapacity. Unlike resize(), which grows capacity via amortized
// doubling, this sets the logical capacity to a precise value. Error if newCapacity < size().
// Reallocates the GPU buffer in-place (same buffer object, new backing memory) if one exists.
//
// Note: data.capacity() is guaranteed to be >= managedCapacity, but may remain larger than
// newCapacity if the underlying vector already had more space allocated.
//
// Valid for attributes and 1D textures only; multidimensional textures always have capacity
// equal to their size, so use the 2D/3D resize() variants instead.
void setCapacity(size_t newCapacity);


// == Members for indexed data

Expand Down Expand Up @@ -200,6 +250,11 @@ class ManagedBuffer : public virtual WeakReferrable {

bool hostBufferIsPopulated; // true if the host buffer contains currently-valid data

// The buffer has capacity for at least this many elements. It is distinct from .size(), which is the actual number of
// elements currently stored in the buffer and may be smaller than the capacity.
// Any resize() operations that stay within the capacity are cheap, and do not trigger a full reallocation and copy.
size_t managedCapacity = 0;

std::shared_ptr<render::AttributeBuffer> renderAttributeBuffer;
std::shared_ptr<render::TextureBuffer> renderTextureBuffer;

Expand Down
1 change: 1 addition & 0 deletions include/polyscope/render/mock_opengl/mock_gl_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class GLAttributeBuffer : public AttributeBuffer {
std::vector<glm::uvec4> getDataRange_uvec4(size_t ind, size_t count) override;

uint32_t getNativeBufferID() override;
void reserveCapacity(size_t n) override;

protected:
private:
Expand Down
1 change: 1 addition & 0 deletions include/polyscope/render/opengl/gl_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class GLAttributeBuffer : public AttributeBuffer {
std::vector<glm::uvec4> getDataRange_uvec4(size_t ind, size_t count) override;

uint32_t getNativeBufferID() override;
void reserveCapacity(size_t n) override;

protected:
VertexBufferHandle VBOLoc;
Expand Down
138 changes: 135 additions & 3 deletions src/render/managed_buffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace render {
template <typename T>
ManagedBuffer<T>::ManagedBuffer(ManagedBufferRegistry* registry_, const std::string& name_, std::vector<T>& data_)
: name(name_), uniqueID(internal::getNextUniqueID()), registry(registry_), data(data_), dataGetsComputed(false),
hostBufferIsPopulated(true) {
hostBufferIsPopulated(true), managedCapacity(data_.size()) {

if (registry) {
registry->addManagedBuffer<T>(this);
Expand All @@ -30,7 +30,7 @@ template <typename T>
ManagedBuffer<T>::ManagedBuffer(ManagedBufferRegistry* registry_, const std::string& name_, std::vector<T>& data_,
std::function<void()> computeFunc_)
: name(name_), uniqueID(internal::getNextUniqueID()), registry(registry_), data(data_), dataGetsComputed(true),
computeFunc(computeFunc_), hostBufferIsPopulated(false) {
computeFunc(computeFunc_), hostBufferIsPopulated(false), managedCapacity(0) {

if (registry) {
registry->addManagedBuffer<T>(this);
Expand All @@ -56,6 +56,8 @@ void ManagedBuffer<T>::setTextureSize(uint32_t sizeX_) {

deviceBufferType = DeviceBufferType::Texture1d;
sizeX = sizeX_;
// Sync managedCapacity: data is expected to be populated before setTextureSize() is called.
if (data.size() > managedCapacity) managedCapacity = data.size();
}

template <typename T>
Expand All @@ -66,6 +68,7 @@ void ManagedBuffer<T>::setTextureSize(uint32_t sizeX_, uint32_t sizeY_) {
deviceBufferType = DeviceBufferType::Texture2d;
sizeX = sizeX_;
sizeY = sizeY_;
if (data.size() > managedCapacity) managedCapacity = data.size();
}

template <typename T>
Expand All @@ -77,6 +80,7 @@ void ManagedBuffer<T>::setTextureSize(uint32_t sizeX_, uint32_t sizeY_, uint32_t
sizeX = sizeX_;
sizeY = sizeY_;
sizeZ = sizeZ_;
if (data.size() > managedCapacity) managedCapacity = data.size();
}

template <typename T>
Expand All @@ -85,6 +89,126 @@ std::array<uint32_t, 3> ManagedBuffer<T>::getTextureSize() const {
return std::array<uint32_t, 3>{sizeX, sizeY, sizeZ};
}

template <typename T>
size_t ManagedBuffer<T>::capacity() {
return managedCapacity;
}

template <typename T>
bool ManagedBuffer<T>::resize(size_t newSize) {
if (deviceBufferType == DeviceBufferType::Texture2d || deviceBufferType == DeviceBufferType::Texture3d)
exception("resize() is not valid for 2D/3D texture buffers; use resizeTexture2D() or resizeTexture3D()");

if (newSize > managedCapacity) {
// Copy device-side data back to host BEFORE modifying data or invalidating the GPU buffer.
// ensureHostBufferPopulated() reads from renderAttributeBuffer into data; if we resize data
// first it would overwrite the resize with the old GPU contents.
ensureHostBufferPopulated();

// Reallocation needed: use amortized doubling
size_t newCapacity = std::max(newSize, 2 * managedCapacity);
data.reserve(newCapacity);
data.resize(newSize);
managedCapacity = newCapacity;

// In-place reallocation: keep the same GPU buffer objects alive so ShaderPrograms
// holding shared_ptr references to them remain valid without needing to be reset.
if (renderAttributeBuffer) {
renderAttributeBuffer->reserveCapacity(managedCapacity);
}
if (renderTextureBuffer && deviceBufferType == DeviceBufferType::Texture1d) {
renderTextureBuffer->resize(static_cast<uint32_t>(managedCapacity));
}
hostBufferIsPopulated = true;

if (deviceBufferType == DeviceBufferType::Texture1d) {
sizeX = static_cast<uint32_t>(newSize);
}

return true;
} else {
// No reallocation: just update size metadata
data.resize(newSize);

if (deviceBufferType == DeviceBufferType::Texture1d) {
sizeX = static_cast<uint32_t>(newSize);
}

return false;
}
}

template <typename T>
void ManagedBuffer<T>::setCapacity(size_t newCapacity) {
if (deviceBufferType == DeviceBufferType::Texture2d || deviceBufferType == DeviceBufferType::Texture3d)
exception("setCapacity() is not valid for 2D/3D texture buffers");

if (newCapacity < data.size())
exception("setCapacity() cannot set capacity below current size (" + std::to_string(data.size()) + ")");

if (newCapacity == managedCapacity) return; // no-op

// Before invalidating the GPU buffer, copy any device-side changes back to the host.
ensureHostBufferPopulated();

data.reserve(newCapacity);
managedCapacity = newCapacity;

// In-place reallocation: keep the same GPU buffer objects alive.
if (renderAttributeBuffer) {
renderAttributeBuffer->reserveCapacity(managedCapacity);
}
if (renderTextureBuffer && deviceBufferType == DeviceBufferType::Texture1d) {
renderTextureBuffer->resize(static_cast<uint32_t>(managedCapacity));
}
hostBufferIsPopulated = true;
}

template <typename T>
bool ManagedBuffer<T>::resizeTexture2D(uint32_t newSizeX, uint32_t newSizeY) {
checkDeviceBufferTypeIs(DeviceBufferType::Texture2d);

if (newSizeX == sizeX && newSizeY == sizeY) return false; // no-op

// Before invalidating the GPU buffer, copy any device-side changes back to the host.
ensureHostBufferPopulated();

sizeX = newSizeX;
sizeY = newSizeY;
managedCapacity = static_cast<size_t>(sizeX) * sizeY;
data.resize(managedCapacity);

if (renderTextureBuffer) {
renderTextureBuffer->resize(sizeX, sizeY);
}
hostBufferIsPopulated = true;

return true;
}

template <typename T>
bool ManagedBuffer<T>::resizeTexture3D(uint32_t newSizeX, uint32_t newSizeY, uint32_t newSizeZ) {
checkDeviceBufferTypeIs(DeviceBufferType::Texture3d);

if (newSizeX == sizeX && newSizeY == sizeY && newSizeZ == sizeZ) return false; // no-op

// Before invalidating the GPU buffer, copy any device-side changes back to the host.
ensureHostBufferPopulated();

sizeX = newSizeX;
sizeY = newSizeY;
sizeZ = newSizeZ;
managedCapacity = static_cast<size_t>(sizeX) * sizeY * sizeZ;
data.resize(managedCapacity);

if (renderTextureBuffer) {
renderTextureBuffer->resize(sizeX, sizeY, sizeZ);
}
hostBufferIsPopulated = true;

return true;
}

template <typename T>
void ManagedBuffer<T>::ensureHostBufferPopulated() {

Expand Down Expand Up @@ -312,7 +436,11 @@ std::shared_ptr<render::AttributeBuffer> ManagedBuffer<T>::getRenderAttributeBuf

if (!renderAttributeBuffer) {
ensureHostBufferPopulated(); // warning: the order of these matters because of how hostBufferPopulated works
// Sync managedCapacity on first GPU allocation: handles the case where data was populated
// externally before this call (e.g. class member ordering caused empty-vector-at-construction).
if (data.size() > managedCapacity) managedCapacity = data.size();
renderAttributeBuffer = generateAttributeBuffer<T>(render::engine);
renderAttributeBuffer->reserveCapacity(managedCapacity);
renderAttributeBuffer->setData(data);
}
return renderAttributeBuffer;
Expand All @@ -324,6 +452,8 @@ std::shared_ptr<render::TextureBuffer> ManagedBuffer<T>::getRenderTextureBuffer(

if (!renderTextureBuffer) {
ensureHostBufferPopulated(); // warning: the order of these matters because of how hostBufferPopulated works
// Sync managedCapacity on first GPU allocation (same rationale as getRenderAttributeBuffer)
if (data.size() > managedCapacity) managedCapacity = data.size();

renderTextureBuffer = generateTextureBuffer<T>(deviceBufferType, render::engine);

Expand All @@ -333,7 +463,9 @@ std::shared_ptr<render::TextureBuffer> ManagedBuffer<T>::getRenderTextureBuffer(
exception("bad call");
break;
case DeviceBufferType::Texture1d:
renderTextureBuffer->resize(sizeX);
// Allocate GPU texture at the full managed capacity so future within-capacity resizes
// don't require a new GPU buffer. setData() below uploads only data.size() elements.
renderTextureBuffer->resize(static_cast<uint32_t>(managedCapacity));
break;
case DeviceBufferType::Texture2d:
renderTextureBuffer->resize(sizeX, sizeY);
Expand Down
19 changes: 11 additions & 8 deletions src/render/mock_opengl/mock_gl_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,22 @@ void GLAttributeBuffer::checkArray(int testArrayCount) {
}


void GLAttributeBuffer::reserveCapacity(size_t n) {
if (n <= bufferSize) return;
bufferSize = static_cast<uint64_t>(n);
setFlag = true;
}

template <typename T>
void GLAttributeBuffer::setData_helper(const std::vector<T>& data) {
bind();

// allocate if needed
if (!isSet() || data.size() > bufferSize) {
setFlag = true;
uint64_t newSize = data.size();
newSize = std::max(newSize, 2 * bufferSize); // if we're expanding, at-least double
uint64_t newSize = static_cast<uint64_t>(data.size());
bufferSize = newSize;
}

// do the actual copy
dataSize = data.size();

checkGLError();
Expand Down Expand Up @@ -411,7 +414,7 @@ void GLTextureBuffer::setData(const std::vector<glm::vec3>& data) {

bind();

if (data.size() != getTotalSize()) {
if (data.size() > getTotalSize()) {
exception("OpenGL error: texture buffer data is not the right size.");
}

Expand All @@ -431,7 +434,7 @@ void GLTextureBuffer::setData(const std::vector<glm::vec4>& data) {

bind();

if (data.size() != getTotalSize()) {
if (data.size() > getTotalSize()) {
exception("OpenGL error: texture buffer data is not the right size.");
}

Expand All @@ -450,7 +453,7 @@ void GLTextureBuffer::setData(const std::vector<glm::vec4>& data) {
void GLTextureBuffer::setData(const std::vector<float>& data) {
bind();

if (data.size() != getTotalSize()) {
if (data.size() > getTotalSize()) {
exception("OpenGL error: texture buffer data is not the right size.");
}

Expand All @@ -469,7 +472,7 @@ void GLTextureBuffer::setData(const std::vector<float>& data) {
void GLTextureBuffer::setData(const std::vector<double>& data) {
bind();

if (data.size() != getTotalSize()) {
if (data.size() > getTotalSize()) {
exception("OpenGL error: texture buffer data is not the right size.");
}

Expand Down
Loading
Loading