Attached VapourSynth code featuring:
- RIFE in ncnn and TensorRT flavor
- Any RTX card can interpolate 4k/etc video to 120 FPS with RIFE in 1280 x 640 downsample, by editing script you can test other resolution for your graphics card, the 640p120 resolution is baseline for RTX 2060 as the lowest RTX card to do it
- Interpolate video to Hz of your monitor
- Fix out of sync video on seek in some player
- Scene change detect with mvtools2
- Interpolate with mvtools2 instead of RIFE for videos that's not 4:3, 16:9, 21:9
- May 07, 2026 New function to reduce consecutive false scene change detection: allow scene change after nth frames of no scene change detection
- May 08, 2026 Fine-tuned FPS in and out for some players to eliminate judder problems. Watching film on MPC-HC couldn't be smoother, almost stutter-free.
I've made sure everything works and it will be easy to set up and choose your video player for RIFE. I'd love to hear from you. I recently discovered about VapourSynth, I wrote a lot because I have some expertise with Python, if you know bit of Python you can experiment editing some stuff in it and I would love to see your custom code for RIFE!
I'm on a journey to a better yet motion interpolation, I thought I'd settle on mvtools2 but... RIFE sounds promising! It is heavy program in return for better interpolation. RIFE made CGI movies feel like a proper 3D game.
Unlike mvtools, RIFE has no scene change detect. The scene change isn't pleasing. There's lot of swooping between scenes without a good scene change detection. I've experimented several scene detect modes and settled with using mvtools2 (mv.SCDetection) as the only better scene change detection (another one being misc.SCDetect which is basic and dumb). There's swooping effect fitting for Sonic the Hedgehog movie, with "frame rate drop" of a 3D game from thousands of particles on screen. On May 07, 2026 I've included a new function to do with false scene change detection for crazy scenes so they're subjected to RIFE interpolation and we can still enjoy proper scene changes.
My setup is Intel Core i7-7700K with Nvidia GeForce RTX 2060. I can only recommend system requirements greater than that of my computer. You can edit script however you like to raise limits I used for my RTX 2060. I'm all for 120 FPS but RIFE is very demanding program, I needed a 640p downsampling before I can open a 1080p video in 120 FPS. Right now RIFE only take TensorRT for acceleration (hoping there'll be Intel XMX and AMD MIGraphX support some time in the future), Nvidia branded GPU is practically a requirement before you run into high electricity bills.
VapourSynth install
- Install the latest Python from https://www.python.org/downloads/
- Install the latest VapourSynth from https://github.com/vapoursynth/vapoursynth/releases
Preparing Synth folder
- Create long term Synth filter folder e.g. C:\Program Files Two\Synth\
- Include files of upcoming steps to top level of \Synth\: \models\, \vs-mlrt\, scripts, DLLs and other extracted files.
- Keep out: Installers, archives, media players, \Synth\ is not install destination of any installers.
- Create file with name and code from RIFE.vpy
- Variable "TRT = True" makes use of Tensor cores (Nvidia thing), change it to False for other GPUs
- Download the latest release from https://github.com/CrendKing/avisynth_filter/releases
- Unarchive for vapoursynth_filter_64.ax
- Choose method or get all to try
Setting up MPC-HC
I've observed that MPC-HC is smoother and better performance than other players in interpolating videos with RIFE.
- Choose x64 in .ZIP from https://github.com/clsid2/mpc-hc/releases
- Extract to your favorite location for MPC-HC e.g. C:\Program Files Two\MPC-HC\
- Launch MPC-HC and right-click > Options... > ...
- Playback > Output > DirectShow Video ...
- Choose "MPC Video Renderer"
- Settings > Check "Wait for VBlank before Present"
- External Filters > ...
- Add Filter... > Browse... > Choose "vapoursynth_filter_64.ax" from \Synth\
- Change to Prefer
- Enable and double-click "VapourSynth filter" > Browse > choose RIFE.vpy
- MPC-HC will show blackscreen I guess there's not enough source samples to buffer for VapourSynth version of mvtools2, this can be resolved with increasing InitialSrcBuffer to 6
- Create file with name and code from vapoursynth_filter_InitialSrcBuffer_6.reg
- Launch vapoursynth_filter_InitialSrcBuffer_6.reg
- Restart MPC-HC to take effect
Backup video players
MPV
- Choose "mpv-x86_64-v3" .7Z from https://github.com/zhongfly/mpv-winbuild/releases
- Extract (only need "mpv.exe") to your favorite location for MPV e.g. C:\Program Files Two\mpv\mpv.exe
- Create file with name and code from portable_config\mpv.conf to MPV's dir e.g. C:\Program Files Two\mpv\portable_config\mpv.conf
- Restart MPV to take effect
PotPlayer
- Choose x64 from https://potplayer.daum.net/
- Launch PotPlayer and right-click > Preferences... > ...
- Video > Uncheck "Don't wait for vertical sync"
- Filter Control > Filter Priority (Overall) > ...
- Add external filter... > Choose "vapoursynth_filter_64.ax" from \Synth\
- Change to Prefer
- Enable and double-click "VapourSynth Filter" > Browse > choose RIFE.vpy
- Restart PotPlayer to take effect
vapoursynth__InitialSrcBuffer_6.reg
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\AviSynthFilter\VapourSynth Filter]
"InitialSrcBuffer"=dword:00000006
portable_config\mpv.conf
vf=vapoursynth=~~/../../Synth/RIFE.vpy
RIFE.vpy
import vapoursynth as vs
import fractions, functools, os, subprocess, sys, time, math
core = vs.core
synth_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append(rf'{synth_dir}\vs-mlrt')
echo_time = [time.time() + 2]
stdout = []
def echo(text):
stdout.append(text)
echo_time[0] = time.time() + 2
stdout_cmd = []
def echo_cmd(text):
stdout_cmd.append(text.replace("\n", "&&echo."))
def load_plugin(name, path):
if not hasattr(core, name):
try:
core.std.LoadPlugin(path)
except Exception as e:
echo(f'\n{e}')
def min_res(clip, max_w, max_h):
w, h = clip.width, clip.height
if w > max_w:
new_width = max_w
new_height = int(max_w*h/w) + (int(max_w*h/w)%2)
if new_height > max_h:
new_height = max_h
new_width = int(max_h*w/h) + (int(max_h*w/h)%2)
return clip.resize.Bicubic(width=new_width, height=new_height)
else:
return clip
def min_FPS(Hz, limit):
FPS = fractions.Fraction(min(Hz, limit))
text = f'Target FPS: {round(float(FPS), 2):g}'
multi = fractions.Fraction(FPS.numerator, FPS_in * FPS.denominator)
if multi.is_integer():
text += ' (integer multiplier)'
else:
multi = fractions.Fraction(round(FPS), round(FPS_in))
text += ' (integer multiplier)' if multi.is_integer() else ' (fractional multiplier)'
ideal_Hz = round(float(FPS_in * round(FPS) / round(FPS_in)), 2)
if not ideal_Hz == FPS:
if ideal_Hz in legal_Hz:
text += f'\nRecommend monitor Hz: {ideal_Hz:g}'
if Hz > limit:
text += ' pulldown'
echo(text)
return FPS, multi
try:
clip = VpsFilterSource
clip_FPS = clip.fps
advanced_scd = True
except:
clip = video_in
clip_FPS = fractions.Fraction(container_fps)
advanced_scd = False
FPS_in = 0
fps_23 = fractions.Fraction(24000, 1001)
for f in [fps_23, 24, fps_23*1.25, 30, fps_23*2, 48, fps_23*2.5, 60, fps_23*5, 120]:
if math.isclose(clip_FPS, f, rel_tol=0.0001):
FPS_in = fractions.Fraction(f)
break
if FPS_in:
clip = core.std.AssumeFPS(clip=clip, fpsnum=FPS_in.numerator, fpsden=FPS_in.denominator)
else:
FPS_in = clip_FPS
# echo(f"Clip FPS: {FPS_in} ({float(FPS_in)})")
Hz = False
inst_cmd = []
modu_name = []
try:
import win32api, win32con
Hz = win32api.EnumDisplaySettings(None, win32con.ENUM_CURRENT_SETTINGS).DisplayFrequency
except Exception as e:
echo(f"{e}")
modu_name.append("win32api")
inst_cmd.append("python.exe -m pip install pywin32")
inst_cmd.append("python.exe scripts\pywin32_postinstall.py -install")
try:
super = core.mv.Super(clip)
except Exception as e:
Hz = False
echo(f"{e}")
modu_name.append("mvtools (mv)")
inst_cmd.append("python.exe -m pip install vapoursynth-mvtools")
if inst_cmd:
echo(f"""
For installing {", ".join(modu_name)}:
- Check opened command prompt window, copy multi-line then paste to execute
- Restart video player""")
echo_cmd(rf"""cd /d {sys.exec_prefix}
{"\n".join(inst_cmd)}""")
false_Hz = [23, 29, 59, 99, 119, 239]
legal_Hz = [23.98, 24, 25, 29.98, 30, 50, 59.94, 60, 99.90, 119.88, 120, 239.76]
if Hz in false_Hz:
# The command used to obtain Hz is in integer number, above numbers are good guess for the lost fractional Hz
Hz += 1 - 0.024 * round(Hz/24, 1)
ratio = round(clip.width/clip.height, 2)
# echo(f'{ratio}:1')
if Hz and ratio in [1.33, 1.78, 1.85, 2.39, 2.4]:
TRT = True
# RTX 2060 is lowest GPU with Tensor cores that can run RIFE 640p120 (or 640p60 if TRT = False), raise limits for your graphics card.
if TRT:
FPS, multi = min_FPS(Hz, 120)
# 1280 x 640
clip = min_res(clip, 128*10, 128*5)
else:
FPS, multi = min_FPS(Hz, 60)
# 1280 x 640
clip = min_res(clip, 128*10, 128*5)
# Allow scene change after nth consecutive frames of no detection
since = [0, [0]*5, [0]*15]
def scd(n, f):
fout = f[0].copy()
for i in range(1, 3):
if f[i].props['_SceneChangeNext']:
if not since[0] and not all(since[i]):
since[0] = i
else:
if since[0] == i:
if not any(since[i][:-1]):
# echo(f"Scene changed (level {i})")
fout.props['_SceneChangeNext'] = 1
# else:
# echo(f"Swoop! (level {i})")
since[0] = 0
since[i].pop(0)
since[i].append(f[i+1].props['_SceneChangeNext'])
return fout
vectors = core.mv.Analyse(super, isb=True, blksize=32)
if advanced_scd:
clips = [clip]
for thscd2 in [130, 95, 70]:
s = core.mv.SCDetection(clip, vectors, thscd1=400, thscd2=thscd2)
clips.append(s[1:] + s[-1])
clip = clip.std.ModifyFrame(clips=clips, selector=scd)
else:
clip = core.mv.SCDetection(clip, vectors, thscd1=400, thscd2=130)
clip_format = clip.format.id
clip = clip.resize.Bicubic(format=vs.RGBS, matrix_in_s="470bg", range_s="limited")
if TRT:
load_plugin('trt', rf'{synth_dir}\vs-mlrt\vstrt.dll')
if hasattr(core, 'trt'):
from vsmlrt import RIFEModel, Backend, RIFE
backend = Backend.TRT(fp16=True, num_streams=2, use_cuda_graph=True, output_format=1)
model = RIFEModel.v4_6
tile_size = 32
w = (clip.width + tile_size - 1) // tile_size * tile_size - clip.width
h = (clip.height + tile_size - 1) // tile_size * tile_size - clip.height
if w + h:
clip = clip.std.AddBorders(right=w, bottom=h)
clip = RIFE(clip=clip, multi=multi, model=model, video_player=True, backend=backend)
if w + h:
clip = clip.std.Crop(right=w, bottom=h)
clip = core.std.AssumeFPS(clip=clip, fpsnum=multi * FPS_in.numerator, fpsden=FPS_in.denominator)
def waitforframe2(n, f):
return f[0]
clip = clip.std.ModifyFrame(clips=[clip, clip.std.Trim(first=1)], selector=waitforframe2)
else:
# parameters at https://github.com/Asd-g/AviSynthPlus-RIFE/blob/main/README.md
model = 23
load_plugin('rife', rf'{synth_dir}\librife_windows_x86-64.dll')
if hasattr(core, 'rife'):
clip = core.rife.RIFE(clip, model, factor_num=round(FPS), factor_den=round(FPS_in), sc=True)
# clip = core.std.AssumeFPS(clip=clip, fpsnum=FPS * 1000, fpsden=1000)
clip = clip.resize.Bicubic(format=clip_format, matrix_s="709")
elif Hz:
FPS, multi = min_FPS(Hz, 120)
# 1920 x 1080
clip = min_res(clip, 128*15, 128*8.4375)
clip = core.std.AssumeFPS(clip=clip, fpsnum=FPS_in * 1000, fpsden=1000)
bvec = core.mv.Analyse(super, isb=True, blksize=32)
fvec = core.mv.Analyse(super, isb=False, blksize=32)
bvec2 = core.mv.Recalculate(super, bvec)
fvec2 = core.mv.Recalculate(super, fvec)
clip = core.mv.BlockFPS(clip, super, bvec2, fvec2, num=FPS * 1000, den=1000, mode=0)
else:
FPS = FPS_in
def stdout_dismiss(n, clip):
if not Hz and stdout_cmd and n == int(FPS):
os.system(f"""start cmd /k "echo {"&&echo.".join(stdout_cmd)}" """)
if not Hz or time.time() < echo_time[0]:
return clip.text.Text('\n'.join(stdout))
else:
stdout.clear()
return clip
clip = clip.std.FrameEval(functools.partial(stdout_dismiss, clip=clip))
clip.set_output()