OpenGL 纹理贴图:加载和卸载

OpenGL Texture Mipmaps: Load and Unload

完美情况下,如果屏幕分辨率为1024x768,仅 给定时刻需要 786432 个纹素和 2 兆字节的内存 足够。但在现实世界中存在管理成本,所以纹理 占用更多内存。

纹理流可以降低纹理的内存消耗,即 也就是说,在给定的时刻,并不是所有的纹理贴图都需要。 纹理需要 0 级 mipmap 因为它靠近相机,如果 它离当前的相机很远,可能是 5 到 11 级的 mipmap 足够。 Camera在场景中移动一会儿,一些mipmaps可以 加载和一些 mipmaps 可以卸载。

我的问题是如何有效地做到这一点。

假设我在场景中有一个 512x512 的 OpenGL 纹理,那么它将有 10 个贴图。从0级到9级分别有:512x512、256x256、 128x128...和 ​​1x1 mipmap。像这样简单地上传数据:

glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 512, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, p1);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, p2);
glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, p3);
...
glBindTexture(GL_TEXTURE_2D, 0);

过了一会儿,相机远离场景中的这个纹理, 64x64 mipmap 就足够了,所以前 3 个 mipmap 将是 已卸载:

glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, p4);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 32, 32, 0, GL_RGBA, GL_UNSIGNED_BYTE, p5);
glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, p6);
...
glBindTexture(GL_TEXTURE_2D, 0);

然后,相机向这个纹理移动,256x256 mipmap是 需要:

glBindTexture(GL_TEXTURE_2D, texId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 256, 256, 0, GL_RGBA, GL_UNSIGNED_BYTE, p4);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 128, 128, 0, GL_RGBA, GL_UNSIGNED_BYTE, p5);
glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, p6);
...
glBindTexture(GL_TEXTURE_2D, 0);

这很低效。基本上它重现了纹理 每次虽然纹理 id 没有改变。 PBO 可以使 它更快,但数据复制仍然是成本。

对于 1024x1024 的 OpenGL 纹理,我可以让它只使用较低的 mipmaps,比如级别 1 到 9,并将级别 0 mipmap 留空(做 不在视频内存中分配)?换句话说:始终保持 从低级别到高级的 mipmap 子集。加载或卸载 更高级别的 mipmap,而不更改 a 的较低级别的 mipmap 质地。我认为从硬件的角度来看,这可能是可能的。

这是我尝试过的:如果我不为级别 0 调用 glTexImage2D mipmap,此纹理可能处于不完整状态。但如果我打电话 带有空数据指针的 glTexImage2D,它将分配在 数据为零的视频内存(通过 gDEBugger 分析)。

so textures take much more memory.

实际上没有。 (编辑)对于一维纹理 第零个 mipmap 级别消耗 N 个字节,第一个 N/2,第二个 N/4,依此类推。所以消耗的总字节数是

sum(i in 0…){ N * 2^-i } = N * sum(i in 0…){2^-i}

这是一个收敛到 2 的几何级数。因此,经过 mipmap 处理的纹理消耗的内存恰好是未经过 mipmap 处理的纹理的两倍。对于 2D 纹理,它是 1/4、1/16 等等。纹理的尺寸越大,mipmap 开销越小。

那不是 "a lot"。

A texture needs level 0 mipmap because it's near the camera, if it's far from the current camera, level 5 to 11 mipmaps may be enough.

OpenGL 中没有相机,这不是确定 mipmaping 级别的方式。所使用的Mipmap级别由纹理坐标在屏幕坐标中的变化率(纹理坐标的梯度)决定。

After a while, camera goes far from this texture in the scene, 64x64 mipmap is enough

也许您在使用浅纹理坐标梯度绘制后立即使用相同的纹理在具有非常陡峭的纹理坐标梯度(低 mipmap 级别)的基元上进行渲染。

But in real world there is managing cost,

我想说的是,尝试正确流式传输 mipmap 的管理开销要高得多,并且会消耗更多的 GPU 资源,而不是简单地加载所有 mipmap 并收工。

现代 GPU 也能够自行获取数据;图形 RAM 只是系统 RAM 的缓存,当您加载纹理时,它实际上并没有首先进入图形内存。 GPU 将获取它需要的数据。

如果您的目标是限制可用的活动纹理内存量,那么 OpenGL 4.5 中的任何内容都无法帮助您。将纹理重新分配为较小的纹理是一个糟糕的想法(不,Unreal engine 不会 那样做)。

