Virtual Texture System — Streaming, Feedback & RVT虚拟纹理系统 — 流式传输、反馈与 RVT
Why Virtual Texturing?为什么需要虚拟纹理?
Modern open-world games use terrain and environmental textures at resolutions that far exceed GPU VRAM. A fully detailed 8km×8km landscape might require a 64K×64K texture — roughly 16 GB at 4 bytes/texel. Virtual Texturing (VT) solves this with the same insight as OS virtual memory: you only ever load and keep in VRAM the pages (tiles) that are actually visible on screen right now, streaming others in and out on demand.
现代开放世界游戏使用的地形和环境纹理分辨率远远超过 GPU VRAM 的容量。 一个完全精细的 8km×8km 地形可能需要 64K×64K 的纹理——以 4 字节/纹素计算约 16 GB。 虚拟纹理(VT)用与操作系统虚拟内存相同的思路解决这个问题: 你只需在 VRAM 中加载并保留当前屏幕上实际可见的页面(瓦片), 其他页面按需流入和流出。
Page Table Architecture — The Indirection Texture页表架构 — 间接纹理
The page table is the GPU-side indirection structure. It's implemented as a texture where each texel stores the location of a physical page in the pool. When a shader samples a virtual texture, it first reads the page table to find out where the data actually lives in physical memory.
页表是 GPU 端的间接结构。它被实现为一个纹理, 每个纹素存储物理池中一个物理页面的位置。当 Shader 采样虚拟纹理时, 它首先读取页表以找出数据实际存放在物理内存的哪个位置。
In UE5, a FVirtualTextureSpace owns the page table. Each virtual texture gets
assigned a region of the space. The page table itself is a small texture (e.g., 256×256 for a 128K virtual texture
with 512-texel pages), where each RGBA8 texel encodes:
在 UE5 中,FVirtualTextureSpace 拥有页表。每个虚拟纹理被分配到空间的某个区域。
页表本身是一个小纹理(例如,对于 128K 虚拟纹理配合 512 纹素页面,页表为 256×256),
每个 RGBA8 纹素编码:
- R, G: X and Y page coordinates in the physical page pool atlas
- R, G:物理页池图集中的 X 和 Y 页坐标
- B: The mip bias — which mip level of the physical page is stored here
- B:Mip 偏移——物理页中存储的是哪个 Mip 级别
- A: Flags (loaded / fallback mip)
- A:标志位(已加载 / 回退 Mip)
UE5 maintains multiple mip levels in the page table. When a page is not yet loaded, the shader falls back to a coarser mip level that is resident. This means you always see some texture data, never a black hole — just potentially lower resolution. This graceful degradation is one of VT's key advantages over a naive streaming approach.
UE5 在页表中维护多个 Mip 级别。当某页尚未加载时, Shader 回退到已驻留的更粗糙 Mip 级别。这意味着你总能看到纹理数据, 不会出现黑洞——只是可能分辨率较低。这种优雅降级是 VT 相对于朴素流式方案的关键优势之一。
// FVirtualTextureSpace — owns the page table and virtual address space // Engine/Source/Runtime/Renderer/Private/VT/VirtualTextureSpace.h class FVirtualTextureSpace { public: // The page table texture (indirection texture on GPU) FTextureRHIRef PageTableTexture; // Size of the virtual address space (in pages) // e.g., 4096x4096 pages × 128-texel page = 512K virtual texture uint32 Dimensions; // Width/height in pages uint32 NumPageTableLayers; // One layer per mip level // Allocate a virtual region for a new texture FVirtualTextureProducerHandle RegisterProducer(...); // Called each frame to flush page table updates to GPU void UpdatePageTable(FRDGBuilder& GraphBuilder, ...); };
Physical Page Pool — Fixed GPU Memory Budget物理页池 — 固定 GPU 内存预算
The physical page pool is the actual GPU texture atlas that stores loaded pages. It's a large 2D texture divided into a grid of page tiles (e.g., 128-texel or 256-texel squares). Every loaded virtual texture page lives somewhere in this atlas. The pool size is fixed, bounding VRAM usage.
物理页池是存储已加载页面的实际 GPU 纹理图集。 它是一个被划分为页面瓦片网格的大型 2D 纹理(例如 128 或 256 纹素的正方形)。 每个已加载的虚拟纹理页面都存储在这个图集的某个位置。池大小是固定的,限制了 VRAM 使用量。
| Property属性 | Typical Value典型值 | Description描述 |
|---|---|---|
| Page size | 128 or 256 texels | Square tile size, including a 4-texel border for bilinear filtering seams正方形瓦片大小,含 4 纹素边框用于双线性过滤接缝 |
| Pool dimensions | 2048×2048 ~ 4096×4096 | Atlas texture resolution — configurable via r.VT.PoolSizeScale图集纹理分辨率——通过 r.VT.PoolSizeScale 配置 |
| Layers | 1 per format | Separate pools for BC1, BC3, BC7, etc. — different compression formats每种格式单独的池——BC1、BC3、BC7 等不同压缩格式 |
| Eviction policy | LRU | Least-recently-used pages are evicted when the pool is full池满时驱逐最近最少使用的页面 |
| Border texels | 4 texels | Duplicate border prevents bilinear filter from sampling adjacent pages重复边框防止双线性过滤采样到相邻页面 |
// FVirtualTexturePhysicalSpace — manages the physical page pool atlas // Engine/Source/Runtime/Renderer/Private/VT/VirtualTexturePhysicalSpace.h class FVirtualTexturePhysicalSpace { public: // The actual GPU atlas texture FTextureRHIRef PooledRenderTarget; // Number of pages that fit in the pool uint32 NumTiles; // Allocate/free individual pages uint16 AcquirePage(uint32 vAddress); void ReleasePage(uint16 pAddress); // Get UV coordinates within the atlas for a given page FVector4f GetPageUVTransform(uint16 pAddress) const; };
GPU Feedback Buffer — "Which Pages Do I Need?"GPU 反馈缓冲区 — "我需要哪些页面?"
The feedback buffer is the core mechanism that allows the GPU to tell the CPU which virtual texture pages are needed this frame. Without it, the CPU would have no way to know which parts of a 64K texture are actually on screen.
反馈缓冲区是允许 GPU 告知 CPU 本帧需要哪些虚拟纹理页面的核心机制。 没有它,CPU 就没有办法知道 64K 纹理的哪些部分实际上在屏幕上。
The feedback buffer is rendered at 1/16th resolution (e.g., 120×67 for 1920×1080). During the base pass, each pixel writes its required virtual texture page ID into this buffer. The page ID encodes: VT space index, mip level, page X, and page Y — packed into 32 bits.
反馈缓冲区以1/16 分辨率渲染(例如 1920×1080 时为 120×67)。 在 Base Pass 期间,每个像素将其所需的虚拟纹理页面 ID 写入此缓冲区。 页面 ID 编码:VT 空间索引、Mip 级别、页面 X 和页面 Y——打包为 32 位。
// VirtualTextureFeedback.usf — shader side // Called inside the base pass material shader when sampling a VT: uint PackFeedback(uint SpaceIndex, uint MipLevel, uint2 PageAddress) { // Pack: [4 bits mip][8 bits space][10 bits pageX][10 bits pageY] return (MipLevel << 28) | (SpaceIndex << 20) | (PageAddress.x << 10) | PageAddress.y; } // The feedback buffer is an R32_UINT UAV // Shader writes: FeedbackBuffer[pixel / 4] = PackFeedback(...); // Only one write per 4x4 pixel block (stochastic sampling)
After rendering, UE5 reads the feedback buffer back to the CPU asynchronously (with 2 frames of latency to avoid GPU stalls). The CPU then parses all the page IDs to build a set of required pages. Pages that are required but not in the pool are queued for streaming.
渲染后,UE5 异步将反馈缓冲区读回 CPU(有 2 帧延迟以避免 GPU 停顿)。 CPU 然后解析所有页面 ID 以建立所需页面集合。所需但不在池中的页面被排队等待流式传输。
Streaming Pipeline — Async I/O to GPU Upload流式传输管线 — 异步 I/O 到 GPU 上传
When the CPU determines a page is needed, the FVirtualTextureProducer is responsible for
supplying the data. Producers are abstract: they might read from disk, decompress from a format like
BC1/BC7, or even render the data on the GPU (for Runtime VT). The pipeline is:
当 CPU 确定某页面被需要时,FVirtualTextureProducer 负责提供数据。
Producer 是抽象的:它们可能从磁盘读取、从 BC1/BC7 等格式解压,
甚至在 GPU 上渲染数据(用于 Runtime VT)。管线为:
UE5 limits how many new pages can be uploaded per frame (r.VT.MaxUploadPerFrame, default 8)
to bound the bandwidth spike. Pages that are frequently needed but not yet loaded will show a lower-resolution
mip fallback until the full page arrives.
UE5 限制每帧可以上传的新页面数量(r.VT.MaxUploadPerFrame,默认 8)以限制带宽峰值。
经常需要但尚未加载的页面将显示低分辨率 Mip 回退,直到完整页面到达。
Shader Integration — Sampling a Virtual TextureShader 集成 — 采样虚拟纹理
Sampling a virtual texture in HLSL is different from a regular texture2D sample. It requires an extra
indirection through the page table. UE5 wraps this in the TextureVirtualSample HLSL function
(defined in VirtualTextureCommon.ush), so Material Editor and HLSL both use the same path.
在 HLSL 中采样虚拟纹理与常规 Texture2D 采样不同。它需要通过页表进行额外的间接访问。
UE5 将这个过程封装在 TextureVirtualSample HLSL 函数中(定义于 VirtualTextureCommon.ush),
因此 Material Editor 和 HLSL 都使用相同的路径。
// VirtualTextureCommon.ush — simplified conceptual version float4 VirtualTextureSample( Texture2D PageTable, // The indirection texture Texture2D PhysicalPool, // The physical page atlas SamplerState Sampler, float2 UV, // Virtual UV [0,1] float4 VTPackedPageTableUniform) { // Step 1: calculate which mip level we need float MipLevel = CalcMipLevel(UV, VirtualResolution); // Step 2: calculate the page address (which tile contains this UV?) float2 PageUV = UV * VirtualPageCount; // in page-space float2 PageCoord = floor(PageUV); // Step 3: look up the page table (indirection) // Result: tells us WHERE in the physical atlas this page lives float4 PageTableEntry = PageTable.SampleLevel(Sampler, PageCoord / PageTableSize, MipLevel); float2 PhysicalPageOffset = DecodePageTableEntry(PageTableEntry); // Step 4: compute physical UV within the pool atlas float2 IntraPageUV = frac(PageUV); // position within the page float2 PhysicalUV = (PhysicalPageOffset + IntraPageUV * PageSizeTexels) / PoolAtlasSize; // Step 5: sample the actual data from the physical pool return PhysicalPool.Sample(Sampler, PhysicalUV); } // Also: while sampling, write feedback // FeedbackBuffer[pixel/4] = PackFeedback(SpaceID, MipLevel, PageCoord);
Runtime Virtual Texture (RVT) — Render to VT运行时虚拟纹理(RVT)— 渲染至 VT
Runtime Virtual Texture is a VT variant where pages are not loaded from disk but rendered on demand by the GPU. This is used primarily for landscape blending: the landscape material can be very complex (blending 8+ layers), but the result is baked into VT pages that terrain meshes can sample cheaply as a single texture lookup.
运行时虚拟纹理(RVT)是一种 VT 变体,页面不从磁盘加载, 而是由 GPU 按需渲染。这主要用于地形混合: 地形材质可以非常复杂(混合 8+ 个层级),但结果被烘焙到 VT 页面中, 地形网格可以作为单次纹理查找廉价采样。
RVT pages are rendered by FRuntimeVirtualTextureRender. When a page is needed, a
scene capture renders the landscape/terrain into a staging render target at the appropriate resolution,
then uploads the result into the physical pool. Pages can be marked as static (never re-render)
or dynamic (re-render when the scene changes).
RVT 页面由 FRuntimeVirtualTextureRender 渲染。需要页面时,
场景捕获以适当分辨率将地形渲染到暂存渲染目标,然后将结果上传到物理池。
页面可以标记为静态(从不重新渲染)或动态(场景变化时重新渲染)。
// Setting up RVT in a Landscape in Blueprints / C++: // 1. Place a RuntimeVirtualTextureVolume actor in the level // 2. Create a VirtualTexture asset (content browser) with type "Runtime" // 3. Assign the VT asset to the Landscape's RuntimeVirtualTextures array // 4. In the Landscape material: use "Runtime Virtual Texture Sample" node // to read from the VT instead of sampling all layers directly // C++ setup: URuntimeVirtualTexture* RVT = NewObject<URuntimeVirtualTexture>(); RVT->SetSize(ETextureSize::TS_4096); // 4096×4096 virtual resolution RVT->SetTileSize(ETileSize::TS_128); // 128-texel physical pages RVT->SetTileBorderSize(4); // 4-texel border RVT->SetMipLevels(9); // log2(4096/128) + 1 // Console: stat VirtualTextureMemory — shows pool usage
Adaptive Virtual Texture — Per-Object VT自适应虚拟纹理 — 逐对象 VT
Adaptive Virtual Texture (AVT) extends VT to work on individual static mesh objects, not just landscapes. Each object can have its own VT space, and the page size adapts based on the object's on-screen pixel density. This is useful for hero assets that need more texture detail than standard streaming can provide without consuming disproportionate VRAM.
自适应虚拟纹理(AVT)将 VT 扩展到单个静态网格对象,而不仅仅是地形。 每个对象可以有自己的 VT 空间,页面大小根据对象的屏幕像素密度自适应。 这对于需要比标准流式传输能提供更多纹理细节的英雄资产非常有用,同时不消耗不成比例的 VRAM。
Usage & Setup Guide使用与配置指南
Enabling VT for a Texture为纹理启用 VT
In the Texture Editor (or bulk-edit): check "Virtual Texture Streaming" in the Texture Compression settings. This makes the texture stream via VT. Then enable r.VirtualTextures=1 (project-wide via Project Settings → Rendering → Virtual Textures).
在纹理编辑器(或批量编辑)中:在纹理压缩设置中勾选"Virtual Texture Streaming"。这使纹理通过 VT 流式传输。然后启用 r.VirtualTextures=1(通过"项目设置 → 渲染 → 虚拟纹理"在项目范围内设置)。
Setting Up RVT for Landscape为地形设置 RVT
- In Project Settings → Rendering, enable Virtual Texture Support and Enable virtual texture support in materials.
- 在"项目设置 → 渲染"中,启用"虚拟纹理支持"和"在材质中启用虚拟纹理支持"。
- Place a RuntimeVirtualTextureVolume actor in your level. Set its bounds to cover the landscape.
- 在关卡中放置 RuntimeVirtualTextureVolume Actor,设置其边界以覆盖地形。
- Create a VirtualTexture asset in Content Browser. Configure: type = Color+Normal, page size = 128–256, mip levels = auto.
- 在内容浏览器中创建 VirtualTexture 资产。配置:类型 = 颜色+法线,页面大小 = 128–256,Mip 级别 = 自动。
- In the Landscape actor, under Virtual Textures, add the VT asset. Enable Draw in Virtual Textures.
- 在地形 Actor 的虚拟纹理下,添加 VT 资产。启用"在虚拟纹理中绘制"。
- In the Landscape Material: add a Runtime Virtual Texture Sample node. Set it to read BaseColor, Normal, Roughness from the RVT.
- 在地形材质中:添加 Runtime Virtual Texture Sample 节点。设置为从 RVT 读取 BaseColor、Normal、Roughness。
- Create a second material (the "write" material) with Runtime Virtual Texture Output node — this renders into the VT pages.
- 创建第二个材质("写入"材质),包含 Runtime Virtual Texture Output 节点——它渲染到 VT 页面中。
Debugging Virtual Textures调试虚拟纹理
-- Console commands for VT debugging -- r.VT.Flush -- Evict all VT pages (forces re-stream) r.VT.Borders=1 -- Visualize page borders in the viewport r.VT.Verbose=1 -- Log detailed page streaming events stat VirtualTextureMemory -- Pool memory usage statistics stat VirtualTexturePerf -- Streaming performance counters -- Viewport Show menu → Virtual Texture → Feedback shows feedback buffer -- Viewport Show menu → Virtual Texture → Page Table shows page occupancy r.VT.PoolSizeScale=1.0 -- Scale physical pool size (0.5 = half, saves VRAM) r.VT.MaxUploadPerFrame=8 -- Max pages uploaded per frame r.VT.NumFeedbackSamples=4 -- Feedback buffer density (higher = more responsive)
Performance Tuning性能调优
| CVar | Default默认 | Effect效果 |
|---|---|---|
| r.VirtualTextures | 0 | Master enable (must enable in Project Settings UI)主开关(须在项目设置 UI 中启用) |
| r.VT.PoolSizeScale | 1.0 | Physical pool size multiplier. Reduce to 0.5 on low-VRAM devices.物理池大小乘数。在低显存设备上减到 0.5。 |
| r.VT.MaxUploadPerFrame | 8 | Limits bandwidth spike from page uploads. Increase for faster streaming.限制页面上传的带宽峰值。增大可加快流式传输速度。 |
| r.VT.FeedbackFactor | 16 | Feedback resolution divisor (16 = 1/16 screen res). Lower = more accurate, more overhead.反馈分辨率除数(16 = 1/16 屏幕分辨率)。越小越精确,开销越大。 |
| r.VT.PageTableExtraLODBias | 0 | Extra mip bias for page table lookups. Positive values use coarser (cheaper) mips.页表查找的额外 Mip 偏差。正值使用更粗糙(更便宜)的 Mip。 |
| r.RVT.TileCountBias | 0 | Increase RVT tile count for more detail on high-end hardware.为高端硬件增加 RVT 瓦片数以获取更多细节。 |
r.VT.PoolSizeScale or reduce the number of unique VT textures in view. Check stat VirtualTextureMemory for pool occupancy.
常见问题 — VT 闪烁:如果看到纹理弹出或闪烁,物理池可能已满且页面被过于积极地驱逐。增大 r.VT.PoolSizeScale 或减少视野中唯一 VT 纹理的数量。通过 stat VirtualTextureMemory 检查池占用情况。
Source Navigation源码导航
All paths relative to Engine/Source/Runtime/Renderer/Private/VT/
所有路径相对于 Engine/Source/Runtime/Renderer/Private/VT/
- VirtualTextureSystem.h/.cppMaster manager: Update(), feedback analysis, page request dispatch主管理器:Update()、反馈分析、页面请求分发
- VirtualTextureSpace.h/.cppVirtual address space, page table texture management虚拟地址空间、页表纹理管理
- VirtualTexturePhysicalSpace.h/.cppPhysical page pool atlas, LRU eviction, page allocation物理页池图集、LRU 驱逐、页面分配
- VirtualTextureProducer.h/.cppAbstract producer interface — disk, RVT, procedural抽象 Producer 接口——磁盘、RVT、程序化
- VirtualTextureFeedback.h/.cppFeedback buffer setup, GPU→CPU readback, page set analysis反馈缓冲区设置、GPU→CPU 回读、页面集分析
- RuntimeVirtualTextureRender.cppRVT page rendering: scene capture, GPU-rendered pagesRVT 页面渲染:场景捕获、GPU 渲染页面
- AdaptiveVirtualTexture.h/.cppPer-object VT support, density-adaptive page size逐对象 VT 支持,密度自适应页面大小
- AllocatedVirtualTexture.h/.cppTracks an allocated region within a VT space追踪 VT 空间内的已分配区域
- ../MaterialCache/MaterialCacheVirtualTexture.cppMaterial cache integration with VT system材质缓存与 VT 系统的集成