/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2020 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #ifndef XENIA_BASE_MEMORY_H_ #define XENIA_BASE_MEMORY_H_ #include #include #include #include #include #include #include "xenia/base/assert.h" #include "xenia/base/byte_order.h" #include "xenia/base/platform.h" namespace xe { namespace memory { #if XE_PLATFORM_ANDROID void AndroidInitialize(); void AndroidShutdown(); #endif // Returns the native page size of the system, in bytes. // This should be ~4KiB. size_t page_size(); // Returns the allocation granularity of the system, in bytes. // This is likely 64KiB. size_t allocation_granularity(); enum class PageAccess { kNoAccess = 0, kReadOnly = 1 << 0, kReadWrite = kReadOnly | 1 << 1, kExecuteReadOnly = kReadOnly | 1 << 2, kExecuteReadWrite = kReadWrite | 1 << 2, }; enum class AllocationType { kReserve = 1 << 0, kCommit = 1 << 1, kReserveCommit = kReserve | kCommit, }; enum class DeallocationType { kRelease = 1 << 0, kDecommit = 1 << 1, }; // Whether the host allows the pages to be allocated or mapped with // PageAccess::kExecuteReadWrite - if not, separate mappings backed by the same // memory-mapped file must be used to write to executable pages. bool IsWritableExecutableMemorySupported(); // Whether PageAccess::kExecuteReadWrite is a supported and preferred way of // writing executable memory, useful for simulating how Xenia would work without // writable executable memory on a system with it. bool IsWritableExecutableMemoryPreferred(); // Allocates a block of memory at the given page-aligned base address. // Fails if the memory is not available. // Specify nullptr for base_address to leave it up to the system. void* AllocFixed(void* base_address, size_t length, AllocationType allocation_type, PageAccess access); // Deallocates and/or releases the given block of memory. // When releasing memory length must be zero, as all pages in the region are // released. bool DeallocFixed(void* base_address, size_t length, DeallocationType deallocation_type); // Sets the access rights for the given block of memory and returns the previous // access rights. Both base_address and length will be adjusted to page_size(). bool Protect(void* base_address, size_t length, PageAccess access, PageAccess* out_old_access = nullptr); // Queries a region of pages to get the access rights. This will modify the // length parameter to the length of pages with the same consecutive access // rights. The length will start from the first byte of the first page of // the region. bool QueryProtect(void* base_address, size_t& length, PageAccess& access_out); // Allocates a block of memory for a type with the given alignment. // The memory must be freed with AlignedFree. template inline T* AlignedAlloc(size_t alignment) { #if XE_COMPILER_MSVC return reinterpret_cast(_aligned_malloc(sizeof(T), alignment)); #else void* ptr = nullptr; if (posix_memalign(&ptr, alignment, sizeof(T))) { return nullptr; } return reinterpret_cast(ptr); #endif // XE_COMPILER_MSVC } // Frees memory previously allocated with AlignedAlloc. template void AlignedFree(T* ptr) { #if XE_COMPILER_MSVC _aligned_free(ptr); #else free(ptr); #endif // XE_COMPILER_MSVC } #if XE_PLATFORM_WIN32 // HANDLE. typedef void* FileMappingHandle; constexpr FileMappingHandle kFileMappingHandleInvalid = nullptr; #else // File descriptor. typedef int FileMappingHandle; constexpr FileMappingHandle kFileMappingHandleInvalid = -1; #endif FileMappingHandle CreateFileMappingHandle(const std::filesystem::path& path, size_t length, PageAccess access, bool commit); void CloseFileMappingHandle(FileMappingHandle handle, const std::filesystem::path& path); void* MapFileView(FileMappingHandle handle, void* base_address, size_t length, PageAccess access, size_t file_offset); bool UnmapFileView(FileMappingHandle handle, void* base_address, size_t length); inline size_t hash_combine(size_t seed) { return seed; } template size_t hash_combine(size_t seed, const T& v, const Ts&... vs) { std::hash hasher; seed ^= hasher(v) + 0x9E3779B9 + (seed << 6) + (seed >> 2); return hash_combine(seed, vs...); } } // namespace memory // TODO(benvanik): move into xe::memory:: inline void* low_address(void* address) { return reinterpret_cast(uint64_t(address) & 0xFFFFFFFF); } void copy_128_aligned(void* dest, const void* src, size_t count); void copy_and_swap_16_aligned(void* dest, const void* src, size_t count); void copy_and_swap_16_unaligned(void* dest, const void* src, size_t count); void copy_and_swap_32_aligned(void* dest, const void* src, size_t count); void copy_and_swap_32_unaligned(void* dest, const void* src, size_t count); void copy_and_swap_64_aligned(void* dest, const void* src, size_t count); void copy_and_swap_64_unaligned(void* dest, const void* src, size_t count); void copy_and_swap_16_in_32_aligned(void* dest, const void* src, size_t count); void copy_and_swap_16_in_32_unaligned(void* dest, const void* src, size_t count); template void copy_and_swap(T* dest, const T* src, size_t count) { bool is_aligned = reinterpret_cast(dest) % 32 == 0 && reinterpret_cast(src) % 32 == 0; if (sizeof(T) == 1) { std::memcpy(dest, src, count); } else if (sizeof(T) == 2) { auto ps = reinterpret_cast(src); auto pd = reinterpret_cast(dest); if (is_aligned) { copy_and_swap_16_aligned(pd, ps, count); } else { copy_and_swap_16_unaligned(pd, ps, count); } } else if (sizeof(T) == 4) { auto ps = reinterpret_cast(src); auto pd = reinterpret_cast(dest); if (is_aligned) { copy_and_swap_32_aligned(pd, ps, count); } else { copy_and_swap_32_unaligned(pd, ps, count); } } else if (sizeof(T) == 8) { auto ps = reinterpret_cast(src); auto pd = reinterpret_cast(dest); if (is_aligned) { copy_and_swap_64_aligned(pd, ps, count); } else { copy_and_swap_64_unaligned(pd, ps, count); } } else { assert_always("Invalid xe::copy_and_swap size"); } } template T load(const void* mem); template <> inline int8_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline uint8_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline int16_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline uint16_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline int32_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline uint32_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline int64_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline uint64_t load(const void* mem) { return *reinterpret_cast(mem); } template <> inline float load(const void* mem) { return *reinterpret_cast(mem); } template <> inline double load(const void* mem) { return *reinterpret_cast(mem); } template inline T load(const void* mem) { if (sizeof(T) == 1) { return static_cast(load(mem)); } else if (sizeof(T) == 2) { return static_cast(load(mem)); } else if (sizeof(T) == 4) { return static_cast(load(mem)); } else if (sizeof(T) == 8) { return static_cast(load(mem)); } else { assert_always("Invalid xe::load size"); } } template T load_and_swap(const void* mem); template <> inline int8_t load_and_swap(const void* mem) { return *reinterpret_cast(mem); } template <> inline uint8_t load_and_swap(const void* mem) { return *reinterpret_cast(mem); } template <> inline int16_t load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline uint16_t load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline int32_t load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline uint32_t load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline int64_t load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline uint64_t load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline float load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline double load_and_swap(const void* mem) { return byte_swap(*reinterpret_cast(mem)); } template <> inline std::string load_and_swap(const void* mem) { std::string value; for (int i = 0;; ++i) { auto c = xe::load_and_swap(reinterpret_cast(mem) + i); if (!c) { break; } value.push_back(static_cast(c)); } return value; } template <> inline std::u16string load_and_swap(const void* mem) { std::u16string value; for (int i = 0;; ++i) { auto c = xe::load_and_swap(reinterpret_cast(mem) + i); if (!c) { break; } value.push_back(static_cast(c)); } return value; } template void store(void* mem, const T& value); template <> inline void store(void* mem, const int8_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint8_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const int16_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint16_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const int32_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint32_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const int64_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const uint64_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const float& value) { *reinterpret_cast(mem) = value; } template <> inline void store(void* mem, const double& value) { *reinterpret_cast(mem) = value; } template constexpr inline void store(const void* mem, const T& value) { if constexpr (sizeof(T) == 1) { store(mem, static_cast(value)); } else if constexpr (sizeof(T) == 2) { store(mem, static_cast(value)); } else if constexpr (sizeof(T) == 4) { store(mem, static_cast(value)); } else if constexpr (sizeof(T) == 8) { store(mem, static_cast(value)); } else { static_assert("Invalid xe::store size"); } } template void store_and_swap(void* mem, const T& value); template <> inline void store_and_swap(void* mem, const int8_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store_and_swap(void* mem, const uint8_t& value) { *reinterpret_cast(mem) = value; } template <> inline void store_and_swap(void* mem, const int16_t& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const uint16_t& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const int32_t& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const uint32_t& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const int64_t& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const uint64_t& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const float& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const double& value) { *reinterpret_cast(mem) = byte_swap(value); } template <> inline void store_and_swap(void* mem, const std::string_view& value) { for (auto i = 0; i < value.size(); ++i) { xe::store_and_swap(reinterpret_cast(mem) + i, value[i]); } } template <> inline void store_and_swap(void* mem, const std::string& value) { return store_and_swap(mem, value); } template <> inline void store_and_swap( void* mem, const std::u16string_view& value) { for (auto i = 0; i < value.size(); ++i) { xe::store_and_swap(reinterpret_cast(mem) + i, value[i]); } } template <> inline void store_and_swap(void* mem, const std::u16string& value) { return store_and_swap(mem, value); } using fourcc_t = uint32_t; // Get FourCC in host byte order // make_fourcc('a', 'b', 'c', 'd') == 0x61626364 constexpr inline fourcc_t make_fourcc(char a, char b, char c, char d) { return fourcc_t((static_cast(a) << 24) | (static_cast(b) << 16) | (static_cast(c) << 8) | static_cast(d)); } // Get FourCC in host byte order // This overload requires fourcc.length() == 4 // make_fourcc("abcd") == 'abcd' == 0x61626364 for most compilers constexpr inline fourcc_t make_fourcc(const std::string_view fourcc) { if (fourcc.length() != 4) { throw std::runtime_error("Invalid fourcc length"); } return make_fourcc(fourcc[0], fourcc[1], fourcc[2], fourcc[3]); } // chrispy::todo:use for command stream vector, resize happens a ton and has to // call memset template class FixedVMemVector { static_assert((sz & 65535) == 0, "Always give fixed_vmem_vector a size divisible by 65536 to " "avoid wasting memory on windows"); uint8_t* data_; size_t nbytes_; public: FixedVMemVector() : data_((uint8_t*)memory::AllocFixed( nullptr, sz, memory::AllocationType::kReserveCommit, memory::PageAccess::kReadWrite)), nbytes_(0) {} ~FixedVMemVector() { if (data_) { memory::DeallocFixed(data_, sz, memory::DeallocationType::kRelease); data_ = nullptr; } nbytes_ = 0; } uint8_t* data() const { return data_; } size_t size() const { return nbytes_; } void resize(size_t newsize) { nbytes_ = newsize; xenia_assert(newsize < sz); } size_t alloc() const { return sz; } void clear() { resize(0); // todo:maybe zero out } void reserve(size_t size) { xenia_assert(size < sz); } }; // software prefetches/cache operations namespace swcache { /* warning, prefetchw's current behavior is not consistent across msvc and clang, for clang it will only compile to prefetchw if the set architecture supports it, for msvc however it will unconditionally compile to prefetchw! so prefetchw support is still in process only use these if you're absolutely certain you know what you're doing; you can easily tank performance through misuse CPUS have excellent automatic prefetchers that can predict patterns, but in situations where memory accesses are super unpredictable and follow no pattern you can make use of them another scenario where it can be handy is when crossing page boundaries, as many automatic prefetchers do not allow their streams to cross pages (no idea what this means for huge pages) I believe software prefetches do not kick off an automatic prefetcher stream, so you can't just prefetch one line of the data you're about to access and be fine, you need to go all the way prefetchnta is implementation dependent, and that makes its use a bit limited. For intel cpus, i believe it only prefetches the line into one way of the L3 for amd cpus, it marks the line as requiring immediate eviction, the next time an entry is needed in the set it resides in it will be evicted. ms does dumb shit for memcpy, like looping over the contents of the source buffer and doing prefetchnta on them, likely evicting some of the data they just prefetched by the end of the buffer, and probably messing up data that was already in the cache another warning for these: this bypasses what i think is called "critical word load", the data will always become available starting from the very beginning of the line instead of from the piece that is needed L1I cache is not prefetchable, however likely all cpus can fulfill requests for the L1I from L2, so prefetchL2 on instructions should be fine todo: clwb, clflush */ #if XE_COMPILER_HAS_GNU_EXTENSIONS == 1 XE_FORCEINLINE static void PrefetchW(const void* addr) { __builtin_prefetch(addr, 1, 0); } XE_FORCEINLINE static void PrefetchNTA(const void* addr) { __builtin_prefetch(addr, 0, 0); } XE_FORCEINLINE static void PrefetchL3(const void* addr) { __builtin_prefetch(addr, 0, 1); } XE_FORCEINLINE static void PrefetchL2(const void* addr) { __builtin_prefetch(addr, 0, 2); } XE_FORCEINLINE static void PrefetchL1(const void* addr) { __builtin_prefetch(addr, 0, 3); } #elif XE_ARCH_AMD64 == 1 && XE_COMPILER_MSVC == 1 XE_FORCEINLINE static void PrefetchW(const void* addr) { _m_prefetchw(addr); } XE_FORCEINLINE static void PrefetchNTA(const void* addr) { _mm_prefetch((const char*)addr, _MM_HINT_NTA); } XE_FORCEINLINE static void PrefetchL3(const void* addr) { _mm_prefetch((const char*)addr, _MM_HINT_T2); } XE_FORCEINLINE static void PrefetchL2(const void* addr) { _mm_prefetch((const char*)addr, _MM_HINT_T1); } XE_FORCEINLINE static void PrefetchL1(const void* addr) { _mm_prefetch((const char*)addr, _MM_HINT_T0); } #else XE_FORCEINLINE static void PrefetchW(const void* addr) {} XE_FORCEINLINE static void PrefetchNTA(const void* addr) {} XE_FORCEINLINE static void PrefetchL3(const void* addr) {} XE_FORCEINLINE static void PrefetchL2(const void* addr) {} XE_FORCEINLINE static void PrefetchL1(const void* addr) {} #endif enum class PrefetchTag { Write, Nontemporal, Level3, Level2, Level1 }; template static void Prefetch(const void* addr) { xenia_assert(false && "Unknown tag"); } template <> void Prefetch(const void* addr) { PrefetchW(addr); } template <> void Prefetch(const void* addr) { PrefetchNTA(addr); } template <> void Prefetch(const void* addr) { PrefetchL3(addr); } template <> void Prefetch(const void* addr) { PrefetchL2(addr); } template <> void Prefetch(const void* addr) { PrefetchL1(addr); } // todo: does aarch64 have streaming stores/loads? /* non-temporal stores/loads the stores allow cacheable memory to behave like write-combining memory. on the first nt store to a line, an intermediate buffer will be allocated by the cpu for stores that come after. once the entire contents of the line have been written the intermediate buffer will be transmitted to memory the written line will not be cached and if it is in the cache it will be invalidated from all levels of the hierarchy the cpu in this case does not have to read line from memory when we first write to it if it is not anywhere in the cache, so we use half the memory bandwidth using these stores non-temporal loads are... loads, but they dont use the cache. you need to manually insert memory barriers (_ReadWriteBarrier, ReadBarrier, etc, do not use any barriers that generate actual code) if on msvc to prevent it from moving the load of the data to just before the use of the data (immediately requiring the memory to be available = big stall) */ #if XE_COMPILER_MSVC == 1 && XE_COMPILER_CLANG_CL == 0 #define XE_MSVC_REORDER_BARRIER _ReadWriteBarrier #else // if the compiler actually has pipelining for instructions we dont need a // barrier #define XE_MSVC_REORDER_BARRIER() static_cast(0) #endif #if XE_ARCH_AMD64 == 1 union alignas(XE_HOST_CACHE_LINE_SIZE) CacheLine { struct { __m256 low32; __m256 high32; }; struct { __m128i xmms[4]; }; float floats[XE_HOST_CACHE_LINE_SIZE / sizeof(float)]; }; XE_FORCEINLINE static void WriteLineNT(CacheLine* XE_RESTRICT destination, const CacheLine* XE_RESTRICT source) { assert_true((reinterpret_cast(destination) & 63ULL) == 0); __m256 low = _mm256_loadu_ps(&source->floats[0]); __m256 high = _mm256_loadu_ps(&source->floats[8]); _mm256_stream_ps(&destination->floats[0], low); _mm256_stream_ps(&destination->floats[8], high); } XE_FORCEINLINE static void ReadLineNT(CacheLine* XE_RESTRICT destination, const CacheLine* XE_RESTRICT source) { assert_true((reinterpret_cast(source) & 63ULL) == 0); __m128i first = _mm_stream_load_si128(&source->xmms[0]); __m128i second = _mm_stream_load_si128(&source->xmms[1]); __m128i third = _mm_stream_load_si128(&source->xmms[2]); __m128i fourth = _mm_stream_load_si128(&source->xmms[3]); destination->xmms[0] = first; destination->xmms[1] = second; destination->xmms[2] = third; destination->xmms[3] = fourth; } XE_FORCEINLINE static void ReadLine(CacheLine* XE_RESTRICT destination, const CacheLine* XE_RESTRICT source) { assert_true((reinterpret_cast(source) & 63ULL) == 0); __m256 low = _mm256_loadu_ps(&source->floats[0]); __m256 high = _mm256_loadu_ps(&source->floats[8]); _mm256_storeu_ps(&destination->floats[0], low); _mm256_storeu_ps(&destination->floats[8], high); } XE_FORCEINLINE static void WriteLine(CacheLine* XE_RESTRICT destination, const CacheLine* XE_RESTRICT source) { assert_true((reinterpret_cast(destination) & 63ULL) == 0); __m256 low = _mm256_loadu_ps(&source->floats[0]); __m256 high = _mm256_loadu_ps(&source->floats[8]); _mm256_storeu_ps(&destination->floats[0], low); _mm256_storeu_ps(&destination->floats[8], high); } XE_FORCEINLINE static void WriteFence() { _mm_sfence(); } XE_FORCEINLINE static void ReadFence() { _mm_lfence(); } XE_FORCEINLINE static void ReadWriteFence() { _mm_mfence(); } #else union alignas(XE_HOST_CACHE_LINE_SIZE) CacheLine { uint8_t bvals[XE_HOST_CACHE_LINE_SIZE]; }; XE_FORCEINLINE static void WriteLineNT(CacheLine* destination, const CacheLine* source) { memcpy(destination, source, XE_HOST_CACHE_LINE_SIZE); } XE_FORCEINLINE static void ReadLineNT(CacheLine* destination, const CacheLine* source) { memcpy(destination, source, XE_HOST_CACHE_LINE_SIZE); } XE_FORCEINLINE static void WriteLine(CacheLine* destination, const CacheLine* source) { memcpy(destination, source, XE_HOST_CACHE_LINE_SIZE); } XE_FORCEINLINE static void ReadLine(CacheLine* destination, const CacheLine* source) { memcpy(destination, source, XE_HOST_CACHE_LINE_SIZE); } XE_FORCEINLINE static void WriteFence() {} XE_FORCEINLINE static void ReadFence() {} XE_FORCEINLINE static void ReadWriteFence() {} #endif } // namespace swcache template static void smallcpy_const(void* destination, const void* source) { #if XE_ARCH_AMD64 == 1 && XE_COMPILER_MSVC == 1 if constexpr ((Size & 7) == 0) { __movsq((unsigned long long*)destination, (const unsigned long long*)source, Size / 8); } else if constexpr ((Size & 3) == 0) { __movsd((unsigned long*)destination, (const unsigned long*)source, Size / 4); // dont even bother with movsw, i think the operand size override prefix // slows it down } else { __movsb((unsigned char*)destination, (const unsigned char*)source, Size); } #else memcpy(destination, source, Size); #endif } template static void smallset_const(void* destination, unsigned char fill_value) { #if XE_ARCH_AMD64 == 1 && XE_COMPILER_MSVC == 1 if constexpr ((Size & 7) == 0) { unsigned long long fill = static_cast(fill_value) * 0x0101010101010101ULL; __stosq((unsigned long long*)destination, fill, Size / 8); } else if constexpr ((Size & 3) == 0) { static constexpr unsigned long fill = static_cast(fill_value) * 0x01010101U; __stosd((unsigned long*)destination, fill, Size / 4); // dont even bother with movsw, i think the operand size override prefix // slows it down } else { __stosb((unsigned char*)destination, fill_value, Size); } #else memset(destination, fill_value, Size); #endif } } // namespace xe #endif // XENIA_BASE_MEMORY_H_