Skip to content

Commit 8ea0e8c

Browse files
committed
Add crash command capture to preserve crashing API call in trace
When a Vulkan driver crashes during an API call, the capture layer currently loses the offending command because parameter encoding happens post-call. This adds a new opt-in capture mode that records which API call is in-flight at the time of a crash. When enabled via GFXRECON_CAPTURE_CRASH_COMMAND=true: - Before each driver dispatch, a pre-encoded function call block (containing the call ID and thread ID) is saved in thread-local storage - Crash signal handlers (SIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL on POSIX, SEH on Windows) write any in-flight call blocks to the capture file and flush - Force-flush is activated so all completed calls are on disk - After the driver call returns, the in-flight marker is cleared The crashing function appears as the last block in the capture file, identifiable by tools like gfxrecon-convert. All preceding calls are preserved with full parameters. Performance impact when disabled: zero (early return on bool check). When enabled: one atomic store + small memcpy per API call, plus force-flush overhead. Fixes #699
1 parent b9c753a commit 8ea0e8c

8 files changed

Lines changed: 3004 additions & 0 deletions

File tree

framework/encode/api_capture_manager.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ class ApiCaptureManager
102102
}
103103
void EndApiCallCapture() { common_manager_->EndApiCallCapture(); }
104104

105+
void SavePreCallData(format::ApiCallId call_id) { common_manager_->SavePreCallData(call_id); }
106+
void ClearPreCallData() { common_manager_->ClearPreCallData(); }
107+
105108
void EndMethodCallCapture() { common_manager_->EndMethodCallCapture(); }
106109

107110
void WriteFrameMarker(format::MarkerType marker_type) { common_manager_->WriteFrameMarker(marker_type); }

framework/encode/capture_manager.cpp

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@
4545
#include <cstdlib>
4646
#include <unordered_map>
4747

48+
#if defined(__linux__) || defined(__APPLE__)
49+
#include <signal.h>
50+
#include <unistd.h>
51+
#elif defined(WIN32)
52+
#include <windows.h>
53+
#endif
54+
4855
#if defined(__unix__)
4956
extern char** environ;
5057
#endif
@@ -70,6 +77,10 @@ static std::mutex external_trim_trigger_mutex_g;
7077
static bool externally_set_trimming_state_g = false;
7178
static bool previous_externally_set_trimming_state_g = false;
7279

