OS functions start (#16)

* OS memory start

OSInit()
MEM1 size configurable via AuroraConfiguration
Fancy guard pages (who knows if it'll be useful)

* OSGetTime() and OSGetTick()

* Fix OSPhysicalToCached etc for 64-bit pointers

* Specify memory size in OS boot info

Used by TP at least

* Fix MEM1Start/MEM1End definition

oops

* Implement some OS arena functions

* DEBUG -> NDEBUG

* Implement memory translation functions

I decided to remove the macros from the header given they're very unlikely to matter perf wise and it reduces code duplication.

There's no distinction between cached and uncached. Physical is relative to MEM1.

* Fix symbol mismatches from forgetting to include the header

* Implement OSReport and similar logging functions

* Fix OSBaseAddress

Ugh

* Implement OSGetPhysicalMemSize

* Fix usage of static for global variables

* Remove ../ prefixes from OS CMake file

CLion did this

* Remaining headers needed for TP to build

* Rider's "Unversioned Files" will keep trolling me

* Remove TP-specific OSReport functions

* Hardcode OS_BUS_CLOCK in os.h

TP has it referenced in an initializer, so relying on MEM1 being initialized is impossible.

* Import TP ARAM emulation code

* Guard against double OSInit() calls.

---------

Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
Pieter-Jan Briers
2026-03-02 16:33:42 +01:00
committed by GitHub
parent 12009bbd7b
commit e4afaeeeb7
15 changed files with 522 additions and 34 deletions
+1
View File
@@ -11,6 +11,7 @@ option(AURORA_ENABLE_GX "Enable GX implementation and WebGPU renderer" ON)
add_subdirectory(extern)
include(cmake/aurora_core.cmake)
include(cmake/aurora_os.cmake)
if (AURORA_ENABLE_GX)
include(cmake/aurora_gx.cmake)
endif ()
+13
View File
@@ -0,0 +1,13 @@
add_library(aurora_os STATIC lib/dolphin/os/OSInit.cpp
lib/dolphin/os/OSMemory.cpp
lib/dolphin/os/internal.hpp
lib/dolphin/os/OSBootInfo.cpp
lib/dolphin/os/OSTime.cpp
lib/dolphin/os/OSArena.cpp
lib/dolphin/os/OSAddress.cpp
lib/dolphin/os/OSReport.cpp
lib/dolphin/AR.cpp)
add_library(aurora::os ALIAS aurora_os)
target_include_directories(aurora_os PUBLIC include)
target_link_libraries(aurora_os PRIVATE aurora::core)
+1
View File
@@ -2,6 +2,7 @@
#include <aurora/event.h>
#include <aurora/main.h>
#include <dolphin/gx.h>
#include <dolphin/os.h>
#include <stdio.h>
#include <string.h>
+16
View File
@@ -51,6 +51,9 @@ typedef struct AuroraEvent AuroraEvent;
typedef void (*AuroraLogCallback)(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
typedef void (*AuroraImGuiInitCallback)(const AuroraWindowSize* size);
#define MEM1_DEFAULT_SIZE = 24 * 1024 * 1024;
#define ARAM_DEFAULT_SIZE = 16 * 1024 * 1024;
typedef struct {
const char* appName;
const char* configPath;
@@ -68,6 +71,19 @@ typedef struct {
uint32_t iconHeight;
AuroraLogCallback logCallback;
AuroraImGuiInitCallback imGuiInitCallback;
/*
* The size of the GameCube's main memory, or MEM1 on the Wii.
* Note that it will not be allocated at the exact 0x80000000 address, as that cannot be guaranteed.
* This can be set to 0 to disable allocating this region.
*/
uint32_t mem1Size;
/*
* The size of the GameCube's ARAM, or MEM2 on the Wii.
* This can be set to 0 to disable allocating this region.
*/
uint32_t mem2Size;
} AuroraConfig;
typedef struct {
+15 -34
View File
@@ -70,8 +70,10 @@ typedef struct {
#define OS_UNCACHED_REGION_PREFIX 0xC000
#define OS_PHYSICAL_MASK 0x3FFF
#define OS_BASE_CACHED (OS_CACHED_REGION_PREFIX << 16)
#define OS_BASE_UNCACHED (OS_UNCACHED_REGION_PREFIX << 16)
extern uintptr_t OSBaseAddress;
#define OS_BASE_CACHED (OSBaseAddress)
#define OS_BASE_UNCACHED (OSBaseAddress)
#ifdef __MWERKS__
u32 __OSPhysicalMemSize AT_ADDRESS(OS_BASE_CACHED | 0x0028);
@@ -89,13 +91,14 @@ OSThread* __gUnkThread1 AT_ADDRESS(OS_BASE_CACHED | 0x00D8);
int __gUnknown800030C0[2] AT_ADDRESS(OS_BASE_CACHED | 0x30C0);
u8 __gUnknown800030E3 AT_ADDRESS(OS_BASE_CACHED | 0x30E3);
#else
#define __OSBusClock 486000000
#define __OSCoreClock (486000000 / 4)
#endif // __MWERKS__
#define __OSBusClock (*(u32 *)(OS_BASE_CACHED + 0x00F8))
#define __OSCoreClock (*(u32 *)(OS_BASE_CACHED + 0x00FC))
#endif
#define OS_BUS_CLOCK __OSBusClock
#define OS_CORE_CLOCK __OSCoreClock
#define OS_TIMER_CLOCK (OS_BUS_CLOCK/4)
#define OS_TIMER_CLOCK_DIVIDER 4
#define OS_BUS_CLOCK 150'000'000
#define OS_CORE_CLOCK 150'000'000
#define OS_TIMER_CLOCK (OS_BUS_CLOCK/OS_TIMER_CLOCK_DIVIDER)
#define OSTicksToSeconds(ticks) ((ticks) / (OS_TIMER_CLOCK))
#define OSTicksToMilliseconds(ticks) ((ticks) / (OS_TIMER_CLOCK/1000))
@@ -122,7 +125,9 @@ void __OSPSInit(void);
void __OSFPRInit(void);
u32 __OSGetDIConfig(void);
#if 0
void OSDefaultExceptionHandler(__OSException exception, OSContext* context);
#endif
typedef struct OSCalendarTime {
/* 0x00 */ int sec;
@@ -212,29 +217,6 @@ void OSFatal NORETURN(GXColor fg, GXColor bg, const char* msg);
#define OSRoundUp32B(x) (((uintptr_t)(x) + 32 - 1) & ~(32 - 1))
#define OSRoundDown32B(x) (((uintptr_t)(x)) & ~(32 - 1))
#ifdef TARGET_PC
static inline void* OSPhysicalToCached(u32 paddr) {
return reinterpret_cast<void*>(static_cast<uintptr_t>(paddr));
}
static inline void* OSPhysicalToUncached(u32 paddr) {
return reinterpret_cast<void*>(static_cast<uintptr_t>(paddr));
}
static inline u32 OSCachedToPhysical(void* caddr) {
return static_cast<u32>(reinterpret_cast<uintptr_t>(caddr));
}
static inline u32 OSUncachedToPhysical(void* ucaddr) {
return static_cast<u32>(reinterpret_cast<uintptr_t>(ucaddr));
}
static inline void* OSCachedToUncached(void* caddr) {
return caddr;
}
static inline void* OSUncachedToCached(void* ucaddr) {
return ucaddr;
}
#else // non-TARGET_PC
void* OSPhysicalToCached(u32 paddr);
void* OSPhysicalToUncached(u32 paddr);
u32 OSCachedToPhysical(void* caddr);
@@ -242,8 +224,6 @@ u32 OSUncachedToPhysical(void* ucaddr);
void* OSCachedToUncached(void* caddr);
void* OSUncachedToCached(void* ucaddr);
#endif // TARGET_PC
#if !DEBUG && !defined(TARGET_PC)
#define OSPhysicalToCached(paddr) ((void*) ((u32)(OS_BASE_CACHED + (u32)(paddr))))
#define OSPhysicalToUncached(paddr) ((void*) ((u32)(OS_BASE_UNCACHED + (u32)(paddr))))
@@ -285,6 +265,7 @@ extern int __OSInIPL;
// This is dumb but we dont have a Metrowerks way to do variadic macros in the macro to make this done in a not scrubby way.
#define ASSERTMSG1LINE(line, cond, msg, arg1) \
((cond) || (OSPanic(__FILE__, line, msg, arg1), 0))
#define ASSERTMSG2LINE(line, cond, msg, arg1, arg2) \
((cond) || (OSPanic(__FILE__, line, msg, arg1, arg2), 0))
@@ -317,6 +298,7 @@ extern int __OSInIPL;
#define ASSERTMSG2LINE(line, cond, msg, arg1, arg2) (void)0
#define ASSERTMSGLINEV(line, cond, ...) (void)0
#endif
#define ASSERT(cond) ASSERTLINE(__LINE__, cond)
inline s16 __OSf32tos16(__REGISTER f32 inF) {
@@ -387,5 +369,4 @@ static inline void OSInitFastCast(void) {
#ifdef __cplusplus
}
#endif
#endif
+111
View File
@@ -0,0 +1,111 @@
#include <dolphin/ar.h>
#include "../internal.hpp"
#include "dolphin/os.h"
static aurora::Module Log("aurora::ar");
static u32 AR_StackPointer;
static u32* AR_BlockLength;
static u32 AR_FreeBlocks;
static BOOL AR_init_flag;
#define ARAM_STACK_START 0x4000;
// ARAM emulation: allocate a large buffer to simulate the GameCube's Auxiliary RAM.
// ARAM "addresses" are offsets into this buffer. On GameCube, ARAM is 16 MB starting
// at a base address returned by ARInit. We emulate this by malloc'ing a buffer
// and using a simple bump allocator (matching ARAlloc behavior on real hardware).
static u8* sAramBuffer = nullptr;
// Convert an ARAM "address" (offset) to a real host pointer
static u8* aramToHost(u32 aramAddr) {
if (!sAramBuffer || aramAddr >= aurora::g_config.mem2Size) {
return nullptr;
}
return sAramBuffer + aramAddr;
}
u32 ARAlloc(u32 length) {
u32 tmp;
ASSERTMSGLINE(430, !(length & 0x1F), "ARAlloc(): length is not multiple of 32bytes!");
ASSERTMSGLINE(434, length <= (__AR_Size - __AR_StackPointer), "ARAlloc(): Out of ARAM!");
ASSERTMSGLINE(435, __AR_FreeBlocks, "ARAlloc(): No more free blocks!");
tmp = AR_StackPointer;
AR_StackPointer += length;
*AR_BlockLength = length;
AR_BlockLength += 1;
AR_FreeBlocks -= 1;
return tmp;
}
u32 ARFree(u32* length) {
AR_BlockLength -= 1;
if (length) {
*length = *AR_BlockLength;
}
AR_StackPointer -= *AR_BlockLength;
AR_FreeBlocks += 1;
return AR_StackPointer;
}
BOOL ARCheckInit(void) { return AR_init_flag; }
u32 ARInit(u32* stack_index_addr, u32 num_entries) {
if (aurora::g_config.mem2Size == 0) {
Log.warn("ARInit called but no mem2Size specified in AuroraConfig. ARAM will not be available!");
return 0;
}
if (AR_init_flag == TRUE) {
return ARAM_STACK_START;
}
sAramBuffer = (u8*)calloc(1, aurora::g_config.mem2Size);
if (sAramBuffer) {
Log.debug("Initialized 0x{:X} bytes of ARAM!", aurora::g_config.mem2Size);
} else {
Log.fatal("Failed to allocate ARAM!");
}
AR_StackPointer = ARAM_STACK_START;
AR_FreeBlocks = num_entries;
AR_BlockLength = stack_index_addr;
AR_init_flag = TRUE;
return AR_StackPointer;
}
u32 ARGetSize(void) { return aurora::g_config.mem2Size; }
#pragma mark ARQ
void ARQPostRequest(ARQRequest* request, u32 owner, u32 type, u32 priority, uintptr_t source, uintptr_t dest,
u32 length, ARQCallback callback) {
// Emulate ARAM DMA transfers using memcpy.
// type 0 = MRAM -> ARAM, type 1 = ARAM -> MRAM
if (type == ARAM_DIR_MRAM_TO_ARAM) {
// Main RAM -> ARAM: source is a host pointer (cast to u32), dest is an ARAM offset
u8* hostSrc = (u8*)(uintptr_t)source;
u8* aramDst = aramToHost(dest);
if (aramDst && hostSrc) {
memcpy(aramDst, hostSrc, length);
}
} else {
// ARAM -> Main RAM: source is an ARAM offset, dest is a host pointer (cast to u32)
u8* aramSrc = aramToHost(source);
u8* hostDst = (u8*)(uintptr_t)dest;
if (aramSrc && hostDst) {
memcpy(hostDst, aramSrc, length);
}
}
// Immediately invoke the callback (synchronous on PC, no DMA latency)
if (callback) {
callback((uintptr_t)request);
}
}
void ARQInit() {
// Nothing to do on PC - ARAM is initialized in ARInit
}
+41
View File
@@ -0,0 +1,41 @@
#include "internal.hpp"
#include <dolphin/os.h>
#include <cassert>
#if NDEBUG
#undef OSPhysicalToCached
#undef OSPhysicalToUncached
#undef OSCachedToPhysical
#undef OSUncachedToPhysical
#undef OSCachedToUncached
#undef OSUncachedToCached
#endif
void* OSPhysicalToCached(u32 paddr) {
assert (paddr <= aurora::g_config.mem1Size);
return POINTER_ADD(MEM1Start, paddr);
}
void* OSPhysicalToUncached(u32 paddr) {
return OSPhysicalToCached(paddr);
}
u32 OSCachedToPhysical(void* caddr) {
assert (caddr >= MEM1Start && caddr <= MEM1End);
return (reinterpret_cast<uintptr_t>(caddr) - reinterpret_cast<uintptr_t>(MEM1Start));
}
u32 OSUncachedToPhysical(void* ucaddr) {
return OSCachedToPhysical(ucaddr);
}
void* OSCachedToUncached(void* caddr) {
return caddr;
}
void* OSUncachedToCached(void* ucaddr) {
return ucaddr;
}
+58
View File
@@ -0,0 +1,58 @@
#include <cassert>
#include "internal.hpp"
#include <dolphin/os.h>
#define ROUND64(n, a) (((u64)(n) + (a)-1) & ~(u64)((a)-1))
#define TRUNC64(n, a) (((u64)(n)) & ~(u64)((a)-1))
static void* ArenaLow;
static void* ArenaHigh;
void* OSGetArenaHi() {
return ArenaHigh;
}
void* OSGetArenaLo() {
return ArenaLow;
}
void OSSetArenaHi(void* newHi) {
assert(newHi <= MEM1End && newHi >= MEM1Start);
ArenaHigh = newHi;
}
void OSSetArenaLo(void* newLo) {
assert(newLo <= MEM1End && newLo >= MEM1Start);
ArenaLow = newLo;
}
void* OSAllocFromArenaLo(const u32 size, const u32 align) {
void* ptr = OSGetArenaLo();
auto arenaLo = static_cast<u8*>(ptr = reinterpret_cast<void*>(ROUND64(ptr, align)));
arenaLo += size;
arenaLo = reinterpret_cast<u8*>(ROUND64(arenaLo, align));
OSSetArenaLo(arenaLo);
return ptr;
}
void* OSAllocFromArenaHi(const u32 size, const u32 align) {
void* ptr;
auto arenaHi = static_cast<u8*>(OSGetArenaHi());
arenaHi = reinterpret_cast<u8*>(TRUNC64(arenaHi, align));
arenaHi -= size;
arenaHi = static_cast<u8*>(ptr = reinterpret_cast<void*>(TRUNC64(arenaHi, align)));
OSSetArenaHi(arenaHi);
return ptr;
}
void AuroraInitArena() {
if (MEM1Start == nullptr) {
return;
}
OSSetArenaLo(static_cast<u8*>(MEM1Start) + ARENA_START_OFFSET);
OSSetArenaHi(MEM1End);
}
+12
View File
@@ -0,0 +1,12 @@
#include "internal.hpp"
#include <dolphin/os.h>
void AuroraFillBootInfo() {
if (OSBaseAddress == 0) {
return;
}
const auto info = static_cast<OSBootInfo*>(OSPhysicalToCached(0));
info->memorySize = aurora::g_config.mem1Size;
}
+18
View File
@@ -0,0 +1,18 @@
#include "internal.hpp"
#include <dolphin/os.h>
static bool AlreadyInitialized;
void OSInit() {
if (AlreadyInitialized) {
return;
}
AlreadyInitialized = true;
AuroraOSInitMemory();
AuroraFillBootInfo();
AuroraInitClock();
AuroraInitArena();
}
+149
View File
@@ -0,0 +1,149 @@
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#endif
#include "fmt/base.h"
#include <cassert>
#include "internal.hpp"
#include <dolphin/os.h>
#include "dolphin/types.h"
#if !NDEBUG && (INTPTR_MAX > INT32_MAX)
#define GUARD_MEMORY 1
#endif
uintptr_t OSBaseAddress = 0;
void* MEM1Start;
void* MEM1End;
static void GuardGCMemory();
static void* AllocMEM1(u32 size);
void AuroraOSInitMemory() {
GuardGCMemory();
if (aurora::g_config.mem1Size > 0) {
MEM1Start = AllocMEM1(aurora::g_config.mem1Size);
MEM1End = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(MEM1Start) + aurora::g_config.mem1Size);
OSBaseAddress = reinterpret_cast<uintptr_t>(MEM1Start);
}
}
#if GUARD_MEMORY
static uintptr_t GetAllocationGranularity() {
#if _WIN32
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
return sysInfo.dwAllocationGranularity;
#else
// TODO: posix impl
return 0;
#endif
}
static void TryGuardRegion(const uintptr_t start, const uintptr_t end, char const* const name) {
assert(start != 0);
#if _WIN32
const auto addr = VirtualAlloc(
reinterpret_cast<LPVOID>(start),
end - start,
MEM_RESERVE,
PAGE_NOACCESS);
if (addr == nullptr) {
Log.debug("Unable to guard memory region: {}", name);
} else {
assert(addr == reinterpret_cast<LPVOID>(start));
Log.debug("Successfully guarded memory range: {:08X}-{:08X} ({})", start, end, name);
}
#else
// TODO: posix impl
#endif
}
static void GuardGCMemory() {
// Reserve the normal GC/Wii memory map so accesses are 100% guaranteed to fail.
// https://www.gc-forever.com/yagcd/chap5.html#sec5.11
// https://wiibrew.org/wiki/Memory_map
// We can't quite map at address 0 (for good reasons) but we *can* map at the next granularity over!
TryGuardRegion(0x00000000 + GetAllocationGranularity(), 0x017fffff, "MEM1 Physical");
TryGuardRegion(0x80000000, 0x817fffff, "MEM1 Logical (cached)");
TryGuardRegion(0xC0000000, 0xC17fffff, "MEM1 Logical (uncached)");
TryGuardRegion(0x10000000, 0x13FFFFFF, "MEM2 Physical");
TryGuardRegion(0x90000000, 0x93FFFFFF, "MEM2 Logical (cached)");
TryGuardRegion(0xD0000000, 0xD3FFFFFF, "MEM2 Logical (uncached)");
TryGuardRegion(0x08000000, 0x08300000, "EFB Physical");
TryGuardRegion(0xC8000000, 0xC8300000, "EFB Logical");
TryGuardRegion(0x0D000000, 0x0D008000, "Hollywood HW registers Physical");
TryGuardRegion(0xCD000000, 0xCD008000, "Hollywood HW registers Logical");
TryGuardRegion(0x0C000000, 0x0C008020, "Broadway/GC HW registers Physical");
TryGuardRegion(0xCC000000, 0xCC008020, "Broadway/GC HW registers Logical");
TryGuardRegion(0xe0000000, 0xe0003fff, "GC L2 cache");
TryGuardRegion(0xfff00000, 0xffffffff, "GC IPL");
}
#else
static void GuardGCMemory() { }
#endif
#if _WIN64 && !NDEBUG
static void* AllocMEM1(u32 size) {
// Allocate an entire 32-bit's worth of memory and allocate the real MEM1 in that.
// This way, if a 64-bit pointer gets truncated to 32-bit, it will still fall in our guard pages.
void* bulkChunk = VirtualAlloc(
nullptr,
8ll * 1024 * 1024 * 1024,
MEM_RESERVE,
PAGE_NOACCESS);
if (bulkChunk == nullptr) {
DWORD err = GetLastError();
fmt::memory_buffer msg;
fmt::format_system_error(
msg,
static_cast<int>(err),
"Failed to allocate bulk chunk for MEM1");
Log.fatal("{}", fmt::to_string(msg));
}
uintptr_t memSpace = (reinterpret_cast<uintptr_t>(bulkChunk) | 0xFFFFFFFF) + 1;
void* mem1Address = reinterpret_cast<void*>(memSpace + 0x80000000);
Log.debug("Reserved memory map at {:016X}-{:016X}", memSpace, memSpace + 0xFFFFFFFF);
Log.debug(
"MEM1 at {:016X}-{:016X}",
reinterpret_cast<uintptr_t>(mem1Address),
reinterpret_cast<uintptr_t>(mem1Address) + size);
void* result = VirtualAlloc(mem1Address, size, MEM_COMMIT, PAGE_READWRITE);
if (result == nullptr) {
DWORD err = GetLastError();
fmt::memory_buffer msg;
fmt::format_system_error(
msg,
static_cast<int>(err),
"Failed to commit memory for MEM1");
Log.fatal("{}", fmt::to_string(msg));
}
assert(result == mem1Address);
return result;
}
#else
static void* AllocMEM1(u32 size) {
return calloc(1, size);
}
#endif
u32 OSGetPhysicalMemSize() {
const auto info = static_cast<OSBootInfo*>(OSPhysicalToCached(0));
return info->memorySize;
}
+41
View File
@@ -0,0 +1,41 @@
#include <dolphin/os.h>
#include <dolphin/gx/GXStruct.h>
#include "fmt/base.h"
#include "fmt/printf.h"
#include "../../logging.hpp"
#include <cstdarg>
static aurora::Module reporter("aurora::os::report");
void OSReport(const char* msg, ...) {
va_list args;
va_start(args, msg);
OSVReport(msg, args);
va_end(args);
}
static std::string FormatToString(const char* msg, va_list list) {
int ret = vsnprintf(nullptr, 0, msg, list);
std::string buf(ret, '\0');
vsnprintf(buf.data(), buf.size(), msg, list);
return buf;
}
void OSVReport(const char* msg, va_list list) {
reporter.info("{}", FormatToString(msg, list));
}
void OSPanic(const char* file, int line, const char* msg, ...) {
va_list args;
va_start(args, msg);
reporter.fatal("PANIC {}:{}: {}", file, line, FormatToString(msg, args));
va_end(args);
}
void OSFatal(GXColor fg, GXColor bg, const char* msg) {
reporter.fatal("{}", msg);
}
+25
View File
@@ -0,0 +1,25 @@
#include <chrono>
#include "internal.hpp"
#include <dolphin/os.h>
namespace chrono = std::chrono;
using TickDuration = chrono::duration<s64, std::ratio<1, OS_TIMER_CLOCK>>;
OSTick OSGetTick() {
return OSGetTime() & 0xFFFFFFFF;
}
OSTime OSGetTime() {
auto clockTime = chrono::steady_clock::now().time_since_epoch();
auto ticksTotal = chrono::duration_cast<TickDuration>(clockTime);
return ticksTotal.count();
}
void AuroraInitClock() {
if (OSBaseAddress == 0) {
return;
}
__OSBusClock = OS_TIMER_CLOCK * OS_TIMER_CLOCK_DIVIDER;
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include "../../internal.hpp"
#include <dolphin/types.h>
static aurora::Module Log("aurora::os");
constexpr uintptr_t ARENA_START_OFFSET = 0x4000;
extern void* MEM1Start;
extern void* MEM1End;
void AuroraOSInitMemory();
void AuroraFillBootInfo();
void AuroraInitClock();
void AuroraInitArena();
+5
View File
@@ -67,6 +67,9 @@ auto underlying(T value) -> std::underlying_type_t<T> {
#define ALIGN(x, a) (((x) + ((a) - 1)) & ~((a) - 1))
#endif
#define POINTER_ADD_TYPE(type_, ptr_, offset_) ((type_)((uintptr_t)(ptr_) + (uintptr_t)(offset_)))
#define POINTER_ADD(ptr_, offset_) POINTER_ADD_TYPE(decltype(ptr_), ptr_, offset_)
#if !defined(__has_cpp_attribute)
#define __has_cpp_attribute(name) 0
#endif
@@ -95,6 +98,8 @@ auto underlying(T value) -> std::underlying_type_t<T> {
if (!(cond)) \
UNLIKELY { Log.warn(msg, ##__VA_ARGS__); }
#define UNIMPLEMENTED() FATAL("UNIMPLEMENTED: {}", __FUNCTION__)
namespace aurora {
extern AuroraConfig g_config;