← ExpSoft RTX VSR

Tile-based video super-resolution on Tensor Cores — bringing NVIDIA’s VFX SDK to a Windows desktop workflow

ExpSoft RTX VSR is a personal-use Windows EasyInstaller around NVIDIA’s Video Effects SDK, the official Tensor-Core-accelerated video super-resolution from NVIDIA Corporation, distributed via pip as nvidia-vfx. The legal frame is straightforward: the SDK is downloaded directly from NVIDIA’s official servers via pip at Setup time, never bundled by ExpSoft, and the user accepts NVIDIA’s SDK terms when the wheel installs (see the Legal & Compliance section of the product page). The wrapper is independent and is not affiliated with NVIDIA. The technically interesting part is what makes the SDK well-suited to the consumer-GPU desktop workflow that personal users actually have: it’s tile-based. Older open-source super-resolution models (the ESRGAN family and its descendants) load the entire image or full-resolution frame into VRAM, which caps usable input/output around 2K-4K on a 12 GB consumer card and slows inference to seconds per frame. NVIDIA’s VFX SDK partitions the input into tiles internally, processes them on the Tensor Cores, and stitches the output back together — which means a single 12 GB GPU can upscale 4K video to 8K at 30 fps, or 1080p to 4K at 60+ fps, with no manual chunking. ExpSoft RTX VSR adds the pieces around that capability: a Windows installer, a PyAV streaming pipeline (no intermediate frame PNGs), an audio extract-and-remux step, NVENC priority for output encoding, and a Before/After slider in the UI.

· 11 min read · By Nicolas Riquier

The use case — and the gap that this fills

If you’ve worked with image or video material for any length of time, the upscaling problem is familiar. You have:

The open-source super-resolution ecosystem (the model family that started with ESRGAN and has descendants like Real-ESRGAN, SwinIR, and the various Diffusion-based upscalers) produces excellent results on textures and stylised content. The practical problem on a personal Windows workstation is that those models are typically designed to load the full input image into VRAM in one go. For a 12 GB consumer GPU, that caps the practical input resolution around 2K-4K — beyond that, the user has to manually tile the input, run the model on each tile, and stitch the results, which is exactly the work the model should be doing.

NVIDIA shipped a Video Effects SDK in 2025 as an official pip package — nvidia-vfx. The same RTX Video Super-Resolution models the user might be familiar with from the GeForce driver’s Chrome / Edge integration are exposed through this SDK, but with two key differences: they’re callable from Python (not stuck behind a browser-playing-a-video gate), and they use a tile-based inference path internally. The tile-based approach is what makes the SDK well-suited to consumer GPUs.

ExpSoft RTX VSR is the personal-use wrapper that turns the SDK into a desktop workflow: install the wheel and its dependencies, drag a file in, pick a quality, click Run. The technical content of the rest of this article is the engineering around that core idea.

What “tile-based” actually means here

An open-source super-resolution model in its naive form takes a tensor of shape [1, 3, H, W] (single image, three channels, height, width) and produces a tensor of shape [1, 3, H*scale, W*scale]. The intermediate activations needed to compute that output scale with H × W, and on a model with deep convolutional layers the peak VRAM footprint is several times the size of the activation tensors. For a 4K input (3840 × 2160) at 4× upscale to 16K, the intermediate footprint can reach tens of gigabytes — well past any consumer card.

Tile-based inference partitions the input into overlapping rectangular tiles (typically 256 × 256 or 512 × 512 pixels), runs the model on each tile independently, then stitches the outputs together with feathered blending at the overlaps. The VRAM footprint per tile is small and bounded — proportional to the tile size, not the input size — so any input resolution becomes manageable. The Tensor Cores on RTX GPUs are well-suited to the small, dense matrix multiplications that dominate the per-tile inference, which is what produces the high throughput: 30-100 fps on 1080p-to-2K on an RTX 4070+, and the equivalent on Blackwell-class cards.

