NRR upscalers - the released 2x model, and a 2.5x tier

Licence: MIT. Copyright (c) 2026 SamurAI-Official. Permission is hereby granted, free of charge, to any person obtaining a copy of these model weights and this card to deal in them without restriction, including the rights to use, copy, modify, merge, publish, distribute, sublicense and/or sell copies, subject to the terms in LICENSE (the MIT warranty disclaimer included). These are the models already distributed under MIT, and they stay MIT.

Future models: from the commit the repository moved to an All Rights Reserved licence, code and model weights developed after that change are All Rights Reserved and are not covered by this MIT grant. A Hugging Face repo carries one license: field, so any such model belongs in its own repo or a clearly-marked path rather than silently inheriting the MIT tag above.

A 2x neural upscaler as a single ONNX graph of about 461 KB, trained in the NRR repository and used there as the released model: the one the Unreal, Godot and Unity plugins load, the one docs/parity.md measures, and the one this repository's headless verifications render.

The same repository publishes a 2.5x model under ratio25/, which is what the 768x432 -> 1920x1080 tier needs. The released 2x model cannot be asked for that tier: the ratio is the graph's own head, so fed a 768x432 frame it returns 1536x864, and it reaches 1920x1080 only from a 960x540 input.

It is a prototype model from a research repository rather than a shipping upscaler. It is published so the plugin side of that repository can be verified against a real model instead of a sample.

The graph

inputs color - the low-resolution frame; jitter - the sub-pixel offset the frame was rendered at (all zeros for a render that does not jitter); scale - the tier token
output one frame, 2x the input on both axes
spatial dynamic H/W: the graph takes whatever size the input is, including non-square. Verified with tools/verify_rect_upscale.py in the NRR repository on a 160x90 rectangular frame: the output is exactly (1, 3, 180, 320) - 2x on both axes, finite, no square reshape anywhere. The 2.5x graphs below are checked the same way
the scale token 0.0 for the 128 -> 256 grid, 1.0 for the 256 -> 512 grid. The model was trained at both, so it is measured at both - docs/parity.md and docs/parity-512.md are two halves of one result rather than two results. NRR's own harness derives the token from the frame's width (log2(width / 128)), so each tier is handed the value it was trained with
depth, motion not inputs. This model declares neither; those belong to the temporal models in the repository, not to this one

Measured quality (this repository's own harness, not a vendor's)

From docs/parity.md - all val scenes (models/training-data/godot-v6-warp, 48 frames, 2.00x), on an NVIDIA GeForce RTX 4070 Ti, driver 610.88. Every arm below was driven from the scene's own frames and scored against that scene's own targets:

arm tier PSNR dB SSIM MS-SSIM LPIPS DISTS VMAF detail
bilinear 128x128->256x256 26.3563 0.87165 0.95257 0.1025 0.2732 36.793 0.2634
bicubic 128x128->256x256 25.7689 0.86524 0.94596 0.0936 0.2795 40.399 0.4258
lanczos 128x128->256x256 25.4109 0.85991 0.94447 0.1025 0.2846 40.912 0.4459
fsr1 (EASU + RCAS) 128x128->256x256 24.6755 0.84641 0.93548 0.0947 0.2958 42.255 0.7668
nrr - this model 128x128->256x256 26.4320 0.88151 0.95515 0.0570 0.1609 43.374 0.5616
DLSS (NGX) 128x128->256x256 26.6829 0.88217 0.96077 0.1047 0.2576 52.253 0.2378
XeSS 2.0.2 128x128->256x256 26.7147 0.87228 0.95586 0.1319 0.2718 31.847 0.1774

That is a comparison of these frames at this tier, which is what it is. No row here is a general claim about any upscaler, and a timing taken on a host doing other work is a timing on a busy host. The repository's docs/evaluation-protocol.md states the rules the table is held to.

The 2.5x tier (ratio25/)

ratio25/upscale_25x_procedural_depth.onnx upscales by 2.5 on both axes, and it is the first model in that repository trained at a ratio that is not a power of two and qualified at it by the repository's own gates - a model that fails one is refused, not exported with a warning next to it.

inputs color - the low-resolution frame; depth - per-pixel ray distance on the input's grid
output one frame, 2.5x the input on both axes (768x432 -> 1920x1080)
spatial dynamic H/W: the graph takes whatever size the input is
parameters 90,974 (369,025 bytes)
sha256 86326c5d7c4ca5b7b5e7b5e206b55de2909f80a7fee7d1938d3d661545c545d9
how the ratio is reached PixelShuffle(5) synthesises 25 polyphase components per colour channel, a learned stride-2 convolution selects the phase, and the skip is the 2.5x bilinear upsample - so nothing outside the graph resamples the frame, and an untrained model is that ratio's bilinear baseline

