From ec5560db738bb0329402f1bb3c86ca6a679fdb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Foucault?= Date: Wed, 12 Jan 2022 13:03:07 +0100 Subject: DRW: Add DRW_gpu_wrapper.hh This adds wrapper classes that make it easier to use GPU objects in C++. ####Motivations:#### - Easier handling of GPU objects. - EEVEE rewrite already makes use of similar wrappers. - There is the ongoing effort to use more C++ in the codebase and lans to port more engines to it. - The shader code refactor will make use of many UBOs with shared struct declaration. This helps managing them. - Safer handling of `TextureFromPool` which can't be bound as normal texture (only texture ref) and can be better tracked in the future. ####Considerations:#### - I chose the `blender::draw` namespace because `blender::gpu` already has private classes (i.e: `gpu::Texture`). - Theses are wrappers that manage a GPU object internally. They might be confused with actual `Texture`. However, the name `TextureWrapper` is a bit too much verbose in my opinion. I'm open to suggestion about better name. Reviewed By: jbakker Differential Revision: http://developer.blender.org/D13805 --- source/blender/draw/CMakeLists.txt | 1 + source/blender/draw/intern/DRW_gpu_wrapper.hh | 812 ++++++++++++++++++++++++++ 2 files changed, 813 insertions(+) create mode 100644 source/blender/draw/intern/DRW_gpu_wrapper.hh diff --git a/source/blender/draw/CMakeLists.txt b/source/blender/draw/CMakeLists.txt index 2a243eb9202..7b55981ba6b 100644 --- a/source/blender/draw/CMakeLists.txt +++ b/source/blender/draw/CMakeLists.txt @@ -197,6 +197,7 @@ set(SRC DRW_engine.h DRW_select_buffer.h + intern/DRW_gpu_wrapper.hh intern/DRW_render.h intern/draw_cache.h intern/draw_cache_extract.h diff --git a/source/blender/draw/intern/DRW_gpu_wrapper.hh b/source/blender/draw/intern/DRW_gpu_wrapper.hh new file mode 100644 index 00000000000..01c57406dac --- /dev/null +++ b/source/blender/draw/intern/DRW_gpu_wrapper.hh @@ -0,0 +1,812 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * Copyright 2022, Blender Foundation. + */ + +#pragma once + +/** \file + * \ingroup draw + * + * Wrapper classes that make it easier to use GPU objects in C++. + * + * All Buffers need to be sent to GPU memory before being used. This is done by using the + * `push_update()`. + * + * A Storage[Array]Buffer can hold much more data than a Uniform[Array]Buffer + * which can only holds 16KB of data. + * + * All types are not copyable and Buffers are not Movable. + * + * drw::UniformArrayBuffer + * Uniform buffer object containing an array of T with len elements. + * Data can be accessed using the [] operator. + * + * drw::UniformBuffer + * A uniform buffer object class inheriting from T. + * Data can be accessed just like a normal T object. + * + * drw::StorageArrayBuffer + * Storage buffer object containing an array of T with len elements. + * The item count can be changed after creation using `resize()`. + * However, this requires the invalidation of the whole buffer and + * discarding all data inside it. + * Data can be accessed using the [] operator. + * + * drw::StorageBuffer + * A storage buffer object class inheriting from T. + * Data can be accessed just like a normal T object. + * + * drw::Texture + * A simple wrapper to GPUTexture. A drw::Texture can be created without allocation. + * The `ensure_[1d|2d|3d|cube][_array]()` method is here to make sure the underlying texture + * will meet the requirements and create (or recreate) the GPUTexture if needed. + * + * drw::TextureFromPool + * A GPUTexture from the viewport texture pool. This texture can be shared with other engines + * and its content is undefined when aquiring it. + * A drw::TextureFromPool is acquired for rendering using `acquire()` and released once the + * rendering is done using `release()`. The same texture can be acquired & released multiple + * time in one draw loop. + * The `sync()` method *MUST* be called once during the cache populate (aka: Sync) phase. + * + * drw::Framebuffer + * Simple wrapper to GPUFramebuffer that can be moved. + * + */ + +#include "MEM_guardedalloc.h" + +#include "draw_texture_pool.h" + +#include "BLI_float4.hh" +#include "BLI_int2.hh" +#include "BLI_int3.hh" +#include "BLI_int4.hh" +#include "BLI_span.hh" +#include "BLI_utildefines.h" +#include "BLI_utility_mixins.hh" + +#include "GPU_framebuffer.h" +#include "GPU_texture.h" +#include "GPU_uniform_buffer.h" +#include "GPU_vertex_buffer.h" + +namespace blender::draw { + +/* -------------------------------------------------------------------- */ +/** \name Implementation Details + * \{ */ + +namespace detail { + +template< + /** Type of the values stored in this uniform buffer. */ + typename T, + /** The number of values that can be stored in this uniform buffer. */ + int64_t len, + /** True if the buffer only resides on GPU memory and cannot be accessed. */ + bool device_only> +class DataBuffer { + protected: + T *data_ = nullptr; + int64_t len_ = len; + + BLI_STATIC_ASSERT((sizeof(T) % 16) == 0, "Type need to be aligned to size of float4."); + + public: + /** + * Get the value at the given index. This invokes undefined behavior when the + * index is out of bounds. + */ + const T &operator[](int64_t index) const + { + BLI_STATIC_ASSERT(!device_only, ""); + BLI_assert(index >= 0); + BLI_assert(index < len); + return data_[index]; + } + + T &operator[](int64_t index) + { + BLI_STATIC_ASSERT(!device_only, ""); + BLI_assert(index >= 0); + BLI_assert(index < len); + return data_[index]; + } + + /** + * Get a pointer to the beginning of the array. + */ + const T *data() const + { + BLI_STATIC_ASSERT(!device_only, ""); + return data_; + } + T *data() + { + BLI_STATIC_ASSERT(!device_only, ""); + return data_; + } + + /** + * Iterator + */ + const T *begin() const + { + BLI_STATIC_ASSERT(!device_only, ""); + return data_; + } + const T *end() const + { + BLI_STATIC_ASSERT(!device_only, ""); + return data_ + len; + } + + T *begin() + { + BLI_STATIC_ASSERT(!device_only, ""); + return data_; + } + T *end() + { + BLI_STATIC_ASSERT(!device_only, ""); + return data_ + len; + } + + operator Span() const + { + BLI_STATIC_ASSERT(!device_only, ""); + return Span(data_, len); + } +}; + +template +class UniformCommon : public DataBuffer, NonMovable, NonCopyable { + protected: + GPUUniformBuf *ubo_; + +#ifdef DEBUG + const char *name_ = typeid(T).name(); +#else + constexpr static const char *name_ = "UniformBuffer"; +#endif + + public: + UniformCommon() + { + ubo_ = GPU_uniformbuf_create_ex(sizeof(T) * len, nullptr, name_); + } + + ~UniformCommon() + { + GPU_uniformbuf_free(ubo_); + } + + void push_update(void) + { + GPU_uniformbuf_update(ubo_, this->data_); + } + + /* To be able to use it with DRW_shgroup_*_ref(). */ + operator GPUUniformBuf *() const + { + return ubo_; + } + + /* To be able to use it with DRW_shgroup_*_ref(). */ + GPUUniformBuf **operator&() + { + return &ubo_; + } +}; + +template +class StorageCommon : public DataBuffer, NonMovable, NonCopyable { + protected: + /* Use vertex buffer for now. Until there is a complete GPUStorageBuf implementation. */ + GPUVertBuf *ssbo_; + +#ifdef DEBUG + const char *name_ = typeid(T).name(); +#else + constexpr static const char *name_ = "StorageBuffer"; +#endif + + public: + StorageCommon() + { + init(len); + } + + ~StorageCommon() + { + GPU_vertbuf_discard(ssbo_); + } + + void resize(int64_t new_size) + { + BLI_assert(new_size > 0); + if (new_size != this->len_) { + GPU_vertbuf_discard(ssbo_); + this->init(new_size); + } + } + + operator GPUVertBuf *() const + { + return ssbo_; + } + /* To be able to use it with DRW_shgroup_*_ref(). */ + GPUVertBuf **operator&() + { + return &ssbo_; + } + + private: + void init(int64_t new_size) + { + this->len_ = new_size; + + GPUVertFormat format = {0}; + GPU_vertformat_attr_add(&format, "dummy", GPU_COMP_F32, 1, GPU_FETCH_FLOAT); + + GPUUsageType usage = device_only ? GPU_USAGE_DEVICE_ONLY : GPU_USAGE_DYNAMIC; + ssbo_ = GPU_vertbuf_create_with_format_ex(&format, usage); + GPU_vertbuf_data_alloc(ssbo_, divide_ceil_u(sizeof(T) * this->len_, 4)); + if (!device_only) { + this->data_ = (T *)GPU_vertbuf_get_data(ssbo_); + GPU_vertbuf_use(ssbo_); + } + } +}; + +} // namespace detail + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Uniform Buffers + * \{ */ + +template< + /** Type of the values stored in this uniform buffer. */ + typename T, + /** The number of values that can be stored in this uniform buffer. */ + int64_t len + /** True if the buffer only resides on GPU memory and cannot be accessed. */ + /* TODO(fclem): Currently unsupported. */ + /* bool device_only = false */> +class UniformArrayBuffer : public detail::UniformCommon { + public: + UniformArrayBuffer() + { + /* TODO(fclem) We should map memory instead. */ + this->data_ = MEM_mallocN_aligned(this->name_); + } +}; + +template< + /** Type of the values stored in this uniform buffer. */ + typename T + /** True if the buffer only resides on GPU memory and cannot be accessed. */ + /* TODO(fclem): Currently unsupported. */ + /* bool device_only = false */> +class UniformBuffer : public T, public detail::UniformCommon { + public: + UniformBuffer() + { + /* TODO(fclem) How could we map this? */ + this->data_ = static_cast(this); + } + + UniformBuffer &operator=(const T &other) + { + *static_cast(this) = other; + return *this; + } +}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Storage Buffer + * \{ */ + +template< + /** Type of the values stored in this uniform buffer. */ + typename T, + /** The number of values that can be stored in this uniform buffer. */ + int64_t len, + /** True if created on device and no memory host memory is allocated. */ + bool device_only = false> +class StorageArrayBuffer : public detail::StorageCommon { + public: + void push_update(void) + { + BLI_assert(!device_only); + /* Get the data again to tag for update. The actual pointer should not + * change. */ + this->data_ = (T *)GPU_vertbuf_get_data(this->ssbo_); + GPU_vertbuf_use(this->ssbo_); + } +}; + +template< + /** Type of the values stored in this uniform buffer. */ + typename T, + /** True if created on device and no memory host memory is allocated. */ + bool device_only = false> +class StorageBuffer : public T, public detail::StorageCommon { + public: + void push_update(void) + { + BLI_assert(!device_only); + /* TODO(fclem): Avoid a full copy. */ + T &vert_data = *(T *)GPU_vertbuf_get_data(this->ssbo_); + vert_data = *this; + + GPU_vertbuf_use(this->ssbo_); + } + + StorageBuffer &operator=(const T &other) + { + *static_cast(this) = other; + return *this; + } +}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Texture + * \{ */ + +class Texture : NonCopyable { + protected: + GPUTexture *tx_ = nullptr; + const char *name_; + + public: + Texture(const char *name = "gpu::Texture") : name_(name) + { + } + + Texture(const char *name, + eGPUTextureFormat format, + int extent, + float *data = nullptr, + bool cubemap = false, + int mips = 1) + : name_(name) + { + tx_ = create(extent, 0, 0, mips, format, data, false, cubemap); + } + + Texture(const char *name, + eGPUTextureFormat format, + int extent, + int layers, + float *data = nullptr, + bool cubemap = false, + int mips = 1) + : name_(name) + { + tx_ = create(extent, layers, 0, mips, format, data, true, cubemap); + } + + Texture( + const char *name, eGPUTextureFormat format, int2 extent, float *data = nullptr, int mips = 1) + : name_(name) + { + tx_ = create(UNPACK2(extent), 0, mips, format, data, false, false); + } + + Texture(const char *name, + eGPUTextureFormat format, + int2 extent, + int layers, + float *data = nullptr, + int mips = 1) + : name_(name) + { + tx_ = create(UNPACK2(extent), layers, mips, format, data, true, false); + } + + Texture( + const char *name, eGPUTextureFormat format, int3 extent, float *data = nullptr, int mips = 1) + : name_(name) + { + tx_ = create(UNPACK3(extent), mips, format, data, false, false); + } + + ~Texture() + { + free(); + } + + /* To be able to use it with DRW_shgroup_uniform_texture(). */ + operator GPUTexture *() const + { + BLI_assert(tx_ != nullptr); + return tx_; + } + + /* To be able to use it with DRW_shgroup_uniform_texture_ref(). */ + GPUTexture **operator&() + { + return &tx_; + } + + Texture &operator=(Texture &&a) + { + if (*this != a) { + this->tx_ = a.tx_; + this->name_ = a.name_; + a.tx_ = nullptr; + } + return *this; + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_1d(eGPUTextureFormat format, int extent, float *data = nullptr, int mips = 1) + { + return ensure_impl(extent, 0, 0, mips, format, data, false, false); + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_1d_array( + eGPUTextureFormat format, int extent, int layers, float *data = nullptr, int mips = 1) + { + return ensure_impl(extent, layers, 0, mips, format, data, true, false); + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_2d(eGPUTextureFormat format, const int2 &extent, float *data = nullptr, int mips = 1) + { + return ensure_impl(UNPACK2(extent), 0, mips, format, data, false, false); + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_2d_array(eGPUTextureFormat format, + const int2 &extent, + int layers, + float *data = nullptr, + int mips = 1) + { + return ensure_impl(UNPACK2(extent), layers, mips, format, data, true, false); + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_3d(eGPUTextureFormat format, const int3 &extent, float *data = nullptr, int mips = 1) + { + return ensure_impl(UNPACK3(extent), mips, format, data, false, false); + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_cube(eGPUTextureFormat format, int extent, float *data = nullptr, int mips = 1) + { + return ensure_impl(extent, extent, 0, mips, format, data, false, true); + } + + /** + * Ensure the texture has the correct properties. Recreating it if needed. + * Return true if a texture has been created. + */ + bool ensure_cube_array( + eGPUTextureFormat format, int extent, int layers, float *data = nullptr, int mips = 1) + { + return ensure_impl(extent, extent, layers, mips, format, data, false, true); + } + + /** + * Returns true if the texture has been allocated or acquired from the pool. + */ + bool is_valid(void) const + { + return tx_ != nullptr; + } + + int width(void) const + { + return GPU_texture_width(tx_); + } + + int height(void) const + { + return GPU_texture_height(tx_); + } + + bool depth(void) const + { + return GPU_texture_depth(tx_); + } + + bool is_stencil(void) const + { + return GPU_texture_stencil(tx_); + } + + bool is_integer(void) const + { + return GPU_texture_integer(tx_); + } + + bool is_cube(void) const + { + return GPU_texture_cube(tx_); + } + + bool is_array(void) const + { + return GPU_texture_array(tx_); + } + + int3 size(int miplvl = 0) const + { + int3 size(0); + GPU_texture_get_mipmap_size(tx_, miplvl, size); + return size; + } + + /** + * Clear the entirety of the texture using one pixel worth of data. + */ + void clear(float4 values) + { + GPU_texture_clear(tx_, GPU_DATA_FLOAT, &values[0]); + } + + /** + * Clear the entirety of the texture using one pixel worth of data. + */ + void clear(uint4 values) + { + GPU_texture_clear(tx_, GPU_DATA_UINT, &values[0]); + } + + /** + * Clear the entirety of the texture using one pixel worth of data. + */ + void clear(uchar4 values) + { + GPU_texture_clear(tx_, GPU_DATA_UBYTE, &values[0]); + } + + /** + * Clear the entirety of the texture using one pixel worth of data. + */ + void clear(int4 values) + { + GPU_texture_clear(tx_, GPU_DATA_INT, &values[0]); + } + + /** + * Returns a buffer containing the texture data for the specified miplvl. + * The memory block needs to be manually freed by MEM_freeN(). + */ + template T *read(eGPUDataFormat format, int miplvl = 0) + { + return reinterpret_cast(GPU_texture_read(tx_, format, miplvl)); + } + + void filter_mode(bool do_filter) + { + GPU_texture_filter_mode(tx_, do_filter); + } + + /** + * Free the internal texture but not the drw::Texture itself. + */ + void free() + { + GPU_TEXTURE_FREE_SAFE(tx_); + } + + private: + bool ensure_impl(int w, + int h = 0, + int d = 0, + int mips = 1, + eGPUTextureFormat format = GPU_RGBA8, + float *data = nullptr, + bool layered = false, + bool cubemap = false) + + { + /* TODO(fclem) In the future, we need to check if mip_count did not change. + * For now it's ok as we always define all mip level.*/ + if (tx_) { + int3 size = this->size(); + if (size != int3(w, h, d) || GPU_texture_format(tx_) != format || + GPU_texture_cube(tx_) != cubemap || GPU_texture_array(tx_) != layered) { + GPU_TEXTURE_FREE_SAFE(tx_); + } + } + if (tx_ == nullptr) { + tx_ = create(w, h, d, mips, format, data, layered, cubemap); + if (mips > 1) { + /* TODO(fclem) Remove once we have immutable storage or when mips are + * generated on creation. */ + GPU_texture_generate_mipmap(tx_); + } + return true; + } + return false; + } + + GPUTexture *create(int w, + int h, + int d, + int mips, + eGPUTextureFormat format, + float *data, + bool layered, + bool cubemap) + { + if (h == 0) { + return GPU_texture_create_1d(name_, w, mips, format, data); + } + else if (d == 0) { + if (layered) { + return GPU_texture_create_1d_array(name_, w, h, mips, format, data); + } + else { + return GPU_texture_create_2d(name_, w, h, mips, format, data); + } + } + else if (cubemap) { + if (layered) { + return GPU_texture_create_cube_array(name_, w, d, mips, format, data); + } + else { + return GPU_texture_create_cube(name_, w, mips, format, data); + } + } + else { + if (layered) { + return GPU_texture_create_2d_array(name_, w, h, d, mips, format, data); + } + else { + return GPU_texture_create_3d(name_, w, h, d, mips, format, GPU_DATA_FLOAT, data); + } + } + } +}; + +class TextureFromPool : public Texture, NonMovable { + private: + GPUTexture *tx_tmp_saved_ = nullptr; + + public: + TextureFromPool(const char *name = "gpu::Texture") : Texture(name){}; + + /* Always use `release()` after rendering. */ + void acquire(int w, int h, eGPUTextureFormat format, void *owner_) + { + if (this->tx_ == nullptr) { + if (tx_tmp_saved_ != nullptr) { + this->tx_ = tx_tmp_saved_; + return; + } + DrawEngineType *owner = (DrawEngineType *)owner_; + this->tx_ = DRW_texture_pool_query_2d(w, h, format, owner); + } + } + + void release(void) + { + tx_tmp_saved_ = this->tx_; + this->tx_ = nullptr; + } + + /** + * Clears any reference. Workaround for pool texture not being able to release on demand. + * Needs to be called at during the sync phase. + */ + void sync(void) + { + tx_tmp_saved_ = nullptr; + } + + /** Remove methods that are forbidden with this type of textures. */ + bool ensure_1d(int, int, eGPUTextureFormat, float *) = delete; + bool ensure_1d_array(int, int, int, eGPUTextureFormat, float *) = delete; + bool ensure_2d(int, int, int, eGPUTextureFormat, float *) = delete; + bool ensure_2d_array(int, int, int, int, eGPUTextureFormat, float *) = delete; + bool ensure_3d(int, int, int, int, eGPUTextureFormat, float *) = delete; + bool ensure_cube(int, int, eGPUTextureFormat, float *) = delete; + bool ensure_cube_array(int, int, int, eGPUTextureFormat, float *) = delete; + void filter_mode(bool) = delete; + void free() = delete; + /** + * Forbid the use of DRW_shgroup_uniform_texture. + * Use DRW_shgroup_uniform_texture_ref instead. + */ + operator GPUTexture *() const = delete; +}; + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Framebuffer + * \{ */ + +class Framebuffer : NonCopyable { + private: + GPUFrameBuffer *fb_ = nullptr; + const char *name_; + + public: + Framebuffer() : name_(""){}; + Framebuffer(const char *name) : name_(name){}; + + ~Framebuffer() + { + GPU_FRAMEBUFFER_FREE_SAFE(fb_); + } + + void ensure(GPUAttachment depth = GPU_ATTACHMENT_NONE, + GPUAttachment color1 = GPU_ATTACHMENT_NONE, + GPUAttachment color2 = GPU_ATTACHMENT_NONE, + GPUAttachment color3 = GPU_ATTACHMENT_NONE, + GPUAttachment color4 = GPU_ATTACHMENT_NONE, + GPUAttachment color5 = GPU_ATTACHMENT_NONE, + GPUAttachment color6 = GPU_ATTACHMENT_NONE, + GPUAttachment color7 = GPU_ATTACHMENT_NONE, + GPUAttachment color8 = GPU_ATTACHMENT_NONE) + { + GPU_framebuffer_ensure_config( + &fb_, {depth, color1, color2, color3, color4, color5, color6, color7, color8}); + } + + Framebuffer &operator=(Framebuffer &&a) + { + if (*this != a) { + this->fb_ = a.fb_; + this->name_ = a.name_; + a.fb_ = nullptr; + } + return *this; + } + + operator GPUFrameBuffer *() const + { + return fb_; + } +}; + +/** \} */ + +} // namespace blender::draw -- cgit v1.2.3