80+
std::mutex CommonCaptureManager::crash_thread_lock_;
81+
std::vector<util::ThreadData*> CommonCaptureManager::crash_threads_;
82+
std::atomic<bool> CommonCaptureManager::crash_handler_installed_{ false };
83+
7384
extern "C"
7485
{
7586
// The following two functions are made public and should be discoverable via dlsym.
@@ -427,6 +438,15 @@ bool CommonCaptureManager::Initialize(format::ApiFamilyId api_
427438
use_asset_file_ = trace_settings.use_asset_file;
428439
ignore_frame_boundary_android_ = trace_settings.ignore_frame_boundary_android;
429440
skip_threads_with_invalid_data_ = trace_settings.skip_threads_with_invalid_data;
441+
crash_command_enabled_ = trace_settings.capture_crash_command;
442+
443+
if (crash_command_enabled_)
444+
{
445+
// Crash command capture requires force flush to ensure all prior calls are on disk.
446+
force_file_flush_ = true;
447+
GFXRECON_LOG_INFO("Crash command capture enabled. Force file flush activated.");
448+
InstallCrashHandler();
449+
}
430450

431451
rv_annotation_info_.gpuva_mask = trace_settings.rv_anotation_info.gpuva_mask;
432452
rv_annotation_info_.descriptor_mask = trace_settings.rv_anotation_info.descriptor_mask;
@@ -1688,6 +1708,135 @@ void CommonCaptureManager::WriteToFile(const void* data, size_t size, util::File
16881708
++block_index_;
16891709
}
16901710

1711+
void CommonCaptureManager::InstallCrashHandler()
1712+
{
1713+
bool expected = false;
1714+
if (!crash_handler_installed_.compare_exchange_strong(expected, true))
1715+
{
1716+
return; // Already installed.
1717+
}
1718+
1719+
#if defined(__linux__) || defined(__APPLE__)
1720+
struct sigaction sa = {};
1721+
sa.sa_handler = CrashSignalHandler;
1722+
sa.sa_flags = SA_RESETHAND; // One-shot: restore default handler after first invocation.
1723+
sigemptyset(&sa.sa_mask);
1724+
1725+
sigaction(SIGSEGV, &sa, nullptr);
1726+
sigaction(SIGABRT, &sa, nullptr);
1727+
sigaction(SIGBUS, &sa, nullptr);
1728+
sigaction(SIGFPE, &sa, nullptr);
1729+
sigaction(SIGILL, &sa, nullptr);
1730+
#elif defined(WIN32)
1731+
SetUnhandledExceptionFilter([](EXCEPTION_POINTERS*) -> LONG {
1732+
CrashSignalHandler(0);
1733+
return EXCEPTION_CONTINUE_SEARCH;
1734+
});
1735+
#endif
1736+
1737+
GFXRECON_LOG_INFO("Crash signal handler installed for capture recovery.");
1738+
}
1739+
1740+
void CommonCaptureManager::CrashSignalHandler(int signal)
1741+
{
1742+
// This runs in a signal handler context. Only async-signal-safe operations are allowed.
1743+
// We avoid mutexes, malloc, and logging here. We use direct file descriptor writes.
1744+
CommonCaptureManager* manager = singleton_;
1745+
if (manager == nullptr || manager->file_stream_ == nullptr)
1746+
{
1747+
return;
1748+
}
1749+
1750+
// First flush any buffered data from prior completed writes.
1751+
manager->file_stream_->Flush();
1752+
1753+
// Scan all registered threads for in-flight calls and write their pre-call buffers.
1754+
// Note: We don't lock crash_thread_lock_ because we're in a signal handler and the
1755+
// mutex may be held by the crashing thread. The vector is append-only during normal
1756+
// operation, so reading it without the lock is safe enough for crash recovery.
1757+
for (auto* thread_data : crash_threads_)
1758+
{
1759+
format::ApiCallId call_id = thread_data->inflight_call_id_.load(std::memory_order_acquire);
1760+
if (call_id != format::ApiCallId::ApiCall_Unknown && thread_data->precall_data_size_ > 0)
1761+
{
1762+
// Write the pre-call function call block to the capture file.
1763+
manager->file_stream_->Write(thread_data->precall_buffer_.data(), thread_data->precall_data_size_);
1764+
}
1765+
}
1766+
1767+
// Final flush to ensure the crash command data reaches disk.
1768+
manager->file_stream_->Flush();
1769+
1770+
// Re-raise the signal with default handler (SA_RESETHAND already restored it).
1771+
#if defined(__linux__) || defined(__APPLE__)
1772+
raise(signal);
1773+
#endif
1774+
}
1775+
1776+
void CommonCaptureManager::RegisterThreadForCrashCapture()
1777+
{
1778+
auto thread_data = GetThreadData();
1779+
std::lock_guard<std::mutex> lock(crash_thread_lock_);
1780+
crash_threads_.push_back(thread_data);
1781+
}
1782+
1783+
void CommonCaptureManager::SavePreCallData(format::ApiCallId call_id)
1784+
{
1785+
if (!crash_command_enabled_)
1786+
{
1787+
return;
1788+
}
1789+
1790+
auto thread_data = GetThreadData();
1791+
1792+
// Lazy-register this thread for crash capture tracking.
1793+
if (!thread_data->crash_capture_registered_)
1794+
{
1795+
std::lock_guard<std::mutex> lock(crash_thread_lock_);
1796+
crash_threads_.push_back(thread_data);
1797+
thread_data->crash_capture_registered_ = true;
1798+
}
1799+
1800+
// Build an uncompressed FunctionCallBlock for the call ID with no parameter data.
1801+
// This serves as a "this call was in-flight when we crashed" marker.
1802+
format::FunctionCallHeader header = {};
1803+
header.block_header.type = format::BlockType::kFunctionCallBlock;
1804+
header.api_call_id = call_id;
1805+
header.thread_id = thread_data->thread_id_;
1806+
header.block_header.size = sizeof(header.api_call_id) + sizeof(header.thread_id);
1807+
1808+
size_t total_size = sizeof(header);
1809+
if (thread_data->precall_buffer_.size() < total_size)
1810+
{
1811+
thread_data->precall_buffer_.resize(total_size);
1812+
}
1813+
memcpy(thread_data->precall_buffer_.data(), &header, sizeof(header));
1814+
thread_data->precall_data_size_ = total_size;
1815+
1816+
// Mark this thread as having an in-flight call. This must be set AFTER the buffer is populated.
1817+
thread_data->inflight_call_id_.store(call_id, std::memory_order_release);
1818+
}
1819+
1820+
void CommonCaptureManager::ClearPreCallData()
1821+
{
1822+
if (!crash_command_enabled_)
1823+
{
1824+
return;
1825+
}
1826+
1827+
auto thread_data = GetThreadData();
1828+
thread_data->inflight_call_id_.store(format::ApiCallId::ApiCall_Unknown, std::memory_order_release);
1829+
thread_data->precall_data_size_ = 0;
1830+
}
1831+
1832+
void CommonCaptureManager::FlushCaptureFile()
1833+
{
1834+
if (file_stream_ != nullptr)
1835+
{
1836+
file_stream_->Flush();
1837+
}
1838+
}
1839+
16911840
void CommonCaptureManager::AtExit()
16921841
{
16931842
if (CommonCaptureManager::singleton_)

framework/encode/capture_manager.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,25 @@ class CommonCaptureManager
352352
{
353353
return force_file_flush_;
354354
}
355+
356+
bool GetCrashCommandEnabled() const
357+
{
358+
return crash_command_enabled_;
359+
}
360+
361+
/// @brief Save a pre-encoded function call block for crash recovery.
362+
/// Call this before dispatching to the driver. If the driver crashes,
363+
/// the signal handler will write this block to the capture file.
364+
void SavePreCallData(format::ApiCallId call_id);
365+
366+
/// @brief Clear the pre-call data after the driver call returns successfully.
367+
void ClearPreCallData();
368+
369+
/// @brief Flush the capture file. Async-signal-safe variant uses low-level I/O.
370+
void FlushCaptureFile();
371+
372+
/// @brief Register the thread for crash capture tracking.
373+
void RegisterThreadForCrashCapture();
355374
CaptureSettings::MemoryTrackingMode GetMemoryTrackingMode() const
356375
{
357376
return memory_tracking_mode_;
@@ -540,8 +559,14 @@ class CommonCaptureManager
540559

541560
private:
542561
static void AtExit();
562+
static void InstallCrashHandler();
563+
static void CrashSignalHandler(int signal);
543564

544565
private:
566+
// Crash capture: track all threads that have pre-call data
567+
static std::mutex crash_thread_lock_;
568+
static std::vector<util::ThreadData*> crash_threads_;
569+
static std::atomic<bool> crash_handler_installed_;
545570
static std::mutex instance_lock_;
546571
static CommonCaptureManager* singleton_;
547572
static thread_local std::unique_ptr<util::ThreadData> thread_data_;
@@ -614,6 +639,7 @@ class CommonCaptureManager
614639
bool write_state_files_;
615640
bool ignore_frame_boundary_android_;
616641
bool skip_threads_with_invalid_data_;
642+
bool crash_command_enabled_;
617643

618644
struct
619645
{

framework/encode/capture_settings.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ const char kIgnoreFrameBoundaryAndroidEnvVar[] = GFXRECON_OPTION_S
115115
const char kSkipThreadsWithInvalidDataEnvVar[] = GFXRECON_OPTION_STR(SKIP_THREADS_WITH_INVALID_DATA);
116116
const char kCaptureEnvironmentEnvVar[] = GFXRECON_OPTION_STR(CAPTURE_ENVIRONMENT);
117117
const char kCaptureProcessNameEnvVar[] = GFXRECON_OPTION_STR(CAPTURE_PROCESS_NAME);
118+
const char kCaptureCrashCommandEnvVar[] = GFXRECON_OPTION_STR(CAPTURE_CRASH_COMMAND);
118119

119120
#if defined(__ANDROID__)
120121
// Android-specific capture options
@@ -180,6 +181,7 @@ const std::string kOptionIgnoreFrameBoundaryAndroid = std::stri
180181
const std::string kOptionSkipThreadsWithInvalidData = std::string(kSettingsFilter) + std::string(SKIP_THREADS_WITH_INVALID_DATA_LOWER);
181182
const std::string kOptionCaptureEnvironment = std::string(kSettingsFilter) + std::string(CAPTURE_ENVIRONMENT_LOWER);
182183
const std::string kOptionCaptureProcessName = std::string(kSettingsFilter) + std::string(CAPTURE_PROCESS_NAME_LOWER);
184+
const std::string kOptionCaptureCrashCommand = std::string(kSettingsFilter) + std::string(CAPTURE_CRASH_COMMAND_LOWER);
183185

184186
#if defined(GFXRECON_ENABLE_LZ4_COMPRESSION)
185187
const format::CompressionType kDefaultCompressionType = format::CompressionType::kLz4;
@@ -362,6 +364,7 @@ void CaptureSettings::LoadOptionsEnvVar(OptionsMap* options, bool load_log_setti
362364

363365
LoadSingleOptionEnvVar(options, kCaptureEnvironmentEnvVar, kOptionCaptureEnvironment);
364366
LoadSingleOptionEnvVar(options, kCaptureProcessNameEnvVar, kOptionCaptureProcessName);
367+
LoadSingleOptionEnvVar(options, kCaptureCrashCommandEnvVar, kOptionCaptureCrashCommand);
365368
}
366369

367370
void CaptureSettings::LoadOptionsFile(OptionsMap* options)
@@ -603,6 +606,9 @@ void CaptureSettings::ProcessOptions(OptionsMap* options, CaptureSettings* setti
603606
util::strings::SplitString(FindOption(options, kOptionCaptureEnvironment), ',');
604607
settings->trace_settings_.capture_process_name =
605608
FindOption(options, kOptionCaptureProcessName, settings->trace_settings_.capture_process_name);
609+
610+
settings->trace_settings_.capture_crash_command = ParseBoolString(
611+
FindOption(options, kOptionCaptureCrashCommand), settings->trace_settings_.capture_crash_command);
606612
}
607613

608614
void CaptureSettings::ProcessLogOptions(OptionsMap* options, CaptureSettings* settings)

framework/encode/capture_settings.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ GFXRECON_BEGIN_NAMESPACE(encode)
154154
#define CAPTURE_ENVIRONMENT_UPPER "CAPTURE_ENVIRONMENT"
155155
#define CAPTURE_PROCESS_NAME_LOWER "capture_process_name"
156156
#define CAPTURE_PROCESS_NAME_UPPER "CAPTURE_PROCESS_NAME"
157+
#define CAPTURE_CRASH_COMMAND_LOWER "capture_crash_command"
158+
#define CAPTURE_CRASH_COMMAND_UPPER "CAPTURE_CRASH_COMMAND"
157159
// clang-format on
158160

159161
class CaptureSettings
@@ -233,6 +235,7 @@ class CaptureSettings
233235
uint32_t trim_key_frames{ 0 };
234236
RuntimeTriggerState runtime_capture_trigger{ kNotUsed };
235237
std::string capture_process_name{ "" };
238+
bool capture_crash_command{ false };
236239
bool runtime_write_assets{ false };
237240
int page_guard_signal_handler_watcher_max_restores{ 1 };
238241
bool page_guard_copy_on_map{ util::PageGuardManager::kDefaultEnableCopyOnMap };

0 commit comments

Comments
 (0)