Repository navigation
Upscaling System
SparkEngine's upscaling system provides a unified interface for spatial and temporal upscaling: AMD FSR 1.0/2.0, NVIDIA DLSS, and Intel XeSS.
-
Files:
Graphics/UpscalingSystem.h(header + inline),Graphics/UpscalingSystem.cpp(compute shaders + utilities) - Platform: Windows (D3D11 compute shaders)
| Mode | Type | Inputs Required |
|---|---|---|
| FSR 1.0 | Spatial | Color only |
| FSR 2.0 | Temporal | Color, depth, motion vectors, jitter |
| DLSS | Temporal | Color, depth, motion vectors, exposure, jitter |
| XeSS | Temporal | Color, depth, motion vectors, jitter |
| Quality | Render Scale | Use Case |
|---|---|---|
| Ultra Performance | 33% | 4K with low-end GPU |
| Performance | 50% | Best performance/quality balance |
| Balanced | 58% | Moderate quality uplift |
| Quality | 67% | High quality with some perf gain |
| Ultra Quality | 77% | Minimal quality loss |
| Native | 100% | Sharpening only |
UpscalingSystem upscaling;
upscaling.Initialize(device, context, 1920, 1080);
UpscalingSettings settings;
settings.mode = UpscalingMode::FSR1;
settings.quality = UpscalingQuality::Quality;
upscaling.SetSettings(settings);
auto [renderW, renderH] = upscaling.GetRenderResolution();
// Render scene at renderW x renderH...
upscaling.Execute(colorSRV, outputUAV);- EASU (Edge Adaptive Spatial Upsampling): 12-tap Lanczos filter with edge detection
- RCAS (Robust Contrast Adaptive Sharpening): Per-pixel adaptive sharpening
- Temporal accumulation with motion vector reprojection
- Jitter-aware sampling using Halton sequences
- Depth-based disocclusion detection
// Halton jitter for TAA/temporal upscaling
float jitter = UpscalingUtils::GenerateHaltonSequence(frameIndex, 2);
// SDK availability detection
bool hasDLSS = UpscalingUtils::DetectDLSSAvailability();
bool hasXeSS = UpscalingUtils::DetectXeSSAvailability();The Edge Adaptive Spatial Upsampling pass is the core of FSR 1.0. It performs a single-pass directional upscale using a 12-tap filter kernel that adapts its shape based on local edge direction and gradient strength.
- Gradient Analysis: For each output pixel, the shader samples a 4x4 neighbourhood in the source image and computes horizontal and vertical gradient magnitudes using finite differences.
- Edge Direction Estimation: The ratio of gradients determines the dominant edge direction. This produces a continuous angle rather than a quantised set, allowing smooth adaptation.
- Kernel Shape Selection: A Lanczos-2 kernel is stretched along the detected edge direction so that samples along the edge receive higher weight and samples across the edge receive lower weight. This preserves edge sharpness while avoiding ringing artefacts perpendicular to the edge.
- 12-Tap Filtering: The shaped kernel is evaluated at 12 sample positions surrounding the output pixel. Bilinear hardware filtering is used for each tap, effectively doubling the number of source texels considered.
- Output: The weighted sum of the 12 taps produces the upscaled pixel value.
Key constants exposed in UpscalingSystem.cpp:
// EASU constants packed into two float4 CBs
struct EASUConstants
{
float inputSizeX, inputSizeY; // Source resolution
float outputSizeX, outputSizeY; // Target resolution
float rcpInputSizeX, rcpInputSizeY;
float rcpOutputSizeX, rcpOutputSizeY;
};The compute shader dispatches one thread per output pixel in 16x16 thread groups:
uint32_t dispatchX = (outputWidth + 15) / 16;
uint32_t dispatchY = (outputHeight + 15) / 16;
context->Dispatch(dispatchX, dispatchY, 1);Robust Contrast Adaptive Sharpening runs as a second pass after EASU (or after temporal accumulation for FSR 2.0). It applies per-pixel adaptive sharpening that avoids amplifying noise or creating halos.
- Local Contrast Measurement: For each pixel, the shader reads the immediate 4-connected neighbours (up, down, left, right) and computes the minimum and maximum luma values.
-
Sharpening Weight Calculation: The weight is derived from
1.0 - saturate((maxLuma - minLuma) / maxLuma). High-contrast edges receive less sharpening to avoid overshoot; low-contrast areas receive more to recover detail lost during upscaling. -
Sharpening Application: A negative-lobe unsharp mask is applied:
sharpenedPixel = pixel + weight * (pixel - average(neighbours)). -
Clamp: The result is clamped to the
[minNeighbour, maxNeighbour]range to prevent ringing.
The sharpening strength is user-configurable:
// 0.0 = maximum sharpening, 1.0 = no sharpening
settings.rcasAttenuation = 0.25f; // Recommended defaultTemporal upscaling modes (FSR 2.0, DLSS, XeSS) require sub-pixel jitter applied to the projection matrix each frame. SparkEngine uses Halton sequences for quasi-random jitter with good coverage properties.
// Generates element n of the Halton sequence with the given base
float UpscalingUtils::GenerateHaltonSequence(uint32_t index, uint32_t base)
{
float result = 0.0f;
float fraction = 1.0f / static_cast<float>(base);
uint32_t i = index;
while (i > 0)
{
result += fraction * static_cast<float>(i % base);
i /= base;
fraction /= static_cast<float>(base);
}
return result;
}The jitter pattern uses base-2 for X and base-3 for Y, cycling over 16 frames (configurable via settings.jitterPhaseCount):
uint32_t phase = frameIndex % settings.jitterPhaseCount;
float jitterX = UpscalingUtils::GenerateHaltonSequence(phase + 1, 2) - 0.5f;
float jitterY = UpscalingUtils::GenerateHaltonSequence(phase + 1, 3) - 0.5f;
// Apply to projection matrix (in NDC pixel units)
projMatrix[2][0] += jitterX * 2.0f / renderWidth;
projMatrix[2][1] += jitterY * 2.0f / renderHeight;The jitter offset must also be passed to the upscaling shader so it can unjitter the current frame during temporal accumulation. Forgetting to unjitter is the most common integration bug and results in a persistent blurry or shimmering image.
The render resolution for each quality preset is calculated from the output (display) resolution and the render scale factor:
// Formula used internally
uint32_t renderWidth = std::max(1u, static_cast<uint32_t>(outputWidth * renderScale));
uint32_t renderHeight = std::max(1u, static_cast<uint32_t>(outputHeight * renderScale));Concrete examples for a 3840x2160 (4K) output:
| Quality Preset | Render Scale | Render Resolution | Pixel Count Ratio |
|---|---|---|---|
| Ultra Performance | 0.33 | 1267x713 | 11% |
| Performance | 0.50 | 1920x1080 | 25% |
| Balanced | 0.58 | 2227x1253 | 34% |
| Quality | 0.67 | 2573x1447 | 45% |
| Ultra Quality | 0.77 | 2957x1663 | 59% |
| Native | 1.00 | 3840x2160 | 100% |
The GetRenderResolution() method returns the calculated pair and also updates internal constant buffers so the compute shaders receive the correct dimensions.
FSR 2.0 and other temporal modes maintain a history buffer that accumulates detail over multiple frames. The process each frame is:
- Reproject History: Use motion vectors to warp the previous frame's accumulated buffer to the current frame's viewpoint.
- Neighbourhood Clamp: Compute a colour-space bounding box (AABB in YCoCg space) from the current frame's local neighbourhood. Clamp the reprojected history sample to this box to reject ghosting from disoccluded or fast-moving regions.
- Disocclusion Detection: Compare the current depth buffer against the reprojected depth. Pixels with large depth discontinuities are marked as disoccluded and receive reduced history weight.
- Blend: Lerp between the current jittered sample and the clamped history. Typical blend factor is 0.05-0.10 for the current frame (i.e., 90-95% history weight). Disoccluded pixels use a much higher current-frame weight (0.5-1.0).
- Sharpen (RCAS): Apply the RCAS pass on the accumulated result to restore fine detail.
- Store: Write the blended result into the history buffer for the next frame.
// Internal temporal accumulation parameters
struct TemporalAccumulationParams
{
float historyWeight = 0.95f; // Weight for history sample
float disocclusionWeight = 0.50f; // Weight when disocclusion detected
float depthThreshold = 0.01f; // Depth delta for disocclusion
float velocityWeight = 1.0f; // Motion vector confidence scale
};Temporal upscaling requires per-pixel motion vectors rendered to a dedicated R16G16_FLOAT render target during the G-buffer pass. Motion vectors encode the screen-space displacement from the current frame to the previous frame in UV coordinates.
Requirements for correct motion vectors:
- Camera motion: All objects must account for the change in view-projection matrix between the current and previous frame.
- Object motion: Dynamic objects must additionally encode their per-vertex displacement due to animation or physics movement.
- Jitter removal: Motion vectors must be computed using unjittered projection matrices. Jittered matrices cause the motion vector to include the jitter delta, confusing temporal accumulation.
- Format: R16G16_FLOAT (16-bit per component) is sufficient for most cases. R32G32_FLOAT provides higher precision for very large resolutions or extreme motion.
// In the motion vector pixel shader
float2 currentNDC = currentClipPos.xy / currentClipPos.w;
float2 previousNDC = previousClipPos.xy / previousClipPos.w;
float2 motionVector = (previousNDC - currentNDC) * float2(0.5, -0.5); // UV spaceThe upscaling system can work alongside dynamic resolution scaling (DRS) to further improve performance. When DRS is active, the render scale is adjusted each frame based on GPU frame time:
UpscalingSettings settings;
settings.mode = UpscalingMode::FSR2;
settings.quality = UpscalingQuality::Quality; // Base render scale = 67%
settings.enableDynamicResolution = true;
settings.drsMinScale = 0.50f; // Never go below 50%
settings.drsMaxScale = 1.00f; // Never exceed native
settings.drsTargetFrameTimeMs = 16.67f; // Target 60 FPS
upscaling.SetSettings(settings);When DRS adjusts the render scale, the EASU/temporal accumulation constants are updated automatically. The history buffer is invalidated when the render resolution changes by more than 10% to avoid accumulation artefacts from mismatched resolutions.
Quality presets in SparkEngine bundle upscaling settings with other rendering parameters for a consistent experience:
// In QualityPresets.h
struct QualityPreset
{
UpscalingMode upscalingMode;
UpscalingQuality upscalingQuality;
float rcasAttenuation;
ShadowQuality shadowQuality;
TextureQuality textureQuality;
// ... other settings
};
static constexpr QualityPreset kLowPreset = {
UpscalingMode::FSR1,
UpscalingQuality::Performance,
0.20f,
ShadowQuality::Low,
TextureQuality::Low
};
static constexpr QualityPreset kUltraPreset = {
UpscalingMode::FSR2,
UpscalingQuality::Quality,
0.25f,
ShadowQuality::Ultra,
TextureQuality::Ultra
};No vendor upscaler SDK is linked: IsFSR2Available(), GetDLSSFeatureInfo().isAvailable, and GetXeSSFeatureInfo().isAvailable all report false, and selecting FSR2/DLSS/XeSS logs a warning once and runs the built-in SparkSR path instead. Console_GetStatus reports that availability truthfully.
The substitution is now visible rather than implied: UpscalingSystem::GetEffectiveMode() returns the mode that actually runs, and the status line reads Mode: DLSS (running SparkSR). Report the effective mode when you quote what is active — a requested mode is not evidence that a vendor upscaler ran.
The upscaling system is designed as a post-processing step after tonemapping and before the final UI overlay. It is not executed in the frame path today: the only production uses of UpscalingSystem are Initialize/Shutdown in GraphicsEngineWindowsInit.cpp and GraphicsEngineLinux.cpp; nothing calls Execute* or GetRenderResolution() per frame. The diagram shows the intended placement:
Scene Render (at render resolution)
-> G-Buffer Pass (+ motion vectors)
-> Lighting Pass
-> Volumetrics
-> Tonemapping
-> Upscaling (EASU+RCAS or Temporal) <- UpscalingSystem::Execute()
-> UI Overlay (at display resolution)
-> Present
The upscaling system manages its own intermediate render targets (history buffer, EASU output) and only requires the caller to provide the tonemapped colour SRV and an output UAV at display resolution.
Not implemented (verified 2026-09): no
UpscalingSettingsComponentexists inSparkEngine/Source,RenderSystemdoes not read one, and SparkEditor has noUpscalingSettingsPanelor "Upscaling Settings" window. Earlier revisions of this page described both as if they shipped. Upscaling is configured only through theUpscalingSystemC++ API above, and that system is not executed in the frame path (see "Integration with Render Pipeline"). Treat a per-camera component and an editor panel as design intent, not as available features.
- FSR 1.0 is the cheapest option (single spatial pass) and is recommended for very low-end hardware or when motion vectors are not available.
- FSR 2.0 provides the best quality-per-cost for AMD and older NVIDIA hardware but requires accurate motion vectors.
- DLSS leverages dedicated Tensor Cores on NVIDIA RTX GPUs and produces the best quality at the lowest render scales, but is vendor-locked.
- XeSS uses XMX cores on Intel Arc GPUs for best quality but also has an effective DP4a fallback path on other vendors.
- The RCAS pass cost is negligible (under 0.1ms at 4K) and should almost always be enabled.
- When using temporal modes, ensure the jitter phase count matches or exceeds the number of frames the history buffer accumulates. A phase count of 16 is recommended for most scenarios.
- Avoid enabling both the engine's built-in TAA and temporal upscaling simultaneously, as double temporal accumulation produces excessive blur and ghosting.
5 unit tests in Tests/TestUpscalingSystem.cpp covering quality presets, render resolution calculation, input requirements, FSR constants, and default settings.
- Rendering and Graphics -- Main rendering pipeline documentation
- Post-Processing -- Other post-processing effects (bloom, DOF, colour grading)
- Shader Pipeline -- How compute shaders are compiled and dispatched
- Quality Presets -- Engine-wide quality preset system
- Entity-Component-System -- ECS architecture and component reference
Published from 2b03dc797148. 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
- Platform Certification
- 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
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- 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