// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // Offline ETL decoder. No process attach, memory read or thread suspension. #define NOMINMAX #include #include #include #include #include #include #include #include #include #include static constexpr GUID NATIVE = { 0x54ee9231, 0x28be, 0x4ab0, {0x98, 0x1e, 0xb2, 0x6f, 0x97, 0x9c, 0x56, 0xa4}}; static FILE* output; static unsigned long long decoded = 0, errors = 0, total = 0; static std::map> schemas; static bool inspect_system = false; static unsigned inspected_schemas = 0, failed_schemas = 0; static std::string UTF8(const wchar_t* value) { if (!value || !*value) return {}; int n = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr); std::string text(n, '\0'); WideCharToMultiByte(CP_UTF8, 0, value, -1, text.data(), n, nullptr, nullptr); text.pop_back(); return text; } static std::string Quote(const std::string& value) { std::string result = "\""; for (unsigned char c : value) { if (c == '"' || c == '\\') { result += '\\'; result += c; } else if (c < 32) { char escaped[7]; std::snprintf(escaped, sizeof(escaped), "\\u%04x", c); result += escaped; } else result += c; } return result + '"'; } static void WINAPI Event(PEVENT_RECORD record) { ++total; const bool native = IsEqualGUID(record->EventHeader.ProviderId, NATIVE); std::string key(reinterpret_cast(&record->EventHeader.ProviderId), sizeof(GUID)); key.append(reinterpret_cast(&record->EventHeader.EventDescriptor), sizeof(EVENT_DESCRIPTOR)); auto& buffer = schemas[key]; const bool first_schema = buffer.empty(); if (native || buffer.empty()) { ULONG bytes = 0; const ULONG status = TdhGetEventInformation(record, 0, nullptr, nullptr, &bytes); if (status != ERROR_INSUFFICIENT_BUFFER) { if (native) ++errors; else if (inspect_system && failed_schemas++ < 20) std::fprintf(stderr, "TDH missing: event=%u opcode=%u status=%lu\n", record->EventHeader.EventDescriptor.Id, record->EventHeader.EventDescriptor.Opcode, status); return; } buffer.resize(bytes); if (TdhGetEventInformation(record, 0, nullptr, reinterpret_cast(buffer.data()), &bytes) != ERROR_SUCCESS) { if (native) ++errors; buffer.clear(); return; } } auto* info = reinterpret_cast(buffer.data()); const auto text = [&](ULONG offset) -> std::string { return offset ? UTF8(reinterpret_cast(buffer.data() + offset)) : ""; }; std::string name = text(info->EventNameOffset); if (name.empty()) name = text(info->TaskNameOffset) + "/" + text(info->OpcodeNameOffset); if (inspect_system && !native && first_schema && inspected_schemas++ < 300) { std::fprintf(stderr, "SCHEMA event=%u opcode=%u name=%s fields=", record->EventHeader.EventDescriptor.Id, record->EventHeader.EventDescriptor.Opcode, name.c_str()); for (ULONG i = 0; i < info->TopLevelPropertyCount && i < 30; ++i) std::fprintf(stderr, "%s%s", i ? "," : "", text(info->EventPropertyInfoArray[i].NameOffset).c_str()); std::fprintf(stderr, "\n"); } bool interesting = native; // Keep scheduling and instruction pointer records for independent qualification. for (ULONG i = 0; i < info->TopLevelPropertyCount; ++i) { const auto field = text(info->EventPropertyInfoArray[i].NameOffset); if (field == "InstructionPointer" || field == "OldThreadId" || field == "TThreadId") interesting = true; } if (!interesting) return; std::string fields; for (ULONG i = 0; i < info->TopLevelPropertyCount; ++i) { const auto& property = info->EventPropertyInfoArray[i]; if (property.Flags & PropertyStruct) continue; const auto field = text(property.NameOffset); PROPERTY_DATA_DESCRIPTOR descriptor{}; descriptor.PropertyName = reinterpret_cast(buffer.data() + property.NameOffset); descriptor.ArrayIndex = ULONG_MAX; ULONG size = 0; if (TdhGetPropertySize(record, 0, nullptr, 1, &descriptor, &size) != ERROR_SUCCESS || size > 65536) { if (native) ++errors; continue; } std::vector bytes(size + 2, 0); if (TdhGetProperty(record, 0, nullptr, 1, &descriptor, size, bytes.data()) != ERROR_SUCCESS) { if (native) ++errors; continue; } std::string value; const auto type = property.nonStructType.InType; if (type == TDH_INTYPE_UNICODESTRING) value = Quote(UTF8(reinterpret_cast(bytes.data()))); else if (type == TDH_INTYPE_ANSISTRING) value = Quote(reinterpret_cast(bytes.data())); else if (size == 1 || size == 2 || size == 4 || size == 8) { unsigned long long number = 0; std::memcpy(&number, bytes.data(), size); value = std::to_string(number); } else { if (native) ++errors; continue; } if (!fields.empty()) fields += ','; fields += Quote(field) + ':' + value; } std::fprintf(output, "{\"native\":%s,\"event\":%s,\"qpc\":%lld,\"pid\":%lu,\"tid\":%lu,\"cpu\":%u," "\"opcode\":%u,\"data\":{%s}}\n", native ? "true" : "false", Quote(name).c_str(), record->EventHeader.TimeStamp.QuadPart, record->EventHeader.ProcessId, record->EventHeader.ThreadId, record->BufferContext.ProcessorNumber, record->EventHeader.EventDescriptor.Opcode, fields.c_str()); ++decoded; } int wmain(int argc, wchar_t** argv) { if (argc != 3 && (argc != 4 || std::wcscmp(argv[3], L"--inspect-system") != 0)) { std::fprintf(stderr, "DecodeP1 input.etl NEW-output.jsonl [--inspect-system]\n"); return 2; } inspect_system = argc == 4; if (GetFileAttributesW(argv[2]) != INVALID_FILE_ATTRIBUTES) { std::fprintf(stderr, "Refusing overwrite\n"); return 2; } if (_wfopen_s(&output, argv[2], L"wb")) return 2; EVENT_TRACE_LOGFILEW logfile{}; logfile.LogFileName = argv[1]; logfile.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_RAW_TIMESTAMP; logfile.EventRecordCallback = Event; TRACEHANDLE trace = OpenTraceW(&logfile); if (trace == INVALID_PROCESSTRACE_HANDLE) { std::fclose(output); return 3; } const ULONG result = ProcessTrace(&trace, 1, nullptr, nullptr); CloseTrace(trace); std::fclose(output); std::printf("{\"status\":%lu,\"total\":%llu,\"decoded\":%llu,\"native_decode_errors\":%llu," "\"events_lost\":%lu,\"buffers_lost\":%lu,\"qpc_frequency\":%lld}\n", result, total, decoded, errors, logfile.LogfileHeader.EventsLost, logfile.LogfileHeader.BuffersLost, logfile.LogfileHeader.PerfFreq.QuadPart); return result != ERROR_SUCCESS || errors != 0 || logfile.LogfileHeader.EventsLost != 0 || logfile.LogfileHeader.BuffersLost != 0; }