Measured on the run's own held-out split (120 procedural pairs, color being the noisy packed input the runtime supplies), with the 2.5x bilinear baseline scored on the same frames by the same code:

arm L1 SSIM PSNR dB MS-SSIM
bilinear at 2.5x 0.02542 0.8897 21.55 0.9427
this model 0.01058 0.9479 27.38 0.9739

58.37% below the baseline on the metric the training gates score (the bar is 5%), with training progress 58.9% and a depth ablation of 0.02742 - the gate that refuses an input the model does not actually use. Through the runtime at the tier: 224.6 ms/frame on an RTX 4070 Ti (CUDA provider, 102.1 ms of it inference) and 2104.3 ms on the same host's CPU execution provider; the benchmark seeds a synthetic depth ramp for depth-consuming models, so those rows measure speed and the quality numbers above come from the trainer's held-out split instead.

What is not claimed about the procedural model. Its data is generated (tools/gen_training_pairs.py --ratio 2.5: each scene rendered twice, at 128 and at 320, so the target is a real render at the ratio rather than a resample onto it), so its 58.37% is an in-family number: held-out pairs, but the same scenes, the same renderer and no held-out content. It is not comparable to the 2x table below either, which is measured on captured frames by a different harness at a different tier.

The same ratio trained on captured frames (ratio25/)

These are the out-of-family numbers: the same ratio, trained on frames captured from Godot - the content pipeline the released 2x model was trained on - over 840 pairs (360 train / 480 held out) of seven scenes the model has never seen.

model inputs parameters held-out L1 vs the 2.5x bilinear baseline SSIM PSNR dB MS-SSIM
ratio25/upscale_25x_captured_nj_depth_s2.onnx color, depth 90,974 0.01359 vs 0.01516 โ†’ 10.37% better 0.9035 vs 0.8947 26.81 vs 27.22 0.9715 vs 0.9624
ratio25/upscale_25x_captured_depthjitter.onnx color, depth, jitter 93,430 0.01399 vs 0.01541 โ†’ 9.24% better 0.9008 vs 0.8936 27.66 vs 27.10 0.9689 vs 0.9600

Three things a reader should know before choosing between them:

  • The color,depth model loses PSNR. It is 0.41 dB below the bilinear baseline (26.81 against 27.22) while gaining on L1, SSIM and MS-SSIM. The training gates score L1, and the column that disagrees is shown rather than dropped. The color,depth,jitter model is above the baseline on all four metrics.
  • The color,depth configuration is seed-fragile on captured content: 1 of 4 seeds qualified. One seed froze at the bilinear plane and then diverged (residual L1 0.01756 โ†’ 0.29974), one reached 1.9% progress with its output 0.000302 from the plane, one reached 2.5%. The published weights are the seed that qualified (20261112). The spread is part of the result - the procedural configuration in the section above qualified on its first seed with 58.37%.
  • The phase is what makes a jittered capture learnable. On the jittered capture - the low-resolution sample moving up to half a pixel between frames - color,depth measured val L1 equal to the baseline's to five decimals (0.01541 against 0.01541), with an output 0.000155 from the plane: a half-pixel misalignment the model was never told about has the bilinear plane as its L1 optimum. Declaring jitter on the same frames reached 9.24%, with both declared inputs measurably used (depth 0.00540, jitter 0.00695).

Both graphs are trained at 128 -> 320 and are dynamic in H/W, so both render 768x432 -> 1920x1080; the runtime rows are docs/measurements/ratio25-captured-* in the repository, and the training configuration, the captures and the packing script are there too.

One thing to know before comparing this model's output across hardware: ONNX Runtime's CUDA execution provider computes convolutions in TF32 by default, so this graph's CPU and CUDA outputs differ by up to 6.341e-04 (mean 1.558e-05) while agreeing to 2.176e-06 with use_tf32=0. The two recorded runtime rows therefore carry different frame hashes for a named reason rather than a mysterious one, and a re-run on one provider reproduces its hash exactly.

The 2.5x tier against the field (docs/parity-ratio25.md)

