diff --git a/Extension/Rendering/replay_export.cpp b/Extension/Rendering/replay_export.cpp index 1248d279..95c01805 100644 --- a/Extension/Rendering/replay_export.cpp +++ b/Extension/Rendering/replay_export.cpp @@ -5,10 +5,16 @@ #include "Engine/Game/Build/addresses.h" #include "Engine/Game/Build/20260929/replay_export.h" #include "Engine/Game/Build/supported_build.h" +#include "Extension/UI/Overlay/gpu_diagnostics.h" #include #include #include +#include #include +#include +#include +#include +#include namespace dingosdk::replay_export { namespace { @@ -27,10 +33,71 @@ void submit(std::uintptr_t encoder, std::uintptr_t pixels, std::uintptr_t pitch, *reinterpret_cast(encoder + native::frame_permit) = 1; original.load()(encoder, pixels, pitch, unused); } + +// A hardware H.264 encoder only accepts frames from a device on its own GPU. On +// a PC with two GPUs (an AMD processor's integrated graphics beside a graphics +// card) the encoder the game gets first can belong to the GPU it does not render +// on, and the export fails with "Failed to save video". +using EnumTransforms = HRESULT(WINAPI*)(GUID, UINT32, const MFT_REGISTER_TYPE_INFO*, const MFT_REGISTER_TYPE_INFO*, + IMFActivate***, UINT32*); +std::atomic original_enum{}; +constexpr GUID video_encoder_category{0xf79eac7d, 0xe545, 0x4387, {0xbd, 0xee, 0xd6, 0x47, 0xd7, 0xbd, 0xe4, 0x2a}}; +constexpr GUID hardware_vendor_id{0x3aecb0cc, 0x035b, 0x4bcc, {0x81, 0x85, 0x2b, 0x8d, 0x55, 0x1e, 0xf3, 0xaf}}; + +// The encoders do not say which adapter they sit on, only their vendor ("VEN_1002"). +UINT render_vendor() noexcept { + LUID render{}; + if (!overlay::render_adapter_luid(render)) return 0; + Microsoft::WRL::ComPtr factory; + Microsoft::WRL::ComPtr adapter; + DXGI_ADAPTER_DESC1 description{}; + if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))) || + FAILED(factory->EnumAdapterByLuid(render, IID_PPV_ARGS(&adapter))) || FAILED(adapter->GetDesc1(&description))) + return 0; + return description.VendorId; +} + +HRESULT WINAPI enum_transforms(GUID category, UINT32 flags, const MFT_REGISTER_TYPE_INFO* input, + const MFT_REGISTER_TYPE_INFO* output, IMFActivate*** activates, UINT32* count) { + const HRESULT result = original_enum.load()(category, flags, input, output, activates, count); + if (FAILED(result) || !IsEqualGUID(category, video_encoder_category) || !activates || !*activates || !count) + return result; + const UINT wanted = render_vendor(); + if (!wanted) return result; + + std::vector kept, dropped; + for (UINT32 i = 0; i < *count; ++i) { + IMFActivate* const activate = (*activates)[i]; + wchar_t vendor[64]{}; + UINT32 length{}; + bool foreign{}; + if (activate && SUCCEEDED(activate->GetString(hardware_vendor_id, vendor, 64, &length))) { + const wchar_t* const id = wcsstr(vendor, L"VEN_"); + foreign = id && static_cast(wcstoul(id + 4, nullptr, 16)) != wanted; + } + (foreign ? dropped : kept).push_back(activate); + } + if (dropped.empty() || kept.empty()) return result; + for (std::size_t i = 0; i < kept.size(); ++i) (*activates)[i] = kept[i]; + *count = static_cast(kept.size()); + for (auto* const activate : dropped) activate->Release(); + logging::write(logging::Level::info, logging::Channel::graphics, + "Replay video export skipped hardware encoders that are not on the graphics card the game renders on."); + return result; +} } // namespace bool start(std::uintptr_t base) noexcept { try { + if (const auto mfplat = LoadLibraryW(L"mfplat.dll")) { + void* const target = reinterpret_cast(GetProcAddress(mfplat, "MFTEnumEx")); + void* previous{}; + if (target && hook_prepare(target, reinterpret_cast(&enum_transforms), &previous) == HookOk) { + original_enum = reinterpret_cast(previous); + if (hook_enable(target) != HookOk) hook_remove(target); + } + } + std::array bytes{}; if (!base || !memory::read_bytes(base + native::submit_frame, bytes.data(), bytes.size()) || bytes != native::submit_frame_prefix) { diff --git a/Extension/Rendering/replay_export.h b/Extension/Rendering/replay_export.h index 296ec215..8a907e55 100644 --- a/Extension/Rendering/replay_export.h +++ b/Extension/Rendering/replay_export.h @@ -7,5 +7,8 @@ namespace dingosdk::replay_export { // drops the rest. Rendering faster than it updates (an uncapped frame rate), // an export came out with a fraction of its frames and played that much too // fast. Every rendered frame of a replay export is encoded instead. +// MP4 export on a PC with GPUs from two vendors (an AMD processor's integrated +// graphics beside a graphics card) no longer picks the hardware encoder of the +// GPU the game does not render on. bool start(std::uintptr_t base) noexcept; } diff --git a/Extension/UI/Overlay/gpu_diagnostics.cpp b/Extension/UI/Overlay/gpu_diagnostics.cpp index 3a4a6674..5dd51482 100644 --- a/Extension/UI/Overlay/gpu_diagnostics.cpp +++ b/Extension/UI/Overlay/gpu_diagnostics.cpp @@ -1068,9 +1068,25 @@ void watch_draws(ID3D12GraphicsCommandList* list) noexcept { SetLastError(error); } +namespace { +std::atomic render_luid{}; +} + +bool render_adapter_luid(LUID& luid) noexcept { + const auto value = render_luid.load(); + if (!value) return false; + luid.LowPart = static_cast(value); + luid.HighPart = static_cast(value >> 32); + return true; +} + void record_gpu_identity(ID3D12Device* device) noexcept { static std::atomic recorded{}; const auto error = GetLastError(); + if (device) { + const auto luid = device->GetAdapterLuid(); + render_luid = (static_cast(static_cast(luid.HighPart)) << 32) | luid.LowPart; + } try { ComPtr factory; ComPtr adapter; diff --git a/Extension/UI/Overlay/gpu_diagnostics.h b/Extension/UI/Overlay/gpu_diagnostics.h index a5986ae6..3d148940 100644 --- a/Extension/UI/Overlay/gpu_diagnostics.h +++ b/Extension/UI/Overlay/gpu_diagnostics.h @@ -42,4 +42,6 @@ void watch_addresses(ID3D12GraphicsCommandList* list) noexcept; // The graphics card the game renders on and its driver's version: said in the log once, and kept // for crash reports' tags (crash_report::set_gpu). Always on. void record_gpu_identity(ID3D12Device* device) noexcept; +// The adapter the game renders on, once record_gpu_identity has seen its device. +bool render_adapter_luid(LUID& luid) noexcept; }