Skip to content

Commit 3458468

Browse files
committed
feat(fetch): multi-source download fallback with mirrors; fetch --runtime all; DOWNLOADS.md manual guide — v0.2.1
1 parent 905f045 commit 3458468

10 files changed

Lines changed: 281 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project are documented here. The format follows
55
[SemVer](https://semver.org/) with a `0.x` experimental prefix. Evidence links
66
point to files in this repository (`docs/`) so the trail is auditable offline.
77

8+
## [0.2.1] — 2026-09-29
9+
10+
### Added
11+
12+
- **Multi-source download fallback** — `fetch` now tries the official
13+
`raw.githubusercontent.com` URL first, then the equivalent `github.com` raw
14+
link, and only then community mirrors of the same URLs (default
15+
`gh-proxy.com`/`ghproxy.net`, overridable via `DLSS_COMBO_MIRRORS`). All
16+
failures list every source tried. We still never redistribute upstream files
17+
(`src/dlss_combo/fetch.py`, `tests/test_fetch_sources.py`).
18+
- **`fetch --runtime all`** — one command downloads both DLSS-G runtimes
19+
(310.9 + 310.1) plus the Swapper portable (`src/dlss_combo/cli.py`,
20+
`tests/test_fetch_all.py`).
21+
- **[DOWNLOADS.md](DOWNLOADS.md)** — manual download & placement guide on the
22+
repo page: direct upstream links for every file, why NVIDIA SDK terms forbid
23+
bundling them into our zips, mirror configuration, integrity notes.
24+
825
## [0.2.0] — 2026-09-29
926

1027
### Added

‎DOWNLOADS.md‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Downloads & manual file placement
2+
3+
[← Back to the project](README.md)
4+
5+
`dlss-combo fetch` downloads everything below automatically, verifies it, and
6+
caches it. **This page is the manual fallback** for when scripted downloads are
7+
impossible. It is also the honest answer to "why isn't everything in one zip":
8+
9+
> The frame-generation DLLs embed **NVIDIA's DLSS-G runtime**, and the Swapper
10+
> portable app contains NVIDIA's neural-rendering library. NVIDIA's SDK terms do
11+
> not allow us to redistribute those binaries inside our release packages, so
12+
> this project links to the **upstream official channels** and fetches from them
13+
> at install time. We never re-host or modify upstream files.
14+
15+
## 1. Frame-generation kit — [sdli1995/dlssg_for_sm86](https://github.com/sdli1995/dlssg_for_sm86)
16+
17+
GPLv3 source (license declared in the upstream README); the embedded NVIDIA
18+
runtime inside these DLLs is NVIDIA's, not ours or upstream's to relicense.
19+
20+
Download these files from the repository (paths below are on `main`; `fetch`
21+
pins the exact commit and records it in `kit.json`):
22+
23+
| Kit file | Runtime 310.9 (repo root) | Runtime 310.1 (repo `310.1/`) |
24+
|---|---|---|
25+
| main proxy | [`version.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/version.dll) | [`310.1/version.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/310.1/version.dll) |
26+
| INI template | [`dlssg_sm86.ini`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/dlssg_sm86.ini) | [`310.1/dlssg_sm86.ini`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/310.1/dlssg_sm86.ini) |
27+
| alternative names | [`alternatives/winmm.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/alternatives/winmm.dll) · [`d3d12.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/alternatives/d3d12.dll) · [`dbghelp.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/alternatives/dbghelp.dll) · [`dinput8.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/alternatives/dinput8.dll) · [`dxgi.dll`](https://github.com/sdli1995/dlssg_for_sm86/raw/main/alternatives/dxgi.dll) | same names under [`310.1/alternatives/`](https://github.com/sdli1995/dlssg_for_sm86/tree/main/310.1/alternatives) |
28+
29+
Place them so your kit directory looks like (default kit: `~/dlss-combo-kit`):
30+
31+
```text
32+
<kit>/dlssg/310.9/version.dll
33+
<kit>/dlssg/310.9/dlssg_sm86.ini
34+
<kit>/dlssg/310.9/alternatives/{winmm,d3d12,dbghelp,dinput8,dxgi}.dll
35+
<kit>/dlssg/310.1/… (same layout)
36+
```
37+
38+
## 2. Image layer — [DLSS5-Swapper](https://github.com/rakanki911/DLSS5-Swapper) (MIT app)
39+
40+
Download the **portable** build from its
41+
[latest release](https://github.com/rakanki911/DLSS5-Swapper/releases/latest)
42+
(`DLSS5Swapper-*-portable.exe`; ignore the Setup installer), then either run it
43+
directly or let the kit manage it:
44+
45+
```text
46+
<kit>/swapper/DLSS5Swapper-<version>-portable.exe
47+
```
48+
49+
## 3. The toolkit itself
50+
51+
Prebuilt `dlss-combo` binaries for Windows x64 and Linux x64 are on our
52+
[Releases](releases/latest) page with `SHA256SUMS.txt` — that part is our code
53+
(MIT) and we do ship it.
54+
55+
## Slow or blocked GitHub? Mirrors
56+
57+
`fetch` tries official sources first (`raw.githubusercontent.com`, then
58+
`github.com/<repo>/raw/…`) and only falls back to community mirrors of those
59+
same URLs afterwards. You can set your own mirror list:
60+
61+
```bash
62+
export DLSS_COMBO_MIRRORS="https://your-mirror.example/,https://another.example/"
63+
```
64+
65+
Default mirrors: `https://gh-proxy.com/`, `https://ghproxy.net/` — they proxy
66+
the official URLs above; files are still hash-recorded locally on download
67+
(`kit.json`, local integrity baseline — upstream publishes no checksums for
68+
these assets, and mirrors are third parties, so treat official sources as the
69+
source of truth when reachable).
70+
71+
## Integrity
72+
73+
Every file the tool installs is SHA256-recorded at download time and re-verified
74+
on every later run (`doctor`, reinstall, launch). Manual downloads are accepted
75+
by `fetch` as cache hits only after the same checks.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
[Compatibility](#will-it-work-with-my-setup) · [FAQ](#frequently-asked-questions) · [Known issues](docs/STATUS.md) · [Contribute](CONTRIBUTING.md) · [Changelog](CHANGELOG.md) · [Dev log](docs/devlog/2026-09-20.md)
3030

31-
*Prebuilt Windows EXE and Linux binaries ship on the [Releases](../../releases) page — or run from source with Python 3.10+. No upstream DLLs are bundled; components are fetched and checksum-verified at install time.*
31+
*Prebuilt Windows EXE and Linux binaries ship on the [Releases](../../releases) page — or run from source with Python 3.10+. No upstream DLLs are bundled; components are fetched and checksum-verified at install time. See [Downloads & manual placement](DOWNLOADS.md).*
3232

3333
## Why this project?
3434

‎README.zh-CN.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Target environment: **Windows 10/11 x64, RTX 20 or RTX 30**, and a D3D12 game wi
7979

8080
## Getting started / 开始使用
8181

82+
所有上游文件由工具自动下载(GitHub 访问不畅时自动回退镜像源,可用 `DLSS_COMBO_MIRRORS` 自定义);也可按 [手动下载与放置指南](DOWNLOADS.md) 自备文件。
83+
84+
8285
The commands below are for experimental testing. Read the known issues and keep an independent backup before writing to any game directory. Not sure which game to start with? See [Recommended test games / 推荐测试游戏](docs/TEST-GAMES.md)(英文 / English).
8386

8487
以下命令供实验测试使用。向游戏目录写入前,请先阅读已知问题并保留独立备份。不知从哪款游戏开始?参考[推荐测试游戏列表](docs/TEST-GAMES.md)(英文)。

‎src/dlss_combo/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
"""dlss-combo: DLSS 5 画质 + DLSS 帧生成 组合安装编排器。"""
2-
__version__ = "0.2.0"
2+
__version__ = "0.2.1"

‎src/dlss_combo/cli.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _build_parser() -> argparse.ArgumentParser:
2222

2323
pf = sub.add_parser("fetch", help="download/update the kit cache (upstream binaries)")
2424
pf.add_argument("--kit-dir", type=Path, default=DEFAULT_KIT)
25-
pf.add_argument("--runtime", choices=["310.9", "310.1"], default="310.9")
25+
pf.add_argument("--runtime", choices=["310.9", "310.1", "all"], default="310.9")
2626
pf.add_argument("--refresh", action="store_true", help="force re-download")
2727

2828
pi = sub.add_parser("install", help="install the frame-gen layer into a game directory")
@@ -78,8 +78,10 @@ def main(argv: list[str] | None = None) -> int:
7878
if args.command == "fetch":
7979
from .fetch import fetch_kit, fetch_swapper
8080

81-
kit = fetch_kit(args.kit_dir, runtime=args.runtime, refresh=args.refresh)
82-
print(f"dlssg kit ready: {kit.root} (commit {kit.dlssg_commit})")
81+
runtimes = ("310.9", "310.1") if args.runtime == "all" else (args.runtime,)
82+
for runtime in runtimes:
83+
kit = fetch_kit(args.kit_dir, runtime=runtime, refresh=args.refresh)
84+
print(f"dlssg kit ready: {kit.root} (commit {kit.dlssg_commit})")
8385
try:
8486
sw = fetch_swapper(args.kit_dir, refresh=args.refresh)
8587
print(f"DLSS5-Swapper portable ready: {sw.zip_path} ({sw.tag})")

‎src/dlss_combo/fetch.py‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,58 @@
2020
DLSSG_REPO = "sdli1995/dlssg_for_sm86"
2121
RAW_BASE = f"https://raw.githubusercontent.com/{DLSSG_REPO}"
2222
COMMIT_API = f"https://api.github.com/repos/{DLSSG_REPO}/commits/main"
23+
24+
# 社区镜像(代理 GitHub 官方渠道,仅作最后回退;DLSS_COMBO_MIRRORS 环境变量可覆盖,逗号分隔)
25+
DEFAULT_MIRRORS: tuple[str, ...] = ("https://gh-proxy.com/", "https://ghproxy.net/")
26+
_MIRROR_ENV = "DLSS_COMBO_MIRRORS"
27+
28+
29+
def _active_mirrors() -> tuple[str, ...]:
30+
raw = os.environ.get(_MIRROR_ENV, "")
31+
if not raw.strip():
32+
return DEFAULT_MIRRORS
33+
return tuple(m for m in (s.strip() for s in raw.split(",")) if m)
34+
35+
36+
def alternate_urls(url: str) -> list[str]:
37+
"""同内容的备用下载源:官方 github.com 原始文件链接优先,社区镜像殿后。
38+
只处理本工具自己用的 raw.githubusercontent.com 仓库文件与 github.com release
39+
资产,其余宿主一律返回空(不做任意 URL 代理)。"""
40+
if url.startswith(RAW_BASE):
41+
rest = url[len("https://raw.githubusercontent.com/"):]
42+
owner, repo, ref, path = rest.split("/", 3)
43+
alt = f"https://github.com/{owner}/{repo}/raw/{ref}/{path}"
44+
return [alt] + [m + url for m in _active_mirrors()]
45+
if url.startswith("https://github.com/") and "/releases/download/" in url:
46+
return [m + url for m in _active_mirrors()]
47+
return []
48+
49+
50+
def fetch_with_fallback(
51+
url: str,
52+
opener: Callable[[str], bytes] | None = None,
53+
) -> bytes:
54+
"""按「官方源 → 官方备用 → 镜像」顺序取回;全部失败时列出所有尝试过的源。"""
55+
fetch = opener or _urlopen_bytes
56+
attempts: list[str] = []
57+
last_error: Exception | None = None
58+
for candidate in [url, *alternate_urls(url)]:
59+
attempts.append(candidate)
60+
try:
61+
return fetch(candidate)
62+
except Exception as e: # noqa: BLE001 — 每源失败换下一个,最后统一上报
63+
last_error = e
64+
tried = "\n ".join(attempts)
65+
raise RuntimeError(
66+
f"all download sources failed for {url} — tried primary and alternate sources:\n {tried}\n"
67+
f"last error: {type(last_error).__name__}: {last_error}"
68+
)
69+
70+
71+
def _urlopen_bytes(url: str) -> bytes:
72+
req = urllib.request.Request(url, headers={"User-Agent": "dlss-combo/0.2"})
73+
with urllib.request.urlopen(req, timeout=120) as resp:
74+
return resp.read()
2375
ALT_NAMES = ["winmm", "d3d12", "dbghelp", "dinput8", "dxgi"]
2476
KIT_JSON = "kit.json"
2577
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
@@ -47,9 +99,7 @@ def kit_files(runtime: str) -> dict[str, str]:
4799

48100

49101
def _default_fetch(url: str) -> bytes:
50-
req = urllib.request.Request(url, headers={"User-Agent": "dlss-combo/0.1"})
51-
with urllib.request.urlopen(req, timeout=120) as resp:
52-
return resp.read()
102+
return fetch_with_fallback(url)
53103

54104

55105
def _resolve_commit(fetch_bytes: Callable[[str], bytes]) -> str:

‎tests/test_fetch_all.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""`fetch --runtime all` downloads both runtimes plus the Swapper in one command."""
2+
import json
3+
4+
from dlss_combo.cli import main
5+
from dlss_combo.fetch import KitInfo, SwapperInfo
6+
7+
8+
def test_cli_fetch_runtime_all_fetches_both_runtimes(tmp_path, monkeypatch, capsys):
9+
import dlss_combo.fetch as fetch_mod
10+
11+
kit_calls: list[str] = []
12+
13+
def fake_fetch_kit(kit_dir, runtime="310.9", refresh=False, fetch_bytes=None):
14+
kit_calls.append(runtime)
15+
return KitInfo(root=kit_dir / "dlssg" / runtime, dlssg_commit="c" * 40, sha256={})
16+
17+
def fake_fetch_swapper(kit_dir, refresh=False, fetch_bytes=None):
18+
return SwapperInfo(tag="v2.2.8", zip_path=kit_dir / "swapper" / "p.exe")
19+
20+
monkeypatch.setattr(fetch_mod, "fetch_kit", fake_fetch_kit)
21+
monkeypatch.setattr(fetch_mod, "fetch_swapper", fake_fetch_swapper)
22+
23+
rc = main(["fetch", "--kit-dir", str(tmp_path), "--runtime", "all"])
24+
25+
assert rc == 0
26+
assert kit_calls == ["310.9", "310.1"]
27+
out = capsys.readouterr().out
28+
assert "310.9" in out and "310.1" in out
29+
30+
31+
def test_cli_fetch_runtime_single_stays_single(tmp_path, monkeypatch):
32+
import dlss_combo.fetch as fetch_mod
33+
34+
kit_calls: list[str] = []
35+
36+
def fake_fetch_kit(kit_dir, runtime="310.9", refresh=False, fetch_bytes=None):
37+
kit_calls.append(runtime)
38+
return KitInfo(root=kit_dir / "dlssg" / runtime, dlssg_commit="c" * 40, sha256={})
39+
40+
monkeypatch.setattr(fetch_mod, "fetch_kit", fake_fetch_kit)
41+
monkeypatch.setattr(fetch_mod, "fetch_swapper",
42+
lambda k, refresh=False, fetch_bytes=None: SwapperInfo("", k))
43+
44+
rc = main(["fetch", "--kit-dir", str(tmp_path), "--runtime", "310.1"])
45+
assert rc == 0
46+
assert kit_calls == ["310.1"]

‎tests/test_fetch_sources.py‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Multi-source download fallback: official sources first, community mirrors last,
2+
every failure surfaces what was tried. We never redistribute upstream files — we
3+
only fetch them from upstream's own channels (mirrors proxy those channels)."""
4+
import pytest
5+
6+
from dlss_combo.fetch import DEFAULT_MIRRORS, alternate_urls, fetch_with_fallback
7+
8+
RAW = "https://raw.githubusercontent.com/sdli1995/dlssg_for_sm86/abc123def/version.dll"
9+
ASSET = (
10+
"https://github.com/rakanki911/DLSS5-Swapper/releases/download/v2.2.8/"
11+
"DLSS5Swapper-2.2.8-portable.exe"
12+
)
13+
14+
15+
def test_alternate_urls_for_raw_file_github_raw_then_mirrors():
16+
alts = alternate_urls(RAW)
17+
assert alts[0] == "https://github.com/sdli1995/dlssg_for_sm86/raw/abc123def/version.dll"
18+
for mirror in DEFAULT_MIRRORS:
19+
assert any(a.startswith(mirror) for a in alts), mirror
20+
# official alternate comes before every mirror
21+
for a in alts[1:]:
22+
assert a.startswith(tuple(DEFAULT_MIRRORS)), a
23+
24+
25+
def test_alternate_urls_for_release_asset_mirror_prefixes_original():
26+
alts = alternate_urls(ASSET)
27+
assert any(a.endswith(ASSET) for a in alts)
28+
assert not any("/raw/" in a for a in alts)
29+
30+
31+
def test_alternate_urls_passthrough_for_unknown_hosts():
32+
assert alternate_urls("https://example.com/x.bin") == []
33+
34+
35+
def test_fetch_with_fallback_primary_first_when_reachable():
36+
calls: list[str] = []
37+
38+
def opener(url: str) -> bytes:
39+
calls.append(url)
40+
return b"DATA"
41+
42+
assert fetch_with_fallback(RAW, opener) == b"DATA"
43+
assert calls == [RAW]
44+
45+
46+
def test_fetch_with_fallback_falls_through_to_first_working_source():
47+
attempts: list[str] = []
48+
49+
def opener(url: str) -> bytes:
50+
attempts.append(url)
51+
# 只按域名前缀拦截官方源;镜像 URL 虽含原始 URL 后缀,但域名不同、应放行
52+
if url.startswith("https://github.com/") or url.startswith(
53+
"https://raw.githubusercontent.com/"
54+
):
55+
raise OSError("blocked")
56+
return b"MIRROR-DATA"
57+
58+
assert fetch_with_fallback(RAW, opener) == b"MIRROR-DATA"
59+
assert attempts[0] == RAW
60+
assert len(attempts) >= 2 and attempts[1] != RAW
61+
62+
63+
def test_fetch_with_fallback_all_sources_fail_lists_everything():
64+
def opener(url: str) -> bytes:
65+
raise OSError("down")
66+
67+
with pytest.raises(RuntimeError) as ei:
68+
fetch_with_fallback(RAW, opener)
69+
msg = str(ei.value)
70+
assert RAW in msg
71+
assert "alternate" in msg
72+
73+
74+
def test_mirror_list_overridable_via_env(monkeypatch):
75+
monkeypatch.setenv("DLSS_COMBO_MIRRORS", "https://m1.example/,https://m2.example/")
76+
alts = alternate_urls(RAW)
77+
assert any(a.startswith("https://m1.example/") for a in alts)
78+
assert any(a.startswith("https://m2.example/") for a in alts)
79+
assert not any(a.startswith(tuple(DEFAULT_MIRRORS)) for a in alts)

‎tests/test_scaffold.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
def test_package_version_single_source():
7-
assert dlss_combo.__version__ == "0.2.0"
7+
assert dlss_combo.__version__ == "0.2.1"
88
# 安装态(pip/PyInstaller 打包取同一字面量)与包内声明一致
99
try:
1010
assert importlib.metadata.version("dlss-combo") == dlss_combo.__version__

0 commit comments

Comments
 (0)