API 适配:NVNGX 函数拦截与决策逻辑
本文详解 upscalerBridge 如何拦截 20 个 NGX D3D12 API 函数,以及每个函数的"转发 vs 内部处理"决策逻辑。
一、为什么要自己实现整套 NGX API
游戏通过GetProcAddress(nvngx, "NVSDK_NGX_D3D12_CreateFeature")获取 NGX 函数指针。如果我们的 DLL 没有导出这些函数,游戏会绕过我们直接调用真正的nvngx.dll。
因此,我们必须完整导出游戏可能调用的所有 NGX 函数,并在每个函数内部决定:这个调用应该转发给真正的 nvngx.dll,还是由我们拦截处理。
[inputs/NVNGX_DLSS_Dx12.cpp](file:///e:/Projects/Repositories/upscalerBridge/upscalerBridge/inputs/NVNGX_DLSS_Dx12.cpp)
二、20 个被拦截函数的完整清单
初始化(3 个)
| 函数 | 行号 | 说明 |
|---|---|---|
NVSDK_NGX_D3D12_Init_Ext | 146 | 带扩展参数的初始化。所有 Init 变体最终汇聚于此 |
NVSDK_NGX_D3D12_Init | 225 | 旧版 API,用ScopedInitDx12防递归后委托给 Init_Ext |
NVSDK_NGX_D3D12_Init_ProjectID | 282 | 按 ProjectID 初始化,保存项目信息后委托给 Init_Ext |
关闭(2 个)
| 函数 | 行号 | 说明 |
|---|---|---|
NVSDK_NGX_D3D12_Shutdown | 372 | 清理所有 DLSS feature、FG context |
NVSDK_NGX_D3D12_Shutdown1 | 422 | 带设备的变体,最终委托给 Shutdown |
Feature 生命周期(3 个)★ 核心
| 函数 | 行号 | 说明 |
|---|---|---|
NVSDK_NGX_D3D12_CreateFeature | 745 | 创建超分/FrameGen 实例 |
NVSDK_NGX_D3D12_EvaluateFeature | 1099 | 每帧执行超分(最高频调用) |
NVSDK_NGX_D3D12_ReleaseFeature | 820 | 释放实例 |
参数管理(5 个)
| 函数 | 行号 | 说明 |
|---|---|---|
NVSDK_NGX_D3D12_GetParameters | 464 | 获取 SDK 管理的持久参数表 |
NVSDK_NGX_D3D12_GetCapabilityParameters | 504 | 分配预填充能力信息的参数表 |
NVSDK_NGX_D3D12_AllocateParameters | 543 | 分配空白参数表 |
NVSDK_NGX_D3D12_DestroyParameters | 590 | 按 AllocType 决定释放方式 |
NVSDK_NGX_D3D12_PopulateParameters_Impl | 568 | 填充参数表 |
查询(2 个)
| 函数 | 行号 | 说明 |
|---|---|---|
NVSDK_NGX_D3D12_GetFeatureRequirements | 904 | 欺骗核心:查询硬件是否支持某 Feature |
NVSDK_NGX_D3D12_GetScratchBufferSize | 1198 | 返回临时缓冲大小(固定 50MB) |
通用 NGX(1 个,在 NVNGX.cpp 中)
| 函数 | 行号 | 说明 |
|---|---|---|
NVSDK_NGX_UpdateFeature | 87 | 运行时更新 ApplicationId/ProjectId |
三、核心决策维度:两个判断依据
所有函数的"转发 vs 内部处理"决策都基于两个条件:
条件 1:Feature 类型(CreateFeature / GetFeatureRequirements)
Feature == SuperSampling → 内部处理(我们替换为 FSR/DLSS) Feature == RayReconstruction → 内部处理 Feature == FrameGeneration → 看 FGInput 配置 其他 Feature(ISP、Reserved) → 转发给真正的 nvngx.dll为什么只拦截这两个 Feature:NGX 支持多种 Feature——ImageSignalProcessing、Reserved1 等。我们只替换超分和光线重建,其余功能让真正的 nvngx 处理。
条件 2:Handle ID 范围(EvaluateFeature / ReleaseFeature)
Handle ID < 1000000 (DLSS_MOD_ID_OFFSET) → 转发(真正的 nvngx 创建的 Handle) Handle ID >= 1000000 → 内部处理(我们创建的 Handle)为什么需要两个维度:同一个进程可能同时有"真正的 DLSS-D"和"我们替换的 DLSS-SR"。用 Handle ID 而非 Feature ID,可以精确区分每个具体实例。
四、逐函数决策逻辑详解
4.1 Init_Ext:唯一"两边都执行"的函数
[第 146-223 行](file:///e:/Projects/Repositories/upscalerBridge/upscalerBridge/inputs/NVNGX_DLSS_Dx12.cpp#L146)
NVSDK_NGX_Result NVSDK_NGX_D3D12_Init_Ext( unsigned long long InApplicationId, const wchar_t* InApplicationDataPath, ID3D12Device* InDevice, NVSDK_NGX_Version InSDKVersion, const NVSDK_NGX_FeatureCommonInfo* InFeatureInfo) { // ① 保存应用信息到 State 单例 State::Instance().NVNGX_ApplicationId = InApplicationId; State::Instance().NVNGX_Version = InSDKVersion; // ② 如果有真正的 nvngx.dll → 转发 Init if (Config::Instance()->DLSSEnabled.value_or_default() && !_skipInit) { if (NVNGXProxy::NVNGXModule() == nullptr) NVNGXProxy::InitNVNGX(); // 惰性加载 nvngx.dll if (NVNGXProxy::NVNGXModule() != nullptr) { auto result = NVNGXProxy::D3D12_Init_Ext()(InApplicationId, ...); if (result == Success) NVNGXProxy::SetDx12Inited(true); // 标记 D3D12 已初始化 } } // ③ 执行我们自己的初始化(无论步骤②是否执行) D3D12Device = InDevice; D3D12Hooks::HookDevice(InDevice); // Hook ID3D12Device 的 vtable UpscalerTimeDx12::Init(InDevice); // GPU 计时器初始化 State::Instance().nvngxDx12Inited = true; return NVSDK_NGX_Result_Success; }为什么是"两边都执行"而不是"二选一":有些游戏在 Init 阶段需要真正的 nvngx.dll 返回能力信息(如支持的 Feature 列表、驱动版本要求),而这些信息我们无法完全模拟。所以我们在转发 Init 的同时,也必须执行自己的初始化(Hook D3D12 设备、启动计时器)。
注意_skipInit标志——当其他 Init 变体(如Init、Init_ProjectID)委托给Init_Ext时,会设置此标志防止重复执行。
4.2 CreateFeature:按 Feature 类型分流
[第 745-818 行](file:///e:/Projects/Repositories/upscalerBridge/upscalerBridge/inputs/NVNGX_DLSS_Dx12.cpp#L745)
CreateFeature(InCmdList, InFeatureID, InParameters, OutHandle) │ ├─ InFeatureID == SuperSampling ? │ ├─ YES → [内部] TryCreateOptiFeature() │ │ ├─ GetUpscalerBackend() # FFX or DLSS? │ │ ├─ FeatureProvider::GetFeature() # 创建 FSR2FeatureDx12 or DLSSFeatureDx12 │ │ ├─ feature->Init() # 初始化管道 │ │ └─ Dx12Contexts[handleId] = feature # 存入 Context Map │ │ │ └─ NO → InFeatureID == RayReconstruction ? │ ├─ YES → [内部] TryCreateOptiFeature() │ └─ NO → [转发] NVNGXProxy::D3D12_CreateFeature()() │ └─ 调用真正的 nvngx.dll为什么 FrameGeneration 不走内部路径:帧生成(DLSS-G)是独立 Feature。我们可以在菜单中配置用 NVIDIA 原生帧生成还是 FSR 帧生成。这通过State::activeFgInput来控制,而不是在 CreateFeature 中拦截。
4.3 EvaluateFeature:按 Handle ID 分流
[第 1099-1192 行](file:///e:/Projects/Repositories/upscalerBridge/upscalerBridge/inputs/NVNGX_DLSS_Dx12.cpp#L1099)
NVSDK_NGX_Result NVSDK_NGX_D3D12_EvaluateFeature( ID3D12GraphicsCommandList* InCmdList, const NVSDK_NGX_Handle* InFeatureHandle, NVSDK_NGX_Parameter* InParameters, ...) { const uint32_t handleId = InFeatureHandle->Id; // ★ 核心判断:这个 Handle 是谁创建的? if (handleId < DLSS_MOD_ID_OFFSET) // < 1000000 { // 真正 nvngx.dll 创建的 → 透明转发 return NVNGXProxy::D3D12_EvaluateFeature()(InCmdList, ...); } // 我们创建的 → TryEvaluateOptiFeature() // → Dx12Contexts[handleId] 查找 IFeature_Dx12 // → feature->Evaluate() → FSR/DLSS 超分 return TryEvaluateOptiFeature(InCmdList, InFeatureHandle, ...); }为什么用 Handle ID 而不是 Feature ID:假设一个游戏同时创建了两个 Feature:
- Handle A (Id=45):真正的 DLSS-D(Ray Reconstruction),由 nvngx.dll 创建
- Handle B (Id=1000001):我们替换的 DLSS-SR(Super Resolution),由 upscalerBridge 创建
两者都是"DLSS Feature",但 Evaluate 时只能用 Handle 区分。用 Feature ID 无法区分谁是谁。
4.4 ReleaseFeature:对称的清理逻辑
[第 820-896 行](file:///e:/Projects/Repositories/upscalerBridge/upscalerBridge/inputs/NVNGX_DLSS_Dx12.cpp#L820)
NVSDK_NGX_Result NVSDK_NGX_D3D12_ReleaseFeature(NVSDK_NGX_Handle* InHandle) { auto handleId = InHandle->Id; if (handleId < DLSS_MOD_ID_OFFSET) { // 真正的 nvngx 创建的 → 转发释放 return NVNGXProxy::D3D12_ReleaseFeature()(InHandle); } // 我们创建的 → 从 Dx12Contexts 移除 if (auto it = Dx12Contexts.find(handleId); it != Dx12Contexts.end()) { if (it->second.feature.get() == State::Instance().currentFeature) State::Instance().currentFeature = nullptr; Dx12Contexts.erase(it); // unique_ptr 自动析构 } return NVSDK_NGX_Result_Success; }对称性:Release 的决策与 Create 完全对称——Handle ID < 1000000 转发,>= 1000000 内部清理。
4.5 GetFeatureRequirements:欺骗的核心
[第 904-957 行](file:///e:/Projects/Repositories/upscalerBridge/upscalerBridge/inputs/NVNGX_DLSS_Dx12.cpp#L904)
NVSDK_NGX_Result NVSDK_NGX_D3D12_GetFeatureRequirements( IDXGIAdapter* Adapter, const NVSDK_NGX_FeatureDiscoveryInfo* FeatureDiscoveryInfo, NVSDK_NGX_FeatureRequirement* OutSupported) { // ★ 超分 → 永远返回 Supported(无论什么显卡) if (FeatureDiscoveryInfo->FeatureID == NVSDK_NGX_Feature_SuperSampling) { OutSupported->FeatureSupported = NVSDK_NGX_FeatureSupportResult_Supported; OutSupported->MinHWArchitecture = 0; // 无硬件限制 strcpy_s(OutSupported->MinOSVersion, "10.0.10240.16384"); return NVSDK_NGX_Result_Success; } // 其他 Feature → DLSS 可用时转发 if (Config::Instance()->DLSSEnabled && gpu.dlssCapable) return NVNGXProxy::D3D12_GetFeatureRequirements()(...); // DLSS 不可用 → 返回不支持 OutSupported->FeatureSupported = AdapterUnsupported; return NVSDK_NGX_Result_FAIL_FeatureNotSupported; }这是整个"适配"逻辑的核心:AMD 显卡上,游戏询问"DLSS 支持吗?“,我们回答"支持!”。游戏放心地创建 DLSS Feature,我们给一个 FSR 实例。
4.6 参数管理函数
| 函数 | 决策逻辑 |
|---|---|
GetParameters | DLSS 可用 → 转发,拿到真实参数后复制 upscalerBridge 配置;不可用 → 返回自定义参数 |
GetCapabilityParameters | DLSS 可用 → 转发,成功后初始化;不可用 → 新建 InternDynamic 参数 |
AllocateParameters | DLSS 可用 → 转发(NVDynamic);不可用 → 新建 InternDynamic 参数 |
DestroyParameters | 读取 AllocType 标记:NVDynamic → 转发 NGX Destroy;InternDynamic → delete |
PopulateParameters_Impl | 总是内部处理(参数表填充) |
4.7 ScratchBuffer 和 UpdateFeature
GetScratchBufferSize:总是返回固定值52428800(50MB),不转发UpdateFeature:总是返回Success,只更新内部的 AppId/ProjectId
五、Handle 机制:多实例隔离的核心
5.1 Handle ID 分配与分界
// CreateFeature 中分配 Handle const uint32_t handleId = IFeature::GetNextHandleId(); // 原子递增 *OutHandle = new NVSDK_NGX_Handle { handleId };Handle ID < 1000000 → nvngx.dll 原生 Handle(透明转发) Handle ID >= 1000000 → upscalerBridge Handle(内部处理)GetNextHandleId()使用std::atomic<uint32_t>保证线程安全,每个新 Feature 获得唯一的递增 ID。
5.2 Context Map
// 全局映射:Handle ID → 超分实例 ankerl::unordered_dense::map<UINT, ContextData> Dx12Contexts; struct ContextData { unique_ptr<IFeature_Dx12> feature; // FSR2FeatureDx12 或 DLSSFeatureDx12 };游戏持有NVSDK_NGX_Handle*(只包含一个uint32_t Id),我们在 O(1) 的 hash map 中查找。游戏无法直接访问IFeature_Dx12,Handle 对游戏来说是一个不透明的整数。
5.3 后端热切换的透明性
用户可以随时在菜单中切换超分后端(DLSS → FSR)。热切换时:
Dx12Contexts[handleId]中的旧IFeature_Dx12被销毁- 新后端创建的
IFeature_Dx12放入同一个 slot - Handle ID 不变,游戏无感知
详见 Overlay 模式博客 第四章。
六、部分决策速查表
| # | 函数 | 行号 | 转发条件 | 内部处理条件 |
|---|---|---|---|---|
| 1 | Init_Ext | 146 | DLSS 可用 | 总是执行(两边都走) |
| 2 | Init | 225 | 同 Init_Ext | 防递归后委托 Init_Ext |
| 3 | Init_ProjectID | 282 | 同 Init_Ext | 委托 Init_Ext |
| 4 | Shutdown | 372 | DLSS 可用 | 总是执行清理 |
| 5 | GetParameters | 464 | DLSS 可用 | 不可用时返回自定义参数 |
| 6 | GetCapabilityParameters | 504 | DLSS 可用 | 创建 InternDynamic |
| 7 | AllocateParameters | 543 | DLSS 可用 | 创建 InternDynamic |
| 8 | DestroyParameters | 590 | AllocType==NVDynamic | AllocType==InternDynamic |
| 9 | CreateFeature | 745 | Feature≠SR/RR | Feature==SR/RR |
| 10 | EvaluateFeature | 1099 | HandleId<1000000 | HandleId≥1000000 |
| 11 | ReleaseFeature | 820 | HandleId<1000000 | HandleId≥1000000 |
| 12 | GetFeatureRequirements | 904 | Feature≠SR | SR 强制 Supported |
七、设计原则
| 原则 | 说明 |
|---|---|
| 全量拦截 | 导出游戏可能调用的所有 NGX 函数,不漏一个 |
| 条件转发 | 只拦截 SuperSampling/RayReconstruction,其余透明转发 |
| Handle 分区 | ID 分界 + Context Map,多实例互不干扰 |
| 热切换兼容 | Handle 不变,只换 Context Map 中的实例 |
| 对称为王 | Create/Release 决策完全对称 |
| 优雅降级 | DLSS 不可用时走 FSR,不返回错误 |