From 78f422310d82d4c76f14b458c35e3a62c4bcd21d Mon Sep 17 00:00:00 2001 From: DreadHeadHippy <155098676+DreadHeadHippy@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:12:54 -0400 Subject: [PATCH] Replay export: MP4 export works on PCs with an AMD iGPU beside another GPU The game takes the first H.264 encoder Media Foundation lists, and that is the AMD one even when the game renders on another vendor's GPU. It cannot take frames from that device, so the export ended in 'Failed to save video'. Hardware encoders of a vendor other than the render GPU's are now left out of the video encoder list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Extension/Rendering/replay_export.cpp | 67 ++++++++++++++++++++++++ Extension/Rendering/replay_export.h | 3 ++ Extension/UI/Overlay/gpu_diagnostics.cpp | 16 ++++++ Extension/UI/Overlay/gpu_diagnostics.h | 2 + 4 files changed, 88 insertions(+) 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; }