mirror of
https://github.com/pound-emu/pound.git
synced 2025-12-15 01:36:57 +00:00
kvm: Add framework for machine types and MMIO dispatch
The core of the machine-type support is the new operations table, kvm_ops_t. This acts as a standard C-style virtual table decoupling the generic KVM core logic from target specific hardware emualtion. The kvm_t VM instance now points to an ops table, which defines the "personality" of the guest. A kvm_probe() factory function has been added to initialize a kvm_t instance with the correct ops table for a given machine type (eg, Switch 1). The ops table's .mmio_read and .mmio_write function pointers are the link between the armv8 CPU core and this new MMIO dispatcher. When a physical memory access is determined to be MMIO, the VM will call the appropriate function pointer, which in turn will use the MMIO dispatcher to find and execute the correct device handler. The initial implementation for the Switch 1 target (targets/switch1/hardware/probe.cpp) is a stub. The bootstrapping logic will be added in subsequent patches. Signed-off-by: Ronald Caesar <github43132@proton.me>
This commit is contained in:
parent
dea94dc259
commit
c6706dd8a0
57 changed files with 533 additions and 70 deletions
442
src/common/Logging/Backend.cpp
Normal file
442
src/common/Logging/Backend.cpp
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
|
||||
#include "common/BoundedQueue.h"
|
||||
#include "common/IoFile.h"
|
||||
#include "common/PathUtil.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "common/Thread.h"
|
||||
|
||||
#include "Backend.h"
|
||||
#include "Log.h"
|
||||
#include "LogEntry.h"
|
||||
#include "TextFormatter.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
using namespace Base::FS;
|
||||
|
||||
// Base backend with shell functions
|
||||
class BaseBackend {
|
||||
public:
|
||||
virtual ~BaseBackend() = default;
|
||||
virtual void Write(const Entry &entry) = 0;
|
||||
virtual void Flush() = 0;
|
||||
};
|
||||
|
||||
|
||||
// Backend that writes to stdout and with color
|
||||
class ColorConsoleBackend : public BaseBackend {
|
||||
public:
|
||||
explicit ColorConsoleBackend() = default;
|
||||
|
||||
~ColorConsoleBackend() = default;
|
||||
|
||||
void Write(const Entry &entry) override {
|
||||
if (enabled.load(std::memory_order_relaxed)) {
|
||||
PrintColoredMessage(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void Flush() override {
|
||||
// stdout shouldn't be buffered
|
||||
}
|
||||
|
||||
void SetEnabled(bool enabled_) {
|
||||
enabled = enabled_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<bool> enabled = true;
|
||||
};
|
||||
|
||||
// Backend that writes to a file passed into the constructor
|
||||
class FileBackend : public BaseBackend {
|
||||
public:
|
||||
explicit FileBackend(const fs::path &filename)
|
||||
: file(filename, FS::FileAccessMode::Write, FS::FileMode::TextMode)
|
||||
{}
|
||||
|
||||
~FileBackend() {
|
||||
file.Close();
|
||||
}
|
||||
|
||||
void Write(const Entry &entry) override {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.formatted) {
|
||||
bytesWritten += file.WriteString(FormatLogMessage(entry).append(1, '\n'));
|
||||
}
|
||||
else {
|
||||
bytesWritten += file.WriteString(entry.message);
|
||||
}
|
||||
|
||||
// Prevent logs from exceeding a set maximum size in the event that log entries are spammed.
|
||||
constexpr u64 writeLimit = 100_MB;
|
||||
const bool writeLimitExceeded = bytesWritten > writeLimit;
|
||||
if (entry.logLevel >= Level::Error || writeLimitExceeded) {
|
||||
if (writeLimitExceeded) {
|
||||
// Stop writing after the write limit is exceeded.
|
||||
// Don't close the file so we can print a stacktrace if necessary
|
||||
enabled = false;
|
||||
}
|
||||
file.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
void Flush() override {
|
||||
file.Flush();
|
||||
}
|
||||
|
||||
private:
|
||||
Base::FS::IOFile file;
|
||||
std::atomic<bool> enabled = true;
|
||||
size_t bytesWritten = 0;
|
||||
};
|
||||
|
||||
bool currentlyInitialising = true;
|
||||
|
||||
// Static state as a singleton.
|
||||
class Impl {
|
||||
public:
|
||||
static Impl& Instance() {
|
||||
if (!instance) {
|
||||
throw std::runtime_error("Using Logging instance before its initialization");
|
||||
}
|
||||
return *instance;
|
||||
}
|
||||
|
||||
static void Initialize(const std::string_view logFile) {
|
||||
if (instance) {
|
||||
LOG_WARNING(Log, "Reinitializing logging backend");
|
||||
return;
|
||||
}
|
||||
const auto logDir = GetUserPath(PathType::LogDir);
|
||||
Filter filter;
|
||||
//filter.ParseFilterString(Config::getLogFilter());
|
||||
instance = std::unique_ptr<Impl, decltype(&Deleter)>(new Impl(logDir / logFile, filter), Deleter);
|
||||
currentlyInitialising = false;
|
||||
}
|
||||
|
||||
static bool IsActive() {
|
||||
return instance != nullptr;
|
||||
}
|
||||
|
||||
static void Start() {
|
||||
instance->StartBackendThread();
|
||||
}
|
||||
|
||||
static void Stop() {
|
||||
instance->StopBackendThread();
|
||||
}
|
||||
|
||||
Impl(const Impl&) = delete;
|
||||
Impl& operator=(const Impl&) = delete;
|
||||
|
||||
Impl(Impl&&) = delete;
|
||||
Impl& operator=(Impl&&) = delete;
|
||||
|
||||
void SetGlobalFilter(const Filter& f) {
|
||||
filter = f;
|
||||
}
|
||||
|
||||
void SetColorConsoleBackendEnabled(bool enabled) {
|
||||
colorConsoleBackend->SetEnabled(enabled);
|
||||
}
|
||||
|
||||
void PushEntry(Class logClass, Level logLevel, const char *filename, u32 lineNum,
|
||||
const char *function, const std::string &message) {
|
||||
|
||||
if (!filter.CheckMessage(logClass, logLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::microseconds;
|
||||
using std::chrono::steady_clock;
|
||||
|
||||
const Entry entry = {
|
||||
.timestamp = duration_cast<microseconds>(steady_clock::now() - timeOrigin),
|
||||
.logClass = logClass,
|
||||
.logLevel = logLevel,
|
||||
.filename = filename,
|
||||
.lineNum = lineNum,
|
||||
.function = function,
|
||||
.message = message,
|
||||
};
|
||||
if (Config::logType() == "async") {
|
||||
messageQueue.EmplaceWait(entry);
|
||||
} else {
|
||||
ForEachBackend([&entry](BaseBackend* backend) { if (backend) { backend->Write(entry); } });
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
void PushEntryNoFmt(Class logClass, Level logLevel, const std::string &message) {
|
||||
if (!filter.CheckMessage(logClass, logLevel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
using std::chrono::duration_cast;
|
||||
using std::chrono::microseconds;
|
||||
using std::chrono::steady_clock;
|
||||
|
||||
const Entry entry = {
|
||||
.timestamp = duration_cast<microseconds>(steady_clock::now() - timeOrigin),
|
||||
.logClass = logClass,
|
||||
.logLevel = logLevel,
|
||||
.message = message,
|
||||
.formatted = false
|
||||
};
|
||||
if (Config::logType() == "async") {
|
||||
messageQueue.EmplaceWait(entry);
|
||||
} else {
|
||||
ForEachBackend([&entry](BaseBackend* backend) { if (backend) { backend->Write(entry); } });
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Impl(const fs::path &fileBackendFilename, const Filter &filter) :
|
||||
filter(filter) {
|
||||
#ifdef _WIN32
|
||||
HANDLE conOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
// Get current console mode
|
||||
ul32 mode = 0;
|
||||
GetConsoleMode(conOut, &mode);
|
||||
// Set WinAPI to use a more 'modern' approach, by enabling VT
|
||||
// Allows ASCII escape codes
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
// Write adjusted mode back
|
||||
SetConsoleMode(conOut, mode);
|
||||
#endif
|
||||
colorConsoleBackend = std::make_unique<ColorConsoleBackend>();
|
||||
fileBackend = std::make_unique<FileBackend>(fileBackendFilename);
|
||||
}
|
||||
|
||||
~Impl() {
|
||||
Stop();
|
||||
fileBackend.reset();
|
||||
colorConsoleBackend.reset();
|
||||
}
|
||||
|
||||
void StartBackendThread() {
|
||||
backendThread = std::jthread([this](std::stop_token stopToken) {
|
||||
Base::SetCurrentThreadName("[Xe] Log");
|
||||
Entry entry = {};
|
||||
const auto writeLogs = [this, &entry]() {
|
||||
ForEachBackend([&entry](BaseBackend *backend) { backend->Write(entry); });
|
||||
};
|
||||
while (!stopToken.stop_requested()) {
|
||||
if (messageQueue.PopWait(entry, stopToken))
|
||||
writeLogs();
|
||||
}
|
||||
// Drain the logging queue. Only writes out up to MAX_LOGS_TO_WRITE to prevent a
|
||||
// case where a system is repeatedly spamming logs even on close.
|
||||
s32 maxLogsToWrite = filter.IsDebug() ? std::numeric_limits<s32>::max() : 100;
|
||||
while (maxLogsToWrite-- > 0) {
|
||||
if (messageQueue.TryPop(entry)) {
|
||||
writeLogs();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopBackendThread() {
|
||||
backendThread.request_stop();
|
||||
if (backendThread.joinable()) {
|
||||
backendThread.join();
|
||||
}
|
||||
|
||||
ForEachBackend([](BaseBackend *backend) { backend->Flush(); });
|
||||
}
|
||||
|
||||
void ForEachBackend(std::function<void(BaseBackend*)> lambda) {
|
||||
lambda(colorConsoleBackend.get());
|
||||
lambda(fileBackend.get());
|
||||
}
|
||||
|
||||
static void Deleter(Impl* ptr) {
|
||||
delete ptr;
|
||||
}
|
||||
|
||||
static inline std::unique_ptr<Impl, decltype(&Deleter)> instance{ nullptr, Deleter };
|
||||
|
||||
Filter filter;
|
||||
std::unique_ptr<ColorConsoleBackend> colorConsoleBackend = {};
|
||||
std::unique_ptr<FileBackend> fileBackend = {};
|
||||
|
||||
MPSCQueue<Entry> messageQueue = {};
|
||||
std::chrono::steady_clock::time_point timeOrigin = std::chrono::steady_clock::now();
|
||||
std::jthread backendThread;
|
||||
};
|
||||
|
||||
std::vector<fs::path> filepaths{};
|
||||
|
||||
void DeleteOldLogs(const fs::path& path, u64 num_logs, const u16 logLimit) {
|
||||
const std::string filename = path.filename().string();
|
||||
const std::chrono::time_point Now = std::chrono::system_clock::now();
|
||||
const time_t timeNow = std::chrono::system_clock::to_time_t(Now);
|
||||
const tm* time = std::localtime(&timeNow);
|
||||
// We want to get rid of anything that isn't that current day's date
|
||||
const std::string currentDate = fmt::format("{}-{}-{}", time->tm_mon + 1, time->tm_mday, 1900 + time->tm_year);
|
||||
if (filename.find(currentDate) == std::string::npos) {
|
||||
fs::remove_all(path);
|
||||
return;
|
||||
}
|
||||
// We want to delete in date of creation, so just add it to a array
|
||||
if (num_logs >= logLimit) {
|
||||
filepaths.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
u64 CreateIntegralTimestamp(const std::string &date) {
|
||||
const u64 monthPos = date.find('-');
|
||||
if (monthPos == std::string::npos) {
|
||||
return 0;
|
||||
}
|
||||
const std::string month = date.substr(0, monthPos);
|
||||
const u64 dayPos = date.find('-', monthPos);
|
||||
if (dayPos == std::string::npos) {
|
||||
return 0;
|
||||
}
|
||||
const u64 yearPos = date.find('-', dayPos);
|
||||
const std::string day = date.substr(monthPos+1);
|
||||
if (yearPos == std::string::npos) {
|
||||
return 0;
|
||||
}
|
||||
const std::string year = date.substr(yearPos + 1);
|
||||
const u64 yearInt = std::stoull(year);
|
||||
const std::string timestamp = fmt::format("{}{}{}", month, day, yearInt - 1900);
|
||||
return std::stoull(timestamp);
|
||||
}
|
||||
|
||||
void CleanupOldLogs(const std::string_view &logFileBase, const fs::path &logDir, const u16 logLimit) {
|
||||
const fs::path LogFile = logFileBase;
|
||||
// Track how many logs we have
|
||||
size_t numLogs = 0;
|
||||
for (auto &entry : fs::directory_iterator(logDir)) {
|
||||
if (entry.is_regular_file()) {
|
||||
const fs::path path = entry.path();
|
||||
const std::string ext = path.extension().string();
|
||||
if (!path.has_extension()) {
|
||||
// Skip anything that isn't a log file
|
||||
continue;
|
||||
}
|
||||
if (ext != LogFile.extension()) {
|
||||
// Skip anything that isn't a log file
|
||||
continue;
|
||||
}
|
||||
numLogs++;
|
||||
DeleteOldLogs(path, numLogs, logLimit);
|
||||
} else {
|
||||
// Skip anything that isn't a file
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (filepaths.empty()) {
|
||||
return;
|
||||
}
|
||||
u64 numToDelete{ logLimit };
|
||||
std::map<u64, fs::path> date_sorted_paths{};
|
||||
for (const auto &path : filepaths) {
|
||||
const std::string stem = path.stem().string();
|
||||
u64 basePos = stem.find('_');
|
||||
// If we cannot get the base, just delete it
|
||||
if (basePos == std::string::npos) {
|
||||
numToDelete--;
|
||||
fs::remove_all(path);
|
||||
} else {
|
||||
const std::string base = stem.substr(0, basePos);
|
||||
const u64 datePos = base.find('_', basePos+1);
|
||||
const std::string date = base.substr(datePos+1);
|
||||
const u64 dateInt = CreateIntegralTimestamp(date);
|
||||
if (datePos == std::string::npos) {
|
||||
// If we cannot find the date, just delete it
|
||||
numToDelete--;
|
||||
fs::remove_all(path);
|
||||
} else {
|
||||
const u64 timePos = base.find('_', datePos+1);
|
||||
const std::string time = base.substr(timePos+1);
|
||||
const u64 timestamp = CreateIntegralTimestamp(time);
|
||||
if (!timestamp) {
|
||||
numToDelete--;
|
||||
fs::remove_all(path);
|
||||
continue;
|
||||
}
|
||||
date_sorted_paths.insert({ dateInt + timestamp, path });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start deleting based off timestamp
|
||||
for (const auto &entry : date_sorted_paths) {
|
||||
fs::remove_all(entry.second);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize(const std::string_view &logFile) {
|
||||
// Create directory vars to so we can use fs::path::stem
|
||||
const fs::path LogDir = GetUserPath(PathType::LogDir);
|
||||
const fs::path LogFile = LOG_FILE;
|
||||
const fs::path LogFileStem = LogFile.stem();
|
||||
const fs::path LogFileName = LogFile.filename();
|
||||
// This is to make string_view happy
|
||||
const std::string LogFileStemStr = LogFileStem.string();
|
||||
const std::string LogFileNameStr = LogFileName.string();
|
||||
// Setup filename
|
||||
const std::string_view filestemBase = logFile.empty() ? LogFileStemStr : logFile;
|
||||
const std::string_view filenameBase = logFile.empty() ? LogFileNameStr : logFile;
|
||||
const std::chrono::time_point now = std::chrono::system_clock::now();
|
||||
const time_t timeNow = std::chrono::system_clock::to_time_t(now);
|
||||
const tm *time = std::localtime(&timeNow);
|
||||
const std::string currentTime = fmt::format("{}-{}-{}", time->tm_hour, time->tm_min, time->tm_sec);
|
||||
const std::string currentDate = fmt::format("{}-{}-{}", time->tm_mon + 1, time->tm_mday, 1900 + time->tm_year);
|
||||
const std::string filename = fmt::format("{}_{}_{}.txt", filestemBase, currentDate, currentTime);
|
||||
CleanupOldLogs(filenameBase, LogDir);
|
||||
Impl::Initialize(logFile.empty() ? filename : logFile);
|
||||
}
|
||||
|
||||
bool IsActive() {
|
||||
return Impl::IsActive();
|
||||
}
|
||||
|
||||
void Start() {
|
||||
Impl::Start();
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
Impl::Stop();
|
||||
}
|
||||
|
||||
void SetGlobalFilter(const Filter &filter) {
|
||||
Impl::Instance().SetGlobalFilter(filter);
|
||||
}
|
||||
|
||||
void SetColorConsoleBackendEnabled(bool enabled) {
|
||||
Impl::Instance().SetColorConsoleBackendEnabled(enabled);
|
||||
}
|
||||
|
||||
void FmtLogMessageImpl(Class logClass, Level logLevel, const char *filename,
|
||||
u32 lineNum, const char *function, const char *format,
|
||||
const fmt::format_args &args) {
|
||||
if (!currentlyInitialising) [[likely]] {
|
||||
Impl::Instance().PushEntry(logClass, logLevel, filename, lineNum, function,
|
||||
fmt::vformat(format, args));
|
||||
}
|
||||
}
|
||||
|
||||
void NoFmtMessage(Class logClass, Level logLevel, const std::string &message) {
|
||||
if (!currentlyInitialising) [[likely]] {
|
||||
Impl::Instance().PushEntryNoFmt(logClass, logLevel, message);
|
||||
}
|
||||
}
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
37
src/common/Logging/Backend.h
Normal file
37
src/common/Logging/Backend.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <filesystem>
|
||||
|
||||
#include "common/PathUtil.h"
|
||||
|
||||
#include "Filter.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
class Filter;
|
||||
|
||||
/// Cleans up logs from previous days, and any logs within the desired limit
|
||||
void CleanupOldLogs(const std::string_view &logFileBase, const fs::path &logDir, const u16 logLimit = 50);
|
||||
|
||||
/// Initializes the logging system
|
||||
void Initialize(const std::string_view &logFile = {});
|
||||
|
||||
bool IsActive();
|
||||
|
||||
/// Starts the logging threads
|
||||
void Start();
|
||||
|
||||
/// Explictily stops the logger thread and flushes the buffers
|
||||
void Stop();
|
||||
|
||||
/// The global filter will prevent any messages from even being processed if they are filtered
|
||||
void SetGlobalFilter(const Filter &filter);
|
||||
|
||||
void SetColorConsoleBackendEnabled(bool enabled);
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
156
src/common/Logging/Filter.cpp
Normal file
156
src/common/Logging/Filter.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#include "common/Assert.h"
|
||||
|
||||
#include "Filter.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
template <typename It>
|
||||
Level GetLevelByName(const It begin, const It end) {
|
||||
for (u8 i = 0; i < static_cast<u8>(Level::Count); ++i) {
|
||||
const char* level_name = GetLevelName(static_cast<Level>(i));
|
||||
if (std::string_view(begin, end).compare(level_name) == 0) {
|
||||
return static_cast<Level>(i);
|
||||
}
|
||||
}
|
||||
return Level::Count;
|
||||
}
|
||||
|
||||
template <typename It>
|
||||
Class GetClassByName(const It begin, const It end) {
|
||||
for (u8 i = 0; i < static_cast<u8>(Class::Count); ++i) {
|
||||
const char* level_name = GetLogClassName(static_cast<Class>(i));
|
||||
if (std::string_view(begin, end).compare(level_name) == 0) {
|
||||
return static_cast<Class>(i);
|
||||
}
|
||||
}
|
||||
return Class::Count;
|
||||
}
|
||||
|
||||
template <typename Iterator>
|
||||
bool ParseFilterRule(Filter &instance, Iterator begin, Iterator end) {
|
||||
const auto levelSeparator = std::find(begin, end, ':');
|
||||
if (levelSeparator == end) {
|
||||
LOG_ERROR(Log, "Invalid log filter. Must specify a log level after `:`: {}", std::string_view(begin, end));
|
||||
return false;
|
||||
}
|
||||
|
||||
const Level level = GetLevelByName(levelSeparator + 1, end);
|
||||
if (level == Level::Count) {
|
||||
LOG_ERROR(Log, "Unknown log level in filter: {}", std::string_view(begin, end));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::string_view(begin, levelSeparator).compare("*") == 0) {
|
||||
instance.ResetAll(level);
|
||||
return true;
|
||||
}
|
||||
|
||||
const Class logClass = GetClassByName(begin, levelSeparator);
|
||||
if (logClass == Class::Count) {
|
||||
LOG_ERROR(Log, "Unknown log class in filter: {}", std::string(begin, end));
|
||||
return false;
|
||||
}
|
||||
|
||||
instance.SetClassLevel(logClass, level);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Macro listing all log classes. Code should define CLS and SUB as desired before invoking this.
|
||||
#define ALL_LOG_CLASSES() \
|
||||
CLS(Log) \
|
||||
CLS(Base) \
|
||||
SUB(Base, Filesystem) \
|
||||
CLS(Config) \
|
||||
CLS(Debug) \
|
||||
CLS(System) \
|
||||
CLS(Render) \
|
||||
CLS(ARM) \
|
||||
CLS(Memory) \
|
||||
CLS(PROBE) \
|
||||
CLS(MMIO_S1)
|
||||
|
||||
// GetClassName is a macro defined by Windows.h, grrr...
|
||||
const char* GetLogClassName(Class logClass) {
|
||||
switch (logClass) {
|
||||
#define CLS(x) \
|
||||
case Class::x: \
|
||||
return #x;
|
||||
#define SUB(x, y) \
|
||||
case Class::x##_##y: \
|
||||
return #x "." #y;
|
||||
ALL_LOG_CLASSES()
|
||||
#undef CLS
|
||||
#undef SUB
|
||||
case Class::Count:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
const char* GetLevelName(Level logLevel) {
|
||||
#define LVL(x) \
|
||||
case Level::x: \
|
||||
return #x
|
||||
switch (logLevel) {
|
||||
LVL(Trace);
|
||||
LVL(Debug);
|
||||
LVL(Info);
|
||||
LVL(Warning);
|
||||
LVL(Error);
|
||||
LVL(Critical);
|
||||
LVL(Guest);
|
||||
case Level::Count:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#undef LVL
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
Filter::Filter(Level defaultLevel) {
|
||||
ResetAll(defaultLevel);
|
||||
}
|
||||
|
||||
void Filter::ResetAll(Level level) {
|
||||
classLevels.fill(level);
|
||||
}
|
||||
|
||||
void Filter::SetClassLevel(Class logClass, Level level) {
|
||||
classLevels[static_cast<size_t>(logClass)] = level;
|
||||
}
|
||||
|
||||
void Filter::ParseFilterString(const std::string_view &filterView) {
|
||||
auto clause_begin = filterView.cbegin();
|
||||
while (clause_begin != filterView.cend()) {
|
||||
auto clause_end = std::find(clause_begin, filterView.cend(), ' ');
|
||||
|
||||
// If clause isn't empty
|
||||
if (clause_end != clause_begin) {
|
||||
ParseFilterRule(*this, clause_begin, clause_end);
|
||||
}
|
||||
|
||||
if (clause_end != filterView.cend()) {
|
||||
// Skip over the whitespace
|
||||
++clause_end;
|
||||
}
|
||||
clause_begin = clause_end;
|
||||
}
|
||||
}
|
||||
|
||||
bool Filter::CheckMessage(Class logClass, Level level) const {
|
||||
return static_cast<u8>(level) >=
|
||||
static_cast<u8>(classLevels[static_cast<size_t>(logClass)]);
|
||||
}
|
||||
|
||||
bool Filter::IsDebug() const {
|
||||
return std::any_of(classLevels.begin(), classLevels.end(), [](const Level& l) {
|
||||
return static_cast<u8>(l) <= static_cast<u8>(Level::Debug);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
66
src/common/Logging/Filter.h
Normal file
66
src/common/Logging/Filter.h
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "LogTypes.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
/*
|
||||
* Returns the name of the passed log class as a C-string. Subclasses are separated by periods
|
||||
* instead of underscores as in the enumeration.
|
||||
*/
|
||||
const char* GetLogClassName(Class log_class);
|
||||
|
||||
/*
|
||||
* Returns the name of the passed log level as a C-string.
|
||||
*/
|
||||
const char* GetLevelName(Level log_level);
|
||||
|
||||
/*
|
||||
* Implements a log message filter which allows different log classes to have different minimum
|
||||
* severity levels. The filter can be changed at runtime and can be parsed from a string to allow
|
||||
* editing via the interface or loading from a configuration file.
|
||||
*/
|
||||
class Filter {
|
||||
public:
|
||||
/// Initializes the filter with all classes having `default_level` as the minimum level.
|
||||
explicit Filter(Level defaultLevel = Level::Info);
|
||||
|
||||
/// Resets the filter so that all classes have `level` as the minimum displayed level.
|
||||
void ResetAll(Level level);
|
||||
|
||||
/// Sets the minimum level of `log_class` (and not of its subclasses) to `level`.
|
||||
void SetClassLevel(Class logClass, Level level);
|
||||
|
||||
/*
|
||||
* Parses a filter string and applies it to this filter.
|
||||
*
|
||||
* A filter string consists of a space-separated list of filter rules, each of the format
|
||||
* `<class>:<level>`. `<class>` is a log class name, with subclasses separated using periods.
|
||||
* `*` is allowed as a class name and will reset all filters to the specified level. `<level>`
|
||||
* a severity level name which will be set as the minimum logging level of the matched classes.
|
||||
* Rules are applied left to right, with each rule overriding previous ones in the sequence.
|
||||
*
|
||||
* A few examples of filter rules:
|
||||
* - `*:Info` -- Resets the level of all classes to Info.
|
||||
* - `Service:Info` -- Sets the level of Service to Info.
|
||||
* - `Service.FS:Trace` -- Sets the level of the Service.FS class to Trace.
|
||||
*/
|
||||
void ParseFilterString(const std::string_view &filterView);
|
||||
|
||||
/// Matches class/level combination against the filter, returning true if it passed.
|
||||
bool CheckMessage(Class logClass, Level level) const;
|
||||
|
||||
/// Returns true if any logging classes are set to debug
|
||||
bool IsDebug() const;
|
||||
|
||||
private:
|
||||
std::array<Level, static_cast<const size_t>(Class::Count)> classLevels;
|
||||
};
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
78
src/common/Logging/Log.h
Normal file
78
src/common/Logging/Log.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "common/Config.h"
|
||||
|
||||
#include "LogTypes.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
constexpr const char* TrimSourcePath(const std::string_view &source) {
|
||||
const auto rfind = [source](const std::string_view match) {
|
||||
return source.rfind(match) == source.npos ? 0 : (source.rfind(match) + match.size());
|
||||
};
|
||||
const auto idx = std::max({ rfind("/"), rfind("\\") });
|
||||
return source.data() + idx;
|
||||
}
|
||||
|
||||
/// Logs a message to the global logger, using fmt
|
||||
void FmtLogMessageImpl(Class logClass, Level logLevel, const char *filename,
|
||||
u32 lineNum, const char *function, const char *format,
|
||||
const fmt::format_args& args);
|
||||
|
||||
/// Logs a message without any formatting
|
||||
void NoFmtMessage(Class logClass, Level logLevel, const std::string &message);
|
||||
|
||||
template <typename... Args>
|
||||
void FmtLogMessage(Class logClass, Level logLevel, const char *filename, u32 lineNum,
|
||||
const char *function, const char *format, const Args&... args) {
|
||||
FmtLogMessageImpl(logClass, logLevel, filename, lineNum, function, format,
|
||||
fmt::make_format_args(args...));
|
||||
}
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
|
||||
// Define the fmt lib macros
|
||||
#define LOG_GENERIC(logClass, logLevel, ...) \
|
||||
Base::Log::FmtLogMessage(logClass, logLevel, Base::Log::TrimSourcePath(__FILE__), \
|
||||
__LINE__, __func__, __VA_ARGS__)
|
||||
#ifdef DEBUG_BUILD
|
||||
#define LOG_TRACE(logClass, ...) \
|
||||
if (Config::log.debugOnly) \
|
||||
Base::Log::FmtLogMessage(Base::Log::Class::logClass, Base::Log::Level::Trace, \
|
||||
Base::Log::TrimSourcePath(__FILE__), __LINE__, __func__, \
|
||||
__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_TRACE(logClass, ...) ;
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_BUILD
|
||||
#define LOG_DEBUG(logClass, ...) \
|
||||
Base::Log::FmtLogMessage(Base::Log::Class::logClass, Base::Log::Level::Debug, \
|
||||
Base::Log::TrimSourcePath(__FILE__), __LINE__, __func__, \
|
||||
__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_DEBUG(logClass, ...) ;
|
||||
#endif
|
||||
#define LOG_INFO(logClass, ...) \
|
||||
Base::Log::FmtLogMessage(Base::Log::Class::logClass, Base::Log::Level::Info, \
|
||||
Base::Log::TrimSourcePath(__FILE__), __LINE__, __func__, \
|
||||
__VA_ARGS__)
|
||||
#define LOG_WARNING(logClass, ...) \
|
||||
Base::Log::FmtLogMessage(Base::Log::Class::logClass, Base::Log::Level::Warning, \
|
||||
Base::Log::TrimSourcePath(__FILE__), __LINE__, __func__, \
|
||||
__VA_ARGS__)
|
||||
#define LOG_ERROR(logClass, ...) \
|
||||
Base::Log::FmtLogMessage(Base::Log::Class::logClass, Base::Log::Level::Error, \
|
||||
Base::Log::TrimSourcePath(__FILE__), __LINE__, __func__, \
|
||||
__VA_ARGS__)
|
||||
#define LOG_CRITICAL(logClass, ...) \
|
||||
Base::Log::FmtLogMessage(Base::Log::Class::logClass, Base::Log::Level::Critical, \
|
||||
Base::Log::TrimSourcePath(__FILE__), __LINE__, __func__, \
|
||||
__VA_ARGS__)
|
||||
28
src/common/Logging/LogEntry.h
Normal file
28
src/common/Logging/LogEntry.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "LogTypes.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
/*
|
||||
* A log entry. Log entries are store in a structured format to permit more varied output
|
||||
* formatting on different frontends, as well as facilitating filtering and aggregation.
|
||||
*/
|
||||
struct Entry {
|
||||
std::chrono::microseconds timestamp = {};
|
||||
Class logClass = {};
|
||||
Level logLevel = {};
|
||||
const char *filename = nullptr;
|
||||
u32 lineNum = 0;
|
||||
std::string function = {};
|
||||
std::string message = {};
|
||||
bool formatted = true;
|
||||
};
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
44
src/common/Logging/LogTypes.h
Normal file
44
src/common/Logging/LogTypes.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/Types.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
/// Specifies the severity or level of detail of the log message
|
||||
enum class Level : const u8 {
|
||||
Trace, ///< Extremely detailed and repetitive debugging information that is likely to pollute logs
|
||||
Debug, ///< Less detailed debugging information
|
||||
Info, ///< Status information from important points during execution
|
||||
Warning, ///< Minor or potential problems found during execution of a task
|
||||
Error, ///< Major problems found during execution of a task that prevent it from being completed
|
||||
Critical, ///< Major problems during execution that threaten the stability of the entire application
|
||||
Guest, ///< Output from the guest system
|
||||
Count ///< Total number of logging levels
|
||||
};
|
||||
|
||||
/*
|
||||
* Specifies the sub-system that generated the log message
|
||||
*
|
||||
* @note If you add a new entry here, also add a corresponding one to `ALL_LOG_CLASSES` in
|
||||
* filtercpp
|
||||
*/
|
||||
enum class Class : const u8 {
|
||||
Log, // Messages about the log system itself
|
||||
Base, // System base routines: FS, logging, etc
|
||||
Base_Filesystem, // Filesystem messages
|
||||
Config, // Emulator configuration (including commandline)
|
||||
Debug, // Debugging tools
|
||||
System, // Base System messages
|
||||
Render, // OpenGL and Window messages
|
||||
ARM,
|
||||
PROBE,
|
||||
MMIO_S1,
|
||||
Memory,
|
||||
Count // Total number of logging classes
|
||||
};
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
68
src/common/Logging/TextFormatter.cpp
Normal file
68
src/common/Logging/TextFormatter.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#include "common/Assert.h"
|
||||
#include "common/Config.h"
|
||||
|
||||
#include "TextFormatter.h"
|
||||
|
||||
#include "Filter.h"
|
||||
#include "LogEntry.h"
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
std::string FormatLogMessage(const Entry &entry) {
|
||||
const char *className = GetLogClassName(entry.logClass);
|
||||
const char *levelName = GetLevelName(entry.logLevel);
|
||||
|
||||
if (Config::isLogAdvanced() && entry.filename) {
|
||||
return fmt::format("[{}] <{}> {}:{}:{}: {}", className, levelName, entry.filename,
|
||||
entry.function, entry.lineNum, entry.message);
|
||||
} else {
|
||||
return fmt::format("[{}] <{}> {}", className, levelName, entry.message);
|
||||
}
|
||||
}
|
||||
|
||||
#define ESC "\x1b"
|
||||
void PrintMessage(const std::string &color, const Entry &entry) {
|
||||
std::string msg = entry.formatted ? FormatLogMessage(entry) : entry.message;
|
||||
const std::string str = color + msg.append(ESC "[0m") + (entry.formatted ? "\n" : "");
|
||||
fputs(str.c_str(), stdout);
|
||||
}
|
||||
|
||||
void PrintColoredMessage(const Entry &entry) {
|
||||
// NOTE: Custom colors can be achieved
|
||||
// std::format("\x1b[{};2;{};{};{}m", color.bg ? 48 : 38, color.r, color.g, color.b)
|
||||
const char *color = "";
|
||||
switch (entry.logLevel) {
|
||||
case Level::Trace: // Grey
|
||||
color = ESC "[1;30m";
|
||||
break;
|
||||
case Level::Debug: // Cyan
|
||||
color = ESC "[0;36m";
|
||||
break;
|
||||
case Level::Info: // Bright gray
|
||||
color = ESC "[0;37m";
|
||||
break;
|
||||
case Level::Warning: // Bright yellow
|
||||
color = ESC "[1;33m";
|
||||
break;
|
||||
case Level::Error: // Bright red
|
||||
color = ESC "[1;31m";
|
||||
break;
|
||||
case Level::Critical: // Bright magenta
|
||||
color = ESC "[1;35m";
|
||||
break;
|
||||
case Level::Guest: // Green
|
||||
color = ESC "[0;92m";
|
||||
break;
|
||||
case Level::Count:
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
PrintMessage(color, entry);
|
||||
}
|
||||
#undef ESC
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
23
src/common/Logging/TextFormatter.h
Normal file
23
src/common/Logging/TextFormatter.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2025 Xenon Emulator Project. All rights reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace Base {
|
||||
namespace Log {
|
||||
|
||||
struct Entry;
|
||||
|
||||
/// Formats a log entry into the provided text buffer.
|
||||
std::string FormatLogMessage(const Entry &entry);
|
||||
|
||||
/// Formats and prints a log entry to stderr.
|
||||
void PrintMessage(const std::string &color, const Entry &entry);
|
||||
|
||||
/// Prints the same message as `PrintMessage`, but colored according to the severity level.
|
||||
void PrintColoredMessage(const Entry &entry);
|
||||
|
||||
} // namespace Log
|
||||
} // namespace Base
|
||||
Loading…
Add table
Add a link
Reference in a new issue