diff --git a/cli/device.cpp b/cli/device.cpp index 8f5de60e..0f9feb76 100644 --- a/cli/device.cpp +++ b/cli/device.cpp @@ -284,18 +284,44 @@ bool VulkanDevice::init_device(const Options &opts) if (vkEnumeratePhysicalDevices(instance, &gpu_count, gpus.data()) != VK_SUCCESS) return false; + int selected_gpu_index = -1; + bool pci_filter_set = opts.device_pci_vendor != 0; + bool pci_filter_matched = false; for (uint32_t i = 0; i < gpu_count; i++) { - vkGetPhysicalDeviceProperties(gpus[i], &gpu_props); + VkPhysicalDeviceProperties gpu_enum_props; + vkGetPhysicalDeviceProperties(gpus[i], &gpu_enum_props); LOGI("Enumerated GPU #%u:\n", i); - LOGI(" name: %s\n", gpu_props.deviceName); + LOGI(" name: %s\n", gpu_enum_props.deviceName); LOGI(" apiVersion: %u.%u.%u\n", - VK_VERSION_MAJOR(gpu_props.apiVersion), - VK_VERSION_MINOR(gpu_props.apiVersion), - VK_VERSION_PATCH(gpu_props.apiVersion)); + VK_VERSION_MAJOR(gpu_enum_props.apiVersion), + VK_VERSION_MINOR(gpu_enum_props.apiVersion), + VK_VERSION_PATCH(gpu_enum_props.apiVersion)); + LOGI(" vendorID: 0x%x\n", gpu_enum_props.vendorID); + LOGI(" deviceID: 0x%x\n", gpu_enum_props.deviceID); + + if (VulkanDevice::pci_filter_matches(opts, + gpu_enum_props.vendorID, + gpu_enum_props.deviceID)) + { + selected_gpu_index = i; + pci_filter_matched = true; + break; + } + } + + if (pci_filter_set && !pci_filter_matched) + { + LOGW("No GPU matched --device-pci-vendor 0x%x%s; falling back to default selection.\n", + opts.device_pci_vendor, + opts.device_pci_device ? " (with --device-pci-device)" : ""); } - if (opts.device_index >= 0) + if (selected_gpu_index >= 0) + { + gpu = gpus[selected_gpu_index]; + } + else if (opts.device_index >= 0) { if (size_t(opts.device_index) >= gpus.size()) { diff --git a/cli/device.hpp b/cli/device.hpp index 193fce21..51d99670 100644 --- a/cli/device.hpp +++ b/cli/device.hpp @@ -38,6 +38,8 @@ class VulkanDevice : public DeviceQueryInterface bool null_device = false; bool want_pipeline_stats = false; int device_index = -1; + unsigned device_pci_vendor = 0; + unsigned device_pci_device = 0; const VkApplicationInfo *application_info = nullptr; const VkPhysicalDeviceFeatures2 *features = nullptr; }; @@ -111,6 +113,22 @@ class VulkanDevice : public DeviceQueryInterface return gpu_props; } + // Pure predicate used both by init_device's selection loop and the + // gpu_selection_test. Keeps PCI filter semantics in a single place so + // the CLI and the test cannot drift apart. + static inline bool pci_filter_matches(const Options &opts, + uint32_t vendor_id, + uint32_t device_id) + { + if (opts.device_pci_vendor == 0) + return false; + if (vendor_id != opts.device_pci_vendor) + return false; + if (opts.device_pci_device != 0 && device_id != opts.device_pci_device) + return false; + return true; + } + // Special helper which deals with SAMPLER_YCBCR_CONVERSION_CREATE_INFO. // Prefer using this instead of vkCreateSampler directly. // The relevant pNext will be mutated into CONVERSION_INFO in-place if it exists. diff --git a/cli/fossilize_replay.cpp b/cli/fossilize_replay.cpp index b087d70a..3b7781ff 100644 --- a/cli/fossilize_replay.cpp +++ b/cli/fossilize_replay.cpp @@ -3637,6 +3637,8 @@ static void print_help() LOGI("fossilize-replay\n" "\t[--help]\n" "\t[--device-index ]\n" + "\t[--device-pci-vendor ]\n" + "\t[--device-pci-device ]\n" "\t[--enable-validation]\n" "\t[--enable-pipeline-stats ]\n" "\t[--spirv-val]\n" @@ -4570,6 +4572,12 @@ int main(int argc, char *argv[]) cbs.default_handler = [&](const char *arg) { databases.push_back(arg); }; cbs.add("--help", [](CLIParser &parser) { print_help(); parser.end(); }); cbs.add("--device-index", [&](CLIParser &parser) { opts.device_index = parser.next_uint(); }); + cbs.add("--device-pci-vendor", [&](CLIParser &parser) { + opts.device_pci_vendor = strtoul(parser.next_string(), nullptr, 0); + }); + cbs.add("--device-pci-device", [&](CLIParser &parser) { + opts.device_pci_device = strtoul(parser.next_string(), nullptr, 0); + }); cbs.add("--enable-validation", [&](CLIParser &) { opts.enable_validation = true; }); cbs.add("--spirv-val", [&](CLIParser &) { replayer_opts.spirv_validate = true; }); cbs.add("--on-disk-pipeline-cache", [&](CLIParser &parser) { replayer_opts.on_disk_pipeline_cache_path = parser.next_string(); }); diff --git a/cli/fossilize_replay_linux.hpp b/cli/fossilize_replay_linux.hpp index a65d2362..818c15d1 100644 --- a/cli/fossilize_replay_linux.hpp +++ b/cli/fossilize_replay_linux.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -524,6 +525,7 @@ bool ProcessProgress::start_child_process(vector &siblings) if (pipe(input_fds) < 0) return false; + pid_t parent_pid = getpid(); pid_t new_pid = fork(); // Fork off a child. if (new_pid > 0) { @@ -560,6 +562,15 @@ bool ProcessProgress::start_child_process(vector &siblings) close(Global::control_fd); // We're the child process. + // Kill the child immediately if the parent (fossilize_replay / steam) dies. + if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) + { + LOGE("Failed to set PR_SET_PDEATHSIG, aborting child startup.\n"); + _exit(EXIT_FAILURE); + } + if (getppid() != parent_pid) + _exit(EXIT_FAILURE); + // Unblock the signal mask. if (pthread_sigmask(SIG_SETMASK, &Global::old_mask, nullptr) != 0) return EXIT_FAILURE; diff --git a/fossilize_external_replayer.hpp b/fossilize_external_replayer.hpp index 55c86d79..3e02b28d 100644 --- a/fossilize_external_replayer.hpp +++ b/fossilize_external_replayer.hpp @@ -107,6 +107,10 @@ class ExternalReplayer // Maps to --device-index. unsigned device_index; + // Maps to --device-pci-vendor and --device-pci-device. + unsigned device_pci_vendor; + unsigned device_pci_device; + // Carve out a range of which pipelines to replay if use_pipeline_range is set. // Used for multi-process replays where each process gets its own slice to churn through. unsigned start_graphics_index; diff --git a/fossilize_external_replayer_linux.hpp b/fossilize_external_replayer_linux.hpp index 3a7dbd0b..bd58e5a8 100644 --- a/fossilize_external_replayer_linux.hpp +++ b/fossilize_external_replayer_linux.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -704,6 +705,21 @@ void ExternalReplayer::Impl::start_replayer_process(const ExternalReplayer::Opti sprintf(index_name, "%u", options.device_index); argv.push_back(index_name); + char pci_vendor_name[16], pci_device_name[16]; + if (options.device_pci_vendor != 0) + { + argv.push_back("--device-pci-vendor"); + snprintf(pci_vendor_name, sizeof(pci_vendor_name), "0x%x", options.device_pci_vendor); + argv.push_back(pci_vendor_name); + } + + if (options.device_pci_device != 0) + { + argv.push_back("--device-pci-device"); + snprintf(pci_device_name, sizeof(pci_device_name), "0x%x", options.device_pci_device); + argv.push_back(pci_device_name); + } + char graphics_range_start[16], graphics_range_end[16]; char compute_range_start[16], compute_range_end[16]; char raytracing_range_start[16], raytracing_range_end[16]; @@ -948,6 +964,7 @@ bool ExternalReplayer::Impl::start(const ExternalReplayer::Options &options) if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, control_fds) < 0) return false; + pid_t parent_pid = getpid(); pid_t new_pid = fork(); if (new_pid > 0) { @@ -966,6 +983,15 @@ bool ExternalReplayer::Impl::start(const ExternalReplayer::Options &options) } else if (new_pid == 0) { + // Kill replayer process immediately if parent application dies. + if (prctl(PR_SET_PDEATHSIG, SIGKILL) != 0) + { + LOGE("Failed to set PR_SET_PDEATHSIG, aborting child startup.\n"); + _exit(1); + } + if (getppid() != parent_pid) + _exit(1); + close(fds[0]); close(control_fds[1]); close(child_fds[0]); diff --git a/fossilize_external_replayer_windows.hpp b/fossilize_external_replayer_windows.hpp index cc81c072..915a97f2 100644 --- a/fossilize_external_replayer_windows.hpp +++ b/fossilize_external_replayer_windows.hpp @@ -586,6 +586,19 @@ bool ExternalReplayer::Impl::start(const ExternalReplayer::Options &options) cmdline += " --device-index "; cmdline += std::to_string(options.device_index); + char hex_buf[32]; + if (options.device_pci_vendor != 0) + { + snprintf(hex_buf, sizeof(hex_buf), " --device-pci-vendor 0x%x", options.device_pci_vendor); + cmdline += hex_buf; + } + + if (options.device_pci_device != 0) + { + snprintf(hex_buf, sizeof(hex_buf), " --device-pci-device 0x%x", options.device_pci_device); + cmdline += hex_buf; + } + if (options.enable_validation) cmdline += " --enable-validation"; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d251a2fd..b101312f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -58,6 +58,12 @@ if (NOT WIN32) add_test(NAME spinlock-futex-test COMMAND futex-test) else() add_test(NAME linux-futex-test COMMAND futex-test) + add_executable(orphan-prevention-test orphan_prevention_test.cpp) + target_link_libraries(orphan-prevention-test fossilize) + add_test(NAME orphan-prevention-test COMMAND orphan-prevention-test) + add_executable(gpu-selection-test gpu_selection_test.cpp) + target_link_libraries(gpu-selection-test cli-utils fossilize) + add_test(NAME gpu-selection-test COMMAND gpu-selection-test) endif() endif() diff --git a/test/gpu_selection_test.cpp b/test/gpu_selection_test.cpp new file mode 100644 index 00000000..726db40b --- /dev/null +++ b/test/gpu_selection_test.cpp @@ -0,0 +1,113 @@ +#include "cli/device.hpp" +#include +#include +#include +#include + +using namespace Fossilize; + +// Synthetic GPU catalogue. Real systems can have multiple iGPUs + dGPUs, +// so the selection loop must tolerate mixed vendor IDs and not assume +// the matching GPU is at index 0. +struct FakeGpu +{ + uint32_t vendor_id; + uint32_t device_id; + const char *name; +}; + +static const FakeGpu kCatalogue[] = { + { 0x10de, 0x2488, "NVIDIA RTX 3070" }, + { 0x1002, 0x73bf, "AMD Radeon RX 6700 XT" }, + { 0x8086, 0x4907, "Intel UHD 770" }, +}; + +static int select_first_match(VulkanDevice::Options opts) +{ + for (size_t i = 0; i < sizeof(kCatalogue) / sizeof(kCatalogue[0]); i++) + { + if (VulkanDevice::pci_filter_matches(opts, + kCatalogue[i].vendor_id, + kCatalogue[i].device_id)) + return int(i); + } + return -1; +} + +int main() +{ + printf("[TEST] Testing GPU PCI filter selection logic...\n"); + + // Case 1: vendor-only filter matches the first NVIDIA dGPU. + { + VulkanDevice::Options opts = {}; + opts.device_pci_vendor = 0x10de; + int idx = select_first_match(opts); + printf(" vendor=0x10de -> index %d (%s)\n", idx, + idx >= 0 ? kCatalogue[idx].name : "(no match)"); + assert(idx == 0); + } + + // Case 2: vendor+device matches NVIDIA RTX 3070 exactly. + { + VulkanDevice::Options opts = {}; + opts.device_pci_vendor = 0x10de; + opts.device_pci_device = 0x2488; + int idx = select_first_match(opts); + printf(" vendor=0x10de device=0x2488 -> index %d (%s)\n", idx, + idx >= 0 ? kCatalogue[idx].name : "(no match)"); + assert(idx == 0); + } + + // Case 3: vendor matches but specific device does not. + { + VulkanDevice::Options opts = {}; + opts.device_pci_vendor = 0x10de; + opts.device_pci_device = 0x9999; + int idx = select_first_match(opts); + printf(" vendor=0x10de device=0x9999 -> index %d\n", idx); + assert(idx == -1); + } + + // Case 4: no filter (vendor == 0) means "do not filter". + { + VulkanDevice::Options opts = {}; + int idx = select_first_match(opts); + printf(" no filter -> index %d\n", idx); + assert(idx == -1); + } + + // Case 5: vendor-only filter selects AMD in the middle slot. + { + VulkanDevice::Options opts = {}; + opts.device_pci_vendor = 0x1002; + int idx = select_first_match(opts); + printf(" vendor=0x1002 -> index %d (%s)\n", idx, + idx >= 0 ? kCatalogue[idx].name : "(no match)"); + assert(idx == 1); + } + + // Case 6: vendor+device matches the last entry (Intel iGPU). + { + VulkanDevice::Options opts = {}; + opts.device_pci_vendor = 0x8086; + opts.device_pci_device = 0x4907; + int idx = select_first_match(opts); + printf(" vendor=0x8086 device=0x4907 -> index %d (%s)\n", idx, + idx >= 0 ? kCatalogue[idx].name : "(no match)"); + assert(idx == 2); + } + + // Case 7: AMD vendor but with NVIDIA device id -- must NOT match. + { + VulkanDevice::Options opts = {}; + opts.device_pci_vendor = 0x1002; + opts.device_pci_device = 0x2488; + int idx = select_first_match(opts); + printf(" vendor=0x1002 device=0x2488 -> index %d\n", idx); + assert(idx == -1); + } + + printf("[TEST] SUCCESS: GPU PCI filter selection logic verified.\n"); + return 0; +} \ No newline at end of file diff --git a/test/orphan_prevention_test.cpp b/test/orphan_prevention_test.cpp new file mode 100644 index 00000000..2a1e3e7a --- /dev/null +++ b/test/orphan_prevention_test.cpp @@ -0,0 +1,120 @@ +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#include +#include + +int main() +{ + printf("[TEST] Testing Linux PR_SET_PDEATHSIG parent termination behavior...\n"); + + int pipe_fds[2]; + if (pipe(pipe_fds) < 0) + return 1; + + pid_t intermediate_pid = fork(); + if (intermediate_pid < 0) + return 1; + + if (intermediate_pid == 0) + { + // Intermediate parent process (simulating fossilize_replay master process or steam) + close(pipe_fds[0]); + + pid_t parent_pid = getpid(); + pid_t child_pid = fork(); + + if (child_pid == 0) + { + // Child process (simulating replayer slave process) + prctl(PR_SET_PDEATHSIG, SIGKILL); + if (getppid() != parent_pid) + _exit(2); + + pid_t my_pid = getpid(); + // Send actual child PID to top-level test runner + if (write(pipe_fds[1], &my_pid, sizeof(my_pid)) < 0) + _exit(1); + close(pipe_fds[1]); + + // Sleep while waiting for parent to die + while (true) + { + sleep(1); + } + _exit(0); + } + else + { + close(pipe_fds[1]); + // Intermediate parent sleeps until forcibly killed by test runner + while (true) + { + sleep(1); + } + _exit(0); + } + } + + // Top-level test runner process + close(pipe_fds[1]); + pid_t child_pid = 0; + if (read(pipe_fds[0], &child_pid, sizeof(child_pid)) <= 0) + { + fprintf(stderr, "Failed to read child PID from pipe.\n"); + return 1; + } + close(pipe_fds[0]); + + printf("[TEST] Spawned intermediate parent PID: %d, worker child PID: %d\n", intermediate_pid, child_pid); + + // Verify child is initially alive + if (kill(child_pid, 0) != 0) + { + fprintf(stderr, "Child process failed to start properly.\n"); + return 1; + } + + // Forcibly kill intermediate parent process with SIGKILL + printf("[TEST] Sending SIGKILL to intermediate parent process %d...\n", intermediate_pid); + kill(intermediate_pid, SIGKILL); + + int status = 0; + waitpid(intermediate_pid, &status, 0); + + // Poll for kernel process reclamation (max 2.0s timeout, 10ms polling interval) + // Eliminates CI runner scheduler starvation flakiness. + bool child_terminated = false; + for (int i = 0; i < 200; i++) + { + if (kill(child_pid, 0) == -1 && errno == ESRCH) + { + child_terminated = true; + break; + } + usleep(10000); // 10ms + } + + if (child_terminated) + { + printf("[TEST] SUCCESS: Child process %d was cleanly terminated by PDEATHSIG when parent died!\n", child_pid); + return 0; + } + else + { + fprintf(stderr, "[TEST] ERROR: Child process %d is still alive after parent died (ORPHANED)!\n", child_pid); + // Cleanup orphan process if test fails + kill(child_pid, SIGKILL); + return 1; + } +} +#else +int main() +{ + return 0; +} +#endif