Merge pull request #305 from DreadHeadHippy/amd-mp4-video-export

Replay export: MP4 export works on PCs with an AMD iGPU beside another GPU
This commit is contained in:
zeex64 authored and GitHub committed 2026-10-21 18:38:56 -05:00
commit 8a14996980
4 files changed
+88

No files matched your search

+67
View File
@@ -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 <array>
#include <atomic>
#include <cstring>
#include <vector>
#include <Windows.h>
#include <dxgi1_4.h>
#include <wrl/client.h>
#include <mfidl.h>
#include <mftransform.h>
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<std::uint8_t*>(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<EnumTransforms> 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<IDXGIFactory4> factory;
Microsoft::WRL::ComPtr<IDXGIAdapter1> 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<IMFActivate*> 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<UINT>(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<UINT32>(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<void*>(GetProcAddress(mfplat, "MFTEnumEx"));
void* previous{};
if (target && hook_prepare(target, reinterpret_cast<void*>(&enum_transforms), &previous) == HookOk) {
original_enum = reinterpret_cast<EnumTransforms>(previous);
if (hook_enable(target) != HookOk) hook_remove(target);
}
}
std::array<unsigned char, native::submit_frame_prefix.size()> bytes{};
if (!base || !memory::read_bytes(base + native::submit_frame, bytes.data(), bytes.size()) ||
bytes != native::submit_frame_prefix) {
+3
View File
@@ -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;
}
+16
View File
@@ -1068,9 +1068,25 @@ void watch_draws(ID3D12GraphicsCommandList* list) noexcept {
SetLastError(error);
}
namespace {
std::atomic<std::uint64_t> render_luid{};
}
bool render_adapter_luid(LUID& luid) noexcept {
const auto value = render_luid.load();
if (!value) return false;
luid.LowPart = static_cast<DWORD>(value);
luid.HighPart = static_cast<LONG>(value >> 32);
return true;
}
void record_gpu_identity(ID3D12Device* device) noexcept {
static std::atomic<bool> recorded{};
const auto error = GetLastError();
if (device) {
const auto luid = device->GetAdapterLuid();
render_luid = (static_cast<std::uint64_t>(static_cast<std::uint32_t>(luid.HighPart)) << 32) | luid.LowPart;
}
try {
ComPtr<IDXGIFactory4> factory;
ComPtr<IDXGIAdapter1> adapter;
+2
View File
@@ -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;
}