Files
ReSkate/Extension/UI/Overlay/gpu_diagnostics.cpp
T
AyeZeeBB f92d405979 GPU: after the GPU stops, the next three launches record its breadcrumbs (DRED) for the crash report
The device-removal watcher always runs, and the fatal handler notes a DXGI stop in gpu-breadcrumbs.txt. Crash reports carry a gpu.breadcrumbs tag (protocol 5).
2026-10-18 16:33:52 -05:00

1207 lines
68 KiB
C++

#include "Engine/Core/Log/logging.h"
#include "gpu_diagnostics.h"
#include "Engine/Core/Hooks/hooks.h"
#include "Engine/Core/Debug/crash_report.h"
#include <dxgi1_4.h>
#include <ShlObj.h>
#include <wrl/client.h>
#include <intrin.h>
#include <format>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <filesystem>
#include <map>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
namespace dingosdk::overlay {
namespace {
using Microsoft::WRL::ComPtr;
struct Log {
std::atomic<bool> removal_recorded{};
std::atomic<bool> breadcrumbs{};
};
Log& log() { static auto* value = new Log; return *value; }
// After the GPU stops responding, this many launches record its breadcrumbs: enough for one of them
// to catch the next stop, where it comes every few sessions.
constexpr int breadcrumb_launches = 3;
// %LOCALAPPDATA%\ReSkate\gpu-breadcrumbs.txt: how many launches are still to record them.
std::filesystem::path breadcrumbs_note() {
PWSTR local{};
std::filesystem::path path;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_DONT_VERIFY, nullptr, &local)))
path = std::filesystem::path(local) / L"ReSkate" / L"gpu-breadcrumbs.txt";
CoTaskMemFree(local);
return path;
}
int breadcrumbs_left() {
const auto path = breadcrumbs_note();
int left{};
if (FILE* file{}; !path.empty() && _wfopen_s(&file, path.c_str(), L"r") == 0 && file) {
if (fscanf_s(file, "%d", &left) != 1) left = 0;
std::fclose(file);
}
return std::clamp(left, 0, breadcrumb_launches);
}
void set_breadcrumbs_left(int left) {
const auto path = breadcrumbs_note();
if (path.empty()) return;
std::error_code error;
if (left <= 0) {
std::filesystem::remove(path, error);
return;
}
std::filesystem::create_directories(path.parent_path(), error);
if (FILE* file{}; _wfopen_s(&file, path.c_str(), L"w") == 0 && file) {
std::fprintf(file, "%d\n", left);
std::fclose(file);
}
}
}
bool gpu_breadcrumbs() noexcept { return log().breadcrumbs.load(); }
void note_gpu_stopped(DWORD exception_code) noexcept {
const auto error = GetLastError();
switch (exception_code) {
case static_cast<DWORD>(DXGI_ERROR_DEVICE_REMOVED):
case static_cast<DWORD>(DXGI_ERROR_DEVICE_HUNG):
case static_cast<DWORD>(DXGI_ERROR_DEVICE_RESET):
case static_cast<DWORD>(DXGI_ERROR_DRIVER_INTERNAL_ERROR):
try { set_breadcrumbs_left(breadcrumb_launches); } catch (...) {}
break;
default:
break;
}
SetLastError(error);
}
bool graphics_option_enabled(const wchar_t* name) noexcept {
wchar_t value[2]{};
return GetEnvironmentVariableW(name, value, 2) == 1 && value[0] == L'1';
}
void initialize_graphics_diagnostics() noexcept {
const auto error = GetLastError();
try {
const bool diagnostic = graphics_option_enabled(L"RESKATE_GPU_DIAGNOSTICS");
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "Graphics startup: gpu_diagnostics=%d", diagnostic);
// The launches after one in which the GPU stopped responding record its breadcrumbs, for
// the report of the next stop. This one counts as one of them.
const int left = breadcrumbs_left();
if (left > 0) {
set_breadcrumbs_left(left - 1);
dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics,
"GPU breadcrumbs are on for this launch: the GPU stopped responding in a recent session (%d more launch(es) after this one).",
left - 1);
}
log().breadcrumbs = diagnostic || left > 0;
if (log().breadcrumbs) {
// DRED must be configured before the game creates its D3D12 device.
// Do not enable the debug layer or change the game's device flags.
ComPtr<ID3D12DeviceRemovedExtendedDataSettings> settings;
const auto result = D3D12GetDebugInterface(IID_PPV_ARGS(&settings));
dingosdk::logging::printf(dingosdk::logging::Level::debug, dingosdk::logging::Channel::graphics, "DRED settings result=0x%08lx", static_cast<unsigned long>(result));
if (SUCCEEDED(result)) {
settings->SetAutoBreadcrumbsEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON);
settings->SetPageFaultEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON);
}
}
} catch (...) {}
SetLastError(error);
}
namespace {
// D3D12_AUTO_BREADCRUMB_OP, by value.
const char* operation_name(D3D12_AUTO_BREADCRUMB_OP op) noexcept {
static constexpr const char* names[]{"SetMarker", "BeginEvent", "EndEvent", "DrawInstanced",
"DrawIndexedInstanced", "ExecuteIndirect", "Dispatch", "CopyBufferRegion", "CopyTextureRegion", "CopyResource",
"CopyTiles", "ResolveSubresource", "ClearRenderTargetView", "ClearUnorderedAccessView", "ClearDepthStencilView",
"ResourceBarrier", "ExecuteBundle", "Present", "ResolveQueryData", "BeginSubmission", "EndSubmission",
"DecodeFrame", "ProcessFrames", "AtomicCopyBufferUint", "AtomicCopyBufferUint64", "ResolveSubresourceRegion",
"WriteBufferImmediate", "DecodeFrame1", "SetProtectedResourceSession", "DecodeFrame2", "ProcessFrames1",
"BuildRaytracingAccelerationStructure", "EmitRaytracingAccelerationStructurePostbuildInfo",
"CopyRaytracingAccelerationStructure", "DispatchRays", "InitializeMetaCommand", "ExecuteMetaCommand",
"EstimateMotion", "ResolveMotionVectorHeap", "SetPipelineState1", "InitializeExtensionCommand",
"ExecuteExtensionCommand", "DispatchMesh", "EncodeFrame", "ResolveEncoderOutputMetadata", "Barrier",
"BeginCommandList", "DispatchGraph", "SetProgram"};
const auto index = static_cast<std::size_t>(op);
return index < std::size(names) ? names[index] : "?";
}
// D3D12_DRED_ALLOCATION_TYPE, by value.
const char* allocation_type(D3D12_DRED_ALLOCATION_TYPE type) noexcept {
switch (static_cast<int>(type)) {
case 19: return "command queue"; case 20: return "command allocator"; case 21: return "pipeline state";
case 22: return "command list"; case 23: return "fence"; case 24: return "descriptor heap"; case 25: return "heap";
case 27: return "query heap"; case 28: return "command signature"; case 29: return "pipeline library";
case 34: return "resource"; case 35: return "pass"; case 42: return "state object"; case 43: return "meta command";
default: return "other";
}
}
std::string narrow(const char* a, const wchar_t* w) {
if (a && *a) return a;
std::string text;
for (; w && *w && text.size() < 200; ++w) text.push_back(*w < 128 ? static_cast<char>(*w) : '?');
return text.empty() ? std::string("(unnamed)") : text;
}
void allocations(const char* what, const D3D12_DRED_ALLOCATION_NODE1* node) {
unsigned shown{}, total{};
for (; node; node = node->pNext, ++total)
if (shown < 24) {
++shown;
dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics, " %s: %s \"%s\"", what,
allocation_type(node->AllocationType), narrow(node->ObjectNameA, node->ObjectNameW).c_str());
}
if (total > shown)
dingosdk::logging::printf(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics, " (%u more %s)", total - shown, what);
}
}
namespace {
// How much video memory the game uses against what Windows lets it have, once a second: said when
// it moves by a quarter of a gigabyte, and when it passes 90% or all of the budget. Past the
// budget, Windows moves the game's memory out of the card, and a game that does not bring it back
// before use reads memory that is not there. The last figures before a GPU failure say whether
// that is how it came about.
void watch_video_memory(LUID adapter_luid) {
SetThreadDescription(GetCurrentThread(), L"ReSkate video memory watch");
ComPtr<IDXGIFactory4> factory;
ComPtr<IDXGIAdapter3> adapter;
if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))) || FAILED(factory->EnumAdapterByLuid(adapter_luid, IID_PPV_ARGS(&adapter)))) return;
using dingosdk::logging::Level;
using dingosdk::logging::Channel;
constexpr double gigabyte = 1024.0 * 1024.0 * 1024.0;
std::uint64_t said{};
bool was_near{}, was_over{};
for (;;) {
DXGI_QUERY_VIDEO_MEMORY_INFO local{}, shared{};
if (SUCCEEDED(adapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &local)) &&
SUCCEEDED(adapter->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP_NON_LOCAL, &shared)) && local.Budget) {
const auto moved = local.CurrentUsage > said ? local.CurrentUsage - said : said - local.CurrentUsage;
const bool now_near = local.CurrentUsage * 10 >= local.Budget * 9, now_over = local.CurrentUsage > local.Budget;
if (moved >= (256ull << 20) || now_near != was_near || now_over != was_over) {
said = local.CurrentUsage;
was_near = now_near;
was_over = now_over;
dingosdk::logging::printf(now_over ? Level::warning : Level::info, Channel::graphics,
"Video memory: %.2f GB of a %.2f GB budget (%.0f%%)%s; %.2f GB of system memory for the GPU.",
local.CurrentUsage / gigabyte, local.Budget / gigabyte, 100.0 * local.CurrentUsage / local.Budget,
now_over ? ", over it: Windows is moving the game's memory out of the card" : "", shared.CurrentUsage / gigabyte);
}
}
Sleep(1000);
}
}
}
namespace {
using DrawInstanced = void (STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, UINT, UINT, UINT);
using DrawIndexedInstanced = void (STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, UINT, UINT, INT, UINT);
std::atomic<DrawInstanced> original_draw{};
std::atomic<DrawIndexedInstanced> original_draw_indexed{};
constexpr std::uint64_t large_draw = 10'000'000;
std::string code_place(void* address) {
HMODULE module{};
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
static_cast<LPCWSTR>(address), &module) || !module)
return std::format("{:#x}", reinterpret_cast<std::uintptr_t>(address));
wchar_t path[MAX_PATH]{};
GetModuleFileNameW(module, path, MAX_PATH);
const wchar_t* name = path;
for (const wchar_t* c = path; *c; ++c) if (*c == L'\\') name = c + 1;
std::string text;
for (; *name; ++name) text.push_back(*name < 128 ? static_cast<char>(*name) : '?');
return std::format("{}+{:#x}", text, reinterpret_cast<std::uintptr_t>(address) - reinterpret_cast<std::uintptr_t>(module));
}
// Said the first time a caller makes a large draw, and again whenever it doubles its largest.
__declspec(noinline) void note_large_draw(const char* kind, bool indexed, UINT count, UINT instances, void* caller) noexcept {
static std::mutex mutex;
static std::array<std::pair<void*, std::uint64_t>, 64> seen{};
const auto size = std::uint64_t{count} * instances;
try {
std::lock_guard lock(mutex);
auto* slot = &seen.back();
for (auto& entry : seen)
if (entry.first == caller || !entry.first) { slot = &entry; break; }
if (slot->first == caller && size < slot->second * 2) return;
*slot = {caller, size};
std::array<void*, 8> frames{};
const auto depth = CaptureStackBackTrace(1, static_cast<DWORD>(frames.size()), frames.data(), nullptr);
std::string stack;
for (USHORT i = 0; i < depth; ++i) stack += " < " + code_place(frames[i]);
dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
"Large draw: {} of {} {} x {} instance(s) = {} from{}", kind, count,
indexed ? "indices" : "vertices", instances, size, stack);
} catch (...) {}
}
void STDMETHODCALLTYPE hooked_draw(ID3D12GraphicsCommandList* list, UINT vertices, UINT instances, UINT first_vertex, UINT first_instance) {
if (std::uint64_t{vertices} * instances >= large_draw) note_large_draw("DrawInstanced", false, vertices, instances, _ReturnAddress());
original_draw.load(std::memory_order_relaxed)(list, vertices, instances, first_vertex, first_instance);
}
void STDMETHODCALLTYPE hooked_draw_indexed(ID3D12GraphicsCommandList* list, UINT indices, UINT instances, UINT first_index, INT base_vertex,
UINT first_instance) {
if (std::uint64_t{indices} * instances >= large_draw) note_large_draw("DrawIndexedInstanced", true, indices, instances, _ReturnAddress());
original_draw_indexed.load(std::memory_order_relaxed)(list, indices, instances, first_index, base_vertex, first_instance);
}
}
namespace {
// Where the game's GPU memory came from and where it went: every heap and resource the game's
// device makes, with the code that made it and, once its last reference goes, the code that let
// it go. A page fault at memory freed shortly before is then said with both, which names the
// engine system that freed memory the GPU was still using.
struct Frames {
std::array<void*, 14> at{};
USHORT depth{};
};
struct Allocation {
bool heap{};
void* object{};
D3D12_RESOURCE_DIMENSION dimension{};
std::uint64_t bytes{}, gpu{}; // (gpu: the first GPU address, 0 while it is not known)
void* in_heap{};
std::uint64_t offset{};
bool ranged{}; // (in Memory::ranges, under gpu)
Frames created, freed;
ULONGLONG freed_at{};
};
constexpr std::size_t freed_kept = 32768;
struct Memory {
std::mutex mutex;
std::unordered_map<void*, Allocation> live;
std::vector<Allocation> freed = std::vector<Allocation>(freed_kept);
std::uint64_t freed_count{}, made_count{};
ID3D12Device* device{};
// The GPU address ranges that exist: committed and reserved buffers, and heaps once a buffer
// placed in one has given its address. Read on every checked bind; a removal bumps the
// generation, which drops each thread's cached range.
std::shared_mutex ranges_mutex;
std::map<std::uint64_t, std::pair<std::uint64_t, void*>> ranges;
std::atomic<std::uint64_t> generation{};
};
Memory& gpu_memory() { static auto* value = new Memory; return *value; }
// (Callers hold Memory::mutex: it is taken before ranges_mutex.)
void add_range(Allocation& a) {
if (!a.gpu || !a.bytes || a.ranged) return;
auto& m = gpu_memory();
std::unique_lock lock(m.ranges_mutex);
m.ranges[a.gpu] = {a.bytes, a.object};
a.ranged = true;
}
void remove_range(const Allocation& a) {
if (!a.ranged) return;
auto& m = gpu_memory();
std::unique_lock lock(m.ranges_mutex);
if (const auto found = m.ranges.find(a.gpu); found != m.ranges.end() && found->second.second == a.object) m.ranges.erase(found);
m.generation.fetch_add(1, std::memory_order_relaxed);
}
Frames capture_frames() noexcept {
Frames frames;
frames.depth = CaptureStackBackTrace(2, static_cast<DWORD>(frames.at.size()), frames.at.data(), nullptr);
return frames;
}
// Release, as the runtime's objects share it: a few distinct functions at most (heaps and
// resources), each hooked once, each with its own forwarder.
using Release = ULONG (STDMETHODCALLTYPE*)(IUnknown*);
constexpr std::size_t release_slots = 4;
std::array<std::atomic<void*>, release_slots> release_targets{};
std::array<std::atomic<Release>, release_slots> release_originals{};
void note_release(IUnknown* object) noexcept {
try {
auto& m = gpu_memory();
Frames frames = capture_frames();
std::lock_guard lock(m.mutex);
const auto found = m.live.find(object);
if (found == m.live.end()) return;
remove_range(found->second);
auto& slot = m.freed[m.freed_count++ % freed_kept];
slot = found->second;
slot.freed = frames;
slot.freed_at = GetTickCount64();
m.live.erase(found);
} catch (...) {}
}
template<std::size_t N> ULONG STDMETHODCALLTYPE hooked_release(IUnknown* object) {
const auto count = release_originals[N].load(std::memory_order_relaxed)(object);
if (!count) note_release(object);
return count;
}
constexpr Release release_forwarders[release_slots]{&hooked_release<0>, &hooked_release<1>, &hooked_release<2>, &hooked_release<3>};
void watch_release(IUnknown* object) noexcept {
static std::mutex mutex;
auto* const target = (*reinterpret_cast<void***>(object))[2];
std::lock_guard lock(mutex);
for (std::size_t i = 0; i < release_slots; ++i) {
if (release_targets[i].load() == target) return;
if (release_targets[i].load()) continue;
void* original{};
if (hook_prepare(target, reinterpret_cast<void*>(release_forwarders[i]), &original) != HookOk) return;
release_originals[i].store(reinterpret_cast<Release>(original));
release_targets[i].store(target);
hook_enable(target);
return;
}
}
void note_heap(void** out, const D3D12_HEAP_DESC* desc) noexcept {
if (!out || !*out || !desc) return;
try {
auto* const object = static_cast<IUnknown*>(*out);
Allocation made{true, object, D3D12_RESOURCE_DIMENSION_UNKNOWN, desc->SizeInBytes};
made.created = capture_frames();
watch_release(object);
auto& m = gpu_memory();
std::lock_guard lock(m.mutex);
m.live[object] = made;
++m.made_count;
} catch (...) {}
}
void note_resource(void** out, void* heap, std::uint64_t offset) noexcept {
if (!out || !*out) return;
try {
auto* const object = static_cast<IUnknown*>(*out);
ComPtr<ID3D12Resource> resource;
if (FAILED(object->QueryInterface(IID_PPV_ARGS(&resource)))) return;
const auto desc = resource->GetDesc();
Allocation made{false, object, desc.Dimension};
made.in_heap = heap;
made.offset = offset;
if (desc.Dimension == D3D12_RESOURCE_DIMENSION_BUFFER) {
made.bytes = desc.Width;
made.gpu = resource->GetGPUVirtualAddress();
} else if (auto* const device = gpu_memory().device) {
// (UINT64_MAX: the runtime could not size it.)
if (const auto size = device->GetResourceAllocationInfo(0, 1, &desc).SizeInBytes; size != UINT64_MAX) made.bytes = size;
}
made.created = capture_frames();
watch_release(object);
auto& m = gpu_memory();
std::lock_guard lock(m.mutex);
// (A buffer placed in a heap gives the heap's GPU address, and the heap's gives a texture's.)
if (const auto found = heap ? m.live.find(heap) : m.live.end(); found != m.live.end()) {
if (made.gpu && !found->second.gpu) {
found->second.gpu = made.gpu - offset;
add_range(found->second);
} else if (!made.gpu && found->second.gpu) {
made.gpu = found->second.gpu + offset;
}
}
if (!heap) add_range(made);
m.live[object] = made;
++m.made_count;
} catch (...) {}
}
using CreateCommitted = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_PROPERTIES*, D3D12_HEAP_FLAGS,
const D3D12_RESOURCE_DESC*, D3D12_RESOURCE_STATES, const D3D12_CLEAR_VALUE*, REFIID, void**);
using CreateHeap = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_DESC*, REFIID, void**);
using CreatePlaced = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, ID3D12Heap*, UINT64, const D3D12_RESOURCE_DESC*,
D3D12_RESOURCE_STATES, const D3D12_CLEAR_VALUE*, REFIID, void**);
using CreateCommitted1 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_PROPERTIES*, D3D12_HEAP_FLAGS,
const D3D12_RESOURCE_DESC*, D3D12_RESOURCE_STATES, const D3D12_CLEAR_VALUE*, ID3D12ProtectedResourceSession*, REFIID, void**);
using CreateHeap1 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_DESC*, ID3D12ProtectedResourceSession*, REFIID, void**);
using CreateCommitted2 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_PROPERTIES*, D3D12_HEAP_FLAGS,
const D3D12_RESOURCE_DESC1*, D3D12_RESOURCE_STATES, const D3D12_CLEAR_VALUE*, ID3D12ProtectedResourceSession*, REFIID, void**);
using CreatePlaced1 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, ID3D12Heap*, UINT64, const D3D12_RESOURCE_DESC1*,
D3D12_RESOURCE_STATES, const D3D12_CLEAR_VALUE*, REFIID, void**);
using CreateCommitted3 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_HEAP_PROPERTIES*, D3D12_HEAP_FLAGS,
const D3D12_RESOURCE_DESC1*, D3D12_BARRIER_LAYOUT, const D3D12_CLEAR_VALUE*, ID3D12ProtectedResourceSession*, UINT32,
const DXGI_FORMAT*, REFIID, void**);
using CreatePlaced2 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, ID3D12Heap*, UINT64, const D3D12_RESOURCE_DESC1*,
D3D12_BARRIER_LAYOUT, const D3D12_CLEAR_VALUE*, UINT32, const DXGI_FORMAT*, REFIID, void**);
std::atomic<CreateCommitted> original_committed{};
std::atomic<CreateHeap> original_heap{};
std::atomic<CreatePlaced> original_placed{};
std::atomic<CreateCommitted1> original_committed1{};
std::atomic<CreateHeap1> original_heap1{};
std::atomic<CreateCommitted2> original_committed2{};
std::atomic<CreatePlaced1> original_placed1{};
std::atomic<CreateCommitted3> original_committed3{};
std::atomic<CreatePlaced2> original_placed2{};
// A resource creation that holds up its thread (the driver waiting on the GPU inside it): said with
// what was made and which game code asked, at most 40 times. A 170 ms stall every 5 s was one of
// these (2026-10-17): the game's batched GPU uploads (Skate 0x1446b3f20) making their upload buffer.
std::uint64_t creation_clock() noexcept {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
template<class Desc>
void note_slow_creation(std::uint64_t started, const D3D12_HEAP_PROPERTIES* properties, D3D12_HEAP_FLAGS heap_flags, const Desc* desc,
void* caller) noexcept {
const auto ms = static_cast<double>(creation_clock() - started) / 1e6;
if (ms < 20) return;
static std::atomic<unsigned> said{};
if (said.fetch_add(1, std::memory_order_relaxed) >= 40) return;
try {
const auto image = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
const auto at = reinterpret_cast<std::uintptr_t>(caller);
const auto where = at > image && at - image < 0x10000000 ? std::format("Skate 0x{:x}", 0x140000000 + (at - image))
: std::format("0x{:x}", at);
static constexpr const char* dimensions[]{"unknown", "buffer", "1D texture", "2D texture", "3D texture"};
const auto dimension = desc && static_cast<unsigned>(desc->Dimension) < 5 ? dimensions[desc->Dimension] : "unknown";
dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
"CreateCommittedResource took {:.1f} ms: heap type {}, heap flags {:#x}; a {} {} x {} x {}, {} mips, format {}, "
"flags {:#x}; asked for by {}.", ms, properties ? static_cast<int>(properties->Type) : -1, static_cast<unsigned>(heap_flags),
dimension, desc ? desc->Width : 0, desc ? desc->Height : 0, desc ? desc->DepthOrArraySize : 0, desc ? desc->MipLevels : 0,
desc ? static_cast<int>(desc->Format) : 0, desc ? static_cast<unsigned>(desc->Flags) : 0u, where);
} catch (...) {}
}
HRESULT STDMETHODCALLTYPE hooked_committed(ID3D12Device* device, const D3D12_HEAP_PROPERTIES* properties, D3D12_HEAP_FLAGS flags,
const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES state, const D3D12_CLEAR_VALUE* clear, REFIID riid, void** out) {
const auto started = creation_clock();
const auto result = original_committed.load()(device, properties, flags, desc, state, clear, riid, out);
note_slow_creation(started, properties, flags, desc, _ReturnAddress());
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_heap(ID3D12Device* device, const D3D12_HEAP_DESC* desc, REFIID riid, void** out) {
const auto result = original_heap.load()(device, desc, riid, out);
if (SUCCEEDED(result)) note_heap(out, desc);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_placed(ID3D12Device* device, ID3D12Heap* heap, UINT64 offset, const D3D12_RESOURCE_DESC* desc,
D3D12_RESOURCE_STATES state, const D3D12_CLEAR_VALUE* clear, REFIID riid, void** out) {
const auto result = original_placed.load()(device, heap, offset, desc, state, clear, riid, out);
if (SUCCEEDED(result)) note_resource(out, heap, offset);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_committed1(ID3D12Device* device, const D3D12_HEAP_PROPERTIES* properties, D3D12_HEAP_FLAGS flags,
const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES state, const D3D12_CLEAR_VALUE* clear, ID3D12ProtectedResourceSession* session,
REFIID riid, void** out) {
const auto started = creation_clock();
const auto result = original_committed1.load()(device, properties, flags, desc, state, clear, session, riid, out);
note_slow_creation(started, properties, flags, desc, _ReturnAddress());
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_heap1(ID3D12Device* device, const D3D12_HEAP_DESC* desc, ID3D12ProtectedResourceSession* session,
REFIID riid, void** out) {
const auto result = original_heap1.load()(device, desc, session, riid, out);
if (SUCCEEDED(result)) note_heap(out, desc);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_committed2(ID3D12Device* device, const D3D12_HEAP_PROPERTIES* properties, D3D12_HEAP_FLAGS flags,
const D3D12_RESOURCE_DESC1* desc, D3D12_RESOURCE_STATES state, const D3D12_CLEAR_VALUE* clear, ID3D12ProtectedResourceSession* session,
REFIID riid, void** out) {
const auto started = creation_clock();
const auto result = original_committed2.load()(device, properties, flags, desc, state, clear, session, riid, out);
note_slow_creation(started, properties, flags, desc, _ReturnAddress());
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_placed1(ID3D12Device* device, ID3D12Heap* heap, UINT64 offset, const D3D12_RESOURCE_DESC1* desc,
D3D12_RESOURCE_STATES state, const D3D12_CLEAR_VALUE* clear, REFIID riid, void** out) {
const auto result = original_placed1.load()(device, heap, offset, desc, state, clear, riid, out);
if (SUCCEEDED(result)) note_resource(out, heap, offset);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_committed3(ID3D12Device* device, const D3D12_HEAP_PROPERTIES* properties, D3D12_HEAP_FLAGS flags,
const D3D12_RESOURCE_DESC1* desc, D3D12_BARRIER_LAYOUT layout, const D3D12_CLEAR_VALUE* clear, ID3D12ProtectedResourceSession* session,
UINT32 formats, const DXGI_FORMAT* castable, REFIID riid, void** out) {
const auto started = creation_clock();
const auto result = original_committed3.load()(device, properties, flags, desc, layout, clear, session, formats, castable, riid, out);
note_slow_creation(started, properties, flags, desc, _ReturnAddress());
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_placed2(ID3D12Device* device, ID3D12Heap* heap, UINT64 offset, const D3D12_RESOURCE_DESC1* desc,
D3D12_BARRIER_LAYOUT layout, const D3D12_CLEAR_VALUE* clear, UINT32 formats, const DXGI_FORMAT* castable, REFIID riid, void** out) {
const auto result = original_placed2.load()(device, heap, offset, desc, layout, clear, formats, castable, riid, out);
if (SUCCEEDED(result)) note_resource(out, heap, offset);
return result;
}
using CreateReserved = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_RESOURCE_DESC*, D3D12_RESOURCE_STATES,
const D3D12_CLEAR_VALUE*, REFIID, void**);
using CreateReserved1 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_RESOURCE_DESC*, D3D12_RESOURCE_STATES,
const D3D12_CLEAR_VALUE*, ID3D12ProtectedResourceSession*, REFIID, void**);
using CreateReserved2 = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_RESOURCE_DESC*, D3D12_BARRIER_LAYOUT,
const D3D12_CLEAR_VALUE*, ID3D12ProtectedResourceSession*, UINT32, const DXGI_FORMAT*, REFIID, void**);
std::atomic<CreateReserved> original_reserved{};
std::atomic<CreateReserved1> original_reserved1{};
std::atomic<CreateReserved2> original_reserved2{};
HRESULT STDMETHODCALLTYPE hooked_reserved(ID3D12Device* device, const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES state,
const D3D12_CLEAR_VALUE* clear, REFIID riid, void** out) {
const auto result = original_reserved.load()(device, desc, state, clear, riid, out);
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_reserved1(ID3D12Device* device, const D3D12_RESOURCE_DESC* desc, D3D12_RESOURCE_STATES state,
const D3D12_CLEAR_VALUE* clear, ID3D12ProtectedResourceSession* session, REFIID riid, void** out) {
const auto result = original_reserved1.load()(device, desc, state, clear, session, riid, out);
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
HRESULT STDMETHODCALLTYPE hooked_reserved2(ID3D12Device* device, const D3D12_RESOURCE_DESC* desc, D3D12_BARRIER_LAYOUT layout,
const D3D12_CLEAR_VALUE* clear, ID3D12ProtectedResourceSession* session, UINT32 formats, const DXGI_FORMAT* castable, REFIID riid,
void** out) {
const auto result = original_reserved2.load()(device, desc, layout, clear, session, formats, castable, riid, out);
if (SUCCEEDED(result)) note_resource(out, nullptr, 0);
return result;
}
bool watch_device_views(ID3D12Device* device) noexcept;
// Slots of ID3D12Device and its later versions (d3d12.h): CreateCommittedResource 27, CreateHeap
// 28, CreatePlacedResource 29, CreateReservedResource 30; Device4 CreateCommittedResource1 53,
// CreateHeap1 54, CreateReservedResource1 55; Device8 CreateCommittedResource2 69,
// CreatePlacedResource1 70; Device10 CreateCommittedResource3 76, CreatePlacedResource2 77,
// CreateReservedResource2 78.
template<class F> bool hook_slot(IUnknown* object, std::size_t slot, void* replacement, std::atomic<F>& original) {
auto* const target = (*reinterpret_cast<void***>(object))[slot];
void* trampoline{};
if (hook_prepare(target, replacement, &trampoline) != HookOk) return false;
original.store(reinterpret_cast<F>(trampoline));
return hook_enable(target) == HookOk;
}
void watch_device_memory(IUnknown* made) noexcept {
static std::atomic<bool> watching{};
ComPtr<ID3D12Device> device;
if (!made || FAILED(made->QueryInterface(IID_PPV_ARGS(&device))) || watching.exchange(true)) return;
gpu_memory().device = device.Get(); // (the game keeps its device for the life of the process)
bool on = hook_slot(device.Get(), 27, reinterpret_cast<void*>(&hooked_committed), original_committed);
on &= hook_slot(device.Get(), 28, reinterpret_cast<void*>(&hooked_heap), original_heap);
on &= hook_slot(device.Get(), 29, reinterpret_cast<void*>(&hooked_placed), original_placed);
on &= hook_slot(device.Get(), 30, reinterpret_cast<void*>(&hooked_reserved), original_reserved);
if (ComPtr<ID3D12Device4> device4; SUCCEEDED(device.As(&device4))) {
on &= hook_slot(device4.Get(), 53, reinterpret_cast<void*>(&hooked_committed1), original_committed1);
on &= hook_slot(device4.Get(), 54, reinterpret_cast<void*>(&hooked_heap1), original_heap1);
on &= hook_slot(device4.Get(), 55, reinterpret_cast<void*>(&hooked_reserved1), original_reserved1);
}
if (ComPtr<ID3D12Device8> device8; SUCCEEDED(device.As(&device8))) {
on &= hook_slot(device8.Get(), 69, reinterpret_cast<void*>(&hooked_committed2), original_committed2);
on &= hook_slot(device8.Get(), 70, reinterpret_cast<void*>(&hooked_placed1), original_placed1);
}
if (ComPtr<ID3D12Device10> device10; SUCCEEDED(device.As(&device10))) {
on &= hook_slot(device10.Get(), 76, reinterpret_cast<void*>(&hooked_committed3), original_committed3);
on &= hook_slot(device10.Get(), 77, reinterpret_cast<void*>(&hooked_placed2), original_placed2);
on &= hook_slot(device10.Get(), 78, reinterpret_cast<void*>(&hooked_reserved2), original_reserved2);
}
on &= watch_device_views(device.Get());
dingosdk::logging::write(on ? dingosdk::logging::Level::info : dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
on ? "GPU diagnostics: the code that makes and frees each GPU heap and resource is kept, for a page fault's report."
: "GPU diagnostics: some of the GPU memory watch could not start.");
}
using CreateDevice = HRESULT (WINAPI*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**);
std::atomic<CreateDevice> original_create_device{};
HRESULT WINAPI hooked_create_device(IUnknown* adapter, D3D_FEATURE_LEVEL level, REFIID riid, void** device) {
const auto result = original_create_device.load()(adapter, level, riid, device);
if (SUCCEEDED(result) && device && *device) watch_device_memory(static_cast<IUnknown*>(*device));
return result;
}
std::string frames_text(const Frames& frames) {
std::string text;
for (USHORT i = 0; i < frames.depth; ++i) text += " < " + code_place(frames.at[i]);
return text.empty() ? std::string(" (unknown)") : text;
}
const char* allocation_kind(const Allocation& a) {
if (a.heap) return "heap";
switch (a.dimension) {
case D3D12_RESOURCE_DIMENSION_BUFFER: return "buffer";
case D3D12_RESOURCE_DIMENSION_TEXTURE1D: return "1D texture";
case D3D12_RESOURCE_DIMENSION_TEXTURE2D: return "2D texture";
case D3D12_RESOURCE_DIMENSION_TEXTURE3D: return "3D texture";
default: return "resource";
}
}
void say_allocation(const char* what, const Allocation& a, ULONGLONG now) {
using dingosdk::logging::Level;
using dingosdk::logging::Channel;
dingosdk::logging::log(Level::error, Channel::graphics, " {}: {} of {} bytes at {:#x}{}{}", what, allocation_kind(a), a.bytes, a.gpu,
a.in_heap ? std::format(" (offset {:#x} in a heap)", a.offset) : std::string(),
a.freed_at ? std::format(", freed {} ms before the report", now - a.freed_at) : std::string());
dingosdk::logging::log(Level::error, Channel::graphics, " made by{}", frames_text(a.created));
if (a.freed_at) dingosdk::logging::log(Level::error, Channel::graphics, " freed by{}", frames_text(a.freed));
}
// Whether a GPU address lies in memory that exists now (0, "none", always does). Each thread keeps
// the last range it found until a range goes away.
thread_local std::uint64_t cached_start{}, cached_end{}, cached_generation{~0ull};
bool known_address(std::uint64_t address) noexcept {
if (!address) return true;
auto& m = gpu_memory();
if (m.generation.load(std::memory_order_relaxed) == cached_generation && address >= cached_start && address < cached_end) return true;
std::shared_lock lock(m.ranges_mutex);
auto found = m.ranges.upper_bound(address);
if (found == m.ranges.begin()) return false;
--found;
if (address - found->first >= found->second.first) return false;
cached_start = found->first;
cached_end = found->first + found->second.first;
cached_generation = m.generation.load(std::memory_order_relaxed);
return true;
}
// A GPU address handed to D3D12 that lies in no memory there is now: said once for each piece of
// code that does it (up to 64 of them), with what was there before, if anything was.
__declspec(noinline) void note_bad_address(const char* api, std::uint64_t address, void* caller) noexcept {
static std::mutex mutex;
static std::array<void*, 64> seen{};
static std::atomic<std::uint64_t> total{};
try {
const auto count = ++total;
{
std::lock_guard lock(mutex);
auto* slot = static_cast<void**>(nullptr);
for (auto& entry : seen)
if (entry == caller) return;
else if (!entry) { slot = &entry; break; }
if (!slot) return;
*slot = caller;
}
using dingosdk::logging::Level;
using dingosdk::logging::Channel;
std::array<void*, 14> frames{};
const auto depth = CaptureStackBackTrace(1, static_cast<DWORD>(frames.size()), frames.data(), nullptr);
std::string stack;
for (USHORT i = 0; i < depth; ++i) stack += " < " + code_place(frames[i]);
dingosdk::logging::log(Level::error, Channel::graphics, "Bad GPU address: {} given {:#x}, which is in no memory that exists (bad address {} so far); from{}",
api, address, count, stack);
auto& m = gpu_memory();
std::lock_guard lock(m.mutex);
const auto now = GetTickCount64();
const auto kept = std::min<std::uint64_t>(m.freed_count, freed_kept);
for (std::uint64_t i = 0; i < kept; ++i) {
const auto& a = m.freed[(m.freed_count - 1 - i) % freed_kept];
if (a.gpu && a.bytes && address >= a.gpu && address - a.gpu < a.bytes) {
say_allocation("it was in", a, now);
return;
}
}
// (Nearest memory below it, to tell a stale address from a wild one.)
std::shared_lock ranges(m.ranges_mutex);
if (auto below = m.ranges.upper_bound(address); below != m.ranges.begin()) {
--below;
dingosdk::logging::log(Level::error, Channel::graphics, " Nothing was freed there; the nearest memory below ends {:#x} bytes before it.",
address - (below->first + below->second.first));
} else {
dingosdk::logging::write(Level::error, Channel::graphics, " Nothing was freed there, and there is no memory below it.");
}
} catch (...) {}
}
inline void check_address(const char* api, std::uint64_t address, void* caller) noexcept {
if (!known_address(address)) note_bad_address(api, address, caller);
}
// Command-list slots (d3d12.h): SetCompute/GraphicsRootConstantBufferView 37/38,
// ...ShaderResourceView 39/40, ...UnorderedAccessView 41/42, IASetIndexBuffer 43,
// IASetVertexBuffers 44; ID3D12GraphicsCommandList4 BuildRaytracingAccelerationStructure 72.
using RootView = void (STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, D3D12_GPU_VIRTUAL_ADDRESS);
using SetIndexBuffer = void (STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, const D3D12_INDEX_BUFFER_VIEW*);
using SetVertexBuffers = void (STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList*, UINT, UINT, const D3D12_VERTEX_BUFFER_VIEW*);
using BuildAccelerationStructure = void (STDMETHODCALLTYPE*)(ID3D12GraphicsCommandList4*,
const D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC*, UINT, const D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_DESC*);
constexpr const char* root_view_names[6]{"SetComputeRootConstantBufferView", "SetGraphicsRootConstantBufferView",
"SetComputeRootShaderResourceView", "SetGraphicsRootShaderResourceView", "SetComputeRootUnorderedAccessView",
"SetGraphicsRootUnorderedAccessView"};
std::array<std::atomic<RootView>, 6> original_root_views{};
std::atomic<SetIndexBuffer> original_index_buffer{};
std::atomic<SetVertexBuffers> original_vertex_buffers{};
std::atomic<BuildAccelerationStructure> original_build{};
template<std::size_t N> void STDMETHODCALLTYPE hooked_root_view(ID3D12GraphicsCommandList* list, UINT index, D3D12_GPU_VIRTUAL_ADDRESS address) {
check_address(root_view_names[N], address, _ReturnAddress());
original_root_views[N].load(std::memory_order_relaxed)(list, index, address);
}
constexpr RootView root_view_forwarders[6]{&hooked_root_view<0>, &hooked_root_view<1>, &hooked_root_view<2>, &hooked_root_view<3>,
&hooked_root_view<4>, &hooked_root_view<5>};
void STDMETHODCALLTYPE hooked_index_buffer(ID3D12GraphicsCommandList* list, const D3D12_INDEX_BUFFER_VIEW* view) {
if (view) check_address("IASetIndexBuffer", view->BufferLocation, _ReturnAddress());
original_index_buffer.load(std::memory_order_relaxed)(list, view);
}
void STDMETHODCALLTYPE hooked_vertex_buffers(ID3D12GraphicsCommandList* list, UINT start, UINT count, const D3D12_VERTEX_BUFFER_VIEW* views) {
for (UINT i = 0; views && i < count; ++i) check_address("IASetVertexBuffers", views[i].BufferLocation, _ReturnAddress());
original_vertex_buffers.load(std::memory_order_relaxed)(list, start, count, views);
}
void STDMETHODCALLTYPE hooked_build(ID3D12GraphicsCommandList4* list, const D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC* desc,
UINT postbuild_count, const D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_DESC* postbuild) {
if (desc) {
void* const caller = _ReturnAddress();
check_address("BuildRaytracingAccelerationStructure (destination)", desc->DestAccelerationStructureData, caller);
check_address("BuildRaytracingAccelerationStructure (scratch)", desc->ScratchAccelerationStructureData, caller);
check_address("BuildRaytracingAccelerationStructure (source)", desc->SourceAccelerationStructureData, caller);
const auto& inputs = desc->Inputs;
if (inputs.Type == D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL) {
if (inputs.DescsLayout == D3D12_ELEMENTS_LAYOUT_ARRAY)
check_address("BuildRaytracingAccelerationStructure (instances)", inputs.InstanceDescs, caller);
} else {
for (UINT i = 0; i < inputs.NumDescs; ++i) {
const auto* geometry = inputs.DescsLayout == D3D12_ELEMENTS_LAYOUT_ARRAY ? inputs.pGeometryDescs + i
: inputs.ppGeometryDescs ? inputs.ppGeometryDescs[i] : nullptr;
if (!geometry) continue;
if (geometry->Type == D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES) {
check_address("BuildRaytracingAccelerationStructure (vertices)", geometry->Triangles.VertexBuffer.StartAddress, caller);
check_address("BuildRaytracingAccelerationStructure (indices)", geometry->Triangles.IndexBuffer, caller);
check_address("BuildRaytracingAccelerationStructure (transform)", geometry->Triangles.Transform3x4, caller);
} else if (geometry->Type == D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABBS) {
check_address("BuildRaytracingAccelerationStructure (boxes)", geometry->AABBs.AABBs.StartAddress, caller);
}
}
}
}
original_build.load(std::memory_order_relaxed)(list, desc, postbuild_count, postbuild);
}
// How far into each descriptor heap the game writes: every view it makes and every descriptor it
// copies lands at an index of some heap. A heap's furthest index is said as it passes each power
// of two from 4,096 (with the code that took it there from 32,768 on), as it passes 75% and 90% of
// the heap and its end, every 5 seconds while a large heap's grows, and with a GPU failure's
// report.
struct DescriptorHeap {
std::uint64_t start{}, count{}, increment{};
D3D12_DESCRIPTOR_HEAP_TYPE type{};
bool visible{};
unsigned number{};
std::atomic<std::uint64_t> furthest{};
std::atomic<unsigned> said_bits{}, said_share{}; // (said_share: 1 at 75%, 2 at 90%, 3 past the end)
std::uint64_t summarised{};
};
struct Descriptors {
std::shared_mutex mutex;
std::map<std::uint64_t, DescriptorHeap*> heaps; // (by first CPU handle; kept for the process)
unsigned made{};
};
Descriptors& descriptors() { static auto* value = new Descriptors; return *value; }
const char* descriptor_type(D3D12_DESCRIPTOR_HEAP_TYPE type) {
switch (type) {
case D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV: return "CBV/SRV/UAV";
case D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER: return "sampler";
case D3D12_DESCRIPTOR_HEAP_TYPE_RTV: return "render target";
case D3D12_DESCRIPTOR_HEAP_TYPE_DSV: return "depth stencil";
default: return "?";
}
}
__declspec(noinline) void note_descriptor_share(DescriptorHeap& heap, std::uint64_t index, unsigned share) noexcept {
try {
std::array<void*, 14> frames{};
const auto depth = CaptureStackBackTrace(2, static_cast<DWORD>(frames.size()), frames.data(), nullptr);
std::string stack;
for (USHORT i = 0; i < depth; ++i) stack += " < " + code_place(frames[i]);
dingosdk::logging::log(share == 3 ? dingosdk::logging::Level::error : dingosdk::logging::Level::warning,
dingosdk::logging::Channel::graphics, "Descriptor heap {} ({}, {}, {} descriptors): written up to index {}, {}; from{}", heap.number,
descriptor_type(heap.type), heap.visible ? "shader-visible" : "CPU only", heap.count, index,
share == 3 ? "PAST ITS END" : share == 2 ? "past 90% of it" : "past 75% of it", stack);
} catch (...) {}
}
__declspec(noinline) void note_descriptor_reach(DescriptorHeap& heap, std::uint64_t index, unsigned bits) noexcept {
try {
std::string stack;
if (index >= 32768) {
std::array<void*, 14> frames{};
const auto depth = CaptureStackBackTrace(2, static_cast<DWORD>(frames.size()), frames.data(), nullptr);
stack = "; from";
for (USHORT i = 0; i < depth; ++i) stack += " < " + code_place(frames[i]);
}
dingosdk::logging::log(index >= 65536 ? dingosdk::logging::Level::warning : dingosdk::logging::Level::info,
dingosdk::logging::Channel::graphics, "Descriptor heap {} ({}, {}, {} descriptors): written up to index {} (past {}){}", heap.number,
descriptor_type(heap.type), heap.visible ? "shader-visible" : "CPU only", heap.count, index, 1ull << (bits - 1), stack);
} catch (...) {}
}
void note_descriptors(std::uint64_t handle, std::uint64_t count) noexcept {
if (!handle || !count) return;
auto& d = descriptors();
DescriptorHeap* heap{};
{
std::shared_lock lock(d.mutex);
auto found = d.heaps.upper_bound(handle);
if (found == d.heaps.begin()) return;
heap = (--found)->second;
}
if (!heap->increment || handle - heap->start >= heap->count * heap->increment) return;
const auto index = (handle - heap->start) / heap->increment + count - 1;
auto furthest = heap->furthest.load(std::memory_order_relaxed);
while (index > furthest && !heap->furthest.compare_exchange_weak(furthest, index, std::memory_order_relaxed)) {}
if (index <= furthest) return;
if (const unsigned share = index >= heap->count ? 3 : index * 10 >= heap->count * 9 ? 2 : index * 4 >= heap->count * 3 ? 1 : 0; share) {
auto said = heap->said_share.load(std::memory_order_relaxed);
while (share > said)
if (heap->said_share.compare_exchange_weak(said, share, std::memory_order_relaxed)) {
note_descriptor_share(*heap, index, share);
break;
}
}
if (index < 4096) return;
unsigned bits{};
for (auto value = index; value; value >>= 1) ++bits;
auto said = heap->said_bits.load(std::memory_order_relaxed);
while (bits > said)
if (heap->said_bits.compare_exchange_weak(said, bits, std::memory_order_relaxed)) {
note_descriptor_reach(*heap, index, bits);
return;
}
}
void say_descriptor_heaps() noexcept {
try {
auto& d = descriptors();
std::shared_lock lock(d.mutex);
for (const auto& [start, heap] : d.heaps)
if (heap->furthest.load() >= 1024 || heap->count >= 65536)
dingosdk::logging::log(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics,
"Descriptor heap {} ({}, {}, {} descriptors): written up to index {}.", heap->number, descriptor_type(heap->type),
heap->visible ? "shader-visible" : "CPU only", heap->count, heap->furthest.load());
} catch (...) {}
}
// Device slots: CreateDescriptorHeap 14, CreateConstantBufferView 17, CreateShaderResourceView 18,
// CreateUnorderedAccessView 19, CopyDescriptors 23, CopyDescriptorsSimple 24.
using CreateDescriptorHeap = HRESULT (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_DESCRIPTOR_HEAP_DESC*, REFIID, void**);
using CreateCbv = void (STDMETHODCALLTYPE*)(ID3D12Device*, const D3D12_CONSTANT_BUFFER_VIEW_DESC*, D3D12_CPU_DESCRIPTOR_HANDLE);
using CreateSrv = void (STDMETHODCALLTYPE*)(ID3D12Device*, ID3D12Resource*, const D3D12_SHADER_RESOURCE_VIEW_DESC*, D3D12_CPU_DESCRIPTOR_HANDLE);
using CreateUav = void (STDMETHODCALLTYPE*)(ID3D12Device*, ID3D12Resource*, ID3D12Resource*, const D3D12_UNORDERED_ACCESS_VIEW_DESC*,
D3D12_CPU_DESCRIPTOR_HANDLE);
using CopyDescriptors = void (STDMETHODCALLTYPE*)(ID3D12Device*, UINT, const D3D12_CPU_DESCRIPTOR_HANDLE*, const UINT*, UINT,
const D3D12_CPU_DESCRIPTOR_HANDLE*, const UINT*, D3D12_DESCRIPTOR_HEAP_TYPE);
using CopyDescriptorsSimple = void (STDMETHODCALLTYPE*)(ID3D12Device*, UINT, D3D12_CPU_DESCRIPTOR_HANDLE, D3D12_CPU_DESCRIPTOR_HANDLE,
D3D12_DESCRIPTOR_HEAP_TYPE);
std::atomic<CreateDescriptorHeap> original_descriptor_heap{};
std::atomic<CreateCbv> original_cbv{};
std::atomic<CreateSrv> original_srv{};
std::atomic<CreateUav> original_uav{};
std::atomic<CopyDescriptors> original_copy{};
std::atomic<CopyDescriptorsSimple> original_copy_simple{};
HRESULT STDMETHODCALLTYPE hooked_descriptor_heap(ID3D12Device* device, const D3D12_DESCRIPTOR_HEAP_DESC* desc, REFIID riid, void** out) {
const auto result = original_descriptor_heap.load()(device, desc, riid, out);
if (SUCCEEDED(result) && desc && out && *out) {
try {
ComPtr<ID3D12DescriptorHeap> made;
if (SUCCEEDED(static_cast<IUnknown*>(*out)->QueryInterface(IID_PPV_ARGS(&made)))) {
auto* heap = new DescriptorHeap;
heap->start = made->GetCPUDescriptorHandleForHeapStart().ptr;
heap->count = desc->NumDescriptors;
heap->increment = device->GetDescriptorHandleIncrementSize(desc->Type);
heap->type = desc->Type;
heap->visible = desc->Flags & D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
auto& d = descriptors();
{
std::unique_lock lock(d.mutex);
heap->number = ++d.made;
d.heaps[heap->start] = heap;
}
if (heap->count >= 4096 || heap->visible || heap->number <= 16)
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics,
"Descriptor heap {} made: {}, {}, {} descriptors.", heap->number, descriptor_type(heap->type),
heap->visible ? "shader-visible" : "CPU only", heap->count);
}
} catch (...) {}
}
return result;
}
void STDMETHODCALLTYPE hooked_cbv(ID3D12Device* device, const D3D12_CONSTANT_BUFFER_VIEW_DESC* desc, D3D12_CPU_DESCRIPTOR_HANDLE handle) {
if (desc) check_address("CreateConstantBufferView", desc->BufferLocation, _ReturnAddress());
note_descriptors(handle.ptr, 1);
original_cbv.load(std::memory_order_relaxed)(device, desc, handle);
}
void STDMETHODCALLTYPE hooked_srv(ID3D12Device* device, ID3D12Resource* resource, const D3D12_SHADER_RESOURCE_VIEW_DESC* desc,
D3D12_CPU_DESCRIPTOR_HANDLE handle) {
if (desc && desc->ViewDimension == D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE)
check_address("CreateShaderResourceView (acceleration structure)", desc->RaytracingAccelerationStructure.Location, _ReturnAddress());
note_descriptors(handle.ptr, 1);
original_srv.load(std::memory_order_relaxed)(device, resource, desc, handle);
}
void STDMETHODCALLTYPE hooked_uav(ID3D12Device* device, ID3D12Resource* resource, ID3D12Resource* counter,
const D3D12_UNORDERED_ACCESS_VIEW_DESC* desc, D3D12_CPU_DESCRIPTOR_HANDLE handle) {
note_descriptors(handle.ptr, 1);
original_uav.load(std::memory_order_relaxed)(device, resource, counter, desc, handle);
}
void STDMETHODCALLTYPE hooked_copy(ID3D12Device* device, UINT ranges, const D3D12_CPU_DESCRIPTOR_HANDLE* starts, const UINT* sizes,
UINT source_ranges, const D3D12_CPU_DESCRIPTOR_HANDLE* source_starts, const UINT* source_sizes, D3D12_DESCRIPTOR_HEAP_TYPE type) {
for (UINT i = 0; starts && i < ranges; ++i) note_descriptors(starts[i].ptr, sizes ? sizes[i] : 1);
original_copy.load(std::memory_order_relaxed)(device, ranges, starts, sizes, source_ranges, source_starts, source_sizes, type);
}
void STDMETHODCALLTYPE hooked_copy_simple(ID3D12Device* device, UINT count, D3D12_CPU_DESCRIPTOR_HANDLE destination,
D3D12_CPU_DESCRIPTOR_HANDLE source, D3D12_DESCRIPTOR_HEAP_TYPE type) {
note_descriptors(destination.ptr, count);
original_copy_simple.load(std::memory_order_relaxed)(device, count, destination, source, type);
}
// Every 5 seconds: the large heaps whose furthest index has grown since they were last said, so a
// hang that ends the process before its report still leaves how full they were.
void summarise_descriptor_heaps() {
SetThreadDescription(GetCurrentThread(), L"ReSkate descriptor heap watch");
for (;;) {
Sleep(5000);
try {
auto& d = descriptors();
std::shared_lock lock(d.mutex);
for (const auto& [start, heap] : d.heaps) {
const auto furthest = heap->furthest.load(std::memory_order_relaxed);
if (heap->count < 65536 || furthest == heap->summarised) continue;
heap->summarised = furthest;
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics,
"Descriptor heap {} ({}, {}): {} of {} used at the furthest ({}%).", heap->number, descriptor_type(heap->type),
heap->visible ? "shader-visible" : "CPU only", furthest + 1, heap->count, (furthest + 1) * 100 / heap->count);
}
} catch (...) {}
}
}
bool watch_device_views(ID3D12Device* device) noexcept {
try {
std::thread(summarise_descriptor_heaps).detach();
} catch (...) {}
return hook_slot(device, 14, reinterpret_cast<void*>(&hooked_descriptor_heap), original_descriptor_heap) &
hook_slot(device, 17, reinterpret_cast<void*>(&hooked_cbv), original_cbv) &
hook_slot(device, 18, reinterpret_cast<void*>(&hooked_srv), original_srv) &
hook_slot(device, 19, reinterpret_cast<void*>(&hooked_uav), original_uav) &
hook_slot(device, 23, reinterpret_cast<void*>(&hooked_copy), original_copy) &
hook_slot(device, 24, reinterpret_cast<void*>(&hooked_copy_simple), original_copy_simple);
}
}
void watch_addresses(ID3D12GraphicsCommandList* list) noexcept {
static std::atomic<bool> watching{};
const auto error = GetLastError();
if (!list || !graphics_option_enabled(L"RESKATE_GPU_DIAGNOSTICS") || !gpu_memory().made_count || watching.exchange(true)) {
SetLastError(error);
return;
}
bool on = true;
for (std::size_t i = 0; i < 6; ++i) on &= hook_slot(list, 37 + i, reinterpret_cast<void*>(root_view_forwarders[i]), original_root_views[i]);
on &= hook_slot(list, 43, reinterpret_cast<void*>(&hooked_index_buffer), original_index_buffer);
on &= hook_slot(list, 44, reinterpret_cast<void*>(&hooked_vertex_buffers), original_vertex_buffers);
if (ComPtr<ID3D12GraphicsCommandList4> list4; SUCCEEDED(list->QueryInterface(IID_PPV_ARGS(&list4))))
on &= hook_slot(list4.Get(), 72, reinterpret_cast<void*>(&hooked_build), original_build);
dingosdk::logging::write(on ? dingosdk::logging::Level::info : dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
on ? "GPU diagnostics: GPU addresses bound for draws, dispatches and ray-tracing builds are checked against the memory that exists."
: "GPU diagnostics: some of the GPU address checks could not start.");
SetLastError(error);
}
void watch_gpu_memory() noexcept {
static std::atomic<bool> started{};
const auto error = GetLastError();
if (!graphics_option_enabled(L"RESKATE_GPU_DIAGNOSTICS") || started.exchange(true)) {
SetLastError(error);
return;
}
// (d3d12.dll is ReSkate's own import, so it is loaded; the game finds D3D12CreateDevice there.)
auto* const create = reinterpret_cast<void*>(GetProcAddress(GetModuleHandleW(L"d3d12.dll"), "D3D12CreateDevice"));
void* trampoline{};
if (!create || hook_prepare(create, reinterpret_cast<void*>(&hooked_create_device), &trampoline) != HookOk) {
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
"GPU diagnostics: the GPU memory watch could not start.");
SetLastError(error);
return;
}
original_create_device.store(reinterpret_cast<CreateDevice>(trampoline));
hook_enable(create);
SetLastError(error);
}
void describe_gpu_address(std::uint64_t address) noexcept {
try {
using dingosdk::logging::Level;
using dingosdk::logging::Channel;
auto& m = gpu_memory();
std::lock_guard lock(m.mutex);
if (!m.made_count) return;
const auto now = GetTickCount64();
const auto kept = std::min<std::uint64_t>(m.freed_count, freed_kept);
// A heap's GPU address, live or freed, for resources placed in it whose own is not known.
// (A freed heap is found by its old pointer, which a newer object may since have taken.)
std::unordered_map<void*, std::uint64_t> freed_heaps;
for (std::uint64_t i = 0; i < kept; ++i)
if (const auto& a = m.freed[i]; a.heap && a.gpu) freed_heaps.try_emplace(a.object, a.gpu);
const auto heap_gpu = [&](void* heap) -> std::uint64_t {
if (const auto found = m.live.find(heap); found != m.live.end() && found->second.gpu) return found->second.gpu;
if (const auto found = freed_heaps.find(heap); found != freed_heaps.end()) return found->second;
return 0;
};
const auto covers = [&](const Allocation& a) {
auto gpu = a.gpu;
if (!gpu && a.in_heap) if (const auto base = heap_gpu(a.in_heap)) gpu = base + a.offset;
return gpu && a.bytes && address >= gpu && address - gpu < a.bytes;
};
dingosdk::logging::log(Level::error, Channel::graphics, "GPU memory watch: {} heaps and resources live, {} freed since the device was made.",
m.live.size(), m.freed_count);
unsigned said{};
for (const auto& [object, a] : m.live)
if (covers(a) && said < 6) { ++said; say_allocation("live there", a, now); }
for (std::uint64_t i = 0; i < kept && said < 12; ++i) {
const auto& a = m.freed[(m.freed_count - 1 - i) % freed_kept];
if (covers(a)) { ++said; say_allocation("freed there", a, now); }
}
if (said) return;
// The address is in nothing whose GPU address is known (textures in heaps with no buffer):
// the last heaps and resources freed, which is what DRED's "freed there recently" names.
dingosdk::logging::write(Level::error, Channel::graphics, " Nothing known covers that address; the last freed heaps and resources:");
unsigned heaps{}, resources{};
for (std::uint64_t i = 0; i < kept && (heaps < 4 || resources < 4); ++i) {
const auto& a = m.freed[(m.freed_count - 1 - i) % freed_kept];
if (a.heap ? heaps++ < 4 : resources++ < 4) say_allocation("freed", a, now);
}
} catch (...) {}
}
void watch_draws(ID3D12GraphicsCommandList* list) noexcept {
static std::atomic<bool> watching{};
const auto error = GetLastError();
if (!list || !graphics_option_enabled(L"RESKATE_GPU_DIAGNOSTICS") || watching.exchange(true)) {
SetLastError(error);
return;
}
// (Slots 12 and 13 of ID3D12GraphicsCommandList: after IUnknown, ID3D12Object, GetDevice,
// GetType, Close, Reset and ClearState.)
auto* const table = *reinterpret_cast<void***>(list);
void* draw_original{};
void* indexed_original{};
const bool draw = hook_prepare(table[12], reinterpret_cast<void*>(&hooked_draw), &draw_original) == HookOk;
if (draw) original_draw.store(reinterpret_cast<DrawInstanced>(draw_original));
const bool indexed = hook_prepare(table[13], reinterpret_cast<void*>(&hooked_draw_indexed), &indexed_original) == HookOk;
if (indexed) original_draw_indexed.store(reinterpret_cast<DrawIndexedInstanced>(indexed_original));
const bool on = (draw && hook_enable(table[12]) == HookOk) | (indexed && hook_enable(table[13]) == HookOk);
dingosdk::logging::write(on ? dingosdk::logging::Level::info : dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
on ? "GPU diagnostics: draws of more than ten million vertices are said in the log."
: "GPU diagnostics: the draw watch could not start.");
SetLastError(error);
}
void record_gpu_identity(ID3D12Device* device) noexcept {
static std::atomic<bool> recorded{};
const auto error = GetLastError();
try {
ComPtr<IDXGIFactory4> factory;
ComPtr<IDXGIAdapter1> adapter;
DXGI_ADAPTER_DESC1 description{};
if (device && !recorded.exchange(true) && SUCCEEDED(CreateDXGIFactory1(IID_PPV_ARGS(&factory))) &&
SUCCEEDED(factory->EnumAdapterByLuid(device->GetAdapterLuid(), IID_PPV_ARGS(&adapter))) &&
SUCCEEDED(adapter->GetDesc1(&description))) {
// The user-mode driver's version, as DXGI reports it for the IDXGIDevice interface.
std::wstring driver;
if (LARGE_INTEGER version{}; SUCCEEDED(adapter->CheckInterfaceSupport(__uuidof(IDXGIDevice), &version)))
driver = std::format(L"{}.{}.{}.{}", HIWORD(version.HighPart), LOWORD(version.HighPart), HIWORD(version.LowPart),
LOWORD(version.LowPart));
dingosdk::crash_report::set_gpu(description.Description, driver);
dingosdk::crash_report::set_gpu_breadcrumbs(gpu_breadcrumbs());
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics,
std::wstring(L"Graphics card: ") + description.Description + (driver.empty() ? L"" : L", driver " + driver) + L".");
}
} catch (...) {}
SetLastError(error);
}
void watch_device_removal(ID3D12Device* device) noexcept {
static std::atomic<bool> watching{};
const auto error = GetLastError();
if (!device || watching.exchange(true)) {
SetLastError(error);
return;
}
// A fence asked to signal at UINT64_MAX does so only when the device is removed (the documented
// way to hear of it). Kept, with the device, for the life of the process.
struct Watch {
ComPtr<ID3D12Device> device;
ComPtr<ID3D12Fence> fence;
HANDLE event{}, wait{};
};
auto* watch = new Watch{device};
watch->event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!watch->event || FAILED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&watch->fence))) ||
FAILED(watch->fence->SetEventOnCompletion(UINT64_MAX, watch->event)) ||
!RegisterWaitForSingleObject(&watch->wait, watch->event, [](void* context, BOOLEAN) {
auto* const watched = static_cast<Watch*>(context);
record_device_failure(watched->device.Get(), watched->device->GetDeviceRemovedReason(), "device removed");
}, watch, INFINITE, WT_EXECUTEONLYONCE)) {
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics,
"GPU diagnostics: the device-removal watch could not start; failures are recorded when the overlay next draws.");
}
if (gpu_breadcrumbs()) try {
std::thread(watch_video_memory, device->GetAdapterLuid()).detach();
} catch (...) {}
SetLastError(error);
}
// What the GPU was doing when it stopped: from DRED, when RESKATE_GPU_DIAGNOSTICS (the launcher's
// --gpu-diagnostics) turned it on before the device was made. A command list whose last completed
// breadcrumb is short of its count was still running; the operation after the last completed one
// is where it stopped, and the markers (the game's profiling events) before it name the pass.
void record_device_failure(ID3D12Device* device, HRESULT result,
const char* operation) noexcept {
const auto error = GetLastError();
try {
if (!device || log().removal_recorded.exchange(true)) {
SetLastError(error);
return;
}
using dingosdk::logging::Level;
using dingosdk::logging::Channel;
const auto reason = device->GetDeviceRemovedReason();
dingosdk::logging::printf(Level::error, Channel::graphics, "GPU failure: operation=%s result=0x%08lx removed_reason=0x%08lx",
operation, static_cast<unsigned long>(result), static_cast<unsigned long>(reason));
// The next launches record what the GPU was doing, for the report of the next stop.
if (reason != S_OK) set_breadcrumbs_left(breadcrumb_launches);
ComPtr<ID3D12DeviceRemovedExtendedData1> dred;
if (FAILED(device->QueryInterface(IID_PPV_ARGS(&dred)))) {
dingosdk::logging::write(Level::error, Channel::graphics, "DRED is unavailable on this device.");
SetLastError(error);
return;
}
if (ComPtr<ID3D12DeviceRemovedExtendedData2> dred2; SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&dred2))))
dingosdk::logging::printf(Level::error, Channel::graphics, "DRED device state %d (3 hung, 6 fault, 7 page fault).",
static_cast<int>(dred2->GetDeviceState()));
D3D12_DRED_AUTO_BREADCRUMBS_OUTPUT1 breadcrumbs{};
if (const auto status = dred->GetAutoBreadcrumbsOutput1(&breadcrumbs); FAILED(status)) {
dingosdk::logging::printf(Level::error, Channel::graphics,
"DRED breadcrumbs unavailable (0x%08lx): the next %d launches record them.", static_cast<unsigned long>(status),
breadcrumb_launches);
} else {
unsigned finished{}, running{}, waiting{};
for (auto* node = breadcrumbs.pHeadAutoBreadcrumbNode; node; node = node->pNext) {
const UINT done = node->pLastBreadcrumbValue ? *node->pLastBreadcrumbValue : 0;
if (done >= node->BreadcrumbCount) { ++finished; continue; }
// (A list with nothing done was queued behind the one at fault: counted, not shown.)
if (!done) { ++waiting; continue; }
if (++running > 8) continue;
dingosdk::logging::printf(Level::error, Channel::graphics, "Still running: list \"%s\" on queue \"%s\", %u of %u operations completed.",
narrow(node->pCommandListDebugNameA, node->pCommandListDebugNameW).c_str(),
narrow(node->pCommandQueueDebugNameA, node->pCommandQueueDebugNameW).c_str(), done, node->BreadcrumbCount);
// The markers (and events) up to where it stopped: the last few name the pass.
std::array<UINT, 6> marked{};
std::size_t marks{};
for (UINT c = 0; c < node->BreadcrumbContextsCount; ++c)
if (node->pBreadcrumbContexts[c].BreadcrumbIndex <= done) marked[marks++ % marked.size()] = c;
for (std::size_t m = marks > marked.size() ? marks - marked.size() : 0; m < marks; ++m) {
const auto& context = node->pBreadcrumbContexts[marked[m % marked.size()]];
dingosdk::logging::printf(Level::error, Channel::graphics, " marker at op %u: %s", context.BreadcrumbIndex,
narrow(nullptr, context.pContextString).c_str());
}
if (!node->pCommandHistory) continue;
const UINT begin = done > 6 ? done - 6 : 0;
const UINT end = std::min<UINT>(node->BreadcrumbCount, done + 4);
for (UINT op = begin; op < end; ++op)
dingosdk::logging::printf(Level::error, Channel::graphics, " %s op %u: %s", op == done ? ">>" : " ", op,
operation_name(node->pCommandHistory[op]));
}
dingosdk::logging::printf(Level::error, Channel::graphics, "DRED: %u command list(s) partway through, %u not started, %u finished.",
running, waiting, finished);
}
say_descriptor_heaps();
D3D12_DRED_PAGE_FAULT_OUTPUT1 fault{};
if (SUCCEEDED(dred->GetPageFaultAllocationOutput1(&fault)) && fault.PageFaultVA) {
dingosdk::logging::printf(Level::error, Channel::graphics, "GPU page fault at 0x%llx.", fault.PageFaultVA);
allocations("still allocated there", fault.pHeadExistingAllocationNode);
allocations("freed there recently", fault.pHeadRecentFreedAllocationNode);
describe_gpu_address(fault.PageFaultVA);
if (!fault.pHeadExistingAllocationNode && !fault.pHeadRecentFreedAllocationNode)
dingosdk::logging::write(Level::error, Channel::graphics,
" No object is allocated at that address, or was freed there recently: the GPU was given an address that belongs to nothing.");
} else {
dingosdk::logging::write(Level::error, Channel::graphics, "DRED: no GPU page fault recorded.");
}
} catch (...) {}
SetLastError(error);
}
}