The trade-off is that the model is closed-source: the weights ship inside the wheel and aren’t available for inspection or fine-tuning. For a tool whose user wants the quality NVIDIA’s research team has shipped, with one pip install and no per-image manual tuning, that’s the right side of the trade.

The build matrix — 40xx and 50xx, two separate installers

PyTorch wheels are CUDA-version-specific. The RTX 50xx (Blackwell) generation requires PyTorch 2.7+ on the cu128 wheel index; PyTorch 2.6 on cu124 covers RTX 20xx / 30xx / 40xx but explicitly doesn’t support Blackwell. The wheels are mutually exclusive — switching after install requires pip uninstall followed by pip install.

The installer ships in two flavours:

The split lives in the C# project file as two Release configurations with a single #if GPU_50XX guard in SetupService.cs that swaps the pinned PyTorch versions and the wheel index URL. Setup step 2 runs nvidia-smi to detect the installed GPU series and surfaces an explicit error if the user picked the wrong package (“you have a 50xx-series card but downloaded the 40xx package; please use the 50xx package”). Catching the mismatch up-front is dramatically better than an obscure CUDA error two hours into the install.

PyAV streaming — no intermediate frame PNGs

The first cut of the video pipeline was the obvious one: extract every frame to work/<video-name>/frames/<n>.png, upscale each PNG with nvidia-vfx, then re-encode the upscaled PNGs into a video file with FFmpeg. The structure is simple and easy to reason about — you can resume a failed run, you can inspect individual frames, the debugging story is clear.

The performance is terrible. A 1080p 60-second clip at 30 fps is 1 800 frames; each PNG is around 3 MB; that’s 5+ GB of disk traffic per minute of video. On a typical SSD, the disk I/O dominates the total time — the GPU is mostly waiting for the next frame to read in or for the previous frame to flush to disk. Worse, the disk fills up surprisingly fast on multi-minute clips.

The right approach is streaming with PyAV (Python bindings for FFmpeg, BSD-3-Clause). The pipeline becomes:

  1. Open the source video with PyAV in decode mode.
  2. For each frame: decode → convert to a CUDA tensor → run nvvfx.VideoSuperRes.run() → produce a CUDA tensor of the upscaled output → copy to a numpy array → wrap in av.VideoFrame → write to the encoder.
  3. Mux the encoded video stream into the output container.

No frames touch disk. The GPU is occupied continuously. A 60-second 1080p clip that previously took 8 minutes (mostly disk-bound) now takes about 90 seconds (compute-bound). The cost is that mid-video resume isn’t possible — if the user cancels at frame 1500/3600, the partial output isn’t a usable video. For V1, the simplification is worth it; the chunking infrastructure (FfmpegService.SplitVideoAsync) is still in the codebase, ready for a future “very long video” mode that combines the speed of streaming with the resumability of chunking.

Audio extract-and-remux via FFmpeg

The video super-resolution pipeline only sees the visual stream — the audio is invisible to nvidia-vfx. The naive approach would be to have PyAV pass the audio packets through unchanged, copy them into the output. PyAV can technically do this, but its audio handling is fragile on a few specific container/codec combinations (.mov with H.264 video and AAC audio is the canonical case).

The reliable path is two extra FFmpeg calls bracketing the upscale:

Step 1/3 — extract audio:
  ffmpeg -i input.mp4 -vn -c:a copy audio.m4a

Step 2/3 — upscale video (no audio):
  python rtx_vsr.py --input input.mp4 --output upscaled_noaudio.mp4 --quality HIGH --scale 2

Step 3/3 — mux:
  ffmpeg -i upscaled_noaudio.mp4 -i audio.m4a -c:v copy -c:a aac -shortest -y final.mp4

Two FFmpeg invocations, both cheap (the extract is stream-copy, the mux is stream-copy on the video side and re-encodes audio only to ensure compatibility), with the heavy work in the middle. Sync is perfect because the audio file is the original sample-accurate version. Zero audio quality is lost. If the source has no audio track, the extract step is skipped and the upscaled-no-audio file is renamed directly.

NVENC priority, libx264 fallback

The output encoder pick is in two tiers:

