skyrim-vr-upscale

Foveated eye-buffer reconstruction for VR titles, loadable through kernels, built for and measured on Skyrim VR. The game renders each eye at reduced resolution; one kernel pass reconstructs the full-resolution buffer with quality that follows the lens: a 4x4 Catmull-Rom with an anti-ringing clamp and a fused unsharp term at the projection centre, plain bilinear at the rim, and a smoothstep-continuous blend of the two weight sets between, so quality falls off radially with no band seam anywhere in the image. Both eyes run in one launch with per-eye projection centres.

A stone-dragon statue reconstructed by this kernel, wiped against a plain bilinear upscale of the same reduced-resolution frame: full Catmull-Rom quality inside the marked circle over the statue, softening smoothly to bilinear at the rim

An A/B wipe between this kernel's foveated reconstruction and a plain bilinear upscale of the same reduced-resolution frame (media/make_hero.py). Full Catmull-Rom quality holds inside the circle over the statue and falls off smoothly to bilinear at the rim, with no band seam. The frame is a stone dragon path-traced with phanerozoic/pathtracer-diff, standing in for a VR eye buffer; it is a reconstruction demonstration.

The open VR injection ecosystem upscales in two tiers with a hard edge between them (vrperfkit runs FSR1 inside a radius and bilinear outside, per 16x16 workgroup), and the visible ring at that edge is the known artifact of the design. This kernel replaces the two-tier switch with a continuous filter family, and adds the determinism discipline of a compute kernel: reconstruction is bitwise reproducible across runs, launch configurations, and CUDA architectures.

Usage

import torch
from kernels import get_kernel

svr = get_kernel("phanerozoic/skyrim-vr-upscale", version=1,
                 trust_remote_code=True)

lo = torch.empty(2, 1725, 1552, 4, dtype=torch.uint8, device="cuda")  # eyes
hi = svr.upscale(lo, out_hw=(2240, 2016),
                 center=[[0.46, 0.52], [0.54, 0.52]],   # per-eye, fractional
                 radii=(0.30, 0.70), sharp=0.4)

src is [H, W, 4] or [B, H, W, 4] RGBA, uint8 or float32. radii are fractions of the output height: full quality inside r0, bilinear outside r1. center defaults to the image centre; a VR runtime's real per-eye projection centres go straight through. version selects the release branch; trust_remote_code is required by kernels for publishers without the trusted-publisher mark.

How it works

Each output pixel maps to its source coordinate under the standard half-pixel convention and evaluates one 4x4 filter whose weights are a radial blend w = cr + t * (tent - cr), with t = smoothstep(r0, r1, r) in eccentricity from the projection centre. Accumulation is centre plus weighted differences, out = c + sum w_i (s_i - c): the weights sum to one analytically, so a constant field reproduces exactly regardless of floating-point weight error. Catmull-Rom overshoot at edges is confined to the inner 2x2 hull (the bilinear part already lies inside it), and a fused unsharp term out += sharp * (1 - t) * (out - bilinear) restores acutance in the fovea, fading with the same radial profile as the filter. Beyond r1 the kernel takes a 4-tap fast path that is arithmetically the t = 1 limit of the blended path, so the fast-path boundary introduces no seam of its own.

Every floating-point step is an explicit round-per-op intrinsic (__fadd_rn / __fmul_rn / __fmaf_rn / __fsqrt_rn), so ptxas cannot contract expressions differently per target and output bytes are a function of the code, not the architecture. One thread owns one output pixel and writes it once: no atomics, bitwise deterministic.

Correctness

The suite is analytic-first; every property below is a test.

  • Constant fields are bit-exact in every band, both dtypes, with sharpening on: the centre-difference form makes every contribution identically zero.
  • Same-size is a bitwise pass-through (sharp = 0): Catmull-Rom is interpolating and both weight sets are the delta at phase zero, at any radii.
  • Linear ramps reconstruct as themselves at every eccentricity, away from the clamped border: Catmull-Rom, tent, their blend, and the unsharp term all have linear precision.
  • The periphery is bilinear: with radii = (0, 0) the kernel matches torch.nn.functional.interpolate(mode="bilinear") to float rounding.
  • No ringing: a step edge reconstructs inside the local hull; the output range is the input range.
  • The float64 reference agrees: a vectorized f64 model of the whole pipeline bounds the f32 kernel at 2e-5 max error on detailed content.
  • Batched equals singles, bitwise, with distinct per-eye centres.
  • Deterministic, bitwise, on repeat calls.
  • Golden digests: SHA-256 of reconstructions of hash-built frames, minted on sm89. Digest equality on another card certifies bit-identical output across CUDA architectures, torch releases, and toolchains.
  • Foveal quality: on band-limited detailed content, foveal MSE against ground truth is under 0.7x bilinear's, while peripheral MSE matches bilinear within 5%.

Kernel throughput

