SoftGLRender源码:Buffer系列类

简介

SoftGLRender针对图像存储,设计了一系列图像缓冲区(Buffer)模板类,以支持不同的内存布局(线性、分块、Morton曲线),主要用于图形渲染或图像处理中高效存储和访问像素数据.

使用模板类,能支持任意像素类型T(如uint8_tfloat,自定义结构).

三种内存布局:

  • 线性布局(Layout_Linear):像素按行优先连续存储(data[x + y * width])。

  • 分块布局(Layout_Tiled):将图像划分为 4x4 的小块,提升局部性(适合 GPU 纹理缓存)。

  • Morton布局(Layout_Morton):使用 Z 阶曲线(Morton Code)存储,优化空间局部性(减少缓存失效)。

对应下面3个枚举值:

enum BufferLayout {
	Layout_Linear,
	Layout_Tiled,
	Layout_Morton,
};

类图关系

img

基类Buffer<T>

线性布局的Buffer,适用于绝大多数需要存储图像的情况.

数据成员

  • width_, height_:图像逻辑尺寸

  • innerWidth_, innerHeight_:实际存储尺寸(可能因对齐或分块大于逻辑尺寸)

  • data_:像素数据(通过 std::shared_ptr 管理内存)

// linear layout buffer
template<typename T>
class Buffer {
    ...
protected:
	size_t width_            = 0; // 逻辑宽度
	size_t height_           = 0; // 逻辑高度
	size_t innerWidth_       = 0; // 实际宽度
	size_t innerHeight_      = 0; // 实际高度
	std::shared_ptr<T> data_ = nullptr; // 像素数据
	size_t dataSize_         = 0; // 实际占用bytes, 值为innerWidth_ * innerHeight_;
}

核心方法

  • create():分配内存并初始化布局

  • convertIndex():将 (x,y) 坐标转换为内存索引(虚函数,子类可重写)

  • get()/set():安全访问像素

  • copyRawDataTo():复制数据到外部缓冲区(可选垂直翻转)

分配内存

create()分配内存并初始化布局. 用户需要提供图像宽度、高度,存放像素数据的线性数组(可选).

默认的布局,不需要padding,即图像的实际尺寸 = 逻辑尺寸.

public:
	virtual void initLayout() {
		innerWidth_  = width_;
		innerHeight_ = height_;
	}

	void create(size_t w, size_t h, const uint8_t* data = nullptr) {
		if (w > 0 && h > 0) { // check input
			if (width_ == w && height_ == h) { // same size
				return;
			}
			width_  = w;
			height_ = h;

			initLayout();
			dataSize_ = innerWidth_ * innerHeight_;
            // 申请像素存储空间
			data_ = MemoryUtils::makeBuffer<T>(dataSize_, data);
		}
	}

销毁内存

销毁由create申请的资源,需要:

  1. 清空数据成员;
  2. 设置像素数据指针data_为nullptr,会自动释放内存.
public:
	virtual void destroy() {
		width_       = 0;
		height_      = 0;
		innerWidth_  = 0;
		innerHeight_ = 0;
		dataSize_    = 0;
		data_        = nullptr;
	}

坐标转换

实际存储像素数据(data_[]),是一块一维线性空间,而图像是原点在左上角的二维空间. 访问图像使用的是(x,y)坐标,如何转换为一维线性空间的索引呢?

img

图像在线性空间中,是逐行存储的. 于是,索引对应关系:
image index(x,y) <=> array index [i]

	// convert image index (x, y) to vector index [i]
	virtual inline size_t convertIndex(size_t x, size_t y) const {
		return x + y * innerWidth_;
	}

访问指定位置像素

通过convertIndex实现快捷、方便、安全的访问指定位置的图像像素数据.

	inline T* get(size_t x, size_t y) {
		T* ptr = data_.get();
		if (ptr != nullptr && x < width_ && y < height_) {
			return &ptr[convertIndex(x, y)];
		}
		return nullptr;
	}

	inline void set(size_t x, size_t y, const T& pixel) {
		T* ptr = data_.get();
		if (ptr != nullptr && x < width_ && y < height_) {
			ptr[convertIndex(x, y)] = pixel;
		}
	}

getter/setter

这部分很简单,不细述.

思考:为什么只能读data_width_height_这些数据,而不能写?

答:因为图像的尺寸涉及底层像素数据的存储,不能随意更改位置、大小,各项属性必须匹配;否则,可能造成异常问题.

public:
	inline T* getRawDataPtr() const {
		return data_.get();
	}

	inline size_t getRawDataSize() const {
		return dataSize_;
	}

	inline size_t getRawDataBytesSize() const {
		return dataSize_ * sizeof(T);
	}

	inline size_t getWidth() const {
		return width_;
	}

	inline size_t getHeight() const {
		return height_;
	}

emtpy()判空

empty()判断图像是否为空.

思考:为什么用data_==nullptr判断图像是否为空,而不是width_=height_=0

答:理论上,是一样的. 因为只有构造图像尺寸(宽、高>0)才会分配data_对应的缓存空间.

public:
	inline bool empty() const {
		return data_ == nullptr;
	}

拷贝出像素数据

copyRawDataTodata_[]存放的所有像素数据拷贝到用户指定的out缓存. 垂直翻转flip_y可选.

