diff --git a/.gitignore b/.gitignore index 5bd038bab..83e03fd8d 100644 --- a/.gitignore +++ b/.gitignore @@ -408,3 +408,7 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# local HDR test builds +.build-cache/ +.build-cache-e-copy/ diff --git a/docs/RELEASE_NOTES_v0.6.6-hdr-fp16compat.md b/docs/RELEASE_NOTES_v0.6.6-hdr-fp16compat.md new file mode 100644 index 000000000..67f263ffd --- /dev/null +++ b/docs/RELEASE_NOTES_v0.6.6-hdr-fp16compat.md @@ -0,0 +1,21 @@ +# Magpie Experimental v0.6.6 HDR FP16 Compatibility + +## 中文说明 + +本版本将官方 Magpie 0.6.6 的核心生命周期、NGX 异常保护、DLSSNR history 重置、Renderer session 管理、延迟回调代际校验、窗口状态恢复和帧率控制流程合入本地 HDR 兼容分支。 + +HDR 兼容链路保留 canonical FP16 工作面、捕获边界路由、正反向归一化、DLSSNR FP16 实验路径、DLSSFG bridge,以及 XeSS、XeSS-FG、FSR、RTX Video、光流和 PassThrough 的 HDR 分支。 + +关闭 HDR 兼容总开关后,HDR 参数自动回到基线状态并进入线上 U8 处理链;HDR 相关设置只在总开关开启时生效。参数窗口的位置和尺寸会持久化到配置文件,旧配置继续使用默认尺寸。 + +完整安装包为 `Magpie-Experimental-x64.zip`,包含 `Magpie.exe`、`resources.pri`、`TouchHelper.exe`、`Updater.exe`、运行时 DLL、效果文件和许可证文件。 + +## English + +This release brings the official Magpie 0.6.6 core lifecycle changes into the local HDR-compatible branch, including NGX exception guards, DLSSNR history reset on input revision changes, renderer session lifetime handling, delayed-callback generation checks, window-state restoration, and the updated frame-rate control. + +The HDR pipeline keeps its canonical FP16 working surface, capture boundary routing, forward and inverse normalization, DLSSNR FP16 experimental path, DLSSFG normalization bridge, and HDR branches for XeSS, XeSS-FG, FSR, RTX Video, optical flow, and PassThrough. + +When the global HDR compatibility switch is off, HDR settings are reset to their baseline state and the online U8 processing chain is selected. HDR-specific settings take effect only while the global switch is enabled. Effect-parameter window position and size are persisted; older configurations use the default size. + +The complete package is `Magpie-Experimental-x64.zip` and includes `Magpie.exe`, `resources.pri`, `TouchHelper.exe`, `Updater.exe`, runtime DLLs, effect files, and license files. diff --git a/docs/experimental/20260908-hdr-anchored-shoulder-curve.md b/docs/experimental/20260908-hdr-anchored-shoulder-curve.md new file mode 100644 index 000000000..25d4c2268 --- /dev/null +++ b/docs/experimental/20260908-hdr-anchored-shoulder-curve.md @@ -0,0 +1,88 @@ +# HDR 色彩边界修正:SDR 复刻合同 + 锚定肩部曲线设计记录 + +日期:2026-09-08 +分支:local-0.6.5-hdr +范围:`HdrColorTransform`(CPU)、`HdrSurfaceAdapter`(GPU mode 0/1/2/3)、`FrameSourceBase`(SDR 白测量告警)、`Renderer`(DLSSNR boundary 日志)、`tests/HdrMechanicalTests.cpp` + +## 1. 问题背景 + +v0.6.6-hdr-fp16compat 实机验证中,DLSS SR / DLSSFG / XeSSFG 颜色正常,DLSSNR 两种模式(R8 用户路径与 FP16 实验路径)均出现偏移;上一轮有理曲线重写后偏移形态变为偏灰偏蓝,且调整残差参数时误差被放大。其余 native 路线走 DirectFP16 / 纯线性 bounded / terminal 路由,完全不经过色调映射;DLSSNR 是唯一同时挂在 SDRCompatible(mode 0/1)与 BoundedHDR(mode 2/3)上的 backend,因此偏移源锁定在曲线与归一化环节。 + +三个已确认的偏移源: + +1. 旧实现的有理曲线 `v·(peak+shoulder)/(peak·(v+shoulder))` 在 SDR 白以下偏离恒等:以 peak=12.5、shoulder=1.0 计,SDR 白 1.0 被压到 0.54,模型输入整体压暗,逆曲线放大残差参数误差(灰蓝偏移与"动参数更严重"的直接机制)。 +2. mode 2 去掉负值钳制后,scRGB 越界负通道原样进入 DLSSNR 模型;模型消费域是 [0, ∞),负线性输入没有训练契约(FP16 路径偏蓝的主要嫌疑)。 +3. `GetMonitorSdrWhiteNits` 失败时静默回退 80 nit,归一化分母与显示器真实 SDR 白(如 360 nit)差 4.5 倍,且无任何日志提示。 + +## 2. 两条路由、两份合同 + +设计过程中确立的关键约束:**恒等段(SDR 白以下逐位通过)与 T < 1 的压缩目标在数学上不相容**。恒等段强制 f(1)=1,单调性禁止 f 在 1 之后回落,而 UNORM8 存储又没有 1 以上的码空间——数值验证确认了这一点(target=0.95 被 clamp 推回 1.000001,k 顶到 0.999999,肩部退化为阶跃)。因此两条路由采用两份各自正确的合同: + +### 2.1 SDRCompatible(mode 0/1,R8 复刻路径):恒等 + saturate 复刻合同 + +```text +前向:y = min(max(v·exposure / (sdrWhite/80), 0), 1) +逆向:v' = y < 1 ? y·(sdrWhite/80)/exposure : framePeak +``` + +SDR 白以下恒等(捕获的 SDR 游戏帧逐位进入 backend,与线上 0.6.5 完全一致);白点以上直接 saturate,逆方向把饱和白恢复到帧声明峰值(高光在峰值档位存活,虽然失去白上细节——这正是 8bit U8 复刻路线的物理上限,也是"复刻原路径"语义的正确代价)。该合同同时服务 DLSSNR R8、GroupA shader 效果与 RTX Video 等 SDRCompatible 路由。 + +### 2.2 BoundedHDR(mode 2/3,DLSSNR FP16 实验路径):锚定肩部曲线族 + +以归一化输入 `x`(canonical ÷ sdrWhite,SDR 白 = 1.0)定义前向 `f`: + +```text +x <= 1 f(x) = x 恒等段 +1 < x <= p f(x) = x / (1 + k(x - 1)) 锚定 Reinhard 肩部 +x > p f(x) = T + f'(p)·(x - p) 线性尾 +``` + +参数:`p = hdrPeakNits / sdrWhiteNits`,`T = f(p)`(设计锚点,恒 > 1), +`k = (p - T) / (T(p - 1))`,`f'(p) = (1-k)T²/p²`。 + +配对逆函数: + +```text +y <= 1 g(y) = y +1 < y <= T g(y) = y(1 - k) / (1 - k·y) 肩部代数精确逆(g(T) = p) +y > T g(y) = p + (y - T) / f'(p) 尾部逆 +``` + +性质:`f(1)=1` 与恒等段 C0 连续;`k ∈ (0,1)` 保证肩部严格单调;尾部斜率 `f'(p) > 0` 保证任意高 nits 输入不逆转;逆增益全域有界(肩部 ≤ 1/(1-k),尾部 = 1/f'(p))。T 取 2.5:实验矩阵显示 scale=2 接近基线、4.5 爆炸,2.5 落在安全带上沿,`f(∞)=1/k ≈ 2.9` 有界。 + +## 3. 各 mode 的最终行为 + +- **mode 0(HDR→SDR)**:复刻合同前向(恒等 + saturate)。 +- **mode 1(SDR→HDR)**:复刻合同逆向;饱和白恢复到帧峰值。 +- **mode 2(canonical→bounded,DLSSNR 输入)**:`max(v,0)` 钳掉越界负值(模型消费域契约),`·normalizationScale/(sdrWhite/80)` 归一,再肩部曲线(T=2.5)压入模型验证带。 +- **mode 3(bounded→canonical,DLSSNR 输出)**:肩部精确逆 + 反归一,全程无钳制。 +- mode 4/5/6(scRGB / HDR10 terminal)不变。 + +CPU(`HdrColorTransform`)与 GPU(`HdrSurfaceAdapter`)共用同一组系数(常量缓冲携带 peak/target/shoulderK/tailSlope,112 字节含对齐 padding),机械测试保证两者实现一致。 + +## 4. 配套改动 + +- `FrameSourceBase.cpp`:SDR 白测量失败且显示为 HDR 时输出 Warn(含峰值与色彩空间),消除静默 4.5 倍错位。 +- `Renderer.cpp` DLSSNR boundary 日志扩展为 `sdrWhite / peak / curvePeak / curveTarget / curveK / curveTailSlope / colorInferred`,实机一条日志即可核对归一化与曲线参数。 +- `tests/HdrMechanicalTests.cpp`:`TestHdrToneMapRoundTrip` 改为复刻合同语义(白点以下精确往返、饱和峰恢复);新增 `TestShoulderCurveFamily`(恒等段、复刻合同饱和/恢复、肩部严格单调至 2.25p、锚点与配对逆含尾上点、T 取值域、bounded 编解码往返 + 带内断言)。全部通过(1174 PASS / 0 FAIL)。 +- `Run-HdrMechanicalValidation.ps1` 锚点更新为复刻合同 + 肩部族符号,全部通过。 + +## 5. 预期与验证 + +预期效果:R8 复刻路径在 HDR 开关下回到逐位级一致(这是本设计的硬保证),灰蓝偏移消失;FP16 路径高光被压回模型验证过的 [0, ~2.5] 邻域,负通道不再进入模型。 + +实机验证顺序: + +1. HDR on + DLSSNR R8 路径:SDR 画面应与 HDR off 基线逐位一致;日志核对 `sdrWhite` 与显示器设置相符。 +2. HDR on + FP16 实验路径:高光不再偏色;调整残差参数不再放大误差。 +3. 高亮 HDR 测试图:确认无色阶乱序(逆转)与带状。 +4. 若仍有残余偏移,日志中 `curveK/curveTarget` 与 `HDR adapter dispatch: white= peak=` 可直接定位是曲线参数还是元数据错位。 + +## 6. 已知边界 + +- R8 复刻合同在白点以上丢弃细节(saturate 后逆恢复到峰值档);这是 8bit U8 复刻语义的物理上限,HDR 高光保真由 FP16 路径承担。 +- 复刻合同逆向把 1.0 码值解释为帧峰值;若后端恰好输出 1.0 的"真 SDR 白",会被提升到峰值档。DLSSNR 输出域按 saturate 语义设计,该解释与其一致。 +- 恒等段与肩部在 x=1 处 C0 连续但导数有跳变;FP16 路径无碍。若需要 C1 平滑可引入 [1-ε, 1+ε] 混合,当前刻意不加以保持逆函数代数精确。 +- 尾部线性外推超出 p 的输入在逆变换后可超过 hdrPeakNits;canonical 保留原始值域,由 presenter 合同承接。 +- `BoundedRouteHighlightTarget = 2.5` 是基于社区 DLL 实验的工程取值;后续 harness A/B 发现更优锚点时只改这一个常量(CPU/GPU/测试自动跟随)。 + diff --git a/docs/experimental/20260908-v0.6.6-hdr-compatibility-upstream-handoff.md b/docs/experimental/20260908-v0.6.6-hdr-compatibility-upstream-handoff.md new file mode 100644 index 000000000..bf5223bcd --- /dev/null +++ b/docs/experimental/20260908-v0.6.6-hdr-compatibility-upstream-handoff.md @@ -0,0 +1,752 @@ +# Magpie 0.6.6 上游交接:HDR/FP16 兼容分支完整设计 + +> 文档用途:提供给 Magpie 原作者和上游维护者的实现交接稿。本文固定了比较版本、代码入口、路由规则、关闭 HDR 后的基线行为、0.6.6 生命周期合并点、验证证据和建议的上游合并顺序。 +> +> 当前 fork:[konodiodaaaaa1/Magpie](https://github.com/konodiodaaaaa1/Magpie) +> 官方参考:[SAOG0721/Magpie](https://github.com/SAOG0721/Magpie) +> 当前分支:local-0.6.5-hdr +> 当前提交:9c2309a283b3bfef59a5197b38a8354b135363f3 + +## 1. 这份交接稿回答什么问题 + +当前分支是在本地 0.6.5 实验线的基础上,保留已经完成的大量 HDR/FP16 管道工作,再把官方 0.6.6 的核心生命周期、安全退出、NGX 保护、参数更新和窗口状态恢复流程合入。上游后续接收时,最重要的事实是:HDR 代码是一层有明确开关和 boundary 的兼容层,官方 0.6.6 的效果路由、会话模型和资源生命周期仍然是主流程。 + +整条设计可以压缩成两条状态机: + +~~~text +HDR compatibility = Off + profile/config + -> official 0.6.5 U8 capture contract + -> official effect/native route + -> official auxiliary resources + -> official publication/presenter + +HDR compatibility = On + profile/config + -> source capture + -> HDR decode + exposure normalization + -> canonical R16G16B16A16_FLOAT HdrFrame + -> per-effect route adapter + -> existing effect/native backend + -> inverse route adapter + -> canonical R16G16B16A16_FLOAT HdrFrame + -> presentation terminal or HDR presenter +~~~ + +关闭状态的验收标准是整条 HDR 处理链被旁路,HDR 专用参数、HDR route、FP16 bridge、HDR presenter 和 HDR metadata 都由统一开关控制;外部窗口本身处于 HDR 显示环境时,应用仍沿用线上 U8/LDR 合同,HDR 参数也保持基线默认值。开启状态的验收标准是 canonical FP16 工作面贯穿各 boundary,效果需要的颜色、深度、光流、曝光、反应遮罩和 frame id 都来自同一捕获帧,随后以效果声明的格式和色彩合同进入 backend。 + +## 2. 固定比较基线 + +| 项目 | 固定值 | +| --- | --- | +| 官方 0.6.5 标签 | v0.6.5-experimental | +| 官方 0.6.5 提交 | 2d37f8c0826f9b1e48d1e985d42f3cc54ff2b76c | +| 官方 0.6.6 标签 | v0.6.6-experimental | +| 官方 0.6.6 提交 | 9824d758b162ad3c5b5acc81e2e14c83f138e13d | +| 当前 fork 分支 | local-0.6.5-hdr | +| 当前 fork HEAD | 9c2309a283b3bfef59a5197b38a8354b135363f3 | +| HDR 管道主提交 | 06435e47fb878780f4b5b1a2d18cd4937c5f01bf | +| 0.6.6 合入提交 | 9c2309a283b3bfef59a5197b38a8354b135363f3 | +| 官方 0.6.5 -> 0.6.6 规模 | 39 个文件,1270 行新增,256 行删除 | +| 本地 0.6.6 合入提交规模 | 23 个核心运行时文件,460 行新增,197 行删除 | +| 构建目录 | E:\Magpie-0.6.5-build | + +官方 0.6.6 的完整差异包含 README、release note、源级回归脚本和 review 文档。当前 fork 的 9c2309a2 重点落地了 Core 运行时流程;HDR 研究文档、机械检查和本地诊断文档已经在前一个 HDR 提交中保留。两者叠加后才是当前可运行的交接版本。 + +比较时应把“官方 0.6.5 基线行为”、“官方 0.6.6 生命周期设计”和“本地 HDR 适配行为”分开观察。效果 backend 的原有类、ZeroMV/Jitter/Optical Flow 入口、DLSSNR 残差路径、XeSSFG 终端发布以及配置迁移逻辑都属于本地分支的有效工作面,上游合并按 boundary 增加条件,保持这些分支的独立路由,避免粗粒度的全局格式替换覆盖它们。 + +## 3. 官方 0.6.6 的实际设计 + +### 3.1 用户可见行为 + +官方 0.6.6 的用户层变化集中在参数交互、会话恢复和故障退出: + +| 主题 | 官方 0.6.6 行为 | 当前 fork 的对应位置 | +| --- | --- | --- | +| 目标帧率 | 工具栏使用 15 到 360 FPS、步进 1 的整数滑条;主页仍保留 0 自动和小数输入语义 | OverlayDrawer.cpp、ScalingWindow.cpp、ScalingOptions.h | +| 参数实时更新 | Live 参数进入 Renderer 的 last-write-wins 队列,在帧边界应用;Restart 参数走重新启用 | Renderer.cpp、ScalingWindow.cpp | +| DLSSNR 历史 | inputRevision 变化时刷新 DLSSNR history;上游实时参数变化无需拆掉整个效果组 | Renderer.cpp、DLSSNRFilter.cpp、NativeEffectBackend.h | +| 参数窗口 | 保存边缘锚点、相对中心位置和 DIP 宽高;捕获区临时缩小时只限制显示尺寸,捕获区恢复后还原偏好尺寸 | OverlayDrawer.cpp、OverlayDrawer.h、include/OverlayWindowGeometry.h | +| 会话状态 | 重新启用成功后恢复工具栏、固定状态、分析器和参数窗口状态;取消重建清除快照 | Renderer.cpp、ScalingWindow.cpp | +| 停止缩放 | WM_DESTROY 早期发布停止标志,后端继续处理必要的关闭消息,捕获回调按会话代际过滤 | Renderer.cpp、GraphicsCaptureFrameSource.cpp、ScalingSessionLifetime.h | +| NGX 故障 | NGX Core、DLSSNR、DLSSFG 的受保护调用统一进入 NgxRuntimeGuard;进程级 fault 后新会话在初始化阶段失败,并提示完整退出 Magpie 后重试 | NgxD3D12Core.cpp、DLSSNRFilter.cpp、DLSSFrameGenerator.cpp、NgxRuntimeGuard.h | +| 窗口 owner | 初始化/清理阶段先解除游戏窗口 owner,Renderer、鼠标和触控初始化完成后再恢复;销毁时先解除 owner 再停止捕获和 backend | ScalingWindow.cpp、ScalingWindowOwner.h | +| 输入 revision | DLSSNR 和多效果链共享 Renderer 侧 revision,避免同一 capture id 混入旧图像历史 | Renderer.cpp、DLSSNRFilter.cpp | + +### 3.2 官方渲染流程 + +官方 0.6.6 仍然使用单一 Renderer 会话和后台线程模型: + +~~~text +ScalingWindow::Start + -> Renderer::Initialize + -> create capture source / native backend / effect drawer + -> backend thread waits for capture + -> capture result enters guidance and effect chain + -> publication and frontend presentation + -> WM_DESTROY / Stop + -> BeginShutdown marks session lifetime + -> backend drains close messages + -> capture and backend release +~~~ + +ScalingSessionLifetime 记录会话编号和 stopping 状态。所有延迟回调都捕获这个共享生命周期对象,并在读取 ScalingWindow、报告错误或执行停止动作前检查 IsCurrent(ScalingWindow::RunId())。这样,旧会话的捕获错误、XeLL 错误和 Front Edge Sync 回调会在新会话创建后自动失效。 + +NgxRuntimeGuard 对 SEH 和正常 API 失败采用两条记录路径。SEH 记录异常码、地址和线程编号,随后设置进程级 fault;正常 API 失败仍沿用现有错误码。fault 状态保留到进程结束,受影响的 NGX 设备和 DLSSNR 模块保持隔离,避免在 SDK 内部临界区状态未知时再次进入关闭或释放入口。 + +### 3.3 官方 0.6.6 文件分组 + +官方 v0.6.5-experimental..v0.6.6-experimental 的 39 个文件可以按职责分为以下六组: + +| 分组 | 文件 | +| --- | --- | +| Renderer / backend | DLSSFrameGenerator.cpp、DLSSNRFilter.cpp、GraphicsCaptureFrameSource.cpp/.h、NativeEffectBackendFactory.cpp、NgxD3D12Core.cpp、Renderer.cpp/.h、ScalingWindow.cpp/.h | +| overlay / geometry | ImGuiImpl.cpp、OverlayDrawer.cpp/.h、include/OverlayWindowGeometry.h、include/ScalingOptions.h | +| lifetime / guard | include/NgxRuntimeGuard.h、include/ScalingSessionLifetime.h、include/ScalingWindowOwner.h、include/EffectParameterRestart.h | +| app / resources | AppSettings.cpp、ErrorService.cpp、三份语言资源、Magpie.Core.vcxproj | +| tests / review | scripts/tests/ngx_runtime_guard.cpp、scaling_window_owner.cpp、test_capture_shutdown.py、test_overlay_window_layout.py、三份 docs/experimental/reviews/20260906-v0.6.6-* | +| release docs | README.md、README_ZH.md、docs/FRAME_SYNC_GUIDE.md、docs/README-EXPERIMENTAL-RELEASE.txt、docs/RELEASE_NOTES_NEXT.md、docs/RELEASE_NOTES_v0.6.6-experimental.md、docs/experimental/README.md | + +本地 HDR 合并保留这些职责边界。HDR 的 adapter、metadata、route catalog 和 surface conversion 归属 HDR 层;停止会话、NGX fault、overlay snapshot、parameter queue 和 window owner 仍由官方 0.6.6 的会话层掌控。 + +### 3.4 本地 0.6.5 实验线的有效工作面 + +本地分支的 HDR 适配建立在一条已经有多轮实验和回归的 0.6.5 工作线上。下面这些内容属于当前实现的输入条件,上游移植时应作为既有行为保留,再在 HDR 开关下增加格式和色彩 boundary: + +| 本地工作面 | 当前实现要点 | 主要入口 | +| --- | --- | --- | +| 统一 native backend 分派 | 原生 SDK effect 通过 NativeEffectBackend 和 NativeEffectBackendFactory 管理,DLSSFG 作为多帧发布终端保留独立阶段 | NativeEffectBackend*、Renderer.cpp | +| DLSSNR 残差链 | 输入分辨率 25% 到 100%、颜色/光流/Zero Depth 调整、full-resolution residual 合成、阴影/结构与反射/发光倍率、HSL 相对变化 | DLSSNRFilter.cpp、NativeEffectBackendFactory.cpp | +| Frame Guidance | AMD OF、NVOF、None provider 选择、质量档位、半分辨率/密集化、history reset 和 frame id 绑定 | FrameGuidanceService.cpp、AmdOpticalFlowProvider.cpp、NvidiaOpticalFlowProvider.cpp | +| DLSS SR / FSR2/3/4 / XeSS SR | ZeroMV、Jitter、光流 provider、输入尺寸和质量档位已经进入本地参数合同 | DLSSSRUpscaler.cpp、FSR2Upscaler.cpp、FSR3Upscaler.cpp、XeSSUpscaler.cpp | +| DLSSFG | 2x/3x/4x、共享 Motion、固定 Zero Depth、CPU Fence、D3D11/D3D12 共享资源、一次重置/一次重建/会话禁用的失败策略 | DLSSFrameGenerator.cpp、Renderer.cpp | +| XeSSFG | 通用显卡 x2、Intel Arc x2 到 x4 多帧生成,能力依据 GPU/driver 报告值限制 | XeSSFGPresenter.cpp、Renderer.cpp | +| RTX Video | VSR 与 Denoiser 的分辨率档位、U8 GPU buffer、normalized-D3D 输入尺度 | RTXVideoDenoiser.cpp | +| 参数会话 | 参数描述解析为 Live 或 RestartRequired,last-write-wins 邮箱、目标值/已生效值分离、自动重建和显式应用分开 | Renderer.cpp、ScalingWindow.cpp、OverlayDrawer.cpp | +| 0.6.5 交互和运行时修复 | 帧同步、Front Edge Sync、任务切换、全屏停止、实时调度优先级和发行包流程 | ScalingOptions.h、ScalingWindow.cpp、scripts/Build-Release.ps1 | + +提交关系如下:2d37f8c 是本地 0.6.5 参数和 DLSSNR 基线,06435e47 在该基线上加入 HDR/FP16 管道、route catalog、capture processor、surface adapter 和诊断文档,9c2309a2 再将官方 0.6.6 核心生命周期和 NGX 流程合入。这个顺序解释了为什么当前实现同时包含本地实验线的效果逻辑与官方 0.6.6 的会话保护。 + +## 4. 当前 fork 的 HDR 总体架构 + +### 4.1 全局配置开关 + +配置键为: + +~~~json +{ + "enableHdrCompatibility": false +} +~~~ + +运行时位为 ScalingFlags::EnableHdrCompatibility,访问器为 ScalingOptions::IsHdrCompatibilityEnabled()。Profile 层的公开属性由 Profile::IsHdrCompatibilityEnabled()、ProfileViewModel::IsHdrCompatibilityEnabled() 和 ProfileViewModel.idl 贯通,设置页位于 src/Magpie/ProfilePage.xaml 的 Profile_General_HdrCompatibility 卡片。 + +全局开关拥有最高优先级: + +~~~text +HDR off: + HDR route selector -> baseline default + HDR boundary -> cleared + HDR parameter -> ignored or reset to baseline + capture format -> online 0.6.5 contract + effect/native route -> online U8/LDR contract + presentation -> online presenter format + +HDR on: + HDR route selector -> selected structured route + HDR boundary -> prepared from canonical HdrFrame + HDR parameter -> active + capture format -> HDR capture contract where available + effect/native route -> route adapter + backend contract + presentation -> HDR terminal or HDR-compatible presenter +~~~ + +关闭状态的关键点是“外部显示器 HDR 状态”和“Magpie HDR compatibility 状态”各自独立。系统或游戏提供 HDR 输入时,Renderer 仍以开关结果选择处理链;单独打开外部 HDR 环境不会把 HDR route、HDR normalization 或 HDR presenter 自动带入 SDR 分支。 + +### 4.2 Canonical FP16 工作面 + +inter-effect 的 canonical storage 是: + +~~~text +DXGI_FORMAT_R16G16B16A16_FLOAT +~~~ + +R16G16B16A16_FLOAT 只表示存储精度。每个 HdrFrame 还携带 HdrFrameMetadata 和 ColorDescription: + +~~~cpp +struct ColorDescription { + DXGI_COLOR_SPACE_TYPE dxgiColorSpace; + HdrColorPrimaries primaries; + HdrTransferFunction transfer; + HdrColorRange range; + float referenceWhiteNits; + float sdrWhiteNits; + float displayPeakNits; + bool isSceneReferred; + bool isPreExposed; + bool isInferred; + bool displayHdrEnabled; + float preExposure; + HdrMetadata metadata; +}; +~~~ + +常见 HDR 输入会先完成 transfer decode、primary 解释和 exposure normalization,再写入 canonical surface。Windows scRGB 的参考白为 80 nit,PQ 使用绝对显示亮度,HLG 使用相对显示亮度;这些信息通过 ColorDescription 传递,纹理格式本身不承担色彩语义。 + +### 4.3 捕获边界 + +捕获方式的选择和窗口获取行为继续复用原有实现。HDR 处理从捕获纹理产生后开始: + +~~~text +selected capture method + -> source texture + DXGI format + color-space metadata + -> HdrCaptureProcessor + -> exposure/transfer normalization + -> canonical HdrFrame +~~~ + +Graphics Capture 在 HDR 状态请求 R16G16B16A16_FLOAT frame pool,并按 linear scRGB 解释。其他捕获方式继续使用线上已有 BGRA/U8 合同,HDR 适配层依据来源和 metadata 做显式解释。R10G10B10A2_UNORM 只有与 HDR10/PQ、BT.2100 色彩空间一起出现时,才进入对应的 bounded/terminal route。 + +核心文件:FrameSourceBase.cpp/.h、GraphicsCaptureFrameSource.cpp/.h、DesktopDuplicationFrameSource.cpp、HdrCaptureProcessor.cpp/.h、HdrColorTransform.cpp/.h。 + +### 4.4 Effect boundary + +每个普通效果在 HDR 状态采用相同的 boundary 形状: + +~~~text +canonical HdrFrame + -> HdrEffectBoundary::Prepare + -> HdrAdapterDispatcher::BuildPlan + -> HdrSurfaceAdapter input conversion + -> backend/effect local surface + -> existing effect or native backend + -> HdrSurfaceAdapter output conversion + -> HdrEffectBoundary::Complete + -> canonical HdrFrame +~~~ + +HdrFormatRoute 记录 effectId、optionId、input/output format、transfer、range、alpha mode、evidence level、hdrNative、adapter profile、HDR/SDR 默认 route 和 normalization scale。route 选择使用结构化字段;effect 名称仅用于 catalog 分组和日志。 + +适配 profile 的含义如下: + +| Profile | 处理含义 | +| --- | --- | +| DirectFP16 | backend 直接接受验证过的浮点 HDR 输入;canonical FP16 只做必要的 transfer/primary 变换 | +| BoundedHDR | backend 需要受限数值范围;使用带 paired inverse 的 named normalization | +| SDRCompatible | backend 合同为 U8/UNORM/SDR;HDR 先形成定义明确的 SDR proxy,再恢复到 canonical domain | +| ConditionalFP16 | 参考实现或实验已经显示 FP16 路线,完整色彩合同仍保留为条件路径 | +| Unknown | 当前证据不足;HDR route 由显式 fallback 和诊断承接 | +| PresentationTerminal | backend 拥有交换链或最终发布面,效果分支在该节点结束 | + +### 4.5 Adapter 的实际转换 + +HdrSurfaceAdapter 使用 compute shader 和 64 字节常量布局,包含: + +- sRGB、PQ、HLG 的 decode/encode; +- Rec.709 与 Rec.2020 的显式转换; +- HDR 到 SDR 的 exposure、shoulder、headroom 映射; +- SDR 到 HDR 的配对恢复; +- canonical 到 bounded domain 的 normalization scale; +- bounded domain 回到 canonical 的 inverse scale; +- canonical scRGB 到 HDR10/PQ R10G10B10A2_UNORM 的 terminal encode; +- HDR10/PQ 回到 canonical FP16 的 terminal decode; +- alpha Preserve、ForceOpaque、Premultiplied 三种规则。 + +所有 surface conversion 都读取输入和输出 descriptor,校验宽高、格式和 bind flags,并在 dispatch 后解绑 SRV/UAV。这样,适配器的实际 resource contract 与 route metadata 保持一致,效果内部的 R8/R10/FP16 选择仍由 backend 自己完成。 + +### 4.6 Route catalog 分组 + +当前 route provider 分成三组: + +| Provider | 覆盖范围 | 主要职责 | +| --- | --- | --- | +| GroupAHdrRoutes | Anime4K、CAS、CRT、CuNNy、CuNNy2、Diagnostics、FSRCNNX、FXAA、MLAA | 外部契约缺失或参考实现有限的 shader 效果;提供结构化 SDR fallback 和 CAS 条件 FP16 选项 | +| GroupBHdrRoutes | DLSS、DLSSNR、FSR、FSR2、FSR3、FSR4、NIS | native SR/NR 后端以及 DLSSNR bounded normalization | +| EffectProtocolCatalogC | XeSS、XeSSFG、RTX Video、光流、PassThrough 等 | 依赖 SDK 或 presentation 合同的效果和辅助资源 | + +路由的 evidence level 目前区分 PublicApiContract、ReferenceImplementation、CommunityExperiment、LocalValidation 和 None。Unknown route 只保留在诊断和后续实验入口,默认选择顺序优先验证过的 HDR-native FP16,其次是 HDR10/bounded route,再其次是显式 SDR-compatible route。 + +## 5. 官方流程与本地 HDR 流程的等价映射 + +| 官方 0.6.6 流程节点 | 本地接入位置 | HDR 开启时 | HDR 关闭时 | +| --- | --- | --- | --- | +| ScalingWindow::Start | Renderer::Initialize、_BuildEffects | 创建 canonical pool、route boundary、HDR presenter 所需资源 | 采用原有 U8 资源描述和原有 backend 初始化 | +| capture source | FrameSourceBase、GraphicsCaptureFrameSource | source texture 进入 HdrCaptureProcessor,生成 canonical FP16 | 直接交给线上 capture/effect 合同 | +| backend thread | Renderer::_BackendThreadProc | 以 HdrFrameMetadata.frameId/captureSequence 贯穿颜色和 auxiliary inputs | 使用官方 0.6.5 的 frame/guidance 路由 | +| effect drawer | EffectDrawer、HdrEffectBoundary | canonical -> route local -> effect -> canonical | 使用原有 descriptor、shader format 和 U8 publication | +| native factory | NativeEffectBackendFactory | 按 backend ID 和 HDR gate 选择 route-compatible input/output | 选择线上 ZeroMV/Jitter/U8 backend 类和参数合同 | +| live parameter queue | Renderer、ScalingWindow | HDR 参数更新同时增加 boundary revision,DLSSNR 清 history | 仅应用基线参数语义,不创建 HDR route | +| NGX calls | NgxRuntimeGuard 包装 Core/DLSSNR/DLSSFG | HDR route 的 NGX 调用仍走 0.6.6 guard | SDR route 也走同一 guard,保持故障隔离 | +| overlay snapshot | OverlayDrawer、RestoreOverlayState | 参数窗口和工具栏状态与 HDR route 状态分离保存 | 与官方 0.6.6 相同 | +| publication | Renderer::_PublishBackendTexture、PassThroughFrames | canonical FP16 或 terminal HDR10 surface 带 metadata 发布 | 原有 U8/shared texture 发布 | +| frontend presenter | AdaptivePresenter、CompSwapchainPresenter、XeSSFGPresenter | HDR presenter 使用对应 swap-chain 色彩空间 | 线上 presenter format 和 color space | +| stop/shutdown | BeginShutdown、ScalingSessionLifetime、capture callback generation | HDR resources 在 session stop 后一起失效 | 官方生命周期语义 | + +这张映射表是上游移植时的核心边界。0.6.6 代码解决的是会话、参数和 SDK 生命周期;HDR 代码只在这些节点上增加 format/color metadata 和 adapter。两套机制共享 Renderer 的 session lifetime、input revision 和 publication 顺序。 + +## 6. 关闭 HDR 的强制回退合同 + +关闭状态按以下顺序执行,任何一层都以全局 gate 为准: + +~~~text +1. Profile flags 中 EnableHdrCompatibility = 0 +2. Renderer 清空所有 HdrEffectBoundaryContext +3. EffectDrawer 选择线上 effect descriptor 和默认 U8 route +4. NativeEffectBackendFactory 选择线上 backend 类和 U8/LDR input contract +5. FrameGuidance 使用线上颜色格式;Zero Depth/MV 按原有 backend 要求提供 +6. DLSSNR/DLSSFG/XeSSFG/FSR/RTX Video 使用各自线上参数与 publication 格式 +7. AdaptivePresenter/CompSwapchainPresenter 选择线上 presentation format +8. PassThrough 使用 REFERENCE_LDR_HLSL +9. 配置中的 HDR-only selector、normalization scale、HDR terminal 参数进入 inactive/default 状态 +~~~ + +这里的“回退”属于路由选择和参数可用性状态。当前本地特有的 DLSSNR 残差、Frame Guidance、ZeroMV、FG failure recovery、effect parameter session 和 0.6.5 的其他行为仍然保留;它们在关闭 HDR 时按现有线上 0.6.5 逻辑运行。 + +需要特别审查的泄露点如下: + +| 泄露点 | 关闭状态要求 | +| --- | --- | +| HDR route context | Renderer::_UpdateHdrEffectBoundaryContexts() 对所有 drawer/backend 写入空 context | +| HDR capture pool | Graphics Capture 的 HDR frame pool 只在 gate 开启时申请 | +| DLSSNR normalization | normalization texture、scale、inverse pass 只在 gate 开启时创建和 dispatch | +| DLSSFG bridge | _dlssFgNormalizedInput、_dlssFgCanonicalGenerated 只在 HDR gate 下使用 | +| XeSS LDR flag | XESS_INIT_FLAG_LDR_INPUT_COLOR 只进入 SDR 初始化;HDR FP16 初始化保留 HDR 输入合同 | +| XeSSFG terminal | HDR10/PQ R10G10B10A2_UNORM 和 RGB_FULL_G2084_NONE_P2020 只在 HDR terminal 下使用 | +| RTX Video | inputScale、U8/float bridge 和 HDR metadata 只在 HDR 分支使用 | +| Optical Flow | HDR canonical conversion shader 只在 HDR 分支使用,SDR 优先原线上 B8G8R8A8/U8 contract | +| PassThrough | REFERENCE_HDR_HLSL 与 HDR publication 只在 gate 开启时使用 | +| effect compiler | MP_HDR_SATURATE、MP_HDR_ALPHA、MP_HDR_COMPATIBILITY 只进入 HDR shader variant | +| HDR UI selector | HDR-only option 在开关关闭时隐藏或恢复基线选项,关闭状态只保留基线 selector 语义,FP16 selector 不生效 | + +## 7. Native backend 逐项说明 + +### 7.1 DLSSNR:canonical FP16 加 bounded normalization + +DLSSNR 是本地 HDR 管道中最重要的垂直切片。它既保留线上 0.6.5 的颜色、深度、光流、exposure、残差和 history 逻辑,又在 HDR 模式下增加 backend boundary: + +~~~text +canonical FP16 color + -> bounded HDR normalization + -> DLSSNR input surface + -> DLSSNR evaluate(color/depth/motion/exposure) + -> DLSSNR output + -> inverse normalization + -> canonical FP16 +~~~ + +实现重点: + +1. 颜色、depth、motion、exposure 与 capture sequence 绑定;DLSSNR 仍然拿到需要的 auxiliary resources。 +2. canonical FP16 是 inter-effect 工作面;DLSSNR 的 bounded 输入是 boundary local surface,随后通过 inverse scale 回到 canonical。 +3. inputRevision 属于 Renderer/NativeEffectDrawContext 的显式字段。上游 live 参数或输入 route 发生变化时,DLSSNR 设置 resetHistory,在下一次 Evaluate 使用新历史。 +4. 0.6.6 的 NGX guard 包装 create/evaluate/release/shutdown、参数读写和 signed snippet 调用;fault 后保留受影响模块并返回 NgxRestartRequired。 +5. DLSSNR 原有 residual 控制、输入分辨率调整、full-resolution residual 合成和默认参数仍由本地 0.6.5 逻辑控制。 +6. HDR 关闭后 route context 清空,normalization 资源不参与 Draw,DLSSNR 进入线上 U8/LDR contract。 + +DLSSNR 的合并审查应同时检查三件事:真正传入 backend 的 texture descriptor、真正的 input revision/history 行为、以及 publish 前的 canonical output。仅看到 EvaluateFeature 成功,只能证明 CPU/GPU 提交路径走通;当前诊断文档因此要求记录每个 boundary 的格式、尺寸、状态、finite pixel 统计和 hash。 + +### 7.2 DLSS SR + +DLSS SR 在 HDR 状态使用 native/DirectFP16 route 或经 route catalog 选出的验证路径。depth、motion、exposure 和 reactive/alpha 资源继续按本地 0.6.5 的 guidance contract 构造;HDR 适配层只负责颜色 surface 和 metadata。关闭状态使用原有 DLSSSRUpscaler 的 U8/LDR 处理和线上输入格式。 + +### 7.3 DLSS Frame Generation + +DLSSFG 是带终端发布和异步 frame job 的 native backend。当前 Renderer 增加了三个关键资源: + +~~~text +_dlssFgNormalizedInput +_dlssFgCanonicalGenerated +_dlssFgHdrNormalizationScale +~~~ + +HDR 状态的前后转换为: + +~~~text +canonical effects output + -> bounded input normalization + -> DLSSFG evaluate with color/depth/motion/exposure + -> bounded generated frame + -> inverse normalization + -> canonical generated frame + -> publication +~~~ + +发布前检查 canonical FP16 descriptor;conversion 或 generated output 失败时沿用本地 FG 的历史重置、单次重建和当前会话禁用策略。HDR 关闭后直接使用线上 DLSSFG 输入和 publication route。 + +### 7.4 XeSS SR + +XeSSUpscaler.cpp 同时保留 HDR FP16 和 SDR U8 两种明确入口: + +| 状态 | 输入合同 | 初始化标记 | +| --- | --- | --- | +| HDR | canonical/route-selected FP16 或声明的 HDR float surface | HDR route 下保持浮点输入语义 | +| SDR | 线上 U8/LDR texture | XESS_INIT_FLAG_LDR_INPUT_COLOR | + +HDR path 使用独立 conversion shader,输入/输出 descriptor 和 color contract 由 route 决定。XESS_INIT_FLAG_LDR_INPUT_COLOR 只在 SDR 路由加入,避免 HDR FP16 surface 被 SDK 当作 U8/LDR 解释。 + +XeSSZeroMVUpscaler 是本地 0.6.5 的独立 ZeroMV backend,需保持其线上 SDR 类和辅助资源合同。上游移植时,XeSSUpscaler 的 HDR 分支和 XeSSZeroMVUpscaler 的基线分支分别审查,使用独立的 XeSS wrapper 与 ZeroMV wrapper 保持两者的实现边界。 + +### 7.5 XeSS Frame Generation + +XeSSFG 属于 PresentationTerminal。HDR terminal contract 为: + +~~~text +canonical FP16 + -> Rec.709 to Rec.2020 + -> PQ / BT.2100 encode + -> R10G10B10A2_UNORM + -> DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 + -> XeSSFG / proxy swap chain +~~~ + +backbuffer、HUD-less texture、UI texture 和 proxy swap chain 统一使用 HDR10 format/color-space contract。进入 terminal 后,普通 post-effect 队列结束,避免把终端 swap-chain surface 当成普通 canonical inter-effect texture。HDR 关闭时恢复线上 LDR format、color space 和 publication。 + +### 7.6 FSR2、FSR3、FSR4 + +FSR2/3/4 的 HDR route 由 GroupBHdrRoutes 和各自 native backend 共同决定: + +- 颜色输入使用 canonical FP16 或 route 声明的 float surface; +- exposure 在 HDR boundary 显式处理; +- motion、depth、reactive/transparency mask 按效果的 auxiliary contract 传入; +- FSR3/FSR4 的 frame generation 或 optical-flow branch 使用单独 terminal/auxiliary route; +- HDR 关闭时回到线上 FSR2/3/4 U8/LDR route、ZeroMV 入口和原有参数默认值。 + +FSR4 仍属于研究性质路径,二进制分发和 SDK 组合需要独立做许可审查;这份交接稿只描述代码路由,不把 SDK 可分发性视作已经完成。 + +### 7.7 RTX Video + +RTX Video VSR 与 Denoiser 采用显式 normalized-D3D/U8 bridge。inputScale 根据实际输入是 float/canonical 还是 U8/LDR 选择;HDR 状态保留 canonical metadata,并在 backend boundary 形成声明的 bounded/U8 surface。关闭状态直接沿用线上 U8 BGRA/RGBA 合同,input scale、HDR normalization 和 HDR metadata 处于 inactive 状态。 + +RTX Video 的产品级 HDR 能力与外部 texture/API contract 是两层信息。当前代码把外部输入格式、surface ownership、alpha 和 output format 记录在 route catalog 中,证据不足的分支进入 explicit fallback 和日志,后续可通过本地 validation 晋级。 + +### 7.8 光流:AMD OF 与 NVIDIA OF + +HDR 状态的光流流程以 canonical frame 为源,然后由 provider 做 provider-local conversion: + +~~~text +canonical FP16 color + -> provider-local ABGR8/GRAYSCALE8/NV12 or supported surface + -> optical flow provider + -> dense MV/confidence + -> FrameGuidance output +~~~ + +NVOF 仍检查尺寸、输入格式、grid、cost、bidirectional 状态和历史 slot。HDR 路由允许 canonical FP16 进入 hdrToNvofShader,随后写入 provider 所需输入面;SDR 状态优先线上 B8G8R8A8/U8 合同。光流输出不是游戏引擎真实 MV,仍属于捕获颜色帧估计结果。 + +### 7.9 PassThrough 与普通 shader effects + +PassThroughFrames.cpp 将参考 shader 分成: + +~~~text +REFERENCE_HDR_HLSL +REFERENCE_LDR_HLSL +~~~ + +HDR 分支保留 canonical FP16 和 HDR metadata,LDR 分支复用线上 reference surface。Effect compiler 通过 EffectCompilerFlags::HdrCompatibility 注入: + +~~~text +MP_HDR_SATURATE +MP_HDR_ALPHA +MP_HDR_COMPATIBILITY +~~~ + +普通 shader effect 的 route 由 effect-local provider 提供。当前 Group A 效果默认使用显式 R8 SDR-compatible fallback;CAS 额外记录参考实现支持的 FP16/linear 条件 route,并保留独立的编译 variant 和 format matching gate。alpha 写入规则也在 route 中显式声明,避免透明边界外溢。 + +## 8. 透明边界、Alpha 和效果层逻辑 + +本地 HDR 工作的一个重点是透明化边界。canonical surface、effect local surface、publication surface 和 overlay/UI surface 都要在 boundary 上声明 alpha 语义: + +~~~text +Preserve 保留输入 alpha +ForceOpaque 输出 alpha 固定为 1.0 +Premultiplied 按预乘合同处理 RGB/alpha +~~~ + +effect route 若声明 ForceOpaque,adapter output 会明确写入 alpha=1;overlay、PassThrough 和需要透明合成的效果选择 Preserve 或 Premultiplied。关闭 HDR 后,alpha 处理回到线上 effect shader 和 publication contract,HDR adapter 的 alpha policy 不会叠加到 SDR 表面。 + +当前审阅建议优先检查四个实际边界: + +1. HdrSurfaceAdapter 输出到 EffectDrawer 的 local surface; +2. EffectDrawer 输出回 canonical 的 surface; +3. PassThrough/reference 分支进入 shared publication 的 surface; +4. XeSSFG HDR terminal 的 UI/HUD surface。 + +每个边界都需要同一组尺寸、format、alpha mode、color description 和 frame id。只检查 route 名称或 shader 编译结果会遗漏 alpha 泄露、旧 surface 指针和错误 publication 分支。 + +## 9. 0.6.6 合并点的文件级说明 + +当前合入提交 9c2309a2 的主要变化如下: + +| 文件 | 合入内容 | HDR 兼容注意事项 | +| --- | --- | --- | +| DLSSFrameGenerator.cpp | 旧的局部 SEH 包装统一到 NgxRuntimeGuard | 保留 HDR bridge 前后的 descriptor 检查和 generated publication | +| DLSSNRFilter.cpp | NGX 调用保护、fault 后模块保留、inputRevision history reset | 保留 bounded normalization、残差和 auxiliary input | +| GraphicsCaptureFrameSource.cpp/.h | capture close 代际和停止状态处理 | HDR frame pool 只在 gate 开启时申请 | +| ImGuiImpl.cpp | 参数面板交互更新 | HDR-only selector 的可见性由 Profile/参数路由控制 | +| NativeEffectBackendFactory.cpp | NGX fault 初始化保护和 HDR gate | ZeroMV/legacy class 映射按线上行为逐项保留 | +| NgxD3D12Core.cpp | Core init/shutdown/evaluate 统一保护 | HDR native backends 共用同一 fault 状态 | +| OverlayDrawer.cpp/.h | session state、窗口尺寸和参数更新 | 参数窗口状态和 HDR route 状态分离保存 | +| Renderer.cpp/.h | session lifetime、shutdown、overlay restore、live queue、input revision、HDR boundary refresh | 所有 HDR 资源跟随 Renderer session 失效 | +| ScalingWindow.cpp/.h | owner 生命周期、重建状态、失败提示 | NGX fault 后停止新会话,HDR 配置仍由 gate 决定 | +| include/NgxRuntimeGuard.h | 进程级 NGX fault guard | 进程结束前保持 fault 状态,避免错误重入 | +| include/ScalingSessionLifetime.h | 会话代际和停止标志 | capture/backend/presentation 回调统一检查 | +| include/ScalingWindowOwner.h | owner 建立/解除辅助函数 | 初始化和清理阶段与 HDR resource teardown 同步 | +| include/OverlayWindowGeometry.h | 参数窗口布局纯函数 | 捕获区域约束只影响临时显示尺寸 | +| include/ScalingOptions.h | 0.6.6 参数和 HDR flag 统一入口 | EnableHdrCompatibility 是全局 gate | +| AppSettings.cpp | HDR 配置序列化和旧配置读取 | HDR-only 选项不改变 SDR defaults | +| ErrorService.cpp、语言资源 | NGX restart 和 shutdown 错误提示 | 提示不改变 backend fallback 状态 | + +官方 0.6.6 的 EffectParameterRestart.h 相关变更把 DLSSNR 的自动重启判定交给 live parameter 和 inputRevision。本地 branch 保留了这一设计,同时保留 DLSSNR residual、输入分辨率、光流 provider 和 FG resource 等真正需要重建的参数能力表。 + +## 10. 当前实现的源文件导航 + +### HDR 基础设施 + +~~~text +src/Magpie.Core/HdrFrame.h/.cpp +src/Magpie.Core/HdrProtocol.h/.cpp +src/Magpie.Core/HdrAdapterDispatcher.h/.cpp +src/Magpie.Core/HdrEffectBoundary.h/.cpp +src/Magpie.Core/HdrCaptureProcessor.h/.cpp +src/Magpie.Core/HdrColorTransform.h/.cpp +src/Magpie.Core/HdrSurfaceAdapter.h/.cpp +src/Magpie.Core/HdrDiagnostics.h/.cpp +src/Magpie.Core/HdrSourceTestBridge.h/.cpp +~~~ + +### Route catalog 和普通 effect + +~~~text +src/Magpie.Core/GroupAHdrRoutes.h/.cpp +src/Magpie.Core/GroupBHdrRoutes.h/.cpp +src/Magpie.Core/GroupBEffectProtocol.h +src/Magpie.Core/EffectProtocolCatalogC.h +src/Magpie.Core/EffectDrawer.h/.cpp +src/Magpie.Core/EffectCompiler.h/.cpp +src/Magpie.Core/PassThroughFrames.h/.cpp +~~~ + +### Capture、guidance 和 presentation + +~~~text +src/Magpie.Core/FrameSourceBase.h/.cpp +src/Magpie.Core/GraphicsCaptureFrameSource.h/.cpp +src/Magpie.Core/FrameGuidanceService.h/.cpp +src/Magpie.Core/NvidiaOpticalFlowProvider.cpp +src/Magpie.Core/AmdOpticalFlowProvider.cpp +src/Magpie.Core/AdaptivePresenter.cpp +src/Magpie.Core/CompSwapchainPresenter.cpp +src/Magpie.Core/XeSSFGPresenter.cpp +~~~ + +### Native backend + +~~~text +src/Magpie.Core/NativeEffectBackend.h/.cpp +src/Magpie.Core/NativeEffectBackendFactory.cpp +src/Magpie.Core/DLSSNRFilter.h/.cpp +src/Magpie.Core/DLSSSRUpscaler.h/.cpp +src/Magpie.Core/DLSSFrameGenerator.h/.cpp +src/Magpie.Core/FSR2Upscaler.h/.cpp +src/Magpie.Core/FSR3Upscaler.h/.cpp +src/Magpie.Core/XeSSUpscaler.h/.cpp +src/Magpie.Core/XeSSFGPresenter.h/.cpp +src/Magpie.Core/RTXVideoDenoiser.h/.cpp +~~~ + +### 用户配置和资源 + +~~~text +src/Magpie/Profile.h +src/Magpie/ProfilePage.xaml +src/Magpie/ProfileViewModel.h/.cpp/.idl +src/Magpie/AppSettings.cpp +src/Magpie.Core/include/ScalingOptions.h +~~~ + +## 11. 上游合并建议 + +建议按以下顺序移植,提交粒度与当前运行时边界保持一致: + +### 阶段 A:先固定官方 0.6.6 核心 + +1. 以 v0.6.6-experimental 为生命周期基线,先合入 NgxRuntimeGuard、ScalingSessionLifetime、ScalingWindowOwner、capture shutdown、overlay state 和 live parameter queue。 +2. 先运行官方 capture shutdown、overlay geometry、NGX guard 的 CPU/source-level tests。 +3. 确认官方 U8/LDR 链路和所有 legacy backend 类的 descriptor、auxiliary input、publication 完整保留。 + +### 阶段 B:加入全局 HDR gate 和 canonical frame + +1. 添加 EnableHdrCompatibility、配置序列化、Profile UI 和旧配置默认值。 +2. 添加 HdrFrame、ColorDescription、HdrCaptureProcessor,让 capture 输出 canonical FP16。 +3. 添加 HdrEffectBoundary 和 HdrSurfaceAdapter,先接入 pass-through 和一个普通 shader effect。 +4. 对 HDR off 运行 baseline descriptor 对比,确保 route context 为空、publication format 与官方 0.6.5 一致。 + +### 阶段 C:按证据接入 native backend + +1. DLSSNR 作为 bounded normalization vertical slice,先验证 color/depth/MV/exposure/frame id/history。 +2. 接入 DLSS SR、FSR2/3/4、XeSS SR 的 DirectFP16 或声明的 float route。 +3. 接入 RTX Video、NVOF/AMD OF 的 provider-local conversion。 +4. 接入 DLSSFG bridge 和 XeSSFG HDR10 terminal;terminal 之后停止普通 post-effect。 +5. 每个 backend 单独提交 route catalog、adapter constants、descriptor check 和回退行为。 + +### 阶段 D:逐项审查关闭态 + +每个效果都跑两组配置: + +~~~text +HDR off + external display HDR on +HDR on + same capture/backend/options +~~~ + +对比内容包括: + +- effect class 和 factory branch; +- input/output DXGI format; +- transfer/range/color-space; +- depth、motion、exposure、reactive mask 的尺寸和格式; +- parameter selector 的可见性和默认值; +- publication texture 和 presenter color space; +- 日志中的 route、normalization 和 fallback reason。 + +## 12. 原作者审阅清单 + +### 合并正确性 + +- [ ] 官方 0.6.6 的 session lifetime 和 NGX guard 可以单独编译运行。 +- [ ] Renderer::_BuildEffects、NativeEffectBackendFactory 保留所有官方 0.6.5 backend branch。 +- [ ] XeSSZeroMVUpscaler、FSR2ZeroMVUpscaler、FSR3ZeroMVUpscaler 的 SDR 入口和 auxiliary contract 与线上版本一致。 +- [ ] DLSSNR residual、输入分辨率、Frame Guidance 和 FG recovery 仍然走本地 0.6.5 逻辑。 +- [ ] HDR gate 关闭后 route context、HDR texture、HDR presenter 和 HDR compiler macro 都处于 inactive 状态。 +- [ ] HDR-only 参数在 UI 中隐藏/禁用并回到基线值,重新开启 HDR 后再恢复可用状态。 + +### HDR 图像正确性 + +- [ ] capture 的 source format、DXGI color space、transfer、reference white 和 display peak 进入 ColorDescription。 +- [ ] canonical inter-effect surface 始终是 R16G16B16A16_FLOAT。 +- [ ] 进入 DLSSNR 的颜色、深度、光流、exposure 和 frame id 属于同一捕获帧。 +- [ ] DLSSNR bounded normalization 有对应 inverse normalization,history reset 由 inputRevision 触发。 +- [ ] DLSSFG 生成帧在 publish 前完成 bounded-to-canonical conversion,并通过 descriptor check。 +- [ ] XeSSFG 的 HDR terminal 使用 HDR10/PQ R10G10B10A2_UNORM 和正确 color space。 +- [ ] PassThrough、processed publication、overlay/UI surface 采用相同的 HDR output contract。 +- [ ] alpha 规则在每个 boundary 显式可见,透明边界外部保持稳定。 + +### 故障和回退 + +- [ ] NGX SEH 后新 Renderer 立即收到 NgxRestartRequired,旧模块保持隔离。 +- [ ] capture shutdown 的延迟回调拥有 session generation 检查。 +- [ ] FG 失败按一次 history reset、一次重建、当前会话禁用的顺序处理。 +- [ ] route evidence 为 Unknown 时使用显式 fallback,并将原因写入诊断。 +- [ ] route/descriptor、dispatch count、GPU readback、pixel finite count 和 image hash 都能落日志。 + +## 13. 已完成的验证证据 + +### 构建 + +当前已在 E:\Magpie-0.6.5-build 完成 Release x64 全量重建: + +~~~powershell +MSBuild.exe Magpie.slnx /m /t:Rebuild /p:Configuration=Release /p:Platform=x64 +~~~ + +产物: + +~~~text +E:\Magpie-0.6.5-build\bin\x64\Release\Magpie.exe +~~~ + +版本字段: + +~~~text +FileVersion 0.6.6-hdr-fp16compat +ProductVersion 0.6.6-hdr-fp16compat +SHA-256 DC6FB593B8EABFA29D969274207DCB4304918C40E12EECB0CFE754FCD4B962C5 +~~~ + +### 机械检查 + +~~~powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\Run-HdrMechanicalValidation.ps1 +~~~ + +结果:source-level checks 全部通过。检查范围包含全局 gate、canonical FP16、route metadata、DLSSNR/DLSSFG normalization、XeSS/XeSSFG terminal、PassThrough、Optical Flow、alpha 和 HDR-off fallback。 + +### 发布包 + +用户 fork 的 release:[v0.6.6-hdr-fp16compat](https://github.com/konodiodaaaaa1/Magpie/releases/tag/v0.6.6-hdr-fp16compat) + +完整包: + +~~~text +Magpie-Experimental-v0.6.6-hdr-fp16compat-x64.zip +~~~ + +包内含 Magpie.exe、resources.pri、TouchHelper.exe、Updater.exe、运行时 DLL、效果文件和许可证文件,共 226 个文件。ZIP 大小为 487155636 字节,SHA-256 为: + +~~~text +18E30C33893127E03CBE2F184D17065DCE215AE2A5585B701383AF4AF169E3C1 +~~~ + +### 现有诊断文档 + +这些文档保留了实现背景、协议证据和已知验证边界: + +- [HDR_COMPATIBILITY_ARCHITECTURE.md](HDR_COMPATIBILITY_ARCHITECTURE.md) +- [HDR_EFFECT_IMPLEMENTATION_CATALOG.md](HDR_EFFECT_IMPLEMENTATION_CATALOG.md) +- [HDR_MECHANICAL_IMPLEMENTATION_REQUIREMENTS.md](HDR_MECHANICAL_IMPLEMENTATION_REQUIREMENTS.md) +- [HDR_PROTOCOL_EVIDENCE.md](HDR_PROTOCOL_EVIDENCE.md) +- [HDR_PROTOCOL_EVIDENCE.json](HDR_PROTOCOL_EVIDENCE.json) +- [GROUP_A_HDR_ROUTES.md](GROUP_A_HDR_ROUTES.md) +- [HDR_EFFECT_PROTOCOL_AUDIT_20260907.md](HDR_EFFECT_PROTOCOL_AUDIT_20260907.md) +- [20260906-full-chain-audit.md](../diagnostics/20260906-full-chain-audit.md) +- [20260906-hdr-sdr-chain-failure-report.md](../diagnostics/20260906-hdr-sdr-chain-failure-report.md) +- [Run-HdrMechanicalValidation.ps1](../../scripts/Run-HdrMechanicalValidation.ps1) + +## 14. 验证边界与后续工作 + +当前证据已经覆盖源码路由、descriptor、resource contract、session lifetime、NGX fault guard 和 mechanical validation。真实游戏环境中的 GPU 像素回读、厂商驱动差异、HDR 显示器峰值亮度、不同窗口捕获方式和反复启停仍应由上游实机回归完成。 + +当前诊断文档已经明确指出,日志中出现 adapter dispatch 或 EvaluateFeature 成功,只能证明 CPU/GPU 提交路径走通。完整验收还需要为每个 effect 记录:source/output descriptor、route profile、每个 boundary 的纹理格式和尺寸、实际 dispatch count、GPU readback 的 finite count、像素统计、图像 hash、publication 统计和最终 presenter descriptor。 + +建议原作者优先做以下实机矩阵: + +| 场景 | HDR off | HDR on | +| --- | --- | --- | +| 外部 SDR 显示器 + Graphics Capture | 线上 U8 baseline | canonical FP16 capture/adapter | +| 外部 HDR 显示器 + Graphics Capture | 仍然线上 U8 baseline | HDR capture + HDR presenter | +| Desktop Duplication | 线上 capture contract | source interpretation + explicit adapter | +| 单独 DLSSNR | U8、residual、history baseline | FP16 + bounded normalization + history revision | +| DLSSFG | 线上 FG publication | normalized input + canonical generated output | +| XeSS SR / XeSSFG | LDR route / LDR terminal | FP16 SR / HDR10 terminal | +| FSR2/3/4 | 原有 U8/ZeroMV | HDR exposure and auxiliary route | +| RTX Video / NVOF | 原有 U8 provider | provider-local conversion | +| PassThrough / overlay | LDR reference | HDR reference and alpha boundary | +| 连续停止/重新启用 | 0.6.6 lifetime | 0.6.6 lifetime + HDR resource teardown | + +## 15. 附录:复现和审阅命令 + +~~~powershell +# 当前分支和远端 +git -C D:\AI\workspace\Magpie-publish-065-20260908b status --short --branch +git -C D:\AI\workspace\Magpie-publish-065-20260908b remote -v + +# 当前提交和本地 HDR 提交 +git -C D:\AI\workspace\Magpie-publish-065-20260908b show -s --format=fuller 9c2309a2 +git -C D:\AI\workspace\Magpie-publish-065-20260908b show --stat 06435e47 + +# 官方固定点 +git -C D:\AI\workspace\_official066-full show -s --format=fuller v0.6.5-experimental +git -C D:\AI\workspace\_official066-full show -s --format=fuller v0.6.6-experimental +git -C D:\AI\workspace\_official066-full diff --stat v0.6.5-experimental..v0.6.6-experimental + +# 源码机械检查 +powershell -NoProfile -ExecutionPolicy Bypass -File D:\AI\workspace\Magpie-publish-065-20260908b\scripts\Run-HdrMechanicalValidation.ps1 + +# Release x64 构建 +MSBuild.exe D:\AI\workspace\Magpie-publish-065-20260908b\Magpie.slnx /m /t:Rebuild /p:Configuration=Release /p:Platform=x64 +~~~ + +官方合并审阅的推荐入口顺序是:先读本文件第 2、3、5、6 节,再按第 10 节进入源文件,最后使用第 12、13、14 节执行验证。这样可以先确认 0.6.6 会话语义,再判断 HDR adapter 是否只改变了数据格式和色彩处理,最后确认关闭状态完全回到线上 baseline。 + +--- + +本文件描述当前 fork 的实现和证据,官方仓库保持原状;后续上游讨论可以直接以表格中的固定提交、文件路径、route profile 和验证命令作为引用坐标。 diff --git a/docs/experimental/GROUP_A_HDR_ROUTES.md b/docs/experimental/GROUP_A_HDR_ROUTES.md new file mode 100644 index 000000000..c16f0de87 --- /dev/null +++ b/docs/experimental/GROUP_A_HDR_ROUTES.md @@ -0,0 +1,35 @@ +# Group-A HDR routes + +`GroupAHdrRoutes.{h,cpp}` is the effect-local provider for Anime4K, CAS, CRT, +CuNNy, CuNNy2, Diagnostics, FSRCNNX, FXAA and MLAA. It returns structured +`HdrFormatRoute` values plus a diagnostic description of auxiliary resources. +The provider is selected by the effect group prefix in the existing Renderer +boundary update. No effect-name branches were added to the shared dispatcher. + +Every group-A effect has a named route with explicit `R8G8B8A8_UNORM` backend +storage, sRGB/full-range bounded semantics and `ForceOpaque` alpha where the +shader writes alpha one. This is the per-effect SDR fallback for effects whose +public HDR contract is unresolved. The route is still effect-owned, so the +dispatcher can report the exact effect ID and auxiliary resource boundary. + +CAS exposes two declared options through the existing integer-choice parameter +syntax (`hdrFormat`): + +| Option | Route | Evidence/profile | Runtime condition | +|---|---|---|---| +| `0` | `R8G8B8A8_UNORM` | SDR/full-range/sRGB, `SDRCompatible` | Default and fully wired with current compiler | +| `1` | `R16G16B16A16_FLOAT` | linear/scene-linear, `ConditionalFP16`, reference implementation evidence | Experimental selection; current effect compiler still emits the CAS main surface as R8, so `EffectDrawer` format matching keeps this route out of the direct path until a CAS-specific FP16 compile variant is supplied | + +This preserves the distinction between a documented backend option and a +verified HDR-native path. CAS output alpha remains forced opaque in both route +descriptions, matching the HLSL writes. + +## Minimal core wiring + +`Renderer::_BuildEffects` preselects the effect-local route before texture +allocation, and `Renderer::_UpdateHdrEffectBoundaryContexts` refreshes it when +parameters change. `EffectDrawer` uses the selected route's input/output +format for HDR-mode working surfaces, which makes CAS option `1` an actual +FP16 surface path while preserving the existing R8 fallback for option `0`. +The shared dispatcher and conversion helpers retain their existing behavior; +the core wiring is limited to these two call sites and the provider registration. diff --git a/docs/experimental/HDR_COMPATIBILITY_ARCHITECTURE.md b/docs/experimental/HDR_COMPATIBILITY_ARCHITECTURE.md new file mode 100644 index 000000000..a54fb405d --- /dev/null +++ b/docs/experimental/HDR_COMPATIBILITY_ARCHITECTURE.md @@ -0,0 +1,344 @@ +# HDR Compatibility Architecture + +## Status and Scope + +This document is the design and implementation baseline for a self-maintained HDR-compatible Magpie fork. It records decisions reached from capture experiments and protocol research, together with the currently implemented generic work surface. Existing SDR behavior remains the compatibility baseline and remains semantically unchanged while HDR compatibility is disabled. + +The evidence record is [HDR_PROTOCOL_EVIDENCE.md](HDR_PROTOCOL_EVIDENCE.md) and its machine-readable companion [HDR_PROTOCOL_EVIDENCE.json](HDR_PROTOCOL_EVIDENCE.json). + +## User Configuration Switch + +HDR compatibility is controlled by one profile-level option in the user's normal/general configuration page. The persisted configuration key is: + +```json +{ + "enableHdrCompatibility": false +} +``` + +The runtime representation is an independent `ScalingFlags::EnableHdrCompatibility` bit with an `IsHdrCompatibilityEnabled()` accessor. This option controls the global capture-to-presentation HDR path; it is not a capture-method choice and it is not an effect-specific protocol selector. + +When enabled, the selected capture method remains unchanged and the first post-capture stage decodes the source color representation, normalizes exposure, and produces the canonical HDR working surface. When disabled, capture output enters the existing SDR effect and presentation path without the HDR front end or HDR boundary adapters. Effect-specific `R8`/`R10`/`FP16` selectors remain separate settings and are evaluated only after this global switch is enabled. + +## Goal + +The HDR path lets every effect participate in one predictable pipeline while allowing each backend to select the least lossy verified protocol it actually accepts. + +```text +HDR capture + -> decode and exposure normalization + -> canonical HDR working surface + -> per-effect protocol adapter + -> existing effect backend + -> per-effect protocol adapter + -> canonical HDR working surface + -> presentation compatibility encoding +``` + +The canonical inter-effect storage format is: + +```text +DXGI_FORMAT_R16G16B16A16_FLOAT +``` + +`R16G16B16A16_FLOAT` defines storage precision only. It does not define color meaning. Every HDR frame therefore carries a color description. + +```cpp +struct ColorDescription { + DXGI_COLOR_SPACE_TYPE dxgiColorSpace; + ColorPrimaries primaries; + TransferFunction transfer; + ColorRange range; + float referenceWhiteNits; + float displayPeakNits; + bool isSceneReferred; + bool isPreExposed; + bool isInferred; + float preExposure; + HdrMetadata metadata; +}; + +struct HdrFrame { + ID3D11Texture2D* texture; // R16G16B16A16_FLOAT in canonical stages + ColorDescription color; +}; +``` + +The working surface normally contains linear RGB in preserved source primaries. Primary conversion is explicit when a backend requires it. Texture format alone never proves Rec.709, Rec.2020, scRGB, PQ, or HLG semantics. + +## Non-Goals + +- Do not rewrite the existing SDR effect chain while HDR compatibility is disabled. +- Do not force every native backend, HLSL effect, or model to process FP16 internally. FP16 is the canonical inter-effect work surface; an effect boundary may select R8, R10, or FP16 when that route is declared and verified. +- Do not claim HDR support because a shader happens to compile with a floating-point texture. +- Do not replace the user's capture-method selection with a separate HDR-only capture method. +- Do not label a lossy SDR round trip as native HDR preservation. + +## User-Facing Control + +Add one global general setting: + +```text +HDR compatibility: Off / On +``` + +`Off` retains current capture, effect, backend handoff, and presentation behavior. `On` enables format-aware capture normalization and per-effect adapters. Effects expose a protocol selector only when more than one usable path has evidence; single-path effects remain selector-free. + +The current generic boundary implementation creates an effect-local input/output surface in the format declared by the existing effect description, performs the selected adapter operation around the effect, and returns a canonical FP16 surface to the next effect. Routes with no concrete evidence use the structured SDR-compatible fallback and emit diagnostics. When HDR compatibility is enabled, the shared publication ring and presenter use canonical FP16/scRGB surfaces, so the terminal path retains HDR values through frontend composition. When HDR compatibility is disabled, the existing R8 publication and presenter path remains active. + +## Capture Contract + +Every existing capture method retains its selection and window-acquisition behavior. HDR handling begins after it produces a texture. + +```text +selected capture method + -> captured texture + source DXGI format + source color-space metadata + -> HDR decode / source transfer handling / exposure normalization + -> canonical RGBA16F HdrFrame +``` + +The capture stage preserves source format and DXGI color-space data, decodes known PQ/HLG/scRGB/SDR transfers, preserves available primaries and metadata, and produces a visually correct HDR frame before the first effect. This prevents HDR code values from being sampled as SDR values and clipping highlights before any effect executes. + +For Graphics Capture, HDR mode requests an `R16G16B16A16_FLOAT` frame pool and treats the returned surface as linear scRGB. The other existing capture methods retain their established 8-bit BGRA capture surfaces; those surfaces are interpreted as inferred sRGB display-referred values because the capture operation has already selected that representation. Display PQ metadata supplies peak-luminance normalization and does not redefine the stored 8-bit code values. + +`R10G10B10A2_UNORM` alone is not an HDR declaration. The associated color-space contract determines whether it is HDR10/PQ or another representation. + +## Canonical Effect Boundary + +Every non-terminal effect follows this shape while HDR compatibility is enabled: + +```text +canonical RGBA16F HdrFrame + -> adapter input conversion + -> backend-specific input texture(s) + -> existing HLSL/native effect + -> backend-specific output texture(s) + -> adapter output conversion + -> canonical RGBA16F HdrFrame +``` + +The backend only sees resources allowed by its protocol. The rest of the pipeline only sees the canonical handoff surface. This isolates texture allocation, format conversion, color conversion, normalizers, alpha policy, and metadata handling from the effect implementation. + +```cpp +enum class EffectColorProfile { + DirectFP16, + BoundedHDR, + SDRCompatible, + Unknown, +}; + +struct EffectColorProtocol { + std::string id; + EffectColorProfile profile; + ColorSurfaceContract input; + ColorSurfaceContract output; + EffectColorAdapter adapter; + EvidenceLevel evidence; + uint8_t qualityRank; + uint8_t performanceRank; + bool userSelectable; + bool isPresentationTerminal; +}; +``` + +`isPresentationTerminal` is topology information, not a fifth color profile. Frame-generation paths often produce swap-chain frames and end a render branch instead of returning a normal inter-effect texture. + +## Adapter Profiles + +### Protocol selection at the effect boundary + +The canonical work surface and the backend protocol are separate concepts. Every HDR-compatible chain hands effects a canonical `R16G16B16A16_FLOAT` frame between adapters, while each concrete effect may expose more than one backend protocol option when evidence supports it. + +Each option records: + +```text +effectId +optionId +inputFormat +outputFormat +inputColorModel +outputColorModel +range +transferFunction +alphaMode +evidenceLevel +hdrNative +adapterProfile +``` + +`hdrNative` means that the selected representation carries the effect's HDR input semantics directly. It does not mean that every format accepted by the backend is HDR-native. A floating-point option may be HDR-native, an `R10G10B10A2_UNORM` option may be a bounded HDR10/PQ route, and an `R8G8B8A8_UNORM` option normally requires an SDR-compatible HDR adapter. + +The selector is shown only when the concrete effect has more than one reliable protocol option. The default order in HDR mode is: verified HDR-native FP16, verified HDR-native R10/HDR10, verified bounded floating-point, then SDR-compatible R8. In SDR mode the existing default and existing path remain unchanged. + +### DirectFP16 + +Use this profile when the upstream API or a verified reference path accepts a suitable linear floating-point HDR resource. + +```text +RGBA16F canonical frame + -> required primaries / transfer conversion only + -> FP16-capable backend + -> required output conversion only + -> RGBA16F canonical frame +``` + +No SDR tone mapping or U8 quantization belongs in this path. Typical candidates are DLSS SR, FSR2, FSR3 SR, FSR4, NIS, and XeSS SR. CAS has a reference FP16 path and remains conditional until local validation confirms the exact Magpie backend path. + +### BoundedHDR + +Use this profile when a backend accepts a bounded numeric domain, needs a display-specific HDR representation, or has an experimentally validated HDR normalizer. + +```text +RGBA16F canonical frame + -> reversible bounded-domain encoding E() + -> backend + -> inverse-domain decoding E^-1() + -> RGBA16F canonical frame +``` + +The encoding stores the curve identifier, reference white, normalization scale, peak/headroom, source transfer assumption, and inverse curve. A generic implementation must not hard-code one scale for all effects. + +DLSSNR uses this profile based on local experiments: + +```text +HDR -> bounded HDR normalization -> DLSSNR -> inverse normalization -> HDR +``` + +The normalizer preserves the tested HDR luminance relationships while placing model input in its bounded domain. It does not imply that DLSSNR has an officially published native HDR texture contract. + +### SDRCompatible + +Use this profile when the documented backend interface is SDR, UNORM, or U8-only. + +```text +RGBA16F canonical frame + -> primary conversion if required + -> HDR-to-SDR appearance mapping + -> SDR transfer encoding + -> quantize to backend format + -> backend + -> dequantize / SDR decode + -> SDR-to-HDR reconstruction policy + -> RGBA16F canonical frame +``` + +This path is compatibility-oriented. The SDR appearance mapping is not mathematically lossless, so output reconstruction is an approximation. The adapter keeps the color transform, tone-mapping function, quantizer, and reconstruction policy together as one named protocol. + +For U8 RGBA/BGRA backends, channel encoding normally is: + +```text +u8 = round(saturate(sdrEncoded) * 255) +``` + +RTX Video VSR and RTX Video Denoiser use interleaved BGRA or RGBA U8 GPU buffers. The `0..255` domain is a storage-code boundary, not a linear HDR luminance domain. + +### Unknown + +Use this profile when source evidence does not establish an upstream mapping or a texture/color contract. HDR mode selects an adapter only after a local test. The catalog retains the evidence gap so a later experiment can promote the entry deliberately. + +## Presentation-Terminal Effects + +Frame generation often binds to backbuffer and swap-chain resources. Model it as a terminal branch: + +```text +canonical RGBA16F HdrFrame + -> presentation-specific encoder + -> frame-generation backend + -> proxy swap chain / presenter +``` + +XeSS FG has the clearest known contract: + +```text +RGBA16F canonical HDR + -> PQ / BT.2100 encoding + -> R10G10B10A2_UNORM + -> XeSS FG + -> proxy swap chain presentation +``` + +XeSS FG requires HDR10 / BT.2100 with `R10G10B10A2_UNORM` for its documented HDR path and does not accept FP16 HDR or scRGB. The backbuffer, HUD-less texture, and UI texture agree on format, dimensions, and color space. The renderer prevents normal post-effects from being scheduled after a presentation-terminal backend unless a verified re-capture path exists. + +## Resource and Synchronization Model + +The HDR path allocates resources from declared surface contracts instead of a global R8 assumption. + +1. Keep one canonical FP16 texture pool per active render size. +2. Reuse canonical ping-pong surfaces for ordinary chains where dependencies permit. +3. Allocate protocol-specific scratch textures only around the effect that needs them. +4. Cache adapter pipelines by source contract, destination contract, dimensions, and shader parameters. +5. Preserve SRV/UAV/RTV state transitions at adapter boundaries so native effects keep their resource-state expectations. +6. Keep alpha semantics explicit. Effects that discard alpha write a defined value, normally `1.0`, before returning to the canonical chain. +7. Keep HDR metadata separate from texture metadata. A copy or shared handle does not preserve semantic color state on its own. + +Performance policy: FP16 stays at effect boundaries, while every backend uses the least lossy verified internal format it accepts. Neural filters and frame generation dominate the cost in most cases. Adapter allocations and conversions still need pooling and fusion because they occur every frame. + +## Effect Selection and Configuration + +Each descriptor declares candidate protocols, evidence level, input/output contract, auxiliary resource needs, terminal state, and adapter parameters. + +```text +Effect name + - Default protocol + - Candidate protocol list + - Evidence level + - Input/output format contract + - Required auxiliary resources + - Presentation-terminal flag + - Adapter parameters +``` + +Examples: + +- XeSS SR defaults to DirectFP16; a lower-bandwidth `R11G11B10_FLOAT` internal option belongs behind later verification and only when alpha is irrelevant. +- RTX Video VSR exposes an SDR/U8 path until a higher-precision public contract exists. +- DLSSNR exposes its tested bounded-HDR normalizer parameters after the initial defaults are stable. +- XeSS FG is bound to its HDR10 `R10G10B10A2_UNORM` terminal protocol and has no FP16 selector. + +## Validation Plan + +The user performs visual/game tests. The implementation records diagnostics sufficient to identify a wrong adapter choice. + +### Capture validation + +- Compare no-effect HDR output with the source window. +- Confirm highlight detail before the first effect. +- Confirm SDR windows remain unchanged while the global option is off. +- Log source DXGI format, DXGI color space, decode path, and canonical color description. + +### Per-effect validation + +- Verify input and output resources match the selected profile. +- Test dark detail, saturated red, UI white, specular highlights, and low-light gradients. +- Check alpha behavior on overlays and composition paths. +- Record GPU time for adapter input, backend, and adapter output separately. +- For SDR-compatible effects, compare against a no-effect HDR reference and record reconstruction loss. + +### Regression validation + +- HDR option off: unchanged SDR pipeline. +- HDR option on with no effects: correct capture and presentation. +- One effect at a time: contract and visual validation. +- Mixed chain: canonical FP16 handoff between non-terminal effects. +- Terminal frame generation: no invalid post-effect after presentation handoff. + +## Delivery Order + +1. Add format-aware capture normalization and `HdrFrame` while retaining the disabled SDR branch. +2. Add adapter infrastructure and diagnostics without changing individual effect backends. +3. Implement DLSSNR as the bounded-HDR vertical slice. +4. Add verified DirectFP16 effects: NIS, XeSS SR, DLSS SR, and FSR2/FSR3 SR/FSR4 where auxiliary inputs are valid. +5. Add SDR-compatible adapters: RTX Video VSR, RTX Video Denoiser, FSR1, xBRZ, NVIDIA Optical Flow, and SMAA. +6. Add terminal frame generation: XeSS FG, then FSR3 FG and DLSS FG after their external temporal contracts are resolved. +7. Promote conditional and unknown entries one by one through local texture-contract tests. + +## Decision Record + +- Canonical handoff: `DXGI_FORMAT_R16G16B16A16_FLOAT` plus explicit `ColorDescription`. +- SDR behavior: preserved while HDR compatibility is off. +- Capture: every selected capture method feeds one HDR normalization stage. +- Backend precision: selected per effect protocol, never globally forced. +- DLSSNR: bounded-HDR normalization with inverse normalization, based on local experiments. +- RTX Video VSR/Denoiser: SDR/U8 adapters with explicit `0..255` quantization. +- XeSS FG: HDR10 `R10G10B10A2_UNORM` terminal protocol outside the ordinary FP16 chain. +- Evidence policy: public API contracts, reference implementations, GitHub experiments, and local experiments remain separate evidence levels. diff --git a/docs/experimental/HDR_EFFECT_IMPLEMENTATION_CATALOG.md b/docs/experimental/HDR_EFFECT_IMPLEMENTATION_CATALOG.md new file mode 100644 index 000000000..f0495e805 --- /dev/null +++ b/docs/experimental/HDR_EFFECT_IMPLEMENTATION_CATALOG.md @@ -0,0 +1,290 @@ +# HDR Effect Implementation Catalog + +## Purpose + +This catalog translates the protocol evidence matrix into an implementation-facing list. It records accepted backend protocols separately from HDR-native protocols, which adapter profile HDR mode should choose, what conversion happens at the boundary, and which conclusions still need local testing. + +The authoritative source URLs and research notes are in [HDR_PROTOCOL_EVIDENCE.md](HDR_PROTOCOL_EVIDENCE.md) and [HDR_PROTOCOL_EVIDENCE.json](HDR_PROTOCOL_EVIDENCE.json). This catalog records integration decisions; it does not claim that current Magpie already supports these paths. + +## Profile Legend + +| Profile | Meaning at the HDR adapter boundary | +|---|---| +| DirectFP16 | Backend has a documented or sufficiently verified floating-point HDR-capable route. Canonical FP16 stays FP16 through the backend boundary. | +| BoundedHDR | Backend needs a bounded HDR domain or a specific display representation. Use a named normalizer or encoder and its paired inverse/presentation policy. | +| SDRCompatible | Backend contract is SDR, UNORM, U8, or another bounded SDR domain. Convert canonical HDR to a defined SDR proxy and restore into the canonical domain afterward. | +| ConditionalFP16 | A reference implementation or user-shader environment uses FP16 while a general HDR color contract remains unproven. Keep this opt-in and experimental. | +| Unknown | No stable upstream mapping or format contract. Keep HDR use behind a local validation gate. | +| PresentationTerminal | Backend produces or owns presentation frames. It ends an effect branch and does not return a normal canonical texture. | + +## Protocol Option Semantics + +The canonical inter-effect surface is always `R16G16B16A16_FLOAT`. This does not limit the format used inside an effect boundary. A concrete backend may expose multiple selectable protocol options when each option has reliable evidence: + +| Backend option | HDR-mode meaning | Adapter behavior | +|---|---|---| +| `R16G16B16A16_FLOAT` | Candidate HDR-native linear/scene-referred route; transfer and range still need metadata | Direct FP16 or bounded-HDR adapter according to the contract | +| `R10G10B10A2_UNORM` | Bounded HDR10/PQ route when the backend explicitly defines BT.2100/HDR10 semantics | Encode PQ/BT.2100 and preserve terminal metadata | +| `R8G8B8A8_UNORM` | Usually SDR-compatible storage, even when accepted by an HDR-capable effect | HDR-to-SDR mapping, clamp/quantize, backend call, paired reconstruction | + +The existence of an R8 and FP16 option means the effect has two selectable backend routes. It does not make the R8 route HDR-native. The implementation must therefore distinguish `acceptedFormatRoutes`, `hdrNativeFormatRoutes`, and `hdrAdapterFormatRoutes` for every effect. + +```json +{ + "acceptedFormatRoutes": ["R8G8B8A8_UNORM", "R16G16B16A16_FLOAT"], + "hdrNativeFormatRoutes": ["R16G16B16A16_FLOAT"], + "hdrAdapterFormatRoutes": ["R8G8B8A8_UNORM"] +} +``` + +An effect-level selector is displayed only when more than one reliable route is available. The selector controls the effect boundary protocol, while all preceding and following effects continue to exchange canonical FP16. SDR mode keeps the current default and does not inherit HDR selector behavior. + +## Integration Summary + +| Effect group / backend | Known accepted resources | HDR adapter decision | Priority | +|---|---|---|---| +| Anime4K | Public texture/color contract unavailable | Unknown | Later | +| CAS | Reference CLI: `R8G8B8A8_UNORM` and `R16G16B16A16_FLOAT` | Two selectable routes: FP16 candidate HDR-native; R8 SDR-compatible | High | +| CRT, CuNNy, CuNNy2, Diagnostics | No unique public API contract | Unknown | Later | +| DLSS SR | HDR color path; depth, MV, exposure, history resources | DirectFP16 | High | +| DLSS FG | Backbuffer/HUD/UI share format and color space | BoundedHDR + PresentationTerminal | Later | +| DLSSNR | Local bounded-HDR experiment is validated | BoundedHDR | First vertical slice | +| FSR1 | Perceptual sRGB `[0,1]` core contract | SDRCompatible | Medium | +| FSR2, FSR3 SR, FSR4 | HDR SR interfaces with temporal auxiliary inputs | DirectFP16 | High | +| FSR3 FG | Backbuffer, HUD-less, MV, depth, optical flow | BoundedHDR + PresentationTerminal | Later | +| FSRCNNX, FXAA, MLAA, Pixel Art, Sharpen | No stable generic format contract | Unknown | Later | +| NIS | Non-integer color resources; PQ and linear HDR modes | DirectFP16 | High | +| NNEDI3, RAVU | mpv `rgba16f/rgba16hf` implementation evidence | ConditionalFP16 | Medium | +| RTX Video VSR/Denoiser | GPU BGRA/RGBA interleaved U8 | SDRCompatible | High | +| RTX Video HDR | Product behavior known; public API texture contract unavailable | Unknown | Do not integrate yet | +| SMAA, xBRZ | RGBA / U8-class resource contracts | SDRCompatible | Medium | +| XeSS SR | `RGBA16F`, `R11G11B10F`, `RGBA8` and linear formats | DirectFP16 | High | +| XeSS FG | HDR10/BT.2100 `R10G10B10A2_UNORM`; no FP16 HDR/scRGB | BoundedHDR + PresentationTerminal | High after core path | +| NVIDIA Optical Flow | `GRAYSCALE8`, `NV12`, `ABGR8`; flow output | SDRCompatible auxiliary path | Medium | +| AMD FidelityFX Optical Flow | Color format not publicly enumerated; flow output | BoundedHDR auxiliary path | Later | + +## Direct FP16 Core Effects + +### DLSS Super Resolution + +- **Input:** HDR-capable application color buffer, motion vectors (`RG16F` or `RG32F`), depth, optional `R16F` exposure, and temporal controls. +- **Output:** Application output buffer; history output is commonly `RGBA16F`. +- **Adapter:** Keep canonical FP16 color and convert only required primary/transfer semantics. Allocate native-format auxiliary resources separately. +- **Boundary:** Color supports FP16. Full temporal quality still needs genuine render-space MV, depth, exposure, jitter, reset, and history behavior; desktop capture does not create equivalent data. + +### FSR2, FSR3 Super Resolution, and FSR4 + +- **Input:** Application-specified color buffer, one-channel depth, two-component motion vectors, exposure, and masks where required. +- **Output:** Application output resource; documented implementations use FP16 internal surfaces. +- **Adapter:** Keep color in canonical FP16 and provide native auxiliary inputs only where external code can supply valid data. +- **Boundary:** Their HDR color route and their temporal resource contract are independent constraints. + +### NVIDIA Image Scaling + +- **Input/output:** Non-integer color resources; documented LDR, PQ, and linear-HDR modes. +- **Adapter:** DirectFP16 with the chosen NIS HDR range, sampler, and resource-state contract stored in its protocol. +- **Boundary:** Spatial behavior makes NIS a practical early external HDR target. + +### XeSS Super Resolution + +- **Input:** `R16G16B16A16_FLOAT`, `R11G11B10_FLOAT`, `R8G8B8A8_UNORM`, and supported linear formats; MV and depth have native formats. +- **Output:** Same format and color space as the input color texture; alpha requires canonical cleanup. +- **Adapter:** Use canonical FP16 by default. A later `R11G11B10_FLOAT` option needs verified alpha-free usage. +- **Boundary:** Temporal auxiliary inputs remain independent from HDR color compatibility. + +## Conditional FP16 Effects + +### CAS + +- **Known resources:** The official reference CLI uses `R8G8B8A8_UNORM` and `R16G16B16A16_FLOAT`; shader paths include linear handling and FP16/FP32 math variants. +- **Adapter:** Start with the current SDR-compatible selection. Add a hidden or experimental DirectFP16 profile after the exact Magpie CAS path proves that it preserves linear FP16 values and alpha. +- **Boundary:** Reference CLI behavior proves a viable FP16 direction. It does not alone prove every CAS integration's resource contract. + +### NNEDI3 + +- **Known resources:** mpv user-shader variants use `rgba16f/rgba16hf` internal surfaces and focus on luma. +- **Adapter:** Treat FP16 as implementation-format capability. Feed a defined luma or RGB projection only after identifying the exact variant; restore alpha and color semantics at the boundary. +- **Boundary:** No public native HDR color contract is established. + +### RAVU + +- **Known resources:** mpv variants use `rgba16f/rgba16hf`; RGB, YUV, luma-only, gather, and compute variants differ. +- **Adapter:** Bind protocol to the exact variant. RGB variants can be FP16 candidates; YUV and luma paths require explicit color/luma conversion. +- **Boundary:** FP16 surface availability does not define PQ, HLG, scene-linear range, or metadata behavior. + +## Bounded HDR and Frame-Generation Paths + +### DLSSNR + +- **Known local result:** Direct HDR texture values create color failure, including red leakage. A bounded HDR normalization path produces the intended result. +- **Required chain:** `canonical FP16 HDR -> normalize -> DLSSNR -> inverse normalize -> canonical FP16 HDR`. +- **Adapter state:** Curve identity, scale, reference white, peak/headroom, alpha rule, source color assumption, and inverse parameters. +- **Boundary:** This is a local experimental protocol. It stays separate from official vendor-contract claims and needs capture/output diagnostics. + +### FSR3 Frame Generation + +- **Known resources:** Presentation backbuffer, optional HUD-less buffer, depth, MV, and optical-flow resources. +- **Adapter:** Treat it as a bounded presentation path whose swap-chain format and color semantics agree across composed inputs. +- **Boundary:** Frame generation needs temporal and motion information unavailable from a simple desktop image. Color adaptation alone does not satisfy the backend contract. + +### XeSS Frame Generation + +- **Known resources:** HDR uses `R10G10B10A2_UNORM` with HDR10 / BT.2100. HUD-less and UI resources match backbuffer format, color space, and size. UI alpha is explicit. +- **Adapter:** Encode canonical FP16 to PQ/BT.2100 `R10G10B10A2_UNORM`, run XeSS FG, then present through its proxy swap chain. +- **Boundary:** The documented HDR route excludes FP16 HDR and scRGB. XeSS FG ends the ordinary effect chain. + +### DLSS Frame Generation + +- **Known resources:** Backbuffer, HUD-less/UI resources, MV/depth, and an output texture matching backbuffer format. +- **Adapter:** Keep behind a presentation-terminal abstraction until the exact external format profile is verified. +- **Boundary:** Current evidence establishes same-format behavior and HDR capability while a complete external texture matrix remains unresolved. + +### AMD FidelityFX Optical Flow + +- **Known resources:** Color input feeds a transfer/luminance conversion; outputs include `R16G16_SINT` flow vectors and `R32_UINT` scene-change data. +- **Adapter:** Use a bounded-HDR luminance adapter after a local test identifies source range and transfer semantics. +- **Boundary:** Optical flow is an auxiliary-resource producer, not a normal RGB effect output. + +## SDR-Compatible Effects + +### FSR1 + +- **Known resources:** Core input uses perceptual/sRGB `[0,1]`; RCAS has invalid behavior for negative values. +- **Adapter:** `canonical FP16 HDR -> SDR appearance mapping -> sRGB [0,1] -> FSR1 -> SDR decode/reconstruction`. +- **Boundary:** Surrounding helper code contains HDR conversion utilities, while the core algorithm contract remains SDR oriented. + +### RTX Video VSR + +- **Known resources:** GPU-resident BGRA or RGBA interleaved U8 input/output. +- **Adapter:** Convert canonical HDR to selected SDR appearance, encode SDR transfer, and quantize each channel to U8 `0..255`. Convert output back through the paired reconstruction policy. +- **Boundary:** U8 is a hard precision boundary. The public interface has no native HDR transfer, primary, metadata, or FP16 contract. + +### RTX Video Denoiser + +- **Known resources:** Same BGRA/RGBA U8 GPU-buffer interface; denoise/deblur output stays at input resolution. +- **Adapter:** Use the same SDR/U8 protocol as VSR, with no scaling assumption. +- **Boundary:** Keep VSR and Denoiser protocol identifiers separate despite shared encoding. + +### SMAA, xBRZ, and NVIDIA Optical Flow + +- **SMAA:** RGBA color texture plus edge/area/search/depth resources. Intermediate textures are normally non-sRGB; final neighborhood blending can be sRGB. +- **xBRZ:** Community TypeScript/WASM path uses RGBA U8 `Uint8ClampedArray` with alpha and scale factors 2 through 6. +- **NVIDIA Optical Flow:** `GRAYSCALE8`, `NV12`, or `ABGR8` input; flow output is signed fixed-point `SHORT2`. +- **Adapter:** Use SDR-compatible conversion. Optical-flow vector outputs remain outside the canonical RGB chain. + +## Effects Awaiting a Stable Public Contract + +### Anime4K + +- **Known resources:** The shader family is distributed as user shaders with variant-specific texture declarations; one universal color-format contract is not established. +- **Adapter:** Keep `Unknown` until the selected preset is inspected. If the preset declares normalized SDR sampling, use `SDRCompatible`; if it declares floating-point linear sampling, promote it to `ConditionalFP16` after a range test. +- **Boundary:** Do not infer HDR support from shader compilation. Record texture format, sampler state, transfer assumption, and whether negative or above-one values survive each pass. + +### CRT + +- **Known resources:** CRT presets combine color, scanline, mask, and sometimes feedback passes; the exact format and transfer behavior depend on the preset. +- **Adapter:** Treat each preset as an independent protocol. Default to `SDRCompatible` and require a per-pass range audit before allowing direct FP16 HDR. +- **Boundary:** Scanline and mask math can be visually valid for SDR while clipping HDR highlights or changing saturated colors. + +### CuNNy and CuNNy2 + +- **Known resources:** Neural shader models use model-specific channel, normalization, and tensor-size assumptions; a single public HDR contract is not established. +- **Adapter:** Keep `Unknown`. A future adapter must identify the model's normalization interval, tensor format, channel order, and output denormalization before enabling HDR. +- **Boundary:** Treat model normalization as part of the protocol. A generic FP16 surface alone does not establish that the network accepts HDR values. + +### Diagnostics + +- **Known resources:** Diagnostic effects may inspect or rewrite channels, ranges, alpha, or color-space metadata depending on the diagnostic selected. +- **Adapter:** Run diagnostics on the canonical FP16 surface where possible, while preserving a raw-capture tap and the pre-adapter color description. +- **Boundary:** Diagnostics are not an image-quality backend. Their output must never be silently reused as a normal HDR frame unless the selected diagnostic explicitly promises that behavior. + +### FSRCNNX + +- **Known resources:** User-shader variants expose model- and preset-specific texture declarations; no stable general HDR input/output contract is established. +- **Adapter:** Default to `SDRCompatible`. Permit `ConditionalFP16` only for a tested variant whose normalization and output range are documented. +- **Boundary:** Verify RGB versus luma-only operation, alpha handling, and any hard clamp in every model pass. + +### FXAA and MLAA + +- **Known resources:** Both are neighborhood-based anti-aliasing families commonly implemented over normalized color textures, with auxiliary edge/luma data varying by implementation. +- **Adapter:** Use `SDRCompatible` until the exact implementation proves unclamped FP16 behavior. Keep edge/luma auxiliary resources separate from the canonical RGB surface. +- **Boundary:** Anti-aliasing thresholds are often tuned in display-referred units; direct scene-linear HDR can change edge detection even when the texture format is floating point. + +### Pixel Art + +- **Known resources:** Presets vary between integer nearest-neighbor logic, palette tests, and shader-specific color thresholds. +- **Adapter:** Default to `SDRCompatible`; allow direct FP16 only for a preset whose comparisons are explicitly range-independent. +- **Boundary:** Preserve exact alpha and integer-like color comparisons. Do not apply an HDR tone curve inside the pixel-art backend without recording it as a deliberate artistic transform. + +### RTX Video HDR + +- **Known resources:** Product-level HDR behavior is known, while a public external texture/API contract suitable for this effect catalog remains unresolved. +- **Adapter:** Keep `Unknown` and do not route canonical frames into it by assumption. Integrate only after the exact API, accepted resource formats, color space, metadata, and output ownership are documented. +- **Boundary:** A display feature or driver capability is not evidence of an externally callable native-HDR filter contract. + +### Sharpen + +- **Known resources:** The name covers multiple backends and shader families, including normalized SDR and floating-point implementations. +- **Adapter:** Resolve by concrete backend identifier. Use `SDRCompatible` for normalized SDR variants and `ConditionalFP16` for variants with verified unclamped linear-FP16 behavior. +- **Boundary:** Negative lobes, overshoot clamps, alpha treatment, and sharpening strength units must be recorded because they directly affect HDR highlight reconstruction. + +## Uniform Runtime Order + +When HDR compatibility is enabled, every ordinary effect branch follows this order: + +1. Capture with the existing capture method and retain source color metadata. +2. Decode the captured transfer function and normalize exposure into canonical `RGBA16F`. +3. Select the effect profile from the concrete backend identifier and evidence level. +4. Convert canonical HDR into the backend's declared input protocol. +5. Run the existing backend without changing its SDR-mode behavior. +6. Convert the backend output back into canonical `RGBA16F`, restoring alpha according to the profile. +7. Continue to the next effect, or hand the terminal presentation branch to its required swap-chain format. +8. Encode the final canonical frame for the existing presentation path and apply metadata at presentation time. + +If a backend has no reliable protocol evidence, use the existing compatible SDR route and record the conversion. Unknown effects must not silently receive direct HDR values. + +## Runtime Diagnostic Record + +Each adapted effect should emit a compact record containing: + +```text +effect identifier +selected profile and evidence level +input DXGI format and ColorDescription +adapter input conversion and numeric range +backend texture formats and resource states +backend output format and range +adapter output conversion +alpha rule and auxiliary-resource formats +GPU time: pre-adapter / backend / post-adapter +observations: highlights / saturated red / UI white / dark gradients +``` + +The record should also include whether the path was direct, bounded, SDR-compatible, or unknown-fallback, plus the normalizer identity and parameters when a bounded-HDR path is used. + +## Validation Matrix + +| Test stage | Required check | Pass condition | +|---|---|---| +| Capture | HDR transfer decode and exposure | No highlight expansion caused by treating HDR code values as SDR | +| Single effect | Adapter round trip | Neutral gray, saturated red, white UI, and dark gradients remain stable | +| Chained effects | Repeated boundary conversions | No progressive hue drift, highlight pumping, or alpha loss | +| Temporal effect | History, MV, depth, and exposure agreement | All temporal inputs use the same frame color description and dimensions | +| Presentation terminal | Swap-chain and metadata ownership | Output format and metadata match the terminal backend contract | +| SDR regression | HDR option disabled | Existing SDR path and output remain unchanged | + +## Implementation Priority + +1. Implement the canonical capture-to-`RGBA16F` HDR front end and make it independently toggleable. +2. Integrate direct FP16 SR effects whose contracts are already sufficiently established. +3. Integrate bounded adapters for DLSSNR and explicitly bounded temporal/presentation paths. +4. Integrate SDR-compatible VSR, Denoiser, FSR1, SMAA, xBRZ, and optical-flow auxiliary paths. +5. Add per-backend selectors only where multiple accepted protocols have reliable evidence. +6. Keep unknown effects behind diagnostics and promote them only after a concrete preset/backend audit. + +## Current Non-Goals + +- Replacing any backend model or retraining a neural effect. +- Changing the existing SDR path when HDR compatibility is disabled. +- Treating `R16G16B16A16_FLOAT` as a color space without transfer and gamut metadata. +- Claiming native HDR support for an effect from format enumeration, compilation success, or product-level marketing behavior alone. diff --git a/docs/experimental/HDR_EFFECT_PROTOCOL_AUDIT_20260907.md b/docs/experimental/HDR_EFFECT_PROTOCOL_AUDIT_20260907.md new file mode 100644 index 000000000..6b01cd151 --- /dev/null +++ b/docs/experimental/HDR_EFFECT_PROTOCOL_AUDIT_20260907.md @@ -0,0 +1,100 @@ +# Magpie 0.6.5 HDR Effect Protocol Audit + +Date: 2026-09-07 +Canonical surface: `R16G16B16A16_FLOAT`, linear scRGB, reference white 80 nit. + +This audit separates source-level protocol evidence from a live SDK result. A +route is marked `Live` only when the current native backend performs its +production Draw/Evaluate/Run call and the renderer records the resulting +texture. A route marked `Contract` has source and SDK evidence but still needs +a target-machine pixel capture. `Fallback` means the canonical bridge is +implemented and the effect remains on the explicit compatibility path. + +## Effect Matrix + +| effectId / variant | selected HDR protocol | backend call | status | evidence / blocking item | +|---|---|---|---|---| +| DLSS SR | DirectFP16, linear | `DLSSSRUpscaler::Draw` -> NGX Evaluate | Contract | zero MV/depth contract; HDR flag/exposure still needs live pixel proof | +| FSR1 | SDRCompatible, sRGB R8 | shader pass | Fallback | FSR1 core is bounded perceptual sRGB | +| FSR2 | DirectFP16, linear | `FSR2Upscaler::Draw` -> FSR2 dispatch | Contract | HDR flag/exposure/depth are effect-local open items | +| FSR3 SR | DirectFP16, linear | `FSR3Upscaler::Draw` -> FSR3 dispatch | Contract | current provider path still needs HDR linear dispatch proof | +| FSR4 | DirectFP16, linear | `FSR3Upscaler::Draw(useFsr4)` | Contract | FSR4 ML/provider contract and live output proof pending | +| XeSS SR | DirectFP16, FP16 linear (default) | `XeSSUpscaler::Draw` -> `xessD3D12Execute` | Contract | `XESS_INIT_FLAG_LDR_INPUT_COLOR` is now omitted for FP16/R10 HDR routes | +| NIS | DirectFP16 candidate | shader pass using NIS linear HDR mode | Contract | exact Magpie `NIS_HDR_MODE` compile route and live pixel proof pending | +| CAS | SDRCompatible R8 | shader pass | Fallback | current shader saturates RGB and forces alpha 1 | +| Anime4K | Unknown -> SDR fallback | shader passes | Fallback | preset-specific FP16 intermediates lack a color contract | +| CRT | Unknown -> SDR fallback | shader passes | Fallback | preset gamma/clamp behavior is SDR-bound | +| CuNNy | Unknown -> SDR fallback | shader passes | Fallback | SNORM model tensors and normalization are unverified HDR | +| CuNNy2 | Unknown -> SDR fallback | shader passes | Fallback | UNORM model tensors and normalization are unverified HDR | +| FSRCNNX | Unknown -> SDR fallback | shader passes | Fallback | FP16 intermediates do not define input/output HDR semantics | +| FXAA | Unknown -> SDR fallback | shader passes | Fallback | luma threshold has no HDR scale contract | +| MLAA | Unknown -> SDR fallback | shader passes | Fallback | R8 edge/count auxiliaries are bounded SDR resources | +| SMAA | SDRCompatible R8/FP16 auxiliaries | shader passes | Fallback | alpha and luma threshold are variant-specific | +| xBRZ | SDRCompatible R8 | shader passes | Fallback | integer-like RGB comparisons and alpha=1 | +| Pixel Art | SDRCompatible R8 | shader passes | Fallback | MMPX/Pixellate force alpha 1; SharpBilinear preserves it | +| Sharpen series | SDRCompatible normalized RGB | shader passes | Fallback | no stable upstream HDR contract | +| DLSSNR | R8 user path: SDRCompatible; experimental path: BoundedHDR FP16 | `DLSSNRFilter::Draw` -> Feature 18 Evaluate | Contract | R8 selection remains explicit; FP16 scale is local experiment, not vendor HDR proof | +| RTXVideo VSR Ultra | SDRCompatible `R8G8B8A8_UNORM` endpoint | `NvCVImage_Transfer` -> `NvVFX_Run` -> output transfer | Contract | U8 interleaved 0..255; `RTXVideoVSR/VSR-U8-sRGB-255` route | +| RTXVideo Denoise Ultra | SDRCompatible `R8G8B8A8_UNORM` endpoint | `NvCVImage_Transfer` -> `NvVFX_Run` -> output transfer | Contract | independent `RTXVideoDenoise/Denoise-U8-RGBA` route; same-resolution rule | +| NVIDIA Optical Flow | SDRCompatible ABGR8 auxiliary | `nvOFExecute` | Contract | output is S10.5 flow plus confidence; never a canonical RGB surface | +| AMD Optical Flow | BoundedHDR auxiliary candidate | FidelityFX optical-flow dispatch | Contract | current input is RGBA8 with fixed sRGB/luminance parameters | +| DLSSFG | PresentationTerminal record | `DLSSFrameGenerator::Draw` / publication | Contract | SDK backbuffer/HUD/UI HDR format and color-space contract still runtime dependent | +| XeSSFG | PresentationTerminal HDR10 R10 | XeSS-FG proxy swap chain + `Present` | Contract | R10 resource and `DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020` are explicit; live capture pending | +| FSR3/FSR4 FG | PresentationTerminal record | no native backend in current tree | Unavailable | protocol record exists; production backend and SDK are missing | +| third-party native backends | per-backend route above | factory-dispatched native `Draw` | Mixed | only VFX, DLSS/FSR/XeSS and optical-flow providers have callable native paths | + +## Required Protocol Record + +Every route is represented by `HdrFormatRoute` and serialized through +`SerializeHdrFormatRoute`. The runtime diagnostic emitted at capture and native +failure boundaries contains the effect route/profile, source format, source +color description, canonical format, conversion path and fallback reason. The +full per-frame evidence record is: + +```text +effectId, effectVariant, selectedProtocol, profile, evidenceLevel, +inputFormat, inputColorModel, inputTransfer, inputRange, +backendInputFormat, backendOutputFormat, outputTransfer, alphaRule, +auxiliaryResources, normalizationParameters, nativeInitialization, +nativeDrawOrEvaluate, nativeResultCode, outputFiniteStats, +canonicalOutputStats, gpuTime, screenshot, sha256 +``` + +`HdrSurfaceAdapter` keeps the paired forward/inverse parameters for +SDR-compatible and bounded routes. `HdrCaptureProcessor` produces and reuses +the canonical FP16 surface after the selected capture source. `EffectDrawer` +executes the adapter before and after the real shader/native boundary; native +failure is logged with an explicit marker-pass fallback. + +## DLSSNR Decision + +The user-selected R8 route remains `DLSSNR/sdr-r8` and allocates R8 input and +output resources. The experimental FP16 route is selected only when +`experimentalHdrPath` is enabled and the validated scale is `1`, `2`, or `4.5`; +it allocates FP16 resources and records `BoundedHDR` with the scale. No route +selection branch promotes the FP16 experiment when the user selected R8. + +The current implementation records the normalization scale, reference white, +display peak and adapter mode. Existing local experiments prove the real +Feature 18 Create/Evaluate path for the tested DLL/driver combinations, while +the exact vendor normalization curve remains an open experimental parameter. + +## RTX Video Decision + +VSR and Denoise now have separate protocol IDs. Both use an explicit U8, +interleaved RGBA/BGRA endpoint with values in `[0,255]`. The native backend +records `NvCVImage_InitFromD3D11Texture`, input transfer, `NvCVImage_Transfer`, +`NvVFX_Run`, output transfer and synchronization. A native failure stays a +failure in the log and the renderer marks the compatibility marker pass +separately. + +## Evidence Gaps + +The current workspace has no reproducible live GPU capture for every listed +effect, so the effects marked `Contract`, `Fallback`, or `Unavailable` are not +reported as visually passed. The existing `E:\Magpie-0.6.5-build\matrix` +captures and DLSSNR experiment logs remain the available pixel/hash evidence. +The startup matrix must be rerun on a target display with the user sample to +populate the remaining screenshot, SHA-256, finite-statistics, and GPU-time +fields. + diff --git a/docs/experimental/HDR_MECHANICAL_IMPLEMENTATION_REQUIREMENTS.md b/docs/experimental/HDR_MECHANICAL_IMPLEMENTATION_REQUIREMENTS.md new file mode 100644 index 000000000..ef1b1bc8d --- /dev/null +++ b/docs/experimental/HDR_MECHANICAL_IMPLEMENTATION_REQUIREMENTS.md @@ -0,0 +1,110 @@ +# HDR Mechanical Implementation Requirements + +## Purpose + +This document assigns the repetitive, locally verifiable work for the first HDR architecture slice. The global design decisions remain in [HDR_COMPATIBILITY_ARCHITECTURE.md](HDR_COMPATIBILITY_ARCHITECTURE.md). This task covers configuration plumbing, protocol data plumbing, reusable conversion dispatch, diagnostics, and the generic effect-boundary execution layer. It does not alter concrete effect shader sources or model implementations. + +## Required configuration plumbing + +Add the profile-level general setting: + +```json +"enableHdrCompatibility": false +``` + +The setting must map to `ScalingFlags::EnableHdrCompatibility` and expose `IsHdrCompatibilityEnabled()` in `ScalingOptions`. Preserve unknown configuration fields and preserve the default-off behavior for existing profiles. Add the setting to profile copy, load, save, and runtime option construction. Add the corresponding general-settings UI binding and localization using the repository's existing conventions. + +## Required protocol data plumbing + +Extend the reusable HDR protocol description so an effect can later declare multiple backend routes without changing the canonical work surface. Each route must be able to store: + +```text +effectId +optionId +inputFormat +outputFormat +inputTransfer +outputTransfer +inputRange +outputRange +alphaMode +evidenceLevel +hdrNative +adapterProfile +defaultForHdr +defaultForSdr +``` + +Do not add unverified effect-specific routes in this task. Add storage, validation, default selection, serialization helpers, and the generic structured SDR fallback route used when a concrete route is absent. + +## Required generic conversion dispatch + +Implement a reusable adapter dispatcher with these profiles: + +```text +DirectFP16 +BoundedHDR +SDRCompatible +ConditionalFP16 +Unknown +PresentationTerminal +``` + +The dispatcher must accept canonical `R16G16B16A16_FLOAT` input and return canonical `R16G16B16A16_FLOAT` output for non-terminal routes. It must select conversion behavior from the route description rather than from effect-name string comparisons. The SDR-compatible route must use paired HDR-to-SDR and SDR-to-HDR parameters and preserve alpha explicitly. Unknown routes must select the existing compatible fallback and emit a diagnostic state. + +## Required capture-front-end hook + +Add a reusable `HdrCaptureProcessor` integration point after any selected frame source produces its texture. Keep Graphics Capture, Desktop Duplication, GDI, and DwmSharedSurface selection unchanged. The processor must expose: + +```text +Process(sourceTexture, sourceFormat, sourceColorDescription) +GetCanonicalTexture() +GetFrameMetadata() +ResetForResize() +``` + +The implementation may use a display-derived source color description where the capture API does not expose texture metadata, but it must mark and record that inference. The processor must use reusable textures and must not replace the existing SDR texture when HDR compatibility is disabled. + +When HDR compatibility is enabled, the delivery path must retain the canonical FP16/scRGB surface through shared publication and presenter composition. R8 publication remains the disabled-mode path and is not used as the terminal HDR representation. + +## Required diagnostics + +Add structured logging for: + +```text +HDR option state +capture method +source format +source color description +canonical format +selected adapter profile +selected route id +conversion path +fallback reason +``` + +Avoid per-pixel CPU readback. Do not add a staging readback path to production rendering. + +## Explicit exclusions + +- No changes to concrete effect shader sources or model implementations. Generic boundary calls may wrap their existing backend resources when HDR compatibility is enabled. +- No model changes, shader-model changes, or effect shader rewrites. +- No change to SDR behavior when the global HDR option is disabled. +- No hard-coded assumption that every FP16 texture is HDR. +- No claim that a format enumeration proves HDR support. + +## Acceptance checks + +1. Existing profiles load with HDR disabled. +2. The general settings control round-trips through profile save/load. +3. The selected capture method is unchanged with HDR enabled. +4. HDR mode exposes a canonical FP16 frame descriptor after capture. +5. SDR mode bypasses the HDR processor and retains the previous resource path. +6. The adapter dispatcher has unit-level coverage for all six profiles. +7. The SDR-compatible adapter uses paired forward/inverse parameters. +8. No concrete effect file is modified. +9. The project files parse and the touched targets compile. + +## Deliverables + +Return the changed file list, configuration key, runtime flag, route serialization shape, adapter dispatch entry point, and validation commands. Report any capture API metadata assumption explicitly. diff --git a/docs/experimental/HDR_PROTOCOL_EVIDENCE.json b/docs/experimental/HDR_PROTOCOL_EVIDENCE.json new file mode 100644 index 000000000..44c109419 --- /dev/null +++ b/docs/experimental/HDR_PROTOCOL_EVIDENCE.json @@ -0,0 +1,2125 @@ +[ + { + "effectGroup": "Anime4K", + "effectFamily": "Anime4K", + "upstreamVendor": "bloc97 / Anime4K contributors", + "mappingConfidence": "明确映射", + "mappingNotes": "公开仓库名称与效果组名一致,且为 Anime4K 官方/上游仓库。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://github.com/bloc97/Anime4K/blob/master/README.md", + "notes": "Anime4K 官方仓库未枚举输入纹理格式;作为 mpv/libplacebo 用户着色器分发,实际纹理格式由宿主渲染器提供。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://github.com/bloc97/Anime4K/blob/master/README.md", + "notes": "输出为目标/上采样后纹理;官方仓库未枚举像素格式。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "implementation_defined", + "alphaSemantics": "未说明;不得从 mpv 用户着色器可编译推断 alpha 契约。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "尺寸缩放/着色器 hook 点由 mpv/libplacebo 宿主决定;官方未提供 UAV/SRV 或 API 资源状态要求。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "十轮内未找到可核实协议,无法给出候选 profile。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "A/B 模式等 GLSL 说明中的品质选择(未提供协议级格式语义)" + ], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://github.com/bloc97/Anime4K/blob/master/README.md", + "title": "Anime4K README", + "evidenceLevel": "OfficialRepository", + "claim": "项目为 Anime4K 官方仓库,未写明 HDR/纹理格式协议", + "quoteOrSymbol": "The simplicity and speed of Anime4K allows the user to watch upscaled anime in real time", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/bloc97/Anime4K/v4.0.1/GLSL_Instructions.md", + "title": "Anime4K v4.0.1 GLSL Instructions", + "evidenceLevel": "OfficialRepository", + "claim": "只说明 mpv/GLSL 用法与模式,未说明纹理格式、数值范围、HDR", + "quoteOrSymbol": "Mode A...", + "searchRound": 2 + } + ], + "searchRoundsUsed": 7, + "unresolvedQuestions": [ + "输入/输出纹理格式", + "数值范围", + "色彩空间", + "alpha", + "HDR/FP16 能力" + ], + "conclusion": "Anime4K 可明确映射到同名公开仓库,但公开仓库未提供图像 I/O 协议细节,本次记录为无协议信息。" + }, + { + "effectGroup": "CAS", + "effectFamily": "FidelityFX Contrast Adaptive Sharpening (CAS)", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "AMD FidelityFX SDK 的 CAS 技术,官方名称一致。", + "documentedInputFormats": [ + { + "format": "DXGI_FORMAT_R8G8B8A8_UNORM", + "apiOrContext": "DXGI / Direct3D 11 reference CLI", + "channelOrder": "RGBA", + "numericRepresentation": "UNORM", + "acceptanceStatus": "reference_implementation_only", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md", + "notes": "官方 CasCmdLine 默认输入格式;sRGB 解释由开关决定。" + }, + { + "format": "DXGI_FORMAT_R16G16B16A16_FLOAT", + "apiOrContext": "DXGI / Direct3D 11 reference CLI", + "channelOrder": "RGBA", + "numericRepresentation": "FLOAT (16-bit)", + "acceptanceStatus": "reference_implementation_only", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md", + "notes": "官方 CasCmdLine --16bit 使用的格式;shader 另有 FP16/FP32 数学路径。" + }, + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/docs/techniques/contrast-adaptive-sharpening.md", + "notes": "SDK 技术文档未对 FfxResource 输入格式做 DXGI/Vulkan 枚举。" + } + ], + "documentedOutputFormats": [ + { + "format": "DXGI_FORMAT_R8G8B8A8_UNORM", + "apiOrContext": "DXGI / Direct3D 11 reference CLI", + "channelOrder": "RGBA", + "numericRepresentation": "UNORM", + "acceptanceStatus": "reference_implementation_only", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md", + "notes": "默认输出格式,与输入同格式;官方样例说明输出可为相同或更大尺寸。" + }, + { + "format": "DXGI_FORMAT_R16G16B16A16_FLOAT", + "apiOrContext": "DXGI / Direct3D 11 reference CLI", + "channelOrder": "RGBA", + "numericRepresentation": "FLOAT (16-bit)", + "acceptanceStatus": "reference_implementation_only", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md", + "notes": "--16bit 输出格式;官方文档保证输出与输入同色彩空间。" + }, + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/docs/techniques/contrast-adaptive-sharpening.md", + "notes": "SDK 的 FfxResource output 未在技术文档中列出具体格式枚举。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "partial", + "outputFormatEnumeration": "partial", + "inputOutputRelation": "same_format_required", + "alphaSemantics": "未说明;SDK 回调只读写 RGB,未见 alpha 透传/置 1 声明。", + "rangeBoundary": "sRGB/linear 解释均支持;shader 转换注释按 [0,1] 线性空间处理。", + "transferBoundary": "linear input expected; output returned to same input color space via FfxCasColorSpaceConversion", + "resourceConstraints": "官方样例支持 sharpen-only/sharpen+upscale;输入 SRV/read、输出 UAV/write 在 SDK 样例路径;格式枚举只来自 CasCmdLine 参考实现。", + "hdrImplication": "官方参考实现支持 R16G16B16A16_FLOAT 与 linear 开关,但这属于参考实现路径;SDK 技术文档未提供 HDR 元数据契约。" + }, + "channelOrderAndAlpha": "回调只处理 RGB;文档未说明 alpha 是否透传或必须为 1。", + "precisionAndRange": "回调存在 float32 与 float16 路径;色彩转换注释按 [0,1] 线性空间处理,文档未给出 8/10/16 位格式清单。", + "transferFunction": "期望线性输入;通过 FfxCasColorSpaceConversion 支持 LINEAR、GAMMA20、GAMMA22、sRGB_OUTPUT、sRGB_INPUT_OUTPUT。", + "primariesAndColorSpace": "未说明原色域。", + "hdrSupport": "Unspecified", + "hdrBoundary": "文档未说明 HDR 直接输入;只要求线性输入并保持输入输出同色彩空间。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "官方 shader 注释将输入转换描述为 0..1 线性空间,且没有任何 HDR/FP16 直接路径声明;保守记为 SDR 兼容。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "sharpening disabled", + "sharpening enabled, upsampling disabled", + "both sharpening and upsampling enabled" + ], + "documentedPerformanceNotes": "文档未比较 FP16/UNORM/HDR 路径成本。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/docs/techniques/contrast-adaptive-sharpening.md", + "title": "FidelityFX Contrast Adaptive Sharpening 1.1", + "evidenceLevel": "OfficialDocs", + "claim": "CAS 需要线性输入;可按输入空间转换并在输出还原", + "quoteOrSymbol": "CAS needs linear input color to perform correctly", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/sdk/include/FidelityFX/host/ffx_cas.h", + "title": "ffx_cas.h", + "evidenceLevel": "OfficialHeader", + "claim": "FfxCasDispatchDescription 仅含 FfxResource color/output、renderSize、sharpness", + "quoteOrSymbol": "FfxResource color; FfxResource output", + "searchRound": 2 + }, + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/sdk/include/FidelityFX/gpu/cas/ffx_cas_callbacks_hlsl.h", + "title": "ffx_cas_callbacks_hlsl.h", + "evidenceLevel": "OfficialHeader", + "claim": "float32/float16 采样与颜色空间转换宏", + "quoteOrSymbol": "casInputHalf... between 0 and 1", + "searchRound": 3 + }, + { + "url": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md", + "title": "FidelityFX-CAS CasCmdLine README", + "evidenceLevel": "OfficialRepository", + "claim": "官方命令行参考实现默认 R8G8B8A8_UNORM,--16bit 用 R16G16B16A16_FLOAT", + "quoteOrSymbol": "uses R8G8B8A8_UNORM ... uses R16G16B16A16_FLOAT", + "searchRound": 5 + } + ], + "searchRoundsUsed": 7, + "unresolvedQuestions": [ + "具体支持格式列表", + "alpha 契约", + "HDR 数值上限", + "primaries" + ], + "conclusion": "CAS 的官方协议只明确了线性输入、输入输出同空间、float/fp16 shader 回调与可选色彩空间转换;具体 DXGI 格式与 HDR 能力未被官方文档枚举。" + }, + { + "effectGroup": "CRT", + "effectFamily": "无信息(CRT 效果族未能在公开资料中唯一映射)", + "upstreamVendor": "无信息", + "mappingConfidence": "无信息", + "mappingNotes": "仅按通用名称“CRT”搜索;公开资料无法唯一对应某个上游算法/SDK,且不能借助 Magpie 源码确认。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "无公开唯一上游来源", + "notes": "无法唯一映射上游;不得把通用 CRT 着色器格式写成该效果组已支持格式。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "无公开唯一上游来源", + "notes": "无法唯一映射上游;输出格式未公开。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明;名称不能唯一映射,无法建立 alpha 契约。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "未确认任何资源状态、缩放或采样限制。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [], + "searchRoundsUsed": 1, + "unresolvedQuestions": [ + "上游映射", + "输入格式", + "色彩空间", + "HDR/FP16" + ], + "conclusion": "“CRT”效果组无法在公开资料中唯一映射到上游算法或 SDK,十轮内无法取得可靠协议结论,记录为无信息。" + }, + { + "effectGroup": "CuNNy", + "effectFamily": "CuNNy", + "upstreamVendor": "Blinue / CuNNy contributors", + "mappingConfidence": "明确映射", + "mappingNotes": "上游仓库 Blinue/CuNNy 名称与效果组一致,README 自述“supports exporting to an mpv shader”和“Magpie effect”。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/Blinue/CuNNy/master/README.md", + "notes": "上游仓库只描述 mpv shader/Magpie effect,未给出纹理格式。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/Blinue/CuNNy/master/README.md", + "notes": "输出为上采样图像,格式未枚举。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "implementation_defined", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "mpv shader 导出路径意味着由 mpv/libplacebo 管理 hook 纹理;无 API 资源约束。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "8x32/4x3 等模型尺寸(来自 README,无格式协议语义)" + ], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/Blinue/CuNNy/master/README.md", + "title": "CuNNy README", + "evidenceLevel": "OfficialRepository", + "claim": "上游仓库描述 CuNNy 可导出 mpv shader/Magpie effect,未给出 I/O 格式", + "quoteOrSymbol": "Supports exporting to an mpv meme shader!", + "searchRound": 1 + } + ], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "输入输出纹理格式", + "HDR/FP16", + "alpha", + "数值范围" + ], + "conclusion": "CuNNy 上游可明确映射,但公开仓库没有图像协议细节,无法确定格式边界。" + }, + { + "effectGroup": "CuNNy2", + "effectFamily": "无信息(未能定位独立 CuNNy2 上游仓库/SDK)", + "upstreamVendor": "无信息", + "mappingConfidence": "无信息", + "mappingNotes": "公开搜索未找到与“CuNNy2”唯一对应的上游算法/模型/SDK 文档;现有结果多为 Magpie 版本页等,不可用于协议推断。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开搜索未发现独立 CuNNy2 上游", + "notes": "无法唯一映射到独立上游;不存在可归属的格式协议。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开搜索未发现独立 CuNNy2 上游", + "notes": "同上。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "无上游资源约束可记录。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "CuNNy2 上游映射", + "格式", + "HDR/FP16", + "alpha" + ], + "conclusion": "CuNNy2 未能在公开资料中唯一映射到上游算法或 SDK,记录为无信息。" + }, + { + "effectGroup": "Diagnostics", + "effectFamily": "无信息(Diagnostics 是通用诊断效果名)", + "upstreamVendor": "无信息", + "mappingConfidence": "无信息", + "mappingNotes": "公开资料中不存在可作为上游协议的“Diagnostics”图像算法/SDK;不依据 Magpie 实现推断。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开资料无对应上游算法", + "notes": "Diagnostics 为通用诊断效果名,不存在公开算法/SDK 格式枚举。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开资料无对应上游算法", + "notes": "同上。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "无资源约束可记录。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [], + "searchRoundsUsed": 1, + "unresolvedQuestions": [ + "上游映射", + "I/O 格式", + "HDR/FP16" + ], + "conclusion": "Diagnostics 无公开上游协议,记录为无信息。" + }, + { + "effectGroup": "DLSS", + "effectFamily": "NVIDIA DLSS Super Resolution (DLSS-SR)", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "NVIDIA DLSS SDK / Streamline 中的 DLSS Super Resolution,公共品牌与效果组一致。", + "documentedInputFormats": [ + "Color input: any supported buffer format for the API", + "Motion vectors: RG32_FLOAT or RG16_FLOAT", + "Depth: any one-channel format (R32_FLOAT/D32_FLOAT) or depth-stencil (D24S8)", + "Exposure: 1x1 texture, R16F preferred (presets J/K)" + ], + "documentedOutputFormats": [ + "Output buffer: any supported buffer format for the API", + "Previous output/history buffer: optional, RGBA16F" + ], + "formatBoundary": "官方编程指南要求输入为 SRV/read 状态,输出为 UAV/write 状态;颜色输入任意 API 支持格式,输出任意 API 支持格式;历史输出建议 RGBA16F;最小输出 32x32;无规定最大分辨率但 8K 以上未保证。", + "channelOrderAndAlpha": "Streamline 默认只放大 RGB;alphaUpscalingEnabled 为实验性时可放大 alpha;官方 DLSS 指南未规定固定通道顺序。", + "precisionAndRange": "LDR 模式颜色值必须 [0,1] 且为感知编码(如 sRGB),不能为线性;HDR 模式可处理线性高范围、无亮度上限,内部高精度。", + "transferFunction": "LDR: sRGB/perceptual encoding; HDR: linear space (IsHDR flag).", + "primariesAndColorSpace": "未说明 primaries;HDR 为线性场景/显示域未细分。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "HDR 输入需将 IsHDR 置 1;需要 exposure(1x1 texture,R16F 首选)供当前帧使用;无 tone mapping 要求;输出仍同输入线性域。", + "requiredHdrMetadata": [ + "exposure value (1x1 texture)" + ], + "candidateInternalProfile": "DirectFP16", + "profileRationale": "官方明确 HDR 线性路径、无亮度上限且历史/输出使用/推荐 RGBA16F;有直接的 HDR/FP16 证据。", + "auxiliaryInputs": [ + "depth", + "motion vectors", + "exposure", + "jitter", + "previous output/history (optional)" + ], + "auxiliaryInputFormatsAndSemantics": "Depth any one-channel/depth-stencil; MVs RG32_FLOAT or RG16_FLOAT in pixel screen-space, can be low-res or high-res/dilated; exposure 1x1, R16F preferred; jitter in [-0.5,0.5] pixels at render res.", + "temporalOrFrameGenerationConstraints": "时域历史由 DLSS 内部维护,可选 previous output RGBA16F;reset/jump cut 语义见编程指南;动态分辨率需保持长宽比;mip bias 需要负偏移。", + "documentedQualityOptions": [ + "DLSS quality/balanced/performance/ultra-performance presets (公开质量档,格式不随档位变化)" + ], + "documentedPerformanceNotes": "官方说明 LDR 模式性能更优且内部量化到 8-bit;HDR 模式内部高精度。", + "qualityPerformanceEvidence": "官方指南有 LDR 性能更优说明,但未给出格式路径基准表。", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://github.com/NVIDIA/DLSS/blob/main/doc/DLSS_Programming_Guide_Release.pdf", + "title": "NVIDIA DLSS Super Resolution Programming Guide Release", + "evidenceLevel": "OfficialDocs", + "claim": "Supported Formats: color any API format; MV RG32_FLOAT/RG16_FLOAT; depth any one-channel/depth-stencil; output any API format; history RGBA16F", + "quoteOrSymbol": "3.3 Supported Formats", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/NVIDIA/DLSS/main/doc/DLSS_Programming_Guide_Release.pdf", + "title": "DLSS Programming Guide PDF (raw)", + "evidenceLevel": "OfficialDocs", + "claim": "LDR [0,1] perceptual; HDR linear unbounded", + "quoteOrSymbol": "DLSS can process color data stored as either LDR or HDR", + "searchRound": 2 + }, + { + "url": "https://raw.githubusercontent.com/NVIDIA-RTX/Streamline/main/docs/ProgrammingGuideDLSS.md", + "title": "Streamline DLSS Super Resolution Integration", + "evidenceLevel": "OfficialRepository", + "claim": "DLSS-SR requires colorIn/colorOut/depth/mvec/exposure; colorBuffersHDR and alphaUpscalingEnabled options", + "quoteOrSymbol": "dlssOptions.colorBuffersHDR = sl::Boolean::eTrue", + "searchRound": 3 + } + ], + "searchRoundsUsed": 6, + "unresolvedQuestions": [ + "具体 DXGI 支持清单(官方用“any API format”而非枚举)", + "primaries", + "输出 alpha 默认值" + ], + "conclusion": "DLSS-SR 官方协议清晰:颜色输入输出可为 API 任意支持格式,HDR 线性模式通过 IsHDR 开启,运动向量固定 RG32/RG16_FLOAT,深度为单通道/深度模板,历史推荐 RGBA16F。" + }, + { + "effectGroup": "DLSSFG", + "effectFamily": "NVIDIA DLSS Frame Generation (DLSS-G / DLSS 4 Multi-Frame Gen)", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "Streamline/NVIDIA DLSS Frame Generation 官方文档与头文件。", + "documentedInputFormats": [ + "Backbuffer/Final Color resource (format not enumerated in public text)", + "Motion vectors/depth: same DLSS-SR requirements" + ], + "documentedOutputFormats": [ + "OutputInterpolated/OutputReal: same texture format as Backbuffer" + ], + "formatBoundary": "官方头文件写明 Required Output Texture (same texture format as Backbuffer);HUDless 与 UI 纹理需与 backbuffer 同尺寸/格式/色彩空间;backbuffer 资源经 Streamline swapchain 拦截。", + "channelOrderAndAlpha": "UI Alpha 为单通道 0..1;UI Color and Alpha 需预乘 alpha;HUDless 为无 UI 全场景颜色;alpha 参与 UI 合成。", + "precisionAndRange": "未公开列举 8/16 位格式;只有 colorBuffersHDR 布尔标志表示 HDR。", + "transferFunction": "未在公开文本中细分;HUDless 与 backbuffer 必须同色彩空间(含 tone mapping)。", + "primariesAndColorSpace": "未说明 primaries。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "头文件有 full HDR 标志;未提供 PQ/HLG/scRGB 解码或 metadata 要求;应把与 backbuffer 相同色彩空间的帧交给帧生成。", + "requiredHdrMetadata": [ + "colorBuffersHDR flag", + "无公开 MaxCLL/MaxFALL/亮度要求" + ], + "candidateInternalProfile": "FormatOnlyNoColorContract", + "profileRationale": "官方确认 HDR 布尔和“输出与 backbuffer 同格式”,但未公开 HDR 的具体格式/色彩空间契约;不能判定 DirectFP16。", + "auxiliaryInputs": [ + "depth", + "motion vectors", + "HUD-less color", + "UI alpha or UI color+alpha", + "bidirectional distortion field (optional)", + "backbuffer subrect" + ], + "auxiliaryInputFormatsAndSemantics": "Depth/mvec same as DLSS-SR; HUDless/UI must match backbuffer; UI alpha 0..1; UI RGB premultiplied; distortion field maps distorted final color to undistorted guide buffers.", + "temporalOrFrameGenerationConstraints": "需要 dense motion vectors, depth, HUDless;插值输出与真实帧输出为同格式;帧生成要求 present 时资源仍有效;子矩形需 extent 匹配;UI 合成公式 Final.RGB = UI.RGB + (1-UI.Alpha)*HUDless.RGB。", + "documentedQualityOptions": [ + "插值帧数/模式等公开选项,未涉及格式" + ], + "documentedPerformanceNotes": "公开指南建议优先提供 UI Alpha 单通道以获得更好性能;无格式基准。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/NVIDIA-RTX/Streamline/main/docs/ProgrammingGuideDLSS_G.md", + "title": "Streamline DLSS-G Programming Guide", + "evidenceLevel": "OfficialRepository", + "claim": "列出深度、运动向量、HUDless、UI 输入与资源生命周期约束", + "quoteOrSymbol": "Hudless ... same color space and post-processing effects as color backbuffer", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/NVIDIA/DLSS/main/include/nvsdk_ngx_defs_dlssg.h", + "title": "nvsdk_ngx_defs_dlssg.h", + "evidenceLevel": "OfficialHeader", + "claim": "Required Output Texture (same texture format as Backbuffer)", + "quoteOrSymbol": "#define NVSDK_NGX_DLSSG_Parameter_BackbufferFormat \"DLSSG.BackbufferFormat\"", + "searchRound": 2 + } + ], + "searchRoundsUsed": 5, + "unresolvedQuestions": [ + "实际接受/输出的 DXGI 格式枚举", + "HDR PQ/HLG/scRGB 颜色契约", + "metadata" + ], + "conclusion": "DLSSFG 官方公开材料确认输入由 swapchain/backbuffer 决定、输出必须与 backbuffer 同格式,并有 HDR 布尔开关;没有公开更细的格式枚举或 PQ/scRGB 协议。" + }, + { + "effectGroup": "FSR", + "effectFamily": "AMD FidelityFX Super Resolution 1 (FSR1 spatial upscaler)", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "Magpie 效果组“FSR”按公开产品命名对应 FSR1 的空间超分;FSR2/3/4 已单列。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "algorithmic reference implementation (shader callback)", + "channelOrder": "RGB (3 channel)", + "numericRepresentation": "FLOAT/half", + "acceptanceStatus": "inferred_from_shader_interface", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/ffx-fsr/ffx_fsr1.h", + "notes": "官方 ffx_fsr1.h 提供 RGB 输入回调;未给出具体 DXGI 格式。" + }, + { + "format": "unspecified", + "apiOrContext": "DXGI/Vulkan/API resource", + "channelOrder": "RGB", + "numericRepresentation": "UNORM/FLOAT", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.0.0/docs/techniques/super-resolution-spatial.md", + "notes": "官方 FSR1 文档只要求 [0,1] perceptual sRGB,不枚举资源格式。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "algorithmic reference implementation (shader callback)", + "channelOrder": "RGB (3 channel)", + "numericRepresentation": "FLOAT/half", + "acceptanceStatus": "inferred_from_shader_interface", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/ffx-fsr/ffx_fsr1.h", + "notes": "EASU/RCAS 输出 RGB;最终输出表面格式由宿主分配。" + }, + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.0.0/docs/techniques/super-resolution-spatial.md", + "notes": "输出格式未在 FSR1 文档中单独列枚举。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "implementation_defined", + "alphaSemantics": "未说明。", + "rangeBoundary": "[0,1] perceptual sRGB;RCAS 负输入会产生 NaN。", + "transferBoundary": "sRGB / perceptual(非 linear);helper 另含 linear HDR {0 to FP16_MAX} 转换工具,但非核心输入契约。", + "resourceConstraints": "shader 回调 gather4;输入资源可为动态分辨率 viewport;建议 32bpp 格式;无 UAV/SRV 官方枚举。", + "hdrImplication": "FSR1 核心文档没有 HDR 直接输入路径;线性 HDR helper 属可选周边工具,不能算核心格式契约。" + }, + "channelOrderAndAlpha": "未说明 alpha。", + "precisionAndRange": "颜色归一化 [0,1];感知编码 sRGB。", + "transferFunction": "sRGB / perceptual(不是线性)。", + "primariesAndColorSpace": "未说明原色域(通常 sRGB,但资料未写 primaries)。", + "hdrSupport": "SDROnly", + "hdrBoundary": "官方文档面向 [0,1] sRGB 输入,未提供 HDR 线性/PQ/HLG 路径。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "官方明确要求 [0-1] perceptual sRGB、不能线性,未见 HDR 路径,故为 SDR 兼容。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "EASU+RCAS 组合/quality mode 由调用方决定(官方未给出档位枚举)" + ], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.0.0/docs/techniques/super-resolution-spatial.md", + "title": "FidelityFX Super Resolution 1 (FSR1)", + "evidenceLevel": "OfficialDocs", + "claim": "Input normalized [0-1], perceptual sRGB not linear; negative RCAS input NaN", + "quoteOrSymbol": "Image should be normalized to [0-1] and be in perceptual color space (sRGB, not linear)", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/ffx-fsr/ffx_fsr1.h", + "title": "FidelityFX-FSR ffx_fsr1.h", + "evidenceLevel": "OfficialHeader", + "claim": "FSR1 通过 RGB 输入回调读取颜色;建议 32bpp 性能格式;RCAS 输入范围 {0 to 1}", + "quoteOrSymbol": "Color needs to be encoded as 3 channel [red, green, blue]", + "searchRound": 4 + } + ], + "searchRoundsUsed": 6, + "unresolvedQuestions": [ + "DXGI 格式清单", + "alpha", + "输出格式要求" + ], + "conclusion": "FSR1 官方资料可确认是 [0,1] sRGB 感知色彩输入的空间超分算法,没有 HDR/FP16 协议证据,候选为 SDRCompatible。" + }, + { + "effectGroup": "FSR2", + "effectFamily": "AMD FidelityFX Super Resolution 2 (FSR2 temporal upscaler)", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "官方文档标题为 AMD FidelityFX Super Resolution 2.3.x。", + "documentedInputFormats": [ + "Color buffer: APPLICATION SPECIFIED (render res)", + "Depth: APPLICATION SPECIFIED (1x FLOAT)", + "Motion vectors: APPLICATION SPECIFIED (2x FLOAT)", + "Reactive mask: R8_UNORM", + "Transparency & composition mask: R8_UNORM", + "Exposure: R32_FLOAT (1x1)" + ], + "documentedOutputFormats": [ + "Adjusted color buffer (internal): R16G16B16A16_FLOAT", + "Upscaled buffer (internal): R16G16B16A16_FLOAT", + "API 输出缓冲的公开格式在文档中未单独枚举,一般随应用分配资源" + ], + "formatBoundary": "颜色输入由应用指定格式;HDR 需设置 HDR flag;深度单浮点;MVs 2x float 屏幕空间范围 [<-w,-h>,];内部多使用 16-bit;输出最终与输入同域(内部 tonemap 会被反转);要求 GPU 支持 typed UAV load 和 R16G16B16A16_UNORM。", + "channelOrderAndAlpha": "内部 Adjusted color 使用 YCoCg 且 alpha 为 disocclusion mask;对外 alpha 契约未说明。", + "precisionAndRange": "颜色可为应用指定的 UNORM/FLOAT 等;HDR 线性域;MVs 内部 16-bit;exposure R32_FLOAT。", + "transferFunction": "HDR 模式要求 linear;SDK 后续也允许 non-linear 标志(见 FSR4/upscaler 文档)。", + "primariesAndColorSpace": "未说明 primaries。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "设置 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE 后输入应为 linear;输出恢复原输入域;需要 pre-exposure/exposure 语义。", + "requiredHdrMetadata": [ + "pre-exposure", + "exposure (R32_FLOAT) 或 auto-exposure" + ], + "candidateInternalProfile": "DirectFP16", + "profileRationale": "官方内部表面明确 R16G16B16A16_FLOAT、HDR 线性支持,且有 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE 直接路径。", + "auxiliaryInputs": [ + "depth", + "motion vectors", + "reactive mask", + "transparency & composition mask", + "exposure", + "camera jitter", + "reset" + ], + "auxiliaryInputFormatsAndSemantics": "MVs in pixels screen-space, low-res/display-res flag; reactive/T&C mask R8_UNORM [0,1]; exposure R32_FLOAT optional; depth flags inverted/infinite.", + "temporalOrFrameGenerationConstraints": "时域算法需要当前/上一帧数据与历史;所有 render-res 输入需 jitter(MV 例外);camera jump cut 需 reset;FrameTimeDelta 输入。", + "documentedQualityOptions": [ + "quality/balanced/performance 等调用方档位(API 主要按 input/output 尺寸)" + ], + "documentedPerformanceNotes": "内部 MVs 使用 16-bit,高精度输入不会额外受益;官方要求 R16G16B16A16_UNORM 支持;无公开格式路径性能基准。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://github.com/GPUOpen-Effects/FidelityFX-FSR2/blob/master/README.md", + "title": "FidelityFX-FSR2 README", + "evidenceLevel": "OfficialRepository", + "claim": "Input resource table and internal R16G16B16A16_FLOAT buffers", + "quoteOrSymbol": "Upscaled buffer ... R16G16B16A16_FLOAT", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/main/Kits/FidelityFX/docs/techniques/super-resolution-temporal.md", + "title": "FidelityFX Super Resolution 2.3.4", + "evidenceLevel": "OfficialDocs", + "claim": "Input resources table, HDR support, linear color", + "quoteOrSymbol": "High dynamic range images are supported... linear color space", + "searchRound": 2 + } + ], + "searchRoundsUsed": 4, + "unresolvedQuestions": [ + "对外输出 FfxResource 的格式枚举", + "alpha 对外行为", + "primaries" + ], + "conclusion": "FSR2 官方协议:颜色输入为应用指定格式,HDR 需 linear 输入与 HDR flag;辅助资源格式明确;内部浮点表面 R16G16B16A16_FLOAT。" + }, + { + "effectGroup": "FSR3", + "effectFamily": "AMD FidelityFX Super Resolution 3.1 Upscaler (FSR3 SR)", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "FSR3 效果组拆为“FSR3 Upscaler”与“FSR3 Frame Interpolation”两个后端;此处为官方 FSR3 3.1.x Upscaler。", + "documentedInputFormats": [ + "Color buffer: APPLICATION SPECIFIED", + "Depth: APPLICATION SPECIFIED (1x FLOAT)", + "Motion vectors: APPLICATION SPECIFIED (2x FLOAT)", + "Reactive mask: R8_UNORM", + "T&C mask: R8_UNORM", + "Exposure: R32_FLOAT" + ], + "documentedOutputFormats": [ + "未单独枚举;文档称输出与原始输入同域(internal tonemap 被反转)" + ], + "formatBoundary": "与 FSR2 输入结构一致;HDR flag 开启时输入 linear;要求 R16G16B16A16_UNORM typed UAV 支持;API 未在文档中列 DXGI 输出枚举。", + "channelOrderAndAlpha": "对外 alpha 未说明。", + "precisionAndRange": "颜色应用指定;HDR linear;MVs 2x float;内部 16-bit。", + "transferFunction": "HDR linear;文档未给出额外非线性颜色自动转换。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "设置 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE,线性输入,输出恢复原输入域;需要 exposure/pre-exposure。", + "requiredHdrMetadata": [ + "pre-exposure", + "exposure", + "auto-exposure flag 可选" + ], + "candidateInternalProfile": "DirectFP16", + "profileRationale": "官方 HDR 线性直接路径 + 内部浮点表面证据与 FSR2 同类。", + "auxiliaryInputs": [ + "depth", + "motion vectors", + "reactive mask", + "T&C mask", + "exposure", + "jitter", + "reset", + "frame time delta" + ], + "auxiliaryInputFormatsAndSemantics": "同 FSR2:depth float、MV 2x float、R8_UNORM masks、R32_FLOAT exposure。", + "temporalOrFrameGenerationConstraints": "时域超分要求 jitter、reset、历史;FrameTimeDelta 输入。", + "documentedQualityOptions": [ + "FSR 质量/性能档由应用选择(未在格式文档中)" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/main/Kits/FidelityFX/docs/techniques/super-resolution-upscaler.md", + "title": "AMD FidelityFX Super Resolution 3.1.5 Upscaler", + "evidenceLevel": "OfficialDocs", + "claim": "FSR3 Upscaler input resources and HDR linear support", + "quoteOrSymbol": "FSR Super Resolution ... HDR support", + "searchRound": 1 + } + ], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "输出格式枚举", + "alpha", + "primaries" + ], + "conclusion": "FSR3 Upscaler 与 FSR2 的输入/辅助资源/HDR linear 协议高度一致;公开文档未列外部输出格式枚举。" + }, + { + "effectGroup": "FSR3", + "effectFamily": "AMD FidelityFX Super Resolution 3 Frame Interpolation / Frame Generation (FSR3 FG)", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "FSR3 的帧生成/插值后端,官方技术名 FidelityFX Frame Interpolation;拆为独立子条目。", + "documentedInputFormats": [ + "currentBackBuffer (presentation color buffer)", + "currentBackBuffer_HUDLess (optional)", + "depth (required for FSR3 interpolation workflow)", + "motion vectors (required)", + "R16G16_SINT optical flow vector", + "R32_UINT optical flow SCD" + ], + "documentedOutputFormats": [ + "interpolated output resource", + "outputSwapChainBuffer", + "格式未在公开文档列枚举;代码示例用 swap chain back buffer format" + ], + "formatBoundary": "官方文档要求传入 backBufferFormat;HUD-less/UI 资源与 backbuffer 关系密切;帧生成需要超分先行;输入资源格式由配置描述,未列完整 DXGI 清单。", + "channelOrderAndAlpha": "HUDLess/UI 合成涉及 alpha/UI 内容;具体 alpha 格式未完整说明。", + "precisionAndRange": "未公开逐格式数值范围;有 FFX_FRAMEINTERPOLATION_ENABLE_HDR_COLOR_INPUT 与 minMaxLuminance。", + "transferFunction": "config 含 backBufferTransferFunction 用于把插值源转换到 linear RGB;HDR 转换依赖 minMaxLuminance。", + "primariesAndColorSpace": "未说明 primaries;backBufferTransferFunction 枚举存在。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "需设置 HDR_COLOR_INPUT;通过 backBufferTransferFunction 与 minMaxLuminance 把 HDR 颜色转到 linear RGB;未公开 MaxCLL/MaxFALL 字段。", + "requiredHdrMetadata": [ + "backBufferTransferFunction", + "minMaxLuminance[2]" + ], + "candidateInternalProfile": "BoundedHDR", + "profileRationale": "官方明确 HDR 颜色输入需经 transfer function + min/max luminance 转换为线性 RGB,即有界/转换契约,而非直接 FP16 无 metadata 路径。", + "auxiliaryInputs": [ + "depth", + "game motion vectors", + "FidelityFX Optical Flow vectors", + "HUDLess color", + "UI/HUD handling (presentCallback or HUDLessColor)" + ], + "auxiliaryInputFormatsAndSemantics": "depth inverted/infinite flags; opticalFlowVector R16G16_SINT; opticalFlowSCD R32_UINT; HUDLess optional FfxResource.", + "temporalOrFrameGenerationConstraints": "帧生成需要 previous/current frame、光流、深度、运动向量;presentCallback/HUDLess 用于 UI;重置、延迟、显示分辨率限制在 FSR3 集成指南。", + "documentedQualityOptions": [ + "FSR3 frame generation on/off, UI composition modes 等(无格式档位)" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/release-FSR3-3.0.3/docs/techniques/frame-interpolation.md", + "title": "FidelityFX Frame Interpolation", + "evidenceLevel": "OfficialDocs", + "claim": "Config includes HDR flag, backBufferTransferFunction, minMaxLuminance; optical flow outputs", + "quoteOrSymbol": "FFX_FRAMEINTERPOLATION_ENABLE_HDR_COLOR_INPUT", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/release-FSR3-3.0.3/docs/techniques/optical-flow.md", + "title": "FidelityFX Optical Flow", + "evidenceLevel": "OfficialDocs", + "claim": "Optical flow vector R16G16_SINT, SCD R32_UINT", + "quoteOrSymbol": "R16G16_SINT ... R32_UINT", + "searchRound": 2 + } + ], + "searchRoundsUsed": 3, + "unresolvedQuestions": [ + "具体 backbuffer 格式支持清单", + "HDR PQ/scRGB 细节", + "alpha 行为" + ], + "conclusion": "FSR3 Frame Generation 官方协议确认需要 backbuffer 格式、HDR 输入标志和 transfer/luminance 参数;外部格式仍由 swapchain/backbuffer 决定,未公开完整格式枚举。" + }, + { + "effectGroup": "FSR4", + "effectFamily": "AMD FidelityFX Super Resolution 4 (FSR4 ML Super Resolution)", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "官方 FidelityFX SDK 文档标题“AMD FSR Super Resolution 4”。", + "documentedInputFormats": [ + "Color buffer: APPLICATION SPECIFIED", + "Depth: APPLICATION SPECIFIED (1x FLOAT)", + "Motion vectors: APPLICATION SPECIFIED (2x FLOAT)", + "Exposure: R32_FLOAT (1x1)" + ], + "documentedOutputFormats": [ + "未单独枚举;文档称输出与原始输入同域" + ], + "formatBoundary": "颜色输入必须 linear,除非设置 NON_LINEAR_COLORSPACE 标志;没有 DXGI 格式枚举,颜色格式由应用指定;MVs 2x float;深度 float;输出同输入域。", + "channelOrderAndAlpha": "未说明。", + "precisionAndRange": "linear HDR/scene-linear 输入,或 non-linear 标志;内部 ML 处理;精度未公开。", + "transferFunction": "linear(推荐);非线性的可选标志。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "HDR 线性输入为推荐;输出恢复同输入域;需要 exposure/pre-exposure 管理;无 PQ/HLG 自动解码说明。", + "requiredHdrMetadata": [ + "pre-exposure", + "exposure R32_FLOAT", + "auto-exposure 可选" + ], + "candidateInternalProfile": "DirectFP16", + "profileRationale": "官方 HDR linear 直接路径、无色彩空间自动处理,说明有直接浮点/HDR 路径。", + "auxiliaryInputs": [ + "depth", + "motion vectors", + "exposure", + "jitter", + "reset" + ], + "auxiliaryInputFormatsAndSemantics": "同 FSR2/3 upscaler 协议;MVs 屏幕空间像素范围。", + "temporalOrFrameGenerationConstraints": "时域 ML 超分需 jitter、reset、历史/帧时间;无公开帧生成。", + "documentedQualityOptions": [ + "FSR4 质量/性能档(未在格式文档中)" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/main/Kits/FidelityFX/docs/techniques/super-resolution-ml.md", + "title": "AMD FSR Super Resolution 4", + "evidenceLevel": "OfficialDocs", + "claim": "FSR4 input resources, HDR linear and non-linear colorspace flags", + "quoteOrSymbol": "High dynamic range images are supported... linear color space", + "searchRound": 1 + } + ], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "输出格式枚举", + "alpha", + "primaries", + "ML 模型输入张量格式" + ], + "conclusion": "FSR4 官方协议说明颜色输入为应用指定的 linear(或 non-linear 标志)HDR 可支持,辅助资源与 FSR2/3 一致;外部输出格式未公开。" + }, + { + "effectGroup": "FSRCNNX", + "effectFamily": "FSRCNNX (基于 FSRCNN 的 mpv/着色器变体)", + "upstreamVendor": "igv / FSRCNN community?", + "mappingConfidence": "推测映射", + "mappingNotes": "公开上可找到 FSRCNN-TensorFlow 项目与 mpv FSRCNNX 发布物,但“FSRCNNX”本身没有独立官方 SDK 文档。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/igv/FSRCNN-TensorFlow/master/README.md", + "notes": "FSRCNNX 无独立官方 SDK;基础 FSRCNN 仓库只说明 TensorFlow 模型,未给出 mpv shader 的纹理格式。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/igv/FSRCNN-TensorFlow/master/README.md", + "notes": "同上;输出格式未枚举。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "implementation_defined", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "社区 mpv 发布物/模型尺寸存在,但无统一资源状态或尺寸约束文档。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "不同 FSRCNNX 模型/尺寸由社区发布(无协议级信息)" + ], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "GitHubExperiment", + "sources": [ + { + "url": "https://raw.githubusercontent.com/igv/FSRCNN-TensorFlow/master/README.md", + "title": "FSRCNN-TensorFlow", + "evidenceLevel": "GitHubExperiment", + "claim": "底层 FSRCNN TensorFlow 实现;README 提到增加 RGB 支持", + "quoteOrSymbol": "Add RGB support", + "searchRound": 1 + } + ], + "searchRoundsUsed": 5, + "unresolvedQuestions": [ + "FSRCNNX 独立仓库/头文件", + "输入输出格式", + "HDR/FP16" + ], + "conclusion": "FSRCNNX 只能推测映射到 FSRCNN 的着色器变体,公开协议资料不足,记录为无信息。" + }, + { + "effectGroup": "FXAA", + "effectFamily": "FXAA (Fast Approximate Anti-Aliasing)", + "upstreamVendor": "Timothy Lottes / NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "算法名称明确对应 Lottes 的 FXAA;公开实现众多,但无单一官方 SDK 协议文档。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "无官方统一来源", + "notes": "FXAA 算法无单一 SDK;多引擎实现把输入绑定为 2D 颜色纹理。" + }, + { + "format": "unspecified", + "apiOrContext": "WebGPU/engine reference implementation (bevy FXAA)", + "channelOrder": "RGBA", + "numericRepresentation": "FLOAT (sampled as vec4)", + "acceptanceStatus": "reference_implementation_only", + "evidenceRef": "https://raw.githubusercontent.com/JMS55/bevy/236aa4e2fc93900b6ada7151b0d8e0567e6650f2/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl", + "notes": "社区/引擎实现使用 texture_2d 采样并输出 alpha 透传;不代表 FXAA 通用格式契约。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "无官方统一来源", + "notes": "FXAA 输出通常为同尺寸颜色纹理,但无统一格式枚举。" + }, + { + "format": "unspecified", + "apiOrContext": "WebGPU/engine reference implementation (bevy FXAA)", + "channelOrder": "RGBA", + "numericRepresentation": "FLOAT", + "acceptanceStatus": "reference_implementation_only", + "evidenceRef": "https://raw.githubusercontent.com/JMS55/bevy/236aa4e2fc93900b6ada7151b0d8e0567e6650f2/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl", + "notes": "bevy 实现返回 vec4(finalColor, centerSample.a),alpha 透传;仅参考实现。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "implementation_defined", + "alphaSemantics": "参考实现可透传 alpha;官方/原算法无统一声明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "shader 输入输出通常同尺寸;无统一 UAV/SRV 契约。", + "hdrImplication": "无官方 HDR 格式契约;个别引擎实现可用浮点纹理,但不能代表算法协议。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "没有可引用为统一协议的官方格式说明。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "FXAA quality presets 由各实现定义,未在本调研确认" + ], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [ + { + "url": "https://raw.githubusercontent.com/JMS55/bevy/236aa4e2fc93900b6ada7151b0d8e0567e6650f2/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl", + "title": "Bevy FXAA shader (NVIDIA FXAA 3.11 port)", + "evidenceLevel": "GitHubExperiment", + "claim": "bevy 实现以 texture_2d 读取颜色,输出 RGBA float 并透传 alpha", + "quoteOrSymbol": "return vec4(finalColor, centerSample.a);", + "searchRound": 3 + } + ], + "searchRoundsUsed": 4, + "unresolvedQuestions": [ + "固定输入格式", + "HDR/FP16", + "色彩空间" + ], + "conclusion": "FXAA 可明确映射到 Lottes 的公开算法,但缺少可作为“外置效果层协议”的统一官方格式契约,记录为无信息。" + }, + { + "effectGroup": "MLAA", + "effectFamily": "MLAA (Morphological Anti-Aliasing)", + "upstreamVendor": "Intel Labs / Alexander Reshetov", + "mappingConfidence": "明确映射", + "mappingNotes": "MLAA 为公开论文/算法名称;无单一 SDK 官方协议。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "无公开统一来源", + "notes": "MLAA 是论文/算法族,无单一官方 SDK 格式枚举。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "无公开统一来源", + "notes": "输出格式未枚举。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "无统一资源约束可记录。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [], + "searchRoundsUsed": 1, + "unresolvedQuestions": [ + "上游 SDK/实现", + "格式", + "HDR/FP16" + ], + "conclusion": "MLAA 可映射到公开形态抗锯齿算法,但未找到统一的图像 I/O 协议资料,记录为无信息。" + }, + { + "effectGroup": "NIS", + "effectFamily": "NVIDIA Image Scaling SDK (NVScaler/NVSharpen)", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "NVIDIA Image Scaling SDK 官方仓库与 Streamline 插件。", + "documentedInputFormats": [ + "Input/output: non-integer data types, examples DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_NV12 (NV12 via NIS_NV12_SUPPORT)", + "HDR modes: LDR [0,1], HDR PQ [0,1], HDR Linear recommended [0,12.5]" + ], + "documentedOutputFormats": [ + "Output UAV: non-integer formats, same docs examples (R8G8B8A8_UNORM/NV12); shader writes RWTexture2D" + ], + "formatBoundary": "输入为 SRV/read state,输出 UAV/write;sampler 必须 linear clamp;支持 viewport 子区域;NV12 需编译开关 NIS_NV12_SUPPORT;系数纹理 R32G32B32A32_FLOAT 或 R16G16B16A16_FLOAT。", + "channelOrderAndAlpha": "颜色 RGBA/BGRA 由纹理格式决定;NV12 为 Y plane + interleaved UV;alpha 行为未公开。", + "precisionAndRange": "LDR/PQ [0,1];HDR Linear [0,12.5] 约 1000nits;fp16/fp32 系数可选;不支持整数格式。", + "transferFunction": "LDR: display-referred after OETF (sRGB/gamma); HDR PQ: Rec.2020 PQ OETF; HDR Linear: scene/display linear.", + "primariesAndColorSpace": "PQ 模式说明 Rec.2020 PQ;HDR Linear 按 BT.709 luma 参考;其余未说明。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "需通过 NIS_HDR_MODE 选 NONE/LINEAR/PQ;PQ 输入应为 display-referred Rec.2020 PQ;线性 HDR 建议 [0,12.5];没有额外 metadata 参数。", + "requiredHdrMetadata": [ + "NIS_HDR_MODE", + "无 MaxCLL/MaxFALL/pre-exposure 参数" + ], + "candidateInternalProfile": "DirectFP16", + "profileRationale": "官方支持 HDR Linear/PQ 模式,系数可 FP16,且对 HDR 范围有明确文档,属于直接 HDR 路径。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": "空间算法,无时域/历史/帧生成约束。", + "documentedQualityOptions": [ + "NIS_HDR_MODE", + "NIS_NV12_SUPPORT", + "NIS_VIEWPORT_SUPPORT", + "NIS_CLAMP_OUTPUT" + ], + "documentedPerformanceNotes": "README 未比较各格式成本。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/NVIDIAGameWorks/NVIDIAImageScaling/main/README.md", + "title": "NVIDIA Image Scaling SDK README", + "evidenceLevel": "OfficialRepository", + "claim": "Color spaces/ranges and supported texture formats", + "quoteOrSymbol": "Input and output formats are expected... DXGI_FORMAT_R8G8B8A8_UNORM or DXGI_FORMAT_NV12", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/NVIDIA-RTX/Streamline/main/docs/ProgrammingGuideNIS.md", + "title": "Streamline NIS Programming Guide", + "evidenceLevel": "OfficialRepository", + "claim": "NIS Streamline integration uses colorIn/colorOut tags and hdrMode option", + "quoteOrSymbol": "nisOptions.hdrMode = NISHDR::eNISHDRNone", + "searchRound": 2 + } + ], + "searchRoundsUsed": 4, + "unresolvedQuestions": [ + "NV12 是否同时作为输出格式", + "alpha 具体行为", + "PQ 输入是否 10-bit/12-bit" + ], + "conclusion": "NIS 官方协议明确支持 R8G8B8A8_UNORM 与 NV12 类非整数格式,支持 LDR/PQ/Linear 三种色彩范围,并有 HDR mode 开关。" + }, + { + "effectGroup": "NNEDI3", + "effectFamily": "NNEDI3", + "upstreamVendor": "Kevin Stone / community (mpv prescaler by bjin)", + "mappingConfidence": "明确映射", + "mappingNotes": "NNEDI3 为公开神经网络倍线算法;Magpie 效果组对应实现可能是 bjin mpv-prescalers 中的 luma prescaler,但未依据 Magpie 源码确认。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "mpv user shader hook (luma plane)", + "channelOrder": "luma", + "numericRepresentation": "unspecified", + "acceptanceStatus": "inferred_from_shader_interface", + "evidenceRef": "https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md", + "notes": "bjin README 表示 nnedi3 只放大 YUV 的 luma plane;外部输入格式由 mpv 提供,未枚举。" + }, + { + "format": "rgba16f / rgba16hf", + "apiOrContext": "mpv user shader internal intermediate", + "channelOrder": "RGBA", + "numericRepresentation": "FLOAT (16-bit)", + "acceptanceStatus": "inferred_from_shader_interface", + "evidenceRef": "https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md", + "notes": "README 关于 rgba16f/rgba16hf 的说明属于 mpv 用户着色器环境中的中间表面,不是 NNEDI3 算法 SDK 契约。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "mpv user shader hook output", + "channelOrder": "luma", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md", + "notes": "输出为放大后的 luma 平面;无原生后端格式枚举。" + }, + { + "format": "rgba16f / rgba16hf", + "apiOrContext": "mpv user shader internal intermediate", + "channelOrder": "RGBA", + "numericRepresentation": "FLOAT (16-bit)", + "acceptanceStatus": "inferred_from_shader_interface", + "evidenceRef": "https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md", + "notes": "同输入条目说明,仅中间表面。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "partial", + "outputFormatEnumeration": "partial", + "inputOutputRelation": "implementation_defined", + "alphaSemantics": "未说明。", + "rangeBoundary": "luma only / unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "luma-only 放大;chroma 由宿主 --cscale 处理;mpv 老 d3d11 驱动需要 rgba16hf 分支。", + "hdrImplication": "rgba16f 只证明 mpv user shader 中间表面,不构成 HDR API 契约。" + }, + "channelOrderAndAlpha": "README 表示 nnedi3/ravu 只放大 YUV 的 luma 平面;RGB 变体另有 ravu-rgb。", + "precisionAndRange": "rgba16f / rgba16hf(fp16);来自 mpv user shader 内部格式。", + "transferFunction": "未说明。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "FormatOnlyNoColorContract", + "hdrBoundary": "mpv user shader 内部处理浮点,但 README 未提供 HDR/PQ/linear 契约;不能作为厂商 HDR 保证。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "DirectFP16", + "profileRationale": "仅有的明确格式证据是 mpv prescaler 使用 rgba16f/rgba16hf 内部表面;这是用户着色器路径而非官方 SDK 契约。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "nnedi3 的 neurons/window 在文件名中(bjin 仓库)" + ], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md", + "title": "bjin/mpv-prescalers README", + "evidenceLevel": "OfficialRepository", + "claim": "Shaders use rgba16f internal format; nnedi3/ravu are luma prescalers", + "quoteOrSymbol": "Unrecognized/unavailable FORMAT name: rgba16f", + "searchRound": 1 + } + ], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "独立 NNEDI3 原生后端的格式契约", + "HDR 颜色空间" + ], + "conclusion": "NNEDI3 的 mpv 用户着色器资料显示内部使用 rgba16f/rgba16hf 并只处理 luma;没有可作为 Magpie 外置后端协议的官方格式列表。" + }, + { + "effectGroup": "Pixel Art", + "effectFamily": "无信息(Pixel Art 泛指像素画放大算法族)", + "upstreamVendor": "无信息", + "mappingConfidence": "无信息", + "mappingNotes": "公开搜索出现 libretro pixel-art enhancement systems、xBRZ、Kopf-Lischinski 等多种算法,无法唯一确定 Magpie 对应后端。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开搜索仅见多种像素画算法", + "notes": "无法唯一映射;不得把 xBRZ/Scale2x/Kopf-Lischinski 等某一算法格式写成该效果组已支持协议。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开搜索仅见多种像素画算法", + "notes": "同上。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "无统一资源约束可记录。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [], + "searchRoundsUsed": 1, + "unresolvedQuestions": [ + "具体像素画算法映射", + "格式", + "HDR/FP16" + ], + "conclusion": "Pixel Art 组无法唯一映射到公开算法,记录为无信息。" + }, + { + "effectGroup": "RAVU", + "effectFamily": "RAVU (Rapid and Accurate Video Upscaling)", + "upstreamVendor": "bjin / mpv-prescalers contributors", + "mappingConfidence": "明确映射", + "mappingNotes": "公开仓库 bjin/mpv-prescalers 的 RAVU 系列。", + "documentedInputFormats": [ + "mpv user-shader internal rgba16f/rgba16hf", + "ravu-yuv assumes YUV video after planes merged", + "ravu-rgb operates on RGB after planes merged" + ], + "documentedOutputFormats": [ + "内部 rgba16f/rgba16hf 表面" + ], + "formatBoundary": "作为 mpv user shader 使用;gather/compute 版本分别用 textureGather/compute;d3d11 老驱动可用 rgba16hf 分支;ravu/ravu-lite 仅放大 luma 平面;ravu-yuv 需 YUV,ravu-rgb 需 RGB;ravu-zoom 任意比例。", + "channelOrderAndAlpha": "YUV 或 RGB 变体;未单独说明 alpha。", + "precisionAndRange": "rgba16f/rgba16hf(16-bit float);未说明数值范围上限。", + "transferFunction": "未说明。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "FormatOnlyNoColorContract", + "hdrBoundary": "内部 FP16 浮点表面不等于厂商 HDR/PQ 契约;README 未说明 HDR 线性/PQ。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "DirectFP16", + "profileRationale": "明确内部 rgba16f/rgba16hf,但无颜色/光域契约;只能作为格式级 FP16 证据。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "ravu-lite", + "ravu", + "ravu-zoom", + "-ar anti-ringing 变体" + ], + "documentedPerformanceNotes": "README 说明 gather 通常较快、compute 对 yuv/rgb 更快,但无数值基准。", + "qualityPerformanceEvidence": "README 有定性速度说明,无可核实格式路径性能数据。", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md", + "title": "bjin/mpv-prescalers README", + "evidenceLevel": "OfficialRepository", + "claim": "RAVU variants and rgba16f/rgba16hf formats", + "quoteOrSymbol": "rgba16f ... rgba16hf", + "searchRound": 1 + } + ], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "外部原生后端的格式协议", + "HDR/色彩空间" + ], + "conclusion": "RAVU 明确为 mpv 用户着色器族,公开资料显示内部使用 rgba16f/rgba16hf 且分 YUV/RGB 变体;缺少可作为外置 SDK 的完整协议。" + }, + { + "effectGroup": "RTXVideo", + "effectFamily": "NVIDIA RTX Video Super Resolution (VFX SDK VSR filter)", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "RTX Video 效果组拆为 VSR、Denoiser、HDR;此条为 VFX SDK Video Super Resolution filter。", + "documentedInputFormats": [ + "GPU buffers in BGRA or RGBA interleaved format, 8-bit unsigned per component" + ], + "documentedOutputFormats": [ + "Same BGRA or RGBA interleaved U8 GPU buffer" + ], + "formatBoundary": "输入输出必须 GPU buffer,BGRA/RGBA interleaved,每分量 8-bit unsigned;Denoise/Deblur modes 输出分辨率必须与输入相同;VSR 支持不同 modes;建议最小 360p。", + "channelOrderAndAlpha": "BGRA 或 RGBA;alpha 分量在 U8 格式中未单独描述行为。", + "precisionAndRange": "8-bit unsigned integer per component;仅 SDR 级像素格式。", + "transferFunction": "未在页面说明(SDR 视频输入通常 YUV->RGB 转换在外部)。", + "primariesAndColorSpace": "未说明;相关 SDK 页面提到颜色不对时交换 709/601(YUV 转换)。", + "hdrSupport": "SDROnly", + "hdrBoundary": "VFX VSR 页面只列 8-bit U8 BGRA/RGBA,无 HDR/PQ/FP16 路径。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "官方页面明确 U8 BGRA/RGBA 输入输出,没有 HDR/FP16。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": "VSR 是视频时域增强;无帧生成/运动向量/深度输入;Denoise/Deblur 同分辨率。", + "documentedQualityOptions": [ + "VSR_Bicubic", + "VSR_Low", + "VSR_Medium", + "VSR_High", + "VSR_Ultra", + "HighBitrate_*", + "Denoise_*", + "Deblur_*" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://docs.nvidia.com/maxine/vfx/1.2.0.0/Filters/VideoSuperResolution.html", + "title": "NVIDIA VFX SDK Video Super Resolution", + "evidenceLevel": "OfficialDocs", + "claim": "VSR input/output BGRA or RGBA U8; modes", + "quoteOrSymbol": "The input and output of the VSR filter are GPU buffers in BGRA or RGBA interleaved format...8-bit unsigned integer", + "searchRound": 1 + } + ], + "searchRoundsUsed": 3, + "unresolvedQuestions": [ + "VSR 是否接受 NV12/P010 内部路径", + "HDR 版本协议" + ], + "conclusion": "RTX Video VSR 的 VFX SDK 官方页面协议为 BGRA/RGBA 8-bit U8 GPU buffer;没有 FP16/HDR 输入输出契约。" + }, + { + "effectGroup": "RTXVideo", + "effectFamily": "NVIDIA RTX Video Denoiser (VFX SDK VSR Denoise modes)", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "VFX SDK VSR filter 内含 Denoise_Low/Medium/High/Ultra modes,作为 Denoiser 子条目。", + "documentedInputFormats": [ + "BGRA or RGBA interleaved U8 GPU buffers" + ], + "documentedOutputFormats": [ + "BGRA or RGBA interleaved U8 GPU buffers, same resolution as input" + ], + "formatBoundary": "Denoise modes 不支持 upscaling,输出分辨率必须等于输入;输入输出同为 BGRA/RGBA U8。", + "channelOrderAndAlpha": "BGRA/RGBA。", + "precisionAndRange": "8-bit unsigned per component。", + "transferFunction": "未说明。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "SDROnly", + "hdrBoundary": "官方页面只有 U8 输入输出,无 HDR 路径。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "官方 VSR 页面把 Denoise modes 也限定为 BGRA/RGBA U8,无 HDR。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": "Denoise 模式不放大,输出同输入分辨率;无光流/深度输入。", + "documentedQualityOptions": [ + "Denoise_Low", + "Denoise_Medium", + "Denoise_High", + "Denoise_Ultra" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://docs.nvidia.com/maxine/vfx/1.2.0.0/Filters/VideoSuperResolution.html", + "title": "NVIDIA VFX SDK Video Super Resolution (Denoise modes)", + "evidenceLevel": "OfficialDocs", + "claim": "Denoise modes same BGRA/RGBA U8 and same output resolution", + "quoteOrSymbol": "Denoise_Low ... resolution of the output must be the same as input", + "searchRound": 1 + } + ], + "searchRoundsUsed": 2, + "unresolvedQuestions": [ + "独立 Denoiser 模型格式", + "HDR" + ], + "conclusion": "RTX Video Denoiser 在官方 VFX 文档中与 VSR 共用 BGRA/RGBA U8、同分辨率输出协议,无 HDR/FP16。" + }, + { + "effectGroup": "RTXVideo", + "effectFamily": "NVIDIA RTX Video HDR", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "NVIDIA 官方博客/产品页将 RTX Video HDR 描述为 RTX Video SDK 功能;未检索到完整公开 API 协议页。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://developer.nvidia.com/blog/enhancing-low-resolution-sdr-video-with-the-nvidia-rtx-video-sdk/", + "notes": "官方博客只说 SDR 视频转 HDR10 质量,未给出输入像素格式/API 上下文。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "https://developer.nvidia.com/blog/enhancing-low-resolution-sdr-video-with-the-nvidia-rtx-video-sdk/", + "notes": "官方博客只说 HDR10 质量输出,未给出输出像素格式/API 上下文。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明。", + "rangeBoundary": "SDR input -> HDR10 output 的产品语义;无量值范围。", + "transferBoundary": "未公开(博客层面提到 HDR10/PQ,但没有 API 转换契约)。", + "resourceConstraints": "无 API 资源状态、尺寸或颜色转换文档。", + "hdrImplication": "产品方向明确为 SDR->HDR,但无公开 HDR/FP16 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "官方博客/介绍提到 HDR10/PQ 场景;公开协议页未给出转换细节。", + "primariesAndColorSpace": "博客提到从 sRGB 扩展到 scRGB/HDR 色域(不同页面措辞不一致);无正式协议。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "官方仅宣称 SDR->HDR,未公开输入/输出格式、metadata、tone mapping 或 pre-exposure 契约。", + "requiredHdrMetadata": "未说明", + "candidateInternalProfile": "无信息", + "profileRationale": "没有可核实的输入输出协议来源。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": "无公开协议。", + "documentedQualityOptions": [ + "RTX Video HDR on/off 等产品级选项,不是 API 格式档位" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialForum", + "sources": [ + { + "url": "https://developer.nvidia.com/blog/enhancing-low-resolution-sdr-video-with-the-nvidia-rtx-video-sdk/", + "title": "NVIDIA Technical Blog: RTX Video SDK", + "evidenceLevel": "OfficialForum", + "claim": "RTX Video HDR converts SDR video to HDR10 quality; VSR upscales", + "quoteOrSymbol": "RTX Video HDR converts SDR video to HDR10 quality", + "searchRound": 1 + } + ], + "searchRoundsUsed": 5, + "unresolvedQuestions": [ + "API 输入输出格式", + "NvCVImage pixel format", + "PQ metadata", + "scRGB/HDR10 工作空间" + ], + "conclusion": "RTX Video HDR 有明确官方产品宣称但缺少公开 API 格式协议,记录为无格式信息、仅 HDR 方向明确。" + }, + { + "effectGroup": "Sharpen", + "effectFamily": "无信息(Sharpen 为通用锐化效果族)", + "upstreamVendor": "无信息", + "mappingConfidence": "无信息", + "mappingNotes": "公开搜索出现 NIS NVSharpen、Android SharpenFilter 等多个互不相同的后端,无法唯一对应。", + "documentedInputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开搜索出现多个锐化实现", + "notes": "无法唯一映射;不得把任一锐化实现的格式写成该效果组已支持协议。" + } + ], + "documentedOutputFormats": [ + { + "format": "unspecified", + "apiOrContext": "公开资料未枚举", + "channelOrder": "unspecified", + "numericRepresentation": "unspecified", + "acceptanceStatus": "not_enumerated", + "evidenceRef": "公开搜索出现多个锐化实现", + "notes": "同上。" + } + ], + "formatBoundary": { + "inputFormatEnumeration": "not_enumerated", + "outputFormatEnumeration": "not_enumerated", + "inputOutputRelation": "unknown", + "alphaSemantics": "未说明。", + "rangeBoundary": "unknown", + "transferBoundary": "unspecified", + "resourceConstraints": "无统一资源约束可记录。", + "hdrImplication": "无公开 HDR 格式契约。" + }, + "channelOrderAndAlpha": "无信息", + "precisionAndRange": "无信息", + "transferFunction": "无信息", + "primariesAndColorSpace": "无信息", + "hdrSupport": "无信息", + "hdrBoundary": "未说明", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "无信息", + "profileRationale": "无信息", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [], + "documentedPerformanceNotes": "无信息", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "无信息", + "sources": [], + "searchRoundsUsed": 1, + "unresolvedQuestions": [ + "具体锐化算法映射", + "格式", + "HDR/FP16" + ], + "conclusion": "Sharpen 效果组无法唯一映射到上游算法,记录为无信息。" + }, + { + "effectGroup": "SMAA", + "effectFamily": "SMAA (Subpixel Morphological Antialiasing)", + "upstreamVendor": "Jorge Jimenez / iryoku", + "mappingConfidence": "明确映射", + "mappingNotes": "官方 iryoku/smaa 仓库。", + "documentedInputFormats": [ + "colorTex: RGBA texture (2D color/luma input)", + "edgesTex/areaTex/searchTex 等内部/预计算纹理", + "depthTex for depth edge detection" + ], + "documentedOutputFormats": [ + "colorTex/blended output 4-channel color buffer" + ], + "formatBoundary": "官方集成说明要求两个 RGBA 时域 render target;创建后清除 alpha;所有 sampler linear + clamp;纹理读写默认非 sRGB,只有最终 NeighborhoodBlending 的 input/output 可 sRGB;64-bit 输入在 GCN 上有半速率线性过滤。", + "channelOrderAndAlpha": "RGBA;alpha 通道在中间步骤被用作速度/索引等;文档警告清除 alpha。", + "precisionAndRange": "float4/float 通用;提到 64-bit 输入和 half-rate filtering,未给出 8/10/16 位清单。", + "transferFunction": "建议在 gamma-corrected(非 sRGB 采样)空间做 luma/color edge detection;最终 blending pass 可处理 sRGB 读写。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "Unspecified", + "hdrBoundary": "官方代码未声称 HDR;luma edge detection 注释要求 gamma-corrected colors,通常为 LDR 空间。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "官方集成注释围绕 gamma/sRGB 空间和 RGBA 临时目标,未见 HDR/线性 HDR 直接路径;保守记为 SDR 兼容。", + "auxiliaryInputs": [ + "velocityTex (temporal variant)", + "depthTex (depth edge detection)", + "areaTex/searchTex lookup textures", + "predicationTex optional" + ], + "auxiliaryInputFormatsAndSemantics": "area/search 为预计算 LUT;velocity 解码方式由 SMAA_DECODE_VELOCITY 定义;depth 可为深度纹理。", + "temporalOrFrameGenerationConstraints": "SMAA 1x 无时域;temporal/supersampling 变体需要多子样本、previous/current color 和 velocity。", + "documentedQualityOptions": [ + "SMAA 1x/2x/T2x 等宏/预设由官方源码定义" + ], + "documentedPerformanceNotes": "官方注释提示 64-bit 输入半速率线性过滤,可用 point filtering 规避;未给完整性能基准。", + "qualityPerformanceEvidence": "官方注释有格式相关性能提示,但没有格式路径性能基准。", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/iryoku/smaa/master/README.md", + "title": "SMAA README", + "evidenceLevel": "OfficialRepository", + "claim": "SMAA official repo and integration pointer", + "quoteOrSymbol": "Checkout the technical paper", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/iryoku/smaa/master/SMAA.hlsl", + "title": "SMAA.hlsl", + "evidenceLevel": "OfficialRepository", + "claim": "Integration notes: RGBA render targets, sRGB rules, filtering", + "quoteOrSymbol": "All texture reads and buffer writes must be non-sRGB...", + "searchRound": 2 + } + ], + "searchRoundsUsed": 4, + "unresolvedQuestions": [ + "具体 DXGI 格式支持", + "HDR 线性输入", + "alpha 对外值" + ], + "conclusion": "SMAA 官方仓库提供了着色器级协议:RGBA 输入输出、RGBA 临时目标、gamma/sRGB 空间建议;未公开现代 HDR/FP16 API 契约。" + }, + { + "effectGroup": "xBRZ", + "effectFamily": "xBRZ (pixel-art scaling algorithm)", + "upstreamVendor": "Zenju", + "mappingConfidence": "明确映射", + "mappingNotes": "xBRZ 原算法由 Zenju 发布;本条目证据来自 TypeScript/WASM 移植 kayahr/xbrz。", + "documentedInputFormats": [ + "RGBA pixel data in Uint8ClampedArray (TypeScript port)" + ], + "documentedOutputFormats": [ + "RGBA pixel data in Uint8ClampedArray" + ], + "formatBoundary": "TypeScript 移植接口为源/目标 RGBA 像素缓冲;缩放因子 2-6;支持 alpha 透明;非原生 SDK 协议。", + "channelOrderAndAlpha": "RGBA;支持 alpha 透明。", + "precisionAndRange": "8-bit per channel in Uint8ClampedArray([0,255]);无 HDR/浮点。", + "transferFunction": "未说明。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "SDROnly", + "hdrBoundary": "移植接口是 8-bit RGBA,无 HDR。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "证据显示 RGBA 8-bit 像素缓冲;无浮点/HDR 接口。", + "auxiliaryInputs": [], + "auxiliaryInputFormatsAndSemantics": [], + "temporalOrFrameGenerationConstraints": [], + "documentedQualityOptions": [ + "scale factors 2-6" + ], + "documentedPerformanceNotes": "未找到可核实的格式路径性能结论。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "GitHubExperiment", + "sources": [ + { + "url": "https://raw.githubusercontent.com/kayahr/xbrz/master/README.md", + "title": "kayahr/xbrz TypeScript/WASM port", + "evidenceLevel": "GitHubExperiment", + "claim": "Port uses RGBA Uint8ClampedArray and supports alpha", + "quoteOrSymbol": "Source and target are RGBA pixel data in a Uint8ClampedArray", + "searchRound": 1 + } + ], + "searchRoundsUsed": 3, + "unresolvedQuestions": [ + "Magpie 原生后端是否使用该接口", + "原 C++ xBRZ 的格式契约", + "HDR" + ], + "conclusion": "xBRZ 映射明确,但可靠公开协议只有社区移植的 RGBA 8-bit 接口,原生后端协议未确认。" + }, + { + "effectGroup": "XeSS", + "effectFamily": "Intel XeSS Super Resolution (XeSS-SR)", + "upstreamVendor": "Intel", + "mappingConfidence": "明确映射", + "mappingNotes": "Intel xess 官方仓库与 Developer Guide。", + "documentedInputFormats": [ + "R16G16B16A16_FLOAT", + "R11G11B10_FLOAT", + "R8G8B8A8_UNORM", + "其他 any linear color format; only UNORM integer color formats allowed", + "Motion vectors: R16G16_FLOAT", + "Depth: any depth format such as D32_FLOAT or D24_UNORM" + ], + "documentedOutputFormats": [ + "Same format and color space as input (2D output texture)" + ], + "formatBoundary": "输入颜色可为 LDR/HDR 任意 linear 格式;整数格式只允许 UNORM;输出必须与输入同格式同色彩空间,且输出 alpha 不保留并填 1.0;D3D12 输入 NON_PIXEL_SHADER_RESOURCE,输出 UAV;Vulkan 对应状态。", + "channelOrderAndAlpha": "RGBA/R11G11B10 等格式;XeSS-SR 不保留输出 alpha,填 1.0。", + "precisionAndRange": "颜色为 scene-referred scRGB,1.0 为 80 nits SDR 白点,HDR 可 >1;LDR 输入需 XESS_INIT_FLAG_LDR_INPUT_COLOR;exposureScale 等。", + "transferFunction": "任何 linear 色彩格式(LDR/HDR);非 sRGB/PQ 编码直接输入。", + "primariesAndColorSpace": "scRGB(scene-referred);输出同输入色彩空间。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "推荐直接提供 HDR 线性/scRGB;可设置 exposureScale/pre-exposure/exposureMultiplier;算法内部自行 tonemap,输出恢复;LDR 也可。", + "requiredHdrMetadata": [ + "exposureScale", + "preExposure/exposureMultiplier 可选", + "无 MaxCLL/MaxFALL 要求" + ], + "candidateInternalProfile": "DirectFP16", + "profileRationale": "官方明确支持 FP16/RGBA16F、scRGB HDR 线性并推荐 HDR 输入。", + "auxiliaryInputs": [ + "motion vectors", + "depth (low-res MV mode)", + "responsive pixel mask (optional)", + "exposure scale/multiplier", + "jitter" + ], + "auxiliaryInputFormatsAndSemantics": "MVs R16G16_FLOAT screen-space pixels, low-res default or high-res dilated; depth D32_FLOAT/D24_UNORM; responsive mask R channel float [0,1]; input states as above.", + "temporalOrFrameGenerationConstraints": "时域超分:需 jittered color, motion vectors, depth when low-res; output target resolution; fixed/dynamic input resolution ranges; reset history on cuts.", + "documentedQualityOptions": [ + "Ultra Quality/Quality/Balanced/Performance presets (via optimal input resolution API)" + ], + "documentedPerformanceNotes": "开发指南建议 HDR input color 推荐、使用 FP16 color buffer in scene linear HDR(性能提示);无格式路径基准。", + "qualityPerformanceEvidence": "指南有使用 FP16 scene-linear HDR 的建议,但无定量比较。", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/intel/xess/main/doc/xess_sr_developer_guide_english.md", + "title": "Intel XeSS-SR Developer Guide", + "evidenceLevel": "OfficialRepository", + "claim": "Formats and color space: R16G16B16A16_FLOAT/R11G11B10_FLOAT/R8G8B8A8_UNORM, scRGB, output same", + "quoteOrSymbol": "XeSS-SR accepts both LDR and HDR input colors in any linear color format", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/intel/xess/main/README.md", + "title": "Intel xess README", + "evidenceLevel": "OfficialRepository", + "claim": "XeSS-SR and XeSS-FG/XeLL availability", + "quoteOrSymbol": "XeSS Super Resolution (XeSS-SR)", + "searchRound": 2 + } + ], + "searchRoundsUsed": 5, + "unresolvedQuestions": [ + "每种 DXGI/Vulkan 格式完整矩阵", + "PQ/HLG 是否需外部转换", + "primaries 非 scRGB" + ], + "conclusion": "XeSS-SR 官方协议非常明确:支持 R16G16B16A16_FLOAT/R11G11B10_FLOAT/R8G8B8A8_UNORM 等线性格式,scRGB scene-referred,输出同输入格式且 alpha 填 1。" + }, + { + "effectGroup": "XeSSFG", + "effectFamily": "Intel XeSS Frame Generation (XeSS-FG)", + "upstreamVendor": "Intel", + "mappingConfidence": "明确映射", + "mappingNotes": "Intel xess 官方仓库 XeSS-FG Developer Guide。", + "documentedInputFormats": [ + "Back buffer/HUD-less/UI-only: R10G10B10A2_UNORM for HDR10/BT.2100 HDR display", + "Motion vectors: R16G16_FLOAT or similar", + "Depth: any depth format such as D32_FLOAT or D24_UNORM", + "UI Alpha: single channel; UI Color and Alpha: same backbuffer format" + ], + "documentedOutputFormats": [ + "Interpolated frames output to proxy swap chain; same back buffer pixel format (HDR10 R10G10B10A2_UNORM documented)" + ], + "formatBoundary": "HDR display 支持 R10G10B10A2_UNORM + HDR10/BT.2100;明确不支持 FP16 HDR/scRGB;back buffer, HUD-less, UI-only 必须同像素格式、色彩空间、尺寸;MV 与 depth buffer 尺寸一致。", + "channelOrderAndAlpha": "UI-only texture 有 alpha;非预乘 alpha 可用标志;合成公式 Final.RGB = UIonly.RGB + (1-UIonly.Alpha) * HUDlessColor.RGB;2-bit alpha 对 UI 合成不足。", + "precisionAndRange": "HDR 显示为 10-bit R10G10B10A2_UNORM HDR10/BT.2100;不支持 FP16/scRGB;LDR 显示用 swapchain 格式。", + "transferFunction": "HDR10 / BT.2100(PQ)用于 HDR 显示。", + "primariesAndColorSpace": "HDR10 / BT.2100;非 HDR 未细说。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "HDR 必须使用 R10G10B10A2_UNORM 与 HDR10/BT.2100,back buffer/HUDless/UI 同格式;不支持 FP16/scRGB;未给出 PQ 解码细节。", + "requiredHdrMetadata": [ + "HDR display pixel format R10G10B10A2_UNORM", + "HDR10/BT.2100 color space", + "无 MaxCLL/MaxFALL 公开参数" + ], + "candidateInternalProfile": "BoundedHDR", + "profileRationale": "官方明确 HDR 只走 10-bit R10G10B10A2 UNORM/HDR10 BT.2100,不接收 FP16/scRGB,故为有界 HDR 契约。", + "auxiliaryInputs": [ + "motion vectors", + "depth", + "HUD-less color", + "UI-only texture", + "jitter offset", + "resetHistory", + "motion vector scale" + ], + "auxiliaryInputFormatsAndSemantics": "MV R16G16_FLOAT/similar low/high res; depth any format, same size as MV; HUDless/UI same as backbuffer format/color; UI alpha single channel.", + "temporalOrFrameGenerationConstraints": "需要 XeLL latency reduction 启用;不能独占全屏;40 FPS 最低/60 FPS 推荐;禁用/减少 motion blur;切换或与第三方帧生成不兼容;UI composition modes.", + "documentedQualityOptions": [ + "UI composition modes", + "interpolated frames count 等 XeSS-FG 公开选项" + ], + "documentedPerformanceNotes": "官方建议 low-res MVs 以免 high-res 大幅降低性能;无格式路径定量表。", + "qualityPerformanceEvidence": "官方说明 high-res MV 性能影响,但无格式路径性能基准。", + "evidenceLevel": "OfficialRepository", + "sources": [ + { + "url": "https://raw.githubusercontent.com/intel/xess/main/doc/xess_fg_developer_guide_english.md", + "title": "Intel XeSS-FG Developer Guide", + "evidenceLevel": "OfficialRepository", + "claim": "HDR display support R10G10B10A2_UNORM HDR10/BT.2100; no FP16/scRGB", + "quoteOrSymbol": "XeSS-FG provides support for HDR displays with the R10G10B10A2_UNORM pixel format... does not support FP16 HDR format and scRGB color space", + "searchRound": 1 + }, + { + "url": "https://raw.githubusercontent.com/intel/xess/main/README.md", + "title": "Intel xess README", + "evidenceLevel": "OfficialRepository", + "claim": "XeSS-FG available on Intel Arc and non-Intel with SM6.4", + "quoteOrSymbol": "XeSS Frame Generation (XeSS-FG)", + "searchRound": 2 + } + ], + "searchRoundsUsed": 4, + "unresolvedQuestions": [ + "LDR 下的完整 swapchain 格式清单", + "PQ metadata 传递" + ], + "conclusion": "XeSS-FG 官方明确 HDR 契约:R10G10B10A2_UNORM/HDR10 BT.2100,不支持 FP16/scRGB;所有参与合成的纹理必须同格式同色彩空间。" + }, + { + "effectGroup": "NVIDIA Optical Flow", + "effectFamily": "NVIDIA Optical Flow SDK (NvOF)", + "upstreamVendor": "NVIDIA", + "mappingConfidence": "明确映射", + "mappingNotes": "附加依赖项对应 NVIDIA Optical Flow SDK NvOF API(另有 NVOFA 新 API,本次主条目为 NvOF 头文件证据)。", + "documentedInputFormats": [ + "NV_OF_BUFFER_FORMAT_GRAYSCALE8", + "NV_OF_BUFFER_FORMAT_NV12", + "NV_OF_BUFFER_FORMAT_ABGR8 (A8B8G8R8)" + ], + "documentedOutputFormats": [ + "NV_OF_FLOW_VECTOR (SHORT2, S10.5 flowx/flowy)", + "NV_OF_STEREO_DISPARITY (for stereo mode)", + "Cost buffer NV_OF_BUFFER_FORMAT_UINT or UINT8" + ], + "formatBoundary": "输入帧支持 GRAYSCALE8/NV12/ABGR8;输出/提示为 SHORT2(S10.5);外部提示/成本格式另有要求;支持 output grid size 1/2/4;有宽高 min/max caps。", + "channelOrderAndAlpha": "ABGR8 为 8-bit packed A8B8G8R8;NV12 为 Y planar + interleaved UV。", + "precisionAndRange": "8-bit input; output flow vector int16 S10.5 表示亚像素;无 HDR/PQ 声明。", + "transferFunction": "未说明。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "SDROnly", + "hdrBoundary": "官方头文件输入格式只列 8-bit 亮度/NV12/ABGR8,无 FP16/HDR。", + "requiredHdrMetadata": "无信息", + "candidateInternalProfile": "SDRCompatible", + "profileRationale": "NvOF 头文件列出的输入只有 8-bit 格式,输出为定点光流。", + "auxiliaryInputs": [ + "externalHints (optional)", + "cost buffer (optional)", + "reference frame" + ], + "auxiliaryInputFormatsAndSemantics": "externalHints/outputBuffer use NV_OF_FLOW_VECTOR for optical flow; cost UINT or UINT8; reference frame same input format as inputFrame.", + "temporalOrFrameGenerationConstraints": "光流在两帧(input/reference)间估计;output grid size 决定流向量网格;scene change/hints 等。", + "documentedQualityOptions": [ + "NV_OF_OUTPUT_VECTOR_GRID_SIZE 1/2/4" + ], + "documentedPerformanceNotes": "头文件注明 legacy 32-bit cost 不高效,建议 8-bit cost;无格式路径完整基准。", + "qualityPerformanceEvidence": "官方头文件有成本格式性能建议,但无定量表。", + "evidenceLevel": "OfficialHeader", + "sources": [ + { + "url": "https://raw.githubusercontent.com/NVIDIA/NVIDIAOpticalFlowSDK/master/nvOpticalFlowCommon.h", + "title": "NVIDIA Optical Flow SDK nvOpticalFlowCommon.h", + "evidenceLevel": "OfficialHeader", + "claim": "Buffer formats: GRAYSCALE8, NV12, ABGR8; flow vector SHORT2", + "quoteOrSymbol": "NV_OF_BUFFER_FORMAT_NV12 ... NV_OF_BUFFER_FORMAT_ABGR8", + "searchRound": 1 + } + ], + "searchRoundsUsed": 4, + "unresolvedQuestions": [ + "NVOFA 新 API 的 RGBA/F16 支持", + "HDR 帧是否需转为 NV12/ABGR8" + ], + "conclusion": "NVIDIA Optical Flow NvOF 官方头文件支持 8-bit GRAYSCALE/NV12/ABGR8 输入,输出为 S10.5 定点 SHORT2 光流;未提供 HDR/FP16 输入路径。" + }, + { + "effectGroup": "AMD FidelityFX Optical Flow", + "effectFamily": "FidelityFX Optical Flow", + "upstreamVendor": "AMD", + "mappingConfidence": "明确映射", + "mappingNotes": "AMD FidelityFX SDK 中用于 FSR3 的光流技术,官方 docs/optical-flow.md。", + "documentedInputFormats": [ + "color input resource (格式未在文档枚举)" + ], + "documentedOutputFormats": [ + "opticalFlowVector: R16G16_SINT", + "opticalFlowSCD: R32_UINT (3x1 scene change detection)" + ], + "formatBoundary": "以 8x8 block 计算,输出纹理尺寸由 (displaySize+block-1)/8 决定;block size 固定 8;color 输入经 transfer function/luminance 转换。", + "channelOrderAndAlpha": "未说明 color 通道顺序;算法使用亮度。", + "precisionAndRange": "输出 R16G16_SINT;SCD R32_UINT;输入颜色格式未枚举。", + "transferFunction": "输入有 backbufferTransferFunction 与 minMaxLuminance,用于 HDR 管道转换到 luminance。", + "primariesAndColorSpace": "未说明。", + "hdrSupport": "ExplicitHDR", + "hdrBoundary": "支持 HDR pipeline 的 luminance 转换:需 backbufferTransferFunction 和 minMaxLuminance;未公开颜色输入格式。", + "requiredHdrMetadata": [ + "backbufferTransferFunction", + "minMaxLuminance" + ], + "candidateInternalProfile": "BoundedHDR", + "profileRationale": "官方要求把 HDR 颜色经 transfer/luminance 转成内部亮度,适合有界转换路径。", + "auxiliaryInputs": [ + "previous/current color input", + "internal history resources for pyramid/histogram" + ], + "auxiliaryInputFormatsAndSemantics": "color input is current frame; internal history resources; SCD output 3x1 R32_UINT.", + "temporalOrFrameGenerationConstraints": "需要连续帧、内部金字塔、历史 histogram 做场景变化检测;用于 FSR3 帧插值。", + "documentedQualityOptions": [ + "8x8 block size fixed in release" + ], + "documentedPerformanceNotes": "文档内存表按 GPU/4K 显示,但未比较不同格式成本。", + "qualityPerformanceEvidence": "未找到可核实的格式路径性能结论", + "evidenceLevel": "OfficialDocs", + "sources": [ + { + "url": "https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/release-FSR3-3.0.3/docs/techniques/optical-flow.md", + "title": "FidelityFX Optical Flow", + "evidenceLevel": "OfficialDocs", + "claim": "Optical flow outputs R16G16_SINT and R32_UINT; HDR transfer/luminance", + "quoteOrSymbol": "opticalFlowVector ... R16G16_SINT ... R32_UINT", + "searchRound": 1 + } + ], + "searchRoundsUsed": 3, + "unresolvedQuestions": [ + "输入 color 的格式枚举", + "输出语义坐标空间精确单位", + "独立于 FSR3 的 SDK" + ], + "conclusion": "AMD FidelityFX Optical Flow 官方协议公开了光流输出 R16G16_SINT、SCD R32_UINT 和 HDR transfer/luminance 输入需求,但输入颜色格式未枚举。" + } +] \ No newline at end of file diff --git a/docs/experimental/HDR_PROTOCOL_EVIDENCE.md b/docs/experimental/HDR_PROTOCOL_EVIDENCE.md new file mode 100644 index 000000000..9f773dc61 --- /dev/null +++ b/docs/experimental/HDR_PROTOCOL_EVIDENCE.md @@ -0,0 +1,1416 @@ +# Magpie 外置 HDR 效果层协议证据调研 + +> 本文件只基于公开网络资料,不读取、引用或推断任何本地 Magpie 源码、效果描述、HLSL、配置、日志、二进制或历史实验文件。 + +## 研究范围 + +研究 25 个 Magpie 效果组和 2 个附加依赖项的公开图像输入/输出协议;未来架构外部交付面预设为 `DXGI_FORMAT_R16G16B16A16_FLOAT`,但本次不设计代码、不修改工程。 + +## 排除项 + +- 不读取/搜索/引用/分析本地 Magpie 源码、效果描述、HLSL、配置、日志、二进制、历史实验文件或本地仓库内容。 +- 不依据 Magpie 当前实现反推任何效果是否支持 FP16/HDR/R8/R10/NV12 等格式。 +- 排除已由本地实验单独验证的独立效果:不搜索、不写入矩阵、不计入效果数。 +- 不下载 SDK 压缩包、驱动安装包、完整仓库镜像、网页缓存、截图、网页转储或原始爬虫数据。 + +## 证据等级 + +- `OfficialDocs`:官方 SDK 文档/编程指南/API 参考 +- `OfficialHeader`:官方头文件 +- `OfficialSample`:官方示例 +- `OfficialRepository`:官方 GitHub 仓库 +- `OfficialForum`:厂商开发者论坛/官方发布说明/官方博客 +- `GitHubExperiment`:GitHub issue/fork/实验/社区结论 +- `无信息`:十轮内没有可靠协议结论 + +## 十轮上限规则 + +每个效果族最多 10 轮;一轮定义为一次新搜索查询或一个新来源的导航/查阅;重复搜索同一关键词或反复打开同一来源仍计轮。资料没有明确写出的字段一律记“未说明/无信息”。 + +## 总览矩阵 + +| 效果组 | 上游/SDK | 已证实输入格式 | 已证实输出格式 | HDR/FP16 状态 | 候选内部 profile | 证据等级 | 搜索轮数 | 核心格式边界 | +|---|---|---|---|---|---|---|---|---| +| Anime4K | Anime4K | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | OfficialRepository | 7 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=implementation_defined; alphaSemantics=未说明;不得从 mpv 用户着色器可编译推断 alpha 契约。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| CAS | FidelityFX Contrast Adaptive Sharpening (CAS) | DXGI_FORMAT_R8G8B8A8_UNORM(reference_implementation_only);DXGI_FORMAT_R16G16B16A16_FLOAT(reference_implementation_only);unspecified(not_enumerated) | DXGI_FORMAT_R8G8B8A8_UNORM(reference_implementation_only);DXGI_FORMAT_R16G16B16A16_FLOAT(reference_implementation_only);unspecified(not_enumerated) | Unspecified | SDRCompatible | OfficialDocs | 7 | inputFormatEnumeration=partial; outputFormatEnumeration=partial; inputOutputRelation=same_format_required; alphaSemantics=未说明;SDK 回调只读写 RGB,未见 alpha 透传/置 1 声明。; rangeBoundary=sRGB/linear 解释均支持;shader 转换注释按 [0,1] 线性空间处理。; hdrImplication=官方参考实现支持 R16G16B16A16_FLOAT 与 linear 开关,但这属于参考实现路径;SDK 技术文档未提供 H… | +| CRT | 无信息(CRT 效果族未能在公开资料中唯一映射) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | 无信息 | 1 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明;名称不能唯一映射,无法建立 alpha 契约。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| CuNNy | CuNNy | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | OfficialRepository | 2 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=implementation_defined; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| CuNNy2 | 无信息(未能定位独立 CuNNy2 上游仓库/SDK) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | 无信息 | 2 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| Diagnostics | 无信息(Diagnostics 是通用诊断效果名) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | 无信息 | 1 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| DLSS | NVIDIA DLSS Super Resolution (DLSS-SR) | Color input: any supported buffer format for the API;Motion vectors: RG32_FLOAT or RG16_FLOAT;Depth: any one-channel format (R32_FLOAT/D32_FLOAT) or depth-stencil (D24S8);Exposure: 1x1 texture, R16F preferred (presets J/… | Output buffer: any supported buffer format for the API;Previous output/history buffer: optional, RGBA16F | ExplicitHDR | DirectFP16 | OfficialDocs | 6 | 官方编程指南要求输入为 SRV/read 状态,输出为 UAV/write 状态;颜色输入任意 API 支持格式,输出任意 API 支持格式;历史输出建议 RGBA16F;最小输出 32x32;无规定最大分辨率但 8K 以上未保证。 | +| DLSSFG | NVIDIA DLSS Frame Generation (DLSS-G / DLSS 4 Multi-Frame Gen) | Backbuffer/Final Color resource (format not enumerated in public text);Motion vectors/depth: same DLSS-SR requirements | OutputInterpolated/OutputReal: same texture format as Backbuffer | ExplicitHDR | FormatOnlyNoColorContract | OfficialRepository | 5 | 官方头文件写明 Required Output Texture (same texture format as Backbuffer);HUDless 与 UI 纹理需与 backbuffer 同尺寸/格式/色彩空间;backbuffer 资源经 Streamline swapchain 拦截。 | +| FSR | AMD FidelityFX Super Resolution 1 (FSR1 spatial upscaler) | unspecified(inferred_from_shader_interface);unspecified(not_enumerated) | unspecified(inferred_from_shader_interface);unspecified(not_enumerated) | SDROnly | SDRCompatible | OfficialDocs | 6 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=implementation_defined; alphaSemantics=未说明。; rangeBoundary=[0,1] perceptual sRGB;RCAS 负输入会产生 NaN。; hdrImplication=FSR1 核心文档没有 HDR 直接输入路径;线性 HDR helper 属可选周边工具,不能算核心格式契约。 | +| FSR2 | AMD FidelityFX Super Resolution 2 (FSR2 temporal upscaler) | Color buffer: APPLICATION SPECIFIED (render res);Depth: APPLICATION SPECIFIED (1x FLOAT);Motion vectors: APPLICATION SPECIFIED (2x FLOAT);Reactive mask: R8_UNORM;Transparency & composition mask: R8_UNORM;Exposure: R32_FL… | Adjusted color buffer (internal): R16G16B16A16_FLOAT;Upscaled buffer (internal): R16G16B16A16_FLOAT;API 输出缓冲的公开格式在文档中未单独枚举,一般随应用分配资源 | ExplicitHDR | DirectFP16 | OfficialRepository | 4 | 颜色输入由应用指定格式;HDR 需设置 HDR flag;深度单浮点;MVs 2x float 屏幕空间范围 [<-w,-h>,];内部多使用 16-bit;输出最终与输入同域(内部 tonemap 会被反转);要求 GPU 支持 typed UAV load 和 R16G16B16A16_UNORM。 | +| FSR3 | AMD FidelityFX Super Resolution 3.1 Upscaler (FSR3 SR) | Color buffer: APPLICATION SPECIFIED;Depth: APPLICATION SPECIFIED (1x FLOAT);Motion vectors: APPLICATION SPECIFIED (2x FLOAT);Reactive mask: R8_UNORM;T&C mask: R8_UNORM;Exposure: R32_FLOAT | 未单独枚举;文档称输出与原始输入同域(internal tonemap 被反转) | ExplicitHDR | DirectFP16 | OfficialDocs | 2 | 与 FSR2 输入结构一致;HDR flag 开启时输入 linear;要求 R16G16B16A16_UNORM typed UAV 支持;API 未在文档中列 DXGI 输出枚举。 | +| FSR3 | AMD FidelityFX Super Resolution 3 Frame Interpolation / Frame Generation (FSR3 FG) | currentBackBuffer (presentation color buffer);currentBackBuffer_HUDLess (optional);depth (required for FSR3 interpolation workflow);motion vectors (required);R16G16_SINT optical flow vector;R32_UINT optical flow SCD | interpolated output resource;outputSwapChainBuffer;格式未在公开文档列枚举;代码示例用 swap chain back buffer format | ExplicitHDR | BoundedHDR | OfficialDocs | 3 | 官方文档要求传入 backBufferFormat;HUD-less/UI 资源与 backbuffer 关系密切;帧生成需要超分先行;输入资源格式由配置描述,未列完整 DXGI 清单。 | +| FSR4 | AMD FidelityFX Super Resolution 4 (FSR4 ML Super Resolution) | Color buffer: APPLICATION SPECIFIED;Depth: APPLICATION SPECIFIED (1x FLOAT);Motion vectors: APPLICATION SPECIFIED (2x FLOAT);Exposure: R32_FLOAT (1x1) | 未单独枚举;文档称输出与原始输入同域 | ExplicitHDR | DirectFP16 | OfficialDocs | 2 | 颜色输入必须 linear,除非设置 NON_LINEAR_COLORSPACE 标志;没有 DXGI 格式枚举,颜色格式由应用指定;MVs 2x float;深度 float;输出同输入域。 | +| FSRCNNX | FSRCNNX (基于 FSRCNN 的 mpv/着色器变体) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | GitHubExperiment | 5 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=implementation_defined; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| FXAA | FXAA (Fast Approximate Anti-Aliasing) | unspecified(not_enumerated);unspecified(reference_implementation_only) | unspecified(not_enumerated);unspecified(reference_implementation_only) | 无信息 | 无信息 | 无信息 | 4 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=implementation_defined; alphaSemantics=参考实现可透传 alpha;官方/原算法无统一声明。; rangeBoundary=unknown; hdrImplication=无官方 HDR 格式契约;个别引擎实现可用浮点纹理,但不能代表算法协议。 | +| MLAA | MLAA (Morphological Anti-Aliasing) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | 无信息 | 1 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| NIS | NVIDIA Image Scaling SDK (NVScaler/NVSharpen) | Input/output: non-integer data types, examples DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_NV12 (NV12 via NIS_NV12_SUPPORT);HDR modes: LDR [0,1], HDR PQ [0,1], HDR Linear recommended [0,12.5] | Output UAV: non-integer formats, same docs examples (R8G8B8A8_UNORM/NV12); shader writes RWTexture2D | ExplicitHDR | DirectFP16 | OfficialRepository | 4 | 输入为 SRV/read state,输出 UAV/write;sampler 必须 linear clamp;支持 viewport 子区域;NV12 需编译开关 NIS_NV12_SUPPORT;系数纹理 R32G32B32A32_FLOAT 或 R16G16B16A16_FLOAT。 | +| NNEDI3 | NNEDI3 | unspecified(inferred_from_shader_interface);rgba16f / rgba16hf(inferred_from_shader_interface) | unspecified(not_enumerated);rgba16f / rgba16hf(inferred_from_shader_interface) | FormatOnlyNoColorContract | DirectFP16 | OfficialRepository | 2 | inputFormatEnumeration=partial; outputFormatEnumeration=partial; inputOutputRelation=implementation_defined; alphaSemantics=未说明。; rangeBoundary=luma only / unknown; hdrImplication=rgba16f 只证明 mpv user shader 中间表面,不构成 HDR API 契约。 | +| Pixel Art | 无信息(Pixel Art 泛指像素画放大算法族) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | 无信息 | 1 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| RAVU | RAVU (Rapid and Accurate Video Upscaling) | mpv user-shader internal rgba16f/rgba16hf;ravu-yuv assumes YUV video after planes merged;ravu-rgb operates on RGB after planes merged | 内部 rgba16f/rgba16hf 表面 | FormatOnlyNoColorContract | DirectFP16 | OfficialRepository | 2 | 作为 mpv user shader 使用;gather/compute 版本分别用 textureGather/compute;d3d11 老驱动可用 rgba16hf 分支;ravu/ravu-lite 仅放大 luma 平面;ravu-yuv 需 YUV,ravu-rgb 需 RGB;ravu-zoom 任意比例。 | +| RTXVideo | NVIDIA RTX Video Super Resolution (VFX SDK VSR filter) | GPU buffers in BGRA or RGBA interleaved format, 8-bit unsigned per component | Same BGRA or RGBA interleaved U8 GPU buffer | SDROnly | SDRCompatible | OfficialDocs | 3 | 输入输出必须 GPU buffer,BGRA/RGBA interleaved,每分量 8-bit unsigned;Denoise/Deblur modes 输出分辨率必须与输入相同;VSR 支持不同 modes;建议最小 360p。 | +| RTXVideo | NVIDIA RTX Video Denoiser (VFX SDK VSR Denoise modes) | BGRA or RGBA interleaved U8 GPU buffers | BGRA or RGBA interleaved U8 GPU buffers, same resolution as input | SDROnly | SDRCompatible | OfficialDocs | 2 | Denoise modes 不支持 upscaling,输出分辨率必须等于输入;输入输出同为 BGRA/RGBA U8。 | +| RTXVideo | NVIDIA RTX Video HDR | unspecified(not_enumerated) | unspecified(not_enumerated) | ExplicitHDR | 无信息 | OfficialForum | 5 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明。; rangeBoundary=SDR input -> HDR10 output 的产品语义;无量值范围。; hdrImplication=产品方向明确为 SDR->HDR,但无公开 HDR/FP16 格式契约。 | +| Sharpen | 无信息(Sharpen 为通用锐化效果族) | unspecified(not_enumerated) | unspecified(not_enumerated) | 无信息 | 无信息 | 无信息 | 1 | inputFormatEnumeration=not_enumerated; outputFormatEnumeration=not_enumerated; inputOutputRelation=unknown; alphaSemantics=未说明。; rangeBoundary=unknown; hdrImplication=无公开 HDR 格式契约。 | +| SMAA | SMAA (Subpixel Morphological Antialiasing) | colorTex: RGBA texture (2D color/luma input);edgesTex/areaTex/searchTex 等内部/预计算纹理;depthTex for depth edge detection | colorTex/blended output 4-channel color buffer | Unspecified | SDRCompatible | OfficialRepository | 4 | 官方集成说明要求两个 RGBA 时域 render target;创建后清除 alpha;所有 sampler linear + clamp;纹理读写默认非 sRGB,只有最终 NeighborhoodBlending 的 input/output 可 sRGB;64-bit 输入在 GCN 上有半速率线性过滤。 | +| xBRZ | xBRZ (pixel-art scaling algorithm) | RGBA pixel data in Uint8ClampedArray (TypeScript port) | RGBA pixel data in Uint8ClampedArray | SDROnly | SDRCompatible | GitHubExperiment | 3 | TypeScript 移植接口为源/目标 RGBA 像素缓冲;缩放因子 2-6;支持 alpha 透明;非原生 SDK 协议。 | +| XeSS | Intel XeSS Super Resolution (XeSS-SR) | R16G16B16A16_FLOAT;R11G11B10_FLOAT;R8G8B8A8_UNORM;其他 any linear color format; only UNORM integer color formats allowed;Motion vectors: R16G16_FLOAT;Depth: any depth format such as D32_FLOAT or D24_UNORM | Same format and color space as input (2D output texture) | ExplicitHDR | DirectFP16 | OfficialRepository | 5 | 输入颜色可为 LDR/HDR 任意 linear 格式;整数格式只允许 UNORM;输出必须与输入同格式同色彩空间,且输出 alpha 不保留并填 1.0;D3D12 输入 NON_PIXEL_SHADER_RESOURCE,输出 UAV;Vulkan 对应状态。 | +| XeSSFG | Intel XeSS Frame Generation (XeSS-FG) | Back buffer/HUD-less/UI-only: R10G10B10A2_UNORM for HDR10/BT.2100 HDR display;Motion vectors: R16G16_FLOAT or similar;Depth: any depth format such as D32_FLOAT or D24_UNORM;UI Alpha: single channel; UI Color and Alpha: s… | Interpolated frames output to proxy swap chain; same back buffer pixel format (HDR10 R10G10B10A2_UNORM documented) | ExplicitHDR | BoundedHDR | OfficialRepository | 4 | HDR display 支持 R10G10B10A2_UNORM + HDR10/BT.2100;明确不支持 FP16 HDR/scRGB;back buffer, HUD-less, UI-only 必须同像素格式、色彩空间、尺寸;MV 与 depth buffer 尺寸一致。 | +| NVIDIA Optical Flow | NVIDIA Optical Flow SDK (NvOF) | NV_OF_BUFFER_FORMAT_GRAYSCALE8;NV_OF_BUFFER_FORMAT_NV12;NV_OF_BUFFER_FORMAT_ABGR8 (A8B8G8R8) | NV_OF_FLOW_VECTOR (SHORT2, S10.5 flowx/flowy);NV_OF_STEREO_DISPARITY (for stereo mode);Cost buffer NV_OF_BUFFER_FORMAT_UINT or UINT8 | SDROnly | SDRCompatible | OfficialHeader | 4 | 输入帧支持 GRAYSCALE8/NV12/ABGR8;输出/提示为 SHORT2(S10.5);外部提示/成本格式另有要求;支持 output grid size 1/2/4;有宽高 min/max caps。 | +| AMD FidelityFX Optical Flow | FidelityFX Optical Flow | color input resource (格式未在文档枚举) | opticalFlowVector: R16G16_SINT;opticalFlowSCD: R32_UINT (3x1 scene change detection) | ExplicitHDR | BoundedHDR | OfficialDocs | 3 | 以 8x8 block 计算,输出纹理尺寸由 (displaySize+block-1)/8 决定;block size 固定 8;color 输入经 transfer function/luminance 转换。 | + +## 分条目证据 + +### 1. Anime4K — Anime4K + +- **mappingConfidence**:明确映射 +- **mappingNotes**:公开仓库名称与效果组名一致,且为 Anime4K 官方/上游仓库。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://github.com/bloc97/Anime4K/blob/master/README.md + **notes**:Anime4K 官方仓库未枚举输入纹理格式;作为 mpv/libplacebo 用户着色器分发,实际纹理格式由宿主渲染器提供。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://github.com/bloc97/Anime4K/blob/master/README.md + **notes**:输出为目标/上采样后纹理;官方仓库未枚举像素格式。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:implementation_defined +- **alphaSemantics**:未说明;不得从 mpv 用户着色器可编译推断 alpha 契约。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:尺寸缩放/着色器 hook 点由 mpv/libplacebo 宿主决定;官方未提供 UAV/SRV 或 API 资源状态要求。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:十轮内未找到可核实协议,无法给出候选 profile。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:A/B 模式等 GLSL 说明中的品质选择(未提供协议级格式语义) +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://github.com/bloc97/Anime4K/blob/master/README.md | 标题: Anime4K README | 等级: OfficialRepository | claim: 项目为 Anime4K 官方仓库,未写明 HDR/纹理格式协议 | quoteOrSymbol: The simplicity and speed of Anime4K allows the user to watch upscaled anime in real time | searchRound: 1 + - URL: https://raw.githubusercontent.com/bloc97/Anime4K/v4.0.1/GLSL_Instructions.md | 标题: Anime4K v4.0.1 GLSL Instructions | 等级: OfficialRepository | claim: 只说明 mpv/GLSL 用法与模式,未说明纹理格式、数值范围、HDR | quoteOrSymbol: Mode A... | searchRound: 2 +- **searchRoundsUsed**:7 +- **unresolvedQuestions**:输入/输出纹理格式;数值范围;色彩空间;alpha;HDR/FP16 能力 +- **conclusion**:Anime4K 可明确映射到同名公开仓库,但公开仓库未提供图像 I/O 协议细节,本次记录为无协议信息。 + +### 2. CAS — FidelityFX Contrast Adaptive Sharpening (CAS) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:AMD FidelityFX SDK 的 CAS 技术,官方名称一致。 +- **documentedInputFormats**: + - **format**:DXGI_FORMAT_R8G8B8A8_UNORM + **apiOrContext**:DXGI / Direct3D 11 reference CLI + **channelOrder**:RGBA + **numericRepresentation**:UNORM + **acceptanceStatus**:reference_implementation_only + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md + **notes**:官方 CasCmdLine 默认输入格式;sRGB 解释由开关决定。 + - **format**:DXGI_FORMAT_R16G16B16A16_FLOAT + **apiOrContext**:DXGI / Direct3D 11 reference CLI + **channelOrder**:RGBA + **numericRepresentation**:FLOAT (16-bit) + **acceptanceStatus**:reference_implementation_only + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md + **notes**:官方 CasCmdLine --16bit 使用的格式;shader 另有 FP16/FP32 数学路径。 + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/docs/techniques/contrast-adaptive-sharpening.md + **notes**:SDK 技术文档未对 FfxResource 输入格式做 DXGI/Vulkan 枚举。 +- **documentedOutputFormats**: + - **format**:DXGI_FORMAT_R8G8B8A8_UNORM + **apiOrContext**:DXGI / Direct3D 11 reference CLI + **channelOrder**:RGBA + **numericRepresentation**:UNORM + **acceptanceStatus**:reference_implementation_only + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md + **notes**:默认输出格式,与输入同格式;官方样例说明输出可为相同或更大尺寸。 + - **format**:DXGI_FORMAT_R16G16B16A16_FLOAT + **apiOrContext**:DXGI / Direct3D 11 reference CLI + **channelOrder**:RGBA + **numericRepresentation**:FLOAT (16-bit) + **acceptanceStatus**:reference_implementation_only + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md + **notes**:--16bit 输出格式;官方文档保证输出与输入同色彩空间。 + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/docs/techniques/contrast-adaptive-sharpening.md + **notes**:SDK 的 FfxResource output 未在技术文档中列出具体格式枚举。 +- **formatBoundary**: +- **inputFormatEnumeration**:partial +- **outputFormatEnumeration**:partial +- **inputOutputRelation**:same_format_required +- **alphaSemantics**:未说明;SDK 回调只读写 RGB,未见 alpha 透传/置 1 声明。 +- **rangeBoundary**:sRGB/linear 解释均支持;shader 转换注释按 [0,1] 线性空间处理。 +- **transferBoundary**:linear input expected; output returned to same input color space via FfxCasColorSpaceConversion +- **resourceConstraints**:官方样例支持 sharpen-only/sharpen+upscale;输入 SRV/read、输出 UAV/write 在 SDK 样例路径;格式枚举只来自 CasCmdLine 参考实现。 +- **hdrImplication**:官方参考实现支持 R16G16B16A16_FLOAT 与 linear 开关,但这属于参考实现路径;SDK 技术文档未提供 HDR 元数据契约。 +- **channelOrderAndAlpha**:回调只处理 RGB;文档未说明 alpha 是否透传或必须为 1。 +- **precisionAndRange**:回调存在 float32 与 float16 路径;色彩转换注释按 [0,1] 线性空间处理,文档未给出 8/10/16 位格式清单。 +- **transferFunction**:期望线性输入;通过 FfxCasColorSpaceConversion 支持 LINEAR、GAMMA20、GAMMA22、sRGB_OUTPUT、sRGB_INPUT_OUTPUT。 +- **primariesAndColorSpace**:未说明原色域。 +- **hdrSupport**:Unspecified +- **hdrBoundary**:文档未说明 HDR 直接输入;只要求线性输入并保持输入输出同色彩空间。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:官方 shader 注释将输入转换描述为 0..1 线性空间,且没有任何 HDR/FP16 直接路径声明;保守记为 SDR 兼容。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:sharpening disabled;sharpening enabled, upsampling disabled;both sharpening and upsampling enabled +- **documentedPerformanceNotes**:文档未比较 FP16/UNORM/HDR 路径成本。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/docs/techniques/contrast-adaptive-sharpening.md | 标题: FidelityFX Contrast Adaptive Sharpening 1.1 | 等级: OfficialDocs | claim: CAS 需要线性输入;可按输入空间转换并在输出还原 | quoteOrSymbol: CAS needs linear input color to perform correctly | searchRound: 1 + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/sdk/include/FidelityFX/host/ffx_cas.h | 标题: ffx_cas.h | 等级: OfficialHeader | claim: FfxCasDispatchDescription 仅含 FfxResource color/output、renderSize、sharpness | quoteOrSymbol: FfxResource color; FfxResource output | searchRound: 2 + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.1.0/sdk/include/FidelityFX/gpu/cas/ffx_cas_callbacks_hlsl.h | 标题: ffx_cas_callbacks_hlsl.h | 等级: OfficialHeader | claim: float32/float16 采样与颜色空间转换宏 | quoteOrSymbol: casInputHalf... between 0 and 1 | searchRound: 3 + - URL: https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-CAS/78c03cf5cbf086ddcc06f635cc4def85a68b22df/CasCmdLine/README.md | 标题: FidelityFX-CAS CasCmdLine README | 等级: OfficialRepository | claim: 官方命令行参考实现默认 R8G8B8A8_UNORM,--16bit 用 R16G16B16A16_FLOAT | quoteOrSymbol: uses R8G8B8A8_UNORM ... uses R16G16B16A16_FLOAT | searchRound: 5 +- **searchRoundsUsed**:7 +- **unresolvedQuestions**:具体支持格式列表;alpha 契约;HDR 数值上限;primaries +- **conclusion**:CAS 的官方协议只明确了线性输入、输入输出同空间、float/fp16 shader 回调与可选色彩空间转换;具体 DXGI 格式与 HDR 能力未被官方文档枚举。 + +### 3. CRT — 无信息(CRT 效果族未能在公开资料中唯一映射) + +- **mappingConfidence**:无信息 +- **mappingNotes**:仅按通用名称“CRT”搜索;公开资料无法唯一对应某个上游算法/SDK,且不能借助 Magpie 源码确认。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:无公开唯一上游来源 + **notes**:无法唯一映射上游;不得把通用 CRT 着色器格式写成该效果组已支持格式。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:无公开唯一上游来源 + **notes**:无法唯一映射上游;输出格式未公开。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明;名称不能唯一映射,无法建立 alpha 契约。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:未确认任何资源状态、缩放或采样限制。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:无信息 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - 无来源(无信息条目;已尝试来源类型与轮数列于条目尾部) +- **searchRoundsUsed**:1 +- **unresolvedQuestions**:上游映射;输入格式;色彩空间;HDR/FP16 +- **conclusion**:“CRT”效果组无法在公开资料中唯一映射到上游算法或 SDK,十轮内无法取得可靠协议结论,记录为无信息。 + +### 4. CuNNy — CuNNy + +- **mappingConfidence**:明确映射 +- **mappingNotes**:上游仓库 Blinue/CuNNy 名称与效果组一致,README 自述“supports exporting to an mpv shader”和“Magpie effect”。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/Blinue/CuNNy/master/README.md + **notes**:上游仓库只描述 mpv shader/Magpie effect,未给出纹理格式。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/Blinue/CuNNy/master/README.md + **notes**:输出为上采样图像,格式未枚举。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:implementation_defined +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:mpv shader 导出路径意味着由 mpv/libplacebo 管理 hook 纹理;无 API 资源约束。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:8x32/4x3 等模型尺寸(来自 README,无格式协议语义) +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/Blinue/CuNNy/master/README.md | 标题: CuNNy README | 等级: OfficialRepository | claim: 上游仓库描述 CuNNy 可导出 mpv shader/Magpie effect,未给出 I/O 格式 | quoteOrSymbol: Supports exporting to an mpv meme shader! | searchRound: 1 +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:输入输出纹理格式;HDR/FP16;alpha;数值范围 +- **conclusion**:CuNNy 上游可明确映射,但公开仓库没有图像协议细节,无法确定格式边界。 + +### 5. CuNNy2 — 无信息(未能定位独立 CuNNy2 上游仓库/SDK) + +- **mappingConfidence**:无信息 +- **mappingNotes**:公开搜索未找到与“CuNNy2”唯一对应的上游算法/模型/SDK 文档;现有结果多为 Magpie 版本页等,不可用于协议推断。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开搜索未发现独立 CuNNy2 上游 + **notes**:无法唯一映射到独立上游;不存在可归属的格式协议。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开搜索未发现独立 CuNNy2 上游 + **notes**:同上。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:无上游资源约束可记录。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:无信息 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - 无来源(无信息条目;已尝试来源类型与轮数列于条目尾部) +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:CuNNy2 上游映射;格式;HDR/FP16;alpha +- **conclusion**:CuNNy2 未能在公开资料中唯一映射到上游算法或 SDK,记录为无信息。 + +### 6. Diagnostics — 无信息(Diagnostics 是通用诊断效果名) + +- **mappingConfidence**:无信息 +- **mappingNotes**:公开资料中不存在可作为上游协议的“Diagnostics”图像算法/SDK;不依据 Magpie 实现推断。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开资料无对应上游算法 + **notes**:Diagnostics 为通用诊断效果名,不存在公开算法/SDK 格式枚举。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开资料无对应上游算法 + **notes**:同上。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:无资源约束可记录。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:无信息 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - 无来源(无信息条目;已尝试来源类型与轮数列于条目尾部) +- **searchRoundsUsed**:1 +- **unresolvedQuestions**:上游映射;I/O 格式;HDR/FP16 +- **conclusion**:Diagnostics 无公开上游协议,记录为无信息。 + +### 7. DLSS — NVIDIA DLSS Super Resolution (DLSS-SR) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:NVIDIA DLSS SDK / Streamline 中的 DLSS Super Resolution,公共品牌与效果组一致。 +- **documentedInputFormats**: +Color input: any supported buffer format for the API +Motion vectors: RG32_FLOAT or RG16_FLOAT +Depth: any one-channel format (R32_FLOAT/D32_FLOAT) or depth-stencil (D24S8) +Exposure: 1x1 texture, R16F preferred (presets J/K) +- **documentedOutputFormats**: +Output buffer: any supported buffer format for the API +Previous output/history buffer: optional, RGBA16F +- **formatBoundary**: +官方编程指南要求输入为 SRV/read 状态,输出为 UAV/write 状态;颜色输入任意 API 支持格式,输出任意 API 支持格式;历史输出建议 RGBA16F;最小输出 32x32;无规定最大分辨率但 8K 以上未保证。 +- **channelOrderAndAlpha**:Streamline 默认只放大 RGB;alphaUpscalingEnabled 为实验性时可放大 alpha;官方 DLSS 指南未规定固定通道顺序。 +- **precisionAndRange**:LDR 模式颜色值必须 [0,1] 且为感知编码(如 sRGB),不能为线性;HDR 模式可处理线性高范围、无亮度上限,内部高精度。 +- **transferFunction**:LDR: sRGB/perceptual encoding; HDR: linear space (IsHDR flag). +- **primariesAndColorSpace**:未说明 primaries;HDR 为线性场景/显示域未细分。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:HDR 输入需将 IsHDR 置 1;需要 exposure(1x1 texture,R16F 首选)供当前帧使用;无 tone mapping 要求;输出仍同输入线性域。 +- **requiredHdrMetadata**:exposure value (1x1 texture) +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:官方明确 HDR 线性路径、无亮度上限且历史/输出使用/推荐 RGBA16F;有直接的 HDR/FP16 证据。 +- **auxiliaryInputs**:depth;motion vectors;exposure;jitter;previous output/history (optional) +- **auxiliaryInputFormatsAndSemantics**:Depth any one-channel/depth-stencil; MVs RG32_FLOAT or RG16_FLOAT in pixel screen-space, can be low-res or high-res/dilated; exposure 1x1, R16F preferred; jitter in [-0.5,0.5] pixels at render res. +- **temporalOrFrameGenerationConstraints**:时域历史由 DLSS 内部维护,可选 previous output RGBA16F;reset/jump cut 语义见编程指南;动态分辨率需保持长宽比;mip bias 需要负偏移。 +- **documentedQualityOptions**:DLSS quality/balanced/performance/ultra-performance presets (公开质量档,格式不随档位变化) +- **documentedPerformanceNotes**:官方说明 LDR 模式性能更优且内部量化到 8-bit;HDR 模式内部高精度。 +- **qualityPerformanceEvidence**:官方指南有 LDR 性能更优说明,但未给出格式路径基准表。 +- **sources**: + - URL: https://github.com/NVIDIA/DLSS/blob/main/doc/DLSS_Programming_Guide_Release.pdf | 标题: NVIDIA DLSS Super Resolution Programming Guide Release | 等级: OfficialDocs | claim: Supported Formats: color any API format; MV RG32_FLOAT/RG16_FLOAT; depth any one-channel/depth-stencil; output any API format; history RGBA16F | quoteOrSymbol: 3.3 Supported Formats | searchRound: 1 + - URL: https://raw.githubusercontent.com/NVIDIA/DLSS/main/doc/DLSS_Programming_Guide_Release.pdf | 标题: DLSS Programming Guide PDF (raw) | 等级: OfficialDocs | claim: LDR [0,1] perceptual; HDR linear unbounded | quoteOrSymbol: DLSS can process color data stored as either LDR or HDR | searchRound: 2 + - URL: https://raw.githubusercontent.com/NVIDIA-RTX/Streamline/main/docs/ProgrammingGuideDLSS.md | 标题: Streamline DLSS Super Resolution Integration | 等级: OfficialRepository | claim: DLSS-SR requires colorIn/colorOut/depth/mvec/exposure; colorBuffersHDR and alphaUpscalingEnabled options | quoteOrSymbol: dlssOptions.colorBuffersHDR = sl::Boolean::eTrue | searchRound: 3 +- **searchRoundsUsed**:6 +- **unresolvedQuestions**:具体 DXGI 支持清单(官方用“any API format”而非枚举);primaries;输出 alpha 默认值 +- **conclusion**:DLSS-SR 官方协议清晰:颜色输入输出可为 API 任意支持格式,HDR 线性模式通过 IsHDR 开启,运动向量固定 RG32/RG16_FLOAT,深度为单通道/深度模板,历史推荐 RGBA16F。 + +### 8. DLSSFG — NVIDIA DLSS Frame Generation (DLSS-G / DLSS 4 Multi-Frame Gen) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:Streamline/NVIDIA DLSS Frame Generation 官方文档与头文件。 +- **documentedInputFormats**: +Backbuffer/Final Color resource (format not enumerated in public text) +Motion vectors/depth: same DLSS-SR requirements +- **documentedOutputFormats**: +OutputInterpolated/OutputReal: same texture format as Backbuffer +- **formatBoundary**: +官方头文件写明 Required Output Texture (same texture format as Backbuffer);HUDless 与 UI 纹理需与 backbuffer 同尺寸/格式/色彩空间;backbuffer 资源经 Streamline swapchain 拦截。 +- **channelOrderAndAlpha**:UI Alpha 为单通道 0..1;UI Color and Alpha 需预乘 alpha;HUDless 为无 UI 全场景颜色;alpha 参与 UI 合成。 +- **precisionAndRange**:未公开列举 8/16 位格式;只有 colorBuffersHDR 布尔标志表示 HDR。 +- **transferFunction**:未在公开文本中细分;HUDless 与 backbuffer 必须同色彩空间(含 tone mapping)。 +- **primariesAndColorSpace**:未说明 primaries。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:头文件有 full HDR 标志;未提供 PQ/HLG/scRGB 解码或 metadata 要求;应把与 backbuffer 相同色彩空间的帧交给帧生成。 +- **requiredHdrMetadata**:colorBuffersHDR flag;无公开 MaxCLL/MaxFALL/亮度要求 +- **candidateInternalProfile**:FormatOnlyNoColorContract +- **profileRationale**:官方确认 HDR 布尔和“输出与 backbuffer 同格式”,但未公开 HDR 的具体格式/色彩空间契约;不能判定 DirectFP16。 +- **auxiliaryInputs**:depth;motion vectors;HUD-less color;UI alpha or UI color+alpha;bidirectional distortion field (optional);backbuffer subrect +- **auxiliaryInputFormatsAndSemantics**:Depth/mvec same as DLSS-SR; HUDless/UI must match backbuffer; UI alpha 0..1; UI RGB premultiplied; distortion field maps distorted final color to undistorted guide buffers. +- **temporalOrFrameGenerationConstraints**:需要 dense motion vectors, depth, HUDless;插值输出与真实帧输出为同格式;帧生成要求 present 时资源仍有效;子矩形需 extent 匹配;UI 合成公式 Final.RGB = UI.RGB + (1-UI.Alpha)*HUDless.RGB。 +- **documentedQualityOptions**:插值帧数/模式等公开选项,未涉及格式 +- **documentedPerformanceNotes**:公开指南建议优先提供 UI Alpha 单通道以获得更好性能;无格式基准。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/NVIDIA-RTX/Streamline/main/docs/ProgrammingGuideDLSS_G.md | 标题: Streamline DLSS-G Programming Guide | 等级: OfficialRepository | claim: 列出深度、运动向量、HUDless、UI 输入与资源生命周期约束 | quoteOrSymbol: Hudless ... same color space and post-processing effects as color backbuffer | searchRound: 1 + - URL: https://raw.githubusercontent.com/NVIDIA/DLSS/main/include/nvsdk_ngx_defs_dlssg.h | 标题: nvsdk_ngx_defs_dlssg.h | 等级: OfficialHeader | claim: Required Output Texture (same texture format as Backbuffer) | quoteOrSymbol: #define NVSDK_NGX_DLSSG_Parameter_BackbufferFormat "DLSSG.BackbufferFormat" | searchRound: 2 +- **searchRoundsUsed**:5 +- **unresolvedQuestions**:实际接受/输出的 DXGI 格式枚举;HDR PQ/HLG/scRGB 颜色契约;metadata +- **conclusion**:DLSSFG 官方公开材料确认输入由 swapchain/backbuffer 决定、输出必须与 backbuffer 同格式,并有 HDR 布尔开关;没有公开更细的格式枚举或 PQ/scRGB 协议。 + +### 9. FSR — AMD FidelityFX Super Resolution 1 (FSR1 spatial upscaler) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:Magpie 效果组“FSR”按公开产品命名对应 FSR1 的空间超分;FSR2/3/4 已单列。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:algorithmic reference implementation (shader callback) + **channelOrder**:RGB (3 channel) + **numericRepresentation**:FLOAT/half + **acceptanceStatus**:inferred_from_shader_interface + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/ffx-fsr/ffx_fsr1.h + **notes**:官方 ffx_fsr1.h 提供 RGB 输入回调;未给出具体 DXGI 格式。 + - **format**:unspecified + **apiOrContext**:DXGI/Vulkan/API resource + **channelOrder**:RGB + **numericRepresentation**:UNORM/FLOAT + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.0.0/docs/techniques/super-resolution-spatial.md + **notes**:官方 FSR1 文档只要求 [0,1] perceptual sRGB,不枚举资源格式。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:algorithmic reference implementation (shader callback) + **channelOrder**:RGB (3 channel) + **numericRepresentation**:FLOAT/half + **acceptanceStatus**:inferred_from_shader_interface + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/ffx-fsr/ffx_fsr1.h + **notes**:EASU/RCAS 输出 RGB;最终输出表面格式由宿主分配。 + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.0.0/docs/techniques/super-resolution-spatial.md + **notes**:输出格式未在 FSR1 文档中单独列枚举。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:implementation_defined +- **alphaSemantics**:未说明。 +- **rangeBoundary**:[0,1] perceptual sRGB;RCAS 负输入会产生 NaN。 +- **transferBoundary**:sRGB / perceptual(非 linear);helper 另含 linear HDR {0 to FP16_MAX} 转换工具,但非核心输入契约。 +- **resourceConstraints**:shader 回调 gather4;输入资源可为动态分辨率 viewport;建议 32bpp 格式;无 UAV/SRV 官方枚举。 +- **hdrImplication**:FSR1 核心文档没有 HDR 直接输入路径;线性 HDR helper 属可选周边工具,不能算核心格式契约。 +- **channelOrderAndAlpha**:未说明 alpha。 +- **precisionAndRange**:颜色归一化 [0,1];感知编码 sRGB。 +- **transferFunction**:sRGB / perceptual(不是线性)。 +- **primariesAndColorSpace**:未说明原色域(通常 sRGB,但资料未写 primaries)。 +- **hdrSupport**:SDROnly +- **hdrBoundary**:官方文档面向 [0,1] sRGB 输入,未提供 HDR 线性/PQ/HLG 路径。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:官方明确要求 [0-1] perceptual sRGB、不能线性,未见 HDR 路径,故为 SDR 兼容。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:EASU+RCAS 组合/quality mode 由调用方决定(官方未给出档位枚举) +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/v1.0.0/docs/techniques/super-resolution-spatial.md | 标题: FidelityFX Super Resolution 1 (FSR1) | 等级: OfficialDocs | claim: Input normalized [0-1], perceptual sRGB not linear; negative RCAS input NaN | quoteOrSymbol: Image should be normalized to [0-1] and be in perceptual color space (sRGB, not linear) | searchRound: 1 + - URL: https://raw.githubusercontent.com/GPUOpen-Effects/FidelityFX-FSR/master/ffx-fsr/ffx_fsr1.h | 标题: FidelityFX-FSR ffx_fsr1.h | 等级: OfficialHeader | claim: FSR1 通过 RGB 输入回调读取颜色;建议 32bpp 性能格式;RCAS 输入范围 {0 to 1} | quoteOrSymbol: Color needs to be encoded as 3 channel [red, green, blue] | searchRound: 4 +- **searchRoundsUsed**:6 +- **unresolvedQuestions**:DXGI 格式清单;alpha;输出格式要求 +- **conclusion**:FSR1 官方资料可确认是 [0,1] sRGB 感知色彩输入的空间超分算法,没有 HDR/FP16 协议证据,候选为 SDRCompatible。 + +### 10. FSR2 — AMD FidelityFX Super Resolution 2 (FSR2 temporal upscaler) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:官方文档标题为 AMD FidelityFX Super Resolution 2.3.x。 +- **documentedInputFormats**: +Color buffer: APPLICATION SPECIFIED (render res) +Depth: APPLICATION SPECIFIED (1x FLOAT) +Motion vectors: APPLICATION SPECIFIED (2x FLOAT) +Reactive mask: R8_UNORM +Transparency & composition mask: R8_UNORM +Exposure: R32_FLOAT (1x1) +- **documentedOutputFormats**: +Adjusted color buffer (internal): R16G16B16A16_FLOAT +Upscaled buffer (internal): R16G16B16A16_FLOAT +API 输出缓冲的公开格式在文档中未单独枚举,一般随应用分配资源 +- **formatBoundary**: +颜色输入由应用指定格式;HDR 需设置 HDR flag;深度单浮点;MVs 2x float 屏幕空间范围 [<-w,-h>,];内部多使用 16-bit;输出最终与输入同域(内部 tonemap 会被反转);要求 GPU 支持 typed UAV load 和 R16G16B16A16_UNORM。 +- **channelOrderAndAlpha**:内部 Adjusted color 使用 YCoCg 且 alpha 为 disocclusion mask;对外 alpha 契约未说明。 +- **precisionAndRange**:颜色可为应用指定的 UNORM/FLOAT 等;HDR 线性域;MVs 内部 16-bit;exposure R32_FLOAT。 +- **transferFunction**:HDR 模式要求 linear;SDK 后续也允许 non-linear 标志(见 FSR4/upscaler 文档)。 +- **primariesAndColorSpace**:未说明 primaries。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:设置 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE 后输入应为 linear;输出恢复原输入域;需要 pre-exposure/exposure 语义。 +- **requiredHdrMetadata**:pre-exposure;exposure (R32_FLOAT) 或 auto-exposure +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:官方内部表面明确 R16G16B16A16_FLOAT、HDR 线性支持,且有 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE 直接路径。 +- **auxiliaryInputs**:depth;motion vectors;reactive mask;transparency & composition mask;exposure;camera jitter;reset +- **auxiliaryInputFormatsAndSemantics**:MVs in pixels screen-space, low-res/display-res flag; reactive/T&C mask R8_UNORM [0,1]; exposure R32_FLOAT optional; depth flags inverted/infinite. +- **temporalOrFrameGenerationConstraints**:时域算法需要当前/上一帧数据与历史;所有 render-res 输入需 jitter(MV 例外);camera jump cut 需 reset;FrameTimeDelta 输入。 +- **documentedQualityOptions**:quality/balanced/performance 等调用方档位(API 主要按 input/output 尺寸) +- **documentedPerformanceNotes**:内部 MVs 使用 16-bit,高精度输入不会额外受益;官方要求 R16G16B16A16_UNORM 支持;无公开格式路径性能基准。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://github.com/GPUOpen-Effects/FidelityFX-FSR2/blob/master/README.md | 标题: FidelityFX-FSR2 README | 等级: OfficialRepository | claim: Input resource table and internal R16G16B16A16_FLOAT buffers | quoteOrSymbol: Upscaled buffer ... R16G16B16A16_FLOAT | searchRound: 1 + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/main/Kits/FidelityFX/docs/techniques/super-resolution-temporal.md | 标题: FidelityFX Super Resolution 2.3.4 | 等级: OfficialDocs | claim: Input resources table, HDR support, linear color | quoteOrSymbol: High dynamic range images are supported... linear color space | searchRound: 2 +- **searchRoundsUsed**:4 +- **unresolvedQuestions**:对外输出 FfxResource 的格式枚举;alpha 对外行为;primaries +- **conclusion**:FSR2 官方协议:颜色输入为应用指定格式,HDR 需 linear 输入与 HDR flag;辅助资源格式明确;内部浮点表面 R16G16B16A16_FLOAT。 + +### 11. FSR3 — AMD FidelityFX Super Resolution 3.1 Upscaler (FSR3 SR) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:FSR3 效果组拆为“FSR3 Upscaler”与“FSR3 Frame Interpolation”两个后端;此处为官方 FSR3 3.1.x Upscaler。 +- **documentedInputFormats**: +Color buffer: APPLICATION SPECIFIED +Depth: APPLICATION SPECIFIED (1x FLOAT) +Motion vectors: APPLICATION SPECIFIED (2x FLOAT) +Reactive mask: R8_UNORM +T&C mask: R8_UNORM +Exposure: R32_FLOAT +- **documentedOutputFormats**: +未单独枚举;文档称输出与原始输入同域(internal tonemap 被反转) +- **formatBoundary**: +与 FSR2 输入结构一致;HDR flag 开启时输入 linear;要求 R16G16B16A16_UNORM typed UAV 支持;API 未在文档中列 DXGI 输出枚举。 +- **channelOrderAndAlpha**:对外 alpha 未说明。 +- **precisionAndRange**:颜色应用指定;HDR linear;MVs 2x float;内部 16-bit。 +- **transferFunction**:HDR linear;文档未给出额外非线性颜色自动转换。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:设置 FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE,线性输入,输出恢复原输入域;需要 exposure/pre-exposure。 +- **requiredHdrMetadata**:pre-exposure;exposure;auto-exposure flag 可选 +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:官方 HDR 线性直接路径 + 内部浮点表面证据与 FSR2 同类。 +- **auxiliaryInputs**:depth;motion vectors;reactive mask;T&C mask;exposure;jitter;reset;frame time delta +- **auxiliaryInputFormatsAndSemantics**:同 FSR2:depth float、MV 2x float、R8_UNORM masks、R32_FLOAT exposure。 +- **temporalOrFrameGenerationConstraints**:时域超分要求 jitter、reset、历史;FrameTimeDelta 输入。 +- **documentedQualityOptions**:FSR 质量/性能档由应用选择(未在格式文档中) +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/main/Kits/FidelityFX/docs/techniques/super-resolution-upscaler.md | 标题: AMD FidelityFX Super Resolution 3.1.5 Upscaler | 等级: OfficialDocs | claim: FSR3 Upscaler input resources and HDR linear support | quoteOrSymbol: FSR Super Resolution ... HDR support | searchRound: 1 +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:输出格式枚举;alpha;primaries +- **conclusion**:FSR3 Upscaler 与 FSR2 的输入/辅助资源/HDR linear 协议高度一致;公开文档未列外部输出格式枚举。 + +### 12. FSR3 — AMD FidelityFX Super Resolution 3 Frame Interpolation / Frame Generation (FSR3 FG) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:FSR3 的帧生成/插值后端,官方技术名 FidelityFX Frame Interpolation;拆为独立子条目。 +- **documentedInputFormats**: +currentBackBuffer (presentation color buffer) +currentBackBuffer_HUDLess (optional) +depth (required for FSR3 interpolation workflow) +motion vectors (required) +R16G16_SINT optical flow vector +R32_UINT optical flow SCD +- **documentedOutputFormats**: +interpolated output resource +outputSwapChainBuffer +格式未在公开文档列枚举;代码示例用 swap chain back buffer format +- **formatBoundary**: +官方文档要求传入 backBufferFormat;HUD-less/UI 资源与 backbuffer 关系密切;帧生成需要超分先行;输入资源格式由配置描述,未列完整 DXGI 清单。 +- **channelOrderAndAlpha**:HUDLess/UI 合成涉及 alpha/UI 内容;具体 alpha 格式未完整说明。 +- **precisionAndRange**:未公开逐格式数值范围;有 FFX_FRAMEINTERPOLATION_ENABLE_HDR_COLOR_INPUT 与 minMaxLuminance。 +- **transferFunction**:config 含 backBufferTransferFunction 用于把插值源转换到 linear RGB;HDR 转换依赖 minMaxLuminance。 +- **primariesAndColorSpace**:未说明 primaries;backBufferTransferFunction 枚举存在。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:需设置 HDR_COLOR_INPUT;通过 backBufferTransferFunction 与 minMaxLuminance 把 HDR 颜色转到 linear RGB;未公开 MaxCLL/MaxFALL 字段。 +- **requiredHdrMetadata**:backBufferTransferFunction;minMaxLuminance[2] +- **candidateInternalProfile**:BoundedHDR +- **profileRationale**:官方明确 HDR 颜色输入需经 transfer function + min/max luminance 转换为线性 RGB,即有界/转换契约,而非直接 FP16 无 metadata 路径。 +- **auxiliaryInputs**:depth;game motion vectors;FidelityFX Optical Flow vectors;HUDLess color;UI/HUD handling (presentCallback or HUDLessColor) +- **auxiliaryInputFormatsAndSemantics**:depth inverted/infinite flags; opticalFlowVector R16G16_SINT; opticalFlowSCD R32_UINT; HUDLess optional FfxResource. +- **temporalOrFrameGenerationConstraints**:帧生成需要 previous/current frame、光流、深度、运动向量;presentCallback/HUDLess 用于 UI;重置、延迟、显示分辨率限制在 FSR3 集成指南。 +- **documentedQualityOptions**:FSR3 frame generation on/off, UI composition modes 等(无格式档位) +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/release-FSR3-3.0.3/docs/techniques/frame-interpolation.md | 标题: FidelityFX Frame Interpolation | 等级: OfficialDocs | claim: Config includes HDR flag, backBufferTransferFunction, minMaxLuminance; optical flow outputs | quoteOrSymbol: FFX_FRAMEINTERPOLATION_ENABLE_HDR_COLOR_INPUT | searchRound: 1 + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/release-FSR3-3.0.3/docs/techniques/optical-flow.md | 标题: FidelityFX Optical Flow | 等级: OfficialDocs | claim: Optical flow vector R16G16_SINT, SCD R32_UINT | quoteOrSymbol: R16G16_SINT ... R32_UINT | searchRound: 2 +- **searchRoundsUsed**:3 +- **unresolvedQuestions**:具体 backbuffer 格式支持清单;HDR PQ/scRGB 细节;alpha 行为 +- **conclusion**:FSR3 Frame Generation 官方协议确认需要 backbuffer 格式、HDR 输入标志和 transfer/luminance 参数;外部格式仍由 swapchain/backbuffer 决定,未公开完整格式枚举。 + +### 13. FSR4 — AMD FidelityFX Super Resolution 4 (FSR4 ML Super Resolution) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:官方 FidelityFX SDK 文档标题“AMD FSR Super Resolution 4”。 +- **documentedInputFormats**: +Color buffer: APPLICATION SPECIFIED +Depth: APPLICATION SPECIFIED (1x FLOAT) +Motion vectors: APPLICATION SPECIFIED (2x FLOAT) +Exposure: R32_FLOAT (1x1) +- **documentedOutputFormats**: +未单独枚举;文档称输出与原始输入同域 +- **formatBoundary**: +颜色输入必须 linear,除非设置 NON_LINEAR_COLORSPACE 标志;没有 DXGI 格式枚举,颜色格式由应用指定;MVs 2x float;深度 float;输出同输入域。 +- **channelOrderAndAlpha**:未说明。 +- **precisionAndRange**:linear HDR/scene-linear 输入,或 non-linear 标志;内部 ML 处理;精度未公开。 +- **transferFunction**:linear(推荐);非线性的可选标志。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:HDR 线性输入为推荐;输出恢复同输入域;需要 exposure/pre-exposure 管理;无 PQ/HLG 自动解码说明。 +- **requiredHdrMetadata**:pre-exposure;exposure R32_FLOAT;auto-exposure 可选 +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:官方 HDR linear 直接路径、无色彩空间自动处理,说明有直接浮点/HDR 路径。 +- **auxiliaryInputs**:depth;motion vectors;exposure;jitter;reset +- **auxiliaryInputFormatsAndSemantics**:同 FSR2/3 upscaler 协议;MVs 屏幕空间像素范围。 +- **temporalOrFrameGenerationConstraints**:时域 ML 超分需 jitter、reset、历史/帧时间;无公开帧生成。 +- **documentedQualityOptions**:FSR4 质量/性能档(未在格式文档中) +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/main/Kits/FidelityFX/docs/techniques/super-resolution-ml.md | 标题: AMD FSR Super Resolution 4 | 等级: OfficialDocs | claim: FSR4 input resources, HDR linear and non-linear colorspace flags | quoteOrSymbol: High dynamic range images are supported... linear color space | searchRound: 1 +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:输出格式枚举;alpha;primaries;ML 模型输入张量格式 +- **conclusion**:FSR4 官方协议说明颜色输入为应用指定的 linear(或 non-linear 标志)HDR 可支持,辅助资源与 FSR2/3 一致;外部输出格式未公开。 + +### 14. FSRCNNX — FSRCNNX (基于 FSRCNN 的 mpv/着色器变体) + +- **mappingConfidence**:推测映射 +- **mappingNotes**:公开上可找到 FSRCNN-TensorFlow 项目与 mpv FSRCNNX 发布物,但“FSRCNNX”本身没有独立官方 SDK 文档。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/igv/FSRCNN-TensorFlow/master/README.md + **notes**:FSRCNNX 无独立官方 SDK;基础 FSRCNN 仓库只说明 TensorFlow 模型,未给出 mpv shader 的纹理格式。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/igv/FSRCNN-TensorFlow/master/README.md + **notes**:同上;输出格式未枚举。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:implementation_defined +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:社区 mpv 发布物/模型尺寸存在,但无统一资源状态或尺寸约束文档。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:不同 FSRCNNX 模型/尺寸由社区发布(无协议级信息) +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/igv/FSRCNN-TensorFlow/master/README.md | 标题: FSRCNN-TensorFlow | 等级: GitHubExperiment | claim: 底层 FSRCNN TensorFlow 实现;README 提到增加 RGB 支持 | quoteOrSymbol: Add RGB support | searchRound: 1 +- **searchRoundsUsed**:5 +- **unresolvedQuestions**:FSRCNNX 独立仓库/头文件;输入输出格式;HDR/FP16 +- **conclusion**:FSRCNNX 只能推测映射到 FSRCNN 的着色器变体,公开协议资料不足,记录为无信息。 + +### 15. FXAA — FXAA (Fast Approximate Anti-Aliasing) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:算法名称明确对应 Lottes 的 FXAA;公开实现众多,但无单一官方 SDK 协议文档。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:无官方统一来源 + **notes**:FXAA 算法无单一 SDK;多引擎实现把输入绑定为 2D 颜色纹理。 + - **format**:unspecified + **apiOrContext**:WebGPU/engine reference implementation (bevy FXAA) + **channelOrder**:RGBA + **numericRepresentation**:FLOAT (sampled as vec4) + **acceptanceStatus**:reference_implementation_only + **evidenceRef**:https://raw.githubusercontent.com/JMS55/bevy/236aa4e2fc93900b6ada7151b0d8e0567e6650f2/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl + **notes**:社区/引擎实现使用 texture_2d 采样并输出 alpha 透传;不代表 FXAA 通用格式契约。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:无官方统一来源 + **notes**:FXAA 输出通常为同尺寸颜色纹理,但无统一格式枚举。 + - **format**:unspecified + **apiOrContext**:WebGPU/engine reference implementation (bevy FXAA) + **channelOrder**:RGBA + **numericRepresentation**:FLOAT + **acceptanceStatus**:reference_implementation_only + **evidenceRef**:https://raw.githubusercontent.com/JMS55/bevy/236aa4e2fc93900b6ada7151b0d8e0567e6650f2/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl + **notes**:bevy 实现返回 vec4(finalColor, centerSample.a),alpha 透传;仅参考实现。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:implementation_defined +- **alphaSemantics**:参考实现可透传 alpha;官方/原算法无统一声明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:shader 输入输出通常同尺寸;无统一 UAV/SRV 契约。 +- **hdrImplication**:无官方 HDR 格式契约;个别引擎实现可用浮点纹理,但不能代表算法协议。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:没有可引用为统一协议的官方格式说明。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:FXAA quality presets 由各实现定义,未在本调研确认 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/JMS55/bevy/236aa4e2fc93900b6ada7151b0d8e0567e6650f2/crates/bevy_core_pipeline/src/fxaa/fxaa.wgsl | 标题: Bevy FXAA shader (NVIDIA FXAA 3.11 port) | 等级: GitHubExperiment | claim: bevy 实现以 texture_2d 读取颜色,输出 RGBA float 并透传 alpha | quoteOrSymbol: return vec4(finalColor, centerSample.a); | searchRound: 3 +- **searchRoundsUsed**:4 +- **unresolvedQuestions**:固定输入格式;HDR/FP16;色彩空间 +- **conclusion**:FXAA 可明确映射到 Lottes 的公开算法,但缺少可作为“外置效果层协议”的统一官方格式契约,记录为无信息。 + +### 16. MLAA — MLAA (Morphological Anti-Aliasing) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:MLAA 为公开论文/算法名称;无单一 SDK 官方协议。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:无公开统一来源 + **notes**:MLAA 是论文/算法族,无单一官方 SDK 格式枚举。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:无公开统一来源 + **notes**:输出格式未枚举。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:无统一资源约束可记录。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:无信息 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - 无来源(无信息条目;已尝试来源类型与轮数列于条目尾部) +- **searchRoundsUsed**:1 +- **unresolvedQuestions**:上游 SDK/实现;格式;HDR/FP16 +- **conclusion**:MLAA 可映射到公开形态抗锯齿算法,但未找到统一的图像 I/O 协议资料,记录为无信息。 + +### 17. NIS — NVIDIA Image Scaling SDK (NVScaler/NVSharpen) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:NVIDIA Image Scaling SDK 官方仓库与 Streamline 插件。 +- **documentedInputFormats**: +Input/output: non-integer data types, examples DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_NV12 (NV12 via NIS_NV12_SUPPORT) +HDR modes: LDR [0,1], HDR PQ [0,1], HDR Linear recommended [0,12.5] +- **documentedOutputFormats**: +Output UAV: non-integer formats, same docs examples (R8G8B8A8_UNORM/NV12); shader writes RWTexture2D +- **formatBoundary**: +输入为 SRV/read state,输出 UAV/write;sampler 必须 linear clamp;支持 viewport 子区域;NV12 需编译开关 NIS_NV12_SUPPORT;系数纹理 R32G32B32A32_FLOAT 或 R16G16B16A16_FLOAT。 +- **channelOrderAndAlpha**:颜色 RGBA/BGRA 由纹理格式决定;NV12 为 Y plane + interleaved UV;alpha 行为未公开。 +- **precisionAndRange**:LDR/PQ [0,1];HDR Linear [0,12.5] 约 1000nits;fp16/fp32 系数可选;不支持整数格式。 +- **transferFunction**:LDR: display-referred after OETF (sRGB/gamma); HDR PQ: Rec.2020 PQ OETF; HDR Linear: scene/display linear. +- **primariesAndColorSpace**:PQ 模式说明 Rec.2020 PQ;HDR Linear 按 BT.709 luma 参考;其余未说明。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:需通过 NIS_HDR_MODE 选 NONE/LINEAR/PQ;PQ 输入应为 display-referred Rec.2020 PQ;线性 HDR 建议 [0,12.5];没有额外 metadata 参数。 +- **requiredHdrMetadata**:NIS_HDR_MODE;无 MaxCLL/MaxFALL/pre-exposure 参数 +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:官方支持 HDR Linear/PQ 模式,系数可 FP16,且对 HDR 范围有明确文档,属于直接 HDR 路径。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:空间算法,无时域/历史/帧生成约束。 +- **documentedQualityOptions**:NIS_HDR_MODE;NIS_NV12_SUPPORT;NIS_VIEWPORT_SUPPORT;NIS_CLAMP_OUTPUT +- **documentedPerformanceNotes**:README 未比较各格式成本。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/NVIDIAGameWorks/NVIDIAImageScaling/main/README.md | 标题: NVIDIA Image Scaling SDK README | 等级: OfficialRepository | claim: Color spaces/ranges and supported texture formats | quoteOrSymbol: Input and output formats are expected... DXGI_FORMAT_R8G8B8A8_UNORM or DXGI_FORMAT_NV12 | searchRound: 1 + - URL: https://raw.githubusercontent.com/NVIDIA-RTX/Streamline/main/docs/ProgrammingGuideNIS.md | 标题: Streamline NIS Programming Guide | 等级: OfficialRepository | claim: NIS Streamline integration uses colorIn/colorOut tags and hdrMode option | quoteOrSymbol: nisOptions.hdrMode = NISHDR::eNISHDRNone | searchRound: 2 +- **searchRoundsUsed**:4 +- **unresolvedQuestions**:NV12 是否同时作为输出格式;alpha 具体行为;PQ 输入是否 10-bit/12-bit +- **conclusion**:NIS 官方协议明确支持 R8G8B8A8_UNORM 与 NV12 类非整数格式,支持 LDR/PQ/Linear 三种色彩范围,并有 HDR mode 开关。 + +### 18. NNEDI3 — NNEDI3 + +- **mappingConfidence**:明确映射 +- **mappingNotes**:NNEDI3 为公开神经网络倍线算法;Magpie 效果组对应实现可能是 bjin mpv-prescalers 中的 luma prescaler,但未依据 Magpie 源码确认。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:mpv user shader hook (luma plane) + **channelOrder**:luma + **numericRepresentation**:unspecified + **acceptanceStatus**:inferred_from_shader_interface + **evidenceRef**:https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md + **notes**:bjin README 表示 nnedi3 只放大 YUV 的 luma plane;外部输入格式由 mpv 提供,未枚举。 + - **format**:rgba16f / rgba16hf + **apiOrContext**:mpv user shader internal intermediate + **channelOrder**:RGBA + **numericRepresentation**:FLOAT (16-bit) + **acceptanceStatus**:inferred_from_shader_interface + **evidenceRef**:https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md + **notes**:README 关于 rgba16f/rgba16hf 的说明属于 mpv 用户着色器环境中的中间表面,不是 NNEDI3 算法 SDK 契约。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:mpv user shader hook output + **channelOrder**:luma + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md + **notes**:输出为放大后的 luma 平面;无原生后端格式枚举。 + - **format**:rgba16f / rgba16hf + **apiOrContext**:mpv user shader internal intermediate + **channelOrder**:RGBA + **numericRepresentation**:FLOAT (16-bit) + **acceptanceStatus**:inferred_from_shader_interface + **evidenceRef**:https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md + **notes**:同输入条目说明,仅中间表面。 +- **formatBoundary**: +- **inputFormatEnumeration**:partial +- **outputFormatEnumeration**:partial +- **inputOutputRelation**:implementation_defined +- **alphaSemantics**:未说明。 +- **rangeBoundary**:luma only / unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:luma-only 放大;chroma 由宿主 --cscale 处理;mpv 老 d3d11 驱动需要 rgba16hf 分支。 +- **hdrImplication**:rgba16f 只证明 mpv user shader 中间表面,不构成 HDR API 契约。 +- **channelOrderAndAlpha**:README 表示 nnedi3/ravu 只放大 YUV 的 luma 平面;RGB 变体另有 ravu-rgb。 +- **precisionAndRange**:rgba16f / rgba16hf(fp16);来自 mpv user shader 内部格式。 +- **transferFunction**:未说明。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:FormatOnlyNoColorContract +- **hdrBoundary**:mpv user shader 内部处理浮点,但 README 未提供 HDR/PQ/linear 契约;不能作为厂商 HDR 保证。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:仅有的明确格式证据是 mpv prescaler 使用 rgba16f/rgba16hf 内部表面;这是用户着色器路径而非官方 SDK 契约。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:nnedi3 的 neurons/window 在文件名中(bjin 仓库) +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md | 标题: bjin/mpv-prescalers README | 等级: OfficialRepository | claim: Shaders use rgba16f internal format; nnedi3/ravu are luma prescalers | quoteOrSymbol: Unrecognized/unavailable FORMAT name: rgba16f | searchRound: 1 +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:独立 NNEDI3 原生后端的格式契约;HDR 颜色空间 +- **conclusion**:NNEDI3 的 mpv 用户着色器资料显示内部使用 rgba16f/rgba16hf 并只处理 luma;没有可作为 Magpie 外置后端协议的官方格式列表。 + +### 19. Pixel Art — 无信息(Pixel Art 泛指像素画放大算法族) + +- **mappingConfidence**:无信息 +- **mappingNotes**:公开搜索出现 libretro pixel-art enhancement systems、xBRZ、Kopf-Lischinski 等多种算法,无法唯一确定 Magpie 对应后端。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开搜索仅见多种像素画算法 + **notes**:无法唯一映射;不得把 xBRZ/Scale2x/Kopf-Lischinski 等某一算法格式写成该效果组已支持协议。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开搜索仅见多种像素画算法 + **notes**:同上。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:无统一资源约束可记录。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:无信息 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - 无来源(无信息条目;已尝试来源类型与轮数列于条目尾部) +- **searchRoundsUsed**:1 +- **unresolvedQuestions**:具体像素画算法映射;格式;HDR/FP16 +- **conclusion**:Pixel Art 组无法唯一映射到公开算法,记录为无信息。 + +### 20. RAVU — RAVU (Rapid and Accurate Video Upscaling) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:公开仓库 bjin/mpv-prescalers 的 RAVU 系列。 +- **documentedInputFormats**: +mpv user-shader internal rgba16f/rgba16hf +ravu-yuv assumes YUV video after planes merged +ravu-rgb operates on RGB after planes merged +- **documentedOutputFormats**: +内部 rgba16f/rgba16hf 表面 +- **formatBoundary**: +作为 mpv user shader 使用;gather/compute 版本分别用 textureGather/compute;d3d11 老驱动可用 rgba16hf 分支;ravu/ravu-lite 仅放大 luma 平面;ravu-yuv 需 YUV,ravu-rgb 需 RGB;ravu-zoom 任意比例。 +- **channelOrderAndAlpha**:YUV 或 RGB 变体;未单独说明 alpha。 +- **precisionAndRange**:rgba16f/rgba16hf(16-bit float);未说明数值范围上限。 +- **transferFunction**:未说明。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:FormatOnlyNoColorContract +- **hdrBoundary**:内部 FP16 浮点表面不等于厂商 HDR/PQ 契约;README 未说明 HDR 线性/PQ。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:明确内部 rgba16f/rgba16hf,但无颜色/光域契约;只能作为格式级 FP16 证据。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:ravu-lite;ravu;ravu-zoom;-ar anti-ringing 变体 +- **documentedPerformanceNotes**:README 说明 gather 通常较快、compute 对 yuv/rgb 更快,但无数值基准。 +- **qualityPerformanceEvidence**:README 有定性速度说明,无可核实格式路径性能数据。 +- **sources**: + - URL: https://raw.githubusercontent.com/bjin/mpv-prescalers/master/README.md | 标题: bjin/mpv-prescalers README | 等级: OfficialRepository | claim: RAVU variants and rgba16f/rgba16hf formats | quoteOrSymbol: rgba16f ... rgba16hf | searchRound: 1 +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:外部原生后端的格式协议;HDR/色彩空间 +- **conclusion**:RAVU 明确为 mpv 用户着色器族,公开资料显示内部使用 rgba16f/rgba16hf 且分 YUV/RGB 变体;缺少可作为外置 SDK 的完整协议。 + +### 21. RTXVideo — NVIDIA RTX Video Super Resolution (VFX SDK VSR filter) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:RTX Video 效果组拆为 VSR、Denoiser、HDR;此条为 VFX SDK Video Super Resolution filter。 +- **documentedInputFormats**: +GPU buffers in BGRA or RGBA interleaved format, 8-bit unsigned per component +- **documentedOutputFormats**: +Same BGRA or RGBA interleaved U8 GPU buffer +- **formatBoundary**: +输入输出必须 GPU buffer,BGRA/RGBA interleaved,每分量 8-bit unsigned;Denoise/Deblur modes 输出分辨率必须与输入相同;VSR 支持不同 modes;建议最小 360p。 +- **channelOrderAndAlpha**:BGRA 或 RGBA;alpha 分量在 U8 格式中未单独描述行为。 +- **precisionAndRange**:8-bit unsigned integer per component;仅 SDR 级像素格式。 +- **transferFunction**:未在页面说明(SDR 视频输入通常 YUV->RGB 转换在外部)。 +- **primariesAndColorSpace**:未说明;相关 SDK 页面提到颜色不对时交换 709/601(YUV 转换)。 +- **hdrSupport**:SDROnly +- **hdrBoundary**:VFX VSR 页面只列 8-bit U8 BGRA/RGBA,无 HDR/PQ/FP16 路径。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:官方页面明确 U8 BGRA/RGBA 输入输出,没有 HDR/FP16。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:VSR 是视频时域增强;无帧生成/运动向量/深度输入;Denoise/Deblur 同分辨率。 +- **documentedQualityOptions**:VSR_Bicubic;VSR_Low;VSR_Medium;VSR_High;VSR_Ultra;HighBitrate_*;Denoise_*;Deblur_* +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://docs.nvidia.com/maxine/vfx/1.2.0.0/Filters/VideoSuperResolution.html | 标题: NVIDIA VFX SDK Video Super Resolution | 等级: OfficialDocs | claim: VSR input/output BGRA or RGBA U8; modes | quoteOrSymbol: The input and output of the VSR filter are GPU buffers in BGRA or RGBA interleaved format...8-bit unsigned integer | searchRound: 1 +- **searchRoundsUsed**:3 +- **unresolvedQuestions**:VSR 是否接受 NV12/P010 内部路径;HDR 版本协议 +- **conclusion**:RTX Video VSR 的 VFX SDK 官方页面协议为 BGRA/RGBA 8-bit U8 GPU buffer;没有 FP16/HDR 输入输出契约。 + +### 22. RTXVideo — NVIDIA RTX Video Denoiser (VFX SDK VSR Denoise modes) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:VFX SDK VSR filter 内含 Denoise_Low/Medium/High/Ultra modes,作为 Denoiser 子条目。 +- **documentedInputFormats**: +BGRA or RGBA interleaved U8 GPU buffers +- **documentedOutputFormats**: +BGRA or RGBA interleaved U8 GPU buffers, same resolution as input +- **formatBoundary**: +Denoise modes 不支持 upscaling,输出分辨率必须等于输入;输入输出同为 BGRA/RGBA U8。 +- **channelOrderAndAlpha**:BGRA/RGBA。 +- **precisionAndRange**:8-bit unsigned per component。 +- **transferFunction**:未说明。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:SDROnly +- **hdrBoundary**:官方页面只有 U8 输入输出,无 HDR 路径。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:官方 VSR 页面把 Denoise modes 也限定为 BGRA/RGBA U8,无 HDR。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:Denoise 模式不放大,输出同输入分辨率;无光流/深度输入。 +- **documentedQualityOptions**:Denoise_Low;Denoise_Medium;Denoise_High;Denoise_Ultra +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://docs.nvidia.com/maxine/vfx/1.2.0.0/Filters/VideoSuperResolution.html | 标题: NVIDIA VFX SDK Video Super Resolution (Denoise modes) | 等级: OfficialDocs | claim: Denoise modes same BGRA/RGBA U8 and same output resolution | quoteOrSymbol: Denoise_Low ... resolution of the output must be the same as input | searchRound: 1 +- **searchRoundsUsed**:2 +- **unresolvedQuestions**:独立 Denoiser 模型格式;HDR +- **conclusion**:RTX Video Denoiser 在官方 VFX 文档中与 VSR 共用 BGRA/RGBA U8、同分辨率输出协议,无 HDR/FP16。 + +### 23. RTXVideo — NVIDIA RTX Video HDR + +- **mappingConfidence**:明确映射 +- **mappingNotes**:NVIDIA 官方博客/产品页将 RTX Video HDR 描述为 RTX Video SDK 功能;未检索到完整公开 API 协议页。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://developer.nvidia.com/blog/enhancing-low-resolution-sdr-video-with-the-nvidia-rtx-video-sdk/ + **notes**:官方博客只说 SDR 视频转 HDR10 质量,未给出输入像素格式/API 上下文。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:https://developer.nvidia.com/blog/enhancing-low-resolution-sdr-video-with-the-nvidia-rtx-video-sdk/ + **notes**:官方博客只说 HDR10 质量输出,未给出输出像素格式/API 上下文。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明。 +- **rangeBoundary**:SDR input -> HDR10 output 的产品语义;无量值范围。 +- **transferBoundary**:未公开(博客层面提到 HDR10/PQ,但没有 API 转换契约)。 +- **resourceConstraints**:无 API 资源状态、尺寸或颜色转换文档。 +- **hdrImplication**:产品方向明确为 SDR->HDR,但无公开 HDR/FP16 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:官方博客/介绍提到 HDR10/PQ 场景;公开协议页未给出转换细节。 +- **primariesAndColorSpace**:博客提到从 sRGB 扩展到 scRGB/HDR 色域(不同页面措辞不一致);无正式协议。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:官方仅宣称 SDR->HDR,未公开输入/输出格式、metadata、tone mapping 或 pre-exposure 契约。 +- **requiredHdrMetadata**:未说明 +- **candidateInternalProfile**:无信息 +- **profileRationale**:没有可核实的输入输出协议来源。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无公开协议。 +- **documentedQualityOptions**:RTX Video HDR on/off 等产品级选项,不是 API 格式档位 +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://developer.nvidia.com/blog/enhancing-low-resolution-sdr-video-with-the-nvidia-rtx-video-sdk/ | 标题: NVIDIA Technical Blog: RTX Video SDK | 等级: OfficialForum | claim: RTX Video HDR converts SDR video to HDR10 quality; VSR upscales | quoteOrSymbol: RTX Video HDR converts SDR video to HDR10 quality | searchRound: 1 +- **searchRoundsUsed**:5 +- **unresolvedQuestions**:API 输入输出格式;NvCVImage pixel format;PQ metadata;scRGB/HDR10 工作空间 +- **conclusion**:RTX Video HDR 有明确官方产品宣称但缺少公开 API 格式协议,记录为无格式信息、仅 HDR 方向明确。 + +### 24. Sharpen — 无信息(Sharpen 为通用锐化效果族) + +- **mappingConfidence**:无信息 +- **mappingNotes**:公开搜索出现 NIS NVSharpen、Android SharpenFilter 等多个互不相同的后端,无法唯一对应。 +- **documentedInputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开搜索出现多个锐化实现 + **notes**:无法唯一映射;不得把任一锐化实现的格式写成该效果组已支持协议。 +- **documentedOutputFormats**: + - **format**:unspecified + **apiOrContext**:公开资料未枚举 + **channelOrder**:unspecified + **numericRepresentation**:unspecified + **acceptanceStatus**:not_enumerated + **evidenceRef**:公开搜索出现多个锐化实现 + **notes**:同上。 +- **formatBoundary**: +- **inputFormatEnumeration**:not_enumerated +- **outputFormatEnumeration**:not_enumerated +- **inputOutputRelation**:unknown +- **alphaSemantics**:未说明。 +- **rangeBoundary**:unknown +- **transferBoundary**:unspecified +- **resourceConstraints**:无统一资源约束可记录。 +- **hdrImplication**:无公开 HDR 格式契约。 +- **channelOrderAndAlpha**:无信息 +- **precisionAndRange**:无信息 +- **transferFunction**:无信息 +- **primariesAndColorSpace**:无信息 +- **hdrSupport**:无信息 +- **hdrBoundary**:未说明 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:无信息 +- **profileRationale**:无信息 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:无信息 +- **documentedPerformanceNotes**:无信息 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - 无来源(无信息条目;已尝试来源类型与轮数列于条目尾部) +- **searchRoundsUsed**:1 +- **unresolvedQuestions**:具体锐化算法映射;格式;HDR/FP16 +- **conclusion**:Sharpen 效果组无法唯一映射到上游算法,记录为无信息。 + +### 25. SMAA — SMAA (Subpixel Morphological Antialiasing) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:官方 iryoku/smaa 仓库。 +- **documentedInputFormats**: +colorTex: RGBA texture (2D color/luma input) +edgesTex/areaTex/searchTex 等内部/预计算纹理 +depthTex for depth edge detection +- **documentedOutputFormats**: +colorTex/blended output 4-channel color buffer +- **formatBoundary**: +官方集成说明要求两个 RGBA 时域 render target;创建后清除 alpha;所有 sampler linear + clamp;纹理读写默认非 sRGB,只有最终 NeighborhoodBlending 的 input/output 可 sRGB;64-bit 输入在 GCN 上有半速率线性过滤。 +- **channelOrderAndAlpha**:RGBA;alpha 通道在中间步骤被用作速度/索引等;文档警告清除 alpha。 +- **precisionAndRange**:float4/float 通用;提到 64-bit 输入和 half-rate filtering,未给出 8/10/16 位清单。 +- **transferFunction**:建议在 gamma-corrected(非 sRGB 采样)空间做 luma/color edge detection;最终 blending pass 可处理 sRGB 读写。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:Unspecified +- **hdrBoundary**:官方代码未声称 HDR;luma edge detection 注释要求 gamma-corrected colors,通常为 LDR 空间。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:官方集成注释围绕 gamma/sRGB 空间和 RGBA 临时目标,未见 HDR/线性 HDR 直接路径;保守记为 SDR 兼容。 +- **auxiliaryInputs**:velocityTex (temporal variant);depthTex (depth edge detection);areaTex/searchTex lookup textures;predicationTex optional +- **auxiliaryInputFormatsAndSemantics**:area/search 为预计算 LUT;velocity 解码方式由 SMAA_DECODE_VELOCITY 定义;depth 可为深度纹理。 +- **temporalOrFrameGenerationConstraints**:SMAA 1x 无时域;temporal/supersampling 变体需要多子样本、previous/current color 和 velocity。 +- **documentedQualityOptions**:SMAA 1x/2x/T2x 等宏/预设由官方源码定义 +- **documentedPerformanceNotes**:官方注释提示 64-bit 输入半速率线性过滤,可用 point filtering 规避;未给完整性能基准。 +- **qualityPerformanceEvidence**:官方注释有格式相关性能提示,但没有格式路径性能基准。 +- **sources**: + - URL: https://raw.githubusercontent.com/iryoku/smaa/master/README.md | 标题: SMAA README | 等级: OfficialRepository | claim: SMAA official repo and integration pointer | quoteOrSymbol: Checkout the technical paper | searchRound: 1 + - URL: https://raw.githubusercontent.com/iryoku/smaa/master/SMAA.hlsl | 标题: SMAA.hlsl | 等级: OfficialRepository | claim: Integration notes: RGBA render targets, sRGB rules, filtering | quoteOrSymbol: All texture reads and buffer writes must be non-sRGB... | searchRound: 2 +- **searchRoundsUsed**:4 +- **unresolvedQuestions**:具体 DXGI 格式支持;HDR 线性输入;alpha 对外值 +- **conclusion**:SMAA 官方仓库提供了着色器级协议:RGBA 输入输出、RGBA 临时目标、gamma/sRGB 空间建议;未公开现代 HDR/FP16 API 契约。 + +### 26. xBRZ — xBRZ (pixel-art scaling algorithm) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:xBRZ 原算法由 Zenju 发布;本条目证据来自 TypeScript/WASM 移植 kayahr/xbrz。 +- **documentedInputFormats**: +RGBA pixel data in Uint8ClampedArray (TypeScript port) +- **documentedOutputFormats**: +RGBA pixel data in Uint8ClampedArray +- **formatBoundary**: +TypeScript 移植接口为源/目标 RGBA 像素缓冲;缩放因子 2-6;支持 alpha 透明;非原生 SDK 协议。 +- **channelOrderAndAlpha**:RGBA;支持 alpha 透明。 +- **precisionAndRange**:8-bit per channel in Uint8ClampedArray([0,255]);无 HDR/浮点。 +- **transferFunction**:未说明。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:SDROnly +- **hdrBoundary**:移植接口是 8-bit RGBA,无 HDR。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:证据显示 RGBA 8-bit 像素缓冲;无浮点/HDR 接口。 +- **auxiliaryInputs**:无信息 +- **auxiliaryInputFormatsAndSemantics**:无信息 +- **temporalOrFrameGenerationConstraints**:无信息 +- **documentedQualityOptions**:scale factors 2-6 +- **documentedPerformanceNotes**:未找到可核实的格式路径性能结论。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/kayahr/xbrz/master/README.md | 标题: kayahr/xbrz TypeScript/WASM port | 等级: GitHubExperiment | claim: Port uses RGBA Uint8ClampedArray and supports alpha | quoteOrSymbol: Source and target are RGBA pixel data in a Uint8ClampedArray | searchRound: 1 +- **searchRoundsUsed**:3 +- **unresolvedQuestions**:Magpie 原生后端是否使用该接口;原 C++ xBRZ 的格式契约;HDR +- **conclusion**:xBRZ 映射明确,但可靠公开协议只有社区移植的 RGBA 8-bit 接口,原生后端协议未确认。 + +### 27. XeSS — Intel XeSS Super Resolution (XeSS-SR) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:Intel xess 官方仓库与 Developer Guide。 +- **documentedInputFormats**: +R16G16B16A16_FLOAT +R11G11B10_FLOAT +R8G8B8A8_UNORM +其他 any linear color format; only UNORM integer color formats allowed +Motion vectors: R16G16_FLOAT +Depth: any depth format such as D32_FLOAT or D24_UNORM +- **documentedOutputFormats**: +Same format and color space as input (2D output texture) +- **formatBoundary**: +输入颜色可为 LDR/HDR 任意 linear 格式;整数格式只允许 UNORM;输出必须与输入同格式同色彩空间,且输出 alpha 不保留并填 1.0;D3D12 输入 NON_PIXEL_SHADER_RESOURCE,输出 UAV;Vulkan 对应状态。 +- **channelOrderAndAlpha**:RGBA/R11G11B10 等格式;XeSS-SR 不保留输出 alpha,填 1.0。 +- **precisionAndRange**:颜色为 scene-referred scRGB,1.0 为 80 nits SDR 白点,HDR 可 >1;LDR 输入需 XESS_INIT_FLAG_LDR_INPUT_COLOR;exposureScale 等。 +- **transferFunction**:任何 linear 色彩格式(LDR/HDR);非 sRGB/PQ 编码直接输入。 +- **primariesAndColorSpace**:scRGB(scene-referred);输出同输入色彩空间。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:推荐直接提供 HDR 线性/scRGB;可设置 exposureScale/pre-exposure/exposureMultiplier;算法内部自行 tonemap,输出恢复;LDR 也可。 +- **requiredHdrMetadata**:exposureScale;preExposure/exposureMultiplier 可选;无 MaxCLL/MaxFALL 要求 +- **candidateInternalProfile**:DirectFP16 +- **profileRationale**:官方明确支持 FP16/RGBA16F、scRGB HDR 线性并推荐 HDR 输入。 +- **auxiliaryInputs**:motion vectors;depth (low-res MV mode);responsive pixel mask (optional);exposure scale/multiplier;jitter +- **auxiliaryInputFormatsAndSemantics**:MVs R16G16_FLOAT screen-space pixels, low-res default or high-res dilated; depth D32_FLOAT/D24_UNORM; responsive mask R channel float [0,1]; input states as above. +- **temporalOrFrameGenerationConstraints**:时域超分:需 jittered color, motion vectors, depth when low-res; output target resolution; fixed/dynamic input resolution ranges; reset history on cuts. +- **documentedQualityOptions**:Ultra Quality/Quality/Balanced/Performance presets (via optimal input resolution API) +- **documentedPerformanceNotes**:开发指南建议 HDR input color 推荐、使用 FP16 color buffer in scene linear HDR(性能提示);无格式路径基准。 +- **qualityPerformanceEvidence**:指南有使用 FP16 scene-linear HDR 的建议,但无定量比较。 +- **sources**: + - URL: https://raw.githubusercontent.com/intel/xess/main/doc/xess_sr_developer_guide_english.md | 标题: Intel XeSS-SR Developer Guide | 等级: OfficialRepository | claim: Formats and color space: R16G16B16A16_FLOAT/R11G11B10_FLOAT/R8G8B8A8_UNORM, scRGB, output same | quoteOrSymbol: XeSS-SR accepts both LDR and HDR input colors in any linear color format | searchRound: 1 + - URL: https://raw.githubusercontent.com/intel/xess/main/README.md | 标题: Intel xess README | 等级: OfficialRepository | claim: XeSS-SR and XeSS-FG/XeLL availability | quoteOrSymbol: XeSS Super Resolution (XeSS-SR) | searchRound: 2 +- **searchRoundsUsed**:5 +- **unresolvedQuestions**:每种 DXGI/Vulkan 格式完整矩阵;PQ/HLG 是否需外部转换;primaries 非 scRGB +- **conclusion**:XeSS-SR 官方协议非常明确:支持 R16G16B16A16_FLOAT/R11G11B10_FLOAT/R8G8B8A8_UNORM 等线性格式,scRGB scene-referred,输出同输入格式且 alpha 填 1。 + +### 28. XeSSFG — Intel XeSS Frame Generation (XeSS-FG) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:Intel xess 官方仓库 XeSS-FG Developer Guide。 +- **documentedInputFormats**: +Back buffer/HUD-less/UI-only: R10G10B10A2_UNORM for HDR10/BT.2100 HDR display +Motion vectors: R16G16_FLOAT or similar +Depth: any depth format such as D32_FLOAT or D24_UNORM +UI Alpha: single channel; UI Color and Alpha: same backbuffer format +- **documentedOutputFormats**: +Interpolated frames output to proxy swap chain; same back buffer pixel format (HDR10 R10G10B10A2_UNORM documented) +- **formatBoundary**: +HDR display 支持 R10G10B10A2_UNORM + HDR10/BT.2100;明确不支持 FP16 HDR/scRGB;back buffer, HUD-less, UI-only 必须同像素格式、色彩空间、尺寸;MV 与 depth buffer 尺寸一致。 +- **channelOrderAndAlpha**:UI-only texture 有 alpha;非预乘 alpha 可用标志;合成公式 Final.RGB = UIonly.RGB + (1-UIonly.Alpha) * HUDlessColor.RGB;2-bit alpha 对 UI 合成不足。 +- **precisionAndRange**:HDR 显示为 10-bit R10G10B10A2_UNORM HDR10/BT.2100;不支持 FP16/scRGB;LDR 显示用 swapchain 格式。 +- **transferFunction**:HDR10 / BT.2100(PQ)用于 HDR 显示。 +- **primariesAndColorSpace**:HDR10 / BT.2100;非 HDR 未细说。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:HDR 必须使用 R10G10B10A2_UNORM 与 HDR10/BT.2100,back buffer/HUDless/UI 同格式;不支持 FP16/scRGB;未给出 PQ 解码细节。 +- **requiredHdrMetadata**:HDR display pixel format R10G10B10A2_UNORM;HDR10/BT.2100 color space;无 MaxCLL/MaxFALL 公开参数 +- **candidateInternalProfile**:BoundedHDR +- **profileRationale**:官方明确 HDR 只走 10-bit R10G10B10A2 UNORM/HDR10 BT.2100,不接收 FP16/scRGB,故为有界 HDR 契约。 +- **auxiliaryInputs**:motion vectors;depth;HUD-less color;UI-only texture;jitter offset;resetHistory;motion vector scale +- **auxiliaryInputFormatsAndSemantics**:MV R16G16_FLOAT/similar low/high res; depth any format, same size as MV; HUDless/UI same as backbuffer format/color; UI alpha single channel. +- **temporalOrFrameGenerationConstraints**:需要 XeLL latency reduction 启用;不能独占全屏;40 FPS 最低/60 FPS 推荐;禁用/减少 motion blur;切换或与第三方帧生成不兼容;UI composition modes. +- **documentedQualityOptions**:UI composition modes;interpolated frames count 等 XeSS-FG 公开选项 +- **documentedPerformanceNotes**:官方建议 low-res MVs 以免 high-res 大幅降低性能;无格式路径定量表。 +- **qualityPerformanceEvidence**:官方说明 high-res MV 性能影响,但无格式路径性能基准。 +- **sources**: + - URL: https://raw.githubusercontent.com/intel/xess/main/doc/xess_fg_developer_guide_english.md | 标题: Intel XeSS-FG Developer Guide | 等级: OfficialRepository | claim: HDR display support R10G10B10A2_UNORM HDR10/BT.2100; no FP16/scRGB | quoteOrSymbol: XeSS-FG provides support for HDR displays with the R10G10B10A2_UNORM pixel format... does not support FP16 HDR format and scRGB color space | searchRound: 1 + - URL: https://raw.githubusercontent.com/intel/xess/main/README.md | 标题: Intel xess README | 等级: OfficialRepository | claim: XeSS-FG available on Intel Arc and non-Intel with SM6.4 | quoteOrSymbol: XeSS Frame Generation (XeSS-FG) | searchRound: 2 +- **searchRoundsUsed**:4 +- **unresolvedQuestions**:LDR 下的完整 swapchain 格式清单;PQ metadata 传递 +- **conclusion**:XeSS-FG 官方明确 HDR 契约:R10G10B10A2_UNORM/HDR10 BT.2100,不支持 FP16/scRGB;所有参与合成的纹理必须同格式同色彩空间。 + +### 29. NVIDIA Optical Flow — NVIDIA Optical Flow SDK (NvOF) + +- **mappingConfidence**:明确映射 +- **mappingNotes**:附加依赖项对应 NVIDIA Optical Flow SDK NvOF API(另有 NVOFA 新 API,本次主条目为 NvOF 头文件证据)。 +- **documentedInputFormats**: +NV_OF_BUFFER_FORMAT_GRAYSCALE8 +NV_OF_BUFFER_FORMAT_NV12 +NV_OF_BUFFER_FORMAT_ABGR8 (A8B8G8R8) +- **documentedOutputFormats**: +NV_OF_FLOW_VECTOR (SHORT2, S10.5 flowx/flowy) +NV_OF_STEREO_DISPARITY (for stereo mode) +Cost buffer NV_OF_BUFFER_FORMAT_UINT or UINT8 +- **formatBoundary**: +输入帧支持 GRAYSCALE8/NV12/ABGR8;输出/提示为 SHORT2(S10.5);外部提示/成本格式另有要求;支持 output grid size 1/2/4;有宽高 min/max caps。 +- **channelOrderAndAlpha**:ABGR8 为 8-bit packed A8B8G8R8;NV12 为 Y planar + interleaved UV。 +- **precisionAndRange**:8-bit input; output flow vector int16 S10.5 表示亚像素;无 HDR/PQ 声明。 +- **transferFunction**:未说明。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:SDROnly +- **hdrBoundary**:官方头文件输入格式只列 8-bit 亮度/NV12/ABGR8,无 FP16/HDR。 +- **requiredHdrMetadata**:无信息 +- **candidateInternalProfile**:SDRCompatible +- **profileRationale**:NvOF 头文件列出的输入只有 8-bit 格式,输出为定点光流。 +- **auxiliaryInputs**:externalHints (optional);cost buffer (optional);reference frame +- **auxiliaryInputFormatsAndSemantics**:externalHints/outputBuffer use NV_OF_FLOW_VECTOR for optical flow; cost UINT or UINT8; reference frame same input format as inputFrame. +- **temporalOrFrameGenerationConstraints**:光流在两帧(input/reference)间估计;output grid size 决定流向量网格;scene change/hints 等。 +- **documentedQualityOptions**:NV_OF_OUTPUT_VECTOR_GRID_SIZE 1/2/4 +- **documentedPerformanceNotes**:头文件注明 legacy 32-bit cost 不高效,建议 8-bit cost;无格式路径完整基准。 +- **qualityPerformanceEvidence**:官方头文件有成本格式性能建议,但无定量表。 +- **sources**: + - URL: https://raw.githubusercontent.com/NVIDIA/NVIDIAOpticalFlowSDK/master/nvOpticalFlowCommon.h | 标题: NVIDIA Optical Flow SDK nvOpticalFlowCommon.h | 等级: OfficialHeader | claim: Buffer formats: GRAYSCALE8, NV12, ABGR8; flow vector SHORT2 | quoteOrSymbol: NV_OF_BUFFER_FORMAT_NV12 ... NV_OF_BUFFER_FORMAT_ABGR8 | searchRound: 1 +- **searchRoundsUsed**:4 +- **unresolvedQuestions**:NVOFA 新 API 的 RGBA/F16 支持;HDR 帧是否需转为 NV12/ABGR8 +- **conclusion**:NVIDIA Optical Flow NvOF 官方头文件支持 8-bit GRAYSCALE/NV12/ABGR8 输入,输出为 S10.5 定点 SHORT2 光流;未提供 HDR/FP16 输入路径。 + +### 30. AMD FidelityFX Optical Flow — FidelityFX Optical Flow + +- **mappingConfidence**:明确映射 +- **mappingNotes**:AMD FidelityFX SDK 中用于 FSR3 的光流技术,官方 docs/optical-flow.md。 +- **documentedInputFormats**: +color input resource (格式未在文档枚举) +- **documentedOutputFormats**: +opticalFlowVector: R16G16_SINT +opticalFlowSCD: R32_UINT (3x1 scene change detection) +- **formatBoundary**: +以 8x8 block 计算,输出纹理尺寸由 (displaySize+block-1)/8 决定;block size 固定 8;color 输入经 transfer function/luminance 转换。 +- **channelOrderAndAlpha**:未说明 color 通道顺序;算法使用亮度。 +- **precisionAndRange**:输出 R16G16_SINT;SCD R32_UINT;输入颜色格式未枚举。 +- **transferFunction**:输入有 backbufferTransferFunction 与 minMaxLuminance,用于 HDR 管道转换到 luminance。 +- **primariesAndColorSpace**:未说明。 +- **hdrSupport**:ExplicitHDR +- **hdrBoundary**:支持 HDR pipeline 的 luminance 转换:需 backbufferTransferFunction 和 minMaxLuminance;未公开颜色输入格式。 +- **requiredHdrMetadata**:backbufferTransferFunction;minMaxLuminance +- **candidateInternalProfile**:BoundedHDR +- **profileRationale**:官方要求把 HDR 颜色经 transfer/luminance 转成内部亮度,适合有界转换路径。 +- **auxiliaryInputs**:previous/current color input;internal history resources for pyramid/histogram +- **auxiliaryInputFormatsAndSemantics**:color input is current frame; internal history resources; SCD output 3x1 R32_UINT. +- **temporalOrFrameGenerationConstraints**:需要连续帧、内部金字塔、历史 histogram 做场景变化检测;用于 FSR3 帧插值。 +- **documentedQualityOptions**:8x8 block size fixed in release +- **documentedPerformanceNotes**:文档内存表按 GPU/4K 显示,但未比较不同格式成本。 +- **qualityPerformanceEvidence**:未找到可核实的格式路径性能结论 +- **sources**: + - URL: https://raw.githubusercontent.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/release-FSR3-3.0.3/docs/techniques/optical-flow.md | 标题: FidelityFX Optical Flow | 等级: OfficialDocs | claim: Optical flow outputs R16G16_SINT and R32_UINT; HDR transfer/luminance | quoteOrSymbol: opticalFlowVector ... R16G16_SINT ... R32_UINT | searchRound: 1 +- **searchRoundsUsed**:3 +- **unresolvedQuestions**:输入 color 的格式枚举;输出语义坐标空间精确单位;独立于 FSR3 的 SDK +- **conclusion**:AMD FidelityFX Optical Flow 官方协议公开了光流输出 R16G16_SINT、SCD R32_UINT 和 HDR transfer/luminance 输入需求,但输入颜色格式未枚举。 + +## 无信息条目汇总 + +| 效果组 | 已尝试来源类型 | 搜索轮数 | 仍缺失字段 | +|---|---|---|---| +| CRT | 网页搜索 | 1 | 上游映射;输入格式;色彩空间;HDR/FP16 | +| CuNNy2 | 网页搜索 | 2 | CuNNy2 上游映射;格式;HDR/FP16;alpha | +| Diagnostics | 网页搜索 | 1 | 上游映射;I/O 格式;HDR/FP16 | +| FXAA | 网页搜索 | 4 | 固定输入格式;HDR/FP16;色彩空间 | +| MLAA | 网页搜索 | 1 | 上游 SDK/实现;格式;HDR/FP16 | +| Pixel Art | 网页搜索 | 1 | 具体像素画算法映射;格式;HDR/FP16 | +| Sharpen | 网页搜索 | 1 | 具体锐化算法映射;格式;HDR/FP16 | + +## 证据边界说明 + +- 官方契约:上表中 `OfficialDocs/OfficialHeader/OfficialRepository` 并带有明确引文的内容。 +- 官方样例行为:`OfficialRepository` 中 CasCmdLine 等参考样例的格式行为只作为参考实现记录,不扩张为 API 保证。 +- GitHub 实验结论:标记为 `GitHubExperiment` 的条目只代表社区/实验证据,不代表厂商保证。 +- 用户着色器内部 `rgba16f/rgba16hf` 只记录为中间表面证据。 +- 当官方资料与实验资料冲突时,本文件分别保留主张,未做强行合并。 + +## 覆盖与排除验证 + +- JSON 共 30 个对象,覆盖效果组:AMD FidelityFX Optical Flow、Anime4K、CAS、CRT、CuNNy、CuNNy2、DLSS、DLSSFG、Diagnostics、FSR、FSR2、FSR3、FSR4、FSRCNNX、FXAA、MLAA、NIS、NNEDI3、NVIDIA Optical Flow、Pixel Art、RAVU、RTXVideo、SMAA、Sharpen、XeSS、XeSSFG、xBRZ。 +- 不存在该排除效果条目。 +- 所有条目 searchRoundsUsed 均不超过 10。 diff --git a/scripts/Run-HdrMechanicalValidation.ps1 b/scripts/Run-HdrMechanicalValidation.ps1 new file mode 100644 index 000000000..18fddefc2 --- /dev/null +++ b/scripts/Run-HdrMechanicalValidation.ps1 @@ -0,0 +1,265 @@ +# Lightweight source-level validation for the HDR mechanical slice. +# This script does not compile; it checks that the required configuration, +# protocol, dispatcher, diagnostics, and capture-processor scaffolding is +# present in the expected source files. Run from the repository root: +# +# powershell -ExecutionPolicy Bypass -File scripts\Run-HdrMechanicalValidation.ps1 + +$ErrorActionPreference = 'Stop' + +function Assert-Contains { + param( + [string]$Path, + [string[]]$Expected, + [string]$Label + ) + + if (-not (Test-Path $Path)) { + throw "Missing file: $Path ($Label)" + } + + $content = Get-Content $Path -Raw + foreach ($needle in $Expected) { + if (-not $content.Contains($needle)) { + throw "Missing '$needle' in $Path ($Label)" + } + } + + Write-Host "PASS $Label" +} + +$root = Split-Path -Parent $PSScriptRoot +$core = Join-Path $root 'src\Magpie.Core' +$app = Join-Path $root 'src\Magpie' + +Assert-Contains (Join-Path $core 'include\ScalingOptions.h') @( + 'EnableHdrCompatibility = 1 << 23', + 'DEFINE_FLAG_ACCESSOR(IsHdrCompatibilityEnabled, ScalingFlags::EnableHdrCompatibility, flags)' +) 'ScalingOptions flag/accessor' + +Assert-Contains (Join-Path $core 'ScalingOptions.cpp') @( + 'IsHdrCompatibilityEnabled: {}', + 'IsHdrCompatibilityEnabled(),' +) 'ScalingOptions log' + +Assert-Contains (Join-Path $app 'Profile.h') @( + 'DEFINE_FLAG_ACCESSOR(IsHdrCompatibilityEnabled, ScalingFlags::EnableHdrCompatibility, scalingFlags)' +) 'Profile flag accessor' + +Assert-Contains (Join-Path $app 'AppSettings.cpp') @( + 'writer.Key("enableHdrCompatibility")', + 'writer.Bool(profile.IsHdrCompatibilityEnabled())', + 'JsonHelper::ReadBoolFlag(profileObj, "enableHdrCompatibility", ScalingFlags::EnableHdrCompatibility, profile.scalingFlags);' +) 'Profile save/load' + +Assert-Contains (Join-Path $app 'ProfilePage.xaml') @( + 'x:Uid="Profile_General_HdrCompatibility"', + 'IsHdrCompatibilityEnabled' +) 'Profile UI' + +Assert-Contains (Join-Path $app 'ProfileViewModel.idl') @('Boolean IsHdrCompatibilityEnabled;') 'ProfileViewModel idl' +Assert-Contains (Join-Path $app 'ProfileViewModel.h') @('bool IsHdrCompatibilityEnabled() const noexcept;') 'ProfileViewModel header' +Assert-Contains (Join-Path $app 'ProfileViewModel.cpp') @('ProfileViewModel::IsHdrCompatibilityEnabled') 'ProfileViewModel cpp' + +Assert-Contains (Join-Path $core 'HdrFrame.h') @( + 'struct HdrFormatRoute', + 'effectId', + 'optionId', + 'inputFormat', + 'outputFormat', + 'inputTransfer', + 'outputTransfer', + 'inputRange', + 'outputRange', + 'alphaMode', + 'evidenceLevel', + 'hdrNative', + 'adapterProfile', + 'defaultForHdr', + 'defaultForSdr' +) 'HDR route data structure' + +Assert-Contains (Join-Path $core 'HdrProtocol.h') @( + 'SelectDefaultHdrRoute', + 'SelectDefaultSdrRoute', + 'GetAcceptedFormatRoutes', + 'GetHdrNativeFormatRoutes', + 'GetHdrAdapterFormatRoutes', + 'SerializeHdrFormatRoute' +) 'HDR protocol helpers' + +Assert-Contains (Join-Path $core 'HdrAdapterDispatcher.h') @( + 'class HdrAdapterDispatcher', + 'DXGI_FORMAT_R16G16B16A16_FLOAT', + 'forwardParameters', + 'inverseParameters' +) 'HDR adapter dispatcher' + +Assert-Contains (Join-Path $core 'HdrAdapterDispatcher.cpp') @( + 'case HdrAdapterProfile::DirectFP16', + 'case HdrAdapterProfile::BoundedHDR', + 'case HdrAdapterProfile::SDRCompatible', + 'case HdrAdapterProfile::ConditionalFP16', + 'case HdrAdapterProfile::Unknown', + 'case HdrAdapterProfile::PresentationTerminal' +) 'HDR adapter profiles' + +Assert-Contains (Join-Path $core 'HdrColorTransform.cpp') @( + 'ShoulderCoefficients BuildShoulder', + 'value / (1.0f + c.k * (value - 1.0f))', + 'c.target + c.tailSlope * (value - c.peak)', + 'y * (1.0f - c.k) / std::max(1.0f - c.k * y, 1e-6f)', + 'BoundedRouteHighlightTarget = HdrColorTransform::BoundedRouteHighlightTarget', + 'SDR-compatible replicate contract', + 'float HdrColorTransform::EncodeBoundedHdr', + 'float HdrColorTransform::DecodeBoundedHdr' +) 'anchored monotone invertible shoulder curve family (CPU)' + +Assert-Contains (Join-Path $core 'HdrSurfaceAdapter.cpp') @( + 'float ApplyShoulder(float value, float peak, float target, float k, float tailSlope)', + 'float InvertShoulder(float value, float peak, float target, float k, float tailSlope)', + 'value / (1.0 + k * (value - 1.0))', + 'target + tailSlope * (value - peak)', + 'value * (1.0 - k) / max(1.0 - k * value, 1e-6)', + 'Replicate contract for SDR-compatible backends', + 'float EncodeExtendedSrgb(float value)', + 'float DecodeExtendedSrgb(float value)', + 'EncodeExtendedSrgb(normalized.r)', + 'DecodeExtendedSrgb(value.r)', + 'float3 MapRec2020ToPqGamut(float3 value)', + 'float3 rec2020 = MapRec2020ToPqGamut(Rec709ToRec2020(value.rgb));', + 'result = Rec2020ToRec709(rec2020);' +) 'GPU shoulder curve matches CPU contract' + +Assert-Contains (Join-Path $core 'FrameSourceBase.cpp') @( + 'HDR source SDR-white measurement failed' +) 'SDR white measurement fallback is logged' + +Assert-Contains (Join-Path $core 'Renderer.cpp') @( + 'sdrWhite={:.1f} peak={:.1f} curvePeak={:.3f} curveTarget={:.3f}' +) 'DLSSNR boundary log carries curve parameters' + +Assert-Contains (Join-Path $core 'DLSSSRUpscaler.cpp') @( + '_hdrProtocol.hdrColorInput', + 'NVSDK_NGX_DLSS_Feature_Flags_IsHDR' +) 'DLSS HDR feature creation flag' + +Assert-Contains (Join-Path $core 'EffectProtocolCatalogC.h') @( + 'optionId = "canonical-FP16-marker"', + 'adapterProfile = HdrAdapterProfile::PresentationTerminal' +) 'frame-generation marker preserves canonical FP16' + +Assert-Contains (Join-Path $core 'HdrDiagnostics.h') @( + 'struct HdrDiagnostics', + 'hdrOptionEnabled', + 'captureMethod', + 'sourceFormat', + 'sourceColorDescription', + 'canonicalFormat', + 'selectedAdapterProfile', + 'selectedRouteId', + 'conversionPath', + 'fallbackReason' +) 'HDR diagnostics record' + +Assert-Contains (Join-Path $core 'HdrCaptureProcessor.h') @( + 'Process(', + 'GetCanonicalTexture()', + 'GetFrameMetadata()', + 'ResetForResize()', + 'LastAssumption()' +) 'HdrCaptureProcessor interface' + +Assert-Contains (Join-Path $core 'DLSSNRFilter.cpp') @( + 'const bool hdrPath = hdrEnabled && getParameter("experimentalHdrPath", 0.0f) >= 0.5f;', + 'impl->useResolutionScaling = settings.enableInputResolutionScaling;', + '.preserveHdrRange = impl.experimentalHdrPath ? 1u : 0u,', + 'chroma * chromaScale * directionalMultiplier;', + 'return original + residual;', + 'float3 output = PreserveHdrRange != 0 ? original + residual :' +) 'DLSSNR HDR setting boundary' + +Assert-Contains (Join-Path $core 'GroupBHdrRoutes.cpp') @( + 'MakeRoute("DLSSNR", "experimental-fp16"', + 'HdrAdapterProfile::BoundedHDR', + 'HdrTransferFunction::SRGB' +) 'DLSSNR single FP16 extended-sRGB route' + +Assert-Contains (Join-Path $root 'src\Effects\DLSSNR\DLSSNR_AI_Filter.hlsl') @( + '//!LABEL HDR Processing Path' +) 'DLSSNR HDR path selector kept' + +Assert-Contains (Join-Path $root 'src\Effects\DLSSNR\DLSSNR_AI_Filter.hlsl') @( + '//!LABEL NR Intensity', + '//!LABEL Local Tone Strength', + '//!LABEL Local Structure Strength', + '//!MAX 2' +) 'DLSSNR detail controls expose the signed-snippet range' + +Assert-Contains (Join-Path $core 'DLSSNRFilter.cpp') @( + 'getClamped("intensity", 1.0f, 0.0f, 2.0f)', + 'getClamped("localToneStrength", 1.0f, 0.0f, 2.0f)', + '"localStructureStrength", 1.0f, 0.0f, 2.0f)' +) 'DLSSNR settings preserve the signed-snippet strength range' + +Assert-Contains (Join-Path $core 'NativeEffectBackendFactory.cpp') @( + 'if (!hdrEnabled) {', + 'if (IsSuperResolutionEffect(effectName)) {', + 'const DLSSNRSettings settings = ParseDLSSNRSettings(option, hdrEnabled);' +) 'Native backend SDR factory path' + +Assert-Contains (Join-Path $core 'Renderer.cpp') @( + 'if (!hdrEnabled) {', + 'return {};' +) 'Renderer HDR route global gate' + +Assert-Contains (Join-Path $core 'XeSSUpscaler.cpp') @( + 'if (!hdrEnabled) {', + 'inputDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM ||', + 'outputDesc.Format != DXGI_FORMAT_R8G8B8A8_UNORM', + '(impl->hdrEnabled ? COLOR_CONVERT_HLSL : COLOR_CONVERT_LDR_HLSL)', + 'initParams.initFlags |= XESS_INIT_FLAG_LDR_INPUT_COLOR;' +) 'XeSS SDR U8 contract' + +Assert-Contains (Join-Path $core 'XeSSFGPresenter.cpp') @( + 'return hdr ? HDR_COLOR_FORMAT : LDR_COLOR_FORMAT;', + 'return hdr ? OVERLAY_FORMAT : LDR_COLOR_FORMAT;', + 'if (impl->hdrEnabled) {', + 'DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020' +) 'XeSSFG terminal format boundary' + +Assert-Contains (Join-Path $core 'FSR2Upscaler.cpp') @( + 'if (_hdrProtocol.hdrColorInput) {', + '_exposure ? L"FSR2_Exposure" : L"FSR2_AutoExposure"' +) 'FSR2 exposure boundary' + +Assert-Contains (Join-Path $core 'RTXVideoDenoiser.cpp') @( + 'if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) {', + 'impl->inputScale = isFloatFormat(inputDesc.Format) ? 255.0f : 1.0f;', + 'if (!_hdrBoundary.hdrEnabled) {' +) 'RTX Video SDR U8 boundary' + +Assert-Contains (Join-Path $core 'include\EffectCompiler.h') @( + 'static constexpr uint32_t HdrCompatibility = 1u << 12;' +) 'Effect cache HDR variant flag' + +Assert-Contains (Join-Path $core 'EffectCompiler.cpp') @( + 'macros.emplace_back("MP_HDR_SATURATE",', + 'macros.emplace_back("MP_HDR_ALPHA",', + 'macros.emplace_back("MP_HDR_COMPATIBILITY", "1");' +) 'Effect shader HDR variants' + +Assert-Contains (Join-Path $core 'NvidiaOpticalFlowProvider.cpp') @( + 'if (!hdrEnabled) {', + 'if (!HasFormat(inputFormats, DXGI_FORMAT_B8G8R8A8_UNORM)) {', + 'inputDxgiFormat = DXGI_FORMAT_B8G8R8A8_UNORM;' +) 'NVIDIA Optical Flow SDR input contract' + +Assert-Contains (Join-Path $core 'PassThroughFrames.cpp') @( + 'constexpr char REFERENCE_LDR_HLSL[]', + 'constexpr char REFERENCE_HDR_HLSL[]', + 'hdrEnabled ? REFERENCE_HDR_HLSL : REFERENCE_LDR_HLSL' +) 'Pass-through SDR shader contract' + +Write-Host '' +Write-Host 'All HDR mechanical source-level validation checks passed.' diff --git a/src/Common.Post.props b/src/Common.Post.props index f7fbe6add..6c5552395 100644 --- a/src/Common.Post.props +++ b/src/Common.Post.props @@ -113,7 +113,7 @@ - + diff --git a/src/Effects/DLSSNR/DLSSNR_AI_Filter.hlsl b/src/Effects/DLSSNR/DLSSNR_AI_Filter.hlsl index e8fa99f9a..6c22abf65 100644 --- a/src/Effects/DLSSNR/DLSSNR_AI_Filter.hlsl +++ b/src/Effects/DLSSNR/DLSSNR_AI_Filter.hlsl @@ -5,6 +5,14 @@ //!VERSION 4 //!SORT_NAME DLSSNR AI Filter (Experimental) +//!PARAMETER +//!GROUP HDR Protocol +//!LABEL HDR Processing Path +//!DEFAULT 0 +//!OPTION 0 SDR RGBA8 compatibility +//!OPTION 1 Experimental FP16 value-domain path +int experimentalHdrPath; + //!PARAMETER //!GROUP Detail Control //!LABEL Adjust Input Resolution\n(Reduces DLSSNR Quality) @@ -94,7 +102,7 @@ int style; //!LABEL NR Intensity //!DEFAULT 1 //!MIN 0 -//!MAX 1 +//!MAX 2 //!STEP 0.05 float intensity; @@ -103,7 +111,7 @@ float intensity; //!LABEL Local Tone Strength //!DEFAULT 1 //!MIN 0 -//!MAX 1 +//!MAX 2 //!STEP 0.05 float localToneStrength; @@ -112,7 +120,7 @@ float localToneStrength; //!LABEL Local Structure Strength //!DEFAULT 1 //!MIN 0 -//!MAX 1 +//!MAX 2 //!STEP 0.05 float localStructureStrength; diff --git a/src/Effects/Effects.vcxproj b/src/Effects/Effects.vcxproj index c6c3a0c79..b1abe2089 100644 --- a/src/Effects/Effects.vcxproj +++ b/src/Effects/Effects.vcxproj @@ -4,8 +4,8 @@ Win32Proj {62503530-b84b-4cc2-80b6-3f89618172b7} 10.0.26100.0 - $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ - $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(BuildRoot)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(BuildRoot)\bin\$(Platform)\$(Configuration)\ diff --git a/src/Effects/MLAA/MLAA.hlsl b/src/Effects/MLAA/MLAA.hlsl index b34c7bc3c..5c2155ca3 100644 --- a/src/Effects/MLAA/MLAA.hlsl +++ b/src/Effects/MLAA/MLAA.hlsl @@ -102,6 +102,12 @@ float2 Pass1(float2 pos) { return float2(abs(center - upper), abs(center - right)) > threshold; } +//!PASS 2 +//!DESC Morphological line search +//!STYLE PS +//!IN INPUT, edgeMask +//!OUT edgeCounts + float LoadEdge(int2 p, uint channel) { return edgeMask.Load(int3(ClampPos(p), 0))[channel]; } @@ -118,39 +124,21 @@ float2 SearchEdge(int2 p, int2 negativeDir, int2 positiveDir, uint channel) { bool positiveActive = true; bool negativeEnd = false; bool positiveEnd = false; - [unroll] for (uint i = 1; i <= MAX_EDGE_LENGTH; ++i) { if (negativeActive) { - if (LoadEdge(p + negativeDir * int(i), channel) > 0.5) { - ++negativeCount; - } else { - negativeActive = false; - negativeEnd = true; - } + if (LoadEdge(p + negativeDir * int(i), channel) > 0.5) ++negativeCount; + else { negativeActive = false; negativeEnd = true; } } - if (positiveActive) { - if (LoadEdge(p + positiveDir * int(i), channel) > 0.5) { - ++positiveCount; - } else { - positiveActive = false; - positiveEnd = true; - } + if (LoadEdge(p + positiveDir * int(i), channel) > 0.5) ++positiveCount; + else { positiveActive = false; positiveEnd = true; } } } - - return float2( - EncodeCount(negativeCount, negativeEnd), + return float2(EncodeCount(negativeCount, negativeEnd), EncodeCount(positiveCount, positiveEnd)); } -//!PASS 2 -//!DESC Morphological line search -//!STYLE PS -//!IN edgeMask -//!OUT edgeCounts - float4 Pass2(float2 pos) { const int2 p = PixelPos(pos); const float2 edges = edgeMask.Load(int3(p, 0)).rg; @@ -239,6 +227,54 @@ void BlendEdge( //!IN INPUT, edgeCounts //!OUT OUTPUT +// Pass 3 is compiled as an independent shader. Keep its helpers in this +// pass section so the effect compiler includes them in the generated source. +uint DecodeRawPass3(float value) { + return (uint)round(saturate(value) * 15.0); +} + +bool ColorsDifferPass3(int2 a, int2 b) { + return abs(LoadLuma(a) - LoadLuma(b)) > threshold; +} + +void BlendEdgePass3(float2 encodedCounts, int2 edgePos, int2 acrossEdge, + int2 alongEdge, bool inverse, inout float4 color) { + const uint rawNegative = DecodeRawPass3(encodedCounts.x); + const uint rawPositive = DecodeRawPass3(encodedCounts.y); + const bool negativeEnd = rawNegative >= 8; + const bool positiveEnd = rawPositive >= 8; + if (!negativeEnd && !positiveEnd) return; + uint negativeCount = rawNegative & 7; + uint positiveCount = rawPositive & 7; + const float4 adjacent = LoadColor(edgePos + acrossEdge); + if (negativeCount + positiveCount == 0) { + const float weight = 0.125 * strength; + color.rgb = sqrt(lerp(color.rgb * color.rgb, adjacent.rgb * adjacent.rgb, weight)); + return; + } + if (!positiveEnd) positiveCount = MAX_EDGE_LENGTH + 1; + if (!negativeEnd) negativeCount = MAX_EDGE_LENGTH + 1; + const float length = negativeCount + positiveCount + 1.0; + const float midpoint = length * 0.5; + const float distance = negativeCount; + uint shape = 0; + if (ColorsDifferPass3(edgePos - alongEdge * int(negativeCount), + edgePos - alongEdge * int(negativeCount + 1))) shape |= 1; + if (ColorsDifferPass3(edgePos + alongEdge * int(positiveCount), + edgePos + alongEdge * int(positiveCount + 1))) shape |= 2; + const bool shouldBlend = + (inverse && ((shape == 2 && float(negativeCount) <= midpoint) || + (shape == 1 && float(negativeCount) >= midpoint) || shape == 0)) || + (!inverse && ((shape == 2 && float(negativeCount) >= midpoint) || + (shape == 1 && float(negativeCount) <= midpoint) || shape == 3)); + if (shouldBlend) { + const float h0 = abs((length - distance) / length - 0.5); + const float h1 = abs((length - distance - 1.0) / length - 0.5); + const float area = 0.5 * (h0 + h1) * strength; + color.rgb = sqrt(lerp(color.rgb * color.rgb, adjacent.rgb * adjacent.rgb, area)); + } +} + float4 Pass3(float2 pos) { const int2 p = PixelPos(pos); float4 color = LoadColor(p); @@ -250,16 +286,16 @@ float4 Pass3(float2 pos) { const float2 leftVertical = edgeCounts.Load(int3(left, 0)).zw; if (any(current.xy > 0)) { - BlendEdge(current.xy, p, int2(0, -1), int2(1, 0), false, color); + BlendEdgePass3(current.xy, p, int2(0, -1), int2(1, 0), false, color); } if (any(belowHorizontal > 0)) { - BlendEdge(belowHorizontal, below, int2(0, 1), int2(1, 0), true, color); + BlendEdgePass3(belowHorizontal, below, int2(0, 1), int2(1, 0), true, color); } if (any(current.zw > 0)) { - BlendEdge(current.zw, p, int2(1, 0), int2(0, -1), false, color); + BlendEdgePass3(current.zw, p, int2(1, 0), int2(0, -1), false, color); } if (any(leftVertical > 0)) { - BlendEdge(leftVertical, left, int2(-1, 0), int2(0, -1), true, color); + BlendEdgePass3(leftVertical, left, int2(-1, 0), int2(0, -1), true, color); } return color; diff --git a/src/Effects/NNEDI3/NNEDI3_nns128_win8x4.hlsl b/src/Effects/NNEDI3/NNEDI3_nns128_win8x4.hlsl index b92930a72..3321edab3 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns128_win8x4.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns128_win8x4.hlsl @@ -3951,7 +3951,7 @@ shared float inp[525]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns128_win8x6.hlsl b/src/Effects/NNEDI3/NNEDI3_nns128_win8x6.hlsl index eea99b9a0..7e243f5fd 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns128_win8x6.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns128_win8x6.hlsl @@ -5646,7 +5646,7 @@ shared float inp[555]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns16_win8x4.hlsl b/src/Effects/NNEDI3/NNEDI3_nns16_win8x4.hlsl index 332ecf3a8..384752f5f 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns16_win8x4.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns16_win8x4.hlsl @@ -659,7 +659,7 @@ shared float inp[525]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns16_win8x6.hlsl b/src/Effects/NNEDI3/NNEDI3_nns16_win8x6.hlsl index 63a72ce62..4799ef105 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns16_win8x6.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns16_win8x6.hlsl @@ -864,7 +864,7 @@ shared float inp[555]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns256_win8x4.hlsl b/src/Effects/NNEDI3/NNEDI3_nns256_win8x4.hlsl index f87f33283..ad5ff4149 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns256_win8x4.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns256_win8x4.hlsl @@ -7807,7 +7807,7 @@ shared float inp[525]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns256_win8x6.hlsl b/src/Effects/NNEDI3/NNEDI3_nns256_win8x6.hlsl index 72b14b33e..951fba454 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns256_win8x6.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns256_win8x6.hlsl @@ -11143,7 +11143,7 @@ shared float inp[555]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns32_win8x4.hlsl b/src/Effects/NNEDI3/NNEDI3_nns32_win8x4.hlsl index 9176e3041..68e351c31 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns32_win8x4.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns32_win8x4.hlsl @@ -1120,7 +1120,7 @@ shared float inp[525]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns32_win8x6.hlsl b/src/Effects/NNEDI3/NNEDI3_nns32_win8x6.hlsl index b5a85a38c..4d6a27e42 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns32_win8x6.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns32_win8x6.hlsl @@ -1554,7 +1554,7 @@ shared float inp[555]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns64_win8x4.hlsl b/src/Effects/NNEDI3/NNEDI3_nns64_win8x4.hlsl index 146aa6ed0..25300b657 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns64_win8x4.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns64_win8x4.hlsl @@ -2038,7 +2038,7 @@ shared float inp[525]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/NNEDI3/NNEDI3_nns64_win8x6.hlsl b/src/Effects/NNEDI3/NNEDI3_nns64_win8x6.hlsl index fb394f929..23861e58f 100644 --- a/src/Effects/NNEDI3/NNEDI3_nns64_win8x6.hlsl +++ b/src/Effects/NNEDI3/NNEDI3_nns64_win8x6.hlsl @@ -2915,7 +2915,7 @@ shared float inp[555]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/Pixel Art/MMPX.hlsl b/src/Effects/Pixel Art/MMPX.hlsl index 6bce8b6df..e86b682fb 100644 --- a/src/Effects/Pixel Art/MMPX.hlsl +++ b/src/Effects/Pixel Art/MMPX.hlsl @@ -64,6 +64,11 @@ void Pass1(uint2 blockStart, uint3 threadId) { float srcX = (gxy.x >> 1) + 0.5f; float srcY = (gxy.y >> 1) + 0.5f; + // MMPX makes RGB-only decisions. Preserve the source pixel alpha across + // the four generated subpixels instead of manufacturing an opaque result. + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, float2(srcX, srcY) * GetInputPt(), 0).a; + #endif float3 A = src(srcX - 1, srcY - 1), B = src(srcX, srcY - 1), C = src(srcX + 1, srcY - 1); float3 D = src(srcX - 1, srcY + 0), E = src(srcX, srcY + 0), F = src(srcX + 1, srcY + 0); @@ -119,14 +124,14 @@ void Pass1(uint2 blockStart, uint3 threadId) { } // not constant // Write four pixels at once - OUTPUT[gxy] = float4(J, 1); + OUTPUT[gxy] = float4(J, MP_HDR_ALPHA); ++gxy.x; - OUTPUT[gxy] = float4(K, 1); + OUTPUT[gxy] = float4(K, MP_HDR_ALPHA); ++gxy.y; - OUTPUT[gxy] = float4(M, 1); + OUTPUT[gxy] = float4(M, MP_HDR_ALPHA); --gxy.x; - OUTPUT[gxy] = float4(L, 1); + OUTPUT[gxy] = float4(L, MP_HDR_ALPHA); } diff --git a/src/Effects/Pixel Art/Pixellate.hlsl b/src/Effects/Pixel Art/Pixellate.hlsl index ded35889c..e95166ab8 100644 --- a/src/Effects/Pixel Art/Pixellate.hlsl +++ b/src/Effects/Pixel Art/Pixellate.hlsl @@ -30,20 +30,43 @@ float4 Pass1(float2 pos) { float right = pos.x + range.x; float bottom = pos.y - range.y; + #ifdef MP_HDR_COMPATIBILITY + float4 topLeft = INPUT.SampleLevel(sam, (floor(float2(left, top) / texelSize) + 0.5) * texelSize, 0); + float4 bottomRight = INPUT.SampleLevel(sam, (floor(float2(right, bottom) / texelSize) + 0.5) * texelSize, 0); + float4 bottomLeft = INPUT.SampleLevel(sam, (floor(float2(left, bottom) / texelSize) + 0.5) * texelSize, 0); + float4 topRight = INPUT.SampleLevel(sam, (floor(float2(right, top) / texelSize) + 0.5) * texelSize, 0); + #else float3 topLeftColor = INPUT.SampleLevel(sam, (floor(float2(left, top) / texelSize) + 0.5) * texelSize, 0).rgb; float3 bottomRightColor = INPUT.SampleLevel(sam, (floor(float2(right, bottom) / texelSize) + 0.5) * texelSize, 0).rgb; float3 bottomLeftColor = INPUT.SampleLevel(sam, (floor(float2(left, bottom) / texelSize) + 0.5) * texelSize, 0).rgb; float3 topRightColor = INPUT.SampleLevel(sam, (floor(float2(right, top) / texelSize) + 0.5) * texelSize, 0).rgb; + #endif float2 border = clamp(round(pos / texelSize) * texelSize, float2(left, bottom), float2(right, top)); float totalArea = 4.0 * range.x * range.y; float3 averageColor; + #ifdef MP_HDR_COMPATIBILITY + float averageAlpha; + const float topLeftWeight = (border.x - left) * (top - border.y) / totalArea; + const float bottomRightWeight = (right - border.x) * (border.y - bottom) / totalArea; + const float bottomLeftWeight = (border.x - left) * (border.y - bottom) / totalArea; + const float topRightWeight = (right - border.x) * (top - border.y) / totalArea; + averageColor = topLeftWeight * topLeft.rgb; + averageColor += bottomRightWeight * bottomRight.rgb; + averageColor += bottomLeftWeight * bottomLeft.rgb; + averageColor += topRightWeight * topRight.rgb; + averageAlpha = topLeftWeight * topLeft.a; + averageAlpha += bottomRightWeight * bottomRight.a; + averageAlpha += bottomLeftWeight * bottomLeft.a; + averageAlpha += topRightWeight * topRight.a; + return float4(averageColor, saturate(averageAlpha)); + #else averageColor = ((border.x - left) * (top - border.y) / totalArea) * topLeftColor; averageColor += ((right - border.x) * (border.y - bottom) / totalArea) * bottomRightColor; averageColor += ((border.x - left) * (border.y - bottom) / totalArea) * bottomLeftColor; averageColor += ((right - border.x) * (top - border.y) / totalArea) * topRightColor; - return float4(averageColor, 1.0); + #endif } diff --git a/src/Effects/RAVU/RAVU_3x_R2.hlsl b/src/Effects/RAVU/RAVU_3x_R2.hlsl index c834cc986..88942bdd7 100644 --- a/src/Effects/RAVU/RAVU_3x_R2.hlsl +++ b/src/Effects/RAVU/RAVU_3x_R2.hlsl @@ -62,7 +62,7 @@ shared float inp[340]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_3x_R3.hlsl b/src/Effects/RAVU/RAVU_3x_R3.hlsl index e78eb9260..cd2a055bb 100644 --- a/src/Effects/RAVU/RAVU_3x_R3.hlsl +++ b/src/Effects/RAVU/RAVU_3x_R3.hlsl @@ -62,7 +62,7 @@ shared float inp[432]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_3x_R4.hlsl b/src/Effects/RAVU/RAVU_3x_R4.hlsl index 97b1e223b..39089bc9b 100644 --- a/src/Effects/RAVU/RAVU_3x_R4.hlsl +++ b/src/Effects/RAVU/RAVU_3x_R4.hlsl @@ -62,7 +62,7 @@ shared float inp[532]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Lite_AR_R2.hlsl b/src/Effects/RAVU/RAVU_Lite_AR_R2.hlsl index 473527c6b..fd3172d24 100644 --- a/src/Effects/RAVU/RAVU_Lite_AR_R2.hlsl +++ b/src/Effects/RAVU/RAVU_Lite_AR_R2.hlsl @@ -62,7 +62,7 @@ shared float inp[340]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Lite_AR_R3.hlsl b/src/Effects/RAVU/RAVU_Lite_AR_R3.hlsl index 0f69785d8..48dc0b4b0 100644 --- a/src/Effects/RAVU/RAVU_Lite_AR_R3.hlsl +++ b/src/Effects/RAVU/RAVU_Lite_AR_R3.hlsl @@ -62,7 +62,7 @@ shared float inp[432]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Lite_AR_R4.hlsl b/src/Effects/RAVU/RAVU_Lite_AR_R4.hlsl index 243b46d0d..6c4b0fffa 100644 --- a/src/Effects/RAVU/RAVU_Lite_AR_R4.hlsl +++ b/src/Effects/RAVU/RAVU_Lite_AR_R4.hlsl @@ -62,7 +62,7 @@ shared float inp[532]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Lite_R2.hlsl b/src/Effects/RAVU/RAVU_Lite_R2.hlsl index cb62cb20c..f79efd2bd 100644 --- a/src/Effects/RAVU/RAVU_Lite_R2.hlsl +++ b/src/Effects/RAVU/RAVU_Lite_R2.hlsl @@ -62,7 +62,7 @@ shared float inp[340]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Lite_R3.hlsl b/src/Effects/RAVU/RAVU_Lite_R3.hlsl index 467a5b112..991c56724 100644 --- a/src/Effects/RAVU/RAVU_Lite_R3.hlsl +++ b/src/Effects/RAVU/RAVU_Lite_R3.hlsl @@ -62,7 +62,7 @@ shared float inp[432]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Lite_R4.hlsl b/src/Effects/RAVU/RAVU_Lite_R4.hlsl index ab6db274e..1556f8cd3 100644 --- a/src/Effects/RAVU/RAVU_Lite_R4.hlsl +++ b/src/Effects/RAVU/RAVU_Lite_R4.hlsl @@ -62,7 +62,7 @@ shared float inp[532]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_R2.hlsl b/src/Effects/RAVU/RAVU_R2.hlsl index 4ffc8faa5..5e4f8aa2f 100644 --- a/src/Effects/RAVU/RAVU_R2.hlsl +++ b/src/Effects/RAVU/RAVU_R2.hlsl @@ -210,7 +210,7 @@ shared float inp1[385]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_R3.hlsl b/src/Effects/RAVU/RAVU_R3.hlsl index e4b847055..4169c9628 100644 --- a/src/Effects/RAVU/RAVU_R3.hlsl +++ b/src/Effects/RAVU/RAVU_R3.hlsl @@ -239,7 +239,7 @@ shared float inp1[481]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_R4.hlsl b/src/Effects/RAVU/RAVU_R4.hlsl index 79104920f..f5ad8e746 100644 --- a/src/Effects/RAVU/RAVU_R4.hlsl +++ b/src/Effects/RAVU/RAVU_R4.hlsl @@ -344,7 +344,7 @@ shared float inp1[585]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Zoom_AR_R2.hlsl b/src/Effects/RAVU/RAVU_Zoom_AR_R2.hlsl index dba8b22ec..8e0e81008 100644 --- a/src/Effects/RAVU/RAVU_Zoom_AR_R2.hlsl +++ b/src/Effects/RAVU/RAVU_Zoom_AR_R2.hlsl @@ -72,7 +72,7 @@ shared float samples[432]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Zoom_AR_R3.hlsl b/src/Effects/RAVU/RAVU_Zoom_AR_R3.hlsl index 99ba60220..a0eb93230 100644 --- a/src/Effects/RAVU/RAVU_Zoom_AR_R3.hlsl +++ b/src/Effects/RAVU/RAVU_Zoom_AR_R3.hlsl @@ -72,7 +72,7 @@ shared float samples[532]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Zoom_R2.hlsl b/src/Effects/RAVU/RAVU_Zoom_R2.hlsl index 408a39da0..a2170de4a 100644 --- a/src/Effects/RAVU/RAVU_Zoom_R2.hlsl +++ b/src/Effects/RAVU/RAVU_Zoom_R2.hlsl @@ -63,7 +63,7 @@ shared float samples[432]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/RAVU/RAVU_Zoom_R3.hlsl b/src/Effects/RAVU/RAVU_Zoom_R3.hlsl index 8a9b9b8c0..44a81f73c 100644 --- a/src/Effects/RAVU/RAVU_Zoom_R3.hlsl +++ b/src/Effects/RAVU/RAVU_Zoom_R3.hlsl @@ -63,7 +63,7 @@ shared float samples[532]; #define imageStore(out_image, pos, val) imageStoreOverride(pos, val.x) void imageStoreOverride(uint2 pos, float value) { float2 UV = mul(rgb2uv, INPUT.SampleLevel(sam_INPUT_LINEAR, HOOKED_map(pos), 0).rgb); - OUTPUT[pos] = float4(mul(yuv2rgb, float3(value.x, UV)), 1.0); + OUTPUT[pos] = float4(MP_HDR_SATURATE(mul(yuv2rgb, float3(value.x, UV))), 1.0); } #define INPUT_tex(pos) GET_SAMPLE(vec4(texture(INPUT, pos))) diff --git a/src/Effects/xBRZ/xBRZ_2x.hlsl b/src/Effects/xBRZ/xBRZ_2x.hlsl index b6a066709..ca8ea132e 100644 --- a/src/Effects/xBRZ/xBRZ_2x.hlsl +++ b/src/Effects/xBRZ/xBRZ_2x.hlsl @@ -98,6 +98,9 @@ void Pass1(uint2 blockStart, uint3 threadId) { const float2 inputPt = GetInputPt(); const float2 pos = ((gxy >> 1) + 0.5f) * inputPt; + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, pos, 0).a; + #endif // A1 B1 C1 // A0 A B C C4 @@ -287,14 +290,14 @@ void Pass1(uint2 blockStart, uint3 threadId) { dst[0] = tempDst3; } - OUTPUT[gxy] = float4(dst[0], 1); + OUTPUT[gxy] = float4(dst[0], MP_HDR_ALPHA); ++gxy.x; - OUTPUT[gxy] = float4(dst[1], 1); + OUTPUT[gxy] = float4(dst[1], MP_HDR_ALPHA); ++gxy.y; - OUTPUT[gxy] = float4(dst[2], 1); + OUTPUT[gxy] = float4(dst[2], MP_HDR_ALPHA); --gxy.x; - OUTPUT[gxy] = float4(dst[3], 1); + OUTPUT[gxy] = float4(dst[3], MP_HDR_ALPHA); } diff --git a/src/Effects/xBRZ/xBRZ_3x.hlsl b/src/Effects/xBRZ/xBRZ_3x.hlsl index 52eb53560..12c81fc47 100644 --- a/src/Effects/xBRZ/xBRZ_3x.hlsl +++ b/src/Effects/xBRZ/xBRZ_3x.hlsl @@ -109,6 +109,9 @@ void Pass1(uint2 blockStart, uint3 threadId) { const float2 inputPt = GetInputPt(); const float2 pos = ((gxy / 3) + 0.5f) * inputPt; + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, pos, 0).a; + #endif // A1 B1 C1 // A0 A B C C4 @@ -328,7 +331,7 @@ void Pass1(uint2 blockStart, uint3 threadId) { for (uint i = 0; i < 3; ++i) { [unroll] for (uint j = 0; j < 3; ++j) { - OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], 1); + OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], MP_HDR_ALPHA); } } } diff --git a/src/Effects/xBRZ/xBRZ_4x.hlsl b/src/Effects/xBRZ/xBRZ_4x.hlsl index 79ab5377e..46426efca 100644 --- a/src/Effects/xBRZ/xBRZ_4x.hlsl +++ b/src/Effects/xBRZ/xBRZ_4x.hlsl @@ -83,6 +83,9 @@ void Pass1(uint2 blockStart, uint3 threadId) { const float2 inputPt = GetInputPt(); const float2 pos = ((gxy >> 2) + 0.5f) * inputPt; + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, pos, 0).a; + #endif // A1 B1 C1 // A0 A B C C4 @@ -312,7 +315,7 @@ void Pass1(uint2 blockStart, uint3 threadId) { for (uint i = 0; i < 4; ++i) { [unroll] for (uint j = 0; j < 4; ++j) { - OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], 1); + OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], MP_HDR_ALPHA); } } } diff --git a/src/Effects/xBRZ/xBRZ_5x.hlsl b/src/Effects/xBRZ/xBRZ_5x.hlsl index a52a31fca..f6554ad27 100644 --- a/src/Effects/xBRZ/xBRZ_5x.hlsl +++ b/src/Effects/xBRZ/xBRZ_5x.hlsl @@ -85,6 +85,9 @@ void Pass1(uint2 blockStart, uint3 threadId) { const float2 inputPt = GetInputPt(); const float2 pos = ((gxy / 5) + 0.5f) * inputPt; + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, pos, 0).a; + #endif // A1 B1 C1 // A0 A B C C4 @@ -339,7 +342,7 @@ void Pass1(uint2 blockStart, uint3 threadId) { for (uint i = 0; i < 5; ++i) { [unroll] for (uint j = 0; j < 5; ++j) { - OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], 1); + OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], MP_HDR_ALPHA); } } } diff --git a/src/Effects/xBRZ/xBRZ_6x.hlsl b/src/Effects/xBRZ/xBRZ_6x.hlsl index 03f20dd1e..4b58e7904 100644 --- a/src/Effects/xBRZ/xBRZ_6x.hlsl +++ b/src/Effects/xBRZ/xBRZ_6x.hlsl @@ -91,6 +91,9 @@ void Pass1(uint2 blockStart, uint3 threadId) { const float2 inputPt = GetInputPt(); const float2 pos = ((gxy / 6) + 0.5f) * inputPt; + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, pos, 0).a; + #endif // A1 B1 C1 // A0 A B C C4 @@ -371,7 +374,7 @@ void Pass1(uint2 blockStart, uint3 threadId) { for (uint i = 0; i < 6; ++i) { [unroll] for (uint j = 0; j < 6; ++j) { - OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], 1); + OUTPUT[gxy + uint2(i, j)] = float4(dst[destIdx[j][i]], MP_HDR_ALPHA); } } } diff --git a/src/Effects/xBRZ/xBRZ_Freescale.hlsl b/src/Effects/xBRZ/xBRZ_Freescale.hlsl index b0afe789c..2add7b5c5 100644 --- a/src/Effects/xBRZ/xBRZ_Freescale.hlsl +++ b/src/Effects/xBRZ/xBRZ_Freescale.hlsl @@ -292,6 +292,9 @@ void Pass2(uint2 blockStart, uint3 threadId) { float2 inputPt = GetInputPt(); float2 outputPt = GetOutputPt(); float2 pos = (gxy + 0.5f) * outputPt; + #ifdef MP_HDR_COMPATIBILITY + const float sourceAlpha = INPUT.SampleLevel(sam, pos, 0).a; + #endif //--------------------------------------- // Input Pixel Mapping: -|B|- @@ -385,5 +388,5 @@ void Pass2(uint2 blockStart, uint3 threadId) { res = lerp(res, blendPix, get_left_ratio(f, origin, direction, scale)); } - OUTPUT[gxy] = float4(res, 1); + OUTPUT[gxy] = float4(res, MP_HDR_ALPHA); } diff --git a/src/Magpie.Core/AdaptivePresenter.cpp b/src/Magpie.Core/AdaptivePresenter.cpp index 128e02bdb..c8ba3a923 100644 --- a/src/Magpie.Core/AdaptivePresenter.cpp +++ b/src/Magpie.Core/AdaptivePresenter.cpp @@ -33,7 +33,9 @@ bool AdaptivePresenter::_Initialize(HWND hwndAttach) noexcept { DXGI_SWAP_CHAIN_DESC1 sd{ .Width = (UINT)rendererSize.cx, .Height = (UINT)rendererSize.cy, - .Format = DXGI_FORMAT_R8G8B8A8_UNORM, + .Format = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? DXGI_FORMAT_R16G16B16A16_FLOAT + : DXGI_FORMAT_R8G8B8A8_UNORM, .SampleDesc = { .Count = 1 }, @@ -68,12 +70,18 @@ bool AdaptivePresenter::_Initialize(HWND hwndAttach) noexcept { Logger::Get().ComError("创建交换链失败", hr); return false; } - _dxgiSwapChain = dxgiSwapChain.try_as(); if (!_dxgiSwapChain) { Logger::Get().Error("获取 IDXGISwapChain2 失败"); return false; } + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) { + hr = _dxgiSwapChain->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709); + if (FAILED(hr)) { + Logger::Get().ComError("设置 HDR 交换链色彩空间失败", hr); + return false; + } + } const auto& options = ScalingWindow::Get().Options(); uint32_t maximumFrameLatency = options.isFrontEdgeSyncEnabled && !options.IsBenchmarkMode() @@ -408,7 +416,9 @@ bool AdaptivePresenter::_ResizeDCompVisual(HWND hwndAttach) noexcept { hr = _dcompDevice->CreateVirtualSurface( (UINT)rendererSize.cx, (UINT)rendererSize.cy, - DXGI_FORMAT_R8G8B8A8_UNORM, + ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? DXGI_FORMAT_R16G16B16A16_FLOAT + : DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ALPHA_MODE_IGNORE, _dcompSurface.put() ); diff --git a/src/Magpie.Core/AmdOpticalFlowProvider.cpp b/src/Magpie.Core/AmdOpticalFlowProvider.cpp index 3403241a2..62f03d7cc 100644 --- a/src/Magpie.Core/AmdOpticalFlowProvider.cpp +++ b/src/Magpie.Core/AmdOpticalFlowProvider.cpp @@ -76,6 +76,9 @@ FrameGuidanceMetadata MakeMetadata( ) noexcept { return { .frameId = frame.frameId, + .captureSequence = frame.captureSequence, + .resourceGeneration = frame.resourceGeneration, + .timestamp100ns = frame.timestamp100ns, .sourceExtent = frame.sourceExtent, .validRegion = frame.validRegion, .resetReason = reason, @@ -496,6 +499,7 @@ struct AmdOpticalFlowProvider::Impl { FrameGuidanceExtent opticalFlowExtent{}; FrameGuidanceExtent sparseExtent{}; AmdOpticalFlowMode mode = AmdOpticalFlowMode::Quality; + AmdOpticalFlowHdrProtocol hdrProtocol{}; FrameGuidanceResetReason resetReason = FrameGuidanceResetReason::Initialize; uint64_t fenceValue = 0; uint64_t lastSubmittedValue = 0; @@ -515,6 +519,7 @@ bool AmdOpticalFlowProvider::Initialize( DeviceResources& resources, FrameGuidanceExtent sourceExtent ) noexcept { + _impl->hdrProtocol = _hdrProtocol; return _impl->Create(resources, sourceExtent, _mode); } @@ -556,8 +561,13 @@ bool AmdOpticalFlowProvider::BeginFrame( L"Magpie AMD OF SCD", FFX_API_RESOURCE_STATE_COMMON), .reset = !impl.historyValid || impl.resetReason != FrameGuidanceResetReason::None, - .backbufferTransferFunction = FFX_API_BACKBUFFER_TRANSFER_FUNCTION_SRGB, - .minMaxLuminance = { 0.0f, 1.0f } + .backbufferTransferFunction = +#ifdef FFX_API_BACKBUFFER_TRANSFER_FUNCTION_LINEAR + impl.hdrProtocol.transfer == GroupBTransfer::Linear ? + FFX_API_BACKBUFFER_TRANSFER_FUNCTION_LINEAR : +#endif + FFX_API_BACKBUFFER_TRANSFER_FUNCTION_SRGB, + .minMaxLuminance = { impl.hdrProtocol.minMaxLuminance[0], impl.hdrProtocol.minMaxLuminance[1] } }; const auto opticalFlowStart = std::chrono::steady_clock::now(); if (ffxOpticalflowContextDispatch( diff --git a/src/Magpie.Core/AmdOpticalFlowProvider.h b/src/Magpie.Core/AmdOpticalFlowProvider.h index 8cf2a3141..008b97f5e 100644 --- a/src/Magpie.Core/AmdOpticalFlowProvider.h +++ b/src/Magpie.Core/AmdOpticalFlowProvider.h @@ -1,5 +1,6 @@ #pragma once #include "FrameGuidanceProvider.h" +#include "GroupBEffectProtocol.h" namespace Magpie { @@ -27,11 +28,13 @@ class AmdOpticalFlowProvider final : public IMotionVectorProvider { void Reset(FrameGuidanceResetReason reason) noexcept override; bool Resize(FrameGuidanceExtent sourceExtent) noexcept override; OpticalFlowInitializationError InitializationError() const noexcept override; + void SetHdrProtocol(const AmdOpticalFlowHdrProtocol& protocol) noexcept { _hdrProtocol = protocol; } private: // 仅在 MP_ENABLE_AMD_OPTICAL_FLOW 构建中使用;无 SDK 的 CI 构建里 ClangCL -Werror 会报未使用 [[maybe_unused]] AmdOpticalFlowMode _mode; std::unique_ptr _impl; + AmdOpticalFlowHdrProtocol _hdrProtocol{}; }; } diff --git a/src/Magpie.Core/CompSwapchainPresenter.cpp b/src/Magpie.Core/CompSwapchainPresenter.cpp index 390a7d6d2..d81f26fff 100644 --- a/src/Magpie.Core/CompSwapchainPresenter.cpp +++ b/src/Magpie.Core/CompSwapchainPresenter.cpp @@ -147,7 +147,9 @@ bool CompSwapchainPresenter::BeginFrame( const SIZE rendererSize = Win32Helper::GetSizeOfRect(ScalingWindow::Get().RendererRect()); D3D11_TEXTURE2D_DESC desc{}; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.Format = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? DXGI_FORMAT_R16G16B16A16_FLOAT + : DXGI_FORMAT_R8G8B8A8_UNORM; desc.SampleDesc.Count = 1; desc.MipLevels = 1; desc.ArraySize = 1; diff --git a/src/Magpie.Core/DLSSNRFilter.cpp b/src/Magpie.Core/DLSSNRFilter.cpp index dc59eb180..0de09e5c9 100644 --- a/src/Magpie.Core/DLSSNRFilter.cpp +++ b/src/Magpie.Core/DLSSNRFilter.cpp @@ -11,7 +11,7 @@ namespace Magpie { -DLSSNRSettings ParseDLSSNRSettings(const EffectOption& option) noexcept { +DLSSNRSettings ParseDLSSNRSettings(const EffectOption& option, bool hdrEnabled) noexcept { auto getParameter = [&](std::string_view name, float defaultValue) noexcept { auto it = option.parameters.find(std::string(name)); return it != option.parameters.end() && std::isfinite(it->second) @@ -28,6 +28,7 @@ DLSSNRSettings ParseDLSSNRSettings(const EffectOption& option) noexcept { static_cast(motionQualityValue) : NvidiaOpticalFlowQuality::Balanced; + const bool hdrPath = hdrEnabled && getParameter("experimentalHdrPath", 0.0f) >= 0.5f; return DLSSNRSettings{ .enableInputResolutionScaling = getParameter("enableInputResolutionScaling", 0.0f) >= 0.5f, @@ -43,15 +44,18 @@ DLSSNRSettings ParseDLSSNRSettings(const EffectOption& option) noexcept { "reflectionGlowMultiplier", 1.0f, 0.0f, 2.0f), .style = std::clamp(static_cast(std::lround( getParameter("style", 0.0f))), 0, 2), - .intensity = getClamped("intensity", 1.0f, 0.0f, 1.0f), - .localToneStrength = getClamped("localToneStrength", 1.0f, 0.0f, 1.0f), + .intensity = getClamped("intensity", 1.0f, 0.0f, 2.0f), + .localToneStrength = getClamped("localToneStrength", 1.0f, 0.0f, 2.0f), .localStructureStrength = getClamped( - "localStructureStrength", 1.0f, 0.0f, 1.0f), + "localStructureStrength", 1.0f, 0.0f, 2.0f), .skinStructureStrength = getClamped( "skinStructureStrength", -1.0f, -1.0f, 2.0f), .useAutoMask = getParameter("useAutoMask", 0.0f) >= 0.5f, .uiCorrection = getParameter("uiCorrection", 0.0f) >= 0.5f, - .motionVectorQuality = motionQuality + .motionVectorQuality = motionQuality, + .experimentalHdr = DlssnrExperimentProtocol{ + .enabled = hdrPath + } }; } @@ -163,7 +167,7 @@ RWTexture2D OutputColor : register(u0); cbuffer ResampleParams : register(b0) { uint2 SourceExtent; uint2 TargetExtent; - uint Padding0; + uint PreserveHdrRange; float2 MotionScale; float ResidualMultiplier; float ResidualSaturation; @@ -235,7 +239,7 @@ RWTexture2D OutputConfidence : register(u2); cbuffer ResampleParams : register(b0) { uint2 SourceExtent; uint2 TargetExtent; - uint Padding0; + uint PreserveHdrRange; float2 MotionScale; float ResidualMultiplier; float ResidualSaturation; @@ -298,7 +302,7 @@ RWTexture2D ControlledResidual : register(u0); cbuffer ResampleParams : register(b0) { uint2 SourceExtent; uint2 TargetExtent; - uint Padding0; + uint PreserveHdrRange; float2 MotionScale; float ResidualMultiplier; float ResidualSaturation; @@ -360,6 +364,23 @@ float3 ToLinear(float3 color) { float3 ApplyResidualControls(float3 original, float3 residual) { residual *= ResidualMultiplier; if (all(residual == 0.0)) return original; + if (PreserveHdrRange != 0) { + // HDR residual controls operate on the luma/chroma decomposition of + // the residual itself. Chroma scaling around an unclamped Y keeps the + // operation linear in the FP16 domain: saturation multiplies only the + // zero-Y chroma part, so ResidualSaturation=2 cannot bleed luma into + // chroma channels the way an HSL-style mix around a raw dot product + // did (that variant tinted highlights when non-default controls were + // combined with the 4.5x scRGB white normalization). + float deltaY = dot(residual, float3(0.2126, 0.7152, 0.0722)); + float directionalMultiplier = deltaY < 0.0 ? ShadowStructureMultiplier : + (deltaY > 0.0 ? ReflectionGlowMultiplier : 1.0); + float3 chroma = residual - deltaY; + float chromaScale = saturate(ResidualSaturation); + residual = deltaY * directionalMultiplier * ResidualLightness + + chroma * chromaScale * directionalMultiplier; + return original + residual; + } float4 fineControls = float4( ResidualSaturation, ResidualLightness, ShadowStructureMultiplier, ReflectionGlowMultiplier); @@ -412,7 +433,7 @@ RWTexture2D HorizontalResidual : register(u0); cbuffer ResampleParams : register(b0) { uint2 SourceExtent; uint2 TargetExtent; - uint Padding0; + uint PreserveHdrRange; float2 MotionScale; float ResidualMultiplier; float ResidualSaturation; @@ -462,7 +483,7 @@ RWTexture2D OutputColor : register(u0); cbuffer ResampleParams : register(b0) { uint2 SourceExtent; uint2 TargetExtent; - uint Padding0; + uint PreserveHdrRange; float2 MotionScale; float ResidualMultiplier; float ResidualSaturation; @@ -503,8 +524,9 @@ void CompositeResidualVertical(uint3 tid : SV_DispatchThreadID) { } residual /= abs(totalWeight) > 1e-6 ? totalWeight : 1.0; } - OutputColor[tid.xy] = float4( - saturate(original + residual), storedOriginal.a); + float3 output = PreserveHdrRange != 0 ? original + residual : + saturate(original + residual); + OutputColor[tid.xy] = float4(output, storedOriginal.a); } )"; @@ -513,7 +535,7 @@ struct ResampleConstants { uint32_t sourceHeight = 0; uint32_t targetWidth = 0; uint32_t targetHeight = 0; - uint32_t padding0 = 0; + uint32_t preserveHdrRange = 0; float motionScaleX = 1.0f; float motionScaleY = 1.0f; float residualMultiplier = 1.0f; @@ -681,6 +703,7 @@ struct DLSSNRFilter::Impl { uint32_t width = 0; uint32_t height = 0; bool convertInputToRgba = false; + bool experimentalHdrPath = false; bool useResolutionScaling = false; bool coreRegistered = false; bool snippetInitialized = false; @@ -1056,7 +1079,7 @@ DLSSNRFilter::Impl::~Impl() { if (callerCompatibilityRestored && !NgxRuntimeGuard::IsFaulted()) { if (!FreeLibrary(snippetModule)) { Logger::Get().Win32Error( - "Release DLSSNR signed snippet DLL failed"); + "Release DLSSNR signed snippet DLL failed"); } } else { Logger::Get().Warn( @@ -1466,6 +1489,7 @@ static bool PrepareInput( .sourceHeight = impl.sourceHeight, .targetWidth = impl.width, .targetHeight = impl.height, + .preserveHdrRange = impl.experimentalHdrPath ? 1u : 0u, .motionScaleX = float(impl.width) / float(impl.sourceWidth), .motionScaleY = float(impl.height) / float(impl.sourceHeight) }; @@ -1566,6 +1590,7 @@ static bool PrepareReducedGuidance( .sourceHeight = impl.sourceHeight, .targetWidth = impl.width, .targetHeight = impl.height, + .preserveHdrRange = impl.experimentalHdrPath ? 1u : 0u, .motionScaleX = float(impl.width) / float(impl.sourceWidth), .motionScaleY = float(impl.height) / float(impl.sourceHeight) }; @@ -1671,6 +1696,7 @@ static bool CompositeResidual( .sourceHeight = impl.sourceHeight, .targetWidth = impl.width, .targetHeight = impl.height, + .preserveHdrRange = impl.experimentalHdrPath ? 1u : 0u, .motionScaleX = float(impl.width) / float(impl.sourceWidth), .motionScaleY = float(impl.height) / float(impl.sourceHeight), .residualMultiplier = settings.residualMultiplier, @@ -1788,7 +1814,9 @@ bool DLSSNRFilter::ApplyLiveParameters( return false; } - const DLSSNRSettings candidate = ParseDLSSNRSettings(option); + // Preserve the active HDR protocol while validating live SDR parameters. + const DLSSNRSettings candidate = ParseDLSSNRSettings( + option, _settings.experimentalHdr.enabled); if (candidate.enableInputResolutionScaling != _settings.enableInputResolutionScaling || candidate.inputResolutionPercent != _settings.inputResolutionPercent || @@ -1854,11 +1882,11 @@ bool DLSSNRFilter::Initialize( _settings.reflectionGlowMultiplier = ClampFinite( _settings.reflectionGlowMultiplier, 0.0f, 2.0f, 1.0f); _settings.intensity = ClampFinite( - _settings.intensity, 0.0f, 1.0f, 1.0f); + _settings.intensity, 0.0f, 2.0f, 1.0f); _settings.localToneStrength = ClampFinite( - _settings.localToneStrength, 0.0f, 1.0f, 1.0f); + _settings.localToneStrength, 0.0f, 2.0f, 1.0f); _settings.localStructureStrength = ClampFinite( - _settings.localStructureStrength, 0.0f, 1.0f, 1.0f); + _settings.localStructureStrength, 0.0f, 2.0f, 1.0f); _ngxCore = &ngxCore; _impl.reset(); FrameGuidancePerformance::ResetDlssnrGpuTiming(); @@ -1871,6 +1899,9 @@ bool DLSSNRFilter::Initialize( D3D11_TEXTURE2D_DESC outputDesc{}; input->GetDesc(&inputDesc); output->GetDesc(&outputDesc); + const bool experimentalHdrPath = settings.experimentalHdr.enabled && + inputDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT && + outputDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT; if (inputDesc.Width != outputDesc.Width || inputDesc.Height != outputDesc.Height) { Logger::Get().Error(fmt::format( "DLSSNR requires same-resolution input/output: {}x{} -> {}x{}", @@ -1879,7 +1910,8 @@ bool DLSSNRFilter::Initialize( } const bool supportedInput = inputDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM || inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM; - if (!supportedInput || outputDesc.Format != DXGI_FORMAT_R8G8B8A8_UNORM) { + if ((!supportedInput || outputDesc.Format != DXGI_FORMAT_R8G8B8A8_UNORM) && + !experimentalHdrPath) { Logger::Get().Error(fmt::format( "DLSSNR SDR path unsupported formats: input={}, output={}", (uint32_t)inputDesc.Format, (uint32_t)outputDesc.Format)); @@ -1887,6 +1919,9 @@ bool DLSSNRFilter::Initialize( } impl->sourceWidth = inputDesc.Width; impl->sourceHeight = inputDesc.Height; + impl->experimentalHdrPath = experimentalHdrPath; + // Resolution scaling, guidance resampling, and residual reconstruction use + // format-neutral float shaders and preserve the FP16 HDR range explicitly. impl->useResolutionScaling = settings.enableInputResolutionScaling; const uint32_t resolutionPercent = std::clamp( settings.inputResolutionPercent, 25u, 100u); @@ -1924,7 +1959,8 @@ bool DLSSNRFilter::Initialize( } D3D11_TEXTURE2D_DESC sharedDesc = outputDesc; - sharedDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sharedDesc.Format = experimentalHdrPath ? + DXGI_FORMAT_R16G16B16A16_FLOAT : DXGI_FORMAT_R8G8B8A8_UNORM; sharedDesc.Width = impl->width; sharedDesc.Height = impl->height; if (!CreateSharedTexture(*impl, sharedDesc, true, @@ -2102,7 +2138,8 @@ bool DLSSNRFilter::Initialize( "residualSaturation={} residualLightness={} shadowStructureMultiplier={} " "reflectionGlowMultiplier={} preset=fixed-0 " "style={} intensity={} localTone={} localStructure={} skinStructure={} " - "motionVectorQuality={} autoMask={} uiCorrection={} depth=zero-contract disabled=false", + "motionVectorQuality={} autoMask={} uiCorrection={} depth=zero-contract disabled=false " + "experimentalHdrPath={}", ENABLE_CORE_FEATURE18_DIAGNOSTIC ? "core-diagnostic" : "signed-snippet", impl->sourceWidth, impl->sourceHeight, static_cast(inputDesc.Format), impl->width, impl->height, @@ -2114,7 +2151,8 @@ bool DLSSNRFilter::Initialize( _settings.intensity, _settings.localToneStrength, _settings.localStructureStrength, _settings.skinStructureStrength, static_cast(_settings.motionVectorQuality), - _settings.useAutoMask, _settings.uiCorrection)); + _settings.useAutoMask, _settings.uiCorrection, + impl->experimentalHdrPath)); _impl = std::move(impl); return true; } diff --git a/src/Magpie.Core/DLSSNRFilter.h b/src/Magpie.Core/DLSSNRFilter.h index 18eb9c5f4..e08d26b94 100644 --- a/src/Magpie.Core/DLSSNRFilter.h +++ b/src/Magpie.Core/DLSSNRFilter.h @@ -1,5 +1,6 @@ #pragma once #include "NativeEffectBackend.h" +#include "GroupBEffectProtocol.h" namespace Magpie { @@ -23,9 +24,11 @@ struct DLSSNRSettings { bool uiCorrection = false; NvidiaOpticalFlowQuality motionVectorQuality = NvidiaOpticalFlowQuality::Balanced; + // Experimental FP16 path. SDR RGBA8 remains the default. + DlssnrExperimentProtocol experimentalHdr{}; }; -DLSSNRSettings ParseDLSSNRSettings(const EffectOption& option) noexcept; +DLSSNRSettings ParseDLSSNRSettings(const EffectOption& option, bool hdrEnabled = false) noexcept; // Experimental same-resolution DLSS neural filter. Magpie only owns the // composited colour frame, so valid zero-filled motion/depth textures are used diff --git a/src/Magpie.Core/DLSSSRUpscaler.cpp b/src/Magpie.Core/DLSSSRUpscaler.cpp index bc0286cc5..39b1ae184 100644 --- a/src/Magpie.Core/DLSSSRUpscaler.cpp +++ b/src/Magpie.Core/DLSSSRUpscaler.cpp @@ -151,9 +151,12 @@ bool DLSSSRUpscaler::Initialize( .InTargetHeight = outputDesc.Height, .InPerfQualityValue = NVSDK_NGX_PerfQuality_Value_Balanced }, - .InFeatureCreateFlags = uint32_t( + .InFeatureCreateFlags = int( NVSDK_NGX_DLSS_Feature_Flags_MVLowRes | - NVSDK_NGX_DLSS_Feature_Flags_AutoExposure), + NVSDK_NGX_DLSS_Feature_Flags_AutoExposure | + (_hdrProtocol.hdrColorInput + ? NVSDK_NGX_DLSS_Feature_Flags_IsHDR + : NVSDK_NGX_DLSS_Feature_Flags_None)), .InEnableOutputSubrects = false }; @@ -272,8 +275,8 @@ bool DLSSSRUpscaler::Draw(const NativeEffectDrawContext& context) noexcept { evalParams.InReset = _resetHistory || guidanceReset ? 1 : 0; evalParams.InMVScaleX = 1.0f; evalParams.InMVScaleY = 1.0f; - evalParams.InPreExposure = 1.0f; - evalParams.InExposureScale = 1.0f; + evalParams.InPreExposure = _hdrProtocol.preExposure; + evalParams.InExposureScale = _hdrProtocol.exposure; const NVSDK_NGX_Result result = NGX_D3D11_EVALUATE_DLSS_EXT( _d3dDC, diff --git a/src/Magpie.Core/DLSSSRUpscaler.h b/src/Magpie.Core/DLSSSRUpscaler.h index de7bb9fc1..1090fa5be 100644 --- a/src/Magpie.Core/DLSSSRUpscaler.h +++ b/src/Magpie.Core/DLSSSRUpscaler.h @@ -1,5 +1,6 @@ #pragma once #include "NativeEffectBackend.h" +#include "GroupBEffectProtocol.h" namespace Magpie { @@ -48,6 +49,7 @@ class DLSSSRUpscaler final : public NativeEffectBackend { ) noexcept override; bool Draw(const NativeEffectDrawContext& context) noexcept override; + void SetDlssHdrProtocol(const FsrHdrProtocol& protocol) noexcept { _hdrProtocol = protocol; } private: void _Reset() noexcept; @@ -70,6 +72,7 @@ class DLSSSRUpscaler final : public NativeEffectBackend { [[maybe_unused]] uint8_t _lastGuidanceBinding = UINT8_MAX; [[maybe_unused]] FrameGuidanceFrameId _lastGuidanceResetFrameId = std::numeric_limits::max(); + FsrHdrProtocol _hdrProtocol{}; }; } diff --git a/src/Magpie.Core/DesktopDuplicationFrameSource.cpp b/src/Magpie.Core/DesktopDuplicationFrameSource.cpp index 31463b550..d6c34a58c 100644 --- a/src/Magpie.Core/DesktopDuplicationFrameSource.cpp +++ b/src/Magpie.Core/DesktopDuplicationFrameSource.cpp @@ -9,6 +9,7 @@ namespace Magpie { + static winrt::com_ptr FindMonitor(IDXGIAdapter1* adapter, HMONITOR hMonitor) noexcept { winrt::com_ptr output; diff --git a/src/Magpie.Core/EffectCompiler.cpp b/src/Magpie.Core/EffectCompiler.cpp index d091750e0..f18359aa2 100644 --- a/src/Magpie.Core/EffectCompiler.cpp +++ b/src/Magpie.Core/EffectCompiler.cpp @@ -1623,6 +1623,13 @@ static uint32_t CompilePasses( Logger::Get().Error(fmt::format("生成 Pass{} 失败", id + 1)); return; } + macros.emplace_back("MP_HDR_SATURATE", + (flags & EffectCompilerFlags::HdrCompatibility) ? "saturate" : ""); + macros.emplace_back("MP_HDR_ALPHA", + (flags & EffectCompilerFlags::HdrCompatibility) ? "sourceAlpha" : "1.0"); + if (flags & EffectCompilerFlags::HdrCompatibility) { + macros.emplace_back("MP_HDR_COMPATIBILITY", "1"); + } if (flags & EffectCompilerFlags::SaveSources) { std::wstring fileName = desc.passes.size() == 1 @@ -1850,6 +1857,19 @@ uint32_t EffectCompiler::Compile( return 1; } } + // Route-aware primary surfaces must be applied before pass source + // generation. GeneratePassSource derives typed SRV/UAV declarations from + // these descriptors, so runtime route metadata and CSO contracts stay + // identical. + const auto applySurfaceFormat = [&](uint32_t shift, EffectIntermediateTextureDesc& surface) { + const uint32_t encoded = (flags >> shift) & EffectCompilerFlags::SurfaceFormatMask; + if (encoded == 0) return; + const uint32_t formatIndex = encoded - 1; + if (formatIndex < std::size(EffectHelper::FORMAT_DESCS) - 1) + surface.format = static_cast(formatIndex); + }; + applySurfaceFormat(EffectCompilerFlags::InputFormatShift, desc.textures[0]); + applySurfaceFormat(EffectCompilerFlags::OutputFormatShift, desc.textures[1]); if (!noCompile) { desc.samplers.clear(); diff --git a/src/Magpie.Core/EffectDrawer.cpp b/src/Magpie.Core/EffectDrawer.cpp index bb07d9d58..fe2de9375 100644 --- a/src/Magpie.Core/EffectDrawer.cpp +++ b/src/Magpie.Core/EffectDrawer.cpp @@ -36,6 +36,9 @@ EffectDrawer::~EffectDrawer() { for (uint32_t i = 1; i < textureCount; ++i) { _descriptorStore->RemoveCache(_textures[i].get()); } + if (_hdrOutput && (_textures.size() < 2 || _hdrOutput.get() != _textures[1].get())) { + _descriptorStore->RemoveCache(_hdrOutput.get()); + } } bool EffectDrawer::Initialize( @@ -47,6 +50,11 @@ bool EffectDrawer::Initialize( ) noexcept { _d3dDC = deviceResources.GetD3DDC(); _descriptorStore = &descriptorStore; + _hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); + if (_hdrEnabled && !_hdrSurfaceAdapter.Initialize(deviceResources, descriptorStore)) { + Logger::Get().Error("初始化 HDR 效果边界适配器失败"); + return false; + } SIZE inputSize{}; { @@ -78,12 +86,28 @@ bool EffectDrawer::Initialize( // 创建中间纹理 // 第一个为 INPUT,第二个为 OUTPUT _textures.resize(desc.textures.size()); - _textures[0].copy_from(*inOutTexture); + _hdrInputSource = *inOutTexture; + D3D11_TEXTURE2D_DESC sourceDesc{}; + (*inOutTexture)->GetDesc(&sourceDesc); + const DXGI_FORMAT effectInputFormat = + _GetHdrInputFormat(desc); + if (!_hdrEnabled) { + _textures[0].copy_from(*inOutTexture); + } else { + _textures[0] = DirectXHelper::CreateTexture2D( + deviceResources.GetD3DDevice(), effectInputFormat, + sourceDesc.Width, sourceDesc.Height, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_textures[0]) { + Logger::Get().Error("创建 HDR 效果输入纹理失败"); + return false; + } + } - // 创建输出纹理,格式始终是 DXGI_FORMAT_R8G8B8A8_UNORM + // 创建效果内部输出纹理;HDR 模式另建 canonical FP16 输出。 _textures[1] = DirectXHelper::CreateTexture2D( deviceResources.GetD3DDevice(), - EffectHelper::FORMAT_DESCS[(uint32_t)desc.textures[1].format].dxgiFormat, + _GetHdrOutputFormat(desc), outputSize.cx, outputSize.cy, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS @@ -97,6 +121,23 @@ bool EffectDrawer::Initialize( if (!ClearEffectTexture(_d3dDC, descriptorStore, _textures[1].get())) { return false; } + if (_hdrEnabled) { + const D3D11_TEXTURE2D_DESC outputDesc = [&]() { + D3D11_TEXTURE2D_DESC result{}; + _textures[1]->GetDesc(&result); + return result; + }(); + _hdrOutput = DirectXHelper::CreateTexture2D( + deviceResources.GetD3DDevice(), + DXGI_FORMAT_R16G16B16A16_FLOAT, + outputDesc.Width, outputDesc.Height, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_hdrOutput || !ClearEffectTexture(_d3dDC, descriptorStore, _hdrOutput.get())) { + Logger::Get().Error("创建 HDR 效果输出纹理失败"); + return false; + } + *inOutTexture = _hdrOutput.get(); + } for (size_t i = 2; i < desc.textures.size(); ++i) { const EffectIntermediateTextureDesc& texDesc = desc.textures[i]; @@ -191,15 +232,26 @@ bool EffectDrawer::Initialize( } void EffectDrawer::Draw(EffectsProfiler& profiler) const noexcept { + if (!PrepareHdrInput()) { + Logger::Get().Error("准备 HDR 效果输入失败"); + return; + } _PrepareForDraw(); for (uint32_t i = 0; i < _dispatches.size(); ++i) { _DrawPass(i); profiler.OnEndPass(_d3dDC); } + if (!CompleteHdrOutput()) { + Logger::Get().Error("完成 HDR 效果输出失败"); + } } void EffectDrawer::DrawForExport(const EffectDesc& desc, uint32_t passIdx) const noexcept { + if (!PrepareHdrInput()) { + Logger::Get().Error("准备 HDR 导出输入失败"); + return; + } _PrepareForDraw(); for (uint32_t i : _CalcPassesToDrawForExport(desc, passIdx)) { @@ -215,17 +267,35 @@ bool EffectDrawer::ResizeTextures( ) noexcept { bool anyChange = false; - if (*inOutTexture != _textures[0].get()) { - _textures[0].copy_from(*inOutTexture); - anyChange = true; - } - SIZE inputSize{}; - { - D3D11_TEXTURE2D_DESC inputDesc; - _textures[0]->GetDesc(&inputDesc); - inputSize = { (LONG)inputDesc.Width, (LONG)inputDesc.Height }; + D3D11_TEXTURE2D_DESC inputDesc{}; + (*inOutTexture)->GetDesc(&inputDesc); + _hdrInputSource = *inOutTexture; + if (!_hdrEnabled) { + if (*inOutTexture != _textures[0].get()) { + _textures[0].copy_from(*inOutTexture); + anyChange = true; + } + } else { + const DXGI_FORMAT effectInputFormat = + _GetHdrInputFormat(desc); + D3D11_TEXTURE2D_DESC currentInput{}; + if (_textures[0]) _textures[0]->GetDesc(¤tInput); + if (!_textures[0] || currentInput.Format != effectInputFormat || + currentInput.Width != inputDesc.Width || currentInput.Height != inputDesc.Height) { + if (_textures[0]) _descriptorStore->RemoveCache(_textures[0].get()); + _textures[0] = DirectXHelper::CreateTexture2D( + deviceResources.GetD3DDevice(), effectInputFormat, + inputDesc.Width, inputDesc.Height, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_textures[0]) { + Logger::Get().Error("调整 HDR 效果输入纹理失败"); + return false; + } + anyChange = true; + } } + inputSize = { (LONG)inputDesc.Width, (LONG)inputDesc.Height }; const SIZE outputSize = _CalcOutputSize(desc, option, inputSize); if (outputSize.cx <= 0 || outputSize.cy <= 0) { @@ -238,10 +308,14 @@ bool EffectDrawer::ResizeTextures( if ((LONG)texDesc.Width != outputSize.cx || (LONG)texDesc.Height != outputSize.cy) { _descriptorStore->RemoveCache(_textures[1].get()); + if (_hdrOutput && _hdrOutput.get() != _textures[1].get()) { + _descriptorStore->RemoveCache(_hdrOutput.get()); + _hdrOutput = nullptr; + } _textures[1] = DirectXHelper::CreateTexture2D( deviceResources.GetD3DDevice(), - texDesc.Format, + _GetHdrOutputFormat(desc), outputSize.cx, outputSize.cy, texDesc.BindFlags @@ -258,7 +332,38 @@ bool EffectDrawer::ResizeTextures( anyChange = true; } - *inOutTexture = _textures[1].get(); + if (_hdrEnabled) { + if (!_hdrOutput) { + _hdrOutput = DirectXHelper::CreateTexture2D( + deviceResources.GetD3DDevice(), + DXGI_FORMAT_R16G16B16A16_FLOAT, + outputSize.cx, outputSize.cy, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_hdrOutput || !ClearEffectTexture(_d3dDC, *_descriptorStore, _hdrOutput.get())) { + Logger::Get().Error("调整 HDR 效果输出纹理失败"); + return false; + } + } else { + D3D11_TEXTURE2D_DESC hdrOutputDesc{}; + _hdrOutput->GetDesc(&hdrOutputDesc); + if (hdrOutputDesc.Width != (UINT)outputSize.cx || + hdrOutputDesc.Height != (UINT)outputSize.cy) { + _descriptorStore->RemoveCache(_hdrOutput.get()); + _hdrOutput = DirectXHelper::CreateTexture2D( + deviceResources.GetD3DDevice(), + DXGI_FORMAT_R16G16B16A16_FLOAT, + outputSize.cx, outputSize.cy, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_hdrOutput || !ClearEffectTexture(_d3dDC, *_descriptorStore, _hdrOutput.get())) { + Logger::Get().Error("调整 HDR 效果输出尺寸失败"); + return false; + } + } + } + *inOutTexture = _hdrOutput.get(); + } else { + *inOutTexture = _textures[1].get(); + } for (size_t i = 2; i < _textures.size(); ++i) { const std::pair& sizeExpr = desc.textures[i].sizeExpr; @@ -325,6 +430,162 @@ bool EffectDrawer::ResizeTextures( return true; } +bool EffectDrawer::_UsesDirectHdrPath() const noexcept { + if (!_hdrEnabled || !_hdrBoundary.prepared || _textures.size() < 2 || + !_textures[0] || !_textures[1]) { + return false; + } + + const HdrAdapterProfile profile = _hdrBoundary.plan.profile; + if (profile != HdrAdapterProfile::DirectFP16 && + profile != HdrAdapterProfile::ConditionalFP16 && + profile != HdrAdapterProfile::PresentationTerminal) { + return false; + } + + D3D11_TEXTURE2D_DESC inputDesc{}, outputDesc{}; + _textures[0]->GetDesc(&inputDesc); + _textures[1]->GetDesc(&outputDesc); + return inputDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT && + outputDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT; +} + +DXGI_FORMAT EffectDrawer::_GetHdrInputFormat(const EffectDesc& desc) const noexcept { + (void)desc; + if (!_hdrEnabled || !_hdrBoundary.prepared || !_hdrBoundary.SelectedRoute()) { + return EffectHelper::FORMAT_DESCS[(uint32_t)desc.textures[0].format].dxgiFormat; + } + const DXGI_FORMAT routeFormat = _hdrBoundary.SelectedRoute()->inputFormat; + return routeFormat == DXGI_FORMAT_UNKNOWN + ? EffectHelper::FORMAT_DESCS[(uint32_t)desc.textures[0].format].dxgiFormat + : routeFormat; +} + +DXGI_FORMAT EffectDrawer::_GetHdrOutputFormat(const EffectDesc& desc) const noexcept { + (void)desc; + if (!_hdrEnabled || !_hdrBoundary.prepared || !_hdrBoundary.SelectedRoute()) { + return EffectHelper::FORMAT_DESCS[(uint32_t)desc.textures[1].format].dxgiFormat; + } + const DXGI_FORMAT routeFormat = _hdrBoundary.SelectedRoute()->outputFormat; + return routeFormat == DXGI_FORMAT_UNKNOWN + ? EffectHelper::FORMAT_DESCS[(uint32_t)desc.textures[1].format].dxgiFormat + : routeFormat; +} + +HdrTransformParameters EffectDrawer::_GetHdrTransformParameters() const noexcept { + if (_hdrBoundary.inputFrame.metadata.IsValid()) { + HdrTransformParameters parameters = HdrColorTransform::ForFrame( + _hdrBoundary.inputFrame.metadata.color); + if (parameters.IsValid()) { + if (const HdrFormatRoute* route = _hdrBoundary.SelectedRoute(); route && + route->alphaMode == HdrAlphaMode::ForceOpaque) { + parameters.preserveAlpha = false; + } + return parameters; + } + } + return {}; +} + +bool EffectDrawer::PrepareHdrInput() const noexcept { + if (!_hdrEnabled) { + return true; + } + if (!_hdrInputSource || !_textures[0]) { + return false; + } + D3D11_TEXTURE2D_DESC sourceDesc{}, inputDesc{}; + _hdrInputSource->GetDesc(&sourceDesc); + _textures[0]->GetDesc(&inputDesc); + if (sourceDesc.Width != inputDesc.Width || sourceDesc.Height != inputDesc.Height) { + Logger::Get().Error(fmt::format( + "HDR effect input size mismatch: source={}x{} adapter={}x{}", + sourceDesc.Width, sourceDesc.Height, inputDesc.Width, inputDesc.Height)); + return false; + } + if (_hdrBoundary.SelectedRoute()) { + const auto* route = _hdrBoundary.SelectedRoute(); + D3D11_TEXTURE2D_DESC outputDesc{}; + _textures[1]->GetDesc(&outputDesc); + if (inputDesc.Format != route->inputFormat || outputDesc.Format != route->outputFormat) { + Logger::Get().Error(fmt::format( + "HDR route/resource mismatch: route={} input={} output={}", + route->Id(), static_cast(inputDesc.Format), + static_cast(outputDesc.Format))); + return false; + } + } + if (_UsesDirectHdrPath()) { + _d3dDC->CopyResource(_textures[0].get(), _hdrInputSource); + return true; + } + const HdrFormatRoute* route = _hdrBoundary.SelectedRoute(); + if (route && route->inputTransfer == HdrTransferFunction::PQ && + inputDesc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) { + return _hdrSurfaceAdapter.ConvertCanonicalToHdr10( + _hdrInputSource, _textures[0].get(), _GetHdrTransformParameters()); + } + if (_hdrBoundary.plan.requiresBoundedMapping) { + return _hdrSurfaceAdapter.ConvertHdrToBounded( + _hdrInputSource, _textures[0].get(), _GetHdrTransformParameters(), + _hdrBoundary.plan.normalizationScale); + } + + if (route && route->inputTransfer == HdrTransferFunction::PQ) { + return _hdrSurfaceAdapter.ConvertHdrToSdr( + _hdrInputSource, _textures[0].get(), _GetHdrTransformParameters(), + HdrTransferFunction::PQ); + } + const HdrTransferFunction transfer = route && + route->inputTransfer != HdrTransferFunction::Unknown + ? route->inputTransfer : HdrTransferFunction::SRGB; + return _hdrSurfaceAdapter.ConvertHdrToSdr( + _hdrInputSource, + _textures[0].get(), + _GetHdrTransformParameters(), + transfer); +} + +bool EffectDrawer::CompleteHdrOutput() const noexcept { + if (!_hdrEnabled) { + return true; + } + if (!_textures[1] || !_hdrOutput) { + return false; + } + if (_UsesDirectHdrPath()) { + _d3dDC->CopyResource(_hdrOutput.get(), _textures[1].get()); + return true; + } + const HdrFormatRoute* route = _hdrBoundary.SelectedRoute(); + D3D11_TEXTURE2D_DESC outputDesc{}; + _textures[1]->GetDesc(&outputDesc); + if (route && route->outputTransfer == HdrTransferFunction::PQ && + outputDesc.Format == DXGI_FORMAT_R10G10B10A2_UNORM) { + return _hdrSurfaceAdapter.ConvertHdr10ToCanonical( + _textures[1].get(), _hdrOutput.get(), _GetHdrTransformParameters()); + } + if (_hdrBoundary.plan.requiresBoundedMapping) { + return _hdrSurfaceAdapter.ConvertBoundedToHdr( + _textures[1].get(), _hdrOutput.get(), _GetHdrTransformParameters(), + _hdrBoundary.plan.normalizationScale); + } + + if (route && route->outputTransfer == HdrTransferFunction::PQ) { + return _hdrSurfaceAdapter.ConvertSdrToHdr( + _textures[1].get(), _hdrOutput.get(), _GetHdrTransformParameters(), + HdrTransferFunction::PQ); + } + const HdrTransferFunction transfer = route && + route->outputTransfer != HdrTransferFunction::Unknown + ? route->outputTransfer : HdrTransferFunction::SRGB; + return _hdrSurfaceAdapter.ConvertSdrToHdr( + _textures[1].get(), + _hdrOutput.get(), + _GetHdrTransformParameters(), + transfer); +} + bool EffectDrawer::UpdateParameters( const EffectDesc& desc, const EffectOption& option, diff --git a/src/Magpie.Core/EffectDrawer.h b/src/Magpie.Core/EffectDrawer.h index 6007d9724..160aac8f0 100644 --- a/src/Magpie.Core/EffectDrawer.h +++ b/src/Magpie.Core/EffectDrawer.h @@ -1,5 +1,8 @@ #pragma once #include "EffectDesc.h" +#include "HdrEffectBoundary.h" +#include "HdrSurfaceAdapter.h" +#include #include "SmallVector.h" // Conan 的 muparser 不含 UNICODE 支持 #pragma push_macro("_UNICODE") @@ -35,6 +38,18 @@ class EffectDrawer { void Draw(EffectsProfiler& profiler) const noexcept; + void SetHdrBoundary(HdrEffectBoundaryContext context) noexcept { _hdrBoundary = std::move(context); } + const HdrEffectBoundaryContext& GetHdrBoundary() const noexcept { return _hdrBoundary; } + // The renderer may replace the canonical handoff texture after capture, + // resize, or an upstream effect rebuild. Keep the boundary source bound to + // the current production texture instead of a stale initialization pointer. + void SetHdrInputSource(ID3D11Texture2D* texture) noexcept { _hdrInputSource = texture; } + bool PrepareHdrInput() const noexcept; + bool CompleteHdrOutput() const noexcept; + ID3D11Texture2D* GetExternalOutputTexture() const noexcept { + return _hdrEnabled && _hdrOutput ? _hdrOutput.get() : _textures[1].get(); + } + void DrawForExport(const EffectDesc& desc, uint32_t passIdx) const noexcept; bool ResizeTextures( @@ -58,6 +73,12 @@ class EffectDrawer { return _textures[idx].get(); } + // Source-equivalent matrix tests use the exact production drawer and expose + // its actual canonical handoff rather than reimplementing effect formulas. + ID3D11Texture2D* GetCanonicalOutputTextureForTesting() const noexcept { + return GetExternalOutputTexture(); + } + private: SIZE _CalcOutputSize( const EffectDesc& desc, @@ -78,6 +99,10 @@ class EffectDrawer { void _PrepareForDraw() const noexcept; void _DrawPass(uint32_t i) const noexcept; + bool _UsesDirectHdrPath() const noexcept; + DXGI_FORMAT _GetHdrInputFormat(const EffectDesc& desc) const noexcept; + DXGI_FORMAT _GetHdrOutputFormat(const EffectDesc& desc) const noexcept; + HdrTransformParameters _GetHdrTransformParameters() const noexcept; SmallVector _CalcPassesToDrawForExport( const EffectDesc& desc, @@ -98,6 +123,11 @@ class EffectDrawer { SmallVector> _shaders; SmallVector> _dispatches; + HdrEffectBoundaryContext _hdrBoundary{}; + HdrSurfaceAdapter _hdrSurfaceAdapter; + winrt::com_ptr _hdrOutput; + ID3D11Texture2D* _hdrInputSource = nullptr; + bool _hdrEnabled = false; static inline mu::Parser _exprParser; }; diff --git a/src/Magpie.Core/EffectProtocolCatalogC.h b/src/Magpie.Core/EffectProtocolCatalogC.h new file mode 100644 index 000000000..dc4bf7a11 --- /dev/null +++ b/src/Magpie.Core/EffectProtocolCatalogC.h @@ -0,0 +1,223 @@ +#pragma once + +#include "HdrAdapterDispatcher.h" + +// Group-C effect protocol declarations. These functions deliberately keep +// each effect family separate so a future Renderer bridge can select a route +// without a shared generic HDR processor or effect-name switch. +namespace Magpie::EffectProtocolC { + +inline HdrFormatRoute SdrRoute( + std::string_view effect, + std::string_view option, + DXGI_FORMAT format, + HdrAlphaMode alpha = HdrAlphaMode::ForceOpaque +) { + return HdrFormatRoute{ + .effectId = std::string(effect), + .optionId = std::string(option), + .inputFormat = format, + .outputFormat = format, + .inputTransfer = HdrTransferFunction::SRGB, + .outputTransfer = HdrTransferFunction::SRGB, + .inputRange = HdrColorRange::Full, + .outputRange = HdrColorRange::Full, + .alphaMode = alpha, + .evidenceLevel = HdrEvidenceLevel::ReferenceImplementation, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::SDRCompatible, + .defaultForHdr = true, + .defaultForSdr = true + }; +} + +inline HdrFormatRoute ConditionalFp16Route( + std::string_view effect, + std::string_view option, + HdrAlphaMode alpha = HdrAlphaMode::ForceOpaque +) { + return HdrFormatRoute{ + .effectId = std::string(effect), + .optionId = std::string(option), + .inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .inputTransfer = HdrTransferFunction::Linear, + .outputTransfer = HdrTransferFunction::Linear, + .inputRange = HdrColorRange::SceneLinear, + .outputRange = HdrColorRange::SceneLinear, + .alphaMode = alpha, + .evidenceLevel = HdrEvidenceLevel::ReferenceImplementation, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::ConditionalFP16, + .defaultForHdr = false, + .defaultForSdr = false + }; +} + +inline HdrFormatRoutes NNEDI3() { + return { SdrRoute("NNEDI3", "luma-r16", DXGI_FORMAT_R8G8B8A8_UNORM) }; +} + +inline HdrFormatRoutes PixelArt() { + return { SdrRoute("Pixel Art", "rgb-thresholds", DXGI_FORMAT_R8G8B8A8_UNORM) }; +} + +inline HdrFormatRoutes RAVU() { + return { SdrRoute("RAVU", "variant-boundary", DXGI_FORMAT_R8G8B8A8_UNORM) }; +} + +inline HdrFormatRoutes RTXVideoVsr() { + HdrFormatRoute route = SdrRoute( + "RTXVideoVSR", "VSR-BGRA8-U8-sRGB-255", DXGI_FORMAT_B8G8R8A8_UNORM); + // NvCVImage receives display-referred sRGB code values in the 0..255 + // domain. The adapter performs transfer encoding before the native call. + route.inputTransfer = HdrTransferFunction::SRGB; + route.outputTransfer = HdrTransferFunction::SRGB; + return { route }; +} + +inline HdrFormatRoutes RTXVideoDenoiser() { + return { SdrRoute("RTXVideoDenoise", "Denoise-BGRA8-U8", DXGI_FORMAT_B8G8R8A8_UNORM) }; +} + +inline HdrFormatRoutes RTXVideoHdr() { + // Product-level SDR->HDR10 behavior has no verified callable texture route. + return {}; +} + +inline HdrFormatRoutes Sharpen() { + return { SdrRoute("Sharpen", "normalized-rgb", DXGI_FORMAT_R8G8B8A8_UNORM) }; +} + +inline HdrFormatRoutes SMAA() { + return { SdrRoute("SMAA", "rgba-edge-blend", DXGI_FORMAT_R8G8B8A8_UNORM) }; +} + +inline HdrFormatRoutes Xbrz() { + return { SdrRoute("xBRZ", "rgba-u8", DXGI_FORMAT_R8G8B8A8_UNORM) }; +} + +inline HdrFormatRoutes XeSS() { + HdrFormatRoute hdr10{ + .effectId = "XeSS", + .optionId = "HDR10-R10G10B10A2", + .inputFormat = DXGI_FORMAT_R10G10B10A2_UNORM, + .outputFormat = DXGI_FORMAT_R10G10B10A2_UNORM, + .inputTransfer = HdrTransferFunction::PQ, + .outputTransfer = HdrTransferFunction::PQ, + .inputRange = HdrColorRange::DisplayReferred, + .outputRange = HdrColorRange::DisplayReferred, + .alphaMode = HdrAlphaMode::ForceOpaque, + .evidenceLevel = HdrEvidenceLevel::PublicApiContract, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::BoundedHDR, + .defaultForHdr = true, + .defaultForSdr = false + }; + HdrFormatRoute fp16 = hdr10; + fp16.optionId = "R16G16B16A16_FLOAT"; + fp16.inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + fp16.outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + fp16.inputTransfer = HdrTransferFunction::Linear; + fp16.outputTransfer = HdrTransferFunction::Linear; + fp16.inputRange = HdrColorRange::SceneLinear; + fp16.outputRange = HdrColorRange::SceneLinear; + fp16.hdrNative = true; + fp16.adapterProfile = HdrAdapterProfile::DirectFP16; + fp16.defaultForHdr = true; + hdr10.defaultForHdr = false; + HdrFormatRoute r11 = fp16; + r11.optionId = "R11G11B10_FLOAT"; + r11.inputFormat = DXGI_FORMAT_R11G11B10_FLOAT; + r11.outputFormat = DXGI_FORMAT_R11G11B10_FLOAT; + r11.defaultForHdr = false; + HdrFormatRoute r8 = SdrRoute("XeSS", "R8G8B8A8_UNORM", DXGI_FORMAT_R8G8B8A8_UNORM); + r8.evidenceLevel = HdrEvidenceLevel::PublicApiContract; + return { hdr10, fp16, r11, r8 }; +} + +inline HdrFormatRoutes XeSSFG() { + return { HdrFormatRoute{ + .effectId = "XeSSFG", + .optionId = "canonical-FP16-marker", + .inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .inputTransfer = HdrTransferFunction::Linear, + .outputTransfer = HdrTransferFunction::Linear, + .inputRange = HdrColorRange::SceneLinear, + .outputRange = HdrColorRange::SceneLinear, + .alphaMode = HdrAlphaMode::Preserve, + .evidenceLevel = HdrEvidenceLevel::PublicApiContract, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::PresentationTerminal, + .defaultForHdr = true, + .defaultForSdr = false + } }; +} + +inline HdrFormatRoutes DLSSFG() { + return { HdrFormatRoute{ + .effectId = "DLSSFG", + .optionId = "presentation-terminal-runtime-format", + .inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .inputTransfer = HdrTransferFunction::Linear, + .outputTransfer = HdrTransferFunction::Linear, + .inputRange = HdrColorRange::SceneLinear, + .outputRange = HdrColorRange::SceneLinear, + .alphaMode = HdrAlphaMode::Preserve, + .evidenceLevel = HdrEvidenceLevel::CommunityExperiment, + .hdrNative = true, + .adapterProfile = HdrAdapterProfile::PresentationTerminal, + .defaultForHdr = true, + .defaultForSdr = false + } }; +} + +inline HdrFormatRoutes FSR3FG() { + return { HdrFormatRoute{ + .effectId = "FSR3FG", + .optionId = "presentation-terminal-runtime-format", + .inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .inputTransfer = HdrTransferFunction::Linear, + .outputTransfer = HdrTransferFunction::Linear, + .inputRange = HdrColorRange::SceneLinear, + .outputRange = HdrColorRange::SceneLinear, + .alphaMode = HdrAlphaMode::Preserve, + .evidenceLevel = HdrEvidenceLevel::PublicApiContract, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::PresentationTerminal, + .defaultForHdr = true, + .defaultForSdr = false + } }; +} + +inline HdrFormatRoutes NvidiaOpticalFlow() { + // Optical flow produces auxiliary vectors, so this route records the input + // boundary only. The vector output remains R16G16_FLOAT/S10.5-decoded data. + HdrFormatRoute route = SdrRoute( + "NVIDIA Optical Flow", "ABGR8-input-S10.5-output", DXGI_FORMAT_R8G8B8A8_UNORM, + HdrAlphaMode::ForceOpaque); + route.evidenceLevel = HdrEvidenceLevel::PublicApiContract; + return { route }; +} + +inline HdrFormatRoutes GetGroupCHdrRoutes(std::string_view effectGroup) { + if (effectGroup == "NNEDI3") return NNEDI3(); + if (effectGroup == "Pixel Art") return PixelArt(); + if (effectGroup == "RAVU") return RAVU(); + if (effectGroup == "RTXVideoVSR") return RTXVideoVsr(); + if (effectGroup == "RTXVideoDenoise") return RTXVideoDenoiser(); + if (effectGroup == "Sharpen") return Sharpen(); + if (effectGroup == "SMAA") return SMAA(); + if (effectGroup == "xBRZ") return Xbrz(); + if (effectGroup == "XeSS") return XeSS(); + if (effectGroup == "XeSSFG") return XeSSFG(); + if (effectGroup == "DLSSFG") return DLSSFG(); + if (effectGroup == "FSR3FG") return FSR3FG(); + if (effectGroup == "NVIDIA Optical Flow") return NvidiaOpticalFlow(); + return {}; +} + +} // namespace Magpie::EffectProtocolC diff --git a/src/Magpie.Core/FSR2Upscaler.cpp b/src/Magpie.Core/FSR2Upscaler.cpp index ca3c2498d..f92ef6872 100644 --- a/src/Magpie.Core/FSR2Upscaler.cpp +++ b/src/Magpie.Core/FSR2Upscaler.cpp @@ -31,6 +31,8 @@ void FSR2Upscaler::_Reset() noexcept { _zeroMotion = nullptr; _zeroDepthUav = nullptr; _zeroDepth = nullptr; + _exposureUav = nullptr; + _exposure = nullptr; _reactiveUav = nullptr; _reactive = nullptr; if (_backendModule) FreeLibrary(_backendModule); @@ -108,13 +110,23 @@ bool FSR2Upscaler::Initialize( desc.device = reinterpret_cast(_getDevice)(_device); desc.maxRenderSize = { inDesc.Width, inDesc.Height }; desc.displaySize = { outDesc.Width, outDesc.Height }; - desc.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE | FFX_FSR2_ENABLE_DEPTH_INVERTED | - FFX_FSR2_ENABLE_DEPTH_INFINITE; + desc.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE | + (_hdrProtocol.depthInverted ? FFX_FSR2_ENABLE_DEPTH_INVERTED : 0) | + (_hdrProtocol.depthInfinite ? FFX_FSR2_ENABLE_DEPTH_INFINITE : 0); + if (_hdrProtocol.hdrColorInput) desc.flags |= FFX_FSR2_ENABLE_HIGH_DYNAMIC_RANGE; ec = reinterpret_cast(_contextCreate)( static_cast(_context), &desc); if (ec != FFX_OK) { Logger::Get().Error(fmt::format("ffxFsr2ContextCreate failed ({})", (int)ec)); _Reset(); return false; } - Logger::Get().Info(fmt::format("FSR2 D3D11 initialized (opticalFlow={}): {}x{} -> {}x{}", - _enableOpticalFlow, inDesc.Width, inDesc.Height, outDesc.Width, outDesc.Height)); + if (_hdrProtocol.hdrColorInput) { + _exposure = DirectXHelper::CreateTexture2D(_device, DXGI_FORMAT_R32_FLOAT, 1, 1, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (_exposure) _device->CreateUnorderedAccessView( + _exposure.get(), nullptr, _exposureUav.put()); + } + Logger::Get().Info(fmt::format("FSR2 D3D11 initialized (opticalFlow={}, hdr={}, transfer={}, exposure={:.3f}): {}x{} -> {}x{}", + _enableOpticalFlow, _hdrProtocol.hdrColorInput, + static_cast(_hdrProtocol.transfer), _hdrProtocol.exposure, + inDesc.Width, inDesc.Height, outDesc.Width, outDesc.Height)); return true; } @@ -133,6 +145,10 @@ bool FSR2Upscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { static constexpr float REACTIVE_OF[4]{ 0.5f,0.5f,0.5f,0.5f }; static constexpr float REACTIVE_ZEROMV[4]{ 0.9f,0.9f,0.9f,0.9f }; _d3dDC->ClearUnorderedAccessViewFloat(_zeroDepthUav.get(), ZERO); + if (_exposureUav) { + const float exposure[4]{ _hdrProtocol.exposure, 0, 0, 0 }; + _d3dDC->ClearUnorderedAccessViewFloat(_exposureUav.get(), exposure); + } ID3D11Texture2D* motionVectors = _zeroMotion.get(); if (_enableOpticalFlow) { D3D11_TEXTURE2D_DESC desc{}; @@ -155,7 +171,8 @@ bool FSR2Upscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { d.depth = getResource(static_cast(_context), _zeroDepth.get(), L"FSR2_ZeroDepth", FFX_RESOURCE_STATE_COMPUTE_READ); d.motionVectors = getResource(static_cast(_context), motionVectors, _enableOpticalFlow ? L"FSR2_OpticalFlow" : L"FSR2_ZeroMotion", FFX_RESOURCE_STATE_COMPUTE_READ); - d.exposure = getResource(static_cast(_context), nullptr, L"FSR2_AutoExposure", FFX_RESOURCE_STATE_COMPUTE_READ); + d.exposure = getResource(static_cast(_context), _exposure.get(), + _exposure ? L"FSR2_Exposure" : L"FSR2_AutoExposure", FFX_RESOURCE_STATE_COMPUTE_READ); d.reactive = getResource(static_cast(_context), _reactive.get(), L"FSR2_FullReactive", FFX_RESOURCE_STATE_COMPUTE_READ); d.transparencyAndComposition = getResource(static_cast(_context), nullptr, nullptr, FFX_RESOURCE_STATE_COMPUTE_READ); d.output = getResource(static_cast(_context), output, L"FSR2_Output", FFX_RESOURCE_STATE_UNORDERED_ACCESS); @@ -167,7 +184,7 @@ bool FSR2Upscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { d.enableSharpening = true; d.sharpness = 0.2f; d.frameTimeDelta = 16.6667f; - d.preExposure = 1.0f; + d.preExposure = _hdrProtocol.preExposure; d.reset = _resetHistory; d.cameraNear = 1.0f; d.cameraFar = FLT_MAX; diff --git a/src/Magpie.Core/FSR2Upscaler.h b/src/Magpie.Core/FSR2Upscaler.h index 37fe84e3a..fd39a48ad 100644 --- a/src/Magpie.Core/FSR2Upscaler.h +++ b/src/Magpie.Core/FSR2Upscaler.h @@ -1,5 +1,6 @@ #pragma once #include "NativeEffectBackend.h" +#include "GroupBEffectProtocol.h" namespace Magpie { @@ -16,6 +17,9 @@ class FSR2Upscaler final : public NativeEffectBackend { MotionVectorRequest motionRequest = {}) noexcept; bool Resize(DeviceResources& resources, ID3D11Texture2D* input, ID3D11Texture2D* output) noexcept override; bool Draw(const NativeEffectDrawContext& context) noexcept override; + void SetFsrHdrProtocol(const FsrHdrProtocol& protocol) noexcept { + _hdrProtocol = protocol; + } FrameGuidanceRequirements GetFrameGuidanceRequirements() const noexcept override { FrameGuidanceRequirements result{ .zero = true }; result.Add(_motionRequest); @@ -30,8 +34,7 @@ class FSR2Upscaler final : public NativeEffectBackend { MotionVectorRequest _motionRequest{}; void _Reset() noexcept; - // 以下字段仅在 MP_ENABLE_FSR2_ZEROMV 构建中使用;无 SDK 的 CI 构建里 - // ClangCL -Werror、-Wunused-private-field 会报错 + // These fields are active only in the MP_ENABLE_FSR2_ZEROMV build. [[maybe_unused]] ID3D11Device* _device = nullptr; [[maybe_unused]] ID3D11DeviceContext4* _d3dDC = nullptr; [[maybe_unused]] HMODULE _coreModule = nullptr; @@ -52,9 +55,12 @@ class FSR2Upscaler final : public NativeEffectBackend { [[maybe_unused]] winrt::com_ptr _zeroDepthUav; [[maybe_unused]] winrt::com_ptr _reactive; [[maybe_unused]] winrt::com_ptr _reactiveUav; + [[maybe_unused]] winrt::com_ptr _exposure; + [[maybe_unused]] winrt::com_ptr _exposureUav; [[maybe_unused]] bool _resetHistory = true; [[maybe_unused]] FrameGuidanceFrameId _lastGuidanceResetFrameId = std::numeric_limits::max(); [[maybe_unused]] bool _enableOpticalFlow = false; + FsrHdrProtocol _hdrProtocol{}; }; } diff --git a/src/Magpie.Core/FSR2ZeroMVUpscaler.cpp b/src/Magpie.Core/FSR2ZeroMVUpscaler.cpp new file mode 100644 index 000000000..9ebfe008b --- /dev/null +++ b/src/Magpie.Core/FSR2ZeroMVUpscaler.cpp @@ -0,0 +1,214 @@ +#include "pch.h" +#include "FSR2ZeroMVUpscaler.h" +#include "DeviceResources.h" +#include "DirectXHelper.h" +#include "Logger.h" +#include "HalfResOpticalFlow.h" + +#ifdef MP_ENABLE_FSR2_ZEROMV +#include +#include + +namespace Magpie { + +template +static T LoadProc(HMODULE module, const char* name) noexcept { + return reinterpret_cast(GetProcAddress(module, name)); +} + +FSR2ZeroMVUpscaler::~FSR2ZeroMVUpscaler() { _Reset(); } + +void FSR2ZeroMVUpscaler::_Reset() noexcept { + if (_context && _contextDestroy) { + reinterpret_cast(_contextDestroy)( + static_cast(_context)); + } + delete static_cast(_context); + _context = nullptr; + delete[] static_cast(_scratch); + _scratch = nullptr; + _scratchSize = 0; + _zeroMotionUav = nullptr; + _zeroMotion = nullptr; + _zeroDepthUav = nullptr; + _zeroDepth = nullptr; + _reactiveUav = nullptr; + _reactive = nullptr; + if (_backendModule) FreeLibrary(_backendModule); + if (_coreModule) FreeLibrary(_coreModule); + _backendModule = nullptr; + _coreModule = nullptr; + _contextCreate = _contextDestroy = _contextDispatch = nullptr; + _getInterface = _getScratchSize = _getDevice = _getResource = nullptr; + _device = nullptr; + _d3dDC = nullptr; + _resetHistory = true; + _enableOpticalFlow = false; + _enableJitter = false; + _frameIndex = 0; + _opticalFlow.reset(); +} + +bool FSR2ZeroMVUpscaler::Initialize( + DeviceResources& resources, ID3D11Texture2D* input, ID3D11Texture2D* output, + bool enableOpticalFlow, bool enableJitter +) noexcept { + _Reset(); + _enableOpticalFlow = enableOpticalFlow; + _enableJitter = enableJitter; + _device = resources.GetD3DDevice(); + _d3dDC = resources.GetD3DDC(); + D3D11_TEXTURE2D_DESC inDesc{}, outDesc{}; + input->GetDesc(&inDesc); + output->GetDesc(&outDesc); + if (inDesc.Width > outDesc.Width || inDesc.Height > outDesc.Height) return false; + + _coreModule = LoadLibraryW(L"ffx_fsr2_api_x64.dll"); + _backendModule = LoadLibraryW(L"ffx_fsr2_api_dx11_x64.dll"); + if (!_coreModule || !_backendModule) { + Logger::Get().Win32Error("Load FSR2 D3D11 runtime failed"); + _Reset(); + return false; + } + _contextCreate = LoadProc(_coreModule, "ffxFsr2ContextCreate"); + _contextDestroy = LoadProc(_coreModule, "ffxFsr2ContextDestroy"); + _contextDispatch = LoadProc(_coreModule, "ffxFsr2ContextDispatch"); + _getInterface = LoadProc(_backendModule, "ffxFsr2GetInterfaceDX11"); + _getScratchSize = LoadProc(_backendModule, "ffxFsr2GetScratchMemorySizeDX11"); + _getDevice = LoadProc(_backendModule, "ffxGetDeviceDX11"); + _getResource = LoadProc(_backendModule, "ffxGetResourceDX11"); + if (!_contextCreate || !_contextDestroy || !_contextDispatch || !_getInterface || + !_getScratchSize || !_getDevice || !_getResource) { + Logger::Get().Error("FSR2 D3D11 runtime exports are incomplete"); + _Reset(); + return false; + } + + _zeroMotion = DirectXHelper::CreateTexture2D(_device, DXGI_FORMAT_R16G16_FLOAT, + inDesc.Width, inDesc.Height, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + _zeroDepth = DirectXHelper::CreateTexture2D(_device, DXGI_FORMAT_R32_FLOAT, + inDesc.Width, inDesc.Height, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + _reactive = DirectXHelper::CreateTexture2D(_device, DXGI_FORMAT_R8_UNORM, + inDesc.Width, inDesc.Height, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + HRESULT hr = _device->CreateUnorderedAccessView(_zeroMotion.get(), nullptr, _zeroMotionUav.put()); + if (SUCCEEDED(hr)) hr = _device->CreateUnorderedAccessView(_zeroDepth.get(), nullptr, _zeroDepthUav.put()); + if (SUCCEEDED(hr)) hr = _device->CreateUnorderedAccessView(_reactive.get(), nullptr, _reactiveUav.put()); + if (FAILED(hr)) { + Logger::Get().ComError("Create FSR2 Zero-MV auxiliary resources failed", hr); + _Reset(); + return false; + } + if (_enableOpticalFlow) { + _opticalFlow = std::make_unique(); + if (!_opticalFlow->Initialize(_device, _d3dDC, input)) { + Logger::Get().Error("Initialize FSR2 half-resolution optical flow failed"); + _Reset(); + return false; + } + } + + FfxFsr2ContextDescription desc{}; + _scratchSize = reinterpret_cast(_getScratchSize)(); + _scratch = new (std::nothrow) char[_scratchSize]; + _context = new (std::nothrow) FfxFsr2Context{}; + if (!_scratch || !_context) { _Reset(); return false; } + FfxErrorCode ec = reinterpret_cast(_getInterface)( + &desc.callbacks, _device, _scratch, _scratchSize); + if (ec != FFX_OK) { Logger::Get().Error(fmt::format("ffxFsr2GetInterfaceDX11 failed ({})", (int)ec)); _Reset(); return false; } + desc.device = reinterpret_cast(_getDevice)(_device); + desc.maxRenderSize = { inDesc.Width, inDesc.Height }; + desc.displaySize = { outDesc.Width, outDesc.Height }; + desc.flags = FFX_FSR2_ENABLE_AUTO_EXPOSURE | FFX_FSR2_ENABLE_DEPTH_INVERTED | + FFX_FSR2_ENABLE_DEPTH_INFINITE; + ec = reinterpret_cast(_contextCreate)( + static_cast(_context), &desc); + if (ec != FFX_OK) { Logger::Get().Error(fmt::format("ffxFsr2ContextCreate failed ({})", (int)ec)); _Reset(); return false; } + Logger::Get().Info(fmt::format("FSR2 D3D11 initialized (opticalFlow={}, jitter={}): {}x{} -> {}x{}", + _enableOpticalFlow, _enableJitter, inDesc.Width, inDesc.Height, outDesc.Width, outDesc.Height)); + return true; +} + +bool FSR2ZeroMVUpscaler::Resize(DeviceResources& r, ID3D11Texture2D* i, ID3D11Texture2D* o) noexcept { + const bool enableOpticalFlow = _enableOpticalFlow; + const bool enableJitter = _enableJitter; + return Initialize(r, i, o, enableOpticalFlow, enableJitter); +} + +static float Halton(uint32_t index, uint32_t base) noexcept { + float result = 0.0f; + float fraction = 1.0f; + while (index) { + fraction /= (float)base; + result += fraction * (float)(index % base); + index /= base; + } + return result; +} + +bool FSR2ZeroMVUpscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { + ID3D11Texture2D* input = drawContext.input; + ID3D11Texture2D* output = drawContext.output; + if (!_context) return false; + static constexpr float ZERO[4]{}; + static constexpr float REACTIVE_OF[4]{ 0.5f,0.5f,0.5f,0.5f }; + static constexpr float REACTIVE_ZEROMV[4]{ 0.9f,0.9f,0.9f,0.9f }; + _d3dDC->ClearUnorderedAccessViewFloat(_zeroDepthUav.get(), ZERO); + ID3D11Texture2D* motionVectors = _zeroMotion.get(); + if (_enableOpticalFlow) { + if (!_opticalFlow || !_opticalFlow->Estimate(input)) return false; + motionVectors = _opticalFlow->GetMotionTexture(); + _d3dDC->ClearUnorderedAccessViewFloat(_reactiveUav.get(), REACTIVE_OF); + } else { + _d3dDC->ClearUnorderedAccessViewFloat(_zeroMotionUav.get(), ZERO); + _d3dDC->ClearUnorderedAccessViewFloat(_reactiveUav.get(), REACTIVE_ZEROMV); + } + D3D11_TEXTURE2D_DESC inDesc{}; + input->GetDesc(&inDesc); + auto getResource = reinterpret_cast(_getResource); + FfxFsr2DispatchDescription d{}; + d.commandList = _d3dDC; + d.color = getResource(static_cast(_context), input, L"FSR2_InputColor", FFX_RESOURCE_STATE_COMPUTE_READ); + d.depth = getResource(static_cast(_context), _zeroDepth.get(), L"FSR2_ZeroDepth", FFX_RESOURCE_STATE_COMPUTE_READ); + d.motionVectors = getResource(static_cast(_context), motionVectors, + _enableOpticalFlow ? L"FSR2_OpticalFlow" : L"FSR2_ZeroMotion", FFX_RESOURCE_STATE_COMPUTE_READ); + d.exposure = getResource(static_cast(_context), nullptr, L"FSR2_AutoExposure", FFX_RESOURCE_STATE_COMPUTE_READ); + d.reactive = getResource(static_cast(_context), _reactive.get(), L"FSR2_FullReactive", FFX_RESOURCE_STATE_COMPUTE_READ); + d.transparencyAndComposition = getResource(static_cast(_context), nullptr, nullptr, FFX_RESOURCE_STATE_COMPUTE_READ); + d.output = getResource(static_cast(_context), output, L"FSR2_Output", FFX_RESOURCE_STATE_UNORDERED_ACCESS); + // HalfResOpticalFlow stores motion directly in pixel units, so no render-size + // multiplication is needed. Applying width/height here made OF vectors huge. + d.motionVectorScale = { 1.0f, 1.0f }; + if (_enableJitter) { + // Metadata-only jitter: Magpie cannot modify the source application's projection. + const uint32_t sample = (_frameIndex++ & 7u) + 1u; + d.jitterOffset = { Halton(sample, 2) - 0.5f, Halton(sample, 3) - 0.5f }; + } else { + d.jitterOffset = { 0.0f, 0.0f }; + } + d.renderSize = { inDesc.Width, inDesc.Height }; + d.enableSharpening = true; + d.sharpness = 0.2f; + d.frameTimeDelta = 16.6667f; + d.preExposure = 1.0f; + d.reset = _resetHistory; + d.cameraNear = 1.0f; + d.cameraFar = FLT_MAX; + d.cameraFovAngleVertical = 1.04719755f; + d.viewSpaceToMetersFactor = 1.0f; + const FfxErrorCode ec = reinterpret_cast(_contextDispatch)( + static_cast(_context), &d); + if (ec != FFX_OK) { Logger::Get().Error(fmt::format("ffxFsr2ContextDispatch failed ({})", (int)ec)); return false; } + _resetHistory = false; + return true; +} + +} +#else +namespace Magpie { +FSR2ZeroMVUpscaler::~FSR2ZeroMVUpscaler() = default; +void FSR2ZeroMVUpscaler::_Reset() noexcept {} +bool FSR2ZeroMVUpscaler::Initialize(DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*, bool, bool) noexcept { return false; } +bool FSR2ZeroMVUpscaler::Resize(DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*) noexcept { return false; } +bool FSR2ZeroMVUpscaler::Draw(const NativeEffectDrawContext&) noexcept { return false; } +} +#endif diff --git a/src/Magpie.Core/FSR2ZeroMVUpscaler.h b/src/Magpie.Core/FSR2ZeroMVUpscaler.h new file mode 100644 index 000000000..242411bff --- /dev/null +++ b/src/Magpie.Core/FSR2ZeroMVUpscaler.h @@ -0,0 +1,51 @@ +#pragma once +#include "HalfResOpticalFlow.h" +#include "NativeEffectBackend.h" + +namespace Magpie { + +class DeviceResources; + +class FSR2ZeroMVUpscaler final : public NativeEffectBackend { +public: + FSR2ZeroMVUpscaler() = default; + FSR2ZeroMVUpscaler(const FSR2ZeroMVUpscaler&) = delete; + FSR2ZeroMVUpscaler& operator=(const FSR2ZeroMVUpscaler&) = delete; + ~FSR2ZeroMVUpscaler() override; + + bool Initialize(DeviceResources& resources, ID3D11Texture2D* input, ID3D11Texture2D* output, + bool enableOpticalFlow = false, bool enableJitter = false) noexcept; + bool Resize(DeviceResources& resources, ID3D11Texture2D* input, ID3D11Texture2D* output) noexcept override; + bool Draw(const NativeEffectDrawContext& context) noexcept override; + +private: + void _Reset() noexcept; + + ID3D11Device* _device = nullptr; + ID3D11DeviceContext* _d3dDC = nullptr; + HMODULE _coreModule = nullptr; + HMODULE _backendModule = nullptr; + void* _context = nullptr; + void* _scratch = nullptr; + size_t _scratchSize = 0; + void* _contextCreate = nullptr; + void* _contextDestroy = nullptr; + void* _contextDispatch = nullptr; + void* _getInterface = nullptr; + void* _getScratchSize = nullptr; + void* _getDevice = nullptr; + void* _getResource = nullptr; + winrt::com_ptr _zeroMotion; + winrt::com_ptr _zeroMotionUav; + winrt::com_ptr _zeroDepth; + winrt::com_ptr _zeroDepthUav; + winrt::com_ptr _reactive; + winrt::com_ptr _reactiveUav; + bool _resetHistory = true; + bool _enableOpticalFlow = false; + bool _enableJitter = false; + uint32_t _frameIndex = 0; + std::unique_ptr _opticalFlow; +}; + +} diff --git a/src/Magpie.Core/FSR3Upscaler.cpp b/src/Magpie.Core/FSR3Upscaler.cpp index 7209a33c7..8c814b790 100644 --- a/src/Magpie.Core/FSR3Upscaler.cpp +++ b/src/Magpie.Core/FSR3Upscaler.cpp @@ -52,6 +52,7 @@ struct FSR3Upscaler::Impl { uint32_t outputWidth = 0; uint32_t outputHeight = 0; bool enableOpticalFlow = false; + FsrHdrProtocol hdrProtocol{}; bool useFsr4 = false; bool resetHistory = true; FrameGuidanceFrameId lastGuidanceResetFrameId = std::numeric_limits::max(); @@ -274,6 +275,7 @@ bool FSR3Upscaler::Initialize( impl->device11 = resources.GetD3DDevice(); impl->context11 = resources.GetD3DDC(); impl->enableOpticalFlow = enableOpticalFlow; + impl->hdrProtocol = _hdrProtocol; impl->useFsr4 = useFsr4; const char* upscalerName = useFsr4 ? "FSR 4.1.1" : "FSR 3.1.5"; @@ -343,7 +345,8 @@ bool FSR3Upscaler::Initialize( D3D12_RESOURCE_BARRIER auxBarriers[5]{}; UINT auxCount = 0; const float zero[4]{}; - const float one[4]{ 1, 1, 1, 1 }; + const float one[4]{ impl->hdrProtocol.exposure, impl->hdrProtocol.exposure, + impl->hdrProtocol.exposure, impl->hdrProtocol.exposure }; const float reactive02[4]{ 0.2f, 0.2f, 0.2f, 0.2f }; const float reactiveFsr4OpticalFlow[4]{ 0.8f, 0.8f, 0.8f, 0.8f }; const float reactive08[4]{ 0.8f, 0.8f, 0.8f, 0.8f }; @@ -444,8 +447,12 @@ bool FSR3Upscaler::Initialize( } impl->createDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE; - impl->createDesc.flags = FFX_UPSCALE_ENABLE_DEPTH_INVERTED | - FFX_UPSCALE_ENABLE_DEPTH_INFINITE | FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE; + impl->createDesc.flags = + (impl->hdrProtocol.depthInverted ? FFX_UPSCALE_ENABLE_DEPTH_INVERTED : 0) | + (impl->hdrProtocol.depthInfinite ? FFX_UPSCALE_ENABLE_DEPTH_INFINITE : 0); + if (!impl->hdrProtocol.hdrColorInput) { + impl->createDesc.flags |= FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE; + } impl->createDesc.maxRenderSize = { inputDesc.Width, inputDesc.Height }; impl->createDesc.maxUpscaleSize = { outputDesc.Width, outputDesc.Height }; impl->backendDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_DX12; @@ -540,13 +547,16 @@ bool FSR3Upscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { desc.enableSharpening = true; desc.sharpness = 0.2f; desc.frameTimeDelta = 16.6667f; - desc.preExposure = 1.0f; + desc.preExposure = impl.hdrProtocol.preExposure; desc.reset = impl.resetHistory; desc.cameraNear = 1.0f; desc.cameraFar = FLT_MAX; desc.cameraFovAngleVertical = 1.04719755f; desc.viewSpaceToMetersFactor = 1.0f; - desc.flags = FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB; + desc.flags = 0; + if (!impl.hdrProtocol.hdrColorInput) { + desc.flags = FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB; + } const ffxReturnCode_t rc = impl.dispatch(&impl.context, &desc.header); if (rc != FFX_API_RETURN_OK) { Logger::Get().Error(fmt::format("Dispatch {} failed ({})", diff --git a/src/Magpie.Core/FSR3Upscaler.h b/src/Magpie.Core/FSR3Upscaler.h index 5dcedab90..ad16927ac 100644 --- a/src/Magpie.Core/FSR3Upscaler.h +++ b/src/Magpie.Core/FSR3Upscaler.h @@ -1,5 +1,6 @@ #pragma once #include "NativeEffectBackend.h" +#include "GroupBEffectProtocol.h" namespace Magpie { @@ -21,6 +22,9 @@ class FSR3Upscaler final : public NativeEffectBackend { bool Resize(DeviceResources& resources, ID3D11Texture2D* input, ID3D11Texture2D* output) noexcept override; bool Draw(const NativeEffectDrawContext& context) noexcept override; + void SetFsrHdrProtocol(const FsrHdrProtocol& protocol) noexcept { + _hdrProtocol = protocol; + } FrameGuidanceRequirements GetFrameGuidanceRequirements() const noexcept override { FrameGuidanceRequirements result{ .zero = true }; result.Add(_motionRequest); @@ -33,9 +37,9 @@ class FSR3Upscaler final : public NativeEffectBackend { private: MotionVectorRequest _motionRequest{}; - // 仅在 MP_ENABLE_FSR3_ZEROMV 构建中使用;无 SDK 的 CI 构建里 ClangCL -Werror 会报未使用 [[maybe_unused]] std::unique_ptr _impl; [[maybe_unused]] bool _useFsr4 = false; + FsrHdrProtocol _hdrProtocol{}; }; } diff --git a/src/Magpie.Core/FSR3ZeroMVUpscaler.cpp b/src/Magpie.Core/FSR3ZeroMVUpscaler.cpp new file mode 100644 index 000000000..765fa5484 --- /dev/null +++ b/src/Magpie.Core/FSR3ZeroMVUpscaler.cpp @@ -0,0 +1,609 @@ +#include "pch.h" +#include "FSR3ZeroMVUpscaler.h" +#include "DeviceResources.h" +#include "HalfResOpticalFlow.h" +#include "Logger.h" + +#ifdef MP_ENABLE_FSR3_ZEROMV +#include +#include +#include +#include + +namespace Magpie { + +struct FSR3ZeroMVUpscaler::Impl { + ~Impl(); + + ID3D11Device5* device11 = nullptr; + ID3D11DeviceContext4* context11 = nullptr; + winrt::com_ptr device12; + winrt::com_ptr queue12; + winrt::com_ptr allocator12; + winrt::com_ptr commandList12; + winrt::com_ptr sharedInput11; + winrt::com_ptr sharedOutput11; + winrt::com_ptr sharedMotion11; + winrt::com_ptr sharedInput12; + winrt::com_ptr sharedOutput12; + winrt::com_ptr sharedMotion12; + winrt::com_ptr zeroMotion12; + winrt::com_ptr flatDepth12; + winrt::com_ptr exposure12; + winrt::com_ptr reactive12; + winrt::com_ptr transparency12; + winrt::com_ptr descriptorHeap12; + winrt::com_ptr fence11; + winrt::com_ptr fence12; + std::unique_ptr opticalFlow; + HMODULE loaderModule = nullptr; + HMODULE providerModule = nullptr; + decltype(&ffxCreateContext) createContext = nullptr; + decltype(&ffxDestroyContext) destroyContext = nullptr; + decltype(&ffxDispatch) dispatch = nullptr; + decltype(&ffxQuery) query = nullptr; + ffxContext context = nullptr; + ffxCreateContextDescUpscale createDesc{}; + ffxCreateBackendDX12Desc backendDesc{}; + ffxCreateContextDescUpscaleVersion apiVersion{}; + ffxOverrideVersion overrideVersion{}; + uint64_t fenceValue = 0; + uint64_t lastSubmittedValue = 0; + uint32_t inputWidth = 0; + uint32_t inputHeight = 0; + uint32_t outputWidth = 0; + uint32_t outputHeight = 0; + bool enableOpticalFlow = false; + bool enableJitter = false; + bool useFsr4 = false; + uint32_t frameIndex = 0; + bool resetHistory = true; +}; + +static bool WaitForFence(FSR3ZeroMVUpscaler::Impl& impl, uint64_t value) noexcept { + if (!value || impl.fence12->GetCompletedValue() >= value) return true; + wil::unique_event_nothrow event; + HRESULT hr = event.create(); + if (FAILED(hr)) return false; + hr = impl.fence12->SetEventOnCompletion(value, event.get()); + if (FAILED(hr)) return false; + WaitForSingleObject(event.get(), INFINITE); + return true; +} + +static bool WaitForQueue(FSR3ZeroMVUpscaler::Impl& impl) noexcept { + const uint64_t value = ++impl.fenceValue; + HRESULT hr = impl.queue12->Signal(impl.fence12.get(), value); + if (FAILED(hr)) return false; + return WaitForFence(impl, value); +} + +FSR3ZeroMVUpscaler::Impl::~Impl() { + if (queue12 && fence12) WaitForQueue(*this); + if (context && destroyContext) destroyContext(&context, nullptr); + if (loaderModule) FreeLibrary(loaderModule); + if (providerModule) FreeLibrary(providerModule); +} + +static bool IsAddressInExecutableSection( + const uint8_t* base, + const IMAGE_NT_HEADERS* nt, + const void* address +) noexcept { + const uintptr_t value = reinterpret_cast(address); + for (const IMAGE_SECTION_HEADER* section = IMAGE_FIRST_SECTION(nt); + section != IMAGE_FIRST_SECTION(nt) + nt->FileHeader.NumberOfSections; ++section) { + if (!(section->Characteristics & IMAGE_SCN_MEM_EXECUTE)) continue; + const uintptr_t begin = reinterpret_cast(base) + section->VirtualAddress; + const uintptr_t end = begin + std::max(section->Misc.VirtualSize, section->SizeOfRawData); + if (value >= begin && value < end) return true; + } + return false; +} + +// FSR 4.1.1 contains a dedicated INT8 provider but hides it when IsSupported +// rejects the real adapter. Patch only that provider's virtual support check in +// this process. This avoids changing the system adapter identity or other apps. +static bool ForceFsr4Int8ProviderSupport(HMODULE module) noexcept { + if (!module) return false; + uint8_t* base = reinterpret_cast(module); + const IMAGE_DOS_HEADER* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + const IMAGE_NT_HEADERS* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + + constexpr char typeName[] = ".?AVffxProvider_FSR4_Int8@@"; + const uint8_t* typeNameAddress = nullptr; + const IMAGE_SECTION_HEADER* sections = IMAGE_FIRST_SECTION(nt); + for (uint16_t i = 0; i < nt->FileHeader.NumberOfSections && !typeNameAddress; ++i) { + const IMAGE_SECTION_HEADER& section = sections[i]; + if (!(section.Characteristics & IMAGE_SCN_MEM_READ)) continue; + const uint8_t* begin = base + section.VirtualAddress; + const size_t size = std::max(section.Misc.VirtualSize, section.SizeOfRawData); + if (size < sizeof(typeName)) continue; + for (size_t offset = 0; offset + sizeof(typeName) <= size; ++offset) { + if (memcmp(begin + offset, typeName, sizeof(typeName)) == 0) { + typeNameAddress = begin + offset; + break; + } + } + } + if (!typeNameAddress || typeNameAddress < base + 2 * sizeof(void*)) return false; + + // MSVC x64 TypeDescriptor stores two pointers immediately before its name. + const uint8_t* typeDescriptor = typeNameAddress - 2 * sizeof(void*); + const uint32_t typeDescriptorRva = static_cast(typeDescriptor - base); + struct CompleteObjectLocator { + uint32_t signature; + uint32_t offset; + uint32_t cdOffset; + uint32_t typeDescriptorRva; + uint32_t classDescriptorRva; + uint32_t selfRva; + }; + + for (uint16_t i = 0; i < nt->FileHeader.NumberOfSections; ++i) { + const IMAGE_SECTION_HEADER& colSection = sections[i]; + if (!(colSection.Characteristics & IMAGE_SCN_MEM_READ)) continue; + uint8_t* colBegin = base + colSection.VirtualAddress; + const size_t colSize = std::max(colSection.Misc.VirtualSize, colSection.SizeOfRawData); + for (size_t colOffset = 0; colOffset + sizeof(CompleteObjectLocator) <= colSize; + colOffset += alignof(uint32_t)) { + const auto* col = reinterpret_cast(colBegin + colOffset); + const uint32_t colRva = static_cast(colBegin + colOffset - base); + if (col->signature != 1 || col->typeDescriptorRva != typeDescriptorRva || + col->selfRva != colRva) continue; + + const uintptr_t colAddress = reinterpret_cast(col); + for (uint16_t j = 0; j < nt->FileHeader.NumberOfSections; ++j) { + const IMAGE_SECTION_HEADER& tableSection = sections[j]; + if (!(tableSection.Characteristics & IMAGE_SCN_MEM_READ)) continue; + uint8_t* tableBegin = base + tableSection.VirtualAddress; + const size_t tableSize = std::max(tableSection.Misc.VirtualSize, tableSection.SizeOfRawData); + for (size_t tableOffset = 0; tableOffset + 4 * sizeof(void*) <= tableSize; + tableOffset += alignof(void*)) { + const auto* locatorPointer = reinterpret_cast(tableBegin + tableOffset); + if (*locatorPointer != colAddress) continue; + void** vtable = reinterpret_cast(tableBegin + tableOffset + sizeof(void*)); + // MSVC order: deleting destructor, CanProvide, IsSupported. + uint8_t* supportFunction = reinterpret_cast(vtable[2]); + if (!IsAddressInExecutableSection(base, nt, supportFunction)) continue; + + DWORD oldProtect = 0; + if (!VirtualProtect(supportFunction, 6, PAGE_EXECUTE_READWRITE, &oldProtect)) return false; + const uint8_t returnTrue[]{ 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 }; + memcpy(supportFunction, returnTrue, sizeof(returnTrue)); + FlushInstructionCache(GetCurrentProcess(), supportFunction, sizeof(returnTrue)); + DWORD ignored = 0; + VirtualProtect(supportFunction, 6, oldProtect, &ignored); + Logger::Get().Info("Enabled process-local FSR 4.1.1 INT8 provider support override"); + return true; + } + } + } + } + return false; +} + +static bool CreateSharedTexture( + FSR3ZeroMVUpscaler::Impl& impl, + const D3D11_TEXTURE2D_DESC& sourceDesc, + winrt::com_ptr& texture11, + winrt::com_ptr& texture12 +) noexcept { + D3D11_TEXTURE2D_DESC desc = sourceDesc; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.CPUAccessFlags = 0; + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLE; + HRESULT hr = impl.device11->CreateTexture2D(&desc, nullptr, texture11.put()); + if (FAILED(hr)) { + Logger::Get().ComError("Create FSR3 shared D3D11 texture failed", hr); + return false; + } + winrt::com_ptr dxgiResource; + hr = texture11->QueryInterface(IID_PPV_ARGS(dxgiResource.put())); + if (FAILED(hr)) return false; + HANDLE rawHandle = nullptr; + hr = dxgiResource->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &rawHandle); + if (FAILED(hr)) return false; + wil::unique_handle handle(rawHandle); + hr = impl.device12->OpenSharedHandle(handle.get(), IID_PPV_ARGS(texture12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Open FSR3 shared texture in D3D12 failed", hr); + return false; + } + return true; +} + +static bool CreateAuxTexture( + FSR3ZeroMVUpscaler::Impl& impl, + DXGI_FORMAT format, + uint32_t width, + uint32_t height, + winrt::com_ptr& resource, + D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle, + D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle, + const float clearValue[4], + D3D12_RESOURCE_BARRIER& barrier +) noexcept { + D3D12_RESOURCE_DESC desc{}; + desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + desc.Width = width; + desc.Height = height; + desc.DepthOrArraySize = 1; + desc.MipLevels = 1; + desc.Format = format; + desc.SampleDesc.Count = 1; + desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; + HRESULT hr = impl.device12->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, + D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, IID_PPV_ARGS(resource.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create FSR3 virtual input texture failed", hr); + return false; + } + D3D12_UNORDERED_ACCESS_VIEW_DESC uav{}; + uav.Format = format; + uav.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + impl.device12->CreateUnorderedAccessView(resource.get(), nullptr, &uav, cpuHandle); + impl.commandList12->ClearUnorderedAccessViewFloat( + gpuHandle, cpuHandle, resource.get(), clearValue, 0, nullptr); + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = resource.get(); + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + return true; +} + +FSR3ZeroMVUpscaler::FSR3ZeroMVUpscaler() = default; + +FSR3ZeroMVUpscaler::~FSR3ZeroMVUpscaler() = default; + +bool FSR3ZeroMVUpscaler::Initialize( + DeviceResources& resources, + ID3D11Texture2D* input, + ID3D11Texture2D* output, + bool enableOpticalFlow, + bool enableJitter, + bool useFsr4 +) noexcept { + _enableOpticalFlow = enableOpticalFlow; + _enableJitter = enableJitter; + _useFsr4 = useFsr4; + _impl.reset(); + auto impl = std::make_unique(); + impl->device11 = resources.GetD3DDevice(); + impl->context11 = resources.GetD3DDC(); + impl->enableOpticalFlow = enableOpticalFlow; + impl->enableJitter = enableJitter; + impl->useFsr4 = useFsr4; + const char* upscalerName = useFsr4 ? "FSR 4.1.1" : "FSR 3.1.5"; + + D3D11_TEXTURE2D_DESC inputDesc{}; + D3D11_TEXTURE2D_DESC outputDesc{}; + input->GetDesc(&inputDesc); + output->GetDesc(&outputDesc); + if (inputDesc.Width > outputDesc.Width || inputDesc.Height > outputDesc.Height) { + Logger::Get().Error("FSR3 experimental backend only supports upscaling"); + return false; + } + if ((float)outputDesc.Width / inputDesc.Width > 3.0f || + (float)outputDesc.Height / inputDesc.Height > 3.0f) { + Logger::Get().Error("FSR3 experimental backend supports up to a 3x scale"); + return false; + } + impl->inputWidth = inputDesc.Width; + impl->inputHeight = inputDesc.Height; + impl->outputWidth = outputDesc.Width; + impl->outputHeight = outputDesc.Height; + + HRESULT hr = D3D12CreateDevice(resources.GetGraphicsAdapter(), D3D_FEATURE_LEVEL_11_0, + IID_PPV_ARGS(impl->device12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create FSR3 D3D12 device failed", hr); + return false; + } + D3D12_COMMAND_QUEUE_DESC queueDesc{}; + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + hr = impl->device12->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(impl->queue12.put())); + if (SUCCEEDED(hr)) hr = impl->device12->CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(impl->allocator12.put())); + if (SUCCEEDED(hr)) hr = impl->device12->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, + impl->allocator12.get(), nullptr, IID_PPV_ARGS(impl->commandList12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create FSR3 D3D12 command objects failed", hr); + return false; + } + + if (!CreateSharedTexture(*impl, inputDesc, impl->sharedInput11, impl->sharedInput12) || + !CreateSharedTexture(*impl, outputDesc, impl->sharedOutput11, impl->sharedOutput12)) { + return false; + } + if (enableOpticalFlow) { + impl->opticalFlow = std::make_unique(); + if (!impl->opticalFlow->Initialize(impl->device11, impl->context11, input)) return false; + D3D11_TEXTURE2D_DESC motionDesc{}; + motionDesc.Width = inputDesc.Width; + motionDesc.Height = inputDesc.Height; + motionDesc.MipLevels = 1; + motionDesc.ArraySize = 1; + motionDesc.Format = DXGI_FORMAT_R16G16_FLOAT; + motionDesc.SampleDesc.Count = 1; + motionDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + if (!CreateSharedTexture(*impl, motionDesc, impl->sharedMotion11, impl->sharedMotion12)) return false; + } + + D3D12_DESCRIPTOR_HEAP_DESC heapDesc{}; + heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + heapDesc.NumDescriptors = 5; + heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + hr = impl->device12->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(impl->descriptorHeap12.put())); + if (FAILED(hr)) return false; + ID3D12DescriptorHeap* heaps[]{ impl->descriptorHeap12.get() }; + impl->commandList12->SetDescriptorHeaps(1, heaps); + const UINT stride = impl->device12->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + D3D12_CPU_DESCRIPTOR_HANDLE cpu = impl->descriptorHeap12->GetCPUDescriptorHandleForHeapStart(); + D3D12_GPU_DESCRIPTOR_HANDLE gpu = impl->descriptorHeap12->GetGPUDescriptorHandleForHeapStart(); + D3D12_RESOURCE_BARRIER auxBarriers[5]{}; + UINT auxCount = 0; + const float zero[4]{}; + const float one[4]{ 1, 1, 1, 1 }; + const float reactive02[4]{ 0.2f, 0.2f, 0.2f, 0.2f }; + const float reactiveFsr4OpticalFlow[4]{ 0.8f, 0.8f, 0.8f, 0.8f }; + const float reactive08[4]{ 0.8f, 0.8f, 0.8f, 0.8f }; + const float reactiveFsr4ZeroMv[4]{ 1.0f, 1.0f, 1.0f, 1.0f }; + auto addAux = [&](DXGI_FORMAT format, uint32_t width, uint32_t height, + winrt::com_ptr& texture, const float value[4]) -> bool { + if (!CreateAuxTexture(*impl, format, width, height, texture, cpu, gpu, value, + auxBarriers[auxCount])) return false; + ++auxCount; + cpu.ptr += stride; + gpu.ptr += stride; + return true; + }; + if (!addAux(DXGI_FORMAT_R32_FLOAT, inputDesc.Width, inputDesc.Height, impl->flatDepth12, zero) || + !addAux(DXGI_FORMAT_R32_FLOAT, 1, 1, impl->exposure12, one) || + !addAux(DXGI_FORMAT_R8_UNORM, inputDesc.Width, inputDesc.Height, impl->reactive12, + useFsr4 ? (enableOpticalFlow ? reactiveFsr4OpticalFlow : reactiveFsr4ZeroMv) : + (enableOpticalFlow ? reactive02 : reactive08)) || + !addAux(DXGI_FORMAT_R8_UNORM, inputDesc.Width, inputDesc.Height, impl->transparency12, zero)) { + return false; + } + if (!enableOpticalFlow && !addAux(DXGI_FORMAT_R16G16_FLOAT, inputDesc.Width, inputDesc.Height, + impl->zeroMotion12, zero)) return false; + impl->commandList12->ResourceBarrier(auxCount, auxBarriers); + hr = impl->commandList12->Close(); + if (FAILED(hr)) return false; + ID3D12CommandList* lists[]{ impl->commandList12.get() }; + impl->queue12->ExecuteCommandLists(1, lists); + + hr = impl->device11->CreateFence(0, D3D11_FENCE_FLAG_SHARED, IID_PPV_ARGS(impl->fence11.put())); + if (FAILED(hr)) return false; + HANDLE rawFence = nullptr; + hr = impl->fence11->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &rawFence); + if (FAILED(hr)) return false; + wil::unique_handle fenceHandle(rawFence); + hr = impl->device12->OpenSharedHandle(fenceHandle.get(), IID_PPV_ARGS(impl->fence12.put())); + if (FAILED(hr) || !WaitForQueue(*impl)) return false; + + if (useFsr4) { + impl->providerModule = LoadLibraryW(L"amd_fidelityfx_upscaler_dx12.dll"); + if (!impl->providerModule) { + Logger::Get().Win32Error("Load FSR 4.1.1 provider DLL failed"); + return false; + } + if (!ForceFsr4Int8ProviderSupport(impl->providerModule)) { + Logger::Get().Error("Locate FSR 4.1.1 INT8 provider support check failed"); + return false; + } + } + + impl->loaderModule = LoadLibraryW(L"amd_fidelityfx_loader_dx12.dll"); + if (!impl->loaderModule) { + Logger::Get().Win32Error("Load amd_fidelityfx_loader_dx12.dll failed"); + return false; + } + impl->createContext = reinterpret_castcreateContext)>( + GetProcAddress(impl->loaderModule, "ffxCreateContext")); + impl->destroyContext = reinterpret_castdestroyContext)>( + GetProcAddress(impl->loaderModule, "ffxDestroyContext")); + impl->dispatch = reinterpret_castdispatch)>( + GetProcAddress(impl->loaderModule, "ffxDispatch")); + impl->query = reinterpret_castquery)>( + GetProcAddress(impl->loaderModule, "ffxQuery")); + if (!impl->createContext || !impl->destroyContext || !impl->dispatch || !impl->query) { + Logger::Get().Error("AMD FidelityFX loader exports are incomplete"); + return false; + } + + ffxQueryDescGetVersions versionQuery{}; + versionQuery.header.type = FFX_API_QUERY_DESC_TYPE_GET_VERSIONS; + versionQuery.createDescType = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE; + versionQuery.device = impl->device12.get(); + uint64_t versionCount = 0; + versionQuery.outputCount = &versionCount; + ffxReturnCode_t rc = impl->query(nullptr, &versionQuery.header); + if (rc != FFX_API_RETURN_OK || !versionCount) { + Logger::Get().Error(fmt::format("Query FSR upscaler versions failed ({})", (uint32_t)rc)); + return false; + } + std::vector versionIds(versionCount); + std::vector versionNames(versionCount); + versionQuery.versionIds = versionIds.data(); + versionQuery.versionNames = versionNames.data(); + rc = impl->query(nullptr, &versionQuery.header); + if (rc != FFX_API_RETURN_OK) return false; + uint64_t selectedVersionId = 0; + const char* requestedVersion = useFsr4 ? "4.1.1" : "3.1.5"; + std::string availableVersions; + for (uint64_t i = 0; i < versionCount; ++i) { + const char* name = versionNames[i] ? versionNames[i] : "unknown"; + if (!availableVersions.empty()) availableVersions += ", "; + availableVersions += name; + if (strstr(name, requestedVersion)) selectedVersionId = versionIds[i]; + } + if (!selectedVersionId) { + Logger::Get().Error(fmt::format("{} provider not found; available: {}", upscalerName, availableVersions)); + return false; + } + + impl->createDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE; + impl->createDesc.flags = FFX_UPSCALE_ENABLE_DEPTH_INVERTED | + FFX_UPSCALE_ENABLE_DEPTH_INFINITE | FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE; + impl->createDesc.maxRenderSize = { inputDesc.Width, inputDesc.Height }; + impl->createDesc.maxUpscaleSize = { outputDesc.Width, outputDesc.Height }; + impl->backendDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_DX12; + impl->backendDesc.device = impl->device12.get(); + impl->apiVersion.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE_VERSION; + impl->apiVersion.version = FFX_UPSCALER_VERSION; + impl->overrideVersion.header.type = FFX_API_DESC_TYPE_OVERRIDE_VERSION; + impl->overrideVersion.versionId = selectedVersionId; + impl->createDesc.header.pNext = &impl->backendDesc.header; + impl->backendDesc.header.pNext = &impl->apiVersion.header; + impl->apiVersion.header.pNext = &impl->overrideVersion.header; + rc = impl->createContext(&impl->context, &impl->createDesc.header, nullptr); + if (rc != FFX_API_RETURN_OK || !impl->context) { + const char* hint = useFsr4 + ? "; FSR4 INT8 may still be rejected by GPU capability detection" + : ""; + Logger::Get().Error(fmt::format("Create {} context failed ({}){}", + upscalerName, (uint32_t)rc, hint)); + return false; + } + Logger::Get().Info(fmt::format( + "{} D3D11/D3D12 backend initialized (opticalFlow={}, jitter={}, virtual auxiliary inputs): {}x{} -> {}x{}", + upscalerName, enableOpticalFlow, enableJitter, + inputDesc.Width, inputDesc.Height, outputDesc.Width, outputDesc.Height)); + _impl = std::move(impl); + return true; +} + +bool FSR3ZeroMVUpscaler::Resize(DeviceResources& resources, ID3D11Texture2D* input, + ID3D11Texture2D* output) noexcept { + return Initialize(resources, input, output, _enableOpticalFlow, _enableJitter, _useFsr4); +} + +static float Halton(uint32_t index, uint32_t base) noexcept { + float result = 0.0f; + float fraction = 1.0f; + while (index) { + fraction /= (float)base; + result += fraction * (float)(index % base); + index /= base; + } + return result; +} + +bool FSR3ZeroMVUpscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { + ID3D11Texture2D* input = drawContext.input; + ID3D11Texture2D* output = drawContext.output; + if (!_impl || !_impl->context) return false; + Impl& impl = *_impl; + if (!WaitForFence(impl, impl.lastSubmittedValue)) return false; + impl.context11->CopyResource(impl.sharedInput11.get(), input); + if (impl.enableOpticalFlow) { + if (!impl.opticalFlow->Estimate(input)) return false; + impl.context11->CopyResource(impl.sharedMotion11.get(), impl.opticalFlow->GetMotionTexture()); + } + const uint64_t inputReady = ++impl.fenceValue; + HRESULT hr = impl.context11->Signal(impl.fence11.get(), inputReady); + if (FAILED(hr)) return false; + impl.context11->Flush(); + hr = impl.queue12->Wait(impl.fence12.get(), inputReady); + if (FAILED(hr)) return false; + + hr = impl.allocator12->Reset(); + if (SUCCEEDED(hr)) hr = impl.commandList12->Reset(impl.allocator12.get(), nullptr); + if (FAILED(hr)) return false; + D3D12_RESOURCE_BARRIER barriers[3]{}; + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition = { impl.sharedInput12.get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE }; + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition = { impl.sharedOutput12.get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_UNORDERED_ACCESS }; + const UINT barrierCount = impl.enableOpticalFlow ? 3 : 2; + if (impl.enableOpticalFlow) { + barriers[2].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[2].Transition = { impl.sharedMotion12.get(), D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE }; + } + impl.commandList12->ResourceBarrier(barrierCount, barriers); + + ffxDispatchDescUpscale desc{}; + desc.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE; + desc.commandList = impl.commandList12.get(); + desc.color = ffxApiGetResourceDX12(impl.sharedInput12.get(), FFX_API_RESOURCE_STATE_COMPUTE_READ); + desc.depth = ffxApiGetResourceDX12(impl.flatDepth12.get(), FFX_API_RESOURCE_STATE_COMPUTE_READ); + desc.motionVectors = ffxApiGetResourceDX12( + impl.enableOpticalFlow ? impl.sharedMotion12.get() : impl.zeroMotion12.get(), + FFX_API_RESOURCE_STATE_COMPUTE_READ); + desc.exposure = ffxApiGetResourceDX12(impl.exposure12.get(), FFX_API_RESOURCE_STATE_COMPUTE_READ); + desc.reactive = ffxApiGetResourceDX12(impl.reactive12.get(), FFX_API_RESOURCE_STATE_COMPUTE_READ); + desc.transparencyAndComposition = ffxApiGetResourceDX12( + impl.transparency12.get(), FFX_API_RESOURCE_STATE_COMPUTE_READ); + desc.output = ffxApiGetResourceDX12(impl.sharedOutput12.get(), FFX_API_RESOURCE_STATE_UNORDERED_ACCESS); + if (impl.enableJitter) { + // Metadata-only jitter: the captured source frame itself is not projection-jittered. + const uint32_t sample = (impl.frameIndex++ & 7u) + 1u; + desc.jitterOffset = { Halton(sample, 2) - 0.5f, Halton(sample, 3) - 0.5f }; + } else { + desc.jitterOffset = { 0.0f, 0.0f }; + } + desc.motionVectorScale = { 1.0f, 1.0f }; + desc.renderSize = { impl.inputWidth, impl.inputHeight }; + desc.upscaleSize = { impl.outputWidth, impl.outputHeight }; + desc.enableSharpening = true; + desc.sharpness = 0.2f; + desc.frameTimeDelta = 16.6667f; + desc.preExposure = 1.0f; + desc.reset = impl.resetHistory; + desc.cameraNear = 1.0f; + desc.cameraFar = FLT_MAX; + desc.cameraFovAngleVertical = 1.04719755f; + desc.viewSpaceToMetersFactor = 1.0f; + desc.flags = FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB; + const ffxReturnCode_t rc = impl.dispatch(&impl.context, &desc.header); + if (rc != FFX_API_RETURN_OK) { + Logger::Get().Error(fmt::format("Dispatch {} failed ({})", + impl.useFsr4 ? "FSR 4.1.1" : "FSR 3.1.5", (uint32_t)rc)); + return false; + } + for (UINT i = 0; i < barrierCount; ++i) { + std::swap(barriers[i].Transition.StateBefore, barriers[i].Transition.StateAfter); + } + impl.commandList12->ResourceBarrier(barrierCount, barriers); + hr = impl.commandList12->Close(); + if (FAILED(hr)) return false; + ID3D12CommandList* lists[]{ impl.commandList12.get() }; + impl.queue12->ExecuteCommandLists(1, lists); + const uint64_t outputReady = ++impl.fenceValue; + hr = impl.queue12->Signal(impl.fence12.get(), outputReady); + impl.lastSubmittedValue = outputReady; + if (SUCCEEDED(hr)) hr = impl.context11->Wait(impl.fence11.get(), outputReady); + if (FAILED(hr)) return false; + impl.context11->CopyResource(output, impl.sharedOutput11.get()); + impl.resetHistory = false; + return true; +} + +} + +#else + +namespace Magpie { +struct FSR3ZeroMVUpscaler::Impl {}; +FSR3ZeroMVUpscaler::FSR3ZeroMVUpscaler() = default; +FSR3ZeroMVUpscaler::~FSR3ZeroMVUpscaler() = default; +bool FSR3ZeroMVUpscaler::Initialize(DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*, bool, bool, bool) noexcept { + Logger::Get().Error("FSR3 support is not enabled in this build"); + return false; +} +bool FSR3ZeroMVUpscaler::Resize(DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*) noexcept { return false; } +bool FSR3ZeroMVUpscaler::Draw(const NativeEffectDrawContext&) noexcept { return false; } +} + +#endif diff --git a/src/Magpie.Core/FSR3ZeroMVUpscaler.h b/src/Magpie.Core/FSR3ZeroMVUpscaler.h new file mode 100644 index 000000000..95510b18b --- /dev/null +++ b/src/Magpie.Core/FSR3ZeroMVUpscaler.h @@ -0,0 +1,33 @@ +#pragma once +#include "NativeEffectBackend.h" + +namespace Magpie { + +class DeviceResources; + +// Experimental FSR 3.1.5 / FSR 4.1.1 upscaler running on D3D12 through +// resources shared with Magpie's D3D11 renderer. Frame generation is omitted. +class FSR3ZeroMVUpscaler final : public NativeEffectBackend { +public: + struct Impl; + + FSR3ZeroMVUpscaler(); + FSR3ZeroMVUpscaler(const FSR3ZeroMVUpscaler&) = delete; + FSR3ZeroMVUpscaler& operator=(const FSR3ZeroMVUpscaler&) = delete; + ~FSR3ZeroMVUpscaler() override; + + bool Initialize(DeviceResources& resources, ID3D11Texture2D* input, + ID3D11Texture2D* output, bool enableOpticalFlow = false, + bool enableJitter = false, bool useFsr4 = false) noexcept; + bool Resize(DeviceResources& resources, ID3D11Texture2D* input, + ID3D11Texture2D* output) noexcept override; + bool Draw(const NativeEffectDrawContext& context) noexcept override; + +private: + std::unique_ptr _impl; + bool _enableOpticalFlow = false; + bool _enableJitter = false; + bool _useFsr4 = false; +}; + +} diff --git a/src/Magpie.Core/FrameGuidanceService.cpp b/src/Magpie.Core/FrameGuidanceService.cpp index 1317ac868..52a204590 100644 --- a/src/Magpie.Core/FrameGuidanceService.cpp +++ b/src/Magpie.Core/FrameGuidanceService.cpp @@ -3,6 +3,7 @@ #include "DeviceResources.h" #include "Logger.h" #include "DirectXHelper.h" +#include "ScalingWindow.h" namespace Magpie { @@ -216,8 +217,12 @@ struct FrameGuidanceService::AdapterCache { } } static constexpr float ZERO[4]{}; - for (const auto& uav : entry->zeroUavs) { - context->ClearUnorderedAccessViewFloat(uav.get(), ZERO); + static constexpr float ONE[4]{ 1.0f, 1.0f, 1.0f, 1.0f }; + const float* depthClear = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? ONE : ZERO; + for (size_t i = 0; i < entry->zeroUavs.size(); ++i) { + context->ClearUnorderedAccessViewFloat( + entry->zeroUavs[i].get(), i == 1 ? depthClear : ZERO); } Entry* result = entry.get(); entries.push_back(std::move(entry)); @@ -507,7 +512,11 @@ bool FrameGuidanceService::Initialize( const FrameGuidanceView& FrameGuidanceService::BeginFrame( FrameGuidanceFrameId frameId, ID3D11Texture2D* sourceFrame, - const FrameGuidanceRequirements& requirements + const FrameGuidanceRequirements& requirements, + uint64_t captureSequence, + uint64_t resourceGeneration, + int64_t timestamp100ns, + const ColorDescription& colorDescription ) noexcept { const FrameGuidanceExtent extent = GetTextureExtent(sourceFrame); if (_hasCachedFrame && _cachedFrameId == frameId && extent == _sourceExtent) { @@ -521,6 +530,10 @@ const FrameGuidanceView& FrameGuidanceService::BeginFrame( return _Produce({ .color = sourceFrame, .frameId = frameId, + .captureSequence = captureSequence, + .resourceGeneration = resourceGeneration, + .timestamp100ns = timestamp100ns, + .colorDescription = colorDescription, .sourceExtent = _sourceExtent, .validRegion = FrameGuidanceRegion::Full(_sourceExtent) }, requirements); diff --git a/src/Magpie.Core/FrameGuidanceService.h b/src/Magpie.Core/FrameGuidanceService.h index 975ce8be8..3c8f011c2 100644 --- a/src/Magpie.Core/FrameGuidanceService.h +++ b/src/Magpie.Core/FrameGuidanceService.h @@ -31,7 +31,11 @@ class FrameGuidanceService { const FrameGuidanceView& BeginFrame( FrameGuidanceFrameId frameId, ID3D11Texture2D* sourceFrame, - const FrameGuidanceRequirements& requirements + const FrameGuidanceRequirements& requirements, + uint64_t captureSequence = 0, + uint64_t resourceGeneration = 0, + int64_t timestamp100ns = 0, + const ColorDescription& colorDescription = {} ) noexcept; bool Resize( FrameGuidanceExtent sourceExtent, diff --git a/src/Magpie.Core/FrameGuidanceTypes.h b/src/Magpie.Core/FrameGuidanceTypes.h index 0ed4f2386..2a7e63577 100644 --- a/src/Magpie.Core/FrameGuidanceTypes.h +++ b/src/Magpie.Core/FrameGuidanceTypes.h @@ -1,5 +1,6 @@ #pragma once #include "MotionVectorRequest.h" +#include "HdrFrame.h" namespace Magpie { @@ -62,7 +63,10 @@ struct FrameGuidanceSyncPoint { }; struct FrameGuidanceMetadata { - FrameGuidanceFrameId frameId = 0; + FrameGuidanceFrameId frameId = 0; + uint64_t captureSequence = 0; + uint64_t resourceGeneration = 0; + int64_t timestamp100ns = 0; FrameGuidanceExtent sourceExtent{}; FrameGuidanceRegion validRegion{}; FrameGuidanceSyncPoint sync{}; @@ -136,8 +140,12 @@ inline FrameGuidanceView SelectFrameGuidanceChannels( struct MotionVectorProviderOutput; struct FrameGuidanceFrame { - ID3D11Texture2D* color = nullptr; - FrameGuidanceFrameId frameId = 0; + ID3D11Texture2D* color = nullptr; + FrameGuidanceFrameId frameId = 0; + uint64_t captureSequence = 0; + uint64_t resourceGeneration = 0; + int64_t timestamp100ns = 0; + ColorDescription colorDescription{}; FrameGuidanceExtent sourceExtent{}; FrameGuidanceRegion validRegion{}; }; diff --git a/src/Magpie.Core/FrameSourceBase.cpp b/src/Magpie.Core/FrameSourceBase.cpp index 78869e8a0..fd4c6598f 100644 --- a/src/Magpie.Core/FrameSourceBase.cpp +++ b/src/Magpie.Core/FrameSourceBase.cpp @@ -5,6 +5,8 @@ #include "DeviceResources.h" #include "DirectXHelper.h" #include "Logger.h" +#include "Win32Helper.h" +#include "HdrDiagnostics.h" #include "ScalingOptions.h" #include "ScalingWindow.h" #include "shaders/DuplicateFrameCS.h" @@ -47,6 +49,22 @@ bool FrameSourceBase::Initialize(DeviceResources& deviceResources, BackendDescri return false; } + _hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); + if (_hdrEnabled && + !_hdrProcessor.Initialize(deviceResources, descriptorStore)) { + Logger::Get().Error("初始化 HDR 捕获处理器失败"); + return false; + } + if (_hdrEnabled) { + D3D11_TEXTURE2D_DESC sourceDesc{}; + _output->GetDesc(&sourceDesc); + if (!_hdrProcessor.Prepare( + _output.get(), sourceDesc.Format, _GetSourceColorDescription())) { + Logger::Get().Error("准备 HDR 捕获输出失败"); + return false; + } + } + assert(_output); _outputSrv = descriptorStore.GetShaderResourceView(_output.get()); if (!_outputSrv) { @@ -59,6 +77,48 @@ bool FrameSourceBase::Initialize(DeviceResources& deviceResources, BackendDescri FrameSourceState FrameSourceBase::Update() noexcept { const FrameSourceState state = _Update(); + if (state == FrameSourceState::NewFrame && _hdrEnabled) { + if (_hdrFrameSequence != _captureSequence) { + ++_resourceGeneration; + _hdrFrameSequence = _captureSequence; + } + D3D11_TEXTURE2D_DESC sourceDesc{}; + _output->GetDesc(&sourceDesc); + ColorDescription sourceColor = _GetSourceColorDescription(); + if (!_hdrProcessor.Process(_output.get(), HdrFrameMetadata{ + .frameId = _captureSequence, + .captureSequence = _captureSequence, + .resourceGeneration = _resourceGeneration, + .timestamp100ns = _captureTimestamp100ns, + .width = sourceDesc.Width, + .height = sourceDesc.Height, + .sourceFormat = sourceDesc.Format, + .color = sourceColor, + .stage = HdrFrameStage::RawCapture, + .valid = true + })) { + _hdrFrameReady = false; + Logger::Get().Error("HDR 捕获帧处理失败"); + return FrameSourceState::Error; + } else { + _hdrFrameReady = true; + if (!_hdrDiagnosticsLogged) { + HdrDiagnostics diagnostics{}; + diagnostics.hdrOptionEnabled = true; + diagnostics.captureMethod = Name(); + diagnostics.sourceFormat = sourceDesc.Format; + diagnostics.sourceColorDescription = _hdrProcessor.GetFrameMetadata().color; + diagnostics.selectedAdapterProfile = HdrAdapterProfile::DirectFP16; + diagnostics.conversionPath = "capture->canonicalFP16"; + if (!_hdrProcessor.LastAssumption().empty()) AppendHdrAssumption(diagnostics, _hdrProcessor.LastAssumption()); + LogHdrDiagnostics(diagnostics, true); + _hdrDiagnosticsLogged = true; + } + } + } + if (_hdrEnabled && state == FrameSourceState::NewFrame && !_hdrFrameReady) { + return FrameSourceState::Error; + } const bool newSequence = state == FrameSourceState::NewFrame && _duplicateCaptureSequence != _captureSequence; if (newSequence) { @@ -165,6 +225,125 @@ FrameSourceState FrameSourceBase::Update() noexcept { } } +ColorDescription FrameSourceBase::_GetSourceColorDescription() const noexcept { + ColorDescription result{}; + if (!_deviceResources) return result; + + const HMONITOR monitor = MonitorFromWindow( + ScalingWindow::Get().SrcTracker().Handle(), MONITOR_DEFAULTTONEAREST); + if (!monitor) return result; + + IDXGIAdapter4* adapter = _deviceResources->GetGraphicsAdapter(); + if (!adapter) return result; + + for (UINT index = 0; ; ++index) { + winrt::com_ptr output; + if (FAILED(adapter->EnumOutputs(index, output.put()))) break; + + DXGI_OUTPUT_DESC outputDesc{}; + if (FAILED(output->GetDesc(&outputDesc)) || outputDesc.Monitor != monitor) continue; + + winrt::com_ptr output6 = output.try_as(); + if (!output6) return result; + + DXGI_OUTPUT_DESC1 desc1{}; + if (FAILED(output6->GetDesc1(&desc1))) return result; + + result.dxgiColorSpace = desc1.ColorSpace; + result.displayHdrEnabled = + desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 || + desc1.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + const bool measuredPeak = desc1.MaxLuminance > 0.0f; + result.displayPeakNits = measuredPeak ? desc1.MaxLuminance : 1000.0f; + const float measuredSdrWhite = Win32Helper::GetMonitorSdrWhiteNits(monitor); + // Some HDR monitors report MaxLuminance at or below the SDR white + // level (observed: 80 nit with a 360-nit SDR white). The bounded + // route derives its normalized peak from displayPeak/sdrWhite, so + // a peak below the white point leaves zero HDR headroom. Keep the + // peak at or above the measured SDR white and make the anomaly + // visible in the log instead of degrading silently. + const float sdrWhiteFloor = measuredSdrWhite > 0.0f ? measuredSdrWhite : 80.0f; + if (result.displayPeakNits < sdrWhiteFloor) { + Logger::Get().Warn(fmt::format( + "HDR source display peak {:.1f} nit is below the SDR white " + "{:.1f} nit; raising the peak to the SDR white level", + result.displayPeakNits, sdrWhiteFloor)); + result.displayPeakNits = sdrWhiteFloor; + } + result.metadata.maxMasteringLuminanceNits = desc1.MaxLuminance; + result.metadata.minMasteringLuminanceNits = desc1.MinLuminance; + result.metadata.maxFrameAverageLightLevelNits = desc1.MaxFullFrameLuminance; + // WGC scRGB stores scene-linear values with 1.0 == 80 nit. SDR content + // rendered on an HDR desktop is raised by the monitor's SDR white-level + // setting (for example 4.5x == 360 nit), so the source description must + // carry that measured white point for the paired SDR bridge. + result.referenceWhiteNits = 80.0f; + result.sdrWhiteNits = measuredSdrWhite > 0.0f ? measuredSdrWhite : 80.0f; + if (measuredSdrWhite <= 0.0f && result.displayHdrEnabled) { + // Silent 80-nit fallback shifts every downstream normalization by + // the monitor's real SDR white ratio (e.g. 4.5x); make it visible. + Logger::Get().Warn(fmt::format( + "HDR source SDR-white measurement failed; falling back to 80 nit. " + "Display peak={:.1f} colorSpace={:#x}", + result.displayPeakNits, + static_cast(desc1.ColorSpace))); + } + result.range = HdrColorRange::Full; + result.isInferred = !measuredPeak; + + D3D11_TEXTURE2D_DESC capturedDesc{}; + if (_output) { + _output->GetDesc(&capturedDesc); + } + if (capturedDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM || + capturedDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM) { + // GDI, DWM shared-surface, and Desktop Duplication expose the + // selected capture as an 8-bit display-referred surface. Monitor + // PQ metadata describes the display, not these stored code values. + result.dxgiColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + result.primaries = HdrColorPrimaries::Rec709; + result.transfer = HdrTransferFunction::SRGB; + result.range = HdrColorRange::Full; + result.isSceneReferred = false; + result.displayHdrEnabled = false; + result.isInferred = false; + return result; + } + + switch (desc1.ColorSpace) { + case DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020: + result.primaries = HdrColorPrimaries::Rec2020; + result.transfer = HdrTransferFunction::PQ; + break; + case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709: + result.primaries = HdrColorPrimaries::Rec709; + result.transfer = HdrTransferFunction::Linear; + result.range = HdrColorRange::SceneLinear; + result.isSceneReferred = true; + break; + case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709: + default: + result.primaries = HdrColorPrimaries::Rec709; + result.transfer = HdrTransferFunction::SRGB; + result.range = HdrColorRange::Full; + break; + } + return result; + } + + return result; +} + +bool FrameSourceBase::PrepareHdrOutputForResize() noexcept { + if (!_hdrEnabled || !_output) return true; + D3D11_TEXTURE2D_DESC sourceDesc{}; + _output->GetDesc(&sourceDesc); + _hdrFrameReady = false; + ++_resourceGeneration; + _hdrFrameSequence = 0; + return _hdrProcessor.Prepare(_output.get(), sourceDesc.Format, _GetSourceColorDescription()); +} + std::pair FrameSourceBase::GetStatisticsForDynamicDetection() const noexcept { return _statistics.load(std::memory_order_relaxed); } diff --git a/src/Magpie.Core/FrameSourceBase.h b/src/Magpie.Core/FrameSourceBase.h index d5a744acf..30afc9348 100644 --- a/src/Magpie.Core/FrameSourceBase.h +++ b/src/Magpie.Core/FrameSourceBase.h @@ -1,4 +1,5 @@ #pragma once +#include "HdrCaptureProcessor.h" namespace Magpie { @@ -37,6 +38,7 @@ class FrameSourceBase { // Backend-thread state. A sequence changes only on a real capture discontinuity; // the first valid frame must reach temporal consumers even if its pixels match. uint64_t CaptureSequence() const noexcept { return _captureSequence; } + uint64_t ResourceGeneration() const noexcept { return _resourceGeneration; } bool IsCaptureInterrupted() const noexcept { return _captureInterrupted; } int64_t CaptureTimestamp100ns() const noexcept { return _captureTimestamp100ns; } const char* CaptureErrorContext() const noexcept { return _captureErrorContext; } @@ -46,10 +48,36 @@ class FrameSourceBase { _forceDuplicateFrameDetection = value; } - ID3D11Texture2D* GetOutput() noexcept { - return _output.get(); + // Compatibility getter. HDR callers must use GetCanonicalFrame(); SDR keeps + // the original raw capture texture contract. + ID3D11Texture2D* GetOutput() noexcept { return _hdrEnabled ? + _hdrProcessor.GetCanonicalTexture() : _output.get(); } + + ID3D11Texture2D* GetPipelineTexture() const noexcept { + return _hdrEnabled ? _hdrProcessor.GetCanonicalTexture() : _output.get(); + } + + HdrFrame GetCanonicalFrame() const noexcept { + if (!_hdrEnabled) return {}; + HdrFrame frame{ + .texture = _hdrProcessor.GetCanonicalTexture(), + .metadata = _hdrProcessor.GetFrameMetadata(), + .workingFormat = DXGI_FORMAT_R16G16B16A16_FLOAT + }; + return frame; } + // Raw capture is an internal capture-front-end input. Renderer/effect code + // must use GetCanonicalFrame() in HDR mode. + ID3D11Texture2D* GetRawCaptureTexture() const noexcept { return _output.get(); } + + const HdrFrameMetadata& GetHdrFrameMetadata() const noexcept { + return _hdrProcessor.GetFrameMetadata(); + } + + bool PrepareHdrOutputForResize() noexcept; + bool IsHdrFrameReady() const noexcept { return !_hdrEnabled || _hdrFrameReady; } + std::pair GetStatisticsForDynamicDetection() const noexcept; virtual const char* Name() const noexcept = 0; @@ -61,6 +89,7 @@ class FrameSourceBase { virtual void OnCursorVisibilityChanged(bool /*isVisible*/, bool /*onDestory*/) noexcept {}; protected: + virtual ColorDescription _GetSourceColorDescription() const noexcept; uint64_t _captureSequence = 0; bool _captureInterrupted = false; int64_t _captureTimestamp100ns = 0; @@ -85,6 +114,12 @@ class FrameSourceBase { DeviceResources* _deviceResources = nullptr; BackendDescriptorStore* _descriptorStore = nullptr; winrt::com_ptr _output; + HdrCaptureProcessor _hdrProcessor; + bool _hdrEnabled = false; + bool _hdrFrameReady = false; + uint64_t _hdrFrameSequence = 0; + uint64_t _resourceGeneration = 1; + bool _hdrDiagnosticsLogged = false; ID3D11ShaderResourceView* _outputSrv = nullptr; winrt::com_ptr _resultBuffer; diff --git a/src/Magpie.Core/GraphicsCaptureFrameSource.cpp b/src/Magpie.Core/GraphicsCaptureFrameSource.cpp index 5d824d4d8..b761d86bc 100644 --- a/src/Magpie.Core/GraphicsCaptureFrameSource.cpp +++ b/src/Magpie.Core/GraphicsCaptureFrameSource.cpp @@ -78,7 +78,9 @@ bool GraphicsCaptureFrameSource::_Initialize() noexcept { _output = DirectXHelper::CreateTexture2D( d3dDevice, - DXGI_FORMAT_B8G8R8A8_UNORM, + ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? DXGI_FORMAT_R16G16B16A16_FLOAT + : DXGI_FORMAT_B8G8R8A8_UNORM, _frameBox.right - _frameBox.left, _frameBox.bottom - _frameBox.top, D3D11_BIND_SHADER_RESOURCE @@ -92,6 +94,24 @@ bool GraphicsCaptureFrameSource::_Initialize() noexcept { return true; } +ColorDescription GraphicsCaptureFrameSource::_GetSourceColorDescription() const noexcept { + ColorDescription result = FrameSourceBase::_GetSourceColorDescription(); + if (!ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) { + return result; + } + + // WGC's FP16 capture surface uses linear scRGB. The monitor metadata still + // supplies the display peak used by normalization. + result.dxgiColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; + result.primaries = HdrColorPrimaries::Rec709; + result.transfer = HdrTransferFunction::Linear; + result.range = HdrColorRange::SceneLinear; + result.isSceneReferred = true; + result.isInferred = true; + result.preExposure = 1.0f; + return result; +} + bool GraphicsCaptureFrameSource::Start() noexcept { _DisableRoundCornerInWin11(); return _StartCapture("initial start"); @@ -164,8 +184,12 @@ FrameSourceState GraphicsCaptureFrameSource::_Update() noexcept { } frame.Close(); } else { - if (_lastFrameTimestamp100ns && timestamp - _lastFrameTimestamp100ns >= 5'000'000) { - _InterruptCapture("capture timestamp discontinuity"); + // Preserve the long-pause optimization, with a 5-second debounce. + // WGC can legitimately skip hundreds of milliseconds for static or + // throttled windows; those gaps must not reset FG history and flash. + if (_lastFrameTimestamp100ns && + timestamp - _lastFrameTimestamp100ns >= 50'000'000) { + _InterruptCapture("capture long-pause discontinuity"); } _deviceResources->GetD3DDC()->CopySubresourceRegion( _output.get(), 0, 0, 0, 0, texture.get(), 0, &_frameBox); @@ -439,7 +463,9 @@ bool GraphicsCaptureFrameSource::_StartCapture(const char* reason) noexcept { #endif _captureFramePool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded( _wrappedD3DDevice, - winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized, + ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? winrt::DirectXPixelFormat::R16G16B16A16Float + : winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized, 4, // 帧的缓存数量,更大的值有利于在低帧率下降低延迟 { (int)_frameBox.right, (int)_frameBox.bottom } // 帧的尺寸为包含源窗口的最小尺寸 ); diff --git a/src/Magpie.Core/GraphicsCaptureFrameSource.h b/src/Magpie.Core/GraphicsCaptureFrameSource.h index 83c8af363..703b375ea 100644 --- a/src/Magpie.Core/GraphicsCaptureFrameSource.h +++ b/src/Magpie.Core/GraphicsCaptureFrameSource.h @@ -28,6 +28,7 @@ class GraphicsCaptureFrameSource final : public FrameSourceBase { protected: bool _Initialize() noexcept override; + ColorDescription _GetSourceColorDescription() const noexcept override; FrameSourceState _Update() noexcept override; diff --git a/src/Magpie.Core/GroupAHdrRoutes.cpp b/src/Magpie.Core/GroupAHdrRoutes.cpp new file mode 100644 index 000000000..01d94f016 --- /dev/null +++ b/src/Magpie.Core/GroupAHdrRoutes.cpp @@ -0,0 +1,114 @@ +#include "pch.h" +#include "GroupAHdrRoutes.h" + +namespace Magpie { + +namespace { + +HdrFormatRoute MakeSdrFallback( + std::string_view effect, + std::string_view option, + DXGI_FORMAT backendFormat = DXGI_FORMAT_R8G8B8A8_UNORM, + HdrAlphaMode alpha = HdrAlphaMode::ForceOpaque +) { + return HdrFormatRoute{ + .effectId = std::string(effect), + .optionId = std::string(option), + .inputFormat = backendFormat, + .outputFormat = backendFormat, + .inputTransfer = HdrTransferFunction::SRGB, + .outputTransfer = HdrTransferFunction::SRGB, + .inputRange = HdrColorRange::Full, + .outputRange = HdrColorRange::Full, + .alphaMode = alpha, + .evidenceLevel = HdrEvidenceLevel::None, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::SDRCompatible, + .defaultForHdr = true, + .defaultForSdr = true, + }; +} + +HdrFormatRoute MakeCasFp16() { + return HdrFormatRoute{ + .effectId = "CAS", + .optionId = "fp16-conditional", + .inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT, + .inputTransfer = HdrTransferFunction::Linear, + .outputTransfer = HdrTransferFunction::Linear, + .inputRange = HdrColorRange::SceneLinear, + .outputRange = HdrColorRange::SceneLinear, + .alphaMode = HdrAlphaMode::ForceOpaque, + .evidenceLevel = HdrEvidenceLevel::ReferenceImplementation, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::ConditionalFP16, + .defaultForHdr = true, + .defaultForSdr = false, + }; +} + +bool IsGroupA(std::string_view effect) noexcept { + return effect == "Anime4K" || effect == "CAS" || effect == "CRT" || + effect == "CuNNy" || effect == "CuNNy2" || effect == "Diagnostics" || + effect == "FSRCNNX" || effect == "FXAA" || effect == "MLAA"; +} + +} + +GroupAHdrEffectDescription GetGroupAHdrEffectDescription( + std::string_view effectName, + int casFormatOption +) { + if (effectName == "CAS") { + HdrFormatRoutes routes; + // The production CAS CSO is compiled from the declared R8 texture + // contract. Keep HDR on the paired SDR adapter until a separately + // compiled FP16 CAS variant exists; changing the runtime surface alone + // would bind a route-incompatible UAV/SRV pair. + routes.push_back(MakeSdrFallback("CAS", "r8-sdr", DXGI_FORMAT_R8G8B8A8_UNORM)); + return { + .routes = std::move(routes), + .auxiliaryResources = "none; RGB input/output, alpha forced opaque", + .evidence = casFormatOption == 1 + ? "CAS FP16 request held behind a separately compiled variant; using declared R8 path" + : "Current Magpie CAS shader uses the declared R8 path", + }; + } + + if (!IsGroupA(effectName)) { + return {}; + } + + const char* auxiliary = "none"; + const char* evidence = "local shader declaration; public HDR protocol unspecified"; + if (effectName == "Anime4K") { + auxiliary = "variant-specific R16G16B16A16_FLOAT CNN surfaces; Thin_HQ R16G16_FLOAT gradient"; + } else if (effectName == "CRT") { + auxiliary = "GTU_v050 tex1 R16G16B16A16_FLOAT; other presets none"; + } else if (effectName == "CuNNy") { + auxiliary = "model-specific R8G8B8A8_SNORM t0..t7"; + } else if (effectName == "CuNNy2") { + auxiliary = "model-specific R8G8B8A8_UNORM T0..T15"; + } else if (effectName == "FSRCNNX") { + auxiliary = "featureMap1/2 and tex1..tex4 R16G16B16A16_FLOAT"; + } else if (effectName == "MLAA") { + auxiliary = "edgeMask R8G8_UNORM; edgeCounts R8G8B8A8_UNORM"; + } + + HdrFormatRoutes routes; + const HdrAlphaMode alpha = effectName == "Diagnostics" || effectName == "MLAA" + ? HdrAlphaMode::Preserve : HdrAlphaMode::ForceOpaque; + routes.push_back(MakeSdrFallback(effectName, "unknown-sdr-fallback", DXGI_FORMAT_R8G8B8A8_UNORM, alpha)); + return { + .routes = std::move(routes), + .auxiliaryResources = auxiliary, + .evidence = evidence, + }; +} + +HdrFormatRoutes GetGroupAHdrRoutes(std::string_view effectName, int casFormatOption) { + return GetGroupAHdrEffectDescription(effectName, casFormatOption).routes; +} + +} diff --git a/src/Magpie.Core/GroupAHdrRoutes.h b/src/Magpie.Core/GroupAHdrRoutes.h new file mode 100644 index 000000000..7b95922da --- /dev/null +++ b/src/Magpie.Core/GroupAHdrRoutes.h @@ -0,0 +1,25 @@ +#pragma once + +#include "HdrProtocol.h" + +#include + +namespace Magpie { + +// Effect-local protocol description for group A. The auxiliary text is kept +// alongside the structured route so diagnostics can expose intermediate +// resource boundaries without teaching the shared HDR dispatcher effect names. +struct GroupAHdrEffectDescription { + HdrFormatRoutes routes; + std::string_view auxiliaryResources; + std::string_view evidence; +}; + +GroupAHdrEffectDescription GetGroupAHdrEffectDescription( + std::string_view effectName, + int casFormatOption = 0 +); + +HdrFormatRoutes GetGroupAHdrRoutes(std::string_view effectName, int casFormatOption = 0); + +} diff --git a/src/Magpie.Core/GroupBEffectProtocol.h b/src/Magpie.Core/GroupBEffectProtocol.h new file mode 100644 index 000000000..f4b227666 --- /dev/null +++ b/src/Magpie.Core/GroupBEffectProtocol.h @@ -0,0 +1,45 @@ +#pragma once + +#include "HdrFrame.h" + +#include +#include + +namespace Magpie { + +// Effect-local protocol values for the DLSS/FSR/NIS/Optical Flow family. +// These fields deliberately stay separate from the renderer-wide HDR model: +// each SDK consumes a different transfer, range, and auxiliary-resource set. +enum class GroupBTransfer : uint8_t { Unspecified, Linear, SRGB, PQ }; + +struct FsrHdrProtocol { + bool hdrColorInput = false; + GroupBTransfer transfer = GroupBTransfer::Linear; + float preExposure = 1.0f; + float exposure = 1.0f; + bool depthInverted = true; + bool depthInfinite = true; + bool useReactiveMask = true; + bool useTransparencyMask = true; +}; + +struct NisHdrProtocol { + enum class Mode : uint8_t { None, Linear, PQ }; + Mode mode = Mode::None; + float linearMax = 12.5f; +}; + +struct AmdOpticalFlowHdrProtocol { + GroupBTransfer transfer = GroupBTransfer::SRGB; + std::array minMaxLuminance{ 0.0f, 1.0f }; +}; + +struct DlssnrExperimentProtocol { + bool enabled = false; + // The FP16 route encodes the normalized color with the extended sRGB + // OETF (matching the model's U8 training contract below the white point + // and continuing the same power law for HDR headroom). No scale + // selector: the normalization is fully derived from the frame metadata. +}; + +} // namespace Magpie diff --git a/src/Magpie.Core/GroupBHdrRoutes.cpp b/src/Magpie.Core/GroupBHdrRoutes.cpp new file mode 100644 index 000000000..43d12d605 --- /dev/null +++ b/src/Magpie.Core/GroupBHdrRoutes.cpp @@ -0,0 +1,92 @@ +#include "pch.h" +#include "GroupBHdrRoutes.h" + +namespace Magpie { + +namespace { + +HdrFormatRoute MakeRoute( + std::string_view effect, + std::string_view option, + DXGI_FORMAT inputFormat, + DXGI_FORMAT outputFormat, + HdrAdapterProfile profile, + HdrTransferFunction inputTransfer, + HdrTransferFunction outputTransfer, + HdrColorRange inputRange, + HdrColorRange outputRange, + HdrEvidenceLevel evidence, + bool hdrNative, + float normalizationScale = 1.0f +) { + return HdrFormatRoute{ + .effectId = std::string(effect), + .optionId = std::string(option), + .inputFormat = inputFormat, + .outputFormat = outputFormat, + .inputTransfer = inputTransfer, + .outputTransfer = outputTransfer, + .inputRange = inputRange, + .outputRange = outputRange, + .alphaMode = HdrAlphaMode::ForceOpaque, + .evidenceLevel = evidence, + .hdrNative = hdrNative, + .adapterProfile = profile, + .defaultForHdr = true, + .defaultForSdr = false, + .normalizationScale = normalizationScale, + }; +} + +HdrFormatRoute MakeSdr(std::string_view effect, std::string_view option) { + return MakeRoute(effect, option, DXGI_FORMAT_R8G8B8A8_UNORM, + DXGI_FORMAT_R8G8B8A8_UNORM, HdrAdapterProfile::SDRCompatible, + HdrTransferFunction::SRGB, HdrTransferFunction::SRGB, + HdrColorRange::Full, HdrColorRange::Full, + HdrEvidenceLevel::PublicApiContract, false); +} + +} + +HdrFormatRoutes GetGroupBHdrRoutes( + std::string_view effectGroup, + bool experimentalDlssnr +) noexcept { + if (effectGroup == "DLSSNR") { + if (experimentalDlssnr) { + return { MakeRoute("DLSSNR", "experimental-fp16", + DXGI_FORMAT_R16G16B16A16_FLOAT, + DXGI_FORMAT_R16G16B16A16_FLOAT, + HdrAdapterProfile::BoundedHDR, + HdrTransferFunction::SRGB, + HdrTransferFunction::SRGB, + HdrColorRange::Full, + HdrColorRange::Full, + HdrEvidenceLevel::LocalValidation, + false) }; + } + return { MakeSdr("DLSSNR", "sdr-r8") }; + } + + if (effectGroup == "DLSS" || effectGroup == "FSR2" || + effectGroup == "FSR3" || effectGroup == "FSR4" || + effectGroup == "NIS") { + return { MakeRoute(effectGroup, "hdr-linear-fp16", + DXGI_FORMAT_R16G16B16A16_FLOAT, + DXGI_FORMAT_R16G16B16A16_FLOAT, + HdrAdapterProfile::DirectFP16, + HdrTransferFunction::Linear, + HdrTransferFunction::Linear, + HdrColorRange::SceneLinear, + HdrColorRange::SceneLinear, + HdrEvidenceLevel::PublicApiContract, true) }; + } + + if (effectGroup == "FSR") { + return { MakeSdr("FSR", "sdr-srgb") }; + } + + return {}; +} + +} diff --git a/src/Magpie.Core/GroupBHdrRoutes.h b/src/Magpie.Core/GroupBHdrRoutes.h new file mode 100644 index 000000000..cf5e5c014 --- /dev/null +++ b/src/Magpie.Core/GroupBHdrRoutes.h @@ -0,0 +1,12 @@ +#pragma once + +#include "HdrProtocol.h" + +namespace Magpie { + +HdrFormatRoutes GetGroupBHdrRoutes( + std::string_view effectGroup, + bool experimentalDlssnr = false +) noexcept; + +} diff --git a/src/Magpie.Core/HalfResOpticalFlow.cpp b/src/Magpie.Core/HalfResOpticalFlow.cpp new file mode 100644 index 000000000..32e593ed6 --- /dev/null +++ b/src/Magpie.Core/HalfResOpticalFlow.cpp @@ -0,0 +1,171 @@ +#include "pch.h" +#include "HalfResOpticalFlow.h" +#include "DirectXHelper.h" +#include "Logger.h" + +namespace Magpie { + +static constexpr char FLOW_HLSL[] = R"( +cbuffer FlowConstants : register(b0) { + uint2 FullSize; + uint2 HalfSize; +}; +Texture2D CurrentColor : register(t0); +Texture2D PreviousColor : register(t1); +RWTexture2D HalfFlowOut : register(u0); + +float Luma(float3 c) { return dot(c, float3(0.299, 0.587, 0.114)); } +int2 ClampPixel(int2 p) { return clamp(p, int2(0, 0), int2(FullSize) - 1); } +float SampleLuma(Texture2D tex, int2 p) { return Luma(tex.Load(int3(ClampPixel(p), 0)).rgb); } + +[numthreads(8, 8, 1)] +void EstimateHalf(uint3 tid : SV_DispatchThreadID) { + if (any(tid.xy >= HalfSize)) return; + int2 p = min(int2(tid.xy * 2 + 1), int2(FullSize) - 1); + static const int2 taps[5] = { + int2(0,0), int2(-2,0), int2(2,0), int2(0,-2), int2(0,2) + }; + float bestError = 3.402823e+38; + int2 bestOffset = int2(0, 0); + [unroll] for (int y = -2; y <= 2; ++y) { + [unroll] for (int x = -2; x <= 2; ++x) { + int2 candidate = int2(x, y) * 2; + float error = 0.0; + [unroll] for (int i = 0; i < 5; ++i) { + float a = SampleLuma(CurrentColor, p + taps[i]); + float b = SampleLuma(PreviousColor, p + candidate + taps[i]); + float d = a - b; + error += d * d; + } + // Prefer smaller motion when candidates are nearly equivalent. + error += dot(float2(candidate), float2(candidate)) * 0.000002; + if (error < bestError) { bestError = error; bestOffset = candidate; } + } + } + HalfFlowOut[tid.xy] = float2(bestOffset); +} + +Texture2D HalfFlowIn : register(t0); +RWTexture2D FullFlowOut : register(u0); + +[numthreads(8, 8, 1)] +void UpsampleFlow(uint3 tid : SV_DispatchThreadID) { + if (any(tid.xy >= FullSize)) return; + float2 hp = (float2(tid.xy) + 0.5) * 0.5 - 0.5; + int2 p0 = int2(floor(hp)); + float2 f = frac(hp); + int2 hi = int2(HalfSize) - 1; + float2 a = HalfFlowIn.Load(int3(clamp(p0, int2(0,0), hi), 0)); + float2 b = HalfFlowIn.Load(int3(clamp(p0 + int2(1,0), int2(0,0), hi), 0)); + float2 c = HalfFlowIn.Load(int3(clamp(p0 + int2(0,1), int2(0,0), hi), 0)); + float2 d = HalfFlowIn.Load(int3(clamp(p0 + int2(1,1), int2(0,0), hi), 0)); + FullFlowOut[tid.xy] = lerp(lerp(a,b,f.x), lerp(c,d,f.x), f.y); +} +)"; + +bool HalfResOpticalFlow::Initialize(ID3D11Device* device, ID3D11DeviceContext* context, + ID3D11Texture2D* input) noexcept { + _device = device; + _context = context; + D3D11_TEXTURE2D_DESC desc{}; + input->GetDesc(&desc); + _width = desc.Width; + _height = desc.Height; + _halfWidth = (_width + 1) / 2; + _halfHeight = (_height + 1) / 2; + + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + desc.MiscFlags = 0; + desc.CPUAccessFlags = 0; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.MipLevels = 1; + desc.ArraySize = 1; + HRESULT hr = _device->CreateTexture2D(&desc, nullptr, _previous.put()); + if (SUCCEEDED(hr)) hr = _device->CreateShaderResourceView(_previous.get(), nullptr, _previousSrv.put()); + if (FAILED(hr) || !_CreateInputSrv(input)) { + Logger::Get().ComError("Create optical-flow history resources failed", hr); + return false; + } + + _halfFlow = DirectXHelper::CreateTexture2D(_device, DXGI_FORMAT_R16G16_FLOAT, + _halfWidth, _halfHeight, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + _fullFlow = DirectXHelper::CreateTexture2D(_device, DXGI_FORMAT_R16G16_FLOAT, + _width, _height, D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_halfFlow || !_fullFlow) return false; + hr = _device->CreateShaderResourceView(_halfFlow.get(), nullptr, _halfFlowSrv.put()); + if (SUCCEEDED(hr)) hr = _device->CreateUnorderedAccessView(_halfFlow.get(), nullptr, _halfFlowUav.put()); + if (SUCCEEDED(hr)) hr = _device->CreateUnorderedAccessView(_fullFlow.get(), nullptr, _fullFlowUav.put()); + if (FAILED(hr)) return false; + + winrt::com_ptr blob; + if (!DirectXHelper::CompileComputeShader(FLOW_HLSL, "EstimateHalf", blob.put(), "HalfResOpticalFlow")) return false; + hr = _device->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _estimateShader.put()); + blob = nullptr; + if (FAILED(hr) || !DirectXHelper::CompileComputeShader(FLOW_HLSL, "UpsampleFlow", blob.put(), "HalfResOpticalFlow")) return false; + hr = _device->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _upsampleShader.put()); + if (FAILED(hr)) return false; + + struct Constants { UINT full[2]; UINT half[2]; } constants{ + {_width, _height}, {_halfWidth, _halfHeight} + }; + D3D11_BUFFER_DESC cbd{ .ByteWidth = sizeof(Constants), .Usage = D3D11_USAGE_IMMUTABLE, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER }; + D3D11_SUBRESOURCE_DATA initial{ .pSysMem = &constants }; + hr = _device->CreateBuffer(&cbd, &initial, _constantBuffer.put()); + if (FAILED(hr)) return false; + + static constexpr float ZERO[4]{}; + _context->ClearUnorderedAccessViewFloat(_fullFlowUav.get(), ZERO); + _context->CopyResource(_previous.get(), input); + _hasHistory = false; + Logger::Get().Info(fmt::format("Half-resolution optical flow initialized: {}x{} -> {}x{}", + _width, _height, _halfWidth, _halfHeight)); + return true; +} + +bool HalfResOpticalFlow::_CreateInputSrv(ID3D11Texture2D* input) noexcept { + if (_input == input && _inputSrv) return true; + _input = input; + _inputSrv = nullptr; + return SUCCEEDED(_device->CreateShaderResourceView(input, nullptr, _inputSrv.put())); +} + +bool HalfResOpticalFlow::Estimate(ID3D11Texture2D* input) noexcept { + if (!_CreateInputSrv(input)) return false; + if (!_hasHistory) { + static constexpr float ZERO[4]{}; + _context->ClearUnorderedAccessViewFloat(_fullFlowUav.get(), ZERO); + _context->CopyResource(_previous.get(), input); + _hasHistory = true; + return true; + } + + ID3D11Buffer* cb = _constantBuffer.get(); + _context->CSSetConstantBuffers(0, 1, &cb); + ID3D11ShaderResourceView* estimateSrvs[2]{ _inputSrv.get(), _previousSrv.get() }; + ID3D11UnorderedAccessView* halfUav = _halfFlowUav.get(); + _context->CSSetShader(_estimateShader.get(), nullptr, 0); + _context->CSSetShaderResources(0, 2, estimateSrvs); + _context->CSSetUnorderedAccessViews(0, 1, &halfUav, nullptr); + _context->Dispatch((_halfWidth + 7) / 8, (_halfHeight + 7) / 8, 1); + + ID3D11ShaderResourceView* nullSrvs[2]{}; + ID3D11UnorderedAccessView* nullUav = nullptr; + _context->CSSetShaderResources(0, 2, nullSrvs); + _context->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + ID3D11ShaderResourceView* halfSrv = _halfFlowSrv.get(); + ID3D11UnorderedAccessView* fullUav = _fullFlowUav.get(); + _context->CSSetShader(_upsampleShader.get(), nullptr, 0); + _context->CSSetShaderResources(0, 1, &halfSrv); + _context->CSSetUnorderedAccessViews(0, 1, &fullUav, nullptr); + _context->Dispatch((_width + 7) / 8, (_height + 7) / 8, 1); + _context->CSSetShaderResources(0, 2, nullSrvs); + _context->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + _context->CSSetShaderResources(0, 2, nullSrvs); + _context->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + _context->CSSetShader(nullptr, nullptr, 0); + _context->CopyResource(_previous.get(), input); + return true; +} + +} diff --git a/src/Magpie.Core/HalfResOpticalFlow.h b/src/Magpie.Core/HalfResOpticalFlow.h new file mode 100644 index 000000000..1e9838d7d --- /dev/null +++ b/src/Magpie.Core/HalfResOpticalFlow.h @@ -0,0 +1,43 @@ +#pragma once + +namespace Magpie { + +// Lightweight colour-only block matching. Motion is estimated on a half-size +// grid and expanded to render resolution for temporal upscalers. +class HalfResOpticalFlow { +public: + HalfResOpticalFlow() = default; + HalfResOpticalFlow(const HalfResOpticalFlow&) = delete; + HalfResOpticalFlow& operator=(const HalfResOpticalFlow&) = delete; + + bool Initialize(ID3D11Device* device, ID3D11DeviceContext* context, + ID3D11Texture2D* input) noexcept; + bool Estimate(ID3D11Texture2D* input) noexcept; + ID3D11Texture2D* GetMotionTexture() const noexcept { return _fullFlow.get(); } + void ResetHistory() noexcept { _hasHistory = false; } + +private: + bool _CreateInputSrv(ID3D11Texture2D* input) noexcept; + + ID3D11Device* _device = nullptr; + ID3D11DeviceContext* _context = nullptr; + ID3D11Texture2D* _input = nullptr; + UINT _width = 0; + UINT _height = 0; + UINT _halfWidth = 0; + UINT _halfHeight = 0; + bool _hasHistory = false; + winrt::com_ptr _previous; + winrt::com_ptr _previousSrv; + winrt::com_ptr _inputSrv; + winrt::com_ptr _halfFlow; + winrt::com_ptr _halfFlowSrv; + winrt::com_ptr _halfFlowUav; + winrt::com_ptr _fullFlow; + winrt::com_ptr _fullFlowUav; + winrt::com_ptr _estimateShader; + winrt::com_ptr _upsampleShader; + winrt::com_ptr _constantBuffer; +}; + +} diff --git a/src/Magpie.Core/HdrAdapterDispatcher.cpp b/src/Magpie.Core/HdrAdapterDispatcher.cpp new file mode 100644 index 000000000..92ab9f6e0 --- /dev/null +++ b/src/Magpie.Core/HdrAdapterDispatcher.cpp @@ -0,0 +1,96 @@ +#include "pch.h" +#include "HdrAdapterDispatcher.h" + +namespace Magpie { + +namespace { + +constexpr std::string_view PathForProfile(HdrAdapterProfile profile) noexcept { + switch (profile) { + case HdrAdapterProfile::DirectFP16: + return "canonicalFP16->DirectFP16->canonicalFP16"; + case HdrAdapterProfile::BoundedHDR: + return "canonicalFP16->boundedHdrEncode->backend->boundedHdrDecode->canonicalFP16"; + case HdrAdapterProfile::SDRCompatible: + return "canonicalFP16->HdrToSdr->backend->SdrToHdr->canonicalFP16"; + case HdrAdapterProfile::ConditionalFP16: + return "canonicalFP16->ConditionalFP16->canonicalFP16"; + case HdrAdapterProfile::PresentationTerminal: + return "canonicalFP16->presentationEncoder->presentationTerminal"; + case HdrAdapterProfile::Unknown: + default: + return "canonicalFP16->SdrCompatibleFallback->backend->SdrToHdr->canonicalFP16"; + } +} + +HdrTransformParameters ParametersForColor(const ColorDescription& color) noexcept { + const HdrTransformParameters params = HdrColorTransform::ForFrame(color); + return params.IsValid() ? params : HdrTransformParameters{}; +} + +} + +HdrAdapterPlan HdrAdapterDispatcher::BuildPlan( + const HdrFormatRoute& route, + const ColorDescription& sourceColor +) noexcept { + HdrAdapterPlan plan; + plan.routeId = route.Id(); + plan.profile = route.adapterProfile; + plan.alphaMode = route.alphaMode; + plan.forwardParameters = ParametersForColor(sourceColor); + plan.inverseParameters = ParametersForColor(sourceColor); + plan.normalizationScale = route.normalizationScale > 0.0f ? route.normalizationScale : 1.0f; + + switch (route.adapterProfile) { + case HdrAdapterProfile::DirectFP16: + plan.conversionPath = PathForProfile(HdrAdapterProfile::DirectFP16); + plan.requiresSdrMapping = false; + plan.requiresBoundedMapping = false; + break; + + case HdrAdapterProfile::BoundedHDR: + plan.conversionPath = PathForProfile(HdrAdapterProfile::BoundedHDR); + plan.requiresBoundedMapping = true; + break; + + case HdrAdapterProfile::SDRCompatible: + plan.conversionPath = PathForProfile(HdrAdapterProfile::SDRCompatible); + plan.requiresSdrMapping = true; + break; + + case HdrAdapterProfile::ConditionalFP16: + plan.conversionPath = PathForProfile(HdrAdapterProfile::ConditionalFP16); + plan.requiresSdrMapping = false; + plan.requiresBoundedMapping = false; + break; + + case HdrAdapterProfile::PresentationTerminal: + plan.conversionPath = PathForProfile(HdrAdapterProfile::PresentationTerminal); + plan.canonicalOutputFormat = DXGI_FORMAT_UNKNOWN; + plan.isPresentationTerminal = true; + break; + + case HdrAdapterProfile::Unknown: + default: + plan.conversionPath = PathForProfile(HdrAdapterProfile::Unknown); + plan.requiresSdrMapping = true; + plan.usesFallback = true; + plan.fallbackReason = "No verified HDR route; using the existing compatible SDR fallback."; + if (plan.alphaMode == HdrAlphaMode::Unknown) { + plan.alphaMode = HdrAlphaMode::ForceOpaque; + } + break; + } + + if (plan.alphaMode == HdrAlphaMode::Unknown) { + plan.alphaMode = HdrAlphaMode::ForceOpaque; + plan.fallbackReason = plan.fallbackReason.empty() + ? "Route did not declare an alpha rule; using ForceOpaque." + : plan.fallbackReason + " Route did not declare an alpha rule; using ForceOpaque."; + } + + return plan; +} + +} diff --git a/src/Magpie.Core/HdrAdapterDispatcher.h b/src/Magpie.Core/HdrAdapterDispatcher.h new file mode 100644 index 000000000..b2c8d6c6a --- /dev/null +++ b/src/Magpie.Core/HdrAdapterDispatcher.h @@ -0,0 +1,50 @@ +#pragma once + +#include "HdrColorTransform.h" +#include "HdrFrame.h" +#include "HdrProtocol.h" + +#include + +namespace Magpie { + +// Reusable plan produced by HdrAdapterDispatcher. This task deliberately does +// not attach the dispatcher to a concrete effect backend; the plan describes +// the conversion boundary so a future Renderer/effect integration can execute +// it without duplicating profile/route rules. +struct HdrAdapterPlan { + static constexpr DXGI_FORMAT CanonicalFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + + HdrAdapterProfile profile = HdrAdapterProfile::Unknown; + std::string routeId; + std::string conversionPath; + DXGI_FORMAT canonicalInputFormat = CanonicalFormat; + // Non-terminal routes always return to the canonical FP16 surface. + // PresentationTerminal leaves the ordinary canonical texture chain. + DXGI_FORMAT canonicalOutputFormat = CanonicalFormat; + HdrAlphaMode alphaMode = HdrAlphaMode::Unknown; + bool isPresentationTerminal = false; + bool requiresSdrMapping = false; + bool requiresBoundedMapping = false; + bool usesFallback = false; + std::string fallbackReason; + float normalizationScale = 1.0f; + // SDRCompatible and BoundedHDR use explicit paired forward/inverse state. + HdrTransformParameters forwardParameters; + HdrTransformParameters inverseParameters; + + bool IsNonTerminalCanonical() const noexcept { + return !isPresentationTerminal && + canonicalInputFormat == CanonicalFormat && + canonicalOutputFormat == CanonicalFormat; + } +}; + +// Selects conversion behavior from the structured route. No effect-name string +// is consulted and no concrete backend is invoked. +class HdrAdapterDispatcher { +public: + HdrAdapterPlan BuildPlan(const HdrFormatRoute& route, const ColorDescription& sourceColor) noexcept; +}; + +} diff --git a/src/Magpie.Core/HdrCaptureProcessor.cpp b/src/Magpie.Core/HdrCaptureProcessor.cpp new file mode 100644 index 000000000..de2035763 --- /dev/null +++ b/src/Magpie.Core/HdrCaptureProcessor.cpp @@ -0,0 +1,302 @@ +#include "pch.h" +#include "HdrCaptureProcessor.h" + +#include "BackendDescriptorStore.h" +#include "DeviceResources.h" +#include "DirectXHelper.h" +#include "Logger.h" + +namespace Magpie { + +HdrCaptureProcessor::~HdrCaptureProcessor() noexcept { + ResetForResize(); +} + +namespace { +constexpr char HLSL[] = R"( +cbuffer Transform : register(b0) { + float exposure; + float inverseExposure; + float sdrWhiteScale; + float hdrPeakNits; + float shoulder; + float sdrWhiteNits; + uint inputTransfer; + uint outputTransfer; + uint preserveAlpha; +}; +Texture2D sourceTexture : register(t0); +RWTexture2D outputTexture : register(u0); + +float DecodeSrgb(float value) { + return value <= 0.04045 ? value / 12.92 : pow(max((value + 0.055) / 1.055, 0.0), 2.4); +} +float DecodePq(float value) { + const float m1 = 2610.0 / 16384.0; + const float m2 = 2523.0 / 32.0; + const float c1 = 3424.0 / 4096.0; + const float c2 = 2413.0 / 128.0; + const float c3 = 2392.0 / 128.0; + float p = pow(max(saturate(value), 0.0), 1.0 / m2); + return pow(max(p - c1, 0.0) / max(c2 - c3 * p, 1e-6), 1.0 / m1) * 10000.0; +} +float DecodeTransfer(float value, uint transfer) { + if (transfer == 2) return DecodeSrgb(value); + if (transfer == 3) return DecodePq(value); + if (transfer == 4) { + const float a = 0.17883277; + const float b = 1.0 - 4.0 * a; + const float c = 0.5 - a * log(4.0 * a); + return value <= 0.5 ? (value * value) / 3.0 : (exp((value - c) / a) + b) / 12.0; + } + return value; +} +float3 ToCanonical(float3 value) { + float3 decoded = float3(DecodeTransfer(value.r, inputTransfer), DecodeTransfer(value.g, inputTransfer), DecodeTransfer(value.b, inputTransfer)); + // Canonical storage is linear scRGB. WGC FP16 already uses this contract. + // PQ decodes to absolute nits and is converted to scRGB (80 nits == 1.0). + // SDR/HLG relative values are embedded at their declared reference white. + if (inputTransfer == 3) decoded /= 80.0; + else if (inputTransfer != 1) decoded *= sdrWhiteNits / 80.0; + return decoded * exposure; +} +[numthreads(8, 8, 1)] +void Main(uint3 id : SV_DispatchThreadID) { + uint width, height; + outputTexture.GetDimensions(width, height); + if (id.x >= width || id.y >= height) return; + float4 value = sourceTexture.Load(int3(id.xy, 0)); + float3 canonical = ToCanonical(value.rgb); + outputTexture[id.xy] = float4(canonical, preserveAlpha != 0 ? value.a : 1.0); +} +)"; +} + +bool HdrCaptureProcessor::Initialize( + DeviceResources& deviceResources, + BackendDescriptorStore& descriptorStore +) noexcept { + _deviceResources = &deviceResources; + _descriptorStore = &descriptorStore; + winrt::com_ptr blob; + if (!DirectXHelper::CompileComputeShader(HLSL, "Main", blob.put(), "HdrCaptureProcessor", nullptr, {}, true)) { + return false; + } + HRESULT hr = deviceResources.GetD3DDevice()->CreateComputeShader( + blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _shader.put()); + if (FAILED(hr)) { + Logger::Get().ComError("创建 HDR 捕获处理 Compute Shader 失败", hr); + return false; + } + const D3D11_BUFFER_DESC desc{ + .ByteWidth = 48, + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE + }; + hr = deviceResources.GetD3DDevice()->CreateBuffer(&desc, nullptr, _constants.put()); + return SUCCEEDED(hr); +} + +bool HdrCaptureProcessor::_EnsureResources(const D3D11_TEXTURE2D_DESC& sourceDesc) noexcept { + if (_canonical) { + D3D11_TEXTURE2D_DESC current{}; + _canonical->GetDesc(¤t); + if (current.Width == sourceDesc.Width && current.Height == sourceDesc.Height) return true; + if (_descriptorStore) { + _descriptorStore->RemoveCache(_canonical.get()); + } + } + _canonical = DirectXHelper::CreateTexture2D( + _deviceResources->GetD3DDevice(), DXGI_FORMAT_R16G16B16A16_FLOAT, + sourceDesc.Width, sourceDesc.Height, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_canonical) return false; + _canonicalUav = _descriptorStore->GetUnorderedAccessView(_canonical.get()); + return _canonicalUav != nullptr; +} + +bool HdrCaptureProcessor::Process(ID3D11Texture2D* source, const HdrFrameMetadata& metadata) noexcept { + _lastAssumption.clear(); + if (!_deviceResources || !_shader || !source || metadata.width == 0 || metadata.height == 0) { + Logger::Get().Error(fmt::format( + "HDR capture processor rejected frame: device={} shader={} source={} size={}x{}", + _deviceResources != nullptr, _shader != nullptr, source != nullptr, + metadata.width, metadata.height)); + return false; + } + HdrFrameMetadata normalizedMetadata = metadata; + if (!normalizedMetadata.color.IsValid()) { + normalizedMetadata.color.dxgiColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + normalizedMetadata.color.primaries = HdrColorPrimaries::Rec709; + normalizedMetadata.color.transfer = HdrTransferFunction::SRGB; + normalizedMetadata.color.range = HdrColorRange::Full; + normalizedMetadata.color.referenceWhiteNits = 80.0f; + normalizedMetadata.color.sdrWhiteNits = 80.0f; + normalizedMetadata.color.displayPeakNits = 1000.0f; + normalizedMetadata.color.preExposure = 1.0f; + normalizedMetadata.color.isInferred = true; + normalizedMetadata.valid = true; + _lastAssumption = "Capture API did not provide a valid source color description; using default SDR."; + } else if (normalizedMetadata.color.isInferred) { + _lastAssumption = "Source color description inferred from the target monitor; capture API metadata was unavailable."; + } + if (!normalizedMetadata.IsValid()) { + Logger::Get().Error("HDR capture processor metadata is invalid after normalization"); + return false; + } + D3D11_TEXTURE2D_DESC sourceDesc{}; + source->GetDesc(&sourceDesc); + if (sourceDesc.Width != normalizedMetadata.width || sourceDesc.Height != normalizedMetadata.height) { + Logger::Get().Error(fmt::format( + "HDR capture processor size mismatch: texture={}x{} metadata={}x{}", + sourceDesc.Width, sourceDesc.Height, + normalizedMetadata.width, normalizedMetadata.height)); + return false; + } + if (!_EnsureResources(sourceDesc)) { + Logger::Get().Error(fmt::format( + "HDR capture processor canonical resource creation failed: format={} size={}x{}", + static_cast(sourceDesc.Format), sourceDesc.Width, sourceDesc.Height)); + return false; + } + _sourceSrv = _descriptorStore->GetShaderResourceView(source); + if (!_sourceSrv) { + Logger::Get().Error("HDR capture processor source SRV creation failed"); + return false; + } + const HdrTransformParameters parameters = HdrColorTransform::ForFrame(normalizedMetadata.color); + const HdrTransformConstants constants = HdrColorTransform::PrepareConstants( + parameters, normalizedMetadata.color.transfer, HdrTransferFunction::Linear); + struct CaptureConstants { + float exposure; + float inverseExposure; + float sdrWhiteScale; + float hdrPeakNits; + float shoulder; + float sdrWhiteNits; + uint32_t inputTransfer; + uint32_t outputTransfer; + uint32_t preserveAlpha; + uint32_t _padding[3]{}; + } captureConstants{ + constants.exposure, constants.inverseExposure, constants.sdrWhiteScale, + constants.hdrPeakNits, constants.shoulder, parameters.sdrWhiteNits, + constants.inputTransfer, constants.outputTransfer, + parameters.preserveAlpha ? 1u : 0u + }; + D3D11_MAPPED_SUBRESOURCE mapped{}; + auto* context = _deviceResources->GetD3DDC(); + if (FAILED(context->Map(_constants.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + Logger::Get().Error("HDR capture processor constant-buffer map failed"); + return false; + } + memcpy(mapped.pData, &captureConstants, sizeof(captureConstants)); + context->Unmap(_constants.get(), 0); + context->CSSetShader(_shader.get(), nullptr, 0); + ID3D11Buffer* constantsBuffer = _constants.get(); + context->CSSetConstantBuffers(0, 1, &constantsBuffer); + context->CSSetShaderResources(0, 1, &_sourceSrv); + context->CSSetUnorderedAccessViews(0, 1, &_canonicalUav, nullptr); + context->Dispatch((sourceDesc.Width + 7) / 8, (sourceDesc.Height + 7) / 8, 1); + ID3D11ShaderResourceView* nullSrv = nullptr; + ID3D11UnorderedAccessView* nullUav = nullptr; + ID3D11Buffer* nullBuffer = nullptr; + context->CSSetShaderResources(0, 1, &nullSrv); + context->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + context->CSSetConstantBuffers(0, 1, &nullBuffer); + context->CSSetShader(nullptr, nullptr, 0); + _metadata = normalizedMetadata; + _metadata.sourceFormat = sourceDesc.Format; + _metadata.stage = HdrFrameStage::CanonicalInput; + return true; +} + +bool HdrCaptureProcessor::Prepare( + ID3D11Texture2D* sourceTexture, + DXGI_FORMAT sourceFormat, + const ColorDescription& sourceColorDescription +) noexcept { + if (!_deviceResources || !sourceTexture) return false; + D3D11_TEXTURE2D_DESC sourceDesc{}; + sourceTexture->GetDesc(&sourceDesc); + if (!_EnsureResources(sourceDesc)) return false; + const float clearValue[4]{}; + _deviceResources->GetD3DDC()->ClearUnorderedAccessViewFloat(_canonicalUav, clearValue); + _metadata = {}; + _metadata.width = sourceDesc.Width; + _metadata.height = sourceDesc.Height; + _metadata.sourceFormat = sourceFormat != DXGI_FORMAT_UNKNOWN ? sourceFormat : sourceDesc.Format; + _metadata.color = sourceColorDescription; + if (!_metadata.color.IsValid()) { + _metadata.color.dxgiColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + _metadata.color.primaries = HdrColorPrimaries::Rec709; + _metadata.color.transfer = HdrTransferFunction::SRGB; + _metadata.color.range = HdrColorRange::Full; + _metadata.color.referenceWhiteNits = 80.0f; + _metadata.color.sdrWhiteNits = 80.0f; + _metadata.color.displayPeakNits = 1000.0f; + _metadata.color.preExposure = 1.0f; + _metadata.color.isInferred = true; + _lastAssumption = "Capture API did not provide a source color description; using default SDR."; + } else if (_metadata.color.isInferred) { + _lastAssumption = "Source color description inferred from the target monitor; capture API metadata was unavailable."; + } + _metadata.valid = true; + _metadata.stage = HdrFrameStage::CanonicalInput; + return true; +} + +bool HdrCaptureProcessor::Process( + ID3D11Texture2D* sourceTexture, + DXGI_FORMAT sourceFormat, + const ColorDescription& sourceColorDescription +) noexcept { + if (!sourceTexture) { + return false; + } + + D3D11_TEXTURE2D_DESC sourceDesc{}; + sourceTexture->GetDesc(&sourceDesc); + + HdrFrameMetadata metadata{}; + metadata.frameId = 0; + metadata.width = sourceDesc.Width; + metadata.height = sourceDesc.Height; + metadata.sourceFormat = sourceFormat != DXGI_FORMAT_UNKNOWN ? sourceFormat : sourceDesc.Format; + metadata.valid = true; + + _lastAssumption.clear(); + if (sourceColorDescription.IsValid()) { + metadata.color = sourceColorDescription; + } else { + metadata.color.dxgiColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + metadata.color.primaries = HdrColorPrimaries::Rec709; + metadata.color.transfer = HdrTransferFunction::SRGB; + metadata.color.range = HdrColorRange::Full; + metadata.color.referenceWhiteNits = 80.0f; + metadata.color.sdrWhiteNits = 80.0f; + metadata.color.displayPeakNits = 1000.0f; + metadata.color.isSceneReferred = false; + metadata.color.isPreExposed = false; + metadata.color.isInferred = true; + metadata.color.preExposure = 1.0f; + _lastAssumption = "Capture API did not provide a source color description; using default SDR " + "(Rec.709/sRGB, 80 nits reference white, 1000 nits display peak)."; + } + + return Process(sourceTexture, metadata); +} + +void HdrCaptureProcessor::ResetForResize() noexcept { + if (_descriptorStore && _canonical) { + _descriptorStore->RemoveCache(_canonical.get()); + } + _canonical = nullptr; + _canonicalUav = nullptr; + _sourceSrv = nullptr; + _metadata = {}; + _lastAssumption.clear(); +} + +} diff --git a/src/Magpie.Core/HdrCaptureProcessor.h b/src/Magpie.Core/HdrCaptureProcessor.h new file mode 100644 index 000000000..59c578f9d --- /dev/null +++ b/src/Magpie.Core/HdrCaptureProcessor.h @@ -0,0 +1,52 @@ +#pragma once + +#include "HdrColorTransform.h" + +#include +#include + +namespace Magpie { + +class BackendDescriptorStore; +class DeviceResources; + +class HdrCaptureProcessor { +public: + ~HdrCaptureProcessor() noexcept; + + bool Initialize(DeviceResources& deviceResources, BackendDescriptorStore& descriptorStore) noexcept; + bool Process(ID3D11Texture2D* source, const HdrFrameMetadata& metadata) noexcept; + // Interface shape prepared for Renderer/FrameSourceBase integration. The + // source color description may be invalid; the processor then substitutes a + // neutral default and records the assumption in LastAssumption(). + bool Process( + ID3D11Texture2D* sourceTexture, + DXGI_FORMAT sourceFormat, + const ColorDescription& sourceColorDescription + ) noexcept; + bool Prepare( + ID3D11Texture2D* sourceTexture, + DXGI_FORMAT sourceFormat, + const ColorDescription& sourceColorDescription + ) noexcept; + void ResetForResize() noexcept; + + ID3D11Texture2D* GetCanonicalTexture() const noexcept { return _canonical.get(); } + const HdrFrameMetadata& GetFrameMetadata() const noexcept { return _metadata; } + std::string_view LastAssumption() const noexcept { return _lastAssumption; } + +private: + bool _EnsureResources(const D3D11_TEXTURE2D_DESC& sourceDesc) noexcept; + + DeviceResources* _deviceResources = nullptr; + BackendDescriptorStore* _descriptorStore = nullptr; + winrt::com_ptr _canonical; + winrt::com_ptr _shader; + winrt::com_ptr _constants; + ID3D11ShaderResourceView* _sourceSrv = nullptr; + ID3D11UnorderedAccessView* _canonicalUav = nullptr; + HdrFrameMetadata _metadata{}; + std::string _lastAssumption; +}; + +} diff --git a/src/Magpie.Core/HdrColorTransform.cpp b/src/Magpie.Core/HdrColorTransform.cpp new file mode 100644 index 000000000..30dde0a0f --- /dev/null +++ b/src/Magpie.Core/HdrColorTransform.cpp @@ -0,0 +1,244 @@ +#include "pch.h" +#include "HdrColorTransform.h" + +#include +#include + +namespace Magpie { + +namespace { + +// Anchored-shoulder curve family for bounded HDR routes (DLSSNR FP16): +// x <= 1 identity (SDR content passes through bit-exact) +// 1 < x <= peak anchored Reinhard shoulder f(x) = x / (1 + k(x - 1)) +// x > peak linear tail with slope f'(peak) +// The anchor fixes f(peak) = highlightTarget so k = (peak - T) / (T(peak - 1)). +// k in (0, 1) keeps f strictly increasing on the shoulder and the tail slope +// f'(peak) = (1 - k) T^2 / peak^2 stays positive, so highlights never invert +// and the paired inverse has bounded gain everywhere. +// SDR-compatible routes cannot use this family: identity below white forces +// f(1) = 1 and monotonicity then forbids T < 1, while UNORM8 storage has no +// room above 1. SDR routes use the replicate contract instead: identity and +// saturate (see MapHdrToSdr/MapSdrToHdr). +struct ShoulderCoefficients { + float peak = 1.0f; // normalized peak: hdrPeakNits / sdrWhiteNits + float target = 1.0f; // f(peak) by design, always > 1 + float k = 0.0f; // shoulder strength in (0, 1) + float tailSlope = 1.0f; // f'(peak) +}; + +constexpr float IdentityEpsilon = 1e-6f; + +// Design target f(peak) for the bounded FP16 route: keeps the DLSSNR model +// input in the experimentally validated band (scale 2 ~ baseline, 4.5 ~ +// blowout). Public constant mirrors HdrColorTransform's constexpr member. +constexpr float BoundedRouteHighlightTarget = HdrColorTransform::BoundedRouteHighlightTarget; + +ShoulderCoefficients BuildShoulder( + const HdrTransformParameters& parameters, + float highlightTarget +) noexcept { + ShoulderCoefficients result; + result.peak = std::max( + parameters.hdrPeakNits / std::max(parameters.sdrWhiteNits, 1e-4f), 1.0f); + // Degenerate monitors report MaxLuminance <= SDR white (e.g. 80 <= 360). + // There is no HDR headroom then; the curve must degrade to the plain + // identity instead of dividing by (peak - 1) == 0 and seeding NaN into + // every downstream conversion. + if (result.peak <= 1.0f + IdentityEpsilon) { + result.target = 1.0f; + result.k = 0.0f; + result.tailSlope = 1.0f; + return result; + } + result.target = std::clamp(highlightTarget, 1.0f + IdentityEpsilon, result.peak); + result.k = std::clamp( + (result.peak - result.target) / + (result.target * (result.peak - 1.0f)), + IdentityEpsilon, 1.0f - IdentityEpsilon); + result.tailSlope = (1.0f - result.k) * result.target * result.target / + (result.peak * result.peak); + return result; +} + +float ApplyShoulder(float x, const ShoulderCoefficients& c) noexcept { + const float value = std::max(x, 0.0f); + if (value <= 1.0f) return value; + if (value <= c.peak) { + return value / (1.0f + c.k * (value - 1.0f)); + } + return c.target + c.tailSlope * (value - c.peak); +} + +float InvertShoulder(float y, const ShoulderCoefficients& c) noexcept { + if (y <= 1.0f) return y; + if (y <= c.target) { + // Algebraic inverse of the anchored Reinhard shoulder. + return y * (1.0f - c.k) / std::max(1.0f - c.k * y, 1e-6f); + } + return c.peak + (y - c.target) / std::max(c.tailSlope, 1e-6f); +} + +// PQ/HLG transfer constants (BT.2100). +constexpr float PQM1 = 2610.0f / 16384.0f; +constexpr float PQM2 = 2523.0f / 32.0f; +constexpr float PQC1 = 3424.0f / 4096.0f; +constexpr float PQC2 = 2413.0f / 128.0f; +constexpr float PQC3 = 2392.0f / 128.0f; +constexpr float PQMaxNits = 10000.0f; + +float Clamp01(float value) noexcept { return std::clamp(value, 0.0f, 1.0f); } + +float DecodePq(float value) noexcept { + const float normalized = std::pow(Clamp01(value), 1.0f / PQM2); + const float numerator = std::max(normalized - PQC1, 0.0f); + const float denominator = PQC2 - PQC3 * normalized; + return denominator > 0.0f ? std::pow(numerator / denominator, 1.0f / PQM1) * PQMaxNits : 0.0f; +} + +float EncodePq(float value) noexcept { + const float normalized = std::pow(std::max(value, 0.0f) / PQMaxNits, PQM1); + return Clamp01(std::pow((PQC1 + PQC2 * normalized) / (1.0f + PQC3 * normalized), PQM2)); +} + +float DecodeHlg(float value) noexcept { + constexpr float a = 0.17883277f; + constexpr float b = 1.0f - 4.0f * a; + const float c = 0.5f - a * std::log(4.0f * a); + const float encoded = Clamp01(value); + return encoded <= 0.5f ? (encoded * encoded) / 3.0f : (std::exp((encoded - c) / a) + b) / 12.0f; +} + +float EncodeHlg(float value) noexcept { + constexpr float a = 0.17883277f; + constexpr float b = 1.0f - 4.0f * a; + const float c = 0.5f - a * std::log(4.0f * a); + const float linear = std::max(value, 0.0f); + return Clamp01(linear <= 1.0f / 12.0f + ? std::sqrt(3.0f * linear) : a * std::log(12.0f * linear - b) + c); +} +} + +bool HdrTransformParameters::IsValid() const noexcept { + return std::isfinite(exposure) && exposure > 0.0f && + std::isfinite(referenceWhiteNits) && referenceWhiteNits == 80.0f && + std::isfinite(sdrWhiteNits) && sdrWhiteNits > 0.0f && + std::isfinite(hdrPeakNits) && hdrPeakNits >= referenceWhiteNits && + std::isfinite(shoulder) && shoulder > 0.0f; +} + +HdrTransformParameters HdrColorTransform::ForFrame(const ColorDescription& color) noexcept { + HdrTransformParameters parameters; + if (!color.IsValid()) return parameters; + parameters.referenceWhiteNits = 80.0f; + parameters.sdrWhiteNits = color.sdrWhiteNits; + parameters.hdrPeakNits = std::max({ + color.displayPeakNits, 80.0f, color.sdrWhiteNits }); + parameters.exposure = color.isPreExposed ? color.preExposure : 1.0f; + return parameters; +} + +HdrTransformConstants HdrColorTransform::PrepareConstants( + const HdrTransformParameters& parameters, + HdrTransferFunction inputTransfer, + HdrTransferFunction outputTransfer +) noexcept { + const HdrTransformParameters valid = parameters.IsValid() ? parameters : HdrTransformParameters{}; + return { valid.exposure, 1.0f / valid.exposure, valid.sdrWhiteNits / valid.hdrPeakNits, + valid.hdrPeakNits, valid.shoulder, static_cast(inputTransfer), + static_cast(outputTransfer) }; +} + +float HdrColorTransform::DecodeTransfer(float value, HdrTransferFunction transfer) noexcept { + switch (transfer) { + case HdrTransferFunction::SRGB: return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f); + case HdrTransferFunction::PQ: return DecodePq(value); + case HdrTransferFunction::HLG: return DecodeHlg(value); + case HdrTransferFunction::Linear: + case HdrTransferFunction::Unknown: + default: return value; + } +} + +float HdrColorTransform::EncodeTransfer(float value, HdrTransferFunction transfer) noexcept { + switch (transfer) { + case HdrTransferFunction::SRGB: return value <= 0.0031308f ? value * 12.92f : 1.055f * std::pow(std::max(value, 0.0f), 1.0f / 2.4f) - 0.055f; + case HdrTransferFunction::PQ: return EncodePq(value); + case HdrTransferFunction::HLG: return EncodeHlg(value); + case HdrTransferFunction::Linear: + case HdrTransferFunction::Unknown: + default: return value; + } +} + +float HdrColorTransform::MapHdrToSdr(float value, const HdrTransformParameters& parameters) noexcept { + // SDR-compatible replicate contract: identity below the SDR white point + // (the captured SDR game frame reaches the backend bit-exact) and plain + // saturation above it. There is no shoulder: identity forces f(1) = 1 and + // UNORM8 storage leaves no code space for a highlight target above 1. + const HdrTransformParameters valid = parameters.IsValid() ? parameters : HdrTransformParameters{}; + const float referenceWhiteScale = valid.sdrWhiteNits / 80.0f; + const float normalized = std::max(value, 0.0f) * valid.exposure / referenceWhiteScale; + return std::min(normalized, 1.0f); +} + +float HdrColorTransform::MapSdrToHdr(float value, const HdrTransformParameters& parameters) noexcept { + // Paired inverse of the replicate contract. Values saturated to 1.0 + // restore to the frame's HDR headroom level in the normalized domain + // (peak/sdrWhite), then scale back by the white point. The previous + // formula multiplied by the white point twice for saturated pixels + // (peak/80 * sdrWhite/80 = 20.25 for a 360-nit display), pushing whites + // to 1620 nit — the source of the yellow-tinted highlight leak. + const HdrTransformParameters valid = parameters.IsValid() ? parameters : HdrTransformParameters{}; + const float referenceWhiteScale = valid.sdrWhiteNits / 80.0f; + const float mapped = std::clamp(value, 0.0f, 1.0f); + const float normalized = mapped >= 1.0f + ? std::max(valid.hdrPeakNits, valid.sdrWhiteNits) / valid.sdrWhiteNits + : mapped; + return normalized * referenceWhiteScale / valid.exposure; +} + +float HdrColorTransform::EncodeBoundedHdr(float value, const HdrTransformParameters& parameters) noexcept { + const HdrTransformParameters valid = parameters.IsValid() ? parameters : HdrTransformParameters{}; + const ShoulderCoefficients curve = BuildShoulder(valid, BoundedRouteHighlightTarget); + return ApplyShoulder(std::max(value, 0.0f), curve); +} + +float HdrColorTransform::DecodeBoundedHdr(float value, const HdrTransformParameters& parameters) noexcept { + const HdrTransformParameters valid = parameters.IsValid() ? parameters : HdrTransformParameters{}; + const ShoulderCoefficients curve = BuildShoulder(valid, BoundedRouteHighlightTarget); + return InvertShoulder(std::max(value, 0.0f), curve); +} + +HdrColorTransform::ShoulderCurve HdrColorTransform::BuildShoulderCurve( + const HdrTransformParameters& parameters, + float highlightTarget +) noexcept { + const ShoulderCoefficients coefficients = BuildShoulder(parameters, highlightTarget); + return ShoulderCurve{ + coefficients.peak, coefficients.target, coefficients.k, coefficients.tailSlope }; +} + +float HdrColorTransform::ApplyShoulderCurve(float value, const ShoulderCurve& curve) noexcept { + return ApplyShoulder(value, { curve.peak, curve.target, curve.k, curve.tailSlope }); +} + +float HdrColorTransform::InvertShoulderCurve(float value, const ShoulderCurve& curve) noexcept { + return InvertShoulder(value, { curve.peak, curve.target, curve.k, curve.tailSlope }); +} + +HdrColor HdrColorTransform::Transform( + const HdrColor& color, + HdrTransferFunction inputTransfer, + HdrTransferFunction outputTransfer, + const HdrTransformParameters& parameters +) noexcept { + HdrColor result = color; + for (size_t channel = 0; channel < 3; ++channel) { + result[channel] = EncodeTransfer(DecodeTransfer(color[channel], inputTransfer), outputTransfer); + } + result[3] = parameters.preserveAlpha ? color[3] : 1.0f; + return result; +} + +} diff --git a/src/Magpie.Core/HdrColorTransform.h b/src/Magpie.Core/HdrColorTransform.h new file mode 100644 index 000000000..1681ef38d --- /dev/null +++ b/src/Magpie.Core/HdrColorTransform.h @@ -0,0 +1,71 @@ +#pragma once + +#include "HdrFrame.h" +#include + +namespace Magpie { + +using HdrColor = std::array; + +struct HdrTransformParameters { + float exposure = 1.0f; + // Canonical scRGB reference white is fixed by the global HDR protocol. + float referenceWhiteNits = 80.0f; + float sdrWhiteNits = 80.0f; + float hdrPeakNits = 1000.0f; + float shoulder = 1.0f; + bool preserveAlpha = true; + bool IsValid() const noexcept; +}; + +struct HdrTransformConstants { + float exposure = 1.0f; + float inverseExposure = 1.0f; + float sdrWhiteScale = 0.08f; + float hdrPeakNits = 1000.0f; + float shoulder = 1.0f; + uint32_t inputTransfer = static_cast(HdrTransferFunction::Linear); + uint32_t outputTransfer = static_cast(HdrTransferFunction::Linear); +}; + +class HdrColorTransform { +public: + static HdrTransformParameters ForFrame(const ColorDescription& color) noexcept; + static HdrTransformConstants PrepareConstants( + const HdrTransformParameters& parameters, + HdrTransferFunction inputTransfer, + HdrTransferFunction outputTransfer + ) noexcept; + static float DecodeTransfer(float value, HdrTransferFunction transfer) noexcept; + static float EncodeTransfer(float value, HdrTransferFunction transfer) noexcept; + static float MapHdrToSdr(float value, const HdrTransformParameters& parameters) noexcept; + static float MapSdrToHdr(float value, const HdrTransformParameters& parameters) noexcept; + // Bounded FP16 routes (DLSSNR experimental path) use the anchored + // shoulder family: identity below the SDR white, Reinhard shoulder up to + // the peak, linear tail beyond; strictly monotone and invertible. + static float EncodeBoundedHdr(float value, const HdrTransformParameters& parameters) noexcept; + static float DecodeBoundedHdr(float value, const HdrTransformParameters& parameters) noexcept; + // Shoulder coefficients derived from the parameters and the route target: + // f(x<=1)=x, anchored Reinhard up to peak, linear tail beyond. + struct ShoulderCurve { + float peak = 1.0f; // hdrPeakNits / sdrWhiteNits (>= 1) + float target = 1.0f; // f(peak) by design, always > 1 + float k = 0.0f; // shoulder strength in (0, 1) + float tailSlope = 1.0f; // f'(peak) + }; + static ShoulderCurve BuildShoulderCurve( + const HdrTransformParameters& parameters, + float highlightTarget + ) noexcept; + static float ApplyShoulderCurve(float value, const ShoulderCurve& curve) noexcept; + static float InvertShoulderCurve(float value, const ShoulderCurve& curve) noexcept; + static constexpr float BoundedRouteHighlightTarget = 2.5f; + static HdrColor Transform( + const HdrColor& color, + HdrTransferFunction inputTransfer, + HdrTransferFunction outputTransfer, + const HdrTransformParameters& parameters + ) noexcept; +}; + +} diff --git a/src/Magpie.Core/HdrDiagnostics.cpp b/src/Magpie.Core/HdrDiagnostics.cpp new file mode 100644 index 000000000..7fa117d97 --- /dev/null +++ b/src/Magpie.Core/HdrDiagnostics.cpp @@ -0,0 +1,78 @@ +#include "pch.h" +#include "HdrDiagnostics.h" +#include "HdrProtocol.h" +#include "Logger.h" + +namespace Magpie { + +void LogHdrDiagnostics(const HdrDiagnostics& diagnostics, bool debugOnly) noexcept { + const bool hasIssue = !diagnostics.fallbackReason.empty() || !diagnostics.assumptions.empty(); + (void)debugOnly; + + const std::string message = fmt::format( + "HDR diagnostics\n" + "\thdrOptionEnabled: {}\n" + "\tcaptureMethod: {}\n" + "\tsourceFormat: {}\n" + "\tsourceColorDescriptionValid: {}\n" + "\tsourceDxgiColorSpace: {}\n" + "\tsourcePrimaries: {}\n" + "\tsourceTransfer: {}\n" + "\tsourceRange: {}\n" + "\tsourceReferenceWhiteNits: {:.3f}\n" + "\tsourceSdrWhiteNits: {:.3f}\n" + "\tsourceDisplayPeakNits: {:.3f}\n" + "\tdisplayHdrEnabled: {}\n" + "\tsourceSceneReferred: {}\n" + "\tsourceColorInferred: {}\n" + "\tsourcePreExposure: {:.6f}\n" + "\tcanonicalFormat: {}\n" + "\tselectedAdapterProfile: {}\n" + "\tselectedRouteId: {}\n" + "\tconversionPath: {}\n" + "\tfallbackReason: {}\n" + "\tassumptions: {}", + diagnostics.hdrOptionEnabled ? "true" : "false", + diagnostics.captureMethod.empty() ? "(not set)" : diagnostics.captureMethod, + static_cast(diagnostics.sourceFormat), + diagnostics.sourceColorDescription.IsValid() ? "true" : "false", + static_cast(diagnostics.sourceColorDescription.dxgiColorSpace), + static_cast(diagnostics.sourceColorDescription.primaries), + ToString(diagnostics.sourceColorDescription.transfer), + ToString(diagnostics.sourceColorDescription.range), + diagnostics.sourceColorDescription.referenceWhiteNits, + diagnostics.sourceColorDescription.sdrWhiteNits, + diagnostics.sourceColorDescription.displayPeakNits, + diagnostics.sourceColorDescription.displayHdrEnabled ? "true" : "false", + diagnostics.sourceColorDescription.isSceneReferred ? "true" : "false", + diagnostics.sourceColorDescription.isInferred ? "true" : "false", + diagnostics.sourceColorDescription.preExposure, + static_cast(diagnostics.canonicalFormat), + ToString(diagnostics.selectedAdapterProfile), + diagnostics.selectedRouteId.empty() ? "(not set)" : diagnostics.selectedRouteId, + diagnostics.conversionPath.empty() ? "(not set)" : diagnostics.conversionPath, + diagnostics.fallbackReason.empty() ? "(none)" : diagnostics.fallbackReason, + diagnostics.assumptions.empty() ? "(none)" : diagnostics.assumptions + ); + + if (hasIssue) { + Logger::Get().Warn(message); + } else { + Logger::Get().Info(message); + } +} + +void AppendHdrAssumption(HdrDiagnostics& diagnostics, std::string_view assumption) noexcept { + if (assumption.empty()) { + return; + } + + if (diagnostics.assumptions.empty()) { + diagnostics.assumptions = std::string(assumption); + } else { + diagnostics.assumptions.append("; "); + diagnostics.assumptions.append(assumption); + } +} + +} diff --git a/src/Magpie.Core/HdrDiagnostics.h b/src/Magpie.Core/HdrDiagnostics.h new file mode 100644 index 000000000..9997ad186 --- /dev/null +++ b/src/Magpie.Core/HdrDiagnostics.h @@ -0,0 +1,34 @@ +#pragma once + +#include "HdrFrame.h" + +#include +#include + +namespace Magpie { + +// Compact, CPU-side diagnostic state. It is intended for DEBUG or existing +// logger output at capture/effect boundaries. It never triggers a CPU staging +// readback and does not alter rendering synchronization. +struct HdrDiagnostics { + bool hdrOptionEnabled = false; + std::string captureMethod; + DXGI_FORMAT sourceFormat = DXGI_FORMAT_UNKNOWN; + ColorDescription sourceColorDescription{}; + DXGI_FORMAT canonicalFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + HdrAdapterProfile selectedAdapterProfile = HdrAdapterProfile::Unknown; + std::string selectedRouteId; + std::string conversionPath; + std::string fallbackReason; + std::string assumptions; +}; + +// Emits the structured diagnostic through the existing logger. debugOnly is a +// policy hint for callers; implementations may still log warnings for explicit +// fallback/assumption reasons. +void LogHdrDiagnostics(const HdrDiagnostics& diagnostics, bool debugOnly = false) noexcept; + +// Convenience for populating "missing source color metadata" assumptions. +void AppendHdrAssumption(HdrDiagnostics& diagnostics, std::string_view assumption) noexcept; + +} diff --git a/src/Magpie.Core/HdrEffectBoundary.cpp b/src/Magpie.Core/HdrEffectBoundary.cpp new file mode 100644 index 000000000..ce1c5d994 --- /dev/null +++ b/src/Magpie.Core/HdrEffectBoundary.cpp @@ -0,0 +1,74 @@ +#include "pch.h" +#include "HdrEffectBoundary.h" + +namespace Magpie { + +namespace { + +HdrFormatRoute MakeGenericSdrFallbackRoute() { + return HdrFormatRoute{ + .effectId = "__generic__", + .optionId = "sdr-compatible-fallback", + .inputFormat = DXGI_FORMAT_R8G8B8A8_UNORM, + .outputFormat = DXGI_FORMAT_R8G8B8A8_UNORM, + .inputTransfer = HdrTransferFunction::SRGB, + .outputTransfer = HdrTransferFunction::SRGB, + .inputRange = HdrColorRange::Full, + .outputRange = HdrColorRange::Full, + .alphaMode = HdrAlphaMode::Preserve, + .evidenceLevel = HdrEvidenceLevel::None, + .hdrNative = false, + .adapterProfile = HdrAdapterProfile::SDRCompatible, + }; +} + +} + +const HdrFormatRoute* HdrEffectBoundary::SelectRoute(bool hdrEnabled, const HdrFormatRoutes& routes) noexcept { + return hdrEnabled ? SelectDefaultHdrRoute(routes) : SelectDefaultSdrRoute(routes); +} + +HdrEffectBoundaryContext HdrEffectBoundary::Prepare(bool hdrEnabled, const HdrFrame& inputFrame, const HdrFormatRoutes& routes, const ColorDescription& sourceColor) noexcept { + HdrEffectBoundaryContext context; + context.hdrEnabled = hdrEnabled; + context.inputFrame = inputFrame; + context.routes = routes; + const HdrFormatRoute* selectedRoute = SelectRoute(hdrEnabled, context.routes); + if (hdrEnabled && !selectedRoute) { + context.routes.push_back(MakeGenericSdrFallbackRoute()); + selectedRoute = &context.routes.back(); + } + if (selectedRoute) { + context.selectedRouteIndex = static_cast(selectedRoute - context.routes.data()); + } + + if (!hdrEnabled) { + context.prepared = true; + return context; + } + + if (!context.inputFrame.IsCanonical() || !context.SelectedRoute()) { + return context; + } + + context.plan = HdrAdapterDispatcher{}.BuildPlan(*context.SelectedRoute(), sourceColor); + context.prepared = context.plan.IsNonTerminalCanonical() || context.plan.isPresentationTerminal; + return context; +} + +bool HdrEffectBoundary::Complete(HdrEffectBoundaryContext& context, const HdrFrame& backendOutput) noexcept { + if (!context.prepared) { + return false; + } + if (!context.hdrEnabled || context.plan.isPresentationTerminal) { + context.completed = true; + return true; + } + if (!backendOutput.IsCanonical()) { + return false; + } + context.completed = true; + return true; +} + +} diff --git a/src/Magpie.Core/HdrEffectBoundary.h b/src/Magpie.Core/HdrEffectBoundary.h new file mode 100644 index 000000000..c7ca59b15 --- /dev/null +++ b/src/Magpie.Core/HdrEffectBoundary.h @@ -0,0 +1,34 @@ +#pragma once + +#include "HdrAdapterDispatcher.h" + +#include + +namespace Magpie { + +struct HdrEffectBoundaryContext { + bool hdrEnabled = false; + HdrFrame inputFrame{}; + HdrFormatRoutes routes{}; + size_t selectedRouteIndex = static_cast(-1); + HdrAdapterPlan plan{}; + bool prepared = false; + bool completed = false; + + bool IsCanonicalInput() const noexcept { + return !hdrEnabled || inputFrame.IsCanonical(); + } + + const HdrFormatRoute* SelectedRoute() const noexcept { + return selectedRouteIndex < routes.size() ? &routes[selectedRouteIndex] : nullptr; + } +}; + +class HdrEffectBoundary { +public: + static HdrEffectBoundaryContext Prepare(bool hdrEnabled, const HdrFrame& inputFrame, const HdrFormatRoutes& routes, const ColorDescription& sourceColor) noexcept; + static bool Complete(HdrEffectBoundaryContext& context, const HdrFrame& backendOutput) noexcept; + static const HdrFormatRoute* SelectRoute(bool hdrEnabled, const HdrFormatRoutes& routes) noexcept; +}; + +} diff --git a/src/Magpie.Core/HdrFrame.cpp b/src/Magpie.Core/HdrFrame.cpp new file mode 100644 index 000000000..41da3cbbe --- /dev/null +++ b/src/Magpie.Core/HdrFrame.cpp @@ -0,0 +1,62 @@ +#include "pch.h" +#include "HdrFrame.h" + +#include + +namespace Magpie { + +bool HdrMetadata::HasLuminanceRange() const noexcept { + return maxMasteringLuminanceNits > 0.0f && minMasteringLuminanceNits >= 0.0f && + maxMasteringLuminanceNits >= minMasteringLuminanceNits; +} + +bool ColorDescription::IsValid() const noexcept { + return primaries != HdrColorPrimaries::Unknown && + transfer != HdrTransferFunction::Unknown && + std::isfinite(referenceWhiteNits) && referenceWhiteNits > 0.0f && + std::isfinite(sdrWhiteNits) && sdrWhiteNits > 0.0f && + std::isfinite(displayPeakNits) && displayPeakNits >= referenceWhiteNits && + std::isfinite(preExposure) && preExposure > 0.0f; +} + +bool HdrFrameMetadata::IsValid() const noexcept { + return valid && width != 0 && height != 0 && color.IsValid() && + stage != HdrFrameStage::Unknown; +} + +bool HdrFrame::IsCanonical() const noexcept { + if (!texture || !metadata.IsValid() || + workingFormat != DXGI_FORMAT_R16G16B16A16_FLOAT) { + return false; + } + + D3D11_TEXTURE2D_DESC textureDesc{}; + texture->GetDesc(&textureDesc); + return textureDesc.Format == workingFormat && + textureDesc.Width == metadata.width && textureDesc.Height == metadata.height; +} + +std::string HdrFormatRoute::Id() const { + if (effectId.empty()) { + return optionId; + } + if (optionId.empty()) { + return effectId; + } + return effectId + "/" + optionId; +} + +bool HdrFormatRoute::IsValid() const noexcept { + if (effectId.empty() || optionId.empty()) { + return false; + } + if (inputFormat == DXGI_FORMAT_UNKNOWN || outputFormat == DXGI_FORMAT_UNKNOWN) { + return false; + } + if (adapterProfile == HdrAdapterProfile::Unknown) { + return false; + } + return alphaMode != HdrAlphaMode::Unknown; +} + +} diff --git a/src/Magpie.Core/HdrFrame.h b/src/Magpie.Core/HdrFrame.h new file mode 100644 index 000000000..a61794d22 --- /dev/null +++ b/src/Magpie.Core/HdrFrame.h @@ -0,0 +1,177 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Magpie { + +enum class HdrColorPrimaries : uint8_t { Unknown, Rec709, Rec2020, P3D65 }; +enum class HdrTransferFunction : uint8_t { Unknown, Linear, SRGB, PQ, HLG }; +enum class HdrColorRange : uint8_t { + Unknown, + Full, + Limited, + SceneLinear, + DisplayReferred +}; + +enum class HdrAlphaMode : uint8_t { + Unknown, + Preserve, + ForceOpaque, + Premultiplied +}; + +enum class HdrEvidenceLevel : uint8_t { + None, + PublicApiContract, + ReferenceImplementation, + CommunityExperiment, + LocalValidation +}; + +enum class HdrAdapterProfile : uint8_t { + DirectFP16, BoundedHDR, SDRCompatible, ConditionalFP16, Unknown, PresentationTerminal +}; + +// Every HDR texture crossing a Magpie module boundary carries one of these +// roles. RawCapture is confined to the capture front end and is never a +// renderer/effect/presentation input. +enum class HdrFrameStage : uint8_t { + Unknown, + RawCapture, + CanonicalInput, + EffectLocalInput, + EffectLocalOutput, + CanonicalOutput, + PublishedOutput, + PresentedOutput, + GeneratedOutput, +}; + +struct HdrMetadata { + float maxMasteringLuminanceNits = 0.0f; + float minMasteringLuminanceNits = 0.0f; + float maxContentLightLevelNits = 0.0f; + float maxFrameAverageLightLevelNits = 0.0f; + std::array displayPrimaries{}; + std::array whitePoint{}; + bool HasLuminanceRange() const noexcept; +}; + +struct ColorDescription { + DXGI_COLOR_SPACE_TYPE dxgiColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + HdrColorPrimaries primaries = HdrColorPrimaries::Unknown; + HdrTransferFunction transfer = HdrTransferFunction::Unknown; + HdrColorRange range = HdrColorRange::Unknown; + // Canonical scRGB reference white. Windows scRGB defines 1.0 as 80 nit. + float referenceWhiteNits = 80.0f; + // Display SDR white level used when SDR content is embedded in HDR. + float sdrWhiteNits = 80.0f; + float displayPeakNits = 1000.0f; + bool isSceneReferred = false; + bool isPreExposed = false; + bool isInferred = false; + bool displayHdrEnabled = false; + float preExposure = 1.0f; + HdrMetadata metadata{}; + bool IsValid() const noexcept; +}; + +struct HdrFrameMetadata { + uint64_t frameId = 0; + uint64_t captureSequence = 0; + uint64_t resourceGeneration = 0; + int64_t timestamp100ns = 0; + uint32_t width = 0; + uint32_t height = 0; + DXGI_FORMAT sourceFormat = DXGI_FORMAT_UNKNOWN; + ColorDescription color{}; + HdrFrameStage stage = HdrFrameStage::Unknown; + std::string routeId; + bool generated = false; + bool valid = false; + bool IsValid() const noexcept; +}; + +struct HdrFrame { + ID3D11Texture2D* texture = nullptr; + HdrFrameMetadata metadata{}; + DXGI_FORMAT workingFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + bool IsCanonical() const noexcept; + + HdrFrame WithStage(HdrFrameStage newStage, std::string route = {}) const { + HdrFrame result = *this; + result.metadata.stage = newStage; + result.metadata.routeId = std::move(route); + return result; + } +}; + +struct HdrAdapterProtocol { + HdrAdapterProfile profile = HdrAdapterProfile::Unknown; + DXGI_FORMAT inputFormat = DXGI_FORMAT_UNKNOWN; + DXGI_FORMAT outputFormat = DXGI_FORMAT_UNKNOWN; + HdrTransferFunction inputTransfer = HdrTransferFunction::Unknown; + HdrTransferFunction outputTransfer = HdrTransferFunction::Unknown; + bool preservesAlpha = true; + bool isPresentationTerminal = false; +}; + +// A protocol route describes one way to carry an effect across the canonical +// HDR boundary. Routes are stored as structured data and are never selected by +// effect-name string comparisons. The route intentionally does not encode a +// color-space guess from its texture format; callers must set hdrNative and the +// transfer/range fields from verified evidence. +struct HdrFormatRoute { + std::string effectId; + std::string optionId; + DXGI_FORMAT inputFormat = DXGI_FORMAT_UNKNOWN; + DXGI_FORMAT outputFormat = DXGI_FORMAT_UNKNOWN; + HdrTransferFunction inputTransfer = HdrTransferFunction::Unknown; + HdrTransferFunction outputTransfer = HdrTransferFunction::Unknown; + HdrColorRange inputRange = HdrColorRange::Unknown; + HdrColorRange outputRange = HdrColorRange::Unknown; + HdrAlphaMode alphaMode = HdrAlphaMode::Unknown; + HdrEvidenceLevel evidenceLevel = HdrEvidenceLevel::None; + bool hdrNative = false; + HdrAdapterProfile adapterProfile = HdrAdapterProfile::Unknown; + bool defaultForHdr = false; + bool defaultForSdr = false; + float normalizationScale = 1.0f; + + std::string Id() const; + + // Structural validity: a route must be identifiable and must name real + // input/output formats plus an explicit adapter profile and alpha rule. + // Unknown evidence routes may still be represented for diagnostics but are + // not considered accepted HDR routes. + bool IsValid() const noexcept; + + bool IsAccepted() const noexcept { + return IsValid(); + } + + // True only when the route explicitly declares HDR semantics. FP16 alone + // and R8/R10 format acceptance alone never make this flag true. + bool IsHdrNative() const noexcept { + return IsValid() && hdrNative; + } + + // Routes that do not directly carry HDR semantics but are accepted through + // an explicit bounded/SDR/conditional adapter. + bool IsHdrAdapter() const noexcept { + return IsValid() && !hdrNative && adapterProfile != HdrAdapterProfile::Unknown && + adapterProfile != HdrAdapterProfile::PresentationTerminal; + } + + bool IsPresentationTerminal() const noexcept { + return adapterProfile == HdrAdapterProfile::PresentationTerminal; + } +}; + +} diff --git a/src/Magpie.Core/HdrProtocol.cpp b/src/Magpie.Core/HdrProtocol.cpp new file mode 100644 index 000000000..44aa2384d --- /dev/null +++ b/src/Magpie.Core/HdrProtocol.cpp @@ -0,0 +1,110 @@ +#include "pch.h" +#include "HdrProtocol.h" + +namespace Magpie { + +const HdrFormatRoute* SelectDefaultHdrRoute(const HdrFormatRoutes& routes) noexcept { + const HdrFormatRoute* firstHdrNative = nullptr; + const HdrFormatRoute* firstHdrAdapter = nullptr; + + for (const HdrFormatRoute& route : routes) { + if (!route.IsValid()) { + continue; + } + + if (route.defaultForHdr) { + return &route; + } + + if (route.IsPresentationTerminal()) { + continue; + } + + if (route.IsHdrNative() && !firstHdrNative) { + firstHdrNative = &route; + } else if (route.IsHdrAdapter() && !firstHdrAdapter) { + firstHdrAdapter = &route; + } + } + + return firstHdrNative ? firstHdrNative : firstHdrAdapter; +} + +const HdrFormatRoute* SelectDefaultSdrRoute(const HdrFormatRoutes& routes) noexcept { + const HdrFormatRoute* firstAccepted = nullptr; + + for (const HdrFormatRoute& route : routes) { + if (!route.IsValid() || route.IsPresentationTerminal()) { + continue; + } + + if (!firstAccepted) { + firstAccepted = &route; + } + + if (route.defaultForSdr) { + return &route; + } + } + + return firstAccepted; +} + +std::vector GetAcceptedFormatRoutes(const HdrFormatRoutes& routes) { + std::vector result; + result.reserve(routes.size()); + for (const HdrFormatRoute& route : routes) { + if (route.IsAccepted()) { + result.push_back(&route); + } + } + return result; +} + +std::vector GetHdrNativeFormatRoutes(const HdrFormatRoutes& routes) { + std::vector result; + result.reserve(routes.size()); + for (const HdrFormatRoute& route : routes) { + if (route.IsHdrNative()) { + result.push_back(&route); + } + } + return result; +} + +std::vector GetHdrAdapterFormatRoutes(const HdrFormatRoutes& routes) { + std::vector result; + result.reserve(routes.size()); + for (const HdrFormatRoute& route : routes) { + if (route.IsHdrAdapter()) { + result.push_back(&route); + } + } + return result; +} + +std::string SerializeHdrFormatRoute(const HdrFormatRoute& route) { + return fmt::format( + "effectId={};optionId={};inputFormat={};outputFormat={};" + "inputTransfer={};outputTransfer={};inputRange={};outputRange={};" + "alphaMode={};evidenceLevel={};hdrNative={};adapterProfile={};" + "defaultForHdr={};defaultForSdr={};normalizationScale={}", + route.effectId, + route.optionId, + static_cast(route.inputFormat), + static_cast(route.outputFormat), + ToString(route.inputTransfer), + ToString(route.outputTransfer), + ToString(route.inputRange), + ToString(route.outputRange), + ToString(route.alphaMode), + ToString(route.evidenceLevel), + route.hdrNative ? "true" : "false", + ToString(route.adapterProfile), + route.defaultForHdr ? "true" : "false", + route.defaultForSdr ? "true" : "false", + route.normalizationScale + ); +} + +} diff --git a/src/Magpie.Core/HdrProtocol.h b/src/Magpie.Core/HdrProtocol.h new file mode 100644 index 000000000..b4d407420 --- /dev/null +++ b/src/Magpie.Core/HdrProtocol.h @@ -0,0 +1,112 @@ +#pragma once + +#include "HdrFrame.h" + +#include +#include +#include + +namespace Magpie { + +using HdrFormatRoutes = std::vector; + +// Textual names are for diagnostics and logging only. Protocol dispatch must +// use the structured route fields, never effect-name string comparisons. +constexpr std::string_view ToString(HdrAdapterProfile profile) noexcept { + switch (profile) { + case HdrAdapterProfile::DirectFP16: + return "DirectFP16"; + case HdrAdapterProfile::BoundedHDR: + return "BoundedHDR"; + case HdrAdapterProfile::SDRCompatible: + return "SDRCompatible"; + case HdrAdapterProfile::ConditionalFP16: + return "ConditionalFP16"; + case HdrAdapterProfile::Unknown: + return "Unknown"; + case HdrAdapterProfile::PresentationTerminal: + return "PresentationTerminal"; + default: + return "Unknown"; + } +} + +constexpr std::string_view ToString(HdrColorRange range) noexcept { + switch (range) { + case HdrColorRange::Full: + return "Full"; + case HdrColorRange::Limited: + return "Limited"; + case HdrColorRange::SceneLinear: + return "SceneLinear"; + case HdrColorRange::DisplayReferred: + return "DisplayReferred"; + case HdrColorRange::Unknown: + default: + return "Unknown"; + } +} + +constexpr std::string_view ToString(HdrAlphaMode alphaMode) noexcept { + switch (alphaMode) { + case HdrAlphaMode::Preserve: + return "Preserve"; + case HdrAlphaMode::ForceOpaque: + return "ForceOpaque"; + case HdrAlphaMode::Premultiplied: + return "Premultiplied"; + case HdrAlphaMode::Unknown: + default: + return "Unknown"; + } +} + +constexpr std::string_view ToString(HdrEvidenceLevel evidenceLevel) noexcept { + switch (evidenceLevel) { + case HdrEvidenceLevel::PublicApiContract: + return "PublicApiContract"; + case HdrEvidenceLevel::ReferenceImplementation: + return "ReferenceImplementation"; + case HdrEvidenceLevel::CommunityExperiment: + return "CommunityExperiment"; + case HdrEvidenceLevel::LocalValidation: + return "LocalValidation"; + case HdrEvidenceLevel::None: + default: + return "None"; + } +} + +constexpr std::string_view ToString(HdrTransferFunction transfer) noexcept { + switch (transfer) { + case HdrTransferFunction::Linear: + return "Linear"; + case HdrTransferFunction::SRGB: + return "SRGB"; + case HdrTransferFunction::PQ: + return "PQ"; + case HdrTransferFunction::HLG: + return "HLG"; + case HdrTransferFunction::Unknown: + default: + return "Unknown"; + } +} + +// SDR mode returns the route marked defaultForSdr. HDR mode returns a route +// marked defaultForHdr, then falls back to an explicit HDR-native or adapter +// route. Unknown-profile routes are not selected because they cannot be +// considered accepted HDR routes. +const HdrFormatRoute* SelectDefaultHdrRoute(const HdrFormatRoutes& routes) noexcept; +const HdrFormatRoute* SelectDefaultSdrRoute(const HdrFormatRoutes& routes) noexcept; + +std::vector GetAcceptedFormatRoutes(const HdrFormatRoutes& routes); +std::vector GetHdrNativeFormatRoutes(const HdrFormatRoutes& routes); +std::vector GetHdrAdapterFormatRoutes(const HdrFormatRoutes& routes); + +// Lightweight stable textual representation for diagnostics and future +// persistence tooling. It intentionally serializes every route field so the +// structured shape remains inspectable without effect-name branches. +std::string SerializeHdrFormatRoute(const HdrFormatRoute& route); + +} diff --git a/src/Magpie.Core/HdrSourceTestBridge.cpp b/src/Magpie.Core/HdrSourceTestBridge.cpp new file mode 100644 index 000000000..8d76770ad --- /dev/null +++ b/src/Magpie.Core/HdrSourceTestBridge.cpp @@ -0,0 +1,49 @@ +#include "pch.h" +#include "HdrSourceTestBridge.h" +#include "EffectCompiler.h" +#include "EffectProtocolCatalogC.h" +#include "GroupAHdrRoutes.h" +#include "GroupBHdrRoutes.h" +#include "HdrAdapterDispatcher.h" + +namespace Magpie { + +namespace { +HdrFormatRoutes RoutesFor(std::string_view name) noexcept { + const size_t separator = name.find('\\'); + const std::string_view group = separator == std::string::npos ? name : name.substr(0, separator); + if (group == "CAS") return GetGroupAHdrRoutes(group, 0); + if (group == "DLSSNR") return GetGroupBHdrRoutes(group, false); + if (group == "DLSS" || group == "FSR" || group == "FSR2" || group == "FSR3" || group == "FSR4" || group == "NIS") return GetGroupBHdrRoutes(group); + return EffectProtocolC::GetGroupCHdrRoutes(group); +} +} + +std::vector HdrSourceTestBridge::CompileAndDescribeAll( + const std::vector& effectNames, bool noFP16) noexcept { + std::vector results; + results.reserve(effectNames.size()); + for (const std::string& effectName : effectNames) { + EffectDesc desc{ .name = effectName }; + const uint32_t flags = noFP16 ? EffectCompilerFlags::NoFP16 : 0; + const bool compiled = EffectCompiler::Compile(desc, flags, nullptr) == 0; + const HdrFormatRoutes routes = RoutesFor(effectName); + const HdrFormatRoute* route = SelectDefaultHdrRoute(routes); + HdrSourceTestResult result; + result.effectId = effectName; + result.routeId = route ? route->Id() : ""; + result.routeValid = route && route->IsValid(); + result.passCount = compiled ? static_cast(desc.passes.size()) : 0; + result.textureCount = compiled ? static_cast(desc.textures.size()) : 0; + if (route) { + result.inputFormat = route->inputFormat; + result.outputFormat = route->outputFormat; + result.canonicalInput = route->inputFormat == DXGI_FORMAT_R16G16B16A16_FLOAT || route->adapterProfile == HdrAdapterProfile::SDRCompatible; + result.canonicalOutput = route->outputFormat == DXGI_FORMAT_R16G16B16A16_FLOAT || route->adapterProfile == HdrAdapterProfile::SDRCompatible; + } + results.push_back(std::move(result)); + } + return results; +} + +} diff --git a/src/Magpie.Core/HdrSourceTestBridge.h b/src/Magpie.Core/HdrSourceTestBridge.h new file mode 100644 index 000000000..5442683be --- /dev/null +++ b/src/Magpie.Core/HdrSourceTestBridge.h @@ -0,0 +1,32 @@ +#pragma once + +#include "EffectDesc.h" +#include "HdrFrame.h" +#include +#include + +namespace Magpie { + +struct HdrSourceTestResult { + std::string effectId; + std::string routeId; + DXGI_FORMAT inputFormat = DXGI_FORMAT_UNKNOWN; + DXGI_FORMAT outputFormat = DXGI_FORMAT_UNKNOWN; + uint32_t passCount = 0; + uint32_t textureCount = 0; + bool routeValid = false; + bool canonicalInput = false; + bool canonicalOutput = false; +}; + +// Source-level contract probe. It uses the production EffectCompiler and +// route providers; it does not emulate shader math or invoke a substitute SDK. +class HdrSourceTestBridge final { +public: + static std::vector CompileAndDescribeAll( + const std::vector& effectNames, + bool noFP16 = false + ) noexcept; +}; + +} diff --git a/src/Magpie.Core/HdrSurfaceAdapter.cpp b/src/Magpie.Core/HdrSurfaceAdapter.cpp new file mode 100644 index 000000000..ce8c29c5b --- /dev/null +++ b/src/Magpie.Core/HdrSurfaceAdapter.cpp @@ -0,0 +1,393 @@ +#include "pch.h" +#include "HdrSurfaceAdapter.h" + +#include "BackendDescriptorStore.h" +#include "DeviceResources.h" +#include "DirectXHelper.h" +#include "Logger.h" + +namespace Magpie { + +namespace { +constexpr char HLSL[] = R"( +cbuffer Transform : register(b0) { + float exposure; + float inverseExposure; + float sdrWhiteScale; + float hdrPeakNits; + float shoulder; + float referenceWhiteNits; + float sdrWhiteNits; + uint inputTransfer; + uint outputTransfer; + uint mode; + uint preserveAlpha; + float normalizationScale; + float peak; + float target; + float shoulderK; + float tailSlope; +}; +Texture2D sourceTexture : register(t0); +RWTexture2D outputTexture : register(u0); + +float DecodeSrgb(float value) { + return value <= 0.04045 ? value / 12.92 : pow(max((value + 0.055) / 1.055, 0.0), 2.4); +} +float EncodeSrgb(float value) { + value = saturate(value); + return value <= 0.0031308 ? value * 12.92 : 1.055 * pow(value, 1.0 / 2.4) - 0.055; +} +// Extended sRGB: the standard OETF on [0, 1] (matching what the U8 route +// feeds the model bit-for-bit) and the same power curve continued above 1 +// for HDR headroom. Monotone and exactly invertible on [0, inf). +float EncodeExtendedSrgb(float value) { + value = max(value, 0.0); + if (value <= 0.0031308) return value * 12.92; + return 1.055 * pow(value, 1.0 / 2.4) - 0.055; +} +float DecodeExtendedSrgb(float value) { + value = max(value, 0.0); + if (value <= 0.04045) return value / 12.92; + return pow((value + 0.055) / 1.055, 2.4); +} +float DecodeHlg(float value) { + const float a = 0.17883277; + const float b = 1.0 - 4.0 * a; + const float c = 0.5 - a * log(4.0 * a); + value = saturate(value); + return value <= 0.5 ? (value * value) / 3.0 : (exp((value - c) / a) + b) / 12.0; +} +float EncodeHlg(float value) { + const float a = 0.17883277; + const float b = 1.0 - 4.0 * a; + const float c = 0.5 - a * log(4.0 * a); + value = max(value, 0.0); + return saturate(value <= 1.0 / 12.0 ? sqrt(3.0 * value) : a * log(12.0 * value - b) + c); +} +float DecodePq(float value) { + const float m1 = 2610.0 / 16384.0; + const float m2 = 2523.0 / 32.0; + const float c1 = 3424.0 / 4096.0; + const float c2 = 2413.0 / 128.0; + const float c3 = 2392.0 / 128.0; + float p = pow(max(saturate(value), 0.0), 1.0 / m2); + return pow(max(p - c1, 0.0) / max(c2 - c3 * p, 1e-6), 1.0 / m1) * 10000.0; +} +float EncodePq(float value) { + const float m1 = 2610.0 / 16384.0; + const float m2 = 2523.0 / 32.0; + const float c1 = 3424.0 / 4096.0; + const float c2 = 2413.0 / 128.0; + const float c3 = 2392.0 / 128.0; + float p = pow(max(value, 0.0) / 10000.0, m1); + return saturate(pow((c1 + c2 * p) / (1.0 + c3 * p), m2)); +} +float3 Rec709ToRec2020(float3 value) { + return float3( + dot(value, float3(0.6274040, 0.3292820, 0.0433136)), + dot(value, float3(0.0690970, 0.9195400, 0.0113612)), + dot(value, float3(0.0163916, 0.0880132, 0.8955950))); +} +float3 Rec2020ToRec709(float3 value) { + return float3( + dot(value, float3(1.6604910, -0.5876411, -0.0728499)), + dot(value, float3(-0.1245505, 1.1328999, -0.0083494)), + dot(value, float3(-0.0181508, -0.1005789, 1.1187297))); +} +float3 MapRec2020ToPqGamut(float3 value) { + float minimum = min(value.r, min(value.g, value.b)); + if (minimum >= 0.0) return value; + float luminance = max(dot(value, float3(0.2627, 0.6780, 0.0593)), 0.0); + float chromaScale = luminance / max(luminance - minimum, 1e-6); + return max(luminance + (value - luminance) * saturate(chromaScale), 0.0); +} +// Anchored-shoulder curve, GPU twin of HdrColorTransform's CPU family. +// x <= 1 identity; 1 < x <= peak: x / (1 + k(x - 1)); linear tail beyond. +// The constant buffer carries peak/target/k/tailSlope so CPU and GPU stay +// bit-comparable; highlights never invert and inverse gain stays bounded. +float ApplyShoulder(float value, float peak, float target, float k, float tailSlope) { + value = max(value, 0.0); + if (value <= 1.0) return value; + if (value <= peak) return value / (1.0 + k * (value - 1.0)); + return target + tailSlope * (value - peak); +} +float InvertShoulder(float value, float peak, float target, float k, float tailSlope) { + value = max(value, 0.0); + if (value <= 1.0) return value; + if (value <= target) return value * (1.0 - k) / max(1.0 - k * value, 1e-6); + return peak + (value - target) / max(tailSlope, 1e-6); +} +float DecodeTransfer(float value, uint transfer) { + if (transfer == 2) return DecodeSrgb(value); + // PQ is absolute-display-referred. Canonical scRGB uses 80 nit as its + // fixed reference; the monitor SDR white level is a separate parameter. + if (transfer == 3) return DecodePq(value) / 80.0; + if (transfer == 4) return DecodeHlg(value); + return value; +} +float EncodeTransfer(float value, uint transfer) { + if (transfer == 2) return EncodeSrgb(value); + if (transfer == 3) return EncodePq(value * 80.0); + if (transfer == 4) return EncodeHlg(value); + return value; +} +float3 MapHdrToSdr(float3 value) { + float referenceWhiteScale = max(sdrWhiteNits / 80.0, 1e-4); + // Replicate contract for SDR-compatible backends: identity below the SDR + // white point (captured SDR frames reach the backend bit-exact), plain + // saturation above it. No shoulder: identity forces f(1)=1 and UNORM8 + // storage leaves no code space above 1. + return saturate(max(value, 0.0) * exposure / referenceWhiteScale); +} +float3 MapSdrToHdr(float3 value) { + float referenceWhiteScale = max(sdrWhiteNits / 80.0, 1e-4); + float3 mapped = saturate(value); + // Paired inverse of the replicate contract. Saturated whites restore to + // the HDR headroom in the normalized domain (peak/sdrWhite), then scale + // by the white point once. Multiplying by the white point twice pushed + // whites to (peak/80)*(sdrWhite/80) = 20.25 on a 360-nit display. + float peakHeadroom = max(hdrPeakNits, sdrWhiteNits) / max(sdrWhiteNits, 1e-4); + float3 restored = float3( + mapped.r >= 1.0 ? peakHeadroom : mapped.r, + mapped.g >= 1.0 ? peakHeadroom : mapped.g, + mapped.b >= 1.0 ? peakHeadroom : mapped.b); + return restored * referenceWhiteScale * inverseExposure; +} +[numthreads(8, 8, 1)] +void Main(uint3 id : SV_DispatchThreadID) { + uint width, height; + outputTexture.GetDimensions(width, height); + if (id.x >= width || id.y >= height) return; + float4 value = sourceTexture.Load(int3(id.xy, 0)); + float3 result; + if (mode == 0) { + float3 mapped = MapHdrToSdr(value.rgb); + result = float3( + EncodeTransfer(mapped.r, outputTransfer), + EncodeTransfer(mapped.g, outputTransfer), + EncodeTransfer(mapped.b, outputTransfer)); + } else if (mode == 1) { + float3 decoded = float3( + DecodeTransfer(value.r, inputTransfer), + DecodeTransfer(value.g, inputTransfer), + DecodeTransfer(value.b, inputTransfer)); + result = MapSdrToHdr(decoded); + } else if (mode == 2) { + // Canonical scRGB may use a display SDR-white scale (for example 4.5 + // for a 360-nit SDR white). Bounded backends operate in a normalized + // domain: normalize against the frame's SDR white point, then encode + // with the extended sRGB OETF. The model's training contract is sRGB- + // encoded U8 input; feeding it linear values leaves mid-tones squeezed + // into the bottom of its range (observed as a washed-out, hazy image). + // The extended curve matches the standard OETF on [0,1] exactly and + // continues the same power law for HDR headroom. + // The consumption domain is [0, inf); clamp out-of-gamut negatives + // before they reach the model (inverse mode 3 keeps full range). + float3 normalized = max(value.rgb, 0.0) * normalizationScale / + max(sdrWhiteNits / 80.0, 1e-4); + result = float3( + EncodeExtendedSrgb(normalized.r), + EncodeExtendedSrgb(normalized.g), + EncodeExtendedSrgb(normalized.b)); + } else if (mode == 3) { + // Paired inverse of mode 2; full range on purpose. + float3 normalized = float3( + DecodeExtendedSrgb(value.r), + DecodeExtendedSrgb(value.g), + DecodeExtendedSrgb(value.b)); + result = normalized * (sdrWhiteNits / 80.0) / max(normalizationScale, 1e-4); + } else if (mode == 5) { + // Canonical scRGB is linear with 1.0 == 80 nit. HDR10 also requires + // Rec.2020 primaries, so convert the canonical Rec.709 values first. + float3 rec2020 = MapRec2020ToPqGamut(Rec709ToRec2020(value.rgb)); + result = float3( + EncodePq(rec2020.r * 80.0), + EncodePq(rec2020.g * 80.0), + EncodePq(rec2020.b * 80.0)); + } else if (mode == 6) { + float3 rec2020 = float3( + DecodePq(value.r) / 80.0, + DecodePq(value.g) / 80.0, + DecodePq(value.b) / 80.0); + result = Rec2020ToRec709(rec2020); + } else { + result = value.rgb; + } + outputTexture[id.xy] = float4(result, preserveAlpha != 0 ? value.a : 1.0); +} +)"; + +struct AdapterConstants { + float exposure; + float inverseExposure; + float sdrWhiteScale; + float hdrPeakNits; + float shoulder; + float referenceWhiteNits; + float sdrWhiteNits; + uint32_t inputTransfer; + uint32_t outputTransfer; + uint32_t mode; + uint32_t preserveAlpha; + float normalizationScale; + float peak; + float target; + float shoulderK; + float tailSlope; + float _padding[12]{}; +}; +// D3D11 constant buffers must be a multiple of 16 bytes; the HLSL cbuffer +// reads 20 scalars (80 bytes), so the CPU copy pads to 112. +static_assert(sizeof(AdapterConstants) == 112, "HDR adapter constant buffer layout must match HLSL"); +static_assert(sizeof(AdapterConstants) % 16 == 0, "constant buffer size must stay 16-byte aligned"); +} + +bool HdrSurfaceAdapter::Initialize( + DeviceResources& deviceResources, + BackendDescriptorStore& descriptorStore +) noexcept { + _deviceResources = &deviceResources; + _descriptorStore = &descriptorStore; + winrt::com_ptr blob; + if (!DirectXHelper::CompileComputeShader(HLSL, "Main", blob.put(), "HdrSurfaceAdapter", nullptr, {}, true)) { + return false; + } + HRESULT hr = deviceResources.GetD3DDevice()->CreateComputeShader( + blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _shader.put()); + if (FAILED(hr)) { + Logger::Get().ComError("创建 HDR 表面适配器 Compute Shader 失败", hr); + return false; + } + const D3D11_BUFFER_DESC desc{ + .ByteWidth = sizeof(AdapterConstants), + .Usage = D3D11_USAGE_DYNAMIC, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER, + .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE + }; + return SUCCEEDED(deviceResources.GetD3DDevice()->CreateBuffer(&desc, nullptr, _constants.put())); +} + +bool HdrSurfaceAdapter::_Convert( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters, + HdrTransferFunction transfer, + bool hdrToSdr, + uint32_t mode, + float normalizationScale +) const noexcept { + if (!_deviceResources || !_descriptorStore || !_shader || !_constants || !input || !output || + !parameters.IsValid()) return false; + D3D11_TEXTURE2D_DESC inputDesc{}, outputDesc{}; + input->GetDesc(&inputDesc); + output->GetDesc(&outputDesc); + if (inputDesc.Width != outputDesc.Width || inputDesc.Height != outputDesc.Height) return false; + ID3D11ShaderResourceView* inputSrv = _descriptorStore->GetShaderResourceView(input); + ID3D11UnorderedAccessView* outputUav = _descriptorStore->GetUnorderedAccessView(output); + if (!inputSrv || !outputUav) { + Logger::Get().Error(fmt::format( + "HDR adapter descriptors unavailable: inputSrv={} outputUav={} mode={} inputFormat={} outputFormat={}", + inputSrv != nullptr, outputUav != nullptr, mode, + static_cast(inputDesc.Format), static_cast(outputDesc.Format))); + return false; + } + const HdrTransformConstants base = HdrColorTransform::PrepareConstants( + parameters, + hdrToSdr ? HdrTransferFunction::Linear : transfer, + hdrToSdr ? transfer : HdrTransferFunction::Linear); + // The GPU shoulder must mirror the CPU curve family exactly. Modes 2/3 + // (bounded) consume the curve; SDR modes (0/1) use the plain replicate + // contract (identity + saturate) and ignore the curve coefficients. + const HdrColorTransform::ShoulderCurve curve = HdrColorTransform::BuildShoulderCurve( + parameters, HdrColorTransform::BoundedRouteHighlightTarget); + const AdapterConstants constants{ + base.exposure, base.inverseExposure, base.sdrWhiteScale, base.hdrPeakNits, + base.shoulder, parameters.referenceWhiteNits, parameters.sdrWhiteNits, + base.inputTransfer, base.outputTransfer, mode, + parameters.preserveAlpha ? 1u : 0u, normalizationScale, + curve.peak, curve.target, curve.k, curve.tailSlope + }; + D3D11_MAPPED_SUBRESOURCE mapped{}; + ID3D11DeviceContext4* context = _deviceResources->GetD3DDC(); + if (FAILED(context->Map(_constants.get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + Logger::Get().Error(fmt::format("HDR adapter constants map failed: mode={}", mode)); + return false; + } + memcpy(mapped.pData, &constants, sizeof(constants)); + context->Unmap(_constants.get(), 0); + context->CSSetShader(_shader.get(), nullptr, 0); + ID3D11Buffer* constantBuffer = _constants.get(); + context->CSSetConstantBuffers(0, 1, &constantBuffer); + context->CSSetShaderResources(0, 1, &inputSrv); + context->CSSetUnorderedAccessViews(0, 1, &outputUav, nullptr); + context->Dispatch((outputDesc.Width + 7) / 8, (outputDesc.Height + 7) / 8, 1); + Logger::Get().Info(fmt::format( + "HDR adapter dispatch: mode={} input={} output={} white={:.3f} peak={:.3f} scale={:.3f}", + mode, static_cast(inputDesc.Format), static_cast(outputDesc.Format), + parameters.sdrWhiteNits, parameters.hdrPeakNits, normalizationScale)); + ID3D11ShaderResourceView* nullSrv = nullptr; + ID3D11UnorderedAccessView* nullUav = nullptr; + ID3D11Buffer* nullBuffer = nullptr; + context->CSSetShaderResources(0, 1, &nullSrv); + context->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + context->CSSetConstantBuffers(0, 1, &nullBuffer); + context->CSSetShader(nullptr, nullptr, 0); + return true; +} + +bool HdrSurfaceAdapter::ConvertHdrToSdr( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters, HdrTransferFunction outputTransfer +) const noexcept { + return _Convert(input, output, parameters, outputTransfer, true); +} + +bool HdrSurfaceAdapter::ConvertSdrToHdr( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters, HdrTransferFunction inputTransfer +) const noexcept { + return _Convert(input, output, parameters, inputTransfer, false, 1u); +} + +bool HdrSurfaceAdapter::ConvertHdrToBounded( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters, float normalizationScale +) const noexcept { + return _Convert(input, output, parameters, HdrTransferFunction::Linear, + false, 2u, normalizationScale); +} + +bool HdrSurfaceAdapter::ConvertBoundedToHdr( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters, float normalizationScale +) const noexcept { + return _Convert(input, output, parameters, HdrTransferFunction::Linear, + false, 3u, normalizationScale); +} + +bool HdrSurfaceAdapter::ConvertHdrToScRgb( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters +) const noexcept { + return _Convert(input, output, parameters, HdrTransferFunction::Linear, + false, 4u, 1.0f); +} + +bool HdrSurfaceAdapter::ConvertCanonicalToHdr10( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters +) const noexcept { + return _Convert(input, output, parameters, HdrTransferFunction::PQ, + false, 5u, 1.0f); +} + +bool HdrSurfaceAdapter::ConvertHdr10ToCanonical( + ID3D11Texture2D* input, ID3D11Texture2D* output, + const HdrTransformParameters& parameters +) const noexcept { + return _Convert(input, output, parameters, HdrTransferFunction::PQ, + false, 6u, 1.0f); +} + +} diff --git a/src/Magpie.Core/HdrSurfaceAdapter.h b/src/Magpie.Core/HdrSurfaceAdapter.h new file mode 100644 index 000000000..ba3e76712 --- /dev/null +++ b/src/Magpie.Core/HdrSurfaceAdapter.h @@ -0,0 +1,81 @@ +#pragma once + +#include "HdrColorTransform.h" + +namespace Magpie { + +class BackendDescriptorStore; +class DeviceResources; + +class HdrSurfaceAdapter { +public: + bool Initialize(DeviceResources& deviceResources, BackendDescriptorStore& descriptorStore) noexcept; + + bool ConvertHdrToSdr( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters, + HdrTransferFunction outputTransfer = HdrTransferFunction::SRGB + ) const noexcept; + + bool ConvertSdrToHdr( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters, + HdrTransferFunction inputTransfer = HdrTransferFunction::SRGB + ) const noexcept; + + bool ConvertHdrToBounded( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters, + float normalizationScale + ) const noexcept; + + bool ConvertBoundedToHdr( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters, + float normalizationScale + ) const noexcept; + + // Canonical HDR uses absolute nits internally; the FP16 presentation + // surface uses scRGB where 1.0 represents the 80-nit reference white. + bool ConvertHdrToScRgb( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters + ) const noexcept; + + // Encode canonical linear scRGB into an HDR10/PQ R10 surface for terminal + // backends such as XeSS-FG. The canonical FP16 surface remains unchanged. + bool ConvertCanonicalToHdr10( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters + ) const noexcept; + + bool ConvertHdr10ToCanonical( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters + ) const noexcept; + +private: + bool _Convert( + ID3D11Texture2D* input, + ID3D11Texture2D* output, + const HdrTransformParameters& parameters, + HdrTransferFunction transfer, + bool hdrToSdr, + uint32_t mode = 0, + float normalizationScale = 1.0f + ) const noexcept; + + DeviceResources* _deviceResources = nullptr; + BackendDescriptorStore* _descriptorStore = nullptr; + winrt::com_ptr _shader; + winrt::com_ptr _constants; +}; + +} diff --git a/src/Magpie.Core/Magpie.Core.vcxproj b/src/Magpie.Core/Magpie.Core.vcxproj index 08b65220c..6433b1858 100644 --- a/src/Magpie.Core/Magpie.Core.vcxproj +++ b/src/Magpie.Core/Magpie.Core.vcxproj @@ -6,12 +6,13 @@ true true false + false low Win32Proj {0e5205ae-dfa9-4cb8-b662-e43cd6512e2a} 10.0.26100.0 - $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ - $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(BuildRoot)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(BuildRoot)\bin\$(Platform)\$(Configuration)\ $(IntDir)\Generated Files\ @@ -27,7 +28,7 @@ - + @@ -45,6 +46,7 @@ + @@ -96,6 +98,7 @@ + @@ -117,15 +120,32 @@ + + + + + + + + + + + + + + + + + @@ -152,6 +172,16 @@ Create + + + + + + + + + + @@ -177,13 +207,18 @@ + + + + NotUsing + NotUsing @@ -209,9 +244,6 @@ NotUsing 4201;%(DisableSpecificWarnings) - - NotUsing - @@ -243,7 +275,12 @@ - + + $(SolutionDir)scripts\Generate-FidelityFXOpticalFlowShaders.ps1 + $(SolutionDir)..\..\scripts\Generate-FidelityFXOpticalFlowShaders.ps1 + + + diff --git a/src/Magpie.Core/Magpie.Core.vcxproj.filters b/src/Magpie.Core/Magpie.Core.vcxproj.filters index f6bf51d17..2a5d18345 100644 --- a/src/Magpie.Core/Magpie.Core.vcxproj.filters +++ b/src/Magpie.Core/Magpie.Core.vcxproj.filters @@ -44,6 +44,19 @@ Capture + + Capture + + Include + Helpers + Helpers + Include + Helpers + Helpers + Helpers + Helpers + Helpers + Helpers Capture @@ -142,6 +155,9 @@ Render + + Render + Render @@ -209,6 +225,16 @@ Capture + Capture + Include + Helpers + Helpers + Include + Helpers + Helpers + Helpers + Helpers + Helpers Capture diff --git a/src/Magpie.Core/NativeEffectBackend.h b/src/Magpie.Core/NativeEffectBackend.h index 4d88f72ff..8cb9d2c16 100644 --- a/src/Magpie.Core/NativeEffectBackend.h +++ b/src/Magpie.Core/NativeEffectBackend.h @@ -1,6 +1,8 @@ #pragma once #include "FrameGuidanceTypes.h" #include "ScalingOptions.h" +#include "HdrEffectBoundary.h" +#include namespace Magpie { @@ -9,6 +11,8 @@ class DeviceResources; struct NativeEffectDrawContext { ID3D11Texture2D* input = nullptr; ID3D11Texture2D* output = nullptr; + HdrFrameMetadata inputMetadata{}; + HdrFrameMetadata outputMetadata{}; FrameGuidanceFrameId frameId = 0; // Changes whenever an earlier effect in the chain changes its output for // the same captured frame. Native effects that cache duplicate frames must @@ -25,6 +29,9 @@ class NativeEffectBackend { public: virtual ~NativeEffectBackend() = default; + virtual void SetHdrBoundary(HdrEffectBoundaryContext context) noexcept { _hdrBoundary = std::move(context); } + const HdrEffectBoundaryContext& GetHdrBoundary() const noexcept { return _hdrBoundary; } + virtual FrameGuidanceRequirements GetFrameGuidanceRequirements() const noexcept { return {}; } @@ -58,6 +65,9 @@ class NativeEffectBackend { ) noexcept = 0; virtual bool Draw(const NativeEffectDrawContext& context) noexcept = 0; + +protected: + HdrEffectBoundaryContext _hdrBoundary{}; }; } diff --git a/src/Magpie.Core/NativeEffectBackendFactory.cpp b/src/Magpie.Core/NativeEffectBackendFactory.cpp index 60a739264..a1b24df73 100644 --- a/src/Magpie.Core/NativeEffectBackendFactory.cpp +++ b/src/Magpie.Core/NativeEffectBackendFactory.cpp @@ -4,13 +4,17 @@ #include "NgxRuntimeGuard.h" #include "DLSSNRFilter.h" #include "DLSSSRUpscaler.h" +#include "FSR2ZeroMVUpscaler.h" +#include "FSR3ZeroMVUpscaler.h" #include "FSR2Upscaler.h" #include "FSR3Upscaler.h" #include "RTXVideoDenoiser.h" +#include "XeSSZeroMVUpscaler.h" #include "XeSSUpscaler.h" #include "FrameGuidanceDiagnostics.h" #include "Logger.h" #include "ScalingOptions.h" +#include "ScalingWindow.h" #include "EffectParameterRules.h" #include "OpticalFlowSettings.h" @@ -41,6 +45,7 @@ NativeEffectBackendResult CreateNativeEffectBackend( ID3D11Texture2D* input, ID3D11Texture2D* output ) noexcept { + const bool hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); if (effectName == "Diagnostics\\FrameGuidance_Motion" || effectName == "Diagnostics\\FrameGuidance_Confidence") { auto getParameter = [&](std::string_view name, float defaultValue) { @@ -62,7 +67,7 @@ NativeEffectBackendResult CreateNativeEffectBackend( } if (effectName == "DLSSNR\\DLSSNR_AI_Filter") { - const DLSSNRSettings settings = ParseDLSSNRSettings(option); + const DLSSNRSettings settings = ParseDLSSNRSettings(option, hdrEnabled); auto backend = std::make_unique(); if (!backend->Initialize(resources, ngxCore, input, output, settings)) { if (NgxRuntimeGuard::IsFaulted()) { @@ -78,19 +83,118 @@ NativeEffectBackendResult CreateNativeEffectBackend( return { true, std::move(backend) }; } - if (IsSuperResolutionEffect(effectName)) { + + if (!hdrEnabled) { + if (IsSuperResolutionEffect(effectName)) { + const auto motion = ParseOpticalFlowRequest(option, + effectName == "DLSS\\DLSS_SR" ? OpticalFlowMethod::Nvidia : OpticalFlowMethod::None); + if (effectName == "DLSS\\DLSS_SR") + return CreateBackend(effectName, resources, input, output, + DLSSSRSettings{ .motionRequest = motion }); + if (effectName == "FSR2\\FSR2_SR") + return CreateBackend(effectName, resources, input, output, motion); + if (effectName == "XeSS\\XeSS_SR") + return CreateBackend(effectName, resources, input, output, motion); + return CreateBackend(effectName, resources, input, output, + motion, effectName == "FSR4\\FSR4_SR"); + } + } + + // Keep the legacy ZeroMV/Jitter/OpticalFlow contracts on their original + // backends. These names carry distinct temporal and auxiliary-resource + // semantics even when HDR compatibility is disabled. + if (hdrEnabled && (effectName == "DLSS\\DLSS_ZeroMV" || + effectName == "DLSS\\DLSS_ZeroMV_Jitter" || + effectName == "DLSS\\DLSS_OpticalFlow")) { + auto getParameter = [&](std::string_view name, float defaultValue) { + auto it = option.parameters.find(std::string(name)); + return it == option.parameters.end() ? defaultValue : it->second; + }; + const bool isLegacyOpticalFlow = effectName == "DLSS\\DLSS_OpticalFlow"; + const auto quality = static_cast(std::clamp( + static_cast(std::lround(getParameter("nvidiaOpticalFlowQuality", 2.0f))), + 0, int(NVIDIA_OPTICAL_FLOW_MAX_QUALITY))); + return CreateBackend( + effectName, resources, input, output, + DLSSSRSettings{ + .motionRequest = isLegacyOpticalFlow + ? MotionVectorRequest::Nvidia(quality) + : MotionVectorRequest{} + }); + } + + if (hdrEnabled && (effectName == "FSR2\\FSR2_ZeroMV" || + effectName == "FSR2\\FSR2_ZeroMV_Jitter" || + effectName == "FSR2\\FSR2_OpticalFlow")) { + return CreateBackend(effectName, resources, input, output, + effectName == "FSR2\\FSR2_OpticalFlow", + effectName == "FSR2\\FSR2_ZeroMV_Jitter"); + } + + const bool isFsr3 = effectName == "FSR3\\FSR3_ZeroMV" || + effectName == "FSR3\\FSR3_ZeroMV_Jitter" || + effectName == "FSR3\\FSR3_OpticalFlow"; + const bool isFsr4 = effectName == "FSR4\\FSR4_ZeroMV" || + effectName == "FSR4\\FSR4_ZeroMV_Jitter" || + effectName == "FSR4\\FSR4_OpticalFlow"; + if (hdrEnabled && (isFsr3 || isFsr4)) { + return CreateBackend(effectName, resources, input, output, + effectName.ends_with("OpticalFlow"), + effectName.ends_with("ZeroMV_Jitter"), isFsr4); + } + + if (hdrEnabled && (effectName == "XeSS\\XeSS_ZeroMV" || + effectName == "XeSS\\XeSS_ZeroMV_Jitter" || + effectName == "XeSS\\XeSS_OpticalFlow")) { + return CreateBackend(effectName, resources, input, output, + effectName == "XeSS\\XeSS_OpticalFlow", + effectName == "XeSS\\XeSS_ZeroMV_Jitter"); + } + + if (hdrEnabled && (effectName == "DLSS\\DLSS_SR" || + effectName == "FSR2\\FSR2_SR" || + effectName == "FSR3\\FSR3_SR" || + effectName == "FSR4\\FSR4_SR" || + effectName == "XeSS\\XeSS_SR")) { const auto motion = ParseOpticalFlowRequest(option, effectName == "DLSS\\DLSS_SR" ? OpticalFlowMethod::Nvidia : OpticalFlowMethod::None); + const D3D11_TEXTURE2D_DESC inputDesc = [&]() { + D3D11_TEXTURE2D_DESC desc{}; + input->GetDesc(&desc); + return desc; + }(); + const bool hdrInput = hdrEnabled && + inputDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT; + const FsrHdrProtocol hdrProtocol{ + .hdrColorInput = hdrInput, + .transfer = GroupBTransfer::Linear, + .preExposure = 1.0f, + .exposure = 1.0f, + .depthInverted = true, + .depthInfinite = true, + .useReactiveMask = true, + .useTransparencyMask = true, + }; if (effectName == "DLSS\\DLSS_SR") { - return CreateBackend(effectName, resources, input, output, - DLSSSRSettings{ .motionRequest = motion }); + auto backend = std::make_unique(); + backend->SetDlssHdrProtocol(hdrProtocol); + if (!backend->Initialize(resources, input, output, + DLSSSRSettings{ .motionRequest = motion })) return { true, nullptr }; + return { true, std::move(backend) }; + } + if (effectName == "FSR2\\FSR2_SR") { + auto backend = std::make_unique(); + backend->SetFsrHdrProtocol(hdrProtocol); + if (!backend->Initialize(resources, input, output, motion)) return { true, nullptr }; + return { true, std::move(backend) }; } - if (effectName == "FSR2\\FSR2_SR") - return CreateBackend(effectName, resources, input, output, motion); if (effectName == "XeSS\\XeSS_SR") return CreateBackend(effectName, resources, input, output, motion); - return CreateBackend(effectName, resources, input, output, - motion, effectName == "FSR4\\FSR4_SR"); + auto backend = std::make_unique(); + backend->SetFsrHdrProtocol(hdrProtocol); + if (!backend->Initialize(resources, input, output, motion, + effectName == "FSR4\\FSR4_SR")) return { true, nullptr }; + return { true, std::move(backend) }; } const bool isRtxVideo = effectName.starts_with("RTXVideo\\RTXVideo_Denoise_") || @@ -110,13 +214,13 @@ NativeEffectBackendResult CreateNativeEffectBackend( qualityLevel = 11; } auto backend = std::make_unique(); - if (!backend->Initialize(resources, input, output, qualityLevel)) { + if (!backend->Initialize(resources, input, output, qualityLevel, + isVsr ? RtxVideoEffectKind::Vsr : RtxVideoEffectKind::Denoise)) { Logger::Get().Error(fmt::format("Initialize native effect {} failed", effectName)); return { true, nullptr, backend->InitializationError() }; } return { true, std::move(backend) }; } - return {}; } diff --git a/src/Magpie.Core/NvCVImageD3D11Proxy.cpp b/src/Magpie.Core/NvCVImageD3D11Proxy.cpp new file mode 100644 index 000000000..e76ede084 --- /dev/null +++ b/src/Magpie.Core/NvCVImageD3D11Proxy.cpp @@ -0,0 +1,57 @@ +#include "pch.h" +#include "NvCVImageD3D11Proxy.h" + +namespace { + +HMODULE GetNvCVImageModule() noexcept { + static HMODULE module = []() noexcept { + if (HMODULE loaded = GetModuleHandleW(L"NVCVImage.dll")) { + return loaded; + } + + wchar_t modulePath[MAX_PATH]{}; + const DWORD length = GetModuleFileNameW(nullptr, modulePath, ARRAYSIZE(modulePath)); + if (length != 0 && length < ARRAYSIZE(modulePath)) { + std::wstring path(modulePath, length); + const size_t separator = path.find_last_of(L"\\/"); + if (separator != std::wstring::npos) { + path.resize(separator); + path += L"\\NVCVImage.dll"; + if (HMODULE runtime = LoadLibraryExW( + path.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32)) { + return runtime; + } + } + } + return LoadLibraryExW( + L"NVCVImage.dll", nullptr, + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32); + }(); + return module; +} + +template +Function GetNvCVImageFunction(const char* name) noexcept { + return reinterpret_cast(GetProcAddress(GetNvCVImageModule(), name)); +} + +} + +NvCV_Status NvCVImage_InitFromD3D11Texture(NvCVImage* image, ID3D11Texture2D* texture) { + using Function = NvCV_Status(__cdecl*)(NvCVImage*, ID3D11Texture2D*); + const auto function = GetNvCVImageFunction("NvCVImage_InitFromD3D11Texture"); + return function ? function(image, texture) : NVCV_ERR_LIBRARY; +} + +NvCV_Status NvCVImage_MapResource(NvCVImage* image, struct CUstream_st* stream) { + using Function = NvCV_Status(__cdecl*)(NvCVImage*, struct CUstream_st*); + const auto function = GetNvCVImageFunction("NvCVImage_MapResource"); + return function ? function(image, stream) : NVCV_ERR_LIBRARY; +} + +NvCV_Status NvCVImage_UnmapResource(NvCVImage* image, struct CUstream_st* stream) { + using Function = NvCV_Status(__cdecl*)(NvCVImage*, struct CUstream_st*); + const auto function = GetNvCVImageFunction("NvCVImage_UnmapResource"); + return function ? function(image, stream) : NVCV_ERR_LIBRARY; +} diff --git a/src/Magpie.Core/NvCVImageD3D11Proxy.h b/src/Magpie.Core/NvCVImageD3D11Proxy.h new file mode 100644 index 000000000..14bbee2e0 --- /dev/null +++ b/src/Magpie.Core/NvCVImageD3D11Proxy.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +struct ID3D11Texture2D; + +NvCV_Status NvCVImage_InitFromD3D11Texture(NvCVImage* image, ID3D11Texture2D* texture); +NvCV_Status NvCVImage_MapResource(NvCVImage* image, struct CUstream_st* stream); +NvCV_Status NvCVImage_UnmapResource(NvCVImage* image, struct CUstream_st* stream); diff --git a/src/Magpie.Core/NvidiaOpticalFlowProvider.cpp b/src/Magpie.Core/NvidiaOpticalFlowProvider.cpp index 3d3321438..dc6e5ac84 100644 --- a/src/Magpie.Core/NvidiaOpticalFlowProvider.cpp +++ b/src/Magpie.Core/NvidiaOpticalFlowProvider.cpp @@ -4,6 +4,7 @@ #include "DeviceResources.h" #include "DirectXHelper.h" #include "Logger.h" +#include "ScalingWindow.h" #ifdef MP_ENABLE_NVIDIA_OPTICAL_FLOW #include @@ -91,6 +92,18 @@ void Densify(uint3 tid : SV_DispatchThreadID) { } )"; +constexpr char HDR_TO_NVOF_HLSL[] = R"( +Texture2D Source : register(t0); +RWTexture2D Target : register(u0); +[numthreads(8, 8, 1)] +void Convert(uint3 tid : SV_DispatchThreadID) { + uint width, height; Target.GetDimensions(width, height); + if (tid.x >= width || tid.y >= height) return; + float4 value = Source.Load(int3(tid.xy, 0)); + Target[tid.xy] = float4(saturate(max(value.rgb, 0.0) / 4.5), saturate(value.a)); +} +)"; + template T GetExport(HMODULE module, const char* name) noexcept { return reinterpret_cast(GetProcAddress(module, name)); @@ -141,6 +154,9 @@ FrameGuidanceMetadata MakeMetadata( ) noexcept { return { .frameId = frame.frameId, + .captureSequence = frame.captureSequence, + .resourceGeneration = frame.resourceGeneration, + .timestamp100ns = frame.timestamp100ns, .sourceExtent = frame.sourceExtent, .validRegion = frame.validRegion, .resetReason = resetReason, @@ -233,6 +249,9 @@ struct NvidiaOpticalFlowProvider::Impl { confidenceUav = nullptr; densifyShader = nullptr; paramsBuffer = nullptr; + hdrToNvofShader = nullptr; + hdrSourceSrv = nullptr; + hdrSourceTexture = nullptr; for (GpuQuerySlot& slot : gpuQuerySlots) { slot.disjoint = nullptr; slot.start = nullptr; @@ -244,6 +263,8 @@ struct NvidiaOpticalFlowProvider::Impl { gpuTimingSampleCount = 0; gpuTimingAvailable = false; profileLabel = {}; + inputDxgiFormat = DXGI_FORMAT_UNKNOWN; + inputBufferFormat = NV_OF_BUFFER_FORMAT_ABGR8; gridSize = 0; costEnabled = false; previousSlot = 0; @@ -355,12 +376,13 @@ struct NvidiaOpticalFlowProvider::Impl { } bool CreateTextures() noexcept { - const UINT sourceBind = D3D11_BIND_SHADER_RESOURCE; + const UINT sourceBind = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; for (auto& texture : input) { texture = DirectXHelper::CreateTexture2D( - device, DXGI_FORMAT_B8G8R8A8_UNORM, extent.width, extent.height, + device, inputDxgiFormat, extent.width, extent.height, sourceBind); - if (!texture) return false; + if (!texture || FAILED(device->CreateUnorderedAccessView(texture.get(), nullptr, + inputUav[&texture - &input[0]].put()))) return false; } const uint32_t flowWidth = (extent.width + gridSize - 1) / gridSize; @@ -417,6 +439,12 @@ struct NvidiaOpticalFlowProvider::Impl { } bool CreatePostProcess() noexcept { + winrt::com_ptr hdrBlob; + if (!DirectXHelper::CompileComputeShader( + HDR_TO_NVOF_HLSL, "Convert", hdrBlob.put(), + "FrameGuidance/NVOF_HdrToInput.hlsl") || + FAILED(device->CreateComputeShader(hdrBlob->GetBufferPointer(), + hdrBlob->GetBufferSize(), nullptr, hdrToNvofShader.put()))) return false; winrt::com_ptr shaderBlob; if (!DirectXHelper::CompileComputeShader( DENSIFY_FLOW_HLSL, "Densify", shaderBlob.put(), @@ -504,9 +532,32 @@ struct NvidiaOpticalFlowProvider::Impl { HasFormat(costFormats, DXGI_FORMAT_R8_UINT); if (!QueryFormats(NV_OF_BUFFER_USAGE_INPUT, inputFormats) || !QueryFormats(NV_OF_BUFFER_USAGE_OUTPUT, outputFormats) || - !HasFormat(inputFormats, DXGI_FORMAT_B8G8R8A8_UNORM) || !HasFormat(outputFormats, DXGI_FORMAT_R16G16_SINT)) { - Logger::Get().Warn("NVOF required ABGR8/S10.5 formats unavailable"); + Logger::Get().Warn("NVOF required S10.5 output format unavailable"); + return false; + } + const bool hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); + if (!hdrEnabled) { + if (!HasFormat(inputFormats, DXGI_FORMAT_B8G8R8A8_UNORM)) { + Logger::Get().Warn("NVOF required ABGR8 input format unavailable"); + return false; + } + inputDxgiFormat = DXGI_FORMAT_B8G8R8A8_UNORM; + inputBufferFormat = NV_OF_BUFFER_FORMAT_ABGR8; + } else if (HasFormat(inputFormats, DXGI_FORMAT_B8G8R8A8_UNORM)) { + inputDxgiFormat = DXGI_FORMAT_B8G8R8A8_UNORM; + inputBufferFormat = NV_OF_BUFFER_FORMAT_ABGR8; + } else if (HasFormat(inputFormats, DXGI_FORMAT_R8G8B8A8_UNORM)) { + inputDxgiFormat = DXGI_FORMAT_R8G8B8A8_UNORM; + inputBufferFormat = NV_OF_BUFFER_FORMAT_ABGR8; + } else if (HasFormat(inputFormats, DXGI_FORMAT_R8_UNORM)) { + inputDxgiFormat = DXGI_FORMAT_R8_UNORM; + inputBufferFormat = NV_OF_BUFFER_FORMAT_GRAYSCALE8; + } else if (HasFormat(inputFormats, DXGI_FORMAT_NV12)) { + inputDxgiFormat = DXGI_FORMAT_NV12; + inputBufferFormat = NV_OF_BUFFER_FORMAT_NV12; + } else { + Logger::Get().Warn("NVOF input formats unavailable (ABGR8/GRAYSCALE8/NV12)"); return false; } @@ -539,7 +590,7 @@ struct NvidiaOpticalFlowProvider::Impl { .enableRoi = NV_OF_FALSE, .predDirection = NV_OF_PRED_DIRECTION_BOTH, .enableGlobalFlow = NV_OF_FALSE, - .inputBufferFormat = NV_OF_BUFFER_FORMAT_ABGR8 + .inputBufferFormat = inputBufferFormat }; NV_OF_STATUS status = api.nvOFInit(session, &init); bidirectional = status == NV_OF_SUCCESS; @@ -669,11 +720,17 @@ struct NvidiaOpticalFlowProvider::Impl { uint64_t gpuTimingSampleCount = 0; std::string_view profileLabel; bool bidirectional = false; + std::array, 2> inputUav; + winrt::com_ptr hdrSourceSrv; + ID3D11Texture2D* hdrSourceTexture = nullptr; + winrt::com_ptr hdrToNvofShader; bool costEnabled = false; bool gpuTimingAvailable = false; bool historyValid = false; OpticalFlowInitializationError initializationError = OpticalFlowInitializationError::ProviderUnavailable; + DXGI_FORMAT inputDxgiFormat = DXGI_FORMAT_UNKNOWN; + NV_OF_BUFFER_FORMAT inputBufferFormat = NV_OF_BUFFER_FORMAT_ABGR8; }; NvidiaOpticalFlowProvider::NvidiaOpticalFlowProvider( @@ -698,9 +755,44 @@ bool NvidiaOpticalFlowProvider::BeginFrame( if (!frame.color || frame.sourceExtent != impl.extent || !impl.session) { return false; } + D3D11_TEXTURE2D_DESC frameDesc{}; + frame.color->GetDesc(&frameDesc); + if (frameDesc.Width != impl.extent.width || frameDesc.Height != impl.extent.height || + (frameDesc.Format != impl.inputDxgiFormat && + !(ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && + frameDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT))) { + Logger::Get().Warn(fmt::format( + "NVOF input format mismatch: frame={}x{} dxgi={}, session dxgi={}; " + "supported contracts are ABGR8/GRAYSCALE8/NV12", + frameDesc.Width, frameDesc.Height, + static_cast(frameDesc.Format), + static_cast(impl.inputDxgiFormat))); + return false; + } const uint32_t currentSlot = impl.historyValid ? 1u - impl.previousSlot : 0u; - impl.context->CopyResource(impl.input[currentSlot].get(), frame.color); + if (frameDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT) { + if (impl.hdrSourceTexture != frame.color) { + impl.hdrSourceSrv = nullptr; + if (FAILED(impl.device->CreateShaderResourceView( + frame.color, nullptr, impl.hdrSourceSrv.put()))) return false; + impl.hdrSourceTexture = frame.color; + } + ID3D11ShaderResourceView* srv = impl.hdrSourceSrv.get(); + ID3D11UnorderedAccessView* uav = impl.inputUav[currentSlot].get(); + impl.context->CSSetShader(impl.hdrToNvofShader.get(), nullptr, 0); + impl.context->CSSetShaderResources(0, 1, &srv); + impl.context->CSSetUnorderedAccessViews(0, 1, &uav, nullptr); + impl.context->Dispatch((impl.extent.width + 7) / 8, + (impl.extent.height + 7) / 8, 1); + ID3D11ShaderResourceView* nullSrv = nullptr; + ID3D11UnorderedAccessView* nullUav = nullptr; + impl.context->CSSetShaderResources(0, 1, &nullSrv); + impl.context->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + impl.context->CSSetShader(nullptr, nullptr, 0); + } else { + impl.context->CopyResource(impl.input[currentSlot].get(), frame.color); + } if (!impl.historyValid) { impl.ClearDenseOutput(); impl.previousSlot = currentSlot; diff --git a/src/Magpie.Core/OverlayDrawer.cpp b/src/Magpie.Core/OverlayDrawer.cpp index 1bd65a013..5b1fc435d 100644 --- a/src/Magpie.Core/OverlayDrawer.cpp +++ b/src/Magpie.Core/OverlayDrawer.cpp @@ -144,9 +144,7 @@ void OverlayDrawer::Draw( if (_effectParametersWindowLayoutDirty && !ImGui::IsAnyMouseDown()) { const ScalingWindow& scalingWindow = ScalingWindow::Get(); const ScalingOptions& options = scalingWindow.Options(); - if (options.save) { - options.save(options, scalingWindow.Handle()); - } + if (options.save) options.save(options, scalingWindow.Handle()); _effectParametersWindowLayoutDirty = false; } _isEffectParameterInputActive = _isEffectParametersVisible && ImGui::IsAnyItemActive(); @@ -250,7 +248,6 @@ void OverlayDrawer::RestoreSessionState(const OverlaySessionState& state) noexce _isToolbarVisible = state.toolbarVisible; _isToolbarPinned = state.toolbarPinned; _isEffectParametersVisible = state.effectParametersVisible; - // Use the action path so the new backend also starts/stops timing queries. if (_isProfilerVisible != state.profilerVisible) InvokeAction(OverlayAction::Profiler); _overlayDirty = true; _ClearStatesIfNoVisibleWindow(); @@ -1343,8 +1340,6 @@ bool OverlayDrawer::_DrawEffectParameters(int& itemId) noexcept { ImGuiCol_ResizeGripActive, ImVec4(0.35f, 0.67f, 0.95f, 1.0f)); const bool expanded = ImGui::Begin(title.c_str(), &_isEffectParametersVisible); ImGuiWindow* window = ImGui::GetCurrentWindow(); - // Use the expanded size even while collapsed. A title-bar double click must - // not persist the collapsed height as the preferred panel size. const OverlayWindowRect rect{ std::clamp(window->Pos.x, 0.0f, std::max(0.0f, displaySize.x - window->SizeFull.x)), std::clamp(window->Pos.y, 0.0f, std::max(0.0f, displaySize.y - window->SizeFull.y)), @@ -1359,7 +1354,6 @@ bool OverlayDrawer::_DrawEffectParameters(int& itemId) noexcept { const bool resizedY = rect.height != previous.height; if (moved || resizedX || resizedY) { RememberOverlayWindowPosition(windowOption, rect, displaySize.x, displaySize.y, _dpiScale); - // Keep the user's preferred size on axes only constrained by a smaller viewport. if (resizedX) windowOption.width = rect.width / _dpiScale; if (resizedY) windowOption.height = rect.height / _dpiScale; _effectParametersWindowLayoutDirty = true; @@ -1405,8 +1399,6 @@ bool OverlayDrawer::_DrawEffectParameters(int& itemId) noexcept { ImGui::TextDisabled("%s", _GetResourceString(L"Overlay_EffectParameters_RestartRequired").c_str()); ImGui::SetNextItemWidth(-1.0f); int targetFps = static_cast(std::lround(_draftFrameSync.frameRate)); - // Only commit on interaction: opening the panel must preserve fractional FPS - // values saved by Home or older versions. const std::string targetFpsText = fmt::format("{:g} FPS", _draftFrameSync.frameRate); if (ImGui::SliderInt("##targetFps", &targetFps, 15, 360, targetFpsText.c_str(), ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_NoInput)) { diff --git a/src/Magpie.Core/PassThroughFrames.cpp b/src/Magpie.Core/PassThroughFrames.cpp index 7b662ca91..73c79f999 100644 --- a/src/Magpie.Core/PassThroughFrames.cpp +++ b/src/Magpie.Core/PassThroughFrames.cpp @@ -8,7 +8,7 @@ namespace Magpie { namespace { -constexpr char REFERENCE_HLSL[] = R"( +constexpr char REFERENCE_LDR_HLSL[] = R"( Texture2D Input : register(t0); SamplerState LinearClamp : register(s0); RWTexture2D Output : register(u0); @@ -21,6 +21,29 @@ void Reference(uint3 id : SV_DispatchThreadID) { Output[id.xy] = float4(Input.SampleLevel(LinearClamp, uv, 0).rgb, 1.0); } )"; + +constexpr char REFERENCE_HDR_HLSL[] = R"( +cbuffer Transform : register(b0) { + uint hdrEnabled; + float exposure; + float sdrWhiteNits; + float shoulder; +}; +Texture2D Input : register(t0); +SamplerState LinearClamp : register(s0); +RWTexture2D Output : register(u0); + +[numthreads(8, 8, 1)] +void Reference(uint3 id : SV_DispatchThreadID) { + uint width, height; + Output.GetDimensions(width, height); + if (id.x >= width || id.y >= height) return; + float2 uv = (float2(id.xy) + 0.5) / float2(width, height); + float3 rgb = Input.SampleLevel(LinearClamp, uv, 0).rgb; + // Canonical HDR and presentation both use linear scRGB. + Output[id.xy] = float4(rgb, 1.0); +} +)"; } void PassThroughFrames::_ClearBackend() noexcept { @@ -29,36 +52,51 @@ void PassThroughFrames::_ClearBackend() noexcept { _handles.fill(nullptr); _valid.fill(false); _frameIds.fill(0); + _metadata.fill({}); _current = nullptr; _previous = nullptr; _inputView = nullptr; _outputView = nullptr; _shader = nullptr; _sampler = nullptr; + _constants = nullptr; + _hdrEnabled = false; + _hdrParameters = {}; _currentValid = false; _previousValid = false; + _currentMetadata = {}; + _previousMetadata = {}; + _presentedMetadata = {}; } bool PassThroughFrames::InitializeBackend(DeviceResources& resources, - ID3D11Texture2D* input, ID3D11Texture2D* output, uint32_t slotCount) noexcept { + ID3D11Texture2D* input, ID3D11Texture2D* output, uint32_t slotCount, + bool hdrEnabled, const HdrTransformParameters& hdrParameters, + const HdrFrameMetadata& frameMetadata) noexcept { _ClearBackend(); _backendResources = &resources; + _hdrEnabled = hdrEnabled; + _hdrParameters = hdrParameters.IsValid() ? hdrParameters : HdrTransformParameters{}; + _currentMetadata = frameMetadata; if (!input || !output || slotCount == 0 || slotCount > MAX_SLOTS) return false; D3D11_TEXTURE2D_DESC outputDesc{}; output->GetDesc(&outputDesc); _width = outputDesc.Width; _height = outputDesc.Height; + const DXGI_FORMAT referenceFormat = hdrEnabled + ? DXGI_FORMAT_R16G16B16A16_FLOAT + : DXGI_FORMAT_R8G8B8A8_UNORM; auto device = resources.GetD3DDevice(); auto fail = [&]() { Logger::Get().Warn("Pass-through reference resources unavailable; effects remain enabled"); _ClearBackend(); return false; }; - _current = DirectXHelper::CreateTexture2D(device, DXGI_FORMAT_R8G8B8A8_UNORM, + _current = DirectXHelper::CreateTexture2D(device, referenceFormat, _width, _height, D3D11_BIND_UNORDERED_ACCESS); if (!_current) return fail(); if (slotCount > 1) { - _previous = DirectXHelper::CreateTexture2D(device, DXGI_FORMAT_R8G8B8A8_UNORM, + _previous = DirectXHelper::CreateTexture2D(device, referenceFormat, _width, _height, 0); if (!_previous) return fail(); } @@ -66,10 +104,33 @@ bool PassThroughFrames::InitializeBackend(DeviceResources& resources, if (SUCCEEDED(hr)) hr = device->CreateUnorderedAccessView(_current.get(), nullptr, _outputView.put()); if (FAILED(hr)) return fail(); winrt::com_ptr blob; - if (!DirectXHelper::CompileComputeShader(REFERENCE_HLSL, "Reference", blob.put(), + if (!DirectXHelper::CompileComputeShader( + hdrEnabled ? REFERENCE_HDR_HLSL : REFERENCE_LDR_HLSL, + "Reference", blob.put(), "PassThroughReference", nullptr, {}, true)) return fail(); hr = device->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, _shader.put()); if (FAILED(hr)) return fail(); + if (hdrEnabled) { + struct ReferenceConstants { + uint32_t hdrEnabled; + float exposure; + float sdrWhiteNits; + float shoulder; + } constants{ + 1u, + _hdrParameters.exposure, + _hdrParameters.sdrWhiteNits, + _hdrParameters.shoulder + }; + D3D11_BUFFER_DESC constantsDesc{ + .ByteWidth = sizeof(constants), + .Usage = D3D11_USAGE_DEFAULT, + .BindFlags = D3D11_BIND_CONSTANT_BUFFER + }; + D3D11_SUBRESOURCE_DATA constantsData{ .pSysMem = &constants }; + hr = device->CreateBuffer(&constantsDesc, &constantsData, _constants.put()); + if (FAILED(hr)) return fail(); + } D3D11_SAMPLER_DESC samplerDesc{}; samplerDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; samplerDesc.AddressU = samplerDesc.AddressV = samplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; @@ -78,7 +139,7 @@ bool PassThroughFrames::InitializeBackend(DeviceResources& resources, if (FAILED(hr)) return fail(); for (uint32_t i = 0; i < slotCount; ++i) { auto& slot = _backendSlots[i]; - slot.texture = DirectXHelper::CreateTexture2D(device, DXGI_FORMAT_R8G8B8A8_UNORM, + slot.texture = DirectXHelper::CreateTexture2D(device, referenceFormat, _width, _height, D3D11_BIND_SHADER_RESOURCE, D3D11_USAGE_DEFAULT, D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX); if (!slot.texture) return fail(); @@ -126,6 +187,7 @@ void PassThroughFrames::UpdateBackend(uint64_t captureFrameId, bool newCapture) if (newCapture && _previous && _currentValid) { context->CopyResource(_previous.get(), _current.get()); _previousFrameId = _currentFrameId; + _previousMetadata = _currentMetadata; _previousValid = true; } context->ClearState(); @@ -133,15 +195,28 @@ void PassThroughFrames::UpdateBackend(uint64_t captureFrameId, bool newCapture) auto output = _outputView.get(); auto sampler = _sampler.get(); context->CSSetShader(_shader.get(), nullptr, 0); + if (_constants) { + auto constants = _constants.get(); + context->CSSetConstantBuffers(0, 1, &constants); + } context->CSSetShaderResources(0, 1, &input); context->CSSetUnorderedAccessViews(0, 1, &output, nullptr); context->CSSetSamplers(0, 1, &sampler); context->Dispatch((_width + 7) / 8, (_height + 7) / 8, 1); context->ClearState(); _currentFrameId = captureFrameId; + _currentMetadata.frameId = captureFrameId; _currentValid = true; } +void PassThroughFrames::UpdateBackend(const HdrFrame& frame, bool newCapture) noexcept { + UpdateBackend(frame.metadata.frameId, newCapture); + if (_hdrEnabled && frame.IsCanonical()) { + _currentMetadata = frame.metadata; + _currentMetadata.stage = HdrFrameStage::CanonicalInput; + } +} + void PassThroughFrames::Publish(uint32_t slot, bool generatedFrame) noexcept { FrameTrace::Scope traceReference(FrameTrace::Event::ReferencePublish); if (!_sharingEnabled || !_backendSlots[slot].texture || !_currentValid) return; @@ -149,6 +224,9 @@ void PassThroughFrames::Publish(uint32_t slot, bool generatedFrame) noexcept { _backendResources->GetD3DDC()->CopyResource(_backendSlots[slot].texture.get(), previous ? _previous.get() : _current.get()); _frameIds[slot] = previous ? _previousFrameId : _currentFrameId; + _metadata[slot] = previous ? _previousMetadata : _currentMetadata; + _metadata[slot].stage = HdrFrameStage::PublishedOutput; + _metadata[slot].generated = generatedFrame; _valid[slot] = true; } @@ -174,6 +252,7 @@ bool PassThroughFrames::Consume(uint32_t slot) noexcept { } _frontendResources->GetD3DDC()->CopyResource(_base.get(), _frontendSlots[slot].texture.get()); _baseFrameId = _frameIds[slot]; + _presentedMetadata = _metadata[slot]; _baseValid = true; return true; } @@ -184,6 +263,7 @@ void PassThroughFrames::OnPresented() noexcept { if (_presentedValid) { _frontendResources->GetD3DDC()->CopyResource(_presented.get(), _base.get()); _presentedFrameId = _baseFrameId; + _presentedMetadata.stage = HdrFrameStage::PresentedOutput; } } diff --git a/src/Magpie.Core/PassThroughFrames.h b/src/Magpie.Core/PassThroughFrames.h index 69a0b1f6e..f84111c4c 100644 --- a/src/Magpie.Core/PassThroughFrames.h +++ b/src/Magpie.Core/PassThroughFrames.h @@ -1,4 +1,5 @@ #pragma once +#include "HdrColorTransform.h" #include #include #include @@ -13,9 +14,12 @@ class PassThroughFrames { public: static constexpr uint32_t MAX_SLOTS = 4; bool InitializeBackend(DeviceResources& resources, ID3D11Texture2D* input, - ID3D11Texture2D* output, uint32_t slotCount) noexcept; + ID3D11Texture2D* output, uint32_t slotCount, bool hdrEnabled = false, + const HdrTransformParameters& hdrParameters = {}, + const HdrFrameMetadata& frameMetadata = {}) noexcept; bool OpenFrontend(DeviceResources& resources, uint32_t slotCount) noexcept; void UpdateBackend(uint64_t captureFrameId, bool newCapture) noexcept; + void UpdateBackend(const HdrFrame& frame, bool newCapture) noexcept; void Publish(uint32_t slot, bool generatedFrame) noexcept; bool Consume(uint32_t slot) noexcept; void OnPresented() noexcept; @@ -32,6 +36,9 @@ class PassThroughFrames { : (_baseValid ? _base.get() : nullptr); } uint64_t PresentedCaptureFrameId() const noexcept { return _presentedFrameId; } + const HdrFrameMetadata& PresentedFrameMetadata() const noexcept { + return _presentedMetadata; + } private: struct Slot { @@ -45,6 +52,7 @@ class PassThroughFrames { std::array _frontendSlots; std::array _handles{}; std::array _frameIds{}; + std::array _metadata{}; std::array _valid{}; // Reconfiguration is frontend-blocked. Failure disables future reference // transactions atomically, retaining resources until in-flight work is done. @@ -54,12 +62,17 @@ class PassThroughFrames { winrt::com_ptr _previous; winrt::com_ptr _inputView; winrt::com_ptr _outputView; + winrt::com_ptr _constants; winrt::com_ptr _shader; winrt::com_ptr _sampler; + bool _hdrEnabled = false; + HdrTransformParameters _hdrParameters{}; uint32_t _width = 0; uint32_t _height = 0; uint64_t _currentFrameId = 0; uint64_t _previousFrameId = 0; + HdrFrameMetadata _currentMetadata{}; + HdrFrameMetadata _previousMetadata{}; bool _currentValid = false; bool _previousValid = false; winrt::com_ptr _base; @@ -68,6 +81,7 @@ class PassThroughFrames { bool _presentedValid = false; uint64_t _baseFrameId = 0; uint64_t _presentedFrameId = 0; + HdrFrameMetadata _presentedMetadata{}; }; } diff --git a/src/Magpie.Core/RTXVideoDenoiser.cpp b/src/Magpie.Core/RTXVideoDenoiser.cpp index aaaa6bd1e..9b2a214ed 100644 --- a/src/Magpie.Core/RTXVideoDenoiser.cpp +++ b/src/Magpie.Core/RTXVideoDenoiser.cpp @@ -2,12 +2,14 @@ #include "RTXVideoDenoiser.h" #include "DeviceResources.h" #include "Logger.h" +#include "ScalingWindow.h" +#include "StrHelper.h" #ifdef MP_ENABLE_RTX_VIDEO_DENOISE #include -#include #include +#include "NvCVImageD3D11Proxy.h" // Required by NVIDIA's MIT-licensed proxy loader. USE_APP_PATH below makes it // load the runtime copied beside Magpie.exe rather than an obsolete system SDK. @@ -17,6 +19,81 @@ namespace Magpie { using NvVFXCudaStreamSynchronizeFn = NvCV_Status(NvVFX_API*)(CUstream); +static std::wstring GetExecutableDirectory() noexcept { + wchar_t modulePath[MAX_PATH]{}; + const DWORD moduleLength = GetModuleFileNameW(nullptr, modulePath, ARRAYSIZE(modulePath)); + if (moduleLength == 0 || moduleLength >= ARRAYSIZE(modulePath)) { + return {}; + } + std::wstring directory(modulePath, moduleLength); + const size_t separator = directory.find_last_of(L"\\/"); + if (separator == std::wstring::npos) { + return {}; + } + directory.resize(separator); + return directory; +} + +static std::wstring GetModulePath(HMODULE module) noexcept { + wchar_t modulePath[MAX_PATH]{}; + const DWORD moduleLength = GetModuleFileNameW(module, modulePath, ARRAYSIZE(modulePath)); + return moduleLength == 0 || moduleLength >= ARRAYSIZE(modulePath) + ? std::wstring{} + : std::wstring(modulePath, moduleLength); +} + +static HMODULE LoadRuntimeModule(const std::wstring& directory, const wchar_t* name) noexcept { + const std::wstring expectedPath = directory + L"\\" + name; + HMODULE module = GetModuleHandleW(name); + if (!module) { + SetLastError(ERROR_SUCCESS); + module = LoadLibraryExW( + expectedPath.c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32); + } + const std::wstring loadedPath = GetModulePath(module); + if (!module) { + const DWORD error = GetLastError(); + Logger::Get().ComError(fmt::format( + "RTX Video LoadLibraryExW failed: {} path={}", + StrHelper::UTF16ToUTF8(expectedPath), error), HRESULT_FROM_WIN32(error)); + return nullptr; + } + Logger::Get().Info(fmt::format( + "RTX Video runtime module: {} handle=0x{:X}", + StrHelper::UTF16ToUTF8(loadedPath), reinterpret_cast(module))); + if (_wcsicmp(loadedPath.c_str(), expectedPath.c_str()) != 0) { + Logger::Get().Error(fmt::format( + "RTX Video runtime module path mismatch: expected={}, loaded={}", + StrHelper::UTF16ToUTF8(expectedPath), StrHelper::UTF16ToUTF8(loadedPath))); + return nullptr; + } + return module; +} + +static HMODULE LoadSystemModule(const wchar_t* name) noexcept { + HMODULE module = GetModuleHandleW(name); + if (!module) { + module = LoadLibraryExW(name, nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + } + const DWORD error = module ? ERROR_SUCCESS : GetLastError(); + Logger::Get().Info(fmt::format( + "RTX Video system module {}: handle=0x{:X} path={} win32={} hr=0x{:08X}", + StrHelper::UTF16ToUTF8(name), reinterpret_cast(module), + StrHelper::UTF16ToUTF8(GetModulePath(module)), error, + static_cast(HRESULT_FROM_WIN32(error)))); + return module; +} + +static void LogExport(HMODULE module, const char* name) noexcept { + const auto address = reinterpret_cast(GetProcAddress(module, name)); + const DWORD error = address ? ERROR_SUCCESS : GetLastError(); + Logger::Get().Info(fmt::format( + "RTX Video export {}: module={} address=0x{:X} win32={} hr=0x{:08X}", + name, StrHelper::UTF16ToUTF8(GetModulePath(module)), address, error, + static_cast(HRESULT_FROM_WIN32(error)))); +} + static bool VFXSucceeded(NvCV_Status status, const char* operation) noexcept { if (status == NVCV_SUCCESS) { return true; @@ -31,12 +108,18 @@ static bool VFXSucceeded(NvCV_Status status, const char* operation) noexcept { struct RTXVideoDenoiser::Impl { NvVFX_Handle effect = nullptr; CUstream stream = nullptr; + ID3D11DeviceContext4* d3dContext = nullptr; NvCVImage inputD3D; NvCVImage outputD3D; NvCVImage inputGPU; NvCVImage outputGPU; + NvCVImage outputCPU; + NvCVImage_PixelFormat nativePixelFormat = NVCV_RGBA; + unsigned outputWidth = 0; + unsigned outputHeight = 0; NvCVImage temporaryGPU; NvVFXCudaStreamSynchronizeFn synchronize = nullptr; + uint64_t drawCount = 0; float inputScale = 1.0f; float outputScale = 1.0f; @@ -51,6 +134,7 @@ struct RTXVideoDenoiser::Impl { // their subsequent C++ destructors no-ops. NvCVImage_Dealloc(&temporaryGPU); NvCVImage_Dealloc(&outputGPU); + NvCVImage_Dealloc(&outputCPU); NvCVImage_Dealloc(&inputGPU); NvCVImage_Dealloc(&outputD3D); NvCVImage_Dealloc(&inputD3D); @@ -66,20 +150,25 @@ RTXVideoDenoiser::RTXVideoDenoiser() = default; RTXVideoDenoiser::~RTXVideoDenoiser() = default; bool RTXVideoDenoiser::Initialize( - DeviceResources&, + DeviceResources& resources, ID3D11Texture2D* input, ID3D11Texture2D* output, - uint32_t qualityLevel + uint32_t qualityLevel, + RtxVideoEffectKind kind ) noexcept { _impl.reset(); _qualityLevel = qualityLevel; + _kind = kind; _initializationError = ScalingError::NoError; - const bool isUpscaleQuality = qualityLevel >= 1 && qualityLevel <= 4; - const bool isDenoiseQuality = qualityLevel >= 8 && qualityLevel <= 11; - const bool isHighBitrateQuality = qualityLevel >= 16 && qualityLevel <= 19; - if (!isUpscaleQuality && !isDenoiseQuality && !isHighBitrateQuality) { - Logger::Get().Error(fmt::format("Invalid RTX Video denoise quality level: {}", qualityLevel)); + const bool isVsr = kind == RtxVideoEffectKind::Vsr; + const bool isDenoise = kind == RtxVideoEffectKind::Denoise; + const bool validVsrQuality = isVsr && qualityLevel >= 1 && qualityLevel <= 4; + const bool validDenoiseQuality = isDenoise && qualityLevel >= 8 && qualityLevel <= 11; + const bool validHighBitrateQuality = isDenoise && qualityLevel >= 16 && qualityLevel <= 19; + if (!validVsrQuality && !validDenoiseQuality && !validHighBitrateQuality) { + Logger::Get().Error(fmt::format("Invalid RTX Video {} quality level: {}", + isVsr ? "VSR" : "Denoise", qualityLevel)); return false; } @@ -87,37 +176,97 @@ bool RTXVideoDenoiser::Initialize( D3D11_TEXTURE2D_DESC outputDesc{}; input->GetDesc(&inputDesc); output->GetDesc(&outputDesc); - if ((isDenoiseQuality && + if ((isDenoise && (inputDesc.Width != outputDesc.Width || inputDesc.Height != outputDesc.Height)) || inputDesc.SampleDesc.Count != 1 || outputDesc.SampleDesc.Count != 1) { Logger::Get().Error("RTX Video received invalid output dimensions or an MSAA texture"); return false; } - // NVIDIA's proxy loader checks this before its first LoadLibrary call. + const std::wstring runtimeDirectory = GetExecutableDirectory(); + if (runtimeDirectory.empty()) { + Logger::Get().Error("RTX Video executable directory lookup failed"); + return false; + } + SetDllDirectoryW(runtimeDirectory.c_str()); + Logger::Get().Info(fmt::format( + "RTX Video runtime directory: {}", + StrHelper::UTF16ToUTF8(runtimeDirectory))); + // Keep the shared initialization order explicit: CUDA driver, NGX runtime, + // image ABI, effect ABI, then the VideoSuperRes implementation. SetEnvironmentVariableW(L"NV_VIDEO_EFFECTS_PATH", L"USE_APP_PATH"); + HMODULE cudaModule = LoadSystemModule(L"nvcuda.dll"); + HMODULE ngxModule = LoadRuntimeModule(runtimeDirectory, L"nvngxruntime.dll"); + HMODULE cvModule = LoadRuntimeModule(runtimeDirectory, L"NVCVImage.dll"); + HMODULE vfxModule = LoadRuntimeModule(runtimeDirectory, L"NVVideoEffects.dll"); + HMODULE superResModule = LoadRuntimeModule(runtimeDirectory, L"nvVFXVideoSuperRes.dll"); + if (!cudaModule || !ngxModule || !cvModule || !vfxModule || !superResModule) { + Logger::Get().Error("RTX Video runtime dependency initialization failed"); + return false; + } + LogExport(cvModule, "NvCVImage_Alloc"); + LogExport(vfxModule, "NvVFX_CreateEffect"); + LogExport(vfxModule, "NvVFX_GetVersion"); + unsigned int runtimeVersion = 0; + const NvCV_Status versionStatus = NvVFX_GetVersion(&runtimeVersion); + if (!VFXSucceeded(versionStatus, "NvVFX_GetVersion")) { + return false; + } + Logger::Get().Info(fmt::format( + "RTX Video runtime version: 0x{:08X} ({}.{}.{})", + runtimeVersion, (runtimeVersion >> 24) & 0xFF, (runtimeVersion >> 16) & 0xFF, + (runtimeVersion >> 8) & 0xFF)); auto impl = std::make_unique(); - auto isFloatFormat = [](DXGI_FORMAT format) noexcept { - return format == DXGI_FORMAT_R16G16B16A16_FLOAT || - format == DXGI_FORMAT_R32G32B32A32_FLOAT || - format == DXGI_FORMAT_R11G11B10_FLOAT; - }; - // VideoSuperRes consumes integer RGBA in the 0..255 range. Magpie normally - // renders effects into FP16 textures whose color range is 0..1. - impl->inputScale = isFloatFormat(inputDesc.Format) ? 255.0f : 1.0f; - impl->outputScale = isFloatFormat(outputDesc.Format) ? (1.0f / 255.0f) : 1.0f; + impl->d3dContext = resources.GetD3DDC(); + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) { + // The HDR boundary uses an explicit normalized-D3D/U8 bridge. + impl->inputScale = 1.0f / 255.0f; + impl->outputScale = 255.0f; + } else { + const auto isFloatFormat = [](DXGI_FORMAT format) noexcept { + return format == DXGI_FORMAT_R16G16B16A16_FLOAT || + format == DXGI_FORMAT_R32G32B32A32_FLOAT || + format == DXGI_FORMAT_R11G11B10_FLOAT; + }; + impl->inputScale = isFloatFormat(inputDesc.Format) ? 255.0f : 1.0f; + impl->outputScale = isFloatFormat(outputDesc.Format) ? (1.0f / 255.0f) : 1.0f; + } + // Keep the native model in an explicit RGBA order. D3D11's endpoint is + // BGRA8, so the boundary performs the channel swizzle exactly once. + const NvCVImage_PixelFormat pixelFormat = NVCV_RGBA; + impl->nativePixelFormat = pixelFormat; + impl->outputWidth = outputDesc.Width; + impl->outputHeight = outputDesc.Height; + Logger::Get().Info(fmt::format( + "RTX Video endpoint channel order: {}", + pixelFormat == NVCV_BGRA ? "BGRA" : "RGBA")); if (!VFXSucceeded(NvVFX_CudaStreamCreate(&impl->stream), "NvVFX_CudaStreamCreate") || !VFXSucceeded(NvCVImage_InitFromD3D11Texture(&impl->inputD3D, input), "NvCVImage_InitFromD3D11Texture(input)") || !VFXSucceeded(NvCVImage_InitFromD3D11Texture(&impl->outputD3D, output), "NvCVImage_InitFromD3D11Texture(output)") || !VFXSucceeded(NvCVImage_Alloc(&impl->inputGPU, inputDesc.Width, inputDesc.Height, - NVCV_RGBA, NVCV_U8, NVCV_INTERLEAVED, NVCV_GPU, 32), "NvCVImage_Alloc(input)") || + pixelFormat, NVCV_U8, NVCV_INTERLEAVED, NVCV_GPU, 32), "NvCVImage_Alloc(input)") || !VFXSucceeded(NvCVImage_Alloc(&impl->outputGPU, outputDesc.Width, outputDesc.Height, - NVCV_RGBA, NVCV_U8, NVCV_INTERLEAVED, NVCV_GPU, 32), "NvCVImage_Alloc(output)")) { + pixelFormat, NVCV_U8, NVCV_INTERLEAVED, NVCV_GPU, 32), "NvCVImage_Alloc(output)")) { return false; } + Logger::Get().Info(fmt::format( + "RTX Video NvCV formats: inputD3D={} inputGPU={} outputD3D={} outputGPU={} " + "types={}/{}/{}/{} layouts={}/{}/{}/{}", + static_cast(impl->inputD3D.pixelFormat), + static_cast(impl->inputGPU.pixelFormat), + static_cast(impl->outputD3D.pixelFormat), + static_cast(impl->outputGPU.pixelFormat), + static_cast(impl->inputD3D.componentType), + static_cast(impl->inputGPU.componentType), + static_cast(impl->outputD3D.componentType), + static_cast(impl->outputGPU.componentType), + static_cast(impl->inputD3D.planar), + static_cast(impl->inputGPU.planar), + static_cast(impl->outputD3D.planar), + static_cast(impl->outputGPU.planar))); const NvCV_Status createEffectStatus = NvVFX_CreateEffect("VideoSuperRes", &impl->effect); @@ -151,8 +300,8 @@ bool RTXVideoDenoiser::Initialize( } Logger::Get().Info(fmt::format( - "RTX Video denoise initialized: quality={}, {}x{}, inputFormat={}, outputFormat={}, scales={}/{}", - qualityLevel, inputDesc.Width, inputDesc.Height, + "RTX Video {} initialized: quality={}, {}x{}, inputFormat={}, outputFormat={}, scales={}/{} staging=CPU-U8", + isVsr ? "VSR" : "Denoise", qualityLevel, inputDesc.Width, inputDesc.Height, (int)inputDesc.Format, (int)outputDesc.Format, impl->inputScale, impl->outputScale)); _impl = std::move(impl); return true; @@ -163,7 +312,7 @@ bool RTXVideoDenoiser::Resize( ID3D11Texture2D* input, ID3D11Texture2D* output ) noexcept { - return Initialize(deviceResources, input, output, _qualityLevel); + return Initialize(deviceResources, input, output, _qualityLevel, _kind); } bool RTXVideoDenoiser::Draw(const NativeEffectDrawContext& drawContext) noexcept { @@ -175,15 +324,11 @@ bool RTXVideoDenoiser::Draw(const NativeEffectDrawContext& drawContext) noexcept // Resize should have recreated the wrappers whenever these resources change. (void)input; - (void)output; + D3D11_TEXTURE2D_DESC outputDesc{}; + output->GetDesc(&outputDesc); bool inputMapped = false; - bool outputMapped = false; auto unmapResources = [&]() noexcept { - if (outputMapped) { - NvCVImage_UnmapResource(&_impl->outputD3D, _impl->stream); - outputMapped = false; - } if (inputMapped) { NvCVImage_UnmapResource(&_impl->inputD3D, _impl->stream); inputMapped = false; @@ -211,24 +356,91 @@ bool RTXVideoDenoiser::Draw(const NativeEffectDrawContext& drawContext) noexcept return false; } - if (!VFXSucceeded(NvCVImage_MapResource(&_impl->outputD3D, _impl->stream), - "NvCVImage_MapResource(output)")) { + // HDR compatibility owns the explicit CPU-U8 bridge below. With HDR off, + // preserve the original native D3D11/CUDA interop path byte-for-byte in + // behavior: the effect writes directly back to its D3D11 endpoint. + if (!_hdrBoundary.hdrEnabled) { + bool outputMapped = false; + if (!VFXSucceeded(NvCVImage_MapResource(&_impl->outputD3D, _impl->stream), + "NvCVImage_MapResource(output)")) { + return false; + } + outputMapped = true; + if (!VFXSucceeded(NvCVImage_Transfer(&_impl->outputGPU, &_impl->outputD3D, + _impl->outputScale, _impl->stream, &_impl->temporaryGPU), + "NvCVImage_Transfer(output)")) { + NvCVImage_UnmapResource(&_impl->outputD3D, _impl->stream); + return false; + } + if (!VFXSucceeded(NvCVImage_UnmapResource(&_impl->outputD3D, _impl->stream), + "NvCVImage_UnmapResource(output)")) { + outputMapped = false; + return false; + } + outputMapped = false; + if (!VFXSucceeded(_impl->synchronize(_impl->stream), + "NvVFX_CudaStreamSynchronize")) { + return false; + } + ++_impl->drawCount; + if (_impl->drawCount <= 2) { + Logger::Get().Info(fmt::format( + "RTX Video native Draw succeeded: mode=SDR-direct frame={} output={}x{} format={}", + _impl->drawCount, outputDesc.Width, outputDesc.Height, + static_cast(outputDesc.Format))); + } + return true; + } + + // The SDK accepts the D3D11 input interop path, while this runtime rejects + // direct GPU-U8 -> D3D11-U8 output transfer with NVCV_ERR_PIXELFORMAT. + // Transfer the native GPU result to a CPU U8 image, then upload the exact + // interleaved bytes into the effect-local D3D11 endpoint. + if (!_impl->outputCPU.pixels && !VFXSucceeded(NvCVImage_Alloc( + &_impl->outputCPU, _impl->outputWidth, _impl->outputHeight, + _impl->nativePixelFormat, NVCV_U8, NVCV_INTERLEAVED, NVCV_CPU, 32), + "NvCVImage_Alloc(outputCPU)")) { return false; } - outputMapped = true; - if (!VFXSucceeded(NvCVImage_Transfer(&_impl->outputGPU, &_impl->outputD3D, - _impl->outputScale, _impl->stream, &_impl->temporaryGPU), "NvCVImage_Transfer(output)")) { + if (!VFXSucceeded(NvCVImage_Transfer(&_impl->outputGPU, &_impl->outputCPU, + _impl->outputScale, _impl->stream, &_impl->temporaryGPU), "NvCVImage_Transfer(outputCPU)")) { unmapResources(); return false; } - if (!VFXSucceeded(NvCVImage_UnmapResource(&_impl->outputD3D, _impl->stream), - "NvCVImage_UnmapResource(output)")) { - outputMapped = false; + if (!VFXSucceeded(_impl->synchronize(_impl->stream), "NvVFX_CudaStreamSynchronize(outputCPU)")) { return false; } - outputMapped = false; - - return VFXSucceeded(_impl->synchronize(_impl->stream), "NvVFX_CudaStreamSynchronize"); + if (!_impl->d3dContext || !_impl->outputCPU.pixels || _impl->outputCPU.pitch <= 0) { + Logger::Get().Error("RTX Video CPU output staging is unavailable"); + return false; + } + std::vector bgra( + size_t(_impl->outputCPU.pitch) * outputDesc.Height); + const auto* rgba = static_cast(_impl->outputCPU.pixels); + for (UINT y = 0; y < outputDesc.Height; ++y) { + const auto* src = rgba + size_t(y) * _impl->outputCPU.pitch; + auto* dst = bgra.data() + size_t(y) * _impl->outputCPU.pitch; + for (UINT x = 0; x < outputDesc.Width; ++x) { + dst[x * 4 + 0] = src[x * 4 + 2]; + dst[x * 4 + 1] = src[x * 4 + 1]; + dst[x * 4 + 2] = src[x * 4 + 0]; + dst[x * 4 + 3] = src[x * 4 + 3]; + } + } + _impl->d3dContext->UpdateSubresource( + output, 0, nullptr, bgra.data(), + static_cast(_impl->outputCPU.pitch), 0); + { + ++_impl->drawCount; + if (_impl->drawCount <= 2) { + Logger::Get().Info(fmt::format( + "RTX Video native Draw succeeded: frame={} outputProtocol=RGBA/BGRA U8 [0,255] " + "canonicalScale={}/{} output={}x{} format={}", + _impl->drawCount, _impl->inputScale, _impl->outputScale, + outputDesc.Width, outputDesc.Height, static_cast(outputDesc.Format))); + } + } + return true; } } @@ -243,7 +455,7 @@ RTXVideoDenoiser::RTXVideoDenoiser() = default; RTXVideoDenoiser::~RTXVideoDenoiser() = default; bool RTXVideoDenoiser::Initialize( - DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*, uint32_t) noexcept { + DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*, uint32_t, RtxVideoEffectKind) noexcept { Logger::Get().Error("RTX Video denoise is disabled at build time"); return false; } diff --git a/src/Magpie.Core/RTXVideoDenoiser.h b/src/Magpie.Core/RTXVideoDenoiser.h index 70549123f..04289b865 100644 --- a/src/Magpie.Core/RTXVideoDenoiser.h +++ b/src/Magpie.Core/RTXVideoDenoiser.h @@ -6,6 +6,8 @@ namespace Magpie { class DeviceResources; +enum class RtxVideoEffectKind : uint8_t { Vsr, Denoise }; + // NVIDIA VideoSuperRes modes 8-11 perform same-resolution denoising. The // native backend uses D3D11/CUDA interop, so no frame is copied through CPU. class RTXVideoDenoiser final : public NativeEffectBackend { @@ -19,7 +21,8 @@ class RTXVideoDenoiser final : public NativeEffectBackend { DeviceResources& deviceResources, ID3D11Texture2D* input, ID3D11Texture2D* output, - uint32_t qualityLevel + uint32_t qualityLevel, + RtxVideoEffectKind kind ) noexcept; bool Resize( @@ -33,9 +36,9 @@ class RTXVideoDenoiser final : public NativeEffectBackend { private: struct Impl; - // 仅在 MP_ENABLE_RTX_VIDEO_DENOISE 构建中使用;无 SDK 的 CI 构建里 ClangCL -Werror 会报未使用 [[maybe_unused]] std::unique_ptr _impl; [[maybe_unused]] uint32_t _qualityLevel = 8; + RtxVideoEffectKind _kind = RtxVideoEffectKind::Vsr; ScalingError _initializationError = ScalingError::NoError; }; diff --git a/src/Magpie.Core/Renderer.cpp b/src/Magpie.Core/Renderer.cpp index 4ed46e193..47f76acf1 100644 --- a/src/Magpie.Core/Renderer.cpp +++ b/src/Magpie.Core/Renderer.cpp @@ -9,7 +9,11 @@ #include "DirectXHelper.h" #include "DwmSharedSurfaceFrameSource.h" #include "EffectCompiler.h" +#include "EffectHelper.h" #include "EffectDrawer.h" +#include "GroupAHdrRoutes.h" +#include "GroupBHdrRoutes.h" +#include "EffectProtocolCatalogC.h" #include "EffectParameterValue.h" #include "EffectParameterRestart.h" #include "EffectsProfiler.h" @@ -26,6 +30,9 @@ #include "TextureHelper.h" #include "Win32Helper.h" #include "DLSSFrameGenerator.h" +#include "DLSSSRUpscaler.h" +#include "FSR2Upscaler.h" +#include "FSR3Upscaler.h" #include "NativeEffectBackend.h" #include "NativeEffectBackendFactory.h" #include "NvidiaOpticalFlowProvider.h" @@ -107,6 +114,78 @@ static bool IsFrameGenerationEffect(std::string_view name) noexcept { return IsDLSSFrameGenerationEffect(name) || IsXeSSFrameGenerationEffect(name); } +static HdrFormatRoutes GetHdrRoutesForEffect( + const EffectOption& effect, + bool hdrEnabled +) noexcept { + // Route selection is a HDR-only contract. Keep this guard local so a + // caller cannot accidentally turn saved HDR parameters into a live route + // while the profile option is disabled. + if (!hdrEnabled) { + return {}; + } + const size_t separator = effect.name.find('\\'); + const std::string_view group = separator == std::string::npos + ? std::string_view(effect.name) + : std::string_view(effect.name).substr(0, separator); + int casFormatOption = 0; + if (group == "Anime4K" || group == "CAS" || group == "CRT" || + group == "CuNNy" || group == "CuNNy2" || group == "Diagnostics" || + group == "FSRCNNX" || group == "FXAA" || group == "MLAA") { + if (const auto it = effect.parameters.find("hdrFormat"); it != effect.parameters.end()) { + casFormatOption = static_cast(std::lround(it->second)); + } + return GetGroupAHdrRoutes(group, casFormatOption); + } + if (group == "DLSSNR") { + const auto path = effect.parameters.find("experimentalHdrPath"); + return GetGroupBHdrRoutes(group, + path != effect.parameters.end() && path->second >= 0.5f); + } + if (group == "DLSS" || group == "FSR" || group == "FSR2" || + group == "FSR3" || group == "FSR4" || group == "NIS") { + return GetGroupBHdrRoutes(group); + } + if (group == "RTXVideo") { + return effect.name.find("_VSR_") != std::string::npos + ? EffectProtocolC::RTXVideoVsr() + : EffectProtocolC::RTXVideoDenoiser(); + } + if (group == "DLSSFG" || group == "FSR3FG") { + return EffectProtocolC::GetGroupCHdrRoutes(group); + } + return EffectProtocolC::GetGroupCHdrRoutes(group); +} + +static void ConfigureHdrBackendProtocol( + std::string_view effectName, + NativeEffectBackend& backend, + const HdrFrameMetadata& metadata, + bool hdrEnabled +) noexcept { + if (!hdrEnabled || !metadata.IsValid()) return; + // Capture normalization has already applied source pre-exposure into the + // canonical FP16 surface. Native SR backends therefore consume linear HDR + // values with a neutral exposure contract for this frame. + const FsrHdrProtocol protocol{ + .hdrColorInput = true, + .transfer = GroupBTransfer::Linear, + .preExposure = 1.0f, + .exposure = 1.0f, + .depthInverted = true, + .depthInfinite = true, + .useReactiveMask = true, + .useTransparencyMask = true, + }; + if (effectName == "DLSS\\DLSS_SR") { + static_cast(backend).SetDlssHdrProtocol(protocol); + } else if (effectName == "FSR2\\FSR2_SR") { + static_cast(backend).SetFsrHdrProtocol(protocol); + } else if (effectName == "FSR3\\FSR3_SR" || effectName == "FSR4\\FSR4_SR") { + static_cast(backend).SetFsrHdrProtocol(protocol); + } +} + static MotionVectorRequest GetMotionVectorRequest( const FrameGuidanceRequirements& requirements ) noexcept { @@ -416,8 +495,6 @@ ScalingError Renderer::Initialize(HWND hwndAttach, OverlayOptions& overlayOption void Renderer::OnCursorVisibilityChanged(bool isVisible, bool onDestory) { _backendThreadDispatcher.TryEnqueue([this, isVisible, onDestory]() { - // Still deliver cursor teardown, but don't reopen WGC for earlier input - // notifications once the frontend has begun stopping the session. if (_frameSource && (onDestory || !_sessionLifetime->IsStopping())) { _frameSource->OnCursorVisibilityChanged(isVisible, onDestory); // Apply capture continuity resets on the first valid frame, not on a @@ -505,6 +582,8 @@ bool Renderer::_OpenFrontendSharedTextures() noexcept { _frontendMotionReset = true; _frontendBaseValid = false; _frontendPresentedBaseValid = false; + _frontendFrameMetadata = {}; + _frontendPresentedFrameMetadata = {}; _frontendBaseNeedsPresent = false; for (uint32_t i = 0; i < MAX_SHARED_TEXTURE_SLOTS; ++i) { _frontendSharedTextureMutexes[i] = nullptr; @@ -613,6 +692,25 @@ bool Renderer::_UpdateFrontendBase(uint32_t sharedTextureSlot) noexcept { if (FAILED(hr)) { return false; } + const uint64_t slotSequence = + _sharedTextureCaptureSequences[sharedTextureSlot].load(std::memory_order_acquire); + const uint64_t activeSequence = + _activeCaptureSequence.load(std::memory_order_acquire); + const uint64_t slotGeneration = _sharedTextureResourceGenerations[sharedTextureSlot].load( + std::memory_order_acquire); + const uint64_t activeGeneration = _activeResourceGeneration.load(std::memory_order_acquire); + if ((slotSequence != 0 && activeSequence != 0 && slotSequence != activeSequence) || + (slotGeneration != 0 && activeGeneration != 0 && slotGeneration != activeGeneration)) { + const HRESULT staleRelease = ReleasePresentationTextures(mutexes, releaseKey); + if (SUCCEEDED(staleRelease)) { + _sharedTextureMutexKeys[sharedTextureSlot].store(releaseKey, std::memory_order_release); + _lastAccessMutexKeys[sharedTextureSlot] = releaseKey; + } + Logger::Get().Info(fmt::format( + "Dropped stale frontend slot={} sequence={}/{} resourceGeneration={}/{}", + sharedTextureSlot, slotSequence, activeSequence, slotGeneration, activeGeneration)); + return false; + } D3D11_TEXTURE2D_DESC sourceDesc{}; source->GetDesc(&sourceDesc); @@ -642,7 +740,8 @@ bool Renderer::_UpdateFrontendBase(uint32_t sharedTextureSlot) noexcept { } if (SUCCEEDED(hr)) { _frontendResources.GetD3DDC()->CopyResource(_frontendBaseTexture.get(), source); - _frontendCaptureFrameId = _sharedMotionFrameIds[sharedTextureSlot].load(std::memory_order_acquire); + _frontendCaptureFrameId = _sharedTextureFrameIds[sharedTextureSlot].load(std::memory_order_acquire); + _frontendFrameMetadata = _sharedFrameMetadata[sharedTextureSlot]; FrameTrace::SetFrame(_frontendCaptureFrameId); traceBase.FrameId(_frontendCaptureFrameId); if (!_passThroughFrames.Consume(sharedTextureSlot) && _isPassThroughActive) { @@ -891,6 +990,9 @@ bool Renderer::_SubmitFrontendFrame() noexcept { d3dDC->CopyResource( _frontendPresentedBaseTexture.get(), _frontendBaseTexture.get()); _frontendPresentedBaseValid = true; + _frontendPresentedFrameMetadata = _frontendFrameMetadata; + _frontendPresentedFrameMetadata = _frontendFrameMetadata; + _frontendPresentedFrameMetadata.stage = HdrFrameStage::PresentedOutput; _passThroughFrames.OnPresented(); } if (!stableBaseOnly) _frontendBaseNeedsPresent = false; @@ -1299,12 +1401,6 @@ void Renderer::OnMove() noexcept { _UpdateDestRect(); } -void Renderer::RestoreOverlayState(const OverlaySessionState& state) noexcept { - ++_overlayActionRevision; - _overlayDrawer.RestoreSessionState(state); - Render(); -} - void Renderer::InvokeOverlayAction(OverlayAction action) noexcept { const ScalingWindow& window = ScalingWindow::Get(); if (action == OverlayAction::Screenshot) { @@ -1326,6 +1422,12 @@ void Renderer::InvokeOverlayAction(OverlayAction action) noexcept { } } +void Renderer::RestoreOverlayState(const OverlaySessionState& state) noexcept { + ++_overlayActionRevision; + _overlayDrawer.RestoreSessionState(state); + Render(); +} + bool Renderer::SetPassThroughActive(bool value) noexcept { if (value && !_passThroughFrames.FrontendTexture(true)) { const auto& window = ScalingWindow::Get(); @@ -1418,10 +1520,16 @@ bool Renderer::_InitFrameSource() noexcept { _backendInitError = ScalingError::CaptureFailed; return false; } + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && + !_hdrPresentationAdapter.Initialize(_backendResources, _backendDescriptorStore)) { + Logger::Get().Error("初始化 HDR 发布适配器失败"); + _backendInitError = ScalingError::GraphicsDeviceInitFailed; + return false; + } // 由于 DPI 缩放,捕获尺寸和边界矩形尺寸不一定相同 D3D11_TEXTURE2D_DESC desc; - _frameSource->GetOutput()->GetDesc(&desc); + _frameSource->GetPipelineTexture()->GetDesc(&desc); Logger::Get().Info(fmt::format("捕获尺寸: {}x{}", desc.Width, desc.Height)); return true; @@ -1430,7 +1538,9 @@ bool Renderer::_InitFrameSource() noexcept { static std::optional CompileEffect( const EffectOption& effectOption, bool noFP16, - bool forceInlineParams = false + bool forceInlineParams = false, + DXGI_FORMAT routeInputFormat = DXGI_FORMAT_UNKNOWN, + DXGI_FORMAT routeOutputFormat = DXGI_FORMAT_UNKNOWN ) noexcept { // 指定效果名 EffectDesc result{ .name = effectOption.name }; @@ -1452,6 +1562,19 @@ static std::optional CompileEffect( if (noFP16) { compileFlag |= EffectCompilerFlags::NoFP16; } + if (scalingOptions.IsHdrCompatibilityEnabled()) { + compileFlag |= EffectCompilerFlags::HdrCompatibility; + } + const auto encodeFormat = [](DXGI_FORMAT format, uint32_t shift) { + if (format == DXGI_FORMAT_UNKNOWN) return uint32_t(0); + for (uint32_t i = 0; i < std::size(EffectHelper::FORMAT_DESCS) - 1; ++i) { + if (EffectHelper::FORMAT_DESCS[i].dxgiFormat == format) + return ((i + 1) & EffectCompilerFlags::SurfaceFormatMask) << shift; + } + return uint32_t(0); + }; + compileFlag |= encodeFormat(routeInputFormat, EffectCompilerFlags::InputFormatShift); + compileFlag |= encodeFormat(routeOutputFormat, EffectCompilerFlags::OutputFormatShift); bool success = true; uint32_t duration = Measure([&]() { @@ -1486,7 +1609,18 @@ ID3D11Texture2D* Renderer::_BuildEffects() noexcept { int duration = Measure([&]() { Win32Helper::RunParallel([&](uint32_t id) { - std::optional desc = CompileEffect(effects[id], noFP16); + DXGI_FORMAT routeInput = DXGI_FORMAT_UNKNOWN; + DXGI_FORMAT routeOutput = DXGI_FORMAT_UNKNOWN; + if (options.IsHdrCompatibilityEnabled()) { + const HdrFormatRoutes routes = GetHdrRoutesForEffect( + effects[id], options.IsHdrCompatibilityEnabled()); + if (!routes.empty() && routes.front().inputFormat != DXGI_FORMAT_UNKNOWN) { + routeInput = routes.front().inputFormat; + routeOutput = routes.front().outputFormat; + } + } + std::optional desc = CompileEffect( + effects[id], noFP16, false, routeInput, routeOutput); auto lk = writeLock.lock_exclusive(); if (desc) { @@ -1515,8 +1649,28 @@ ID3D11Texture2D* Renderer::_BuildEffects() noexcept { _dlssFgRecoveryAttempts = 0; std::optional dlssFrameGenerationSettings; - ID3D11Texture2D* inOutTexture = _frameSource->GetOutput(); + ID3D11Texture2D* inOutTexture = _frameSource->GetPipelineTexture(); + if (!_frameSource->PrepareHdrOutputForResize()) { + Logger::Get().Error("准备 HDR 输出尺寸失败"); + return nullptr; + } + inOutTexture = _frameSource->GetPipelineTexture(); + HdrFrame initialHdrFrame{}; + if (options.IsHdrCompatibilityEnabled()) { + initialHdrFrame.texture = _frameSource->GetPipelineTexture(); + initialHdrFrame.metadata = _frameSource->GetHdrFrameMetadata(); + initialHdrFrame.workingFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + } for (uint32_t i = 0; i < effectCount; ++i) { + HdrEffectBoundaryContext initialHdrBoundary{}; + if (options.IsHdrCompatibilityEnabled()) { + const EffectOption& effectOption = effects[i]; + initialHdrBoundary = HdrEffectBoundary::Prepare( + true, initialHdrFrame, + GetHdrRoutesForEffect(effectOption, options.IsHdrCompatibilityEnabled()), + initialHdrFrame.metadata.color); + _effectDrawers[i].SetHdrBoundary(initialHdrBoundary); + } if (!_effectDrawers[i].Initialize( _effectDescs[i], effects[i], @@ -1544,6 +1698,11 @@ ID3D11Texture2D* Renderer::_BuildEffects() noexcept { return nullptr; } _nativeEffectBackends[i] = std::move(nativeBackend.backend); + if (_nativeEffectBackends[i] && options.IsHdrCompatibilityEnabled()) { + _nativeEffectBackends[i]->SetHdrBoundary(std::move(initialHdrBoundary)); + ConfigureHdrBackendProtocol(effects[i].name, *_nativeEffectBackends[i], + initialHdrFrame.metadata, true); + } if (effects[i].name == "DLSSNR\\DLSSNR_AI_Filter" && !_nativeEffectBackends[i] && options.reportErrorDetails) { options.reportErrorDetails(ScalingWindow::Get().SrcTracker().Handle(), @@ -1592,6 +1751,7 @@ ID3D11Texture2D* Renderer::_BuildEffects() noexcept { } _UpdateActiveEffectDescs(); + _UpdateHdrEffectBoundaryContexts(); // 初始化所有效果共用的动态常量缓冲区 for (const EffectDesc& effectDesc : _effectDescs) { @@ -1616,9 +1776,67 @@ ID3D11Texture2D* Renderer::_BuildEffects() noexcept { return inOutTexture; } +void Renderer::_UpdateHdrEffectBoundaryContexts() noexcept { + const ScalingOptions& options = ScalingWindow::Get().Options(); + if (!options.IsHdrCompatibilityEnabled()) { + for (size_t i = 0; i < _effectDrawers.size(); ++i) { + _effectDrawers[i].SetHdrBoundary({}); + if (i < _nativeEffectBackends.size() && _nativeEffectBackends[i]) { + _nativeEffectBackends[i]->SetHdrBoundary({}); + } + } + return; + } + if (!_frameSource) { + return; + } + + HdrFrame inputFrame{}; + inputFrame.texture = _frameSource->GetPipelineTexture(); + inputFrame.metadata = _frameSource->GetHdrFrameMetadata(); + inputFrame.workingFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + + for (size_t i = 0; i < _effectDrawers.size(); ++i) { + const EffectOption& effectOption = _runtimeEffectOptions[i]; + const HdrFormatRoutes routes = GetHdrRoutesForEffect(effectOption, options.IsHdrCompatibilityEnabled()); + HdrEffectBoundaryContext context = HdrEffectBoundary::Prepare( + true, inputFrame, routes, inputFrame.metadata.color); + if (effectOption.name == "DLSSNR\\DLSSNR_AI_Filter") { + D3D11_TEXTURE2D_DESC sourceDesc{}; + inputFrame.texture->GetDesc(&sourceDesc); + const HdrTransformParameters transform = HdrColorTransform::ForFrame( + inputFrame.metadata.color); + const HdrColorTransform::ShoulderCurve boundedCurve = + HdrColorTransform::BuildShoulderCurve( + transform, HdrColorTransform::BoundedRouteHighlightTarget); + Logger::Get().Info(fmt::format( + "DLSSNR HDR boundary: enabled={} route={} profile={} requiresBounded={} " + "normalizationScale={} sourceFormat={} source={}x{} " + "sdrWhite={:.1f} peak={:.1f} curvePeak={:.3f} curveTarget={:.3f} " + "curveK={:.4f} curveTailSlope={:.4f} colorInferred={}", + options.IsHdrCompatibilityEnabled(), + context.SelectedRoute() ? context.SelectedRoute()->Id() : "(none)", + ToString(context.plan.profile), context.plan.requiresBoundedMapping, + context.plan.normalizationScale, static_cast(sourceDesc.Format), + sourceDesc.Width, sourceDesc.Height, + transform.sdrWhiteNits, transform.hdrPeakNits, + boundedCurve.peak, boundedCurve.target, boundedCurve.k, + boundedCurve.tailSlope, + inputFrame.metadata.color.isInferred)); + } + _effectDrawers[i].SetHdrBoundary(context); + if (i < _nativeEffectBackends.size() && _nativeEffectBackends[i]) { + _nativeEffectBackends[i]->SetHdrBoundary(std::move(context)); + ConfigureHdrBackendProtocol(_runtimeEffectOptions[i].name, + *_nativeEffectBackends[i], inputFrame.metadata, true); + } + } +} + void Renderer::_BuildEffectParameterRuntimeInfos() noexcept { _effectParameterRuntimeInfos.clear(); _effectParameterRuntimeInfos.resize(_effectDescs.size()); + const bool hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); for (uint32_t effectIdx = 0; effectIdx < _effectDescs.size(); ++effectIdx) { const EffectDesc& desc = _effectDescs[effectIdx]; @@ -1629,7 +1847,18 @@ void Renderer::_BuildEffectParameterRuntimeInfos() noexcept { for (const EffectParameterDesc& parameter : desc.params) { EffectParameterRuntimeInfo info{ .name = parameter.name }; - if (IsFrameRateFilterEffect(option.name)) { + const bool isHdrOnlyParameter = + (((option.name == "CAS\\CAS" || option.name == "CAS\\CAS_Scaling") && + parameter.name == "hdrFormat") || + (option.name == "DLSSNR\\DLSSNR_AI_Filter" && + parameter.name == "experimentalHdrPath")); + if (!hdrEnabled && isHdrOnlyParameter) { + info.applyMode = EffectParameterApplyMode::Unavailable; + info.restartReason = EffectParameterRestartReason::None; + } else if (isHdrOnlyParameter) { + info.applyMode = EffectParameterApplyMode::RestartRequired; + info.restartReason = EffectParameterRestartReason::ResourceRecreation; + } else if (IsFrameRateFilterEffect(option.name)) { info.applyMode = EffectParameterApplyMode::Live; info.restartReason = EffectParameterRestartReason::None; } else if (IsDLSSFrameGenerationEffect(option.name)) { @@ -1652,8 +1881,8 @@ void Renderer::_BuildEffectParameterRuntimeInfos() noexcept { info.applyMode = EffectParameterApplyMode::Live; info.restartReason = EffectParameterRestartReason::None; } - // Honor the running backend's live support. DLSSNR resets its history - // for core/upstream edits without tearing down the effect group. + // The running backend owns live support and history reset semantics. + // DLSSNR observes inputRevision without tearing down the effect group. infos.push_back(std::move(info)); } } @@ -1665,6 +1894,7 @@ bool Renderer::QueueEffectParameterUpdate( float value, bool waitForOverlaySave ) noexcept { + (void)waitForOverlaySave; if (!std::isfinite(value) || !_backendThreadDispatcher || effectIdx >= _effectParameterRuntimeInfos.size() || parameterIdx >= _effectParameterRuntimeInfos[effectIdx].size()) { @@ -1675,11 +1905,6 @@ bool Renderer::QueueEffectParameterUpdate( if (info.applyMode != EffectParameterApplyMode::Live) { return false; } - if (info.automaticRestart) { - // Keep the old NR instance and its upstream inputs unchanged until teardown. - return ScalingWindow::Get().QueueEffectParameterRestart(effectIdx, parameterIdx, value, waitForOverlaySave); - } - const uint64_t key = (uint64_t(effectIdx) << 32) | parameterIdx; bool enqueueWake = false; { @@ -1765,7 +1990,7 @@ void Renderer::_ApplyPendingEffectParameters() noexcept { const EffectParameterRuntimeInfo& info = _effectParameterRuntimeInfos[effectIdx][update.parameterIdx]; if (info.name != parameter.name || - info.applyMode != EffectParameterApplyMode::Live || info.automaticRestart || + info.applyMode != EffectParameterApplyMode::Live || !std::isfinite(update.value)) { valid = false; break; @@ -1906,10 +2131,15 @@ ID3D11Texture2D* Renderer::_ResizeEffects() noexcept { return nullptr; } - ID3D11Texture2D* inOutTexture = _frameSource->GetOutput(); + ID3D11Texture2D* inOutTexture = _frameSource->GetPipelineTexture(); D3D11_TEXTURE2D_DESC sourceDesc{}; + if (!_frameSource->PrepareHdrOutputForResize()) { + Logger::Get().Error("准备 HDR 输出尺寸失败"); + return nullptr; + } + inOutTexture = _frameSource->GetPipelineTexture(); inOutTexture->GetDesc(&sourceDesc); - const FrameGuidanceRequirements guidanceRequirements = + FrameGuidanceRequirements guidanceRequirements = CollectFrameGuidanceRequirements( _nativeEffectBackends, _dlssFrameGenerator.get(), _xessMotionRequest); if (_frameGuidanceService.IsInitialized()) { @@ -2002,6 +2232,7 @@ ID3D11Texture2D* Renderer::_ResizeEffects() noexcept { _effectsProfiler.SetPassCount(_backendResources.GetD3DDevice(), passCount); } } + _UpdateHdrEffectBoundaryContexts(); if (_dlssFrameGenerator) { const DLSSFrameGenerationSettings settings = @@ -2026,7 +2257,7 @@ bool Renderer::_InitializeDLSSFrameGenerator( _dlssFrameGenerator.reset(); } D3D11_TEXTURE2D_DESC sourceDesc{}; - _frameSource->GetOutput()->GetDesc(&sourceDesc); + _frameSource->GetPipelineTexture()->GetDesc(&sourceDesc); auto frameGenerator = std::make_unique(); if (!frameGenerator->Initialize( _backendResources, _ngxD3D12Core, input, @@ -2181,10 +2412,54 @@ void Renderer::_UpdateDestRect() noexcept { HANDLE Renderer::_CreateSharedTexture(ID3D11Texture2D* effectsOutput) noexcept { D3D11_TEXTURE2D_DESC desc; effectsOutput->GetDesc(&desc); + if (_hdrPresentationTexture) { + _backendDescriptorStore.RemoveCache(_hdrPresentationTexture.get()); + _hdrPresentationTexture = nullptr; + } + if (_dlssFgNormalizedInput) { + _backendDescriptorStore.RemoveCache(_dlssFgNormalizedInput.get()); + _dlssFgNormalizedInput = nullptr; + } + if (_dlssFgCanonicalGenerated) { + _backendDescriptorStore.RemoveCache(_dlssFgCanonicalGenerated.get()); + _dlssFgCanonicalGenerated = nullptr; + } + const bool hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); + const bool xessFgHdrTerminal = hdrEnabled && _isXeSSFrameGenerationActive; + const DXGI_FORMAT presentationFormat = hdrEnabled + ? (xessFgHdrTerminal ? DXGI_FORMAT_R10G10B10A2_UNORM : DXGI_FORMAT_R16G16B16A16_FLOAT) + : DXGI_FORMAT_R8G8B8A8_UNORM; + if (hdrEnabled && desc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT) { + Logger::Get().Error("HDR 发布要求 canonical FP16 输出"); + return NULL; + } SIZE textureSize = { (LONG)desc.Width, (LONG)desc.Height }; + _dlssFgHdrNormalizationScale = 1.0f; + if (hdrEnabled && _dlssFrameGenerator && _frameSource) { + const auto color = _frameSource->GetHdrFrameMetadata().color; + if (color.IsValid() && color.sdrWhiteNits > 80.0f) { + _dlssFgHdrNormalizationScale = 80.0f / color.sdrWhiteNits; + _dlssFgNormalizedInput = DirectXHelper::CreateTexture2D( + _backendResources.GetD3DDevice(), DXGI_FORMAT_R16G16B16A16_FLOAT, + textureSize.cx, textureSize.cy, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + _dlssFgCanonicalGenerated = DirectXHelper::CreateTexture2D( + _backendResources.GetD3DDevice(), DXGI_FORMAT_R16G16B16A16_FLOAT, + textureSize.cx, textureSize.cy, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_dlssFgNormalizedInput || !_dlssFgCanonicalGenerated) { + Logger::Get().Error("Create DLSSFG HDR normalization textures failed"); + return NULL; + } + Logger::Get().Info(fmt::format( + "DLSSFG HDR bounded bridge enabled: sdrWhiteNits={:.3f} inputScale={:.6f}", + color.sdrWhiteNits, _dlssFgHdrNormalizationScale)); + } + } _sharedTextureSlotCount = _dlssFrameGenerator ? std::clamp(_dlssFrameGenerator->Multiplier(), 2u, MAX_SHARED_TEXTURE_SLOTS) : 1u; _sharedTextureGeneration.fetch_add(1, std::memory_order_release); + _pendingDLSSFGFrontendFrames.store(0, std::memory_order_release); _nextBackendSharedTextureSlot = 0; _latestSharedTextureSlot.store(0, std::memory_order_relaxed); for (uint32_t i = 0; i < MAX_SHARED_TEXTURE_SLOTS; ++i) { @@ -2199,12 +2474,18 @@ HANDLE Renderer::_CreateSharedTexture(ID3D11Texture2D* effectsOutput) noexcept { _sharedMotionFrameIds[i].store(0, std::memory_order_relaxed); _sharedMotionValid[i].store(false, std::memory_order_relaxed); _sharedMotionReset[i].store(true, std::memory_order_relaxed); + _sharedTextureCaptureSequences[i].store(0, std::memory_order_relaxed); + _sharedTextureFrameIds[i].store(0, std::memory_order_relaxed); + _sharedTextureResourceGenerations[i].store( + _sharedTextureGeneration.load(std::memory_order_relaxed), std::memory_order_relaxed); + _sharedTextureTimestamps[i].store(0, std::memory_order_relaxed); + _sharedFrameMetadata[i] = {}; } for (uint32_t i = 0; i < _sharedTextureSlotCount; ++i) { _backendSharedTextures[i] = DirectXHelper::CreateTexture2D( _backendResources.GetD3DDevice(), - DXGI_FORMAT_R8G8B8A8_UNORM, + presentationFormat, textureSize.cx, textureSize.cy, D3D11_BIND_SHADER_RESOURCE, @@ -2278,8 +2559,21 @@ HANDLE Renderer::_CreateSharedTexture(ID3D11Texture2D* effectsOutput) noexcept { "DLSSFG bounded presentation ring initialized: slots={}", _sharedTextureSlotCount)); } + if (xessFgHdrTerminal) { + _hdrPresentationTexture = DirectXHelper::CreateTexture2D( + _backendResources.GetD3DDevice(), DXGI_FORMAT_R10G10B10A2_UNORM, + textureSize.cx, textureSize.cy, + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS); + if (!_hdrPresentationTexture) { + Logger::Get().Error("Create XeSSFG HDR10 presentation texture failed"); + return NULL; + } + } if (!_passThroughFrames.InitializeBackend(_backendResources, - _frameSource->GetOutput(), effectsOutput, _sharedTextureSlotCount)) { + _frameSource->GetPipelineTexture(), effectsOutput, _sharedTextureSlotCount, + ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(), + HdrColorTransform::ForFrame(_frameSource->GetHdrFrameMetadata().color), + _frameSource->GetHdrFrameMetadata())) { const auto& window = ScalingWindow::Get(); if (const auto& report = window.Options().reportErrorDetails) { report(window.SrcTracker().Handle(), ScalingError::PassThroughUnavailable, @@ -2339,8 +2633,6 @@ void Renderer::_BackendThreadProc() noexcept { return; } if (_sessionLifetime->IsStopping()) { - // Drain queued teardown (including cursor restoration) until WM_QUIT. - // No pacing waits, frame acquisition or rendering after shutdown. if (GetMessage(&msg, NULL, 0, 0) <= 0) { _frameSource.reset(); return; @@ -2381,7 +2673,6 @@ void Renderer::_BackendThreadProc() noexcept { } traceMessages.End(); - // DispatchMessage may have just closed the capture session. if (_sessionLifetime->IsStopping()) continue; if (_pendingFrameGenerationInput) { const auto now = std::chrono::steady_clock::now(); @@ -2389,7 +2680,7 @@ void Renderer::_BackendThreadProc() noexcept { // Do not replace colour/reference/motion before this input enters FG. auto input = std::move(_pendingFrameGenerationInput); _fgInputClock.Submitted(now); - _CompleteBackendFrame(input.get(), true); + _CompleteBackendFrame(input.get(), true, _captureSequence); if (!_dlssFrameGenerator || !_synchronousFramePresentationEnabled.load(std::memory_order_acquire)) { PostMessage(ScalingWindow::Get().Handle(), CommonSharedConstants::WM_FRONTEND_RENDER, 0, 0); } @@ -2416,7 +2707,6 @@ void Renderer::_BackendThreadProc() noexcept { traceCapture.End(); FrameTrace::Mark(FrameTrace::Event::CaptureResult, static_cast(frameSourceState), _frameSource->CaptureTimestamp100ns()); - // An acquisition already in progress may finish while the frontend stops. if (_sessionLifetime->IsStopping()) continue; switch (frameSourceState) { case FrameSourceState::Waiting: @@ -2450,7 +2740,7 @@ void Renderer::_BackendThreadProc() noexcept { _forceNextRender = false; _backendMayDeferFG = true; _BackendRender( - _effectDrawers.back().GetOutputTexture(), + _effectDrawers.back().GetExternalOutputTexture(), frameSourceState == FrameSourceState::NewFrame); _backendMayDeferFG = false; // DLSSFG uses synchronous, individually paced presentation so generated @@ -2578,6 +2868,7 @@ HANDLE Renderer::_InitBackend() noexcept { if (!_InitFrameSource()) { return NULL; } + _activeResourceGeneration.store(_frameSource->ResourceGeneration(), std::memory_order_release); { if (_frameSource->WaitType() == FrameSourceWaitType::NoWait) { // 某些捕获方式不会限制捕获帧率,因此将捕获帧率限制为屏幕刷新率 @@ -2603,10 +2894,13 @@ HANDLE Renderer::_InitBackend() noexcept { return NULL; } - const FrameGuidanceRequirements guidanceRequirements = + FrameGuidanceRequirements guidanceRequirements = CollectFrameGuidanceRequirements( _nativeEffectBackends, _dlssFrameGenerator.get(), _xessMotionRequest); _motionConsumers.clear(); + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && guidanceRequirements.HasMotion()) { + Logger::Get().Info("HDR mode: optical-flow providers receive the canonical frame and perform provider-local format adaptation"); + } for (size_t i = 0; i < _runtimeEffectOptions.size(); ++i) { const auto& effect = _runtimeEffectOptions[i]; const MotionVectorRequest request = IsDLSSFrameGenerationEffect(effect.name) && _dlssFrameGenerator ? @@ -2633,7 +2927,7 @@ HANDLE Renderer::_InitBackend() noexcept { #endif }); if (!_frameGuidanceService.Initialize( - _backendResources, _frameSource->GetOutput(), guidanceRequirements)) { + _backendResources, _frameSource->GetPipelineTexture(), guidanceRequirements)) { const OpticalFlowMethod method = _frameGuidanceService.InitializationFailedMethod(); const OpticalFlowInitializationError error = @@ -2702,6 +2996,9 @@ void Renderer::_BackendRender( const uint64_t sequence = _frameSource->CaptureSequence(); if (sequence != _captureSequence) { _captureSequence = sequence; + _activeCaptureSequence.store(sequence, std::memory_order_release); + _activeResourceGeneration.store( + _frameSource->ResourceGeneration(), std::memory_order_release); _captureCadence.RestartSequence(); _captureCadenceQueueWait = {}; if (_capturedFrameId) { @@ -2718,9 +3015,12 @@ void Renderer::_BackendRender( const auto downstreamWait = std::exchange(_captureCadenceQueueWait, std::chrono::steady_clock::duration::zero()); if (_captureCadence.Observe(captureTime, downstreamWait)) { - if (_frameGuidanceService.IsInitialized()) - _frameGuidanceService.ResetHistory(FrameGuidanceResetReason::LongPause); - if (_dlssFrameGenerator) _dlssFrameGenerator->RequestHistoryReset(); + // A delayed WGC notification is a delivery-time observation, not a + // capture interruption. Resetting DLSSFG history here drops the first + // generated frame after an otherwise valid static-window gap and causes + // visible flashing. Actual interruptions already advance CaptureSequence + // and reset temporal consumers in the branch above. + Logger::Get().Info("Capture cadence gap observed; preserving temporal history until capture sequence changes"); } if (_dlssFrameGenerator) _synchronousPresentInterval = _captureCadence.Interval(_dlssFrameGenerator->Multiplier(), _baseFrameRateLimit); @@ -2734,7 +3034,10 @@ void Renderer::_BackendRender( if (_frameGuidanceService.IsInitialized()) { FrameTrace::Scope traceGuidance(FrameTrace::Event::Guidance); _frameGuidanceService.BeginFrame( - _capturedFrameId, _frameSource->GetOutput(), guidanceRequirements); + _capturedFrameId, _frameSource->GetPipelineTexture(), guidanceRequirements, + _frameSource->CaptureSequence(), _frameSource->ResourceGeneration(), + _frameSource->CaptureTimestamp100ns(), + _frameSource->GetHdrFrameMetadata().color); } } if (_dlssFrameGenerator && isNewCaptureFrame) { @@ -2743,7 +3046,17 @@ void Renderer::_BackendRender( FrameTrace::SetFrame(_capturedFrameId); traceRender.FrameId(_capturedFrameId); - _passThroughFrames.UpdateBackend(_capturedFrameId, isNewCaptureFrame); + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && isNewCaptureFrame && _capturedFrameId <= 2) { + _LogHdrTextureStats(_frameSource->GetPipelineTexture(), "capture-canonical"); + } + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) { + HdrFrame captureFrame = _frameSource->GetCanonicalFrame(); + captureFrame.metadata.frameId = _capturedFrameId; + captureFrame.metadata.captureSequence = _captureSequence; + _passThroughFrames.UpdateBackend(captureFrame, isNewCaptureFrame); + } else { + _passThroughFrames.UpdateBackend(_capturedFrameId, isNewCaptureFrame); + } ID3D11DeviceContext4* d3dDC = _backendResources.GetD3DDC(); d3dDC->ClearState(); @@ -2754,10 +3067,26 @@ void Renderer::_BackendRender( } _effectsProfiler.OnBeginEffects(d3dDC); + if (isNewCaptureFrame) { + _UpdateHdrEffectBoundaryContexts(); + } for (uint32_t i = 0; i < _effectDrawers.size(); ++i) { const EffectDrawer& effectDrawer = _effectDrawers[i]; + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) { + // Rebind every boundary to the actual upstream canonical handoff for + // this frame. Initialization-time pointers become stale after the + // first capture and after any resize/rebuild. + ID3D11Texture2D* upstream = i == 0 + ? _frameSource->GetPipelineTexture() + : _effectDrawers[i - 1].GetExternalOutputTexture(); + _effectDrawers[i].SetHdrInputSource(upstream); + } if (i < _nativeEffectBackends.size() && _nativeEffectBackends[i]) { + if (!effectDrawer.PrepareHdrInput()) { + Logger::Get().Error("准备 native HDR 效果输入失败"); + continue; + } D3D11_TEXTURE2D_DESC inputDesc{}; effectDrawer.GetTexture(0)->GetDesc(&inputDesc); const FrameGuidanceConsumerViews guidance = @@ -2768,6 +3097,12 @@ void Renderer::_BackendRender( const NativeEffectDrawContext drawContext{ .input = effectDrawer.GetTexture(0), .output = effectDrawer.GetOutputTexture(), + .inputMetadata = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? effectDrawer.GetHdrBoundary().inputFrame.metadata : HdrFrameMetadata{}, + // The backend writes the route-local output; downstream canonical + // metadata is rebuilt by CompleteHdrOutput from the same color. + .outputMetadata = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? effectDrawer.GetHdrBoundary().inputFrame.metadata : HdrFrameMetadata{}, .frameId = _capturedFrameId, .inputRevision = i < _effectInputRevisions.size() ? _effectInputRevisions[i] : 0, @@ -2775,16 +3110,50 @@ void Renderer::_BackendRender( .zeroFrameGuidance = guidance.zero }; FrameTrace::Scope traceNative(FrameTrace::Event::NativeEffect, i); - if (!_nativeEffectBackends[i]->Draw(drawContext)) { - Logger::Get().Error("Draw native effect failed"); + const bool nativeDrawSucceeded = _nativeEffectBackends[i]->Draw(drawContext); + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && isNewCaptureFrame && _capturedFrameId <= 2) { + _LogHdrTextureStats(drawContext.input, fmt::format("effect-{}-input", i)); + _LogHdrTextureStats(drawContext.output, fmt::format("effect-{}-backend-output", i)); + } + if (!nativeDrawSucceeded) { + const HdrEffectBoundaryContext& boundary = effectDrawer.GetHdrBoundary(); + D3D11_TEXTURE2D_DESC outputDesc{}; + if (effectDrawer.GetOutputTexture()) { + effectDrawer.GetOutputTexture()->GetDesc(&outputDesc); + } + Logger::Get().Error(fmt::format( + "Native effect Draw failed: effect={} route={} profile={} " + "inputFormat={} outputFormat={} fallback=marker-pass", + _runtimeEffectOptions[i].name, + boundary.SelectedRoute() ? boundary.SelectedRoute()->Id() : "(none)", + ToString(boundary.plan.profile), + static_cast(inputDesc.Format), + static_cast(outputDesc.Format))); + // A runtime SDK failure must not publish the cleared route output. + // Execute the production marker pass through the same drawer so the + // chain remains visible at the requested output size. + effectDrawer.Draw(_effectsProfiler); + } else if (!effectDrawer.CompleteHdrOutput()) { + Logger::Get().Error("完成 native HDR 效果输出失败"); } _effectsProfiler.OnEndPass(d3dDC); } else { effectDrawer.Draw(_effectsProfiler); + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && isNewCaptureFrame && _capturedFrameId <= 2) { + _LogHdrTextureStats(effectDrawer.GetTexture(0), fmt::format("effect-{}-adapter-input", i)); + _LogHdrTextureStats(effectDrawer.GetExternalOutputTexture(), fmt::format("effect-{}-canonical-output", i)); + } } } _effectsProfiler.OnEndEffects(d3dDC); + // Keep the pacing/queue optimization, while avoiding duplicate real-frame + // submissions during a capture gap. FG must advance from a new canonical + // capture or its own generated frame, never from a repeated stale input. + if (!isNewCaptureFrame && (_dlssFrameGenerator || _isXeSSFrameGenerationActive)) { + d3dDC->Flush(); + return; + } if (_frontEdgeSyncEnabled && _dlssFrameGenerator && isNewCaptureFrame && _backendMayDeferFG && _synchronousFramePresentationEnabled.load(std::memory_order_acquire)) { @@ -2792,30 +3161,102 @@ void Renderer::_BackendRender( d3dDC->Flush(); return; } - _CompleteBackendFrame(effectsOutput, isNewCaptureFrame); + _CompleteBackendFrame(effectsOutput, isNewCaptureFrame, _captureSequence); +} + +void Renderer::_LogHdrTextureStats(ID3D11Texture2D* texture, std::string_view label) noexcept { + if (!texture) { + Logger::Get().Warn(fmt::format("HDR texture stats: label={} texture=null", label)); + return; + } + D3D11_TEXTURE2D_DESC sourceDesc{}; + texture->GetDesc(&sourceDesc); + if (sourceDesc.ArraySize != 1 || sourceDesc.MipLevels != 1) return; + D3D11_TEXTURE2D_DESC staging = sourceDesc; + staging.Usage = D3D11_USAGE_STAGING; + staging.BindFlags = 0; + staging.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + staging.MiscFlags = 0; + winrt::com_ptr readback; + if (FAILED(_backendResources.GetD3DDevice()->CreateTexture2D(&staging, nullptr, readback.put()))) { + Logger::Get().Warn(fmt::format("HDR texture stats: label={} staging-create-failed format={}", label, static_cast(sourceDesc.Format))); + return; + } + _backendResources.GetD3DDC()->CopyResource(readback.get(), texture); + _backendResources.GetD3DDC()->Flush(); + D3D11_MAPPED_SUBRESOURCE mapped{}; + if (FAILED(_backendResources.GetD3DDC()->Map(readback.get(), 0, D3D11_MAP_READ, 0, &mapped))) { + Logger::Get().Warn(fmt::format("HDR texture stats: label={} map-failed format={}", label, static_cast(sourceDesc.Format))); + return; + } + double sum[4]{}; float minimum[4]{ FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX }; + float maximum[4]{ -FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX }; uint64_t finite = 0, invalid = 0; + for (UINT y = 0; y < sourceDesc.Height; ++y) { + const auto* row = static_cast(mapped.pData) + size_t(y) * mapped.RowPitch; + for (UINT x = 0; x < sourceDesc.Width; ++x) { + float values[4]{}; + if (sourceDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM) { + const auto* p = row + size_t(x) * 4; for (int c = 0; c < 4; ++c) values[c] = p[c] / 255.0f; + } else if (sourceDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT) { + const auto* p = reinterpret_cast(row) + size_t(x) * 4; + for (int c = 0; c < 4; ++c) { const uint16_t h = p[c]; const uint32_t e = (h >> 10) & 31u; const uint32_t f = h & 1023u; const bool s = (h & 0x8000u) != 0; values[c] = e == 0 ? std::ldexp(float(f), -24) : e == 31 ? (f ? NAN : (s ? -INFINITY : INFINITY)) : std::ldexp(float(1024 + f), int(e) - 25) * (s ? -1.0f : 1.0f); } + } else { continue; } + for (int c = 0; c < 4; ++c) { if (std::isfinite(values[c])) { sum[c] += values[c]; minimum[c] = (std::min)(minimum[c], values[c]); maximum[c] = (std::max)(maximum[c], values[c]); ++finite; } else ++invalid; } + } + } + _backendResources.GetD3DDC()->Unmap(readback.get(), 0); + const double pixels = double(sourceDesc.Width) * double(sourceDesc.Height); + Logger::Get().Info(fmt::format("HDR texture stats: label={} format={} size={}x{} finite={} invalid={} R=[{:.6g},{:.6g},{:.6g}] G=[{:.6g},{:.6g},{:.6g}] B=[{:.6g},{:.6g},{:.6g}] A=[{:.6g},{:.6g},{:.6g}]", label, static_cast(sourceDesc.Format), sourceDesc.Width, sourceDesc.Height, finite, invalid, minimum[0], maximum[0], sum[0] / pixels, minimum[1], maximum[1], sum[1] / pixels, minimum[2], maximum[2], sum[2] / pixels, minimum[3], maximum[3], sum[3] / pixels)); } -void Renderer::_CompleteBackendFrame(ID3D11Texture2D* effectsOutput, bool isNewCaptureFrame) noexcept { +void Renderer::_CompleteBackendFrame( + ID3D11Texture2D* effectsOutput, + bool isNewCaptureFrame, + uint64_t captureSequence +) noexcept { auto* d3dDC = _backendResources.GetD3DDC(); if (_frontEdgeSyncEnabled) _baseFrameRateLimit = _FrontEdgeFrameRate(); if (_dlssFrameGenerator) _synchronousPresentInterval = _captureCadence.Interval(_dlssFrameGenerator->Multiplier(), _baseFrameRateLimit); if (_dlssFrameGenerator && isNewCaptureFrame) { D3D11_TEXTURE2D_DESC sourceDesc{}; - _frameSource->GetOutput()->GetDesc(&sourceDesc); + _frameSource->GetPipelineTexture()->GetDesc(&sourceDesc); const FrameGuidanceConsumerViews guidance = _frameGuidanceService.GetConsumerViews( _capturedFrameId, { sourceDesc.Width, sourceDesc.Height }, GetMotionVectorRequest( _dlssFrameGenerator->GetFrameGuidanceRequirements())); _dlssFgPresentationStopping = false; + ID3D11Texture2D* dlssInput = effectsOutput; + if (_dlssFgNormalizedInput && _dlssFgHdrNormalizationScale != 1.0f) { + if (!_hdrPresentationAdapter.ConvertHdrToBounded( + effectsOutput, _dlssFgNormalizedInput.get(), + HdrColorTransform::ForFrame(_frameSource->GetHdrFrameMetadata().color), + _dlssFgHdrNormalizationScale)) { + Logger::Get().Error("DLSSFG canonical-to-bounded input conversion failed"); + return; + } + dlssInput = _dlssFgNormalizedInput.get(); + } const bool generated = _dlssFrameGenerator->Draw( - effectsOutput, + dlssInput, _capturedFrameId, guidance.produced, guidance.zero, - [this](ID3D11Texture2D* generatedFrame) { - return _PublishBackendTexture(generatedFrame, true, true); + [this, captureSequence](ID3D11Texture2D* generatedFrame) { + ID3D11Texture2D* canonicalGenerated = generatedFrame; + if (_dlssFgCanonicalGenerated && _dlssFgHdrNormalizationScale != 1.0f) { + if (!_hdrPresentationAdapter.ConvertBoundedToHdr( + generatedFrame, _dlssFgCanonicalGenerated.get(), + HdrColorTransform::ForFrame(_frameSource->GetHdrFrameMetadata().color), + _dlssFgHdrNormalizationScale)) { + Logger::Get().Error("DLSSFG bounded-to-canonical output conversion failed"); + return false; + } + canonicalGenerated = _dlssFgCanonicalGenerated.get(); + } + return _PublishBackendTexture(canonicalGenerated, true, true, captureSequence, + _frameSource ? _frameSource->ResourceGeneration() : 0); } ); if (!generated && !_dlssFgPresentationStopping) { @@ -2830,7 +3271,8 @@ void Renderer::_CompleteBackendFrame(ID3D11Texture2D* effectsOutput, bool isNewC const bool synchronous = _dlssFrameGenerator && _synchronousFramePresentationEnabled.load(std::memory_order_acquire); - if (!_PublishBackendTexture(effectsOutput, synchronous, false)) { + if (!_PublishBackendTexture(effectsOutput, synchronous, false, captureSequence, + _frameSource ? _frameSource->ResourceGeneration() : 0)) { return; } @@ -2841,13 +3283,71 @@ void Renderer::_CompleteBackendFrame(ID3D11Texture2D* effectsOutput, bool isNewC bool Renderer::_PublishBackendTexture( ID3D11Texture2D* texture, bool synchronous, - bool generatedFrame + bool generatedFrame, + uint64_t captureSequence, + uint64_t resourceGeneration ) noexcept { + const uint64_t currentGeneration = _frameSource ? _frameSource->ResourceGeneration() : + _sharedTextureGeneration.load(std::memory_order_acquire); + if (captureSequence != 0 && captureSequence != _captureSequence) { + Logger::Get().Warn(fmt::format( + "Dropping stale {} frame: generation={} currentGeneration={}", + generatedFrame ? "generated" : "real", captureSequence, _captureSequence)); + // The SDK submission itself completed; treat the stale publication as + // consumed so a recovery does not disable an otherwise healthy FG path. + return true; + } + if (resourceGeneration != 0 && resourceGeneration != currentGeneration) { + Logger::Get().Warn(fmt::format( + "Dropping stale {} frame: resourceGeneration={} currentResourceGeneration={}", + generatedFrame ? "generated" : "real", resourceGeneration, currentGeneration)); + return true; + } + if (!texture) { + Logger::Get().Error("Dropping frame publication with null texture"); + return false; + } ID3D11DeviceContext4* d3dDC = _backendResources.GetD3DDC(); + ID3D11Texture2D* publicationTexture = texture; + HdrFrameMetadata publicationMetadata{}; + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled()) { + publicationMetadata = _frameSource->GetHdrFrameMetadata(); + publicationMetadata.frameId = _capturedFrameId; + publicationMetadata.captureSequence = captureSequence; + publicationMetadata.resourceGeneration = _frameSource->ResourceGeneration(); + publicationMetadata.timestamp100ns = _frameSource->CaptureTimestamp100ns(); + publicationMetadata.stage = generatedFrame + ? HdrFrameStage::GeneratedOutput : HdrFrameStage::CanonicalOutput; + publicationMetadata.generated = generatedFrame; + D3D11_TEXTURE2D_DESC textureDesc{}; + texture->GetDesc(&textureDesc); + if (textureDesc.Format != DXGI_FORMAT_R16G16B16A16_FLOAT) { + Logger::Get().Error("HDR 发布收到非 canonical FP16 纹理"); + return false; + } + if (_isXeSSFrameGenerationActive) { + if (!_hdrPresentationTexture || + !_hdrPresentationAdapter.ConvertCanonicalToHdr10( + texture, _hdrPresentationTexture.get(), + HdrColorTransform::ForFrame(_frameSource->GetHdrFrameMetadata().color))) { + Logger::Get().Error("XeSSFG canonical-to-HDR10 conversion failed"); + return false; + } + publicationTexture = _hdrPresentationTexture.get(); + publicationMetadata.stage = HdrFrameStage::PublishedOutput; + publicationMetadata.color.transfer = HdrTransferFunction::PQ; + publicationMetadata.sourceFormat = DXGI_FORMAT_R10G10B10A2_UNORM; + } + } const bool queuedPresentation = synchronous && _synchronousFramePresentationEnabled.load(std::memory_order_acquire); const uint32_t sharedTextureSlot = _nextBackendSharedTextureSlot++ % _sharedTextureSlotCount; + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && + _capturedFrameId <= 2) { + _LogHdrTextureStats(publicationTexture, fmt::format( + "publication-source-slot-{}", sharedTextureSlot)); + } bool slotReserved = false; if (queuedPresentation) { const auto ringWaitStart = std::chrono::steady_clock::now(); @@ -2932,7 +3432,22 @@ bool Renderer::_PublishBackendTexture( hr = AcquirePresentationTextures(mutexes, currentKey, 250); } if (SUCCEEDED(hr)) { - d3dDC->CopyResource(_backendSharedTextures[sharedTextureSlot].get(), texture); + HdrFrameMetadata frameMetadata = _frameSource ? + _frameSource->GetHdrFrameMetadata() : HdrFrameMetadata{}; + frameMetadata.frameId = _capturedFrameId; + frameMetadata.captureSequence = captureSequence; + frameMetadata.resourceGeneration = resourceGeneration != 0 ? resourceGeneration : currentGeneration; + frameMetadata.timestamp100ns = _frameSource ? _frameSource->CaptureTimestamp100ns() : 0; + frameMetadata.generated = generatedFrame; + frameMetadata.stage = generatedFrame ? HdrFrameStage::GeneratedOutput : HdrFrameStage::PublishedOutput; + frameMetadata.valid = true; + _sharedFrameMetadata[sharedTextureSlot] = frameMetadata; + d3dDC->CopyResource(_backendSharedTextures[sharedTextureSlot].get(), publicationTexture); + if (ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() && + _capturedFrameId <= 2) { + _LogHdrTextureStats(_backendSharedTextures[sharedTextureSlot].get(), + fmt::format("publication-shared-slot-{}", sharedTextureSlot)); + } _passThroughFrames.Publish(sharedTextureSlot, generatedFrame); if (mutexes[2] && xessMotionValid) { d3dDC->CopyResource(_backendSharedMotionTextures[sharedTextureSlot].get(), xessMotion); @@ -2941,6 +3456,15 @@ bool Renderer::_PublishBackendTexture( _sharedMotionValid[sharedTextureSlot].store(xessMotionValid, std::memory_order_release); _sharedMotionReset[sharedTextureSlot].store(xessMotionReset || !xessMotionValid, std::memory_order_release); + _sharedTextureCaptureSequences[sharedTextureSlot].store( + captureSequence, std::memory_order_release); + _sharedTextureFrameIds[sharedTextureSlot].store( + publicationMetadata.frameId, std::memory_order_release); + _sharedTextureResourceGenerations[sharedTextureSlot].store( + publicationMetadata.resourceGeneration, std::memory_order_release); + _sharedTextureTimestamps[sharedTextureSlot].store( + _frameSource->CaptureTimestamp100ns(), std::memory_order_release); + _sharedFrameMetadata[sharedTextureSlot] = publicationMetadata; hr = ReleasePresentationTextures(mutexes, key); if (SUCCEEDED(hr)) _sharedTextureMutexKeys[sharedTextureSlot].store(key, std::memory_order_release); } diff --git a/src/Magpie.Core/Renderer.h b/src/Magpie.Core/Renderer.h index fae11ab83..89e0af536 100644 --- a/src/Magpie.Core/Renderer.h +++ b/src/Magpie.Core/Renderer.h @@ -36,7 +36,6 @@ class Renderer { Renderer(Renderer&&) = delete; ScalingError Initialize(HWND hwndAttach, OverlayOptions& overlayOptions) noexcept; - // Frontend: cancel new capture/presentation work before cursor teardown. void BeginShutdown() noexcept; const std::string& InitializationContext() const noexcept { return _backendInitContext; } const std::wstring& MotionConfigurationNotice() const noexcept { return _motionConfigurationNotice; } @@ -144,7 +143,9 @@ class Renderer { wil::unique_handle _fgInputTimer; winrt::com_ptr _pendingFrameGenerationInput; bool _backendMayDeferFG = false; - void _CompleteBackendFrame(ID3D11Texture2D* effectsOutput, bool isNewCaptureFrame) noexcept; + void _CompleteBackendFrame(ID3D11Texture2D* effectsOutput, bool isNewCaptureFrame, + uint64_t captureSequence) noexcept; + void _LogHdrTextureStats(ID3D11Texture2D* texture, std::string_view label) noexcept; struct PendingFrontendFrame { bool stableBaseOnly = false; bool contentFrame = false; @@ -198,6 +199,8 @@ class Renderer { ID3D11Texture2D* _BuildEffects() noexcept; + void _UpdateHdrEffectBoundaryContexts() noexcept; + void _UpdateActiveEffectDescs() noexcept; bool _ShouldAppendBicubic(ID3D11Texture2D* outTexture) noexcept; @@ -222,7 +225,9 @@ class Renderer { bool _PublishBackendTexture( ID3D11Texture2D* texture, bool synchronous, - bool generatedFrame = false + bool generatedFrame = false, + uint64_t captureSequence = 0, + uint64_t resourceGeneration = 0 ) noexcept; bool _InitializeDLSSFrameGenerator( @@ -315,6 +320,11 @@ class Renderer { std::array, MAX_SHARED_TEXTURE_SLOTS> _backendSharedTextures; + HdrSurfaceAdapter _hdrPresentationAdapter; + winrt::com_ptr _hdrPresentationTexture; + winrt::com_ptr _dlssFgNormalizedInput; + winrt::com_ptr _dlssFgCanonicalGenerated; + float _dlssFgHdrNormalizationScale = 1.0f; std::array, MAX_SHARED_TEXTURE_SLOTS> _backendSharedTextureMutexes; std::array, MAX_SHARED_TEXTURE_SLOTS> @@ -343,6 +353,19 @@ class Renderer { _sharedMotionValid{}; std::array, MAX_SHARED_TEXTURE_SLOTS> _sharedMotionReset{}; + std::array, MAX_SHARED_TEXTURE_SLOTS> + _sharedTextureCaptureSequences{}; + std::array, MAX_SHARED_TEXTURE_SLOTS> + _sharedTextureFrameIds{}; + std::array, MAX_SHARED_TEXTURE_SLOTS> + _sharedTextureResourceGenerations{}; + std::array, MAX_SHARED_TEXTURE_SLOTS> + _sharedTextureTimestamps{}; + std::array _sharedFrameMetadata{}; + HdrFrameMetadata _frontendFrameMetadata{}; + HdrFrameMetadata _frontendPresentedFrameMetadata{}; + std::atomic _activeCaptureSequence = 0; + std::atomic _activeResourceGeneration = 0; std::atomic _latestSharedTextureSlot = 0; std::atomic _sharedTextureGeneration = 0; std::atomic _synchronousFramePresentationEnabled = false; diff --git a/src/Magpie.Core/ScalingOptions.cpp b/src/Magpie.Core/ScalingOptions.cpp index 72e16b399..5826c3f1f 100644 --- a/src/Magpie.Core/ScalingOptions.cpp +++ b/src/Magpie.Core/ScalingOptions.cpp @@ -56,6 +56,7 @@ void ScalingOptions::Log() const noexcept { IsCaptureTitleBar: {} IsAdjustCursorSpeed: {} IsDirectFlipDisabled: {} + IsHdrCompatibilityEnabled: {} cropping: {},{},{},{} graphicsCardId: idx: {} @@ -94,6 +95,7 @@ void ScalingOptions::Log() const noexcept { IsCaptureTitleBar(), IsAdjustCursorSpeed(), IsDirectFlipDisabled(), + IsHdrCompatibilityEnabled(), cropping.Left, cropping.Top, cropping.Right, cropping.Bottom, graphicsCardId.idx, graphicsCardId.vendorId, diff --git a/src/Magpie.Core/ScalingWindow.cpp b/src/Magpie.Core/ScalingWindow.cpp index faf8b7a12..a4d4c01f2 100644 --- a/src/Magpie.Core/ScalingWindow.cpp +++ b/src/Magpie.Core/ScalingWindow.cpp @@ -241,7 +241,7 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept { _windowRect.top, _windowRect.right - _windowRect.left, _windowRect.bottom - _windowRect.top, - nullptr, // Associate with the source only after initialization succeeds. + nullptr, NULL, wil::GetModuleInstanceHandle(), this @@ -301,7 +301,7 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept { _windowRect.top, _windowRect.right - _windowRect.left, _windowRect.bottom - _windowRect.top, - nullptr, // An initializing window must not block its source's window messages. + nullptr, NULL, wil::GetModuleInstanceHandle(), this @@ -344,8 +344,6 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept { Logger::Get().Win32Error("Set scaling window owner after initialization failed"); return ScalingError::ScalingWindowCreationFailed; } - // Restore the existing input behavior only after establishing ownership. - // This does not replace removing ownership before a blocking teardown. if (!AttachThreadInput(GetCurrentThreadId(), GetWindowThreadProcessId(hwndSrc, nullptr), FALSE)) { Logger::Get().Win32Warn("Detach source input queue after setting window owner failed"); } @@ -1104,13 +1102,10 @@ LRESULT ScalingWindow::_MessageHandler(UINT msg, WPARAM wParam, LPARAM lParam) n } case WM_DESTROY: { - // The source can synchronously wait for an owned popup's window thread. - // Remove that relationship before joining workers or entering SDK teardown. if (!SetScalingWindowOwner(Handle(), nullptr)) { Logger::Get().Win32Warn("Detach scaling window owner before teardown failed"); } const bool ngxWasFaulted = NgxRuntimeGuard::IsFaulted(); - // Invalidate queued callbacks before any teardown can dispatch messages. ++_runId; Logger::Get().Info("缩放结束"); if (_renderer) { diff --git a/src/Magpie.Core/Win32Helper.cpp b/src/Magpie.Core/Win32Helper.cpp index 59505d802..13a8bb9b0 100644 --- a/src/Magpie.Core/Win32Helper.cpp +++ b/src/Magpie.Core/Win32Helper.cpp @@ -11,6 +11,7 @@ #pragma pop_macro("ShellExecute") #include #include +#include namespace Magpie { @@ -88,6 +89,42 @@ std::vector GetActiveDisplayTargets() noexcept { return result; } +float QuerySdrWhiteNits(std::wstring_view gdiDeviceName) noexcept { + UINT32 pathCount = 0; + UINT32 modeCount = 0; + if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount) != ERROR_SUCCESS) { + return 0.0f; + } + std::vector paths(pathCount); + std::vector modes(modeCount); + if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &pathCount, paths.data(), &modeCount, + modes.data(), nullptr) != ERROR_SUCCESS) { + return 0.0f; + } + for (const auto& path : std::span(paths.data(), pathCount)) { + DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName{}; + sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + sourceName.header.size = sizeof(sourceName); + sourceName.header.adapterId = path.sourceInfo.adapterId; + sourceName.header.id = path.sourceInfo.id; + if (DisplayConfigGetDeviceInfo(&sourceName.header) != ERROR_SUCCESS || + CompareStringOrdinal(sourceName.viewGdiDeviceName, -1, + gdiDeviceName.data(), static_cast(gdiDeviceName.size()), TRUE) != CSTR_EQUAL) { + continue; + } + DISPLAYCONFIG_SDR_WHITE_LEVEL white{}; + white.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL; + white.header.size = sizeof(white); + white.header.adapterId = path.targetInfo.adapterId; + white.header.id = path.targetInfo.id; + if (DisplayConfigGetDeviceInfo(&white.header) == ERROR_SUCCESS && white.SDRWhiteLevel > 0) { + const float nits = 80.0f * static_cast(white.SDRWhiteLevel) / 1000.0f; + return std::clamp(nits, 40.0f, 1000.0f); + } + } + return 0.0f; +} + bool EqualDeviceName(std::wstring_view left, std::wstring_view right) noexcept { return CompareStringOrdinal( left.data(), (int)left.size(), @@ -96,6 +133,14 @@ bool EqualDeviceName(std::wstring_view left, std::wstring_view right) noexcept { } +float Win32Helper::GetMonitorSdrWhiteNits(HMONITOR monitor) noexcept { + if (!monitor) return 0.0f; + MONITORINFOEXW info{}; + info.cbSize = sizeof(info); + if (!GetMonitorInfoW(monitor, &info)) return 0.0f; + return QuerySdrWhiteNits(info.szDevice); +} + std::vector Win32Helper::GetDisplayMonitors() noexcept { std::vector result; const std::vector activeTargets = GetActiveDisplayTargets(); diff --git a/src/Magpie.Core/XeSSFGPresenter.cpp b/src/Magpie.Core/XeSSFGPresenter.cpp index 73b00b81b..ea5a339b6 100644 --- a/src/Magpie.Core/XeSSFGPresenter.cpp +++ b/src/Magpie.Core/XeSSFGPresenter.cpp @@ -16,7 +16,22 @@ namespace Magpie { static constexpr uint32_t BUFFER_COUNT = 3; -static constexpr DXGI_FORMAT COLOR_FORMAT = DXGI_FORMAT_R8G8B8A8_UNORM; +// XeSS-FG HDR terminal contract: HDR10/BT.2100 packed 10:10:10:2 UNORM. +// The proxy swap-chain, shared color surface, and back buffers all use this +// exact format so the SDK observes one consistent terminal resource format. +static constexpr DXGI_FORMAT HDR_COLOR_FORMAT = DXGI_FORMAT_R10G10B10A2_UNORM; +static constexpr DXGI_FORMAT LDR_COLOR_FORMAT = DXGI_FORMAT_R8G8B8A8_UNORM; +// DirectComposition virtual surfaces do not accept the XeSS terminal's +// packed R10 format. Keep the independent UI surface in FP16/scRGB; the +// XeSS proxy swap chain and terminal color resources remain HDR10 R10. +static constexpr DXGI_FORMAT OVERLAY_FORMAT = DXGI_FORMAT_R16G16B16A16_FLOAT; + +static DXGI_FORMAT ColorFormat(bool hdr) noexcept { + return hdr ? HDR_COLOR_FORMAT : LDR_COLOR_FORMAT; +} +static DXGI_FORMAT OverlayFormat(bool hdr) noexcept { + return hdr ? OVERLAY_FORMAT : LDR_COLOR_FORMAT; +} static bool XeFGSucceeded(xefg_swapchain_result_t result) noexcept { return result >= XEFG_SWAPCHAIN_RESULT_SUCCESS; @@ -111,6 +126,7 @@ struct XeSSFGPresenter::Impl { bool externalMotionValid = false; bool externalMotionReset = true; bool resetHistory = true; + bool hdrEnabled = false; std::chrono::steady_clock::time_point lastPresent{}; }; @@ -158,7 +174,7 @@ static bool CreateSharedColor(XeSSFGPresenter::Impl& impl) noexcept { desc.Height = impl.height; desc.MipLevels = 1; desc.ArraySize = 1; - desc.Format = COLOR_FORMAT; + desc.Format = ColorFormat(impl.hdrEnabled); desc.SampleDesc.Count = 1; desc.Usage = D3D11_USAGE_DEFAULT; desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; @@ -389,7 +405,7 @@ bool XeSSFGPresenter::_ResizeOverlaySurface() noexcept { hr = impl.overlayDCompSurface->Resize(impl.width, impl.height); } else { hr = impl.overlayDCompDevice->CreateVirtualSurface( - impl.width, impl.height, COLOR_FORMAT, + impl.width, impl.height, OverlayFormat(impl.hdrEnabled), DXGI_ALPHA_MODE_PREMULTIPLIED, impl.overlayDCompSurface.put()); if (SUCCEEDED(hr)) { hr = impl.overlayDCompVisual->SetContent(impl.overlayDCompSurface.get()); @@ -440,6 +456,7 @@ bool XeSSFGPresenter::_Initialize(HWND hwndAttach) noexcept { impl->width = static_cast(size.cx); impl->height = static_cast(size.cy); impl->externalMotionEnabled = _useExternalMotion; + impl->hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); HRESULT hr = D3D12CreateDevice( _deviceResources->GetGraphicsAdapter(), D3D_FEATURE_LEVEL_11_0, @@ -548,7 +565,7 @@ bool XeSSFGPresenter::_Initialize(HWND hwndAttach) noexcept { DXGI_SWAP_CHAIN_DESC1 swapChainDesc{}; swapChainDesc.Width = impl->width; swapChainDesc.Height = impl->height; - swapChainDesc.Format = COLOR_FORMAT; + swapChainDesc.Format = ColorFormat(impl->hdrEnabled); swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swapChainDesc.BufferCount = BUFFER_COUNT; swapChainDesc.SampleDesc.Count = 1; @@ -573,6 +590,15 @@ bool XeSSFGPresenter::_Initialize(HWND hwndAttach) noexcept { LogXeFGResult("get proxy swap chain failed", result); return false; } + if (impl->hdrEnabled) { + HRESULT colorSpaceHr = impl->swapChain->SetColorSpace1( + DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); + if (FAILED(colorSpaceHr)) { + Logger::Get().ComError("Set XeSSFG HDR10/BT.2100 color space failed", colorSpaceHr); + return false; + } + Logger::Get().Info("XeSSFG endpoint: format=R10G10B10A2_UNORM colorSpace=HDR10/BT.2100"); + } impl->swapChain->SetMaximumFrameLatency(1); impl->frameLatencyWaitableObject.reset( impl->swapChain->GetFrameLatencyWaitableObject()); @@ -974,11 +1000,20 @@ bool XeSSFGPresenter::OnResize() noexcept { const UINT flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT | (_deviceResources->IsTearingSupported() && ScalingWindow::Get().Options().isVRREnabled ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0); HRESULT hr = impl.swapChain->ResizeBuffers( - BUFFER_COUNT, width, height, COLOR_FORMAT, flags); + BUFFER_COUNT, width, height, ColorFormat(impl.hdrEnabled), flags); if (FAILED(hr)) { Logger::Get().ComError("Resize XeSSFG proxy swap chain failed", hr); return false; } + if (impl.hdrEnabled) { + hr = impl.swapChain->SetColorSpace1( + DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020); + if (FAILED(hr)) { + Logger::Get().ComError( + "Restore XeSSFG HDR10/BT.2100 color space after resize failed", hr); + return false; + } + } impl.width = width; impl.height = height; impl.frameLatencyWaitableObject.reset( diff --git a/src/Magpie.Core/XeSSUpscaler.cpp b/src/Magpie.Core/XeSSUpscaler.cpp index b9aed5168..379011e6b 100644 --- a/src/Magpie.Core/XeSSUpscaler.cpp +++ b/src/Magpie.Core/XeSSUpscaler.cpp @@ -3,6 +3,7 @@ #include "DeviceResources.h" #include "DirectXHelper.h" #include "Logger.h" +#include "ScalingWindow.h" #ifdef MP_ENABLE_XESS_ZEROMV #include @@ -41,6 +42,7 @@ struct XeSSUpscaler::Impl { uint32_t outputHeight = 0; uint64_t lastSubmittedValue = 0; bool convertInputToRgba = false; + bool hdrEnabled = false; bool enableOpticalFlow = false; bool resetHistory = true; FrameGuidanceFrameId lastGuidanceResetFrameId = std::numeric_limits::max(); @@ -50,6 +52,20 @@ static constexpr char COLOR_CONVERT_HLSL[] = R"( Texture2D InputColor : register(t0); RWTexture2D OutputColor : register(u0); +[numthreads(8, 8, 1)] +void ConvertToRgba(uint3 tid : SV_DispatchThreadID) { + uint width, height; + OutputColor.GetDimensions(width, height); + if (tid.x >= width || tid.y >= height) return; + float4 color = InputColor.Load(int3(tid.xy, 0)); + // XeSS-SR does not preserve alpha; make the shared color contract opaque. + OutputColor[tid.xy] = float4(color.bgr, 1.0); +} +)"; + +static constexpr char COLOR_CONVERT_LDR_HLSL[] = R"( +Texture2D InputColor : register(t0); +RWTexture2D OutputColor : register(u0); [numthreads(8, 8, 1)] void ConvertToRgba(uint3 tid : SV_DispatchThreadID) { uint width, height; @@ -185,19 +201,44 @@ bool XeSSUpscaler::Initialize( D3D11_TEXTURE2D_DESC outputDesc{}; input->GetDesc(&inputDesc); output->GetDesc(&outputDesc); + const bool hdrEnabled = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled(); if (inputDesc.Width > outputDesc.Width || inputDesc.Height > outputDesc.Height) { Logger::Get().Error(fmt::format( "XeSS Zero-MV only supports upscaling: {}x{} -> {}x{}", inputDesc.Width, inputDesc.Height, outputDesc.Width, outputDesc.Height)); return false; } - const bool supportedInputFormat = inputDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM || - inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM; - if (!supportedInputFormat || outputDesc.Format != DXGI_FORMAT_R8G8B8A8_UNORM) { - Logger::Get().Error(fmt::format( - "XeSS Zero-MV unsupported texture formats: input={}, output={}", - (uint32_t)inputDesc.Format, (uint32_t)outputDesc.Format)); - return false; + if (!hdrEnabled) { + const bool supportedInputFormat = + inputDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM || + inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM; + if (!supportedInputFormat || outputDesc.Format != DXGI_FORMAT_R8G8B8A8_UNORM) { + Logger::Get().Error(fmt::format( + "XeSS Zero-MV unsupported texture formats: input={}, output={}", + (uint32_t)inputDesc.Format, (uint32_t)outputDesc.Format)); + return false; + } + } else { + const auto isXeSSColorFormat = [](DXGI_FORMAT format) noexcept { + return format == DXGI_FORMAT_R16G16B16A16_FLOAT || + format == DXGI_FORMAT_R11G11B10_FLOAT || + format == DXGI_FORMAT_R10G10B10A2_UNORM || + format == DXGI_FORMAT_R8G8B8A8_UNORM || + format == DXGI_FORMAT_B8G8R8A8_UNORM; + }; + const bool supportedOutputFormat = + outputDesc.Format != DXGI_FORMAT_B8G8R8A8_UNORM && + isXeSSColorFormat(outputDesc.Format); + const bool compatiblePair = inputDesc.Format == outputDesc.Format || + (inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM && + outputDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM); + if (!isXeSSColorFormat(inputDesc.Format) || + !supportedOutputFormat || !compatiblePair) { + Logger::Get().Error(fmt::format( + "XeSS Zero-MV unsupported texture formats: input={}, output={}", + (uint32_t)inputDesc.Format, (uint32_t)outputDesc.Format)); + return false; + } } const float scaleX = (float)outputDesc.Width / inputDesc.Width; const float scaleY = (float)outputDesc.Height / inputDesc.Height; @@ -210,7 +251,12 @@ bool XeSSUpscaler::Initialize( impl->inputHeight = inputDesc.Height; impl->outputWidth = outputDesc.Width; impl->outputHeight = outputDesc.Height; + impl->hdrEnabled = hdrEnabled; impl->convertInputToRgba = inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM; + const bool hdrColorInput = + impl->hdrEnabled && (inputDesc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT || + inputDesc.Format == DXGI_FORMAT_R11G11B10_FLOAT || + inputDesc.Format == DXGI_FORMAT_R10G10B10A2_UNORM); HRESULT hr = D3D12CreateDevice(deviceResources.GetGraphicsAdapter(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(impl->device12.put())); @@ -238,7 +284,8 @@ bool XeSSUpscaler::Initialize( } D3D11_TEXTURE2D_DESC xessInputDesc = inputDesc; - xessInputDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + xessInputDesc.Format = impl->convertInputToRgba ? + DXGI_FORMAT_R8G8B8A8_UNORM : inputDesc.Format; xessInputDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; if (!CreateSharedTexture(*impl, xessInputDesc, impl->sharedInput11, impl->sharedInput12) || !CreateSharedTexture(*impl, outputDesc, impl->sharedOutput11, impl->sharedOutput12)) { @@ -250,7 +297,8 @@ bool XeSSUpscaler::Initialize( impl->sharedInput11.get(), nullptr, impl->sharedInputUav11.put()); winrt::com_ptr shaderBlob; if (SUCCEEDED(hr) && !DirectXHelper::CompileComputeShader( - COLOR_CONVERT_HLSL, "ConvertToRgba", shaderBlob.put(), "XeSSColorConvert")) { + (impl->hdrEnabled ? COLOR_CONVERT_HLSL : COLOR_CONVERT_LDR_HLSL), + "ConvertToRgba", shaderBlob.put(), "XeSSColorConvert")) { hr = E_FAIL; } if (SUCCEEDED(hr)) hr = impl->device11->CreateComputeShader( @@ -420,7 +468,10 @@ bool XeSSUpscaler::Initialize( xess_d3d12_init_params_t initParams{}; initParams.outputResolution = { outputDesc.Width, outputDesc.Height }; initParams.qualitySetting = quality; - initParams.initFlags = XESS_INIT_FLAG_LDR_INPUT_COLOR | XESS_INIT_FLAG_RESPONSIVE_PIXEL_MASK; + initParams.initFlags = XESS_INIT_FLAG_RESPONSIVE_PIXEL_MASK; + if (!hdrColorInput) { + initParams.initFlags |= XESS_INIT_FLAG_LDR_INPUT_COLOR; + } if (!impl->enableOpticalFlow) initParams.initFlags |= XESS_INIT_FLAG_HIGH_RES_MV; if (!XessSucceeded(xessD3D12Init(impl->xessContext, &initParams), "xessD3D12Init") || !XessSucceeded(xessSetVelocityScale(impl->xessContext, 1.0f, 1.0f), "xessSetVelocityScale")) { @@ -430,9 +481,9 @@ bool XeSSUpscaler::Initialize( } Logger::Get().Info(fmt::format( - "XeSS experimental D3D11/D3D12 backend initialized (quality {}, {}, BGRA conversion={}): {}x{} -> {}x{}", + "XeSS experimental D3D11/D3D12 backend initialized (quality {}, {}, BGRA conversion={}, color={}): {}x{} -> {}x{}", (int)quality, impl->enableOpticalFlow ? "Shared optical flow" : "Zero-MV", - impl->convertInputToRgba, + impl->convertInputToRgba, hdrColorInput ? "linear HDR" : "LDR UNORM", inputDesc.Width, inputDesc.Height, outputDesc.Width, outputDesc.Height)); _impl = std::move(impl); return true; diff --git a/src/Magpie.Core/XeSSZeroMVUpscaler.cpp b/src/Magpie.Core/XeSSZeroMVUpscaler.cpp new file mode 100644 index 000000000..a1901c463 --- /dev/null +++ b/src/Magpie.Core/XeSSZeroMVUpscaler.cpp @@ -0,0 +1,605 @@ +#include "pch.h" +#include "XeSSZeroMVUpscaler.h" +#include "DeviceResources.h" +#include "DirectXHelper.h" +#include "HalfResOpticalFlow.h" +#include "Logger.h" + +#ifdef MP_ENABLE_XESS_ZEROMV +#include +#include +#include + +namespace Magpie { + +struct XeSSZeroMVUpscaler::Impl { + ID3D11Device5* device11 = nullptr; + ID3D11DeviceContext4* context11 = nullptr; + winrt::com_ptr device12; + winrt::com_ptr queue12; + winrt::com_ptr allocator12; + winrt::com_ptr commandList12; + winrt::com_ptr sharedInput11; + winrt::com_ptr sharedOutput11; + winrt::com_ptr sharedMotion11; + winrt::com_ptr inputSrv11; + winrt::com_ptr sharedInputUav11; + winrt::com_ptr colorConvertShader11; + winrt::com_ptr sharedInput12; + winrt::com_ptr sharedOutput12; + winrt::com_ptr sharedMotion12; + winrt::com_ptr zeroMotion12; + winrt::com_ptr flatDepth12; + winrt::com_ptr responsiveMask12; + winrt::com_ptr descriptorHeap12; + winrt::com_ptr fence11; + winrt::com_ptr fence12; + xess_context_handle_t xessContext = nullptr; + std::unique_ptr opticalFlow; + uint64_t fenceValue = 0; + uint32_t inputWidth = 0; + uint32_t inputHeight = 0; + uint32_t outputWidth = 0; + uint32_t outputHeight = 0; + uint64_t lastSubmittedValue = 0; + bool convertInputToRgba = false; + bool enableOpticalFlow = false; + bool enableJitter = false; + uint32_t frameIndex = 0; + bool resetHistory = true; +}; + +static constexpr char COLOR_CONVERT_HLSL[] = R"( +Texture2D InputColor : register(t0); +RWTexture2D OutputColor : register(u0); + +[numthreads(8, 8, 1)] +void ConvertToRgba(uint3 tid : SV_DispatchThreadID) { + uint width, height; + OutputColor.GetDimensions(width, height); + if (tid.x >= width || tid.y >= height) return; + OutputColor[tid.xy] = InputColor.Load(int3(tid.xy, 0)); +} +)"; + +static bool XessSucceeded(xess_result_t result, std::string_view operation) noexcept { + if (result == XESS_RESULT_SUCCESS) { + return true; + } + Logger::Get().Error(fmt::format("{} failed (XeSS result {})", operation, (int)result)); + return false; +} + +static bool WaitForD3D12(XeSSZeroMVUpscaler::Impl& impl) noexcept { + const uint64_t value = ++impl.fenceValue; + HRESULT hr = impl.queue12->Signal(impl.fence12.get(), value); + if (FAILED(hr)) { + Logger::Get().ComError("Signal XeSS D3D12 fence failed", hr); + return false; + } + + wil::unique_event_nothrow event; + hr = event.create(); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS fence event failed", hr); + return false; + } + hr = impl.fence12->SetEventOnCompletion(value, event.get()); + if (FAILED(hr)) { + Logger::Get().ComError("Set XeSS fence event failed", hr); + return false; + } + WaitForSingleObject(event.get(), INFINITE); + return true; +} + +static bool WaitForFenceValue(XeSSZeroMVUpscaler::Impl& impl, uint64_t value) noexcept { + if (!value || impl.fence12->GetCompletedValue() >= value) return true; + wil::unique_event_nothrow event; + HRESULT hr = event.create(); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS completion event failed", hr); + return false; + } + hr = impl.fence12->SetEventOnCompletion(value, event.get()); + if (FAILED(hr)) { + Logger::Get().ComError("Set XeSS completion event failed", hr); + return false; + } + WaitForSingleObject(event.get(), INFINITE); + return true; +} + +static bool CreateSharedTexture( + XeSSZeroMVUpscaler::Impl& impl, + const D3D11_TEXTURE2D_DESC& sourceDesc, + winrt::com_ptr& texture11, + winrt::com_ptr& texture12 +) noexcept { + D3D11_TEXTURE2D_DESC desc = sourceDesc; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.CPUAccessFlags = 0; + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED | D3D11_RESOURCE_MISC_SHARED_NTHANDLE; + + HRESULT hr = impl.device11->CreateTexture2D(&desc, nullptr, texture11.put()); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS shared D3D11 texture failed", hr); + return false; + } + + winrt::com_ptr dxgiResource; + hr = texture11->QueryInterface(IID_PPV_ARGS(dxgiResource.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Query XeSS shared IDXGIResource1 failed", hr); + return false; + } + + HANDLE rawHandle = nullptr; + hr = dxgiResource->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &rawHandle); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS texture shared handle failed", hr); + return false; + } + wil::unique_handle sharedHandle(rawHandle); + hr = impl.device12->OpenSharedHandle(sharedHandle.get(), IID_PPV_ARGS(texture12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Open XeSS texture in D3D12 failed", hr); + return false; + } + return true; +} + +static xess_quality_settings_t SelectQuality(float scale) noexcept { + if (scale <= 1.0f) return XESS_QUALITY_SETTING_AA; + if (scale <= 1.3f) return XESS_QUALITY_SETTING_ULTRA_QUALITY; + if (scale <= 1.5f) return XESS_QUALITY_SETTING_QUALITY; + if (scale <= 1.7f) return XESS_QUALITY_SETTING_BALANCED; + if (scale <= 2.0f) return XESS_QUALITY_SETTING_PERFORMANCE; + return XESS_QUALITY_SETTING_ULTRA_PERFORMANCE; +} + +XeSSZeroMVUpscaler::XeSSZeroMVUpscaler() = default; + +XeSSZeroMVUpscaler::~XeSSZeroMVUpscaler() { + if (_impl && _impl->xessContext) { + if (_impl->queue12 && _impl->fence12) { + WaitForD3D12(*_impl); + } + xessDestroyContext(_impl->xessContext); + _impl->xessContext = nullptr; + } +} + +bool XeSSZeroMVUpscaler::Initialize( + DeviceResources& deviceResources, + ID3D11Texture2D* input, + ID3D11Texture2D* output, + bool enableOpticalFlow, + bool enableJitter +) noexcept { + _enableOpticalFlow = enableOpticalFlow; + _enableJitter = enableJitter; + _impl.reset(); + auto impl = std::make_unique(); + impl->enableOpticalFlow = enableOpticalFlow; + impl->enableJitter = enableJitter; + impl->device11 = deviceResources.GetD3DDevice(); + impl->context11 = deviceResources.GetD3DDC(); + + D3D11_TEXTURE2D_DESC inputDesc{}; + D3D11_TEXTURE2D_DESC outputDesc{}; + input->GetDesc(&inputDesc); + output->GetDesc(&outputDesc); + if (inputDesc.Width > outputDesc.Width || inputDesc.Height > outputDesc.Height) { + Logger::Get().Error(fmt::format( + "XeSS Zero-MV only supports upscaling: {}x{} -> {}x{}", + inputDesc.Width, inputDesc.Height, outputDesc.Width, outputDesc.Height)); + return false; + } + const bool supportedInputFormat = inputDesc.Format == DXGI_FORMAT_R8G8B8A8_UNORM || + inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM; + if (!supportedInputFormat || outputDesc.Format != DXGI_FORMAT_R8G8B8A8_UNORM) { + Logger::Get().Error(fmt::format( + "XeSS Zero-MV unsupported texture formats: input={}, output={}", + (uint32_t)inputDesc.Format, (uint32_t)outputDesc.Format)); + return false; + } + const float scaleX = (float)outputDesc.Width / inputDesc.Width; + const float scaleY = (float)outputDesc.Height / inputDesc.Height; + const float scale = (std::max)(scaleX, scaleY); + if (scale > 3.0f) { + Logger::Get().Error("XeSS Zero-MV supports up to a 3x scale"); + return false; + } + impl->inputWidth = inputDesc.Width; + impl->inputHeight = inputDesc.Height; + impl->outputWidth = outputDesc.Width; + impl->outputHeight = outputDesc.Height; + impl->convertInputToRgba = inputDesc.Format == DXGI_FORMAT_B8G8R8A8_UNORM; + + HRESULT hr = D3D12CreateDevice(deviceResources.GetGraphicsAdapter(), D3D_FEATURE_LEVEL_11_0, + IID_PPV_ARGS(impl->device12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS D3D12 device failed", hr); + return false; + } + D3D12_FEATURE_DATA_SHADER_MODEL shaderModel{ D3D_SHADER_MODEL_6_4 }; + hr = impl->device12->CheckFeatureSupport(D3D12_FEATURE_SHADER_MODEL, &shaderModel, sizeof(shaderModel)); + if (FAILED(hr) || shaderModel.HighestShaderModel < D3D_SHADER_MODEL_6_4) { + Logger::Get().Error("XeSS cross-vendor path requires Shader Model 6.4 / DP4a support"); + return false; + } + + D3D12_COMMAND_QUEUE_DESC queueDesc{}; + queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + hr = impl->device12->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(impl->queue12.put())); + if (SUCCEEDED(hr)) hr = impl->device12->CreateCommandAllocator( + D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(impl->allocator12.put())); + if (SUCCEEDED(hr)) hr = impl->device12->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, + impl->allocator12.get(), nullptr, IID_PPV_ARGS(impl->commandList12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS D3D12 command objects failed", hr); + return false; + } + + D3D11_TEXTURE2D_DESC xessInputDesc = inputDesc; + xessInputDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + xessInputDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS; + if (!CreateSharedTexture(*impl, xessInputDesc, impl->sharedInput11, impl->sharedInput12) || + !CreateSharedTexture(*impl, outputDesc, impl->sharedOutput11, impl->sharedOutput12)) { + return false; + } + if (impl->convertInputToRgba) { + hr = impl->device11->CreateShaderResourceView(input, nullptr, impl->inputSrv11.put()); + if (SUCCEEDED(hr)) hr = impl->device11->CreateUnorderedAccessView( + impl->sharedInput11.get(), nullptr, impl->sharedInputUav11.put()); + winrt::com_ptr shaderBlob; + if (SUCCEEDED(hr) && !DirectXHelper::CompileComputeShader( + COLOR_CONVERT_HLSL, "ConvertToRgba", shaderBlob.put(), "XeSSColorConvert")) { + hr = E_FAIL; + } + if (SUCCEEDED(hr)) hr = impl->device11->CreateComputeShader( + shaderBlob->GetBufferPointer(), shaderBlob->GetBufferSize(), nullptr, + impl->colorConvertShader11.put()); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS BGRA-to-RGBA conversion resources failed", hr); + return false; + } + } + + D3D12_DESCRIPTOR_HEAP_DESC heapDesc{}; + heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + heapDesc.NumDescriptors = 3; + heapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + hr = impl->device12->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(impl->descriptorHeap12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS descriptor heap failed", hr); + return false; + } + + D3D12_HEAP_PROPERTIES heapProperties{}; + heapProperties.Type = D3D12_HEAP_TYPE_DEFAULT; + if (impl->enableOpticalFlow) { + impl->opticalFlow = std::make_unique(); + if (!impl->opticalFlow->Initialize(impl->device11, impl->context11, input)) { + Logger::Get().Error("Initialize XeSS 50% optical flow failed"); + return false; + } + D3D11_TEXTURE2D_DESC motionDesc11{}; + motionDesc11.Width = inputDesc.Width; + motionDesc11.Height = inputDesc.Height; + motionDesc11.MipLevels = 1; + motionDesc11.ArraySize = 1; + motionDesc11.Format = DXGI_FORMAT_R16G16_FLOAT; + motionDesc11.SampleDesc.Count = 1; + motionDesc11.BindFlags = D3D11_BIND_SHADER_RESOURCE; + if (!CreateSharedTexture(*impl, motionDesc11, impl->sharedMotion11, impl->sharedMotion12)) { + return false; + } + } else { + D3D12_RESOURCE_DESC motionDesc{}; + motionDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + motionDesc.Width = outputDesc.Width; + motionDesc.Height = outputDesc.Height; + motionDesc.DepthOrArraySize = 1; + motionDesc.MipLevels = 1; + motionDesc.Format = DXGI_FORMAT_R16G16_FLOAT; + motionDesc.SampleDesc.Count = 1; + motionDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + motionDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + hr = impl->device12->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, &motionDesc, + D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, IID_PPV_ARGS(impl->zeroMotion12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS zero motion-vector texture failed", hr); + return false; + } + } + + // Supply a real (but constant) depth resource in both modes. XeSS can omit + // depth with high-resolution motion vectors, but a non-null flat texture + // keeps the colour-only Zero-MV experiment explicit and comparable with the + // DLSS/FSR2 adapters. + D3D12_RESOURCE_DESC depthDesc{}; + depthDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + depthDesc.Width = inputDesc.Width; + depthDesc.Height = inputDesc.Height; + depthDesc.DepthOrArraySize = 1; + depthDesc.MipLevels = 1; + depthDesc.Format = DXGI_FORMAT_R32_FLOAT; + depthDesc.SampleDesc.Count = 1; + depthDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + depthDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS; + hr = impl->device12->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, &depthDesc, + D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, IID_PPV_ARGS(impl->flatDepth12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS flat depth texture failed", hr); + return false; + } + D3D12_RESOURCE_DESC responsiveMaskDesc = depthDesc; + responsiveMaskDesc.Format = DXGI_FORMAT_R8_UNORM; + hr = impl->device12->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, + &responsiveMaskDesc, D3D12_RESOURCE_STATE_UNORDERED_ACCESS, nullptr, + IID_PPV_ARGS(impl->responsiveMask12.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS responsive pixel mask failed", hr); + return false; + } + + const UINT descriptorSize = impl->device12->GetDescriptorHandleIncrementSize( + D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + const D3D12_CPU_DESCRIPTOR_HANDLE depthCpu = impl->descriptorHeap12->GetCPUDescriptorHandleForHeapStart(); + const D3D12_GPU_DESCRIPTOR_HANDLE depthGpu = impl->descriptorHeap12->GetGPUDescriptorHandleForHeapStart(); + D3D12_UNORDERED_ACCESS_VIEW_DESC depthUavDesc{}; + depthUavDesc.Format = DXGI_FORMAT_R32_FLOAT; + depthUavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + impl->device12->CreateUnorderedAccessView(impl->flatDepth12.get(), nullptr, &depthUavDesc, depthCpu); + ID3D12DescriptorHeap* heaps[] = { impl->descriptorHeap12.get() }; + impl->commandList12->SetDescriptorHeaps(1, heaps); + const float flatDepthValue[4]{ 1.0f, 1.0f, 1.0f, 1.0f }; + impl->commandList12->ClearUnorderedAccessViewFloat( + depthGpu, depthCpu, impl->flatDepth12.get(), flatDepthValue, 0, nullptr); + D3D12_CPU_DESCRIPTOR_HANDLE responsiveCpu = depthCpu; + responsiveCpu.ptr += descriptorSize; + D3D12_GPU_DESCRIPTOR_HANDLE responsiveGpu = depthGpu; + responsiveGpu.ptr += descriptorSize; + D3D12_UNORDERED_ACCESS_VIEW_DESC responsiveUavDesc{}; + responsiveUavDesc.Format = DXGI_FORMAT_R8_UNORM; + responsiveUavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + impl->device12->CreateUnorderedAccessView( + impl->responsiveMask12.get(), nullptr, &responsiveUavDesc, responsiveCpu); + const float responsiveValue[4]{ 0.5f, 0.5f, 0.5f, 0.5f }; + impl->commandList12->ClearUnorderedAccessViewFloat( + responsiveGpu, responsiveCpu, impl->responsiveMask12.get(), responsiveValue, 0, nullptr); + + D3D12_RESOURCE_BARRIER auxiliaryBarriers[3]{}; + auxiliaryBarriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + auxiliaryBarriers[0].Transition.pResource = impl->flatDepth12.get(); + auxiliaryBarriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + auxiliaryBarriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + auxiliaryBarriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + auxiliaryBarriers[1] = auxiliaryBarriers[0]; + auxiliaryBarriers[1].Transition.pResource = impl->responsiveMask12.get(); + UINT auxiliaryBarrierCount = 2; + if (!impl->enableOpticalFlow) { + D3D12_CPU_DESCRIPTOR_HANDLE motionCpu = depthCpu; + motionCpu.ptr += descriptorSize * 2; + D3D12_GPU_DESCRIPTOR_HANDLE motionGpu = depthGpu; + motionGpu.ptr += descriptorSize * 2; + D3D12_UNORDERED_ACCESS_VIEW_DESC motionUavDesc{}; + motionUavDesc.Format = DXGI_FORMAT_R16G16_FLOAT; + motionUavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + impl->device12->CreateUnorderedAccessView( + impl->zeroMotion12.get(), nullptr, &motionUavDesc, motionCpu); + const float zeroMotionValue[4]{}; + impl->commandList12->ClearUnorderedAccessViewFloat( + motionGpu, motionCpu, impl->zeroMotion12.get(), zeroMotionValue, 0, nullptr); + auxiliaryBarriers[2] = auxiliaryBarriers[0]; + auxiliaryBarriers[2].Transition.pResource = impl->zeroMotion12.get(); + auxiliaryBarrierCount = 3; + } + impl->commandList12->ResourceBarrier(auxiliaryBarrierCount, auxiliaryBarriers); + hr = impl->commandList12->Close(); + if (FAILED(hr)) { + Logger::Get().ComError("Close XeSS initialization command list failed", hr); + return false; + } + ID3D12CommandList* commandLists[] = { impl->commandList12.get() }; + impl->queue12->ExecuteCommandLists(1, commandLists); + + hr = impl->device11->CreateFence(0, D3D11_FENCE_FLAG_SHARED, IID_PPV_ARGS(impl->fence11.put())); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS shared fence failed", hr); + return false; + } + HANDLE rawFenceHandle = nullptr; + hr = impl->fence11->CreateSharedHandle(nullptr, GENERIC_ALL, nullptr, &rawFenceHandle); + if (FAILED(hr)) { + Logger::Get().ComError("Create XeSS fence shared handle failed", hr); + return false; + } + wil::unique_handle fenceHandle(rawFenceHandle); + hr = impl->device12->OpenSharedHandle(fenceHandle.get(), IID_PPV_ARGS(impl->fence12.put())); + if (FAILED(hr) || !WaitForD3D12(*impl)) { + if (FAILED(hr)) Logger::Get().ComError("Open XeSS fence in D3D12 failed", hr); + return false; + } + + const xess_result_t createResult = xessD3D12CreateContext(impl->device12.get(), &impl->xessContext); + if (!XessSucceeded(createResult, "xessD3D12CreateContext")) return false; + const xess_quality_settings_t quality = SelectQuality(scale); + xess_d3d12_init_params_t initParams{}; + initParams.outputResolution = { outputDesc.Width, outputDesc.Height }; + initParams.qualitySetting = quality; + initParams.initFlags = XESS_INIT_FLAG_LDR_INPUT_COLOR | XESS_INIT_FLAG_RESPONSIVE_PIXEL_MASK; + if (!impl->enableOpticalFlow) initParams.initFlags |= XESS_INIT_FLAG_HIGH_RES_MV; + if (!XessSucceeded(xessD3D12Init(impl->xessContext, &initParams), "xessD3D12Init") || + !XessSucceeded(xessSetVelocityScale(impl->xessContext, 1.0f, 1.0f), "xessSetVelocityScale")) { + xessDestroyContext(impl->xessContext); + impl->xessContext = nullptr; + return false; + } + + Logger::Get().Info(fmt::format( + "XeSS experimental D3D11/D3D12 backend initialized (quality {}, {}, jitter={}, BGRA conversion={}): {}x{} -> {}x{}", + (int)quality, impl->enableOpticalFlow ? "OpticalFlow50" : "Zero-MV", impl->enableJitter, + impl->convertInputToRgba, + inputDesc.Width, inputDesc.Height, outputDesc.Width, outputDesc.Height)); + _impl = std::move(impl); + return true; +} + +bool XeSSZeroMVUpscaler::Resize( + DeviceResources& deviceResources, + ID3D11Texture2D* input, + ID3D11Texture2D* output +) noexcept { + return Initialize(deviceResources, input, output, _enableOpticalFlow, _enableJitter); +} + +static float Halton(uint32_t index, uint32_t base) noexcept { + float result = 0.0f; + float fraction = 1.0f; + while (index) { + fraction /= (float)base; + result += fraction * (float)(index % base); + index /= base; + } + return result; +} + +bool XeSSZeroMVUpscaler::Draw(const NativeEffectDrawContext& drawContext) noexcept { + ID3D11Texture2D* input = drawContext.input; + ID3D11Texture2D* output = drawContext.output; + if (!_impl || !_impl->xessContext) return false; + Impl& impl = *_impl; + // A command allocator cannot be reset while its previous D3D12 submission + // is still executing. Usually the D3D11 consumer has already waited for it. + if (!WaitForFenceValue(impl, impl.lastSubmittedValue)) return false; + if (impl.convertInputToRgba) { + ID3D11ShaderResourceView* inputSrv = impl.inputSrv11.get(); + ID3D11UnorderedAccessView* outputUav = impl.sharedInputUav11.get(); + impl.context11->CSSetShader(impl.colorConvertShader11.get(), nullptr, 0); + impl.context11->CSSetShaderResources(0, 1, &inputSrv); + impl.context11->CSSetUnorderedAccessViews(0, 1, &outputUav, nullptr); + impl.context11->Dispatch((impl.inputWidth + 7) / 8, (impl.inputHeight + 7) / 8, 1); + ID3D11ShaderResourceView* nullSrv = nullptr; + ID3D11UnorderedAccessView* nullUav = nullptr; + impl.context11->CSSetShaderResources(0, 1, &nullSrv); + impl.context11->CSSetUnorderedAccessViews(0, 1, &nullUav, nullptr); + impl.context11->CSSetShader(nullptr, nullptr, 0); + } else { + impl.context11->CopyResource(impl.sharedInput11.get(), input); + } + if (impl.enableOpticalFlow) { + if (!impl.opticalFlow->Estimate(input)) { + Logger::Get().Error("Estimate XeSS 50% optical flow failed"); + return false; + } + impl.context11->CopyResource(impl.sharedMotion11.get(), impl.opticalFlow->GetMotionTexture()); + } + const uint64_t inputReady = ++impl.fenceValue; + HRESULT hr = impl.context11->Signal(impl.fence11.get(), inputReady); + if (FAILED(hr)) { + Logger::Get().ComError("Signal XeSS input-ready fence failed", hr); + return false; + } + impl.context11->Flush(); + hr = impl.queue12->Wait(impl.fence12.get(), inputReady); + if (FAILED(hr)) { + Logger::Get().ComError("Wait for XeSS D3D11 input failed", hr); + return false; + } + + hr = impl.allocator12->Reset(); + if (SUCCEEDED(hr)) hr = impl.commandList12->Reset(impl.allocator12.get(), nullptr); + if (FAILED(hr)) { + Logger::Get().ComError("Reset XeSS command list failed", hr); + return false; + } + D3D12_RESOURCE_BARRIER barriers[3]{}; + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition.pResource = impl.sharedInput12.get(); + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition.pResource = impl.sharedOutput12.get(); + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + const UINT barrierCount = impl.enableOpticalFlow ? 3 : 2; + if (impl.enableOpticalFlow) { + barriers[2].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[2].Transition.pResource = impl.sharedMotion12.get(); + barriers[2].Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON; + barriers[2].Transition.StateAfter = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE; + barriers[2].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + } + impl.commandList12->ResourceBarrier(barrierCount, barriers); + + xess_d3d12_execute_params_t params{}; + params.pColorTexture = impl.sharedInput12.get(); + params.pVelocityTexture = impl.enableOpticalFlow ? impl.sharedMotion12.get() : impl.zeroMotion12.get(); + params.pDepthTexture = impl.flatDepth12.get(); + params.pResponsivePixelMaskTexture = impl.responsiveMask12.get(); + params.pOutputTexture = impl.sharedOutput12.get(); + if (impl.enableJitter) { + // Metadata-only jitter: the source application's projection is unchanged. + const uint32_t sample = (impl.frameIndex++ & 7u) + 1u; + params.jitterOffsetX = Halton(sample, 2) - 0.5f; + params.jitterOffsetY = Halton(sample, 3) - 0.5f; + } else { + params.jitterOffsetX = 0.0f; + params.jitterOffsetY = 0.0f; + } + params.exposureScale = 1.0f; + params.resetHistory = impl.resetHistory ? 1u : 0u; + params.inputWidth = impl.inputWidth; + params.inputHeight = impl.inputHeight; + const xess_result_t result = xessD3D12Execute(impl.xessContext, impl.commandList12.get(), ¶ms); + if (!XessSucceeded(result, "xessD3D12Execute")) return false; + std::swap(barriers[0].Transition.StateBefore, barriers[0].Transition.StateAfter); + std::swap(barriers[1].Transition.StateBefore, barriers[1].Transition.StateAfter); + if (impl.enableOpticalFlow) { + std::swap(barriers[2].Transition.StateBefore, barriers[2].Transition.StateAfter); + } + impl.commandList12->ResourceBarrier(barrierCount, barriers); + hr = impl.commandList12->Close(); + if (FAILED(hr)) { + Logger::Get().ComError("Close XeSS command list failed", hr); + return false; + } + ID3D12CommandList* lists[] = { impl.commandList12.get() }; + impl.queue12->ExecuteCommandLists(1, lists); + const uint64_t outputReady = ++impl.fenceValue; + hr = impl.queue12->Signal(impl.fence12.get(), outputReady); + impl.lastSubmittedValue = outputReady; + if (SUCCEEDED(hr)) hr = impl.context11->Wait(impl.fence11.get(), outputReady); + if (FAILED(hr)) { + Logger::Get().ComError("Synchronize XeSS output failed", hr); + return false; + } + impl.context11->CopyResource(output, impl.sharedOutput11.get()); + impl.resetHistory = false; + return true; +} + +} + +#else + +namespace Magpie { + +struct XeSSZeroMVUpscaler::Impl {}; +XeSSZeroMVUpscaler::XeSSZeroMVUpscaler() = default; +XeSSZeroMVUpscaler::~XeSSZeroMVUpscaler() = default; +bool XeSSZeroMVUpscaler::Initialize(DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*, bool, bool) noexcept { + Logger::Get().Error("XeSS Zero-MV support is not enabled in this build"); + return false; +} +bool XeSSZeroMVUpscaler::Resize(DeviceResources&, ID3D11Texture2D*, ID3D11Texture2D*) noexcept { return false; } +bool XeSSZeroMVUpscaler::Draw(const NativeEffectDrawContext&) noexcept { return false; } + +} + +#endif diff --git a/src/Magpie.Core/XeSSZeroMVUpscaler.h b/src/Magpie.Core/XeSSZeroMVUpscaler.h new file mode 100644 index 000000000..f62af918a --- /dev/null +++ b/src/Magpie.Core/XeSSZeroMVUpscaler.h @@ -0,0 +1,41 @@ +#pragma once +#include "NativeEffectBackend.h" + +namespace Magpie { + +class DeviceResources; + +// Experimental colour-only XeSS-SR adapter. Magpie renders with D3D11, while +// the cross-vendor XeSS path is D3D12, so resources are shared between APIs. +class XeSSZeroMVUpscaler final : public NativeEffectBackend { +public: + struct Impl; + + XeSSZeroMVUpscaler(); + XeSSZeroMVUpscaler(const XeSSZeroMVUpscaler&) = delete; + XeSSZeroMVUpscaler& operator=(const XeSSZeroMVUpscaler&) = delete; + ~XeSSZeroMVUpscaler() override; + + bool Initialize( + DeviceResources& deviceResources, + ID3D11Texture2D* input, + ID3D11Texture2D* output, + bool enableOpticalFlow = false, + bool enableJitter = false + ) noexcept; + + bool Resize( + DeviceResources& deviceResources, + ID3D11Texture2D* input, + ID3D11Texture2D* output + ) noexcept override; + + bool Draw(const NativeEffectDrawContext& context) noexcept override; + +private: + std::unique_ptr _impl; + bool _enableOpticalFlow = false; + bool _enableJitter = false; +}; + +} diff --git a/src/Magpie.Core/ZeroFrameGuidanceProvider.cpp b/src/Magpie.Core/ZeroFrameGuidanceProvider.cpp index a936a3c1f..082571095 100644 --- a/src/Magpie.Core/ZeroFrameGuidanceProvider.cpp +++ b/src/Magpie.Core/ZeroFrameGuidanceProvider.cpp @@ -3,6 +3,7 @@ #include "DeviceResources.h" #include "DirectXHelper.h" #include "Logger.h" +#include "ScalingWindow.h" namespace Magpie { @@ -73,7 +74,10 @@ bool ZeroFrameGuidanceResources::_CreateTextures( } static constexpr float ZERO[4]{}; - _context->ClearUnorderedAccessViewFloat(depthUav.get(), ZERO); + static constexpr float ONE[4]{ 1.0f, 1.0f, 1.0f, 1.0f }; + const float* depthClear = ScalingWindow::Get().Options().IsHdrCompatibilityEnabled() + ? ONE : ZERO; + _context->ClearUnorderedAccessViewFloat(depthUav.get(), depthClear); _context->ClearUnorderedAccessViewFloat(motionUav.get(), ZERO); _context->ClearUnorderedAccessViewFloat(confidenceUav.get(), ZERO); _depth = std::move(depth); @@ -89,6 +93,9 @@ static FrameGuidanceMetadata MakeZeroMetadata( ) noexcept { return { .frameId = frame.frameId, + .captureSequence = frame.captureSequence, + .resourceGeneration = frame.resourceGeneration, + .timestamp100ns = frame.timestamp100ns, .sourceExtent = frame.sourceExtent, .validRegion = frame.validRegion, .resetReason = resetReason, diff --git a/src/Magpie.Core/include/EffectCompiler.h b/src/Magpie.Core/include/EffectCompiler.h index 6a49880ec..a9c3c5f83 100644 --- a/src/Magpie.Core/include/EffectCompiler.h +++ b/src/Magpie.Core/include/EffectCompiler.h @@ -7,6 +7,12 @@ struct EffectCompilerFlags { // 会影响编译出的字节码的标志放在低 16 位中,这样组织是为了便于缓存 static constexpr uint32_t InlineParams = 1; static constexpr uint32_t NoFP16 = 1 << 1; + // Optional primary-surface format overrides. Values are stored in the + // compile flags so effect-cache entries remain distinct per typed contract. + static constexpr uint32_t InputFormatShift = 2; + static constexpr uint32_t OutputFormatShift = 7; + static constexpr uint32_t SurfaceFormatMask = 0x1Fu; + static constexpr uint32_t HdrCompatibility = 1u << 12; // 只解析输出尺寸和参数,供用户界面使用 static constexpr uint32_t NoCompile = 1 << 16; diff --git a/src/Magpie.Core/include/NgxRuntimeGuard.h b/src/Magpie.Core/include/NgxRuntimeGuard.h index 2d8edad8e..4266a05cd 100644 --- a/src/Magpie.Core/include/NgxRuntimeGuard.h +++ b/src/Magpie.Core/include/NgxRuntimeGuard.h @@ -5,8 +5,6 @@ namespace Magpie { -// An SEH can bypass NGX's internal unlocks. This state belongs to the process, -// not a Renderer, and deliberately has no reset/retry operation. class NgxRuntimeGuard { public: static bool IsFaulted() noexcept { return _faultCode.load(std::memory_order_acquire) != 0; } @@ -16,8 +14,6 @@ class NgxRuntimeGuard { template static Result Invoke(Function&& function, Result failure, DWORD* sehCode) noexcept { - // Serialize the availability check with SDK entry and fault publication. - // This lock is outside the SEH frame, so ordinary C++ cleanup releases it. std::lock_guard lock(_callMutex); *sehCode = 0; if (IsFaulted()) return failure; diff --git a/src/Magpie.Core/include/OverlayWindowGeometry.h b/src/Magpie.Core/include/OverlayWindowGeometry.h index a04e3dd62..4be5d22df 100644 --- a/src/Magpie.Core/include/OverlayWindowGeometry.h +++ b/src/Magpie.Core/include/OverlayWindowGeometry.h @@ -6,21 +6,14 @@ namespace Magpie { struct OverlayWindowOption { - // 0: distance from the leading edge in DIPs; 1: proportional center; - // 2: distance from the trailing edge in DIPs. uint16_t hArea = 0; uint16_t vArea = 0; float hPos = 0.0f; float vPos = 0.0f; - // Preferred expanded size in DIPs. Zero keeps the default for old settings. float width = 0.0f; float height = 0.0f; }; -struct OverlayWindowRect { - float x, y, width, height; -}; - inline void SanitizeOverlayWindowOption(OverlayWindowOption& option) noexcept { auto sanitizeAxis = [](uint16_t& area, float& pos) { if (area > 2) area = 0; @@ -33,6 +26,10 @@ inline void SanitizeOverlayWindowOption(OverlayWindowOption& option) noexcept { if (!std::isfinite(option.height) || option.height < 0.0f) option.height = 0.0f; } +struct OverlayWindowRect { + float x, y, width, height; +}; + inline OverlayWindowRect RestoreEffectParametersWindow( OverlayWindowOption option, float viewportWidth, float viewportHeight, float dpiScale ) noexcept { diff --git a/src/Magpie.Core/include/ScalingOptions.h b/src/Magpie.Core/include/ScalingOptions.h index dea90fe79..7b66be03c 100644 --- a/src/Magpie.Core/include/ScalingOptions.h +++ b/src/Magpie.Core/include/ScalingOptions.h @@ -261,7 +261,6 @@ enum class ToolbarState { COUNT }; -// Transient state for rebuilding the current effect group, not global defaults. struct OverlaySessionState { bool toolbarVisible = false; bool toolbarPinned = false; @@ -386,6 +385,7 @@ struct ScalingFlags { static constexpr uint32_t BenchmarkMode = 1 << 20; static constexpr uint32_t DeveloperMode = 1 << 21; static constexpr uint32_t DisableTopmost = 1 << 22; + static constexpr uint32_t EnableHdrCompatibility = 1 << 23; }; struct ScalingOptions { @@ -408,6 +408,7 @@ struct ScalingOptions { DEFINE_FLAG_ACCESSOR(IsCaptureTitleBar, ScalingFlags::CaptureTitleBar, flags) DEFINE_FLAG_ACCESSOR(IsAdjustCursorSpeed, ScalingFlags::AdjustCursorSpeed, flags) DEFINE_FLAG_ACCESSOR(IsDirectFlipDisabled, ScalingFlags::DisableDirectFlip, flags) + DEFINE_FLAG_ACCESSOR(IsHdrCompatibilityEnabled, ScalingFlags::EnableHdrCompatibility, flags) std::vector effects; uint32_t scalingModeIdx = 0; diff --git a/src/Magpie.Core/include/ScalingSessionLifetime.h b/src/Magpie.Core/include/ScalingSessionLifetime.h index 1ce917d9a..243880d0f 100644 --- a/src/Magpie.Core/include/ScalingSessionLifetime.h +++ b/src/Magpie.Core/include/ScalingSessionLifetime.h @@ -4,9 +4,6 @@ namespace Magpie { -// A queued callback owns this token, never the Renderer. The run ID is fixed -// at session creation; reading the global ID while reporting a late failure -// could accidentally assign that failure to the next session. class ScalingSessionLifetime { public: explicit ScalingSessionLifetime(uint32_t runId) noexcept : _runId(runId) {} diff --git a/src/Magpie.Core/include/ScalingWindowOwner.h b/src/Magpie.Core/include/ScalingWindowOwner.h index 7c808c51e..533157e4b 100644 --- a/src/Magpie.Core/include/ScalingWindowOwner.h +++ b/src/Magpie.Core/include/ScalingWindowOwner.h @@ -3,8 +3,6 @@ namespace Magpie { -// Call on the scaling window's thread. Keep windows unowned during potentially -// blocking initialization/teardown; input-queue detachment alone is insufficient. inline bool SetScalingWindowOwner(HWND window, HWND owner) noexcept { SetLastError(ERROR_SUCCESS); const LONG_PTR previous = SetWindowLongPtrW(window, GWLP_HWNDPARENT, diff --git a/src/Magpie.Core/include/Win32Helper.h b/src/Magpie.Core/include/Win32Helper.h index 1a693e78d..eebdfe5ed 100644 --- a/src/Magpie.Core/include/Win32Helper.h +++ b/src/Magpie.Core/include/Win32Helper.h @@ -19,6 +19,11 @@ struct Win32Helper { // 相比 \\.\DISPLAY1 之类的 GDI 名称,在显示器插拔和顺序变化后更稳定。 static std::vector GetDisplayMonitors() noexcept; + // Returns the Windows HDR SDR-white calibration for a monitor in nits. + // The value is unavailable on older paths or when the display driver does + // not expose the calibration; callers should keep their explicit fallback. + static float GetMonitorSdrWhiteNits(HMONITOR monitor) noexcept; + static SIZE GetSizeOfRect(const RECT& rect) noexcept { return { rect.right - rect.left, rect.bottom - rect.top }; } diff --git a/src/Magpie/AppSettings.cpp b/src/Magpie/AppSettings.cpp index 5040ddc3a..f971c4c82 100644 --- a/src/Magpie/AppSettings.cpp +++ b/src/Magpie/AppSettings.cpp @@ -123,6 +123,8 @@ static void WriteProfile(rapidjson::PrettyWriter& write writer.Bool(profile.IsAdjustCursorSpeed()); writer.Key("disableDirectFlip"); writer.Bool(profile.IsDirectFlipDisabled()); + writer.Key("enableHdrCompatibility"); + writer.Bool(profile.IsHdrCompatibilityEnabled()); writer.Key("cursorScaling"); writer.Uint((uint32_t)profile.cursorScaling); @@ -1229,6 +1231,7 @@ bool AppSettings::_LoadProfile( } JsonHelper::ReadBoolFlag(profileObj, "adjustCursorSpeed", ScalingFlags::AdjustCursorSpeed, profile.scalingFlags); JsonHelper::ReadBoolFlag(profileObj, "disableDirectFlip", ScalingFlags::DisableDirectFlip, profile.scalingFlags); + JsonHelper::ReadBoolFlag(profileObj, "enableHdrCompatibility", ScalingFlags::EnableHdrCompatibility, profile.scalingFlags); { uint32_t cursorScaling = (uint32_t)CursorScaling::NoScaling; diff --git a/src/Magpie/Magpie.vcxproj b/src/Magpie/Magpie.vcxproj index 1bd8fbf90..0e98bcb34 100644 --- a/src/Magpie/Magpie.vcxproj +++ b/src/Magpie/Magpie.vcxproj @@ -7,6 +7,7 @@ true true true + false low true {1239537c-e5b8-427a-9e7f-ea443d1f3529} @@ -18,8 +19,8 @@ 10.0 10.0.26100.0 10.0.26100.0 - $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ - $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(BuildRoot)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(BuildRoot)\bin\$(Platform)\$(Configuration)\ $(IntDir)\Generated Files\ x64|arm64 true @@ -879,3 +880,5 @@ Text="Release runtime layout is incomplete: $(OutDir)resources.pri was not generated. Do not deploy Magpie.exe by itself." /> + + diff --git a/src/Magpie/Profile.h b/src/Magpie/Profile.h index 69a85622d..4a4eadbde 100644 --- a/src/Magpie/Profile.h +++ b/src/Magpie/Profile.h @@ -112,6 +112,7 @@ struct Profile { DEFINE_FLAG_ACCESSOR(IsCaptureTitleBar, ScalingFlags::CaptureTitleBar, scalingFlags) DEFINE_FLAG_ACCESSOR(IsAdjustCursorSpeed, ScalingFlags::AdjustCursorSpeed, scalingFlags) DEFINE_FLAG_ACCESSOR(IsDirectFlipDisabled, ScalingFlags::DisableDirectFlip, scalingFlags) + DEFINE_FLAG_ACCESSOR(IsHdrCompatibilityEnabled, ScalingFlags::EnableHdrCompatibility, scalingFlags) // 默认规则 name、pathRule 和 classNameRule 均为空 std::wstring name; diff --git a/src/Magpie/ProfilePage.xaml b/src/Magpie/ProfilePage.xaml index dc52eed26..ac15a638f 100644 --- a/src/Magpie/ProfilePage.xaml +++ b/src/Magpie/ProfilePage.xaml @@ -56,6 +56,13 @@ + + + + + + IsHdrCompatibilityEnabled(); +} + +void ProfileViewModel::IsHdrCompatibilityEnabled(bool value) { + if (_data->IsHdrCompatibilityEnabled() == value) { + return; + } + + _data->IsHdrCompatibilityEnabled(value); + AppSettings::Get().SaveAsync(); + + RaisePropertyChanged(L"IsHdrCompatibilityEnabled"); +} + bool ProfileViewModel::HasMultipleMonitors() const noexcept { return GetSystemMetrics(SM_CMONITORS) > 1; } diff --git a/src/Magpie/ProfileViewModel.h b/src/Magpie/ProfileViewModel.h index 5c4b22a3c..80fc12814 100644 --- a/src/Magpie/ProfileViewModel.h +++ b/src/Magpie/ProfileViewModel.h @@ -79,6 +79,9 @@ struct ProfileViewModel : ProfileViewModelT, bool Is3DGameMode() const noexcept; void Is3DGameMode(bool value); + bool IsHdrCompatibilityEnabled() const noexcept; + void IsHdrCompatibilityEnabled(bool value); + bool HasMultipleMonitors() const noexcept; IVector MonitorOptions() const noexcept { diff --git a/src/Magpie/ProfileViewModel.idl b/src/Magpie/ProfileViewModel.idl index 30758a37d..8f9466a98 100644 --- a/src/Magpie/ProfileViewModel.idl +++ b/src/Magpie/ProfileViewModel.idl @@ -32,6 +32,7 @@ namespace Magpie { Int32 AutoScale; Boolean Is3DGameMode; + Boolean IsHdrCompatibilityEnabled; Boolean HasMultipleMonitors { get; }; IVector MonitorOptions { get; }; diff --git a/src/Magpie/Resources.language-de.resw b/src/Magpie/Resources.language-de.resw index 6745efffb..509af8aa8 100644 --- a/src/Magpie/Resources.language-de.resw +++ b/src/Magpie/Resources.language-de.resw @@ -237,6 +237,9 @@ 3D Spielmodus + + HDR compatibility + Automatisch skalieren, wenn im Vordergrund @@ -931,6 +934,9 @@ Aktiviert Optimierungen für 3D‑Spiele, ist aber nicht mit Symbolleiste und Fensterskalierung kompatibel + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Vollbildskalierung diff --git a/src/Magpie/Resources.language-en-US.resw b/src/Magpie/Resources.language-en-US.resw index 8632cb98c..225cbc8d2 100644 --- a/src/Magpie/Resources.language-en-US.resw +++ b/src/Magpie/Resources.language-en-US.resw @@ -426,6 +426,9 @@ 3D game mode + + HDR compatibility + Automatically enable effect group when in foreground @@ -946,6 +949,9 @@ Enables optimizations for 3D games, but is incompatible with the toolbar and windowed mode + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Screenshot save directory diff --git a/src/Magpie/Resources.language-es.resw b/src/Magpie/Resources.language-es.resw index 1c022b438..d222f4e80 100644 --- a/src/Magpie/Resources.language-es.resw +++ b/src/Magpie/Resources.language-es.resw @@ -315,6 +315,9 @@ Modo de juego 3D + + HDR compatibility + Escalar de manera automática cuando está en primer plano @@ -895,6 +898,9 @@ Activa optimizaciones para juegos 3D, pero no es compatible con la barra de herramientas y el escalado en modo ventana + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Ruta de guardado de capturas de pantalla diff --git a/src/Magpie/Resources.language-fi.resw b/src/Magpie/Resources.language-fi.resw index 2490364e9..255f6c5d9 100644 --- a/src/Magpie/Resources.language-fi.resw +++ b/src/Magpie/Resources.language-fi.resw @@ -343,6 +343,9 @@ 3D pelitila + + HDR compatibility + Aktiivisen ruudun automaattinen skaalaus @@ -854,6 +857,9 @@ Mahdollistaa 3D-pelien optimoinnit, mutta ei ole yhteensopiva työkalupalkin ja ikkunoidun skaalauksen kanssa + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Kuvaruudunkaappausten tallennushakemisto diff --git a/src/Magpie/Resources.language-fr.resw b/src/Magpie/Resources.language-fr.resw index 76ee28bce..c6ce153b7 100644 --- a/src/Magpie/Resources.language-fr.resw +++ b/src/Magpie/Resources.language-fr.resw @@ -406,6 +406,9 @@ Mode jeux 3D + + HDR compatibility + Pas de mise à l'échelle @@ -910,6 +913,9 @@ Permet des optimisations pour les jeux 3D, mais est incompatible avec la barre d'outils et la mise à l'échelle en mode fenêtré + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Répertoire de sauvegarde des captures d'écran diff --git a/src/Magpie/Resources.language-hu.resw b/src/Magpie/Resources.language-hu.resw index 611d6ea29..1fc4c89e7 100644 --- a/src/Magpie/Resources.language-hu.resw +++ b/src/Magpie/Resources.language-hu.resw @@ -364,4 +364,10 @@ Magpie a főablak bezárása után is a háttérben fog futni - \ No newline at end of file + + HDR-kompatibilitás + + + Engedélyezi a kísérleti HDR-rögzítés és effektusok közötti kompatibilitási útvonalat. Csak HDR-támogatás tesztelésekor kapcsold be. + + diff --git a/src/Magpie/Resources.language-id.resw b/src/Magpie/Resources.language-id.resw index 169447a81..ed5ad65e8 100644 --- a/src/Magpie/Resources.language-id.resw +++ b/src/Magpie/Resources.language-id.resw @@ -369,6 +369,9 @@ Mode 3D game + + HDR compatibility + Skala otomatis ketika berada di latar depan @@ -913,6 +916,9 @@ Mengaktifkan optimasi untuk game 3D, tetapi tidak kompatibel dengan bilah alat dan penskalaan berjendela + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Direktori penyimpanan tangkapan layar diff --git a/src/Magpie/Resources.language-it.resw b/src/Magpie/Resources.language-it.resw index 2548c669e..5db9cfbcb 100644 --- a/src/Magpie/Resources.language-it.resw +++ b/src/Magpie/Resources.language-it.resw @@ -324,6 +324,12 @@ Modalità gioco 3D + + HDR compatibility + + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Imposta il fattore di ridimensionamento dopo aver riempito lo schermo con il ridimensionamento proporzionale diff --git a/src/Magpie/Resources.language-ja.resw b/src/Magpie/Resources.language-ja.resw index efcc65059..bad6e8e88 100644 --- a/src/Magpie/Resources.language-ja.resw +++ b/src/Magpie/Resources.language-ja.resw @@ -589,6 +589,9 @@ 3Dゲームモード + + HDR compatibility + 最前面時自動スケーリング @@ -910,6 +913,9 @@ 3Dゲームの最適化を可能にしますが、ツールバーやウィンドウ・スケーリングとの互換性はありません + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + スクリーンショットの保存先 diff --git a/src/Magpie/Resources.language-ka.resw b/src/Magpie/Resources.language-ka.resw index 4ade50e89..8ff42d355 100644 --- a/src/Magpie/Resources.language-ka.resw +++ b/src/Magpie/Resources.language-ka.resw @@ -376,4 +376,10 @@ სხვა ბმულები - \ No newline at end of file + + HDR თავსებადობა + + + ჩართავს ექსპერიმენტულ HDR გადაღება-ეფექტის თავსებადობის გზას. გამორთული დატოვეთ, თუ HDR მხარდაჭერას არ ამოწმებთ. + + diff --git a/src/Magpie/Resources.language-ko.resw b/src/Magpie/Resources.language-ko.resw index 97aa26b56..95fdd4592 100644 --- a/src/Magpie/Resources.language-ko.resw +++ b/src/Magpie/Resources.language-ko.resw @@ -315,6 +315,9 @@ 3D 게임 모드 + + HDR compatibility + 모든 모니터 @@ -877,6 +880,9 @@ 3D 게임 최적화 기능으로 활성화하지만, 툴바 및 창 크기 조정 기능과 호환되지 않습니다 + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + 스크린샷 저장 경로 diff --git a/src/Magpie/Resources.language-pl.resw b/src/Magpie/Resources.language-pl.resw index b83751963..3a8ba3ead 100644 --- a/src/Magpie/Resources.language-pl.resw +++ b/src/Magpie/Resources.language-pl.resw @@ -154,6 +154,9 @@ Tryb gry 3D + + HDR compatibility + Automatycznie skaluj, gdy okno jest na pierwszym planie @@ -854,6 +857,9 @@ Włącza optymalizacje dla gier 3D, ale nie zadziała z paskiem narzędzi i skalowaniem okien + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Katalog zapisu zrzutów ekranu diff --git a/src/Magpie/Resources.language-pt-BR.resw b/src/Magpie/Resources.language-pt-BR.resw index e962172fd..c7909af79 100644 --- a/src/Magpie/Resources.language-pt-BR.resw +++ b/src/Magpie/Resources.language-pt-BR.resw @@ -639,6 +639,12 @@ Modo de jogo 3D + + HDR compatibility + + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Maior proximidade diff --git a/src/Magpie/Resources.language-ru.resw b/src/Magpie/Resources.language-ru.resw index f392cc294..3cd84380c 100644 --- a/src/Magpie/Resources.language-ru.resw +++ b/src/Magpie/Resources.language-ru.resw @@ -429,6 +429,9 @@ Режим 3D игры + + HDR compatibility + Автоматическое масштабирование когда на переднем плане @@ -910,6 +913,9 @@ Включает оптимизации под 3D-игры, но несовместимо с панелью инструментов и масштабированием окна + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Директория сохранения скриншотов diff --git a/src/Magpie/Resources.language-ta.resw b/src/Magpie/Resources.language-ta.resw index c67782242..544b86401 100644 --- a/src/Magpie/Resources.language-ta.resw +++ b/src/Magpie/Resources.language-ta.resw @@ -453,6 +453,9 @@ 3D விளையாட்டு முறை + + HDR compatibility + முன்புறத்தில் இருக்கும்போது ஆட்டோ அளவுகோல் @@ -913,6 +916,9 @@ 3D கேம்களுக்கான மேம்படுத்தல்களை இயக்குகிறது, ஆனால் கருவிப்பட்டி மற்றும் சாளர அளவீடு ஆகியவற்றுடன் பொருந்தாது + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + திரைக்காட்சி சேமிப்பு அடைவு diff --git a/src/Magpie/Resources.language-tr.resw b/src/Magpie/Resources.language-tr.resw index 838828460..56cdd46b2 100644 --- a/src/Magpie/Resources.language-tr.resw +++ b/src/Magpie/Resources.language-tr.resw @@ -420,6 +420,9 @@ 3D oyun kipi + + HDR compatibility + Ön planda otomatik ölçeklendir @@ -913,6 +916,9 @@ 3D oyunlar için optimizasyonları etkinleştirir, ancak araç çubuğu ve pencereli ölçeklendirme ile uyumsuzdur + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Ekran görüntüsü kaydetme dizini diff --git a/src/Magpie/Resources.language-uk.resw b/src/Magpie/Resources.language-uk.resw index f04255906..5b628dfaa 100644 --- a/src/Magpie/Resources.language-uk.resw +++ b/src/Magpie/Resources.language-uk.resw @@ -333,6 +333,12 @@ Режим 3D гри + + HDR compatibility + + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Автоматичне масштабування на передньому плані diff --git a/src/Magpie/Resources.language-vi.resw b/src/Magpie/Resources.language-vi.resw index 3d50ed4c4..263c7a57a 100644 --- a/src/Magpie/Resources.language-vi.resw +++ b/src/Magpie/Resources.language-vi.resw @@ -361,6 +361,9 @@ Chế độ game 3D + + HDR compatibility + Tự động scale cửa sổ trên cùng @@ -940,6 +943,9 @@ Cho phép tối ưu hóa cho trò chơi 3D, nhưng không tương thích với thanh công cụ và tỷ lệ cửa sổ + + Enable the experimental HDR capture-to-effect compatibility path. Keep disabled unless you are testing HDR support + Thư mục lưu ảnh chụp màn hình diff --git a/src/Magpie/Resources.language-zh-Hans.resw b/src/Magpie/Resources.language-zh-Hans.resw index 2ab639964..8766d6d1d 100644 --- a/src/Magpie/Resources.language-zh-Hans.resw +++ b/src/Magpie/Resources.language-zh-Hans.resw @@ -426,6 +426,9 @@ 3D 游戏模式 + + HDR 兼容 + 位于前台时自动启用效果组 @@ -946,6 +949,9 @@ 针对 3D 游戏优化,不支持工具栏和窗口模式 + + 启用实验性 HDR 捕获到效果兼容路径。除非正在测试 HDR 支持,请保持关闭 + 截图保存目录 diff --git a/src/Magpie/Resources.language-zh-Hant.resw b/src/Magpie/Resources.language-zh-Hant.resw index 0cdcd6062..640db8f03 100644 --- a/src/Magpie/Resources.language-zh-Hant.resw +++ b/src/Magpie/Resources.language-zh-Hant.resw @@ -384,6 +384,9 @@ 3D 遊戲模式 + + HDR 相容 + 位於前景時自動啟用效果組 @@ -919,6 +922,9 @@ 針對 3D 遊戲最佳化,不支援工具列和視窗模式 + + 啟用實驗性 HDR 擷取到效果相容路徑。除非正在測試 HDR 支援,請保持關閉 + 截圖儲存目錄 diff --git a/src/Magpie/ScalingModesService.cpp b/src/Magpie/ScalingModesService.cpp index c0463dddd..b32ed466f 100644 --- a/src/Magpie/ScalingModesService.cpp +++ b/src/Magpie/ScalingModesService.cpp @@ -315,7 +315,7 @@ static V065NormalizationStats NormalizeV065ScalingModes( auto it = effect.parameters.find(name); if (it == effect.parameters.end()) continue; const float clamped = std::isfinite(it->second) ? - std::clamp(it->second, 0.0f, 1.0f) : 1.0f; + std::clamp(it->second, 0.0f, 2.0f) : 1.0f; if (clamped != it->second) { it->second = clamped; ++stats.clampedParameters; diff --git a/src/TouchHelper/TouchHelper.vcxproj b/src/TouchHelper/TouchHelper.vcxproj index 3593d2fa4..d144a0954 100644 --- a/src/TouchHelper/TouchHelper.vcxproj +++ b/src/TouchHelper/TouchHelper.vcxproj @@ -4,8 +4,8 @@ Win32Proj {05b51bb8-08cb-4907-884f-8e2ad6bf6052} 10.0.26100.0 - $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ - $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(BuildRoot)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(BuildRoot)\bin\$(Platform)\$(Configuration)\ @@ -65,4 +65,4 @@ - \ No newline at end of file + diff --git a/src/Updater/Updater.vcxproj b/src/Updater/Updater.vcxproj index 9d71741f6..5419bef9a 100644 --- a/src/Updater/Updater.vcxproj +++ b/src/Updater/Updater.vcxproj @@ -4,8 +4,8 @@ Win32Proj {e82b7a20-0557-4dc1-b418-87977d7450a4} 10.0.26100.0 - $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ - $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(BuildRoot)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(BuildRoot)\bin\$(Platform)\$(Configuration)\ @@ -67,4 +67,4 @@ - \ No newline at end of file + diff --git a/src/_ConanDeps/_ConanDeps.vcxproj b/src/_ConanDeps/_ConanDeps.vcxproj index 0b31ba54c..6b4ae701e 100644 --- a/src/_ConanDeps/_ConanDeps.vcxproj +++ b/src/_ConanDeps/_ConanDeps.vcxproj @@ -4,8 +4,8 @@ Win32Proj {456ccae4-2c51-4cf2-8d3a-1efce8c41a2d} 10.0.26100.0 - $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ - $(SolutionDir)\bin\$(Platform)\$(Configuration)\ + $(BuildRoot)\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)\ + $(BuildRoot)\bin\$(Platform)\$(Configuration)\ diff --git a/tests/HdrMechanicalTests.cpp b/tests/HdrMechanicalTests.cpp new file mode 100644 index 000000000..16edfefd6 --- /dev/null +++ b/tests/HdrMechanicalTests.cpp @@ -0,0 +1,318 @@ +// Lightweight unit-style validation for the HDR mechanical slice. +// +// This file is intentionally kept independent of the UI/configuration projects +// so it can be compiled as a small console executable when a test runner is +// available. It validates protocol route data and the adapter dispatcher plan. +// Configuration round-trip and source-level invariants are covered by +// scripts\Run-HdrMechanicalValidation.ps1 and by Magpie's normal build. +// +// Suggested compile shape (after MSVC environment is set up): +// cl /std:c++17 /EHsc /I src\Magpie.Core src\Magpie.Core\HdrFrame.cpp ^ +// src\Magpie.Core\HdrProtocol.cpp src\Magpie.Core\HdrColorTransform.cpp ^ +// src\Magpie.Core\HdrAdapterDispatcher.cpp tests\HdrMechanicalTests.cpp + +#include "HdrAdapterDispatcher.h" +#include "EffectProtocolCatalogC.h" +#include "HdrColorTransform.h" +#include "HdrFrame.h" +#include "HdrProtocol.h" + +#include +#include +#include +#include +#include + +using namespace Magpie; + +namespace { + +int g_failures = 0; + +void Check(bool condition, const char* message) { + if (!condition) { + std::printf("FAIL: %s\n", message); + ++g_failures; + } else { + std::printf("PASS: %s\n", message); + } +} + +HdrFormatRoute MakeRoute(HdrAdapterProfile profile) { + HdrFormatRoute route; + route.effectId = "TestEffect"; + route.optionId = profile == HdrAdapterProfile::Unknown ? "UnknownRoute" : "Route"; + route.inputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + route.outputFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; + route.inputTransfer = HdrTransferFunction::Linear; + route.outputTransfer = HdrTransferFunction::Linear; + route.inputRange = HdrColorRange::SceneLinear; + route.outputRange = HdrColorRange::SceneLinear; + route.alphaMode = HdrAlphaMode::Preserve; + route.evidenceLevel = HdrEvidenceLevel::LocalValidation; + route.adapterProfile = profile; + route.hdrNative = profile == HdrAdapterProfile::DirectFP16 || + profile == HdrAdapterProfile::BoundedHDR || + profile == HdrAdapterProfile::ConditionalFP16; + route.defaultForHdr = profile == HdrAdapterProfile::DirectFP16; + route.defaultForSdr = profile == HdrAdapterProfile::SDRCompatible; + return route; +} + +void TestRouteStorageAndCategories() { + HdrFormatRoute route = MakeRoute(HdrAdapterProfile::DirectFP16); + Check(route.Id() == "TestEffect/Route", "route id combines effectId and optionId"); + Check(route.IsValid(), "complete route is valid"); + Check(route.IsHdrNative(), "explicit hdrNative route is HDR-native"); + Check(!route.IsHdrAdapter(), "hdrNative route is not an adapter-only route"); + + HdrFormatRoute r8Sdr = MakeRoute(HdrAdapterProfile::SDRCompatible); + r8Sdr.inputFormat = DXGI_FORMAT_R8G8B8A8_UNORM; + r8Sdr.outputFormat = DXGI_FORMAT_R8G8B8A8_UNORM; + r8Sdr.hdrNative = false; + Check(r8Sdr.IsHdrAdapter(), "R8 SDRCompatible route is an adapter route"); + Check(!r8Sdr.IsHdrNative(), "R8 route is not automatically HDR-native"); + + HdrFormatRoute fp16WithoutFlag = MakeRoute(HdrAdapterProfile::DirectFP16); + fp16WithoutFlag.hdrNative = false; + Check(!fp16WithoutFlag.IsHdrNative(), "FP16 route without explicit hdrNative is not HDR-native"); + + HdrFormatRoute r10 = MakeRoute(HdrAdapterProfile::BoundedHDR); + r10.inputFormat = DXGI_FORMAT_R10G10B10A2_UNORM; + r10.outputFormat = DXGI_FORMAT_R10G10B10A2_UNORM; + r10.inputTransfer = HdrTransferFunction::PQ; + r10.outputTransfer = HdrTransferFunction::PQ; + r10.hdrNative = true; + Check(r10.IsHdrNative(), "R10 route is HDR-native only when PQ/BT.2100 semantics are explicit"); + + HdrFormatRoutes routes{ MakeRoute(HdrAdapterProfile::SDRCompatible), MakeRoute(HdrAdapterProfile::DirectFP16) }; + Check(SelectDefaultHdrRoute(routes) != nullptr, "HDR mode selects a default HDR route"); + Check(SelectDefaultHdrRoute(routes)->adapterProfile == HdrAdapterProfile::DirectFP16, + "HDR mode prefers DirectFP16 when marked defaultForHdr"); + Check(SelectDefaultSdrRoute(routes)->adapterProfile == HdrAdapterProfile::SDRCompatible, + "SDR mode keeps SDRCompatible default"); + Check(GetAcceptedFormatRoutes(routes).size() == 2, "accepted route list contains valid routes"); + Check(GetHdrNativeFormatRoutes(routes).size() == 1, "hdrNative route list is explicit only"); + Check(GetHdrAdapterFormatRoutes(routes).size() == 1, "hdrAdapter route list contains SDR route"); +} + +void TestDispatcherProfiles() { + HdrAdapterDispatcher dispatcher; + ColorDescription color{}; + color.referenceWhiteNits = 80.0f; + color.displayPeakNits = 1000.0f; + + const char* profileNames[] = { + "DirectFP16", "BoundedHDR", "SDRCompatible", + "ConditionalFP16", "Unknown", "PresentationTerminal" + }; + const HdrAdapterProfile profiles[] = { + HdrAdapterProfile::DirectFP16, HdrAdapterProfile::BoundedHDR, + HdrAdapterProfile::SDRCompatible, HdrAdapterProfile::ConditionalFP16, + HdrAdapterProfile::Unknown, HdrAdapterProfile::PresentationTerminal + }; + + for (size_t i = 0; i < std::size(profiles); ++i) { + HdrFormatRoute route = MakeRoute(profiles[i]); + const HdrAdapterPlan plan = dispatcher.BuildPlan(route, color); + Check(plan.profile == profiles[i], profileNames[i]); + } + + HdrFormatRoute direct = MakeRoute(HdrAdapterProfile::DirectFP16); + HdrAdapterPlan plan = dispatcher.BuildPlan(direct, color); + Check(plan.IsNonTerminalCanonical(), "DirectFP16 returns canonical FP16"); + Check(!plan.requiresSdrMapping && !plan.requiresBoundedMapping, + "DirectFP16 does not introduce SDR mapping"); + + HdrFormatRoute sdr = MakeRoute(HdrAdapterProfile::SDRCompatible); + plan = dispatcher.BuildPlan(sdr, color); + Check(plan.requiresSdrMapping, "SDRCompatible requires HDR-to-SDR mapping"); + Check(plan.forwardParameters.IsValid() && plan.inverseParameters.IsValid(), + "SDRCompatible carries paired forward/inverse parameters"); + + HdrFormatRoute bounded = MakeRoute(HdrAdapterProfile::BoundedHDR); + plan = dispatcher.BuildPlan(bounded, color); + Check(plan.requiresBoundedMapping, "BoundedHDR requires bounded encode/decode"); + Check(plan.forwardParameters.IsValid() && plan.inverseParameters.IsValid(), + "BoundedHDR carries paired forward/inverse parameters"); + + HdrFormatRoute unknown = MakeRoute(HdrAdapterProfile::Unknown); + unknown.alphaMode = HdrAlphaMode::Unknown; + plan = dispatcher.BuildPlan(unknown, color); + Check(plan.usesFallback, "Unknown route selects fallback"); + Check(!plan.fallbackReason.empty(), "Unknown route records fallback reason"); + Check(plan.alphaMode == HdrAlphaMode::ForceOpaque, "Unknown fallback makes alpha explicit"); + + HdrFormatRoute terminal = MakeRoute(HdrAdapterProfile::PresentationTerminal); + plan = dispatcher.BuildPlan(terminal, color); + Check(plan.isPresentationTerminal, "PresentationTerminal is terminal"); + Check(plan.canonicalOutputFormat == DXGI_FORMAT_UNKNOWN, + "PresentationTerminal does not promise a normal canonical output"); +} + +void TestHdrToneMapRoundTrip() { + HdrTransformParameters parameters{}; + parameters.sdrWhiteNits = 80.0f; + parameters.hdrPeakNits = 1000.0f; + parameters.shoulder = 1.0f; + + // The replicate contract is exact below the white point and collapses + // everything above it to saturation; the white point itself maps to the + // frame peak on the way back by design. + const float samples[] = { 0.0f, 0.25f, 0.5f, 0.75f, 0.999f }; + float previous = -1.0f; + for (float sample : samples) { + const float mapped = HdrColorTransform::MapHdrToSdr(sample, parameters); + Check(mapped >= previous, "HDR-to-SDR mapping is monotonic through highlights"); + previous = mapped; + const float reconstructed = HdrColorTransform::MapSdrToHdr(mapped, parameters); + Check(std::abs(reconstructed - sample) <= 1e-3f, + "HDR-to-SDR-to-HDR round trip preserves in-range luminance"); + } + + parameters.exposure = 0.5f; + const float mappedPeak = HdrColorTransform::MapHdrToSdr(12.5f, parameters); + const float reconstructedPeak = HdrColorTransform::MapSdrToHdr(mappedPeak, parameters); + Check(reconstructedPeak >= 11.0f, + "saturated peak restores to the frame peak with exposure"); +} + +void TestShoulderCurveFamily() { + HdrTransformParameters parameters{}; + parameters.sdrWhiteNits = 80.0f; + parameters.hdrPeakNits = 1000.0f; + parameters.exposure = 1.0f; + + // Identity below the SDR white point keeps SDR content bit-exact for + // both the replicate contract (SDR routes) and the bounded shoulder. + for (int i = 0; i <= 100; ++i) { + const float x = static_cast(i) / 100.0f; + Check(HdrColorTransform::MapHdrToSdr(x, parameters) == x, + "SDR-route mapping is the identity on [0, 1]"); + Check(HdrColorTransform::EncodeBoundedHdr(x, parameters) == x, + "bounded mapping is the identity on [0, 1]"); + } + + // The replicate contract saturates above the white point and restores + // saturated whites to the frame peak on the way back. + Check(HdrColorTransform::MapHdrToSdr(5.0f, parameters) == 1.0f, + "SDR-route mapping saturates HDR highlights"); + Check(HdrColorTransform::MapSdrToHdr(1.0f, parameters) >= 12.0f, + "SDR-route inverse restores saturated whites to the frame peak"); + // A 360-nit display (sdrWhite == peak) must restore saturated whites to + // the white point itself, never above it: the earlier formula produced + // (peak/80)*(sdrWhite/80) = 20.25 canonical (1620 nit) and leaked yellow. + { + HdrTransformParameters display{}; + display.sdrWhiteNits = 360.0f; + display.hdrPeakNits = 360.0f; + Check(HdrColorTransform::MapSdrToHdr(1.0f, display) <= 360.0f / 80.0f + 1e-4f, + "saturated white restores at or below the display white point"); + } + const float roundTrip = HdrColorTransform::MapSdrToHdr( + HdrColorTransform::MapHdrToSdr(0.37f, parameters), parameters); + Check(std::abs(roundTrip - 0.37f) <= 1e-5f, + "SDR-route round trip is exact below the white point"); + + // Strict monotonicity through the shoulder, the anchor and the linear + // tail of the bounded curve. + const auto curve = HdrColorTransform::BuildShoulderCurve( + parameters, HdrColorTransform::BoundedRouteHighlightTarget); + const float step = curve.peak / 400.0f; + float previous = -1.0f; + for (int i = 0; i <= 900; ++i) { + const float x = static_cast(i) * step; + const float y = HdrColorTransform::ApplyShoulderCurve(x, curve); + Check(y > previous, "anchored shoulder is strictly increasing"); + previous = y; + } + // Anchor and paired inverse, including points on the linear tail. + const float anchor = HdrColorTransform::ApplyShoulderCurve(curve.peak, curve); + Check(std::abs(anchor - curve.target) <= 1e-4f, + "shoulder maps the peak to its design target"); + const float tailPoints[] = { 0.0f, 0.5f, 1.0f, curve.peak, curve.peak * 1.5f }; + for (float x : tailPoints) { + const float y = HdrColorTransform::ApplyShoulderCurve(x, curve); + const float restored = HdrColorTransform::InvertShoulderCurve(y, curve); + Check(std::abs(restored - x) <= 1e-3f * std::max(1.0f, x), + "shoulder inverse restores the original value"); + } + + // Bounded target keeps the DLSSNR model input in the validated band. + Check(HdrColorTransform::BoundedRouteHighlightTarget >= 2.0f && + HdrColorTransform::BoundedRouteHighlightTarget <= 3.0f, + "bounded route target stays in the experimentally validated band"); + + // Degenerate monitors report peak <= SDR white; the curve must degrade + // to the plain identity instead of producing NaN (observed live: a + // 80-nit MaxLuminance report with a 360-nit SDR white). + { + HdrTransformParameters degenerate{}; + degenerate.sdrWhiteNits = 360.0f; + degenerate.hdrPeakNits = 80.0f; + const auto identity = HdrColorTransform::BuildShoulderCurve( + degenerate, HdrColorTransform::BoundedRouteHighlightTarget); + Check(identity.peak == 1.0f && identity.k == 0.0f && + identity.tailSlope == 1.0f && !std::isnan(identity.k) && + !std::isnan(identity.tailSlope), + "degenerate peak degrades to the identity curve without NaN"); + for (float x : { 0.0f, 1.0f, 4.5f, 12.5f }) { + Check(HdrColorTransform::ApplyShoulderCurve(x, identity) == x && + HdrColorTransform::InvertShoulderCurve(x, identity) == x, + "degenerate curve is the identity for every input"); + } + } + + // Bounded route round trip through the DLSSNR band. + const float boundedSamples[] = { 0.0f, 0.25f, 1.0f, 2.0f, 4.0f, 12.5f }; + for (float x : boundedSamples) { + const float encoded = HdrColorTransform::EncodeBoundedHdr(x, parameters); + Check(encoded <= HdrColorTransform::BoundedRouteHighlightTarget + 1e-3f, + "bounded encoding keeps the model input inside the band"); + const float decoded = HdrColorTransform::DecodeBoundedHdr(encoded, parameters); + Check(std::abs(decoded - x) <= 1e-3f * std::max(1.0f, x), + "bounded encode/decode round trip restores the canonical value"); + } +} + +void TestPresentationTerminalSelection() { + HdrFormatRoute terminal = MakeRoute(HdrAdapterProfile::PresentationTerminal); + terminal.defaultForHdr = true; + const HdrFormatRoutes routes{ terminal }; + Check(SelectDefaultHdrRoute(routes) == &routes.front(), + "HDR mode selects an explicitly default presentation terminal"); + Check(SelectDefaultSdrRoute(routes) == nullptr, + "SDR mode does not select a presentation terminal"); + + const HdrFormatRoutes fgRoutes[] = { + EffectProtocolC::DLSSFG(), + EffectProtocolC::XeSSFG(), + EffectProtocolC::FSR3FG() + }; + for (const HdrFormatRoutes& markerRoutes : fgRoutes) { + const HdrFormatRoute* selected = SelectDefaultHdrRoute(markerRoutes); + Check(selected && selected->IsPresentationTerminal(), + "FG marker selects its presentation-terminal route in HDR mode"); + Check(selected && selected->inputFormat == DXGI_FORMAT_R16G16B16A16_FLOAT && + selected->outputFormat == DXGI_FORMAT_R16G16B16A16_FLOAT, + "FG marker preserves the canonical FP16 working surface"); + } +} + +} + +int main() { + TestRouteStorageAndCategories(); + TestDispatcherProfiles(); + TestHdrToneMapRoundTrip(); + TestShoulderCurveFamily(); + TestPresentationTerminalSelection(); + + if (g_failures == 0) { + std::printf("All HDR mechanical tests passed.\n"); + return 0; + } + + std::printf("%d HDR mechanical test(s) failed.\n", g_failures); + return 1; +} diff --git a/version.json b/version.json index b7133ad6e..4ad71d4de 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "0.6.1", - "tag": "v0.6.1-experimental", - "releasePage": "https://github.com/SAOG0721/Magpie/releases/tag/v0.6.1-experimental" + "version": "0.6.6", + "tag": "v0.6.6-hdr-fp16compat", + "releasePage": "https://github.com/konodiodaaaaa1/Magpie/releases/tag/v0.6.6-hdr-fp16compat" }