RTX 6000 Ada (sm89), both eyes per launch, medians of cudaEvent timings with buffers resident (bench/eye_bench.py). Output shapes span SteamVR render targets for a Quest 3 over Link; sources are the renderScale = 0.77 buffers an injector feeds. Baselines on the same tensors: the kernel pinned fully bilinear (radii = (0, 0)), pinned fully foveal (radii = (9, 9.9)), and torch's own resamplers.

output/eye source/eye uint8 float32 uint8 Gpx/s bilinear-only fovea-only torch bilinear torch bicubic
1600x1440 1232x1109 0.130 ms 0.207 ms 35.5 0.075 0.130 0.176 0.500
2240x2016 1725x1552 0.203 ms 0.407 ms 44.5 0.102 0.202 0.348 0.940
2736x2464 2107x1897 0.312 ms 0.580 ms 43.2 0.156 0.311 0.517 1.419
3216x3072 2476x2365 0.459 ms 0.825 ms 43.1 0.261 0.463 0.762 2.030

At the standard shape the full graded pass costs 0.10 ms per eye in uint8: about 0.9% of a 90 Hz frame budget, faster than torch's uniform bilinear resample of the same buffers, and 4.6x faster than torch bicubic, while delivering Catmull-Rom quality in the fovea. The graded frame prices at its foveal ceiling (0.202 vs 0.203 ms): the radial blend itself is free, and the periphery fast path buys headroom that widens as r1 tightens.

Frame-rate benefit in VR

An upscaler earns frame time by rendering fewer, cheaper pixels, then reconstructing full resolution. Measured on an RTX 6000 Ada with GPU timestamp queries, native D3D11 path (bench/perf_hlsl.cpp), both eyes per frame, on a demanding scene (Quest 3 high, 2464x2736/eye):

renderScale source pixels full-res frame with SVR speedup pixel ceiling
0.90 81% 1.708 ms 1.614 ms 1.06x 1.23x
0.77 59% 1.705 ms 1.259 ms 1.35x 1.69x
0.67 45% 1.707 ms 1.002 ms 1.70x 2.23x
0.59 35% 1.706 ms 0.827 ms 2.06x 2.87x

Reconstruction costs about 0.23 ms per frame at this resolution regardless of scene, roughly 2% of a 90 Hz budget, which is why the measured speedup sits just under the pixel-count ceiling: the scene render drops by the full pixel ratio and only the reconstruction is added back. The gain scales with how GPU-bound the scene is (the number above is a heavy shading load); a CPU-bound scene gains nothing from any resolution upscaler. renderScale is the dial between frame rate and source detail, and the foveal reconstruction buys some of that detail back.

The CUDA-bridge path (bench/perf_cuda_bridge.cpp) measures 1.05x at renderScale 0.77 on the same scene: the CUDA-D3D11 interop map/unmap and staging copies add about 0.4 ms per frame on top of a 0.1 ms kernel, which is why the in-game path uses the HLSL shader.

In-game injection

The kernel drives an injection path for OpenVR/SteamVR titles, not Skyrim alone. A vrperfkit-derived dxgi wrapper hooks the game's D3D11 device and its compositor submit, reads the runtime's per-eye projection centres, and reconstructs each submitted eye buffer. Any OpenVR D3D11 title on SteamVR hits the same path; this was verified headless on SteamVR's null driver with a non-game OpenVR harness (bench/openvr_inject_harness.cpp), where the injector hooked and ran the kernel on submitted eye textures with no headset or game present.

game/ carries the injection artifacts: svr_upscale.hlsl, a D3D11 compute-shader port of the kernel that reconstructs bit-for-bit what the CUDA kernel does (within 1/255, verified by bench/hlsl_vs_cuda_check.cpp) and runs entirely in D3D11 for the numbers above; svr_bridge.cu, which runs the published CUDA kernel in-process through cudaGraphicsD3D11RegisterResource; and the vrperfkit integration. The .cu kernel is torch-free behind a C ABI (svr_cuda/upscale_launch.h) so the Hub build and the in-process bridge compile the identical source. Fixed-foveation radii follow the headset lens, not eye tracking.

Requirements and limits

  • NVIDIA GPU, compute capability 7.5+; CUDA.
  • RGBA, uint8 or float32, contiguous [B, H, W, 4]; sources at least 2x2. Any output size; no alignment constraints.
  • Fixed foveation: the centre is per-eye but static per call. sRGB filtering is performed in the stored space, matching the injection ecosystem's practice.
  • Published variants are Linux x86_64. On Windows, or for a torch build without a matching variant, load_local.py JIT-builds the same source and exposes the identical API.

References

Catmull and Rom, "A class of local interpolating splines" (1974); Mitchell and Netravali, "Reconstruction filters in computer graphics" (SIGGRAPH 1988); Guenter et al., "Foveated 3D graphics" (SIGGRAPH Asia 2012); Patney et al., "Towards foveated rendering for gaze-tracked virtual reality" (SIGGRAPH Asia 2016); fholger/vrperfkit (the two-tier radial upscale this kernel supersedes); AMD FidelityFX Super Resolution 1.0 and NVIDIA Image Scaling (the uniform single-pass upscalers of the same ecosystem).

License

Apache-2.0.