public:
	void copyRawDataTo(T* out, bool flip_y = false) const {
		T* ptr = data_.get();
		if (ptr != nullptr) {
			if (!flip_y) {
				memcpy(out, ptr, dataSize_ * sizeof(T));
			}
			else {
				for (int i = 0; i < innerHeight_; i++) {
					memcpy(out + innerWidth_ * i, ptr + innerWidth_ * (innerHeight_ - 1 - i),
						innerWidth_ * sizeof(T));
				}
			}
		}
	}

清除像素数据

clear()清除data_[]存放的所有像素数据,但不释放存储空间.

	inline void clear() const {
		T* ptr = data_.get();
		if (ptr != nullptr) {
			memset(ptr, 0, dataSize_ * sizeof(T));
		}
	}

设置像素数据

setAll(val)设置data_[]中所有像素为指定值.

	inline void setAll(T val) const {
		T* ptr = data_.get();
		if (ptr != nullptr) {
			for (int i = 0; i < dataSize_; i++) {
				ptr[i] = val;
			}
		}
	}

工厂方法

Buffer<T>提供了2个static 工厂方法,用于创建std::shared_ptr包裹的Buffer<T>或派生类对象:

  • makeDefault():根据宏(SOFTGL_TEXTURE_TILED/MORTON)选择布局,默认布局为Layout_Linear.
  • makeLayout():由用户传入参数,动态指定布局
// 工厂方法

// 根据宏静态选择创建不同布局的Buffer
template<typename T>
inline std::shared_ptr<Buffer<T>> Buffer<T>::makeDefault(size_t w, size_t h) {
	std::shared_ptr<Buffer<T>> ret = nullptr;
#if SOFTGL_TEXTURE_TILED
	ret = std::make_shared<TiledBuffer<T>>();
#elif SOFTGL_TEXTURE_MORTON
	ret = std::make_shared<MortonBuffer<T>>();
#else
	ret = std::make_shared<Buffer<T>>();
#endif
	ret->create(w, h);
	return ret;
}

// 根据参数动态选择创建不同布局的Buffer
template<typename T>
inline std::shared_ptr<Buffer<T>> Buffer<T>::makeLayout(size_t w, size_t h, BufferLayout layout) {
	std::shared_ptr<Buffer<T>> ret = nullptr;
	switch (layout) {
	case Layout_Tiled: {
		ret = std::make_shared<TiledBuffer<T>>();
		break;
	}
	case Layout_Morton: {
		ret = std::make_shared<MortonBuffer<T>>();
		break;
	}
	case Layout_Linear: 
	default: {
		ret = std::make_shared<Buffer<T>>();
	}
	}
	ret->create(w, h);
	return ret;
}

e.g. 当我们想创建一个长宽为w, h的图像缓冲区时,可以直接这样创建std::shared_ptr<Buffer<RGBA>>

auto buffer = Buffer<RGBA>::makeDefault(w, h);

派生类TiledBuffer<T>

TiledBuffer<T> 是分块布局的Buffer.

特点:

  • 将图像按 tileSize x tileSize(默认 4x4)划分成小块
  • 每块 tile 连续存储,提高局部访问效率
  • 用位运算加速 (x,y) 到线性地址的映射

分块布局优势:适合GPU并行处理(特别是移动GPU),如纹理采样,从而提高渲染效率.

TiledBuffer<T>存放像素数据的底层结构,还是其基类Buffert<T>,不同的是TiledBuffer<T>按4x4大小对图像进行分块.

分块布局的逻辑布局

img

// tiled layout buffer
template<typename T>
class TiledBuffer : public Buffer<T> {
public:

	void initLayout() override {
		tileWidth_  = (this->width_ + tileSize_ - 1) / tileSize_;
		tileHeight_ = (this->height_ + tileSize_ - 1) / tileSize_;
		this->innerWidth_  = tileWidth_ * tileSize_;
		this->innerHeight_ = tileHeight_ * tileSize_;
	}

	BufferLayout getLayout() const override {
		return Layout_Tiled;
	}

private:
	const static int tileSize_ = 4; // 4 x 4
	const static int bits_     = 2; // tileSize_ = 2^bits_
	size_t tileWidth_          = 0; // 分块宽度, 即水平分块数
	size_t tileHeight_         = 0; // 分块高度, 即垂直分块数
};

坐标变换

TiledBuffer<T>tileSize_ x tileSize_大小对图像进行分块,因此,将图像像素(x,y)转换为线性坐标时,可以先考虑分块的坐标,然后再考虑分块内的坐标.

img

public:
	inline size_t convertIndex(size_t x, size_t y) const override {
		uint16_t tileX = x >> bits_; // 分块X编号, x / tileSize_
		uint16_t tileY = y >> bits_; // 分块Y编号, y / tileSize_
		uint16_t inTileX = x & (tileSize_ - 1); // 分块内x值, x % tileSize_
		uint16_t inTileY = y & (tileSize_ - 1); // 分块内y值, y % tileSize_
        
		return ((tileY * tileWidth_ + tileX) << bits_ << bits_) + (inTileY << bits_) + inTileX;
	}

tips: 用位运算是为了加速运算.

派生类MortonBuffer<T>

由于项目未用到,暂时略.

posted @ 2025-05-18 16:54  明明1109  阅读(60)  评论(0)    收藏  举报