-
Notifications
You must be signed in to change notification settings - Fork 2
Virtual Texturing
SparkEngine includes a virtual texturing system that keeps only the needed texture pages resident in a GPU page cache. A shader-driven feedback buffer reports which pages are visible each frame, and the CPU-side manager loads requested pages and evicts stale ones using LRU.
Source: SparkEngine/Source/Graphics/VirtualTexture.h
Namespace: Spark::Graphics
- Overview
- Page Cache Architecture
- Feedback Loop
- Configuration
- API Reference
- Usage Example
- Integration
- See Also
Traditional texture streaming loads entire mip levels of each texture. Virtual texturing instead divides textures into tiles (pages) and loads only the tiles visible on screen, dramatically reducing VRAM usage for scenes with many large textures.
┌───────────────────────────────────────────────────────────┐
│ GPU Rendering │
│ │
│ Pixel shader writes to feedback buffer: │
│ "I need texture 5, mip 3 at tile (2, 1)" │
│ │
├───────────────────────────────────────────────────────────┤
│ CPU Readback │
│ │
│ ProcessFeedback(): │
│ - Parse feedback entries │
│ - Mark needed pages as used (update LRU) │
│ - Queue missing pages for async loading │
│ - Evict stale pages when cache is full │
│ │
├───────────────────────────────────────────────────────────┤
│ Page Cache │
│ │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │ T1 │ T3 │ T5 │ T5 │ T2 │free │ Resident pages │
│ │ M0 │ M2 │ M3 │ M1 │ M0 │ │ │
│ │(0,0)│(1,0)│(2,1)│(0,0)│(3,2)│ │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┘ │
│ Max 1024 pages (configurable) │
└───────────────────────────────────────────────────────────┘
Each page is one tile of a specific mip level of a source texture:
struct VirtualTexturePage
{
uint32_t textureId; // Source texture identifier
uint32_t mipLevel; // Mip level this page belongs to
uint32_t tileX; // Tile column within the mip level
uint32_t tileY; // Tile row within the mip level
bool resident; // True if loaded in the cache
uint32_t lastUsedFrame; // LRU tracking
};Shaders write what they need into the feedback buffer:
struct FeedbackEntry
{
uint32_t textureId; // Texture that was sampled
uint32_t requiredMip; // Mip level needed for this pixel
};Each frame follows this cycle:
-
Render: Pixel shaders sample virtual textures and write
FeedbackEntryrecords to a GPU feedback buffer - Readback: The feedback buffer is read back to CPU
-
ProcessFeedback: The manager analyzes entries:
- Pages already resident get their
lastUsedFrameupdated - Missing pages are queued for async loading
- When the cache is full, the least-recently-used pages are evicted
- Pages already resident get their
- Upload: Newly loaded page data is uploaded to the GPU page cache texture
- Update indirection: The indirection table (maps virtual page → physical cache slot) is updated
| Parameter | Default | Description |
|---|---|---|
pageCacheSize |
1024 | Maximum resident pages |
tileSize |
128 | Pixels per tile edge |
framesToKeep |
60 | Frames a page stays resident without use |
At default settings: 1024 pages x 128x128 pixels x 4 bytes (RGBA8) = 64 MB page cache.
| Method | Description |
|---|---|
Initialize(pageCacheSize, tileSize) |
Set up page cache |
Shutdown() |
Release all resources |
ProcessFeedback(feedback, currentFrame) |
Analyze GPU feedback, schedule loads/evictions |
RequestPage(textureId, mip, tileX, tileY) |
Manually request a specific page |
EvictStalePages(currentFrame, framesToKeep) |
Remove unused pages (returns eviction count) |
GetResidentPageCount() |
Pages currently in cache |
GetPendingPageCount() |
Pages queued for loading |
Console_GetStatus() |
Formatted debug string |
using namespace Spark::Graphics;
auto& vtm = VirtualTextureManager::GetInstance();
vtm.Initialize(2048, 128); // 2048 pages, 128px tiles
// Each frame after rendering:
std::vector<FeedbackEntry> feedback = ReadbackFeedbackBuffer();
vtm.ProcessFeedback(feedback, frameNumber);
// Periodic cleanup (e.g., every 30 frames)
if (frameNumber % 30 == 0)
{
uint32_t evicted = vtm.EvictStalePages(frameNumber, 60);
if (evicted > 0)
{
LOG_INFO("Evicted {} stale virtual texture pages", evicted);
}
}
// Debug output
LOG_INFO("VT: {} resident, {} pending",
vtm.GetResidentPageCount(), vtm.GetPendingPageCount());- Asset Pipeline: Source textures are pre-tiled during asset import. See Asset Pipeline
- Material System: Materials reference virtual texture IDs rather than GPU texture handles
- RHI Backend: The indirection table and page cache physical texture are managed by the RHI layer
- Terrain: Large terrain textures are ideal candidates for virtual texturing. See Terrain and Procedural Generation
Initialize the VirtualTextureManager singleton early in the rendering startup, after the RHI device is created:
#include "Graphics/VirtualTexture.h"
using namespace Spark::Graphics;
// Get the singleton
auto& vtm = VirtualTextureManager::GetInstance();
// Initialize with custom settings:
// pageCacheSize: maximum number of pages in the GPU cache
// tileSize: pixel dimensions of each tile edge
bool ok = vtm.Initialize(2048, 128);
if (!ok)
{
LOG_ERROR("Failed to initialize virtual texture manager");
}Call Shutdown() during engine teardown to release all GPU resources:
vtm.Shutdown();
// After shutdown, m_initialized is false and the page cache is clearedThe page table is an indirection structure that maps virtual texture coordinates to physical cache locations. The VirtualTextureManager maintains two internal lists:
Pages currently loaded in GPU memory. Each page tracks:
struct VirtualTexturePage
{
uint32_t textureId; // Which source texture this tile belongs to
uint32_t mipLevel; // Mip level (0 = highest resolution)
uint32_t tileX, tileY; // Tile coordinates within the mip level
bool resident; // Always true for pages in m_pageCache
uint32_t lastUsedFrame; // Updated every frame the page is referenced
};Pages that have been requested but are not yet loaded. These are queued for async I/O:
// Check if a page is already pending
VirtualTexturePage* pending = vtm.FindPendingPage(textureId, mipLevel, tileX, tileY);
// Returns nullptr if not in the pending queueWhen ProcessFeedback() encounters a feedback entry:
- Search
m_pageCachefor a matching resident page (FindResidentPage) - If found, update
lastUsedFrameto the current frame number - If not found, search
m_pendingLoads(FindPendingPage) - If not pending, create a new
VirtualTexturePageentry and add tom_pendingLoads
In the pixel shader, each virtual texture sample writes a FeedbackEntry to a UAV feedback buffer:
// Virtual texture sampling in HLSL
RWStructuredBuffer<FeedbackEntry> feedbackBuffer : register(u1);
float4 SampleVirtualTexture(float2 uv, uint textureId)
{
// Calculate required mip level based on screen-space derivatives
float2 ddxUV = ddx(uv);
float2 ddyUV = ddy(uv);
float maxDeriv = max(length(ddxUV), length(ddyUV));
uint mipLevel = (uint)log2(max(1.0, maxDeriv * textureSize));
// Write feedback entry
uint idx;
InterlockedAdd(feedbackCounter[0], 1, idx);
feedbackBuffer[idx].textureId = textureId;
feedbackBuffer[idx].requiredMip = mipLevel;
// Sample from the indirection table -> physical cache
return SampleFromPageCache(uv, textureId, mipLevel);
}// 1. Read back the feedback buffer from GPU
std::vector<FeedbackEntry> feedback = ReadbackFeedbackBuffer();
// 2. Process feedback: mark used pages, queue missing pages, evict stale
vtm.ProcessFeedback(feedback, currentFrameNumber);
// ProcessFeedback internally:
// - For each entry, checks if the page is already resident
// - If resident: updates lastUsedFrame
// - If not resident and not pending: adds to m_pendingLoads
// - If cache is full: evicts least-recently-used pages firstYou can request specific pages outside the feedback loop (e.g., for preloading):
// Preload mip 0 of texture 5 at tile (0,0)
bool queued = vtm.RequestPage(5, 0, 0, 0);
// Returns true if the page is already resident or was successfully queuedThe page cache has a fixed maximum size. Choose settings based on your VRAM budget:
| Configuration | Pages | Tile Size | VRAM (RGBA8) | Use Case |
|---|---|---|---|---|
| Low | 512 | 128 | 32 MB | Mobile / low-spec |
| Medium | 1024 | 128 | 64 MB | Default |
| High | 2048 | 128 | 128 MB | Desktop with 4+ GB VRAM |
| Ultra | 4096 | 256 | 1 GB | High-end / terrain-heavy |
// Low-VRAM configuration
vtm.Initialize(512, 128);
// High-VRAM configuration with larger tiles (fewer page faults)
vtm.Initialize(4096, 256);| Tile Size | Pros | Cons |
|---|---|---|
| 64 px | Fine-grained streaming, low waste | More page faults, higher management overhead |
| 128 px | Good balance (default) | Moderate waste at edges |
| 256 px | Fewer page management operations | Higher VRAM per page, coarser granularity |
The framesToKeep parameter controls how aggressively pages are evicted:
// Aggressive eviction: pages expire after 30 frames (~0.5s at 60fps)
// Good for rapidly moving cameras (e.g., vehicle games)
uint32_t evicted = vtm.EvictStalePages(currentFrame, 30);
// Conservative eviction: pages stay for 120 frames (~2s at 60fps)
// Good for exploration games where players revisit areas
uint32_t evicted = vtm.EvictStalePages(currentFrame, 120);
// Default: 60 frames (~1 second at 60fps)
uint32_t evicted = vtm.EvictStalePages(currentFrame, 60);To reduce page faults at the cost of quality, bias the mip level upward:
// In the shader: add a bias to request a lower-resolution mip
uint mipLevel = (uint)log2(max(1.0, maxDeriv * textureSize));
mipLevel += mipBias; // mipBias = 1 halves page requestsvt_status # Display current page cache statistics
vt_resident # Show count of resident pages
vt_pending # Show count of pages waiting to load
vt_evict # Force eviction of all stale pages
vt_cache_size <n> # Set maximum resident page count
vt_tile_size <n> # Set tile size (requires reinitialize)
The Console_GetStatus() method returns a formatted string:
std::string status = vtm.Console_GetStatus();
// Example output:
// "VirtualTexture: 847/2048 resident, 12 pending, tile=128px"Page faults (missing pages that must be loaded) cause visual pop-in. Strategies to minimize them:
- Increase cache size -- More pages = fewer evictions = fewer re-loads
-
Preload visible pages -- Use
RequestPage()during loading screens to warm the cache -
Increase
framesToKeep-- Keep pages resident longer to handle camera oscillation - Bias mip levels -- Request lower-resolution mips to reduce unique page count
// Per-frame monitoring
uint32_t resident = vtm.GetResidentPageCount();
uint32_t pending = vtm.GetPendingPageCount();
float utilization = static_cast<float>(resident) / maxPages;
// Log warnings for cache pressure
if (pending > resident / 4)
{
LOG_WARN("VT: High page fault rate ({} pending vs {} resident)", pending, resident);
LOG_WARN("VT: Consider increasing pageCacheSize or mip bias");
}
if (utilization > 0.95f)
{
LOG_WARN("VT: Cache nearly full ({:.1f}% utilized), eviction rate may increase",
utilization * 100.0f);
}To avoid per-frame spikes, spread eviction across multiple frames:
// Instead of evicting all stale pages at once:
if (frameNumber % 30 == 0)
{
vtm.EvictStalePages(frameNumber, 60);
}
// Consider processing feedback in chunks for very large scenes:
constexpr size_t MAX_FEEDBACK_PER_FRAME = 4096;
if (feedback.size() > MAX_FEEDBACK_PER_FRAME)
{
// Process the most recent entries first (likely most relevant)
auto truncated = std::vector<FeedbackEntry>(
feedback.end() - MAX_FEEDBACK_PER_FRAME, feedback.end());
vtm.ProcessFeedback(truncated, frameNumber);
}
else
{
vtm.ProcessFeedback(feedback, frameNumber);
}- Rendering and Graphics — Texture streaming and material system
- Asset Pipeline — Asset import and texture processing
- Terrain and Procedural Generation — Terrain texture streaming
Published from ad9c3c1953cc. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Golden Image Testing
- Utilities
- Testing
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers