Preserve formatted NAND boot and Wii Remote pairing, add DMA/SRAM conformance tests and a Wii comparison probe. Keep P1 profiling tools and native update A/B evidence; reject the unhelpful targeted SRAM shortcut.
205 lines
7.1 KiB
C++
205 lines
7.1 KiB
C++
// 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 <windows.h>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cwchar>
|
|
#include <evntrace.h>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <tdh.h>
|
|
#include <vector>
|
|
|
|
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<std::string, std::vector<unsigned char>> 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<const char*>(&record->EventHeader.ProviderId), sizeof(GUID));
|
|
key.append(reinterpret_cast<const char*>(&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<PTRACE_EVENT_INFO>(buffer.data()),
|
|
&bytes) != ERROR_SUCCESS)
|
|
{
|
|
if (native)
|
|
++errors;
|
|
buffer.clear();
|
|
return;
|
|
}
|
|
}
|
|
auto* info = reinterpret_cast<PTRACE_EVENT_INFO>(buffer.data());
|
|
const auto text = [&](ULONG offset) -> std::string {
|
|
return offset ? UTF8(reinterpret_cast<wchar_t*>(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<ULONGLONG>(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<unsigned char> 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<wchar_t*>(bytes.data())));
|
|
else if (type == TDH_INTYPE_ANSISTRING)
|
|
value = Quote(reinterpret_cast<char*>(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;
|
|
}
|