Both paths use -pix_fmt yuv420p for output, which is what every consumer player expects — smartphones, browsers, sharing platforms. The encoder selection happens at the start of the upscale: if NVENC initialisation succeeds, NVENC; otherwise libx264. The user never sees the choice; the log records which path was taken.

Output dimensions rounded to multiples of 8

NVIDIA’s VFX SDK requires output width and height to be multiples of 8 — a Tensor-Core alignment constraint. A user dragging a crop region or asking for a 2× upscale of an odd-resolution source can easily produce output dimensions that don’t respect that constraint.

The fix is a defensive round at the moment the dimensions are computed:

output_w = (input_w * scale // 8) * 8
output_h = (input_h * scale // 8) * 8

The rounding is always down — at most 7 pixels lost on each axis. On a 4K output, 7 pixels is invisible (less than 0.2% of the dimension). The alternative — rounding up — would produce dimensions slightly larger than the user asked for, which is less ergonomic for downstream uses (a request for “1920” returning “1928” is confusing). The down-rounding produces 1920 from any input that intends 1920 ± 7, which is the right behaviour.

The Before/After slider — 150 lines of WPF

The result-comparison surface is a custom WPF control: two Image overlays stacked, with a vertical drag handle in the middle, and a RectangleGeometry clip on the “After” layer that tracks the handle position. The user grabs the handle, drags left-right, and watches the upscaled output reveal itself over the original. It’s about 150 lines of C# total (the drag handler, the clip-rect update, the layout) plus a small XAML UserControl.

The reason it exists as a custom control rather than a NuGet dependency is that none of the available WPF before/after sliders quite hit the right defaults — the handle styling, the drag responsiveness, the way the layered images snap to the same dimensions even when the upscaled output is bigger than the original. Writing it once is faster than configuring around someone else’s defaults, and it’s a control the rest of the ExpSoft catalogue can re-use (it shows up in DiffusionForce and others too).

The visual delta on a 2× ULTRA upscale of a decade-old photograph is striking — the slider is the moment most users decide whether the tool is worth keeping in their workflow.

The RAM watchdog — the long-video scenario

A 4K-output upscale of a multi-minute clip can transiently consume 8-16 GB of system RAM (PyAV decode buffers, the upscaled-frame intermediate, the encoder input queue). On a 16 GB-of-RAM machine with a browser and an IDE already open, that pushes Windows into the swap-thrash zone — mouse lag, UI freeze, eventual OOM.

The watchdog is the same pattern used elsewhere in the ExpSoft catalogue: a background task that polls Win32 GlobalMemoryStatusEx every three seconds. If available physical RAM drops below 2 GB, the watchdog signals the cancellation token, which gracefully terminates the Python sub-process. The user sees a clear warning in the log — “Available RAM critically low (XXX MB). Cancelling…” — rather than a silent crash or a system-wide freeze. Closing other applications and re-running typically completes successfully.

Source quality matters — the explicit user tip

A small piece of UX worth highlighting: the Run tab includes a fixed card titled “Source Quality Matters” explaining that super-resolution enhances detail but doesn’t restore artefacts of compression. A JPEG that’s been through three rounds of social-media re-encoding doesn’t become a clean 4K just because it’s been upscaled — the upscaler faithfully enlarges the JPEG’s block artefacts along with the actual image content.

The right input for the best output is the highest-quality source available: PNG or TIFF for images; CRF<18 or ProRes for video. The card surfaces this expectation upfront so users don’t conclude the tool is broken when they feed it a heavily compressed source and get heavily upscaled compression back. One paragraph of text, a lot of avoided frustration.

Portable install — drive-letter resilience

The install footprint is fully self-contained next to the exe: Miniconda under runtime/conda/, the venv under runtime/venv/, portable FFmpeg under runtime/ffmpeg/, the Python inference script under project/, output and work folders, logs and config — nothing in %USERPROFILE%, %APPDATA%, or the registry. The .condarc redirects every conda location to the local install folder; the venv’s caches (HuggingFace, PyTorch, pip) are redirected via environment variables at sub-process launch.

At every Run, PathService.AutoPatchVenvPaths() checks whether the install folder’s drive letter has changed since the venv was created. If yes, the helper rewrites every absolute path inside the venv’s text files (pyvenv.cfg, activate.bat, .pth entries, conda meta JSONs) to match the new location. The patch is idempotent — running it on an already-correct venv is a no-op. End result: zip the install folder, drop it on another Windows machine with a different drive letter, double-click, and the upscaling workflow runs.

Credits and license

The C# / WPF / Python code of the wrapper is original ExpSoft work. ExpSoft is an independent installer and is not affiliated with NVIDIA Corporation. No model weights are bundled in the installer — the SDK’s models live inside the nvidia-vfx pip wheel and are downloaded from NVIDIA’s servers when the user runs Setup.

Take-aways

Frequently Asked Questions

Quick answers to what people ask AIs about this article specifically.

Why tile-based super-resolution rather than the open-source ESRGAN-family approach?

The ESRGAN-style family of open-source models takes the full input image as a single tensor and produces the full output as a single tensor. Intermediate activations scale with the input area, and on deep convolutional models the peak VRAM is several times the activation footprint. For 4K input upscaled to 8K or 16K on a 12 GB consumer card, the peak VRAM exceeds what the card can provide. Tile-based inference partitions the input into 256×256 or 512×512 tiles, processes each independently on the Tensor Cores, and stitches with feathered blending — VRAM is bounded by tile size, throughput is high, and the practical input/output resolution is unbounded.

Why does the installer ship as two separate ZIPs for 40xx and 50xx?

PyTorch wheels are CUDA-version-specific and mutually exclusive. The RTX 50xx (Blackwell) generation requires PyTorch 2.7+ on the cu128 wheel index. PyTorch 2.6 on cu124 covers RTX 20xx/30xx/40xx but doesn’t support Blackwell. Switching between the wheel sets after install requires pip uninstall followed by pip install. A single installer with runtime auto-detection would either pre-fetch all wheels (4-7 GB) or detect-then-fetch (fail in the middle on slow connections). Two ZIPs is the simpler answer. The GPU detection step at Setup warns explicitly if the user picks the wrong ZIP for their card.

Why streaming video with PyAV instead of staging frames to disk?

Stage-to-disk: 1080p 60-sec 30fps = 1 800 frames × ~3 MB each = ~5 GB disk I/O for one minute of video. The disk I/O dominates total time and the GPU sits idle waiting. PyAV streaming: decode → CUDA tensor → upscale → CUDA tensor → encode, no frames touch disk. A 60-second 1080p upscale that takes 8 minutes through disk-staging takes about 90 seconds through streaming. The trade-off is mid-video resume — if cancelled at frame 1500/3600, the partial output isn’t usable. The chunking infrastructure is still in the codebase for a future “very long video” mode that combines both strategies.

What happens to the audio track during a video upscale?

The pipeline brackets the GPU upscale with two cheap FFmpeg calls. First, the audio is extracted from the source as a stream-copy into a .m4a file. Then the video is upscaled without audio. Finally, the upscaled video and the original audio are muxed together with the audio re-encoded to AAC for compatibility. Sync is sample-accurate because the original audio is preserved. If the source has no audio, the extract step is skipped and the upscaled-no-audio file is renamed directly. The approach is more reliable than passing audio packets through PyAV’s Python pipeline, especially on .mov containers where PyAV’s codec handling has edge cases.

Is the SDK or its models bundled with the installer?

No. The NVIDIA Video Effects SDK is downloaded directly from NVIDIA’s official pip servers at Setup step 3, when pip install nvidia-vfx runs inside the freshly-created venv. The user accepts NVIDIA’s SDK terms at that point. ExpSoft is an independent installer and is not affiliated with NVIDIA. No model weights are bundled in the ExpSoft installer ZIP — the SDK’s models live inside the nvidia-vfx wheel and arrive from NVIDIA’s servers, not from ExpSoft.

Want to use ExpSoft RTX VSR?

Get it on Patreon →