Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
193d019
Upscale only the drawings and draw the in-betweens at 2x
Alitleis123 Nov 10, 2026
0446596
Draw and write frames in a pool while the GPU draws the next
Alitleis123 Nov 10, 2026
ad28a3e
Put renders beside Resolve's cache, a folder per project
Alitleis123 Nov 10, 2026
2a087c8
Give each test a home folder of its own
Alitleis123 Nov 10, 2026
85175c1
Cut the CPU work around each RIFE call
Alitleis123 Nov 10, 2026
5f152b5
Package standalone installers with automatic Python setup
Alitleis123 Nov 10, 2026
824d692
Give each render worker a share of OpenCV's threads
Alitleis123 Nov 10, 2026
40e8e57
Rebuild the release folders
Alitleis123 Nov 10, 2026
8349df8
Point the home page buttons at setup and drop the Python rerun advice
Alitleis123 Nov 10, 2026
4611595
Double trailing backslashes in Windows arguments
Alitleis123 Nov 10, 2026
9ae89ef
Give a project named like a Windows device a folder it can have
Alitleis123 Nov 10, 2026
f484414
Rebuild the release folders
Alitleis123 Nov 10, 2026
b24a793
Show what Upflow2x needs in the installers and the panel
Alitleis123 Nov 10, 2026
dccfb12
Rebuild the release folders
Alitleis123 Nov 10, 2026
01daf23
Give the Mac Python limits in the specs
Alitleis123 Nov 10, 2026
c536670
Rebuild the release folders
Alitleis123 Nov 10, 2026
78e592d
Merge branch 'fleet/upflow-automatic-setup-20261110' into release-0.5.1
Alitleis123 Nov 10, 2026
42a55d2
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
19a9c0d
Merge branch 'feat/specs-in-software' into release-0.5.1
Alitleis123 Nov 10, 2026
edee8a3
Say where renders go and how long they take in the README and help
Alitleis123 Nov 10, 2026
dc9a8e4
Walk through the unsigned-setup prompts and split the 50 MB download
Alitleis123 Nov 10, 2026
e985c31
Fall back to Resolve's own folder for renders, never OneDrive
Alitleis123 Nov 10, 2026
c53eabd
Rebuild the release folders
Alitleis123 Nov 10, 2026
0d09aa4
Check the folder renders go to really is outside OneDrive and writable
Alitleis123 Nov 10, 2026
20fba49
Rebuild the release folders
Alitleis123 Nov 10, 2026
6841fc9
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
7a12caa
Delete a render that stops or fails
Alitleis123 Nov 10, 2026
2d52b85
Rebuild the release folders
Alitleis123 Nov 10, 2026
e639db9
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
4f02441
Say that stopped and failed renders are deleted
Alitleis123 Nov 10, 2026
0c7ab0c
Only ever delete a render this run made
Alitleis123 Nov 10, 2026
92749b3
Rebuild the release folders
Alitleis123 Nov 10, 2026
0d154c5
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
76d0758
Let the panel save renders to a chosen folder
Alitleis123 Nov 10, 2026
ee89f08
Rebuild the release folders
Alitleis123 Nov 10, 2026
31976a8
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
8fd7cf9
Draw a video into a file of the run's own
Alitleis123 Nov 10, 2026
8894ba6
Rebuild the release folders
Alitleis123 Nov 10, 2026
e95a67e
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
d897df5
Keep a chosen drive root whole, and never refill Save to for nothing
Alitleis123 Nov 10, 2026
32e2fae
Rebuild the release folders
Alitleis123 Nov 10, 2026
9fc8f13
Merge branch 'fix/render-speed-storage' into release-0.5.1
Alitleis123 Nov 10, 2026
6e1ec77
Say only finished renders are never deleted
Alitleis123 Nov 10, 2026
ce35cea
Install the plugin in DaVinci Resolve's own folder
Alitleis123 Nov 10, 2026
4fde4a6
Rebuild the release folders
Alitleis123 Nov 10, 2026
7555f7e
Test the oldest Python Upflow2x supports, 3.10
Alitleis123 Nov 10, 2026
b82f435
Say where the log is for an install that kept its folder
Alitleis123 Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.9", "3.12"]
python: ["3.10", "3.12"]

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
Expand Down
45 changes: 44 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,45 @@ jobs:
pip install -r requirements.txt -r requirements-dev.txt
python -m pytest -q

