#include #include #include #include #include "UnityPluginAPI/IUnityInterface.h" #include "UnityPluginAPI/IUnityLog.h" #include "UnityPluginAPI/IUnityGraphics.h" #include "UnityPluginAPI/IUnityRenderingExtensions.h" #include #include #include #include "UnityPluginAPI/IUnityGraphicsD3D11.h" #include "UnityPluginAPI/IUnityGraphicsD3D12.h" #include "UnityPluginAPI/IUnityGraphicsVulkan.h" #include "ffx-fsr2-api/ffx_fsr2.h" #include "ffx-fsr2-api/dx11/ffx_fsr2_dx11.h" #include "ffx-fsr2-api/dx12/ffx_fsr2_dx12.h" #include "ffx-fsr2-api/vk/ffx_fsr2_vk.h" #include "FSR2UnityTypes.h" static const int32_t BaseEventId = 0; static IUnityInterfaces* s_UnityInterfaces = nullptr; static IUnityLog* s_Log = nullptr; static IUnityGraphics* s_Graphics = nullptr; static IUnityGraphicsD3D11* s_GraphicsD3D11 = nullptr; static IUnityGraphicsD3D12v7* s_GraphicsD3D12 = nullptr; static IUnityGraphicsVulkanV2* s_GraphicsVulkan = nullptr; static UnityGfxRenderer s_RendererType = kUnityGfxRendererNull; static FfxDevice s_Device = nullptr; static FfxFsr2Interface s_Fsr2Interface; static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType); static void UNITY_INTERFACE_API OnRenderEventAndData(int eventID, void* data); static void UNITY_INTERFACE_API OnSetTextureEvent(int eventID, void* data); struct FSR2Feature { FfxFsr2Context upscalingContext; uint32_t flags; bool isValid; uint64_t dispatchFrameValue; FSR2TextureTable textureTable; }; static std::vector s_Features; static std::queue s_FeatureSlots; static std::mutex s_FeatureMutex; static HANDLE s_FrameFenceEventHandle = nullptr; // Unity plugin load event extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces) { s_UnityInterfaces = unityInterfaces; s_Log = unityInterfaces->Get(); s_Graphics = unityInterfaces->Get(); s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent); // Run OnGraphicsDeviceEvent(initialize) manually on plugin load // to not miss the event in case the graphics device is already initialized OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize); } // Unity plugin unload event extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload() { s_Graphics->UnregisterDeviceEventCallback(OnGraphicsDeviceEvent); } static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType) { std::lock_guard lock(s_FeatureMutex); switch (eventType) { case kUnityGfxDeviceEventInitialize: { s_RendererType = s_Graphics->GetRenderer(); switch (s_RendererType) { case kUnityGfxRendererD3D11: { s_GraphicsD3D11 = s_UnityInterfaces->Get(); if (s_GraphicsD3D11 == nullptr) { UNITY_LOG_ERROR(s_Log, "Could not obtain D3D11 Graphics interface!"); return; } break; } case kUnityGfxRendererD3D12: { s_GraphicsD3D12 = s_UnityInterfaces->Get(); if (s_GraphicsD3D12 == nullptr) { UNITY_LOG_ERROR(s_Log, "Could not obtain D3D12 Graphics interface!"); return; } break; } case kUnityGfxRendererVulkan: { s_GraphicsVulkan = s_UnityInterfaces->Get(); if (s_GraphicsVulkan == nullptr) { UNITY_LOG_ERROR(s_Log, "Could not obtain Vulkan Graphics interface!"); return; } break; } }; break; } case kUnityGfxDeviceEventShutdown: { s_GraphicsVulkan = nullptr; s_GraphicsD3D12 = nullptr; s_GraphicsD3D11 = nullptr; s_RendererType = kUnityGfxRendererNull; break; } case kUnityGfxDeviceEventBeforeReset: { break; } case kUnityGfxDeviceEventAfterReset: { break; } }; } static uint32_t AllocateFeatureSlot() { if (s_FeatureSlots.empty()) { // Create a new feature if there are no free slots uint32_t featureSlot = (uint32_t)s_Features.size(); s_Features.push_back(std::move(FSR2Feature())); memset(&s_Features[featureSlot], 0, sizeof(FSR2Feature)); return featureSlot; } // Reallocate an existing free slot uint32_t featureSlot = s_FeatureSlots.front(); s_FeatureSlots.pop(); return featureSlot; } static void FreeFeatureSlot(uint32_t featureSlot) { s_FeatureSlots.push(featureSlot); memset(&s_Features[featureSlot], 0, sizeof(FSR2Feature)); } static PFN_vkVoidFunction GetVulkanDeviceProcAddr(VkDevice device, const char* pName) { UnityVulkanInstance instance = s_GraphicsVulkan->Instance(); return instance.getInstanceProcAddr(instance.instance, pName); } extern "C" bool UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_InitApi() { std::lock_guard lock(s_FeatureMutex); if (s_GraphicsD3D11 != nullptr) { ID3D11Device* device = s_GraphicsD3D11->GetDevice(); if (device == nullptr) return false; s_Device = ffxGetDeviceDX11(device); size_t scratchBufferSize = ffxFsr2GetScratchMemorySizeDX11(); void* scratchBuffer = malloc(scratchBufferSize); ffxFsr2GetInterfaceDX11(&s_Fsr2Interface, device, scratchBuffer, scratchBufferSize); return true; } else if (s_GraphicsD3D12 != nullptr) { ID3D12Device* device = s_GraphicsD3D12->GetDevice(); if (device == nullptr) return false; s_Device = ffxGetDeviceDX12(device); s_FrameFenceEventHandle = CreateEventEx(nullptr, nullptr, 0, EVENT_ALL_ACCESS); size_t scratchBufferSize = ffxFsr2GetScratchMemorySizeDX12(); void* scratchBuffer = malloc(scratchBufferSize); ffxFsr2GetInterfaceDX12(&s_Fsr2Interface, device, scratchBuffer, scratchBufferSize); return true; } else if (s_GraphicsVulkan != nullptr) { UnityVulkanInstance instance = s_GraphicsVulkan->Instance(); if (instance.device == nullptr) return false; s_Device = ffxGetDeviceVK(instance.device); size_t scratchBufferSize = ffxFsr2GetScratchMemorySizeVK(instance.physicalDevice); void* scratchBuffer = malloc(scratchBufferSize); ffxFsr2GetInterfaceVK(&s_Fsr2Interface, scratchBuffer, scratchBufferSize, instance.physicalDevice, &GetVulkanDeviceProcAddr); return true; } return false; } static void DestroyFeature(uint32_t featureSlot) { auto& feature = s_Features[featureSlot]; if (s_GraphicsD3D12 != nullptr) { // If there's still an upscale dispatch executing on the current frame, wait until rendering is finished before destroying its context. // Thanks to https://alextardif.com/D3D11To12P1.html for explaining how fences work and how to make the CPU wait for a command queue to complete. ID3D12Fence* frameFence = s_GraphicsD3D12->GetFrameFence(); if (feature.dispatchFrameValue > frameFence->GetCompletedValue()) { frameFence->SetEventOnCompletion(feature.dispatchFrameValue, s_FrameFenceEventHandle); WaitForSingleObjectEx(s_FrameFenceEventHandle, INFINITE, false); } } else if (s_GraphicsVulkan != nullptr) { UnityVulkanRecordingState state; s_GraphicsVulkan->CommandRecordingState(&state, kUnityVulkanGraphicsQueueAccess_DontCare); if (feature.dispatchFrameValue > state.safeFrameNumber) { UnityVulkanInstance instance = s_GraphicsVulkan->Instance(); vkQueueWaitIdle(instance.graphicsQueue); } } if (feature.isValid) { ffxFsr2ContextDestroy(&feature.upscalingContext); FreeFeatureSlot(featureSlot); } } extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_ShutdownApi() { std::lock_guard lock(s_FeatureMutex); for (uint32_t slot = 0; slot < s_Features.size(); ++slot) { DestroyFeature(slot); } if (s_Fsr2Interface.scratchBuffer != nullptr) { free(s_Fsr2Interface.scratchBuffer); s_Fsr2Interface.scratchBuffer = nullptr; } if (s_FrameFenceEventHandle != nullptr) { CloseHandle(s_FrameFenceEventHandle); s_FrameFenceEventHandle = nullptr; } s_Device = nullptr; } extern "C" uint32_t UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_GetDeviceVersion() { return 0x0u; } extern "C" UnityRenderingEventAndData UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_GetRenderEventCallback() { return OnRenderEventAndData; } extern "C" UnityRenderingEventAndData UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_GetSetTextureEventCallback() { return OnSetTextureEvent; } extern "C" uint32_t UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_CreateFeatureSlot() { std::lock_guard lock(s_FeatureMutex); return AllocateFeatureSlot(); } extern "C" float UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_GetUpscaleRatioFromQualityMode(FSR2Quality qualityMode) { switch (qualityMode) { case FSR2Quality::qQuality: return 1.5f; case FSR2Quality::qBalanced: return 1.7f; case FSR2Quality::qPerformance: return 2.0f; case FSR2Quality::qUltraPerformance: return 3.0f; default: return 1.0f; } } extern "C" bool UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_GetRenderResolutionFromQualityMode(FSR2Quality qualityMode, uint32_t displayWidth, uint32_t displayHeight, uint32_t* renderWidth, uint32_t* renderHeight) { if (renderWidth == nullptr || renderHeight == nullptr) return false; float ratio = AMDUP_GetUpscaleRatioFromQualityMode(qualityMode); *renderWidth = (uint32_t)roundf(displayWidth / ratio); *renderHeight = (uint32_t)roundf(displayHeight / ratio); return true; } extern "C" int32_t UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API AMDUP_GetBaseEventId() { return BaseEventId; } static FfxResource GetVulkanTextureResource(UnityVulkanInstance& instance, FSR2Feature& feature, FSR2TextureDesc& texture, FfxResourceStates state = FFX_RESOURCE_STATE_COMPUTE_READ) { VkImageView imageView = nullptr; if (texture.image != 0) { VkImageViewCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; createInfo.image = (VkImage)texture.image; createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; createInfo.format = (VkFormat)texture.format; createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; createInfo.subresourceRange.levelCount = 1; createInfo.subresourceRange.layerCount = 1; vkCreateImageView(instance.device, &createInfo, nullptr, &imageView); } texture.view = (intptr_t)imageView; return ffxGetTextureResourceVK(&feature.upscalingContext, (VkImage)texture.image, imageView, texture.width, texture.height, (VkFormat)texture.format, nullptr, state); } static void DestroyVulkanImageView(UnityVulkanInstance& instance, FSR2TextureDesc& texture) { if (texture.view == 0) return; vkDestroyImageView(instance.device, (VkImageView)texture.view, nullptr); texture.view = 0; } // Plugin function to handle a specific rendering event static void UNITY_INTERFACE_API OnRenderEventAndData(int eventID, void* data) { std::lock_guard lock(s_FeatureMutex); if (s_Device == nullptr) return; // User rendering code switch (eventID) { case BaseEventId + FSR2PluginEvent::eDestroyFeature: { uint32_t featureSlot = (uint32_t)(int64_t)data; if (featureSlot < 0 || featureSlot >= s_Features.size()) return; DestroyFeature(featureSlot); break; } case BaseEventId + FSR2PluginEvent::eExecute: { auto* params = (FSR2CommandExecutionData*)data; if (params->featureSlot < 0 || params->featureSlot >= s_Features.size()) return; auto& feature = s_Features[params->featureSlot]; if (!feature.isValid) return; FfxFsr2DispatchDescription dispatchDescription{}; if (s_GraphicsD3D11 != nullptr) { ID3D11DeviceContext* ctx = nullptr; s_GraphicsD3D11->GetDevice()->GetImmediateContext(&ctx); dispatchDescription.commandList = ctx; dispatchDescription.color = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.colorInput.image); dispatchDescription.depth = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.depth.image); dispatchDescription.motionVectors = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.motionVectors.image); dispatchDescription.exposure = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.exposureTexture.image); dispatchDescription.reactive = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.reactiveMask.image); dispatchDescription.transparencyAndComposition = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.transparencyMask.image); dispatchDescription.output = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.colorOutput.image, nullptr, FFX_RESOURCE_STATE_UNORDERED_ACCESS); if (dispatchDescription.reactive.resource == nullptr) dispatchDescription.reactive = ffxGetResourceDX11(&feature.upscalingContext, (ID3D11Resource*)feature.textureTable.biasColorMask.image); } else if (s_GraphicsD3D12 != nullptr) { UnityGraphicsD3D12RecordingState state; s_GraphicsD3D12->CommandRecordingState(&state); dispatchDescription.commandList = ffxGetCommandListDX12(state.commandList); // Keep track of which frame this dispatch is happening on, so we can verify when it's finished feature.dispatchFrameValue = s_GraphicsD3D12->GetNextFrameFenceValue(); dispatchDescription.color = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.colorInput.image); dispatchDescription.depth = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.depth.image); dispatchDescription.motionVectors = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.motionVectors.image); dispatchDescription.exposure = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.exposureTexture.image); dispatchDescription.reactive = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.reactiveMask.image); dispatchDescription.transparencyAndComposition = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.transparencyMask.image); dispatchDescription.output = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.colorOutput.image, nullptr, FFX_RESOURCE_STATE_UNORDERED_ACCESS); if (dispatchDescription.reactive.resource == nullptr) dispatchDescription.reactive = ffxGetResourceDX12(&feature.upscalingContext, (ID3D12Resource*)feature.textureTable.biasColorMask.image); } else if (s_GraphicsVulkan != nullptr) { UnityVulkanRecordingState state; s_GraphicsVulkan->CommandRecordingState(&state, kUnityVulkanGraphicsQueueAccess_DontCare); dispatchDescription.commandList = ffxGetCommandListVK(state.commandBuffer); // Keep track of which frame this dispatch is happening on, so we can verify when it's finished feature.dispatchFrameValue = state.currentFrameNumber; UnityVulkanInstance instance = s_GraphicsVulkan->Instance(); dispatchDescription.color = GetVulkanTextureResource(instance, feature, feature.textureTable.colorInput); dispatchDescription.depth = GetVulkanTextureResource(instance, feature, feature.textureTable.depth); dispatchDescription.motionVectors = GetVulkanTextureResource(instance, feature, feature.textureTable.motionVectors); dispatchDescription.exposure = GetVulkanTextureResource(instance, feature, feature.textureTable.exposureTexture); dispatchDescription.reactive = GetVulkanTextureResource(instance, feature, feature.textureTable.reactiveMask); dispatchDescription.transparencyAndComposition = GetVulkanTextureResource(instance, feature, feature.textureTable.transparencyMask); dispatchDescription.output = GetVulkanTextureResource(instance, feature, feature.textureTable.colorOutput, FFX_RESOURCE_STATE_UNORDERED_ACCESS); if (dispatchDescription.reactive.resource == nullptr) dispatchDescription.reactive = GetVulkanTextureResource(instance, feature, feature.textureTable.biasColorMask); } dispatchDescription.jitterOffset.x = params->jitterOffsetX; dispatchDescription.jitterOffset.y = params->jitterOffsetY; dispatchDescription.motionVectorScale.x = params->MVScaleX; dispatchDescription.motionVectorScale.y = params->MVScaleY; dispatchDescription.reset = params->reset; dispatchDescription.enableSharpening = params->enableSharpening; dispatchDescription.sharpness = params->sharpness; dispatchDescription.frameTimeDelta = params->frameTimeDelta; dispatchDescription.preExposure = params->preExposure; dispatchDescription.renderSize.width = params->renderSizeWidth; dispatchDescription.renderSize.height = params->renderSizeHeight; dispatchDescription.cameraFovAngleVertical = params->cameraFovAngleVertical; if (feature.flags & FFX_FSR2_ENABLE_DEPTH_INVERTED) { dispatchDescription.cameraFar = params->cameraNear; dispatchDescription.cameraNear = params->cameraFar; } else { dispatchDescription.cameraFar = params->cameraFar; dispatchDescription.cameraNear = params->cameraNear; } ffxFsr2ContextDispatch(&feature.upscalingContext, &dispatchDescription); break; } case BaseEventId + FSR2PluginEvent::ePostExecute: { auto* params = (FSR2CommandExecutionData*)data; if (params->featureSlot < 0 || params->featureSlot >= s_Features.size()) return; auto& feature = s_Features[params->featureSlot]; if (s_GraphicsVulkan != nullptr) { UnityVulkanInstance instance = s_GraphicsVulkan->Instance(); DestroyVulkanImageView(instance, feature.textureTable.colorInput); DestroyVulkanImageView(instance, feature.textureTable.depth); DestroyVulkanImageView(instance, feature.textureTable.motionVectors); DestroyVulkanImageView(instance, feature.textureTable.exposureTexture); DestroyVulkanImageView(instance, feature.textureTable.reactiveMask); DestroyVulkanImageView(instance, feature.textureTable.transparencyMask); DestroyVulkanImageView(instance, feature.textureTable.colorOutput); } break; } case BaseEventId + FSR2PluginEvent::eInit: { auto* params = (FSR2CommandInitializationData*)data; if (params->featureSlot < 0 || params->featureSlot >= s_Features.size()) return; auto& feature = s_Features[params->featureSlot]; feature.flags = params->flags; FfxFsr2ContextDescription contextDescription{}; contextDescription.callbacks = s_Fsr2Interface; contextDescription.device = s_Device; contextDescription.displaySize = { params->displaySizeWidth, params->displaySizeHeight }; contextDescription.maxRenderSize = { params->maxRenderSizeWidth, params->maxRenderSizeHeight }; contextDescription.flags = params->flags; feature.isValid = ffxFsr2ContextCreate(&feature.upscalingContext, &contextDescription) == FFX_OK; break; } } } static void UNITY_INTERFACE_API OnSetTextureEvent(int eventID, void* data) { std::lock_guard lock(s_FeatureMutex); auto* params = (UnityRenderingExtTextureUpdateParamsV2*)data; // userData = (featureId & (int) ushort.MaxValue) << 16 | (textureSlot & (int) short.MaxValue) << 1 | (clearTextureTable ? 1 : 0); uint32_t featureSlot = (params->userData >> 16) & 0xFFFF; uint32_t textureSlot = (params->userData >> 1) & 0x7FFF; uint32_t clearTextureTable = params->userData & 0x1; if (featureSlot < 0 || featureSlot >= s_Features.size()) return; auto& feature = s_Features[featureSlot]; // User rendering code switch (eventID) { case kUnityRenderingExtEventUpdateTextureBeginV2: { if (clearTextureTable) { memset(&feature.textureTable, 0, sizeof(FSR2TextureTable)); } break; } case kUnityRenderingExtEventUpdateTextureEndV2: { // We organized the texture table struct to be ordered the same as the texture slot enum // This way we can use the texture slot value simply as a pointer offset into the texture table FSR2TextureDesc* textureDesc = ((FSR2TextureDesc*)&feature.textureTable) + textureSlot; if (s_GraphicsD3D11 != nullptr) { textureDesc->image = (intptr_t)s_GraphicsD3D11->TextureFromNativeTexture((UnityTextureID)params->textureID); } else if (s_GraphicsD3D12 != nullptr) { textureDesc->image = (intptr_t)s_GraphicsD3D12->TextureFromNativeTexture((UnityTextureID)params->textureID); } else if (s_GraphicsVulkan != nullptr) { VkAccessFlags accessFlags = (textureSlot == FSR2Textures::tColorOutput) ? VK_ACCESS_SHADER_WRITE_BIT : VK_ACCESS_SHADER_READ_BIT; UnityVulkanImage image; if (s_GraphicsVulkan->AccessTextureByID((UnityTextureID)params->textureID, UnityVulkanWholeImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, accessFlags, kUnityVulkanResourceAccess_PipelineBarrier, &image)) { textureDesc->image = (intptr_t)image.image; textureDesc->view = 0; textureDesc->width = params->width; textureDesc->height = params->height; textureDesc->format = (uint32_t)image.format; } else { textureDesc->image = 0; textureDesc->view = 0; } } break; } } }