在两种情况下,您所说的可能很重要。情况 1 将是 Unreal engine 使用它的目的:加载性能。它分配整个纹理,它的所有 mipmap,但它只首先 加载 较低的 mipmap。这可以节省加载关卡的时间。这也可以通过做同样的事情来加速流媒体性能。

这在 OpenGL 4.5 中很容易完成。这就是 mipmap 范围设置的目的;您只加载较低的 mipmap 并将 GL_TEXTURE_BASE_LEVELGL_TEXTURE_MAX_LEVEL 设置为该范围。 OpenGL 保证它不会尝试访问超出该范围的内存。

但是从内存中动态逐出 mipmap 并不是 OpenGL 4.5 的机制。

ARB_sparse_texture 但是 确实 。它允许您声明某些 mipmap 是 "sparse"。您可以像往常一样为它们分配存储空间,但您可以声明更高级别的内存可能会被逐出。您可以通过为它们提供虚拟页面来决定何时可用级别。您可以删除这些页面,这样 GPU 就可以将该内存用于其他人。

理论上,这样做的目标是能够使用更多纹理,而不会 运行 GPU 内存不足(因此会出现抖动)。但是,您实际上并没有能够有效执行此操作的工具。

这是因为 OpenGL 不会告诉您有多少可用内存。它也没有说明为 buffers/textures 分配了多少内存。它也没有说明分配的 buffers/textures 中有多少当前处于活动状态并驻留在 GPU 上。所以你真的不知道什么时候你就要崩溃了。

这意味着,如果您 "close to" 有许多大纹理,您仍然可以突破内存限制。并且 OpenGL 不会在您这样做时告诉您。

即便如此,如果您围绕它来规划关卡布局,这仍然会有所帮助。哦,稀疏纹理对 Unreal engine 的情况也很有用。


I am thinking about that every mipmap of an OpenGL texture is stored independently, we can attach a lower level mipmap to a texture at runtime, why cannot attach a higher one.

不要误以为硬件遵循 API 所说的。

您能否只对第 0 层以外的 mipmap 发出 glTexImage*D 调用?是的;只需使用 base/max 级别来防止超出分配范围的访问(这将保持纹理完整)。

是否保证该实现只为那些特定的 mipmap 级别分配内存?不;实现可能会分配超出该范围的 mipmap 级别。

的确,有证据表明它不是那样工作的。考虑 ARB_texture_storage。这个扩展是 OpenGL 4.3+ 的核心,它提供了一个单一的函数,可以一次分配纹理的所有 mipmap 级别。因此,您不必为每个级别调用 glTexImage2D,而是调用一次 glTexStorage2D,它将根据您指定的大小分配所有指定的 mipmap 级别。你可以在小范围内留下一些,但不能在顶部留下一些。

这也使纹理不可变,因此您不能再次更改该纹理的存储。你可以上传到它,但你不能在它上面调用 glTexStorage*DglTexImage*D。所以没有重新分配。

为什么 ARB 会创建一个扩展,其全部目的是防止您分配单个 mipmap,如果硬件实际支持单个分配?如果您认为这是侥幸,也请考虑一下。

创建ARB_direct_state_access时,他们显然添加了DSA风格的纹理分配函数。但是请注意,他们没有添加 DSA 风格的函数来制作非不可变纹理;他们没有添加 glTextureImage*D。他们的推理? "Immutable texture is a more robust approach to handle textures".

很明显,ARB 认为 API 中没有任何值可以让用户说出哪些 mipmap 已分配,哪些未分配。

而且应该注意的是,Direct3D 12 中的任何东西,一个低得多的级别 API,都不允许这样做。 how much memory a texture needs is purely a byte-count + alignment. It is not a series of byte counts, one-per-mipmap. And the functions to allocate resources 问题的答案同样不允许扩展 mipmap 或类似的东西。

我什至还查看了 Mantle 的文档。它无法使图像的内存不连续。 grBindObjectMemory(将内存存储与纹理相关联的函数)不采用指定 mipmap 级别的参数。

为了完整起见,Vulkan 也没有。它将图像 mipmap 金字塔视为单个连续的存储块。稀疏图像可以工作,但在页面边界上工作,而不是在整个 mipmap 级别。

所以我不会假设基于旧 OpenGL APIs 的硬件的性质。