mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 15:35:20 +02: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.
138 lines
7.4 KiB
C++
138 lines
7.4 KiB
C++
#include "crash_report.h"
|
|
#include "protocol.h"
|
|
#include "crash_report_config.h"
|
|
#include <algorithm>
|
|
#include <format>
|
|
#include <vector>
|
|
|
|
namespace dingosdk::crash_report {
|
|
namespace {
|
|
std::array<HANDLE, handle_count> handles{};
|
|
HANDLE helper{};
|
|
SharedReport* report{};
|
|
// 0 = ready, 1 = capture owns the IPC, 2 = stopped/initializing.
|
|
volatile LONG capturing{2};
|
|
struct View {
|
|
void* value{};
|
|
~View() { if (value) UnmapViewOfFile(value); }
|
|
};
|
|
template<std::size_t N> bool copy(std::array<wchar_t, N>& target, const std::wstring& value) {
|
|
if (value.size() >= N) return false;
|
|
std::copy(value.begin(), value.end(), target.begin());
|
|
target[value.size()] = 0;
|
|
return true;
|
|
}
|
|
template<std::size_t N> void copy_cut(std::array<wchar_t, N>& target, std::wstring_view value) noexcept {
|
|
const auto size = std::min(value.size(), N - 1);
|
|
std::copy_n(value.begin(), size, target.begin());
|
|
target[size] = 0;
|
|
}
|
|
}
|
|
StartResult start(const std::filesystem::path& log_directory) noexcept {
|
|
try {
|
|
if (report) return StartResult::ready;
|
|
if (environment(L"RESKATE_CRASH_REPORTING") == L"0") return StartResult::disabled;
|
|
auto url = environment(L"RESKATE_CRASH_REPORT_URL");
|
|
if (url.empty()) url = configured_url;
|
|
if (url.empty()) return StartResult::disabled;
|
|
if (!url.starts_with(L"https://") || url.size() >= SharedReport{}.url.size()) return StartResult::failed;
|
|
HMODULE module{};
|
|
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
reinterpret_cast<LPCWSTR>(&handles), &module)) return StartResult::failed;
|
|
std::array<wchar_t, 32768> module_path{}, process_path{};
|
|
const auto module_size = GetModuleFileNameW(module, module_path.data(), static_cast<DWORD>(module_path.size()));
|
|
const auto process_size = GetModuleFileNameW(nullptr, process_path.data(), static_cast<DWORD>(process_path.size()));
|
|
if (!module_size || module_size >= module_path.size() || !process_size || process_size >= process_path.size())
|
|
return StartResult::failed;
|
|
const auto executable = std::filesystem::path(module_path.data()).parent_path() / L"ReSkateLauncher.exe";
|
|
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
|
|
std::array<Handle, handle_count> owned;
|
|
owned[mapping].value = CreateFileMappingW(INVALID_HANDLE_VALUE, &security, PAGE_READWRITE, 0, sizeof(SharedReport), nullptr);
|
|
for (std::size_t i = requested; i <= ready; ++i) owned[i].value = CreateEventW(&security, TRUE, FALSE, nullptr);
|
|
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &owned[parent].value,
|
|
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, 0)) return StartResult::failed;
|
|
std::array<HANDLE, handle_count> inherited{};
|
|
for (std::size_t i = 0; i < handle_count; ++i) {
|
|
if (!owned[i].value) return StartResult::failed;
|
|
inherited[i] = owned[i].value;
|
|
}
|
|
View view{MapViewOfFile(owned[mapping].value, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedReport))};
|
|
if (!view.value) return StartResult::failed;
|
|
auto* shared = new (view.value) SharedReport{};
|
|
if (!copy(shared->url, url) || !copy(shared->directory, std::filesystem::absolute(log_directory / L"crashes").wstring()) ||
|
|
!copy(shared->application, std::filesystem::path(process_path.data()).filename().wstring())) return StartResult::failed;
|
|
// Frostbite handles some fatal exceptions itself and writes a native dump
|
|
// before exiting, so those faults never reach our last-chance filter. The
|
|
// game's LocalAppData is redirected to ReSkate\Game (Extension/Boot/user_data_redirect.cpp).
|
|
if (_wcsicmp(shared->application.data(), L"Skate.exe") == 0) {
|
|
const auto local = environment(L"LOCALAPPDATA");
|
|
if (!local.empty() && !copy(shared->native_directory,
|
|
(std::filesystem::path(local) / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps").wstring()))
|
|
return StartResult::failed;
|
|
}
|
|
|
|
SIZE_T size{};
|
|
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
|
|
std::vector<unsigned char> storage(size);
|
|
auto* attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage.data());
|
|
if (!InitializeProcThreadAttributeList(attributes, 1, 0, &size)) return StartResult::failed;
|
|
struct Attributes { LPPROC_THREAD_ATTRIBUTE_LIST value; ~Attributes() { DeleteProcThreadAttributeList(value); } } cleanup{attributes};
|
|
if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
|
inherited.data(), sizeof(inherited), nullptr, nullptr)) return StartResult::failed;
|
|
auto command = std::format(L"\"{}\" --reskate-crash-helper", executable.wstring());
|
|
for (const auto handle : inherited) command += std::format(L" {}", reinterpret_cast<ULONG_PTR>(handle));
|
|
STARTUPINFOEXW startup{};
|
|
startup.StartupInfo.cb = sizeof(startup);
|
|
startup.lpAttributeList = attributes;
|
|
PROCESS_INFORMATION process{};
|
|
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE,
|
|
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, executable.parent_path().c_str(),
|
|
&startup.StartupInfo, &process)) return StartResult::failed;
|
|
Handle child(process.hProcess), thread(process.hThread);
|
|
// Prevent later child processes from inheriting our IPC and parent handles.
|
|
for (const auto handle : inherited) SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
|
|
const HANDLE wait_for[]{owned[ready].value, child.value};
|
|
if (WaitForMultipleObjects(2, wait_for, FALSE, 5000) != WAIT_OBJECT_0) {
|
|
SetEvent(owned[stopping].value);
|
|
return StartResult::failed;
|
|
}
|
|
for (std::size_t i = 0; i < handle_count; ++i) handles[i] = owned[i].release();
|
|
helper = child.release();
|
|
report = shared;
|
|
view.value = nullptr;
|
|
InterlockedExchange(&capturing, 0);
|
|
return StartResult::ready;
|
|
} catch (...) { return StartResult::failed; }
|
|
}
|
|
void capture(EXCEPTION_POINTERS* exception) noexcept {
|
|
if (!exception || InterlockedCompareExchange(&capturing, 1, 0) != 0) return;
|
|
report->thread_id = GetCurrentThreadId();
|
|
report->exception = exception;
|
|
MemoryBarrier();
|
|
if (SetEvent(handles[requested])) {
|
|
const HANDLE wait_for[]{handles[captured], helper};
|
|
WaitForMultipleObjects(2, wait_for, FALSE, capture_timeout_ms);
|
|
}
|
|
}
|
|
void set_destination(std::wstring_view destination) noexcept {
|
|
if (auto* const shared = report) copy_cut(shared->destination, destination);
|
|
}
|
|
void set_gpu(std::wstring_view name, std::wstring_view driver) noexcept {
|
|
if (auto* const shared = report) {
|
|
copy_cut(shared->gpu_name, name);
|
|
copy_cut(shared->gpu_driver, driver);
|
|
}
|
|
}
|
|
void stop() noexcept {
|
|
// An exception already using these handles owns them until process exit.
|
|
// Never unmap its report or close its events from a concurrent shutdown.
|
|
if (InterlockedCompareExchange(&capturing, 2, 0) != 0) return;
|
|
SetEvent(handles[stopping]);
|
|
UnmapViewOfFile(report);
|
|
report = nullptr;
|
|
for (auto& handle : handles) { CloseHandle(handle); handle = nullptr; }
|
|
CloseHandle(helper);
|
|
helper = nullptr;
|
|
}
|
|
}
|