mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-29 03:46:50 +01:00
The renderer gives each frame 131,072 shader-visible descriptors past a persistent 120,000, in heaps of 333,333. Dense custom maps (the Los Santos City sublevels, San Andreas) draw more than that in a frame, and the renderer wrote past the end of the heap: shaders read garbage descriptors, which faulted or hung the GPU (page faults at nonsense addresses, in draws, texture fetches and ray tracing alike). ReSkate now patches the device setup at boot so each heap is D3D12's 1,000,000 and a frame may use 524,288; heaps are 644,288 descriptors. LS-B2, LS-B3 and San Andreas (210,000 a frame at its peak) now hold. Also in this change: - Memory: perf memory owners/heap/gpu/report, a ledger of who commits the game's RAM; the render-resource pool no longer allocates its 115 MB twice; the EadpErrorsData arena is capped at 32 MiB (EA's error reporting is blocked anyway). - Crash reports: tagged with the map, destination, graphics card and driver version (protocol v3), so Sentry can group GPU hangs by map and card. - GPU diagnostics (--gpu-diagnostics): a fuller DRED report, the device-removal and video memory watches, a large-draw watch, a record of which game code made and freed each GPU heap and resource, checks of every GPU address bound, and a meter of each descriptor heap's use. - World pools: peak use of entity pages, render instances and spatial blocks in the log. - dingosdk_mod_mesh_check: checks every MeshSet a mod ships against its geometry; the MeshSet reader now knows static meshes' layout.
146 lines
8.1 KiB
C++
146 lines
8.1 KiB
C++
#include <Windows.h>
|
|
#include "crash_reporter.h"
|
|
#include <DbgHelp.h>
|
|
#include "Engine/Core/Debug/protocol.h"
|
|
#include "Engine/Core/Debug/upload.h"
|
|
#include "Engine/Core/Debug/native_dump.h"
|
|
#include "Engine/Core/Debug/multipart.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include "crash_report_config.h"
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <format>
|
|
#include <thread>
|
|
|
|
namespace {
|
|
using namespace dingosdk;
|
|
using namespace dingosdk::crash_report;
|
|
std::string utf8(const wchar_t* text) {
|
|
const auto size = WideCharToMultiByte(CP_UTF8, 0, text, -1, nullptr, 0, nullptr, nullptr);
|
|
if (size <= 1) return {};
|
|
std::string result(static_cast<std::size_t>(size), '\0');
|
|
WideCharToMultiByte(CP_UTF8, 0, text, -1, result.data(), size, nullptr, nullptr);
|
|
result.pop_back();
|
|
return result;
|
|
}
|
|
template<std::size_t N> std::string field(const std::array<wchar_t, N>& value) {
|
|
return utf8(std::wstring(value.data(), wcsnlen(value.data(), N)).c_str());
|
|
}
|
|
// "Levels/Game/DingoLevel_Root/DingoLevel_Root + levels/game/x/x @ Start" -> "x": the sublevel when
|
|
// there is one, else the level, by its last name.
|
|
std::string map_name(std::string destination) {
|
|
if (const auto at = destination.find(" @ "); at != std::string::npos) destination.resize(at);
|
|
if (const auto plus = destination.find(" + "); plus != std::string::npos) destination.erase(0, plus + 3);
|
|
if (const auto slash = destination.find_last_of('/'); slash != std::string::npos) destination.erase(0, slash + 1);
|
|
return destination;
|
|
}
|
|
bool publish_dump(const std::filesystem::path& temporary, const SharedReport& report, const char* source) {
|
|
auto dump = temporary; dump.replace_extension(L".dmp");
|
|
Json attributes = Json::object();
|
|
attributes["application"] = "ReSkate";
|
|
attributes["process"] = utf8(report.application.data());
|
|
attributes["version"] = build_version;
|
|
#ifdef _DEBUG
|
|
attributes["build.configuration"] = "Debug";
|
|
#else
|
|
attributes["build.configuration"] = "Release";
|
|
#endif
|
|
attributes["game.supported_sha256"] = supported_build::game_sha256;
|
|
attributes["error.type"] = "crash";
|
|
attributes["report.id"] = utf8(dump.stem().c_str());
|
|
attributes["capture.source"] = source;
|
|
// (Tags for grouping crashes by the level and the graphics card; absent when unknown.)
|
|
if (const auto destination = field(report.destination); !destination.empty()) {
|
|
attributes["game.destination"] = destination;
|
|
attributes["game.map"] = map_name(destination);
|
|
}
|
|
if (const auto gpu = field(report.gpu_name); !gpu.empty()) attributes["gpu.name"] = gpu;
|
|
if (const auto driver = field(report.gpu_driver); !driver.empty()) attributes["gpu.driver"] = driver;
|
|
const auto log = snapshot_log(std::filesystem::path(report.directory.data()).parent_path() / L"ReSkate.log", dump);
|
|
attributes["attachment.log"] = log == LogSnapshot::complete ? "complete" : log == LogSnapshot::tail ? "tail" : "unavailable";
|
|
std::ofstream metadata(dump.wstring() + L".json", std::ios::binary);
|
|
metadata << attributes.dump();
|
|
metadata.close();
|
|
if (!metadata || !MoveFileExW(temporary.c_str(), dump.c_str(), MOVEFILE_WRITE_THROUGH)) {
|
|
DeleteFileW(temporary.c_str());
|
|
DeleteFileW((dump.wstring() + L".json").c_str());
|
|
DeleteFileW(log_attachment_path(dump).c_str());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
bool save_dump(HANDLE process, const SharedReport& report, const std::filesystem::path& directory) {
|
|
const auto id = std::format(L"{}-{}-{}", GetCurrentProcessId(), GetTickCount64(), report.thread_id);
|
|
const auto temporary = directory / (id + L".tmp");
|
|
Handle file(CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
|
|
if (file.value == INVALID_HANDLE_VALUE) return false;
|
|
MINIDUMP_EXCEPTION_INFORMATION exception{report.thread_id, report.exception, TRUE};
|
|
constexpr auto flags = static_cast<MINIDUMP_TYPE>(MiniDumpNormal | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules);
|
|
const auto written = MiniDumpWriteDump(process, GetProcessId(process), file.value, flags, &exception, nullptr, nullptr);
|
|
const auto durable = written && FlushFileBuffers(file.value);
|
|
CloseHandle(file.release());
|
|
if (!durable) { DeleteFileW(temporary.c_str()); return false; }
|
|
return publish_dump(temporary, report, "unhandled-exception");
|
|
}
|
|
bool collect_native_dump(HANDLE process, const SharedReport& report, const std::filesystem::path& directory) {
|
|
const auto native = find_native_dump(report.native_directory.data(), GetProcessId(process), process_creation_seconds(process));
|
|
if (!native) return false;
|
|
const auto temporary = directory / std::format(L"native-{}-{}-{}.tmp", native->process_id, native->process_created, native->thread_id);
|
|
if (!CopyFileW(native->path.c_str(), temporary.c_str(), TRUE)) return false;
|
|
return publish_dump(temporary, report, "game-native");
|
|
}
|
|
int run(int argc, wchar_t** argv) {
|
|
if (argc != handle_count + 2 || std::wstring_view(argv[1]) != L"--reskate-crash-helper") return 2;
|
|
std::array<Handle, handle_count> handles;
|
|
for (std::size_t i = 0; i < handle_count; ++i) {
|
|
const std::wstring_view value(argv[i + 2]);
|
|
if (value.empty() || value.find_first_not_of(L"0123456789") != std::wstring::npos) return 2;
|
|
wchar_t* end{};
|
|
const auto number = wcstoull(argv[i + 2], &end, 10);
|
|
if (!number || *end) return 2;
|
|
handles[i].value = reinterpret_cast<HANDLE>(number);
|
|
DWORD flags{};
|
|
if (!GetHandleInformation(handles[i].value, &flags)) return 2;
|
|
SetHandleInformation(handles[i].value, HANDLE_FLAG_INHERIT, 0);
|
|
}
|
|
auto* shared = static_cast<SharedReport*>(MapViewOfFile(handles[mapping].value, FILE_MAP_READ, 0, 0, sizeof(SharedReport)));
|
|
if (!shared) return 3;
|
|
struct View { void* value; ~View() { UnmapViewOfFile(value); } } view{shared};
|
|
if (shared->version != protocol_version || shared->url.back() || shared->directory.back() ||
|
|
shared->application.back() || shared->native_directory.back()) return 3;
|
|
const std::wstring url(shared->url.data());
|
|
Endpoint endpoint;
|
|
if (!parse_endpoint(url, endpoint)) return 3;
|
|
const auto directory = queue_directory(shared->directory.data(), url);
|
|
std::filesystem::create_directories(directory);
|
|
// Uploading an old queue must never delay capturing a new crash.
|
|
std::jthread uploader([&] { upload_pending(url, directory); });
|
|
SetEvent(handles[ready].value);
|
|
const HANDLE wait_for[]{handles[requested].value, handles[stopping].value, handles[parent].value};
|
|
const auto reason = WaitForMultipleObjects(3, wait_for, FALSE, INFINITE);
|
|
bool saved{};
|
|
if (reason == WAIT_OBJECT_0) {
|
|
try { saved = save_dump(handles[parent].value, *shared, directory); } catch (...) {}
|
|
// Release the crashed process before any network activity or uploader join.
|
|
SetEvent(handles[captured].value);
|
|
std::ofstream(directory / L"capture-status.txt") << (saved ? "Captured unhandled exception.\n" : "Could not capture minidump; local crash log retained.\n");
|
|
} else if (reason == WAIT_OBJECT_0 + 2 && shared->native_directory[0]) {
|
|
// The game's native fatal handler may replace/bypass our Windows filter.
|
|
// After exit all native dump writers are closed. Preserve their original
|
|
// exception context rather than taking a context-free termination dump.
|
|
try { saved = collect_native_dump(handles[parent].value, *shared, directory); } catch (...) {}
|
|
DWORD exit_code{}; GetExitCodeProcess(handles[parent].value, &exit_code);
|
|
std::ofstream(directory / L"capture-status.txt") << std::format(
|
|
"Game process {} exited (0x{:08x}); {}\n", GetProcessId(handles[parent].value), exit_code,
|
|
saved ? "queued its native crash dump." : "no native dump matched this process and start time.");
|
|
} else return 0;
|
|
uploader.join();
|
|
if (saved) upload_pending(url, directory);
|
|
return saved ? 0 : 4;
|
|
}
|
|
}
|
|
int dingosdk::crash_report::run_reporter(int argc, wchar_t** argv) noexcept {
|
|
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
|
|
try { return run(argc, argv); } catch (...) { return 1; }
|
|
}
|