Downloads last month
3
vr
foveated-rendering
upscaling
image-processing
skyrim
apache-2.0
Supported hardwares new
CUDA
7.58.08.68.99.010.012.0
GPU
B300
288GB
NVIDIA SXM
B200
192GB
NVIDIA SXM
H200
141GB
NVIDIA SXM
H100
80GB
GPU
H800
80GB
GPU
H20
96GB
GPU
L40s
48GB
GPU
L40
48GB
GPU
L20
48GB
GPU
L4
24GB
DGX Spark
GB10
128GB
GPU
RTX PRO 6000 WS
96GB
GPU
RTX PRO 6000 Max-Q
96GB
GPU
RTX PRO 5000
48GB
GPU
RTX PRO 4500 WS
32GB
GPU
RTX PRO 4000
24GB
GPU
RTX PRO 4000 SFF
24GB
GPU
RTX PRO 2000
16GB
GPU
RTX 6000 Ada
48GB
GPU
RTX 5880 Ada
48GB
RTX
RTX 5000 Ada
32GB
GPU
RTX 4500 Ada
24GB
RTX
RTX 4000 Ada
20GB
RTX
RTX 4000 SFF Ada
20GB
GPU
RTX 3500 Ada Mobile
12GB
GPU
RTX 2000 Ada
16GB
GPU
RTX A6000
48GB
GPU
RTX A5000
8GB
GPU
RTX A5000 Max-Q
16GB
GPU
RTX A5000 Mobile
16GB
GPU
RTX A4000
16GB
GPU
RTX A4000 Max-Q
8GB
GPU
RTX A4000 Mobile
8GB
GPU
RTX A3000 Mobile
6GB
GPU
RTX A2000
6GB
GPU
RTX A2000 Embedded
4GB
GPU
RTX A2000 Max-Q
4GB
GPU
RTX A2000 Mobile
4GB
GPU
A800
40GB
GPU
A100
80GB
GPU
A40
48GB
GPU
A30
24GB
GPU
A10
24GB
GPU
A2
16GB
RTX
RTX 5090
32GB
RTX
RTX 5090 D
32GB
RTX
RTX 5090 Mobile
24GB
RTX
RTX 5080
16GB
RTX
RTX 5080 Mobile
16GB
RTX
RTX 5070
12GB
RTX
RTX 5070 Mobile
8GB
RTX
RTX 5070 Ti
16GB
RTX
RTX 5070 Ti Mobile
12GB
RTX
RTX 5060 Ti
16GB
RTX
RTX 5060
8GB
RTX
RTX 5060 Mobile
8GB
RTX
RTX 5050
8GB
RTX
RTX 5050 Mobile
8GB
RTX
RTX 4090
24GB
RTX
RTX 4090D
24GB
RTX
RTX 4090 Mobile
16GB
RTX
RTX 4080 SUPER
16GB
RTX
RTX 4080
16GB
RTX
RTX 4080 Mobile
12GB
RTX
RTX 4070
12GB
RTX
RTX 4070 Mobile
8GB
RTX
RTX 4070 Ti
12GB
RTX
RTX 4070 Super
12GB
RTX
RTX 4070 Ti Super
16GB
RTX
RTX 4060
8GB
RTX
RTX 4060 Ti
8GB
RTX
RTX 4090 Laptop
16GB
RTX
RTX 4080 Laptop
12GB
RTX
RTX 4070 Laptop
8GB
RTX
RTX 4060 Laptop
8GB
RTX
RTX 4050 Laptop
6GB
RTX
RTX 3090
24GB
RTX
RTX 3090 Ti
24GB
RTX
RTX 3080
12GB
RTX
RTX 3080 Ti
12GB
RTX
RTX 3080 Mobile
16GB
RTX
RTX 3070
8GB
RTX
RTX 3070 Ti
8GB
RTX
RTX 3070 Ti Mobile
8GB
RTX
RTX 3060 Ti
8GB
RTX
RTX 3060
12GB
GPU
RTX 2080 Ti
11GB
GPU
RTX 2080
8GB
GPU
RTX 2070
8GB
GPU
RTX 2070 SUPER Mobile
8GB
GPU
RTX 2070 SUPER
8GB
RTX
RTX 3060 Mobile
6GB
RTX
RTX 3050 Mobile
4GB
GPU
RTX 2060
6GB
GPU
RTX 2060 12GB
12GB
GPU
RTX 2060 Mobile
6GB
GPU
RTX 2050 Mobile
4GB
GPU
RTX Titan
24GB
GPU
GTX 1660
6GB
GPU
GTX 1650 Mobile
4GB
NVIDIA T4
T4
16GB
GPU
T10
16GB
Jetson
Jetson AGX Orin 64GB
64GB
Jetson
Jetson AGX Orin 32GB
32GB
Jetson
Jetson Orin NX 16GB
16GB
Jetson
Jetson Orin NX 8GB
8GB
Jetson
Jetson Orin Nano 8GB
8GB
Jetson
Jetson Orin Nano 4GB
4GB
OS
linux
Arch
x86_64
Kernel Builder
19aaa64