项目实训个人9--api适配

项目实训个人9--api适配

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_Ext146带扩展参数的初始化。所有 Init 变体最终汇聚于此
NVSDK_NGX_D3D12_Init225旧版 API,用ScopedInitDx12防递归后委托给 Init_Ext
NVSDK_NGX_D3D12_Init_ProjectID282按 ProjectID 初始化,保存项目信息后委托给 Init_Ext

关闭(2 个)

函数行号说明
NVSDK_NGX_D3D12_Shutdown372清理所有 DLSS feature、FG context
NVSDK_NGX_D3D12_Shutdown1422带设备的变体,最终委托给 Shutdown

Feature 生命周期(3 个)★ 核心

函数行号说明
NVSDK_NGX_D3D12_CreateFeature745创建超分/FrameGen 实例
NVSDK_NGX_D3D12_EvaluateFeature1099每帧执行超分(最高频调用)
NVSDK_NGX_D3D12_ReleaseFeature820释放实例

参数管理(5 个)

函数行号说明
NVSDK_NGX_D3D12_GetParameters464获取 SDK 管理的持久参数表
NVSDK_NGX_D3D12_GetCapabilityParameters504分配预填充能力信息的参数表
NVSDK_NGX_D3D12_AllocateParameters543分配空白参数表
NVSDK_NGX_D3D12_DestroyParameters590按 AllocType 决定释放方式
NVSDK_NGX_D3D12_PopulateParameters_Impl568填充参数表

查询(2 个)

函数行号说明
NVSDK_NGX_D3D12_GetFeatureRequirements904欺骗核心:查询硬件是否支持某 Feature
NVSDK_NGX_D3D12_GetScratchBufferSize1198返回临时缓冲大小(固定 50MB)

通用 NGX(1 个,在 NVNGX.cpp 中)

函数行号说明
NVSDK_NGX_UpdateFeature87运行时更新 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 变体(如InitInit_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 参数管理函数

函数决策逻辑
GetParametersDLSS 可用 → 转发,拿到真实参数后复制 upscalerBridge 配置;不可用 → 返回自定义参数
GetCapabilityParametersDLSS 可用 → 转发,成功后初始化;不可用 → 新建 InternDynamic 参数
AllocateParametersDLSS 可用 → 转发(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)。热切换时:

  1. Dx12Contexts[handleId]中的旧IFeature_Dx12被销毁
  2. 新后端创建的IFeature_Dx12放入同一个 slot
  3. Handle ID 不变,游戏无感知

详见 Overlay 模式博客 第四章。


六、部分决策速查表

#函数行号转发条件内部处理条件
1Init_Ext146DLSS 可用总是执行(两边都走)
2Init225同 Init_Ext防递归后委托 Init_Ext
3Init_ProjectID282同 Init_Ext委托 Init_Ext
4Shutdown372DLSS 可用总是执行清理
5GetParameters464DLSS 可用不可用时返回自定义参数
6GetCapabilityParameters504DLSS 可用创建 InternDynamic
7AllocateParameters543DLSS 可用创建 InternDynamic
8DestroyParameters590AllocType==NVDynamicAllocType==InternDynamic
9CreateFeature745Feature≠SR/RRFeature==SR/RR
10EvaluateFeature1099HandleId<1000000HandleId≥1000000
11ReleaseFeature820HandleId<1000000HandleId≥1000000
12GetFeatureRequirements904Feature≠SRSR 强制 Supported

七、设计原则

原则说明
全量拦截导出游戏可能调用的所有 NGX 函数,不漏一个
条件转发只拦截 SuperSampling/RayReconstruction,其余透明转发
Handle 分区ID 分界 + Context Map,多实例互不干扰
热切换兼容Handle 不变,只换 Context Map 中的实例
对称为王Create/Release 决策完全对称
优雅降级DLSS 不可用时走 FSR,不返回错误