Add FSR3 Generic API path and harden runtime diagnostics

- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
  generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
  runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
  creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
  initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
  and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
  missing VK permutation headers; DXC auto-downloaded on Linux/Windows
  MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
  permutation script before SDK build, scope FFX pragma/ODR warning
  suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
  performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen
This commit is contained in:
Kelsi 2026-03-09 12:51:59 -07:00
parent ae48e4d7a6
commit bae32c1823
11 changed files with 537 additions and 16 deletions

View file

@ -17,7 +17,11 @@
#if WOWEE_HAS_AMD_FSR3_FRAMEGEN
#include "third_party/ffx_fsr3_legacy_compat.h"
#include <ffx_api.h>
#include <ffx_framegeneration.h>
#include <ffx_upscale.h>
#include <ffx_vk.h>
#include <vk/ffx_api_vk.h>
#endif
namespace wowee::rendering {
@ -34,6 +38,10 @@ struct AmdFsr3Runtime::RuntimeFns {
decltype(&ffxFsr3ConfigureFrameGeneration) fsr3ConfigureFrameGeneration = nullptr;
decltype(&ffxFsr3DispatchFrameGeneration) fsr3DispatchFrameGeneration = nullptr;
decltype(&ffxFsr3ContextDestroy) fsr3ContextDestroy = nullptr;
PfnFfxCreateContext createContext = nullptr;
PfnFfxDestroyContext destroyContext = nullptr;
PfnFfxConfigure configure = nullptr;
PfnFfxDispatch dispatch = nullptr;
};
#else
struct AmdFsr3Runtime::RuntimeFns {};
@ -51,6 +59,43 @@ FfxErrorCode vkSwapchainConfigureNoop(const FfxFrameGenerationConfig*) {
return FFX_OK;
}
std::string narrowWString(const wchar_t* msg) {
if (!msg) return {};
std::string out;
for (const wchar_t* p = msg; *p; ++p) {
const wchar_t wc = *p;
if (wc >= 0 && wc <= 0x7f) {
out.push_back(static_cast<char>(wc));
} else {
out.push_back('?');
}
}
return out;
}
void ffxApiLogMessage(uint32_t type, const wchar_t* message) {
const std::string narrowed = narrowWString(message);
if (type == FFX_API_MESSAGE_TYPE_ERROR) {
LOG_ERROR("FSR3 runtime/API: ", narrowed);
} else {
LOG_WARNING("FSR3 runtime/API: ", narrowed);
}
}
const char* ffxApiReturnCodeName(ffxReturnCode_t rc) {
switch (rc) {
case FFX_API_RETURN_OK: return "OK";
case FFX_API_RETURN_ERROR: return "ERROR";
case FFX_API_RETURN_ERROR_UNKNOWN_DESCTYPE: return "ERROR_UNKNOWN_DESCTYPE";
case FFX_API_RETURN_ERROR_RUNTIME_ERROR: return "ERROR_RUNTIME_ERROR";
case FFX_API_RETURN_NO_PROVIDER: return "NO_PROVIDER";
case FFX_API_RETURN_ERROR_MEMORY: return "ERROR_MEMORY";
case FFX_API_RETURN_ERROR_PARAMETER: return "ERROR_PARAMETER";
case FFX_API_RETURN_PROVIDER_NO_SUPPORT_NEW_DESCTYPE: return "PROVIDER_NO_SUPPORT_NEW_DESCTYPE";
default: return "UNKNOWN";
}
}
template <typename T, typename = void>
struct HasUpscaleOutputSize : std::false_type {};
@ -141,6 +186,7 @@ FfxResourceDescription makeResourceDescription(VkFormat format,
description.usage = usage;
return description;
}
} // namespace
#endif
@ -184,23 +230,36 @@ bool AmdFsr3Runtime::initialize(const AmdFsr3RuntimeInitDesc& desc) {
candidates.emplace_back("libffx_fsr3.so");
#endif
std::string lastDlopenError;
for (const std::string& path : candidates) {
#if defined(_WIN32)
HMODULE h = LoadLibraryA(path.c_str());
if (!h) continue;
libHandle_ = reinterpret_cast<void*>(h);
#else
dlerror();
void* h = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!h) continue;
if (!h) {
const char* err = dlerror();
if (err && *err) lastDlopenError = err;
continue;
}
libHandle_ = h;
#endif
loadedLibraryPath_ = path;
loadPathKind_ = LoadPathKind::Official;
LOG_INFO("FSR3 runtime: opened library candidate ", loadedLibraryPath_);
break;
}
if (!libHandle_) {
lastError_ = "no official runtime (Path A) found";
#if !defined(_WIN32)
if (!lastDlopenError.empty()) {
lastError_ += " dlopen error: ";
lastError_ += lastDlopenError;
}
#endif
return false;
}
@ -223,16 +282,127 @@ bool AmdFsr3Runtime::initialize(const AmdFsr3RuntimeInitDesc& desc) {
fns_->fsr3ConfigureFrameGeneration = reinterpret_cast<decltype(fns_->fsr3ConfigureFrameGeneration)>(resolveSym("ffxFsr3ConfigureFrameGeneration"));
fns_->fsr3DispatchFrameGeneration = reinterpret_cast<decltype(fns_->fsr3DispatchFrameGeneration)>(resolveSym("ffxFsr3DispatchFrameGeneration"));
fns_->fsr3ContextDestroy = reinterpret_cast<decltype(fns_->fsr3ContextDestroy)>(resolveSym("ffxFsr3ContextDestroy"));
fns_->createContext = reinterpret_cast<decltype(fns_->createContext)>(resolveSym("ffxCreateContext"));
fns_->destroyContext = reinterpret_cast<decltype(fns_->destroyContext)>(resolveSym("ffxDestroyContext"));
fns_->configure = reinterpret_cast<decltype(fns_->configure)>(resolveSym("ffxConfigure"));
fns_->dispatch = reinterpret_cast<decltype(fns_->dispatch)>(resolveSym("ffxDispatch"));
if (!fns_->getScratchMemorySizeVK || !fns_->getDeviceVK || !fns_->getInterfaceVK ||
!fns_->getCommandListVK || !fns_->getResourceVK || !fns_->fsr3ContextCreate || !fns_->fsr3ContextDispatchUpscale ||
!fns_->fsr3ContextDestroy) {
LOG_WARNING("FSR3 runtime: required symbols not found in ", loadedLibraryPath_);
lastError_ = "missing required Vulkan FSR3 symbols in runtime library";
const bool hasLegacyApi = (fns_->getScratchMemorySizeVK && fns_->getDeviceVK && fns_->getInterfaceVK &&
fns_->getCommandListVK && fns_->getResourceVK && fns_->fsr3ContextCreate &&
fns_->fsr3ContextDispatchUpscale && fns_->fsr3ContextDestroy);
const bool hasGenericApi = (fns_->createContext && fns_->destroyContext && fns_->configure && fns_->dispatch);
if (!hasLegacyApi && !hasGenericApi) {
LOG_WARNING("FSR3 runtime: required symbols not found in ", loadedLibraryPath_,
" (need legacy ffxFsr3* or generic ffxCreateContext/ffxDispatch)");
lastError_ = "missing required Vulkan FSR3 symbols in runtime library (legacy and generic APIs unavailable)";
shutdown();
return false;
}
apiMode_ = hasLegacyApi ? ApiMode::LegacyFsr3 : ApiMode::GenericApi;
if (apiMode_ == ApiMode::GenericApi) {
ffxConfigureDescGlobalDebug1 globalDebug{};
globalDebug.header.type = FFX_API_CONFIGURE_DESC_TYPE_GLOBALDEBUG1;
globalDebug.header.pNext = nullptr;
globalDebug.fpMessage = &ffxApiLogMessage;
globalDebug.debugLevel = FFX_API_CONFIGURE_GLOBALDEBUG_LEVEL_VERBOSE;
(void)fns_->configure(nullptr, reinterpret_cast<ffxConfigureDescHeader*>(&globalDebug));
ffxCreateBackendVKDesc backendDesc{};
backendDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK;
backendDesc.header.pNext = nullptr;
backendDesc.vkDevice = desc.device;
backendDesc.vkPhysicalDevice = desc.physicalDevice;
ffxCreateContextDescUpscaleVersion upVerDesc{};
upVerDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE_VERSION;
upVerDesc.header.pNext = nullptr;
upVerDesc.version = FFX_UPSCALER_VERSION;
ffxCreateContextDescUpscale upDesc{};
upDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
upDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&backendDesc);
upDesc.flags = FFX_UPSCALE_ENABLE_AUTO_EXPOSURE | FFX_UPSCALE_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION;
upDesc.flags |= FFX_UPSCALE_ENABLE_DEBUG_CHECKING;
if (desc.hdrInput) upDesc.flags |= FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE;
if (desc.depthInverted) upDesc.flags |= FFX_UPSCALE_ENABLE_DEPTH_INVERTED;
upDesc.maxRenderSize.width = desc.maxRenderWidth;
upDesc.maxRenderSize.height = desc.maxRenderHeight;
upDesc.maxUpscaleSize.width = desc.displayWidth;
upDesc.maxUpscaleSize.height = desc.displayHeight;
upDesc.fpMessage = &ffxApiLogMessage;
backendDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&upVerDesc);
ffxContext upscaleCtx = nullptr;
const ffxReturnCode_t upCreateRc =
fns_->createContext(&upscaleCtx, reinterpret_cast<ffxCreateContextDescHeader*>(&upDesc), nullptr);
if (upCreateRc != FFX_API_RETURN_OK) {
const std::string loadedPath = loadedLibraryPath_;
lastError_ = "ffxCreateContext (upscale) failed rc=" + std::to_string(upCreateRc) +
" (" + ffxApiReturnCodeName(upCreateRc) + "), runtimeLib=" + loadedPath;
shutdown();
return false;
}
genericUpscaleContext_ = upscaleCtx;
backendDesc.header.pNext = nullptr;
if (desc.enableFrameGeneration) {
ffxCreateContextDescFrameGenerationVersion fgVerDesc{};
fgVerDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION_VERSION;
fgVerDesc.header.pNext = nullptr;
fgVerDesc.version = FFX_FRAMEGENERATION_VERSION;
ffxCreateContextDescFrameGeneration fgDesc{};
fgDesc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION;
fgDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&backendDesc);
fgDesc.flags = FFX_FRAMEGENERATION_ENABLE_MOTION_VECTORS_JITTER_CANCELLATION;
fgDesc.flags |= FFX_FRAMEGENERATION_ENABLE_DEBUG_CHECKING;
if (desc.hdrInput) fgDesc.flags |= FFX_FRAMEGENERATION_ENABLE_HIGH_DYNAMIC_RANGE;
if (desc.depthInverted) fgDesc.flags |= FFX_FRAMEGENERATION_ENABLE_DEPTH_INVERTED;
fgDesc.displaySize.width = desc.displayWidth;
fgDesc.displaySize.height = desc.displayHeight;
fgDesc.maxRenderSize.width = desc.maxRenderWidth;
fgDesc.maxRenderSize.height = desc.maxRenderHeight;
fgDesc.backBufferFormat = ffxApiGetSurfaceFormatVK(desc.colorFormat);
backendDesc.header.pNext = reinterpret_cast<ffxApiHeader*>(&fgVerDesc);
ffxContext fgCtx = nullptr;
const ffxReturnCode_t fgCreateRc =
fns_->createContext(&fgCtx, reinterpret_cast<ffxCreateContextDescHeader*>(&fgDesc), nullptr);
if (fgCreateRc != FFX_API_RETURN_OK) {
const std::string loadedPath = loadedLibraryPath_;
lastError_ = "ffxCreateContext (framegeneration) failed rc=" + std::to_string(fgCreateRc) +
" (" + ffxApiReturnCodeName(fgCreateRc) + "), runtimeLib=" + loadedPath;
shutdown();
return false;
}
genericFramegenContext_ = fgCtx;
backendDesc.header.pNext = nullptr;
ffxConfigureDescFrameGeneration fgCfg{};
fgCfg.header.type = FFX_API_CONFIGURE_DESC_TYPE_FRAMEGENERATION;
fgCfg.header.pNext = nullptr;
fgCfg.frameGenerationEnabled = true;
fgCfg.allowAsyncWorkloads = false;
fgCfg.flags = FFX_FRAMEGENERATION_FLAG_NO_SWAPCHAIN_CONTEXT_NOTIFY;
fgCfg.onlyPresentGenerated = false;
fgCfg.frameID = genericFrameId_;
if (fns_->configure(reinterpret_cast<ffxContext*>(&genericFramegenContext_),
reinterpret_cast<ffxConfigureDescHeader*>(&fgCfg)) != FFX_API_RETURN_OK) {
lastError_ = "ffxConfigure (framegeneration) failed";
shutdown();
return false;
}
frameGenerationReady_ = true;
}
ready_ = true;
LOG_INFO("FSR3 runtime: loaded generic API from ", loadedLibraryPath_,
" framegenReady=", frameGenerationReady_ ? "yes" : "no");
return true;
}
scratchBufferSize_ = fns_->getScratchMemorySizeVK(FFX_FSR3_CONTEXT_COUNT);
if (scratchBufferSize_ == 0) {
LOG_WARNING("FSR3 runtime: scratch buffer size query returned 0.");
@ -344,6 +514,49 @@ bool AmdFsr3Runtime::dispatchUpscale(const AmdFsr3RuntimeDispatchDesc& desc) {
lastError_ = "invalid upscale dispatch resources";
return false;
}
if (apiMode_ == ApiMode::GenericApi) {
if (!genericUpscaleContext_ || !fns_->dispatch) {
lastError_ = "generic API upscale context unavailable";
return false;
}
ffxDispatchDescUpscale up{};
up.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
up.header.pNext = nullptr;
up.commandList = reinterpret_cast<void*>(desc.commandBuffer);
up.color = ffxApiGetResourceVK(desc.colorImage, desc.colorFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
up.depth = ffxApiGetResourceVK(desc.depthImage, desc.depthFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
up.motionVectors = ffxApiGetResourceVK(desc.motionVectorImage, desc.motionVectorFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
up.exposure = FfxApiResource{};
up.reactive = FfxApiResource{};
up.transparencyAndComposition = FfxApiResource{};
up.output = ffxApiGetResourceVK(desc.outputImage, desc.outputFormat, desc.outputWidth, desc.outputHeight, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
up.jitterOffset.x = desc.jitterX;
up.jitterOffset.y = desc.jitterY;
up.motionVectorScale.x = desc.motionScaleX;
up.motionVectorScale.y = desc.motionScaleY;
up.renderSize.width = desc.renderWidth;
up.renderSize.height = desc.renderHeight;
up.upscaleSize.width = desc.outputWidth;
up.upscaleSize.height = desc.outputHeight;
up.enableSharpening = false;
up.sharpness = 0.0f;
up.frameTimeDelta = std::max(0.001f, desc.frameTimeDeltaMs);
up.preExposure = 1.0f;
up.reset = desc.reset;
up.cameraNear = desc.cameraNear;
up.cameraFar = desc.cameraFar;
up.cameraFovAngleVertical = desc.cameraFovYRadians;
up.viewSpaceToMetersFactor = 1.0f;
up.flags = 0;
if (fns_->dispatch(reinterpret_cast<ffxContext*>(&genericUpscaleContext_),
reinterpret_cast<ffxDispatchDescHeader*>(&up)) != FFX_API_RETURN_OK) {
lastError_ = "ffxDispatch (upscale) failed";
return false;
}
lastError_.clear();
return true;
}
if (!contextStorage_ || !fns_->fsr3ContextDispatchUpscale) {
lastError_ = "official runtime upscale context unavailable";
return false;
@ -413,6 +626,67 @@ bool AmdFsr3Runtime::dispatchFrameGeneration(const AmdFsr3RuntimeDispatchDesc& d
lastError_ = "invalid frame generation dispatch resources";
return false;
}
if (apiMode_ == ApiMode::GenericApi) {
if (!genericFramegenContext_ || !fns_->dispatch) {
lastError_ = "generic API frame generation context unavailable";
return false;
}
ffxDispatchDescFrameGenerationPrepareV2 prep{};
prep.header.type = FFX_API_DISPATCH_DESC_TYPE_FRAMEGENERATION_PREPARE_V2;
prep.header.pNext = nullptr;
prep.frameID = genericFrameId_;
prep.flags = 0;
prep.commandList = reinterpret_cast<void*>(desc.commandBuffer);
prep.renderSize.width = desc.renderWidth;
prep.renderSize.height = desc.renderHeight;
prep.jitterOffset.x = desc.jitterX;
prep.jitterOffset.y = desc.jitterY;
prep.motionVectorScale.x = desc.motionScaleX;
prep.motionVectorScale.y = desc.motionScaleY;
prep.frameTimeDelta = std::max(0.001f, desc.frameTimeDeltaMs);
prep.reset = desc.reset;
prep.cameraNear = desc.cameraNear;
prep.cameraFar = desc.cameraFar;
prep.cameraFovAngleVertical = desc.cameraFovYRadians;
prep.viewSpaceToMetersFactor = 1.0f;
prep.depth = ffxApiGetResourceVK(desc.depthImage, desc.depthFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
prep.motionVectors = ffxApiGetResourceVK(desc.motionVectorImage, desc.motionVectorFormat, desc.renderWidth, desc.renderHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
prep.cameraPosition[0] = prep.cameraPosition[1] = prep.cameraPosition[2] = 0.0f;
prep.cameraUp[0] = 0.0f; prep.cameraUp[1] = 1.0f; prep.cameraUp[2] = 0.0f;
prep.cameraRight[0] = 1.0f; prep.cameraRight[1] = 0.0f; prep.cameraRight[2] = 0.0f;
prep.cameraForward[0] = 0.0f; prep.cameraForward[1] = 0.0f; prep.cameraForward[2] = -1.0f;
if (fns_->dispatch(reinterpret_cast<ffxContext*>(&genericFramegenContext_),
reinterpret_cast<ffxDispatchDescHeader*>(&prep)) != FFX_API_RETURN_OK) {
lastError_ = "ffxDispatch (framegeneration prepare) failed";
return false;
}
ffxDispatchDescFrameGeneration fg{};
fg.header.type = FFX_API_DISPATCH_DESC_TYPE_FRAMEGENERATION;
fg.header.pNext = nullptr;
fg.commandList = reinterpret_cast<void*>(desc.commandBuffer);
fg.presentColor = ffxApiGetResourceVK(desc.outputImage, desc.outputFormat, desc.outputWidth, desc.outputHeight, FFX_API_RESOURCE_STATE_COMPUTE_READ);
fg.outputs[0] = ffxApiGetResourceVK(desc.frameGenOutputImage, desc.outputFormat, desc.outputWidth, desc.outputHeight, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
fg.numGeneratedFrames = 1;
fg.reset = desc.reset;
fg.backbufferTransferFunction = FFX_API_BACKBUFFER_TRANSFER_FUNCTION_SRGB;
fg.minMaxLuminance[0] = 0.0f;
fg.minMaxLuminance[1] = 1.0f;
fg.generationRect.left = 0;
fg.generationRect.top = 0;
fg.generationRect.width = desc.outputWidth;
fg.generationRect.height = desc.outputHeight;
fg.frameID = genericFrameId_;
if (fns_->dispatch(reinterpret_cast<ffxContext*>(&genericFramegenContext_),
reinterpret_cast<ffxDispatchDescHeader*>(&fg)) != FFX_API_RETURN_OK) {
lastError_ = "ffxDispatch (framegeneration) failed";
return false;
}
++genericFrameId_;
lastError_.clear();
return true;
}
if (!contextStorage_ || !fns_->fsr3DispatchFrameGeneration) {
lastError_ = "official runtime frame generation context unavailable";
return false;
@ -449,7 +723,18 @@ bool AmdFsr3Runtime::dispatchFrameGeneration(const AmdFsr3RuntimeDispatchDesc& d
void AmdFsr3Runtime::shutdown() {
#if WOWEE_HAS_AMD_FSR3_FRAMEGEN
if (contextStorage_ && fns_ && fns_->fsr3ContextDestroy) {
if (apiMode_ == ApiMode::GenericApi && fns_ && fns_->destroyContext) {
if (genericFramegenContext_) {
auto ctx = reinterpret_cast<ffxContext*>(&genericFramegenContext_);
fns_->destroyContext(ctx, nullptr);
genericFramegenContext_ = nullptr;
}
if (genericUpscaleContext_) {
auto ctx = reinterpret_cast<ffxContext*>(&genericUpscaleContext_);
fns_->destroyContext(ctx, nullptr);
genericUpscaleContext_ = nullptr;
}
} else if (contextStorage_ && fns_ && fns_->fsr3ContextDestroy) {
fns_->fsr3ContextDestroy(reinterpret_cast<FfxFsr3Context*>(contextStorage_));
}
#endif
@ -476,6 +761,8 @@ void AmdFsr3Runtime::shutdown() {
libHandle_ = nullptr;
loadedLibraryPath_.clear();
loadPathKind_ = LoadPathKind::None;
apiMode_ = ApiMode::LegacyFsr3;
genericFrameId_ = 1;
}
} // namespace wowee::rendering