docs/parity-ratio25.md scores the 2.5x ratio against the offline field and one commercial arm, all driven from the same captured frames and scored against the same targets. It is a 2.5x table, so its offline baselines upscale by 2.5 (a 2x-tier table's baselines upscale by 2 - a row that says 768x768->1920x1920 while its bilinear arm upscaled by 2 would be scoring one grid and labelling it another):

arm tier PSNR dB SSIM MS-SSIM LPIPS DISTS detail
bilinear 768x768->1920x1920 27.1135 0.88770 0.94527 0.1168 0.2992 0.1330
bicubic 768x768->1920x1920 26.4337 0.87777 0.93720 0.1343 0.3095 0.2052
lanczos 768x768->1920x1920 26.1083 0.87372 0.93362 0.1432 0.3106 0.2178
fsr1 (EASU + RCAS) 768x768->1920x1920 25.6000 0.86508 0.92990 0.1402 0.3447 0.3919
nrr - the procedural model 768x768->1920x1920 26.9605 0.87093 0.94155 0.1332 0.3312 0.2919
DLSS (NGX) 768x768->1920x1920 26.9670 0.89735 0.95684 0.0934 0.2684 0.1567

24 frames, one held-out scene, RTX 4070 Ti, driver 610.88. What a reader should hold on to:

  • The grid is square, not 16:9. The capture renders a square viewport, so this tier is 768x768 -> 1920x1920 rather than the 768x432 -> 1920x1080 the model's README names. The ratio is the same, the crop is not; the graph is dynamic in H/W and renders either, so the measured table is the square one.
  • The procedural model loses to bilinear here, and that is the point of the section above. Its 58.37% is an in-family number from generated data; scored on captured frames it lands below its own bilinear baseline on PSNR (26.96 vs 27.11), SSIM and MS-SSIM, keeping only detail (0.2919 vs 0.1330). That is the out-of-family gap the captured models above close - upscale_25x_captured_depthjitter.onnx is the nrr row this table wants next.
  • XeSS has no 2.5x row. Its presets are fixed input sizes: balanced at a 1920 output wants 960, which is exactly 2.0x. The row is absent because XeSS refuses to produce one, not because it was dropped.
  • FSR 2/4 are absent by structure, not omission. FSR 4 is not distributed as a runtime this harness can call, and FSR 2/3/4 are temporal and consume an engine's motion/jitter/history planes, so their rows come from the engines that ship them.
  • VMAF is skipped (--skip-vmaf): evaluate_model's VMAF path writes into a temp directory that is not there, so this run omits the column rather than being quietly shortened.

The 16:9 model (rect/rect-model.onnx) - a plumbing proof, not a quality result

rect-model.onnx is not a released model and is not an alternative to the 2x graph above. It exists to answer one question: does a non-square canvas render end to end, or does something in the pipeline assume a square? It renders. tools/verify_rect_upscale.py in the NRR repository feeds a 160x90 frame through it and asserts the output is exactly (1, 3, 180, 320) - 2x on both axes, finite, no square reshape anywhere.

Read its numbers with that in mind, because they are worse than bilinear:

arm 160x90 -> 320x180, 6 held-out val pairs PSNR dB SSIM MS-SSIM
bilinear (the baseline) 18 captured pairs, 12 train / 6 val 28.7766 0.90028 0.96080
rect-model.onnx same split 28.7725 0.89984 0.96075

It was exported with --export-unqualified, and its own rect-model.report.json records the three gates it fails:

  • does not beat the baseline - val L1 0.011708 against the baseline's 0.011638, which is not 5% lower;
  • is the baseline - its output differs from the plane it corrects by only 0.000724, against a 0.0010 floor, so the network learned almost nothing;
  • ignores motion - zeroing the motion input changes the output by 0.000000, against a 0.0001 floor.

So: 112,907 parameters trained for 3 epochs (6.91 s on CPU) over 18 pairs from a single capture, which is a smoke test's worth of data. Its motion and depth ablations are near zero because the ablation, not the training, is the interesting part of the check - and verify_rect_upscale.py is what actually passes here.

It is published beside the released model for the same reason the 2.5x tier is: it is the artifact a claim in the repository points at, and a claim a reader cannot download is a claim they have to take on trust. Its provenance travels with it as rect/rect-model.report.json, which is gitignored in the repository itself.

Files

file bytes what it is
upscale_msreal_scale.onnx 461,501 the released model: the two-tier (scale token) upscaler described above
rect/rect-model.onnx 456,548 the 16:9 plumbing proof: 160x90 -> 320x180, inputs color, depth, motion, 112,907 parameters, exported --export-unqualified and below its bilinear baseline on all three image metrics - see the section above
rect/rect-model.report.json 3,714 its provenance, including the three release gates it fails
ratio25/upscale_25x_procedural_depth.onnx 369,025 the 2.5x model: 768x432 -> 1920x1080, inputs color, depth
ratio25/upscale_25x_procedural_depth.report.json 2,934 that model's gate report: the configuration, the sha256, the held-out numbers and the baselines they were scored against - the repository gitignores models/**/*.json, so this copy is the one that travels with the weights
ratio25/upscale_25x_captured_nj_depth_s2.onnx 369,025 the same ratio trained on captured frames: inputs color, depth, 10.37% below its ratio's bilinear baseline on 480 held-out captured pairs
ratio25/upscale_25x_captured_nj_depth_s2.report.json 2,955 its gate report, including the baselines every number was scored against
ratio25/upscale_25x_captured_depthjitter.onnx 384,090 the same capture with the phase declared: inputs color, depth, jitter, 9.24% better and above the baseline on all four metrics
ratio25/upscale_25x_captured_depthjitter.report.json 2,991 its gate report
phases/upscale_subsampled.onnx 424,038 the 128 -> 256 model docs/evaluation-protocol.md names when it explains why a model must be judged at the tier it was trained for
phases/upscale_mid.onnx 424,038 mid-training model, kept because the phase tables compare against it
phases/upscale_mid_regression.onnx 424,038 the mid model as a regression baseline
probes/*.onnx 790 - 45,082 the sample model and the small probe graphs the test suite and the plugin drift guards load (nrr_upscaler_v0.1, nrr_passthrough_2x, nrr_scale_token_probe, nrr_unrecognised_input_probe, nrr_history_probe)
tests/p4_tjit_20261025.onnx 499,854 the reference model the Unity verification project renders with: its tests score their output against engine_plugins/unity_verify/Assets/StreamingAssets/smoke_fixture/fixture.json, whose recorded reference means were produced by this exact file. The tests select the highest-dated models/p5/p4_tjit_*.onnx, so this is a real training checkpoint - one of the history, whose other 118 checkpoints are in SamurAI-Official/NRR-checkpoints

Using it

With NRR itself:

nrr_model_load(device, "upscale_msreal_scale.onnx", &model);
nrr_render(device, model, /*references=*/NULL, &input, &output);   /* the runtime derives scale from the width */

The third argument is the reference set, not the inputs: NRR hands a model the planes it declares, and takes them from the frame - NRRFrameInput.color / .depth / .motion_vectors, NRRTemporalState.jitter and .previous_output for a temporal model, and scale derived from the frame's width. That is the whole difference between the two contracts on this page. The released 2x model declares color, jitter, scale, which the runtime already has; the 2.5x model declares color and depth, so a caller has to populate input.depth:

nrr_model_load(device, "ratio25/upscale_25x_procedural_depth.onnx", &model);
nrr_render(device, model, /*references=*/NULL, &input, &output);   /* input.depth carries the frame's depth */

Fetching it into a checkout, which puts it exactly where the repository expects it (engine_plugins/unreal_verify/setup.ps1 and the test suite load it from there):

hf download SamurAI-Official/NRR upscale_msreal_scale.onnx --local-dir models/phase4

Anywhere else it is a plain ONNX graph:

import onnxruntime
session = onnxruntime.InferenceSession("upscale_msreal_scale.onnx")

Provenance, and what is not claimed

Trained in the NRR repository's own trainer on frames captured from Godot; the training configuration, the comparison harness, the plugins that load it and every number above live in that repository. The released model is not tracked by git there (models/phase4/ is working state), which is why it is here.

The 2.5x model in ratio25/ was trained in the same repository, by tools/run_ratio25.ps1 on data it generates rather than data it captured (tools/gen_training_pairs.py --ratio 2.5), and its gate report travels with it because the repository gitignores models/**/*.json - without that copy the artifact's provenance would live only in a changelog. The weights are tracked there, so the two copies can be compared byte for byte.

Verification status, stated because a model that loads is not a model that ran: NRR's Unreal project renders this model at three tiers headless and prints RESULT: PASS (engine_plugins/unreal_verify/README.md), and the Godot addon is verified end to end (CHANGELOG.md). Those runs are on a host where the CUDA execution provider crashes during model load - a host/driver result, recorded in that README - so the numbers there are CPU numbers.

One description in the repository does not match this model: models/architecture.md there still describes an aspirational 1080p -> 4K color+depth+motion network, which is not what is published here. The contract above is this graph's real one, read from the model.

Licence: All Rights Reserved - see LICENSE (Copyright (c) 2026 SamurAI-Official). The weights and this card are covered by the same terms as the repository; nothing here is granted under the MIT licence the repository originally carried.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support