publish:
installers:
needs: test
strategy:
matrix:
os: [windows-latest, macos-15-intel]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
if: runner.os == 'Windows'
with:
python-version: "3.13"
- name: Install universal Python for the Mac app
if: runner.os == 'macOS'
run: |
curl --fail --location --output "$RUNNER_TEMP/python.pkg" https://www.python.org/ftp/python/3.13.17/python-3.13.17-macos11.pkg
echo "3053592af66128269efab37fe7851a4894920b27c442aeec99ce4d34d17c07e3 $RUNNER_TEMP/python.pkg" | shasum -a 256 --check
sudo installer -pkg "$RUNNER_TEMP/python.pkg" -target /
echo "/Library/Frameworks/Python.framework/Versions/3.13/bin" >> "$GITHUB_PATH"
- name: Build Windows setup
if: runner.os == 'Windows'
run: |
python -m pip install pyinstaller==6.22.3
python Installer/build_installer.py
- name: Build Mac setup
if: runner.os == 'macOS'
run: |
python3 -m pip install pyinstaller==6.22.3
python3 Installer/build_installer.py
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: installer-${{ runner.os }}
path: |
dist/Upflow2xInstaller.exe
dist/Upflow2xInstaller-mac.zip
if-no-files-found: error

publish:
needs: [test, installers]
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -70,6 +107,12 @@ jobs:
with:
python-version: "3.12"

- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: installer-*
merge-multiple: true
path: dist

- name: Build, publish and verify
env:
GH_TOKEN: ${{ github.token }}
Expand Down
89 changes: 85 additions & 4 deletions Installer/Eternal2x.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ end

local function trim_trailing_sep(path)
if not path then return "" end
-- A drive or the disk's root keeps its separator. D: alone means the
-- folder that drive was last in, and / trimmed is nothing at all.
if path:match("^%a:[/\\]+$") or path:match("^[/\\]+$") then
return path:sub(1, path:match("^%a:") and 3 or 1)
end
local out = path:gsub("[/\\]+$", "")
return out
end
Expand Down Expand Up @@ -52,7 +57,10 @@ end
local function shell_quote(s)
if not s then return "" end
if is_windows() then
return '"' .. s:gsub('"', '""') .. '"'
-- A backslash before the closing quote escapes it, so a folder like
-- D:\Cache\ ran on into the next argument. Doubled, it stays one.
local body = s:gsub('"', '""'):gsub("(\\+)$", "%1%1")
return '"' .. body .. '"'
end
-- Single quotes, so $, backticks and backslashes in a path stay literal.
-- Inside double quotes the shell would expand `$HOME` in a file name, or
Expand Down Expand Up @@ -934,10 +942,18 @@ local TIPS = {
upscale = "Draw every frame at 2x with Real-ESRGAN's anime model, on the GPU on Windows. Without a GPU, Resolve's Super Scale does it.\nIt shows on a timeline bigger than the clip.",
interp = "Draw the missing in-betweens with the RIFE network, on the GPU on Windows. Its first use downloads about 50 MB.\nThe render plays at a third of the clip's speed, ready to time remap.\nOff keeps every drawing on its frame and the clip's length, and draws nothing, to upscale only.",
format = "Image sequence is lossless and imports most reliably. MP4 is one small file but lossy, and MKV is one lossless file. All are 8-bit RGB.",
-- The same list as Installer/install_upflow2x.py's REQUIREMENTS.
needs = "Upflow2x needs:\n"
.. "DaVinci Resolve Studio 18 or newer. The free edition blocks script panels.\n"
.. "Windows 10 or 11, or macOS 12 or newer.\n"
.. "A DirectX 12 GPU on Windows draws and upscales. Without one, and on a Mac, "
.. "the in-betweens run on the CPU and Super Scale upscales.\n"
.. "An internet connection for setup and the first render, which downloads about 50 MB.",
folder = "Where renders are saved, a folder for each project. By default that's an Upflow2x folder beside the project's Resolve cache, or Resolve's own folder. Choose a folder to keep them somewhere else, like a drive with more space.",
}

local function info(tip)
return ui:Label{ Text = "i", ToolTip = tip, StyleSheet = INFO, Weight = 0,
local function info(tip, id)
return ui:Label{ ID = id, Text = "i", ToolTip = tip, StyleSheet = INFO, Weight = 0,
Alignment = { AlignCenter = true },
MinimumSize = { 16, 16 }, MaximumSize = { 16, 16 } }
end
Expand Down Expand Up @@ -971,6 +987,7 @@ local win = disp:AddWindow({
ui:Label{ ID = "SubTitle", Text = "v" .. CURRENT_VERSION, StyleSheet = VERSION, Weight = 0,
Alignment = { AlignRight = true, AlignVCenter = true } },
ui:Button{ ID = "UpdateBtn", Text = "Check for Updates", StyleSheet = LINK, Weight = 0 },
info(TIPS.needs, "NeedsInfo"),
},

ui:Label{ ID = "SourceLabel", Text = "No clip under the playhead.", StyleSheet = CARD,
Expand All @@ -995,6 +1012,7 @@ local win = disp:AddWindow({
Checked = DO_INTERPOLATE, StyleSheet = TOGGLE, ToolTip = TIPS.interp },
},
row("Output", ui:ComboBox{ ID = "FormatCombo", Weight = 0.8 }, TIPS.format),
row("Save to", ui:ComboBox{ ID = "FolderCombo", Weight = 0.8 }, TIPS.folder),
ui:Button{ ID = "SmoothBtn", Text = RENDER_LABEL, StyleSheet = START, ToolTip = TIPS.render,
MinimumSize = { 0, 44 }, MaximumSize = { 16777215, 44 } },

Expand Down Expand Up @@ -1026,6 +1044,22 @@ local items = win:GetItems()
for _, label in ipairs(FORMAT_LABELS) do items.FormatCombo:AddItem(label) end
items.FormatCombo.CurrentIndex = index_of(FORMATS, FORMAT)

-- Save to: Resolve's folder, the folder chosen last, and a way to choose one.
-- Refilling it changes the selection, which is not a choice.
local FILLING_FOLDERS = false
local function fill_folders()
FILLING_FOLDERS = true
items.FolderCombo:Clear()
items.FolderCombo:AddItem("Resolve's folder")
if OUTPUT_DIR ~= "" then items.FolderCombo:AddItem(short_path(OUTPUT_DIR, 30)) end
items.FolderCombo:AddItem("Choose a folder...")
items.FolderCombo.CurrentIndex = OUTPUT_DIR ~= "" and 1 or 0
items.FolderCombo.ToolTip = OUTPUT_DIR ~= "" and OUTPUT_DIR
or "Beside the project's Resolve cache, or in Resolve's own folder."
FILLING_FOLDERS = false
end
fill_folders()

-- ---------------------------------------------------------------------------
-- state
-- ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1207,6 +1241,20 @@ local function mark_drawings(clip, marks)
return placed, shared
end

-- The open project's name and Resolve's cache folder, which decide where the
-- stage puts a render. Either can be missing, and the stage then falls back.
local function render_folder_hints()
local name, cache = "", ""
pcall(function()
local project = get_resolve():GetProjectManager():GetCurrentProject()
local got = project:GetName()
if type(got) == "string" then name = got end
got = project:GetSetting("perfCacheClipsLocation")
if type(got) == "string" then cache = got end
end)
return name, cache
end

local function current_args(extra)
local args = ""
if SOURCE_PATH and SOURCE_PATH ~= "" then
Expand Down Expand Up @@ -1237,7 +1285,16 @@ local function current_args(extra)
-- Each step doubles or halves how big a change counts, around the default.
args = args .. " --threshold " .. string.format("%.6f", 0.004 * 2 ^ ((THRESHOLD - 5) / 2)):gsub(",", ".")
end
if OUTPUT_DIR ~= "" then args = args .. " --output-dir " .. shell_quote(OUTPUT_DIR) end
-- Renders go in a folder per project: in the folder chosen under Save to,
-- or in one Upflow2x folder beside Resolve's cache, rather than among the
-- clips they came from.
local project, cache = render_folder_hints()
if project ~= "" then args = args .. " --project " .. shell_quote(project) end
if OUTPUT_DIR ~= "" then
args = args .. " --output-dir " .. shell_quote(OUTPUT_DIR)
elseif cache ~= "" then
args = args .. " --cache-dir " .. shell_quote(cache)
end
args = args .. " --status-file " .. shell_quote(STATUS_PATH)
return args .. (extra or "")
end
Expand Down Expand Up @@ -1916,6 +1973,30 @@ function win.On.FormatCombo.CurrentIndexChanged(ev)
save_conf()
end

function win.On.FolderCombo.CurrentIndexChanged(ev)
if FILLING_FOLDERS then return end
local index = items.FolderCombo.CurrentIndex or 0
-- Showing the folder already in use is no choice. Resolve may deliver
-- the refill's own events after it, and refilling for them would loop.
if index == (OUTPUT_DIR ~= "" and 1 or 0) then return end
if index == 0 then
OUTPUT_DIR = ""
elseif index == (OUTPUT_DIR ~= "" and 2 or 1) then
-- Fusion's folder dialog, which Resolve's scripts share. Cancelled,
-- it hands back nothing and the folder stays as it was.
local ok, chosen = pcall(function()
return global("fu"):RequestDir(OUTPUT_DIR ~= "" and OUTPUT_DIR or nil)
end)
if ok and type(chosen) == "string" and chosen ~= "" then
OUTPUT_DIR = trim_trailing_sep(chosen)
elseif not ok then
set_status("Resolve could not open a folder dialog here.")
end
end
save_conf()
fill_folders()
end

function win.On.UpscaleCB.Clicked(ev)
DO_UPSCALE = items.UpscaleCB.Checked
save_conf()
Expand Down
41 changes: 31 additions & 10 deletions Installer/build_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@

Produces:
dist/Upflow2xInstaller.exe (Windows)
dist/Upflow2xInstaller (macOS/Linux)
dist/Upflow2xInstaller-mac.zip (macOS, universal app)

The compiled exe should be placed in the root of the release zip
so buyers can double-click it alongside the plugin folder contents.
Each installer includes the plugin files. It can be run on its own.
"""

from __future__ import annotations
Expand All @@ -19,8 +18,15 @@

import subprocess
import sys
import tempfile
from pathlib import Path

try:
from Installer.build_release import PAYLOAD, _copy
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from Installer.build_release import PAYLOAD, _copy


def main() -> int:
repo_root = Path(__file__).resolve().parent.parent
Expand All @@ -46,23 +52,38 @@ def main() -> int:

cmd = [
sys.executable, "-m", "PyInstaller",
"--onefile",
"--onedir" if sys.platform == "darwin" else "--onefile",
"--windowed",
"--noconfirm",
"--paths", str(repo_root),
"--name", name,
"--distpath", str(repo_root / "dist"),
"--workpath", str(repo_root / "build"),
"--specpath", str(repo_root / "build"),
*icon_arg,
str(script),
]

print(f"Building {name}...")
print(f" Command: {' '.join(cmd)}\n")
result = subprocess.run(cmd)
if sys.platform == "darwin":
cmd += ["--target-arch", "universal2"]

with tempfile.TemporaryDirectory(prefix="upflow2x-setup-") as staging:
payload = Path(staging) / "plugin"
for item in PAYLOAD:
_copy(repo_root / item, payload / item)
cmd += ["--add-data", f"{payload}:plugin", str(script)]
print(f"Building {name}...")
result = subprocess.run(cmd)

if result.returncode == 0:
ext = ".exe" if sys.platform.startswith("win") else ""
output = repo_root / "dist" / f"{name}{ext}"
output = repo_root / "dist" / (name + ".exe" if sys.platform.startswith("win") else name)
if sys.platform == "darwin":
output = repo_root / "dist" / "Upflow2xInstaller-mac.zip"
result = subprocess.run([
"ditto", "-c", "-k", "--sequesterRsrc", "--keepParent",
str(repo_root / "dist" / f"{name}.app"), str(output),
])
if result.returncode:
return result.returncode
print(f"\nBuild complete: {output}")
print(f"Size: {output.stat().st_size / 1024 / 1024:.1f} MB")
else:
Expand Down
30 changes: 28 additions & 2 deletions Installer/build_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@

# What ships to users. Tests, release artefacts and dev tooling stay behind.
PAYLOAD = ["Pipeline", "Stages", "Installer", "VERSION", "README.md",
"requirements.txt"]
"requirements.txt", "LICENSE.md"]
INSTALLERS = ("Upflow2xInstaller.exe", "Upflow2xInstaller-mac.zip")
SKIP_DIRS = {"__pycache__", ".pytest_cache"}
SKIP_SUFFIXES = {".pyc", ".pyo"}

Expand Down Expand Up @@ -95,10 +96,35 @@ def verify_feed(meta: dict) -> bool:
return ok


def verify_installers(version: str) -> bool:
"""Check that the standalone downloads contain the binaries just built."""
import urllib.error
import urllib.request

for name in INSTALLERS:
url = f"https://github.com/{REPO}/releases/download/{release_tag(version)}/{name}"
try:
with urllib.request.urlopen(url, timeout=60) as response:
digest = hashlib.sha256(response.read()).hexdigest()
except (urllib.error.URLError, OSError) as exc:
print(f" {name}: FAILED, {exc}")
return False
if digest != sha256(REPO_ROOT / "dist" / name):
print(f" {name}: checksum mismatch against the published file")
return False
return True


def publish(version: str) -> bool:
"""Create the GitHub release, with the tag derived from VERSION."""
tag = release_tag(version)
archives = [str(REPO_ROOT / "release" / f"{name}.zip") for name in PLATFORMS]
installers = [REPO_ROOT / "dist" / name for name in INSTALLERS]
missing = [path.name for path in installers if not path.is_file()]
if missing:
print(" Missing standalone installers: " + ", ".join(missing))
return False
archives.extend(str(path) for path in installers)
existing = subprocess.run(["gh", "release", "view", tag, "--repo", REPO],
capture_output=True, text=True)
if existing.returncode == 0:
Expand Down Expand Up @@ -241,7 +267,7 @@ def main() -> int:
if not publish(version):
return 1
print("Verifying the published downloads")
if not verify_feed(meta):
if not verify_feed(meta) or not verify_installers(version):
print("The published downloads do not match this build, so "
"update/latest.json was left untouched. If shipped files changed "
f"since {tag} went out, bump VERSION and release again.")
Expand Down
Loading
Loading