From 8b94e8f55c7f65f6d6d221adf8cdd3345296dc9a Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Mon, 18 May 2026 13:42:41 +0200 Subject: [PATCH] Added first draft of RAM class --- .zed/tasks.json | 4 +- CMakeLists.txt | 39 ++- other/build.nr | 2 +- src/RAM_test.cpp | 422 +++++++++++++++++++++++ src/hardware/{VirtualCPU.cpp => CPU.cpp} | 4 +- src/hardware/{VirtualCPU.hpp => CPU.hpp} | 4 +- src/hardware/RAM.cpp | 159 +++++++++ src/hardware/RAM.hpp | 66 ++++ 8 files changed, 687 insertions(+), 13 deletions(-) create mode 100644 src/RAM_test.cpp rename src/hardware/{VirtualCPU.cpp => CPU.cpp} (93%) rename src/hardware/{VirtualCPU.hpp => CPU.hpp} (95%) create mode 100644 src/hardware/RAM.cpp create mode 100644 src/hardware/RAM.hpp diff --git a/.zed/tasks.json b/.zed/tasks.json index 724fed9..bfd283d 100644 --- a/.zed/tasks.json +++ b/.zed/tasks.json @@ -18,9 +18,9 @@ "reveal": "always", }, { - "label": "build dbg", + "label": "build gdb", "command": "./build", - "args": ["dbg"], + "args": ["gdb"], "cwd": "$ZED_WORKTREE_ROOT", "use_new_terminal": false, "allow_concurrent_runs": false, diff --git a/CMakeLists.txt b/CMakeLists.txt index ba7c94f..68e338c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,12 +31,14 @@ list(APPEND INCLUDE_DIRS list(APPEND RUNTIME_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/runtime/runtime_main.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualCPU.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/CPU.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/RAM.cpp ) list(APPEND DEBUGGER_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/debugger/debugger_main.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/hardware/VirtualCPU.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/CPU.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/RAM.cpp ) list(APPEND ASSEMBLER_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/assembler/assembler_main.cpp @@ -44,6 +46,10 @@ list(APPEND ASSEMBLER_SOURCE_FILES list(APPEND TOOLS_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/tools/tools_main.cpp ) +list(APPEND TEST_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/src/RAM_test.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/hardware/RAM.cpp +) #----------------------------------------------------------------------------------------- #Targets @@ -51,7 +57,7 @@ list(APPEND TOOLS_SOURCE_FILES set(RUNTIME_TARGET dvm) add_executable(${RUNTIME_TARGET} ${RUNTIME_SOURCE_FILES}) if(WIN32) - target_sources(${MAIN_EXECUTABLE} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dvm_appIcon.rc") + target_sources(${RUNTIME_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dvm_appIcon.rc") endif() target_include_directories(${RUNTIME_TARGET} PUBLIC ${INCLUDE_DIRS}) @@ -59,7 +65,7 @@ target_include_directories(${RUNTIME_TARGET} PUBLIC ${INCLUDE_DIRS}) set(DEBUGGER_TARGET ddb) add_executable(${DEBUGGER_TARGET} ${DEBUGGER_SOURCE_FILES}) if(WIN32) - target_sources(${MAIN_EXECUTABLE} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/ddb_appIcon.rc") + target_sources(${DEBUGGER_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/ddb_appIcon.rc") endif() target_include_directories(${DEBUGGER_TARGET} PUBLIC ${INCLUDE_DIRS}) @@ -67,7 +73,7 @@ target_include_directories(${DEBUGGER_TARGET} PUBLIC ${INCLUDE_DIRS}) set(ASSEMBLER_TARGET dasm) add_executable(${ASSEMBLER_TARGET} ${ASSEMBLER_SOURCE_FILES}) if(WIN32) - target_sources(${MAIN_EXECUTABLE} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dasm_appIcon.rc") + target_sources(${ASSEMBLER_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dasm_appIcon.rc") endif() target_include_directories(${ASSEMBLER_TARGET} PUBLIC ${INCLUDE_DIRS}) @@ -75,14 +81,23 @@ target_include_directories(${ASSEMBLER_TARGET} PUBLIC ${INCLUDE_DIRS}) set(TOOLS_TARGET dtools) add_executable(${TOOLS_TARGET} ${TOOLS_SOURCE_FILES}) if(WIN32) - target_sources(${MAIN_EXECUTABLE} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dtools_appIcon.rc") + target_sources(${TOOLS_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dtools_appIcon.rc") endif() target_include_directories(${TOOLS_TARGET} PUBLIC ${INCLUDE_DIRS}) + +set(TEST_TARGET test_exec) +add_executable(${TEST_TARGET} ${TEST_SOURCE_FILES}) +if(WIN32) + target_sources(${TEST_TARGET} PUBLIC "${CMAKE_CURRENT_LIST_DIR}/other/dtools_appIcon.rc") +endif() +target_include_directories(${TEST_TARGET} PUBLIC ${INCLUDE_DIRS}) + target_compile_definitions(${RUNTIME_TARGET} PUBLIC BUILD_NR=${BUILD_NUMBER} MAJ_V=${MAJOR_VER} MIN_V=${MINOR_VER} VERSION_STR="${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER}") target_compile_definitions(${DEBUGGER_TARGET} PUBLIC BUILD_NR=${BUILD_NUMBER} MAJ_V=${MAJOR_VER} MIN_V=${MINOR_VER} VERSION_STR="${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER}") target_compile_definitions(${ASSEMBLER_TARGET} PUBLIC BUILD_NR=${BUILD_NUMBER} MAJ_V=${MAJOR_VER} MIN_V=${MINOR_VER} VERSION_STR="${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER}") target_compile_definitions(${TOOLS_TARGET} PUBLIC BUILD_NR=${BUILD_NUMBER} MAJ_V=${MAJOR_VER} MIN_V=${MINOR_VER} VERSION_STR="${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER}") +target_compile_definitions(${TEST_TARGET} PUBLIC BUILD_NR=${BUILD_NUMBER} MAJ_V=${MAJOR_VER} MIN_V=${MINOR_VER} VERSION_STR="${MAJOR_VER}.${MINOR_VER}.${BUILD_NUMBER}") if (WIN32) set(MSYS2_UCRT64 "C:/msys64/ucrt64") @@ -108,6 +123,12 @@ if (WIN32) "${MSYS2_UCRT64}/include/c++/13.2.0" "${MSYS2_UCRT64}/include/c++/13.2.0/x86_64-w64-mingw32" + ) + target_include_directories(${TEST_TARGET} PRIVATE + "${MSYS2_UCRT64}/include" + "${MSYS2_UCRT64}/include/c++/13.2.0" + "${MSYS2_UCRT64}/include/c++/13.2.0/x86_64-w64-mingw32" + ) endif() @@ -116,12 +137,14 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_definitions(${DEBUGGER_TARGET} PUBLIC BUILD_CONFIG_DEBUG OX_DEBUG_BUILD) target_compile_definitions(${ASSEMBLER_TARGET} PUBLIC BUILD_CONFIG_DEBUG OX_DEBUG_BUILD) target_compile_definitions(${TOOLS_TARGET} PUBLIC BUILD_CONFIG_DEBUG OX_DEBUG_BUILD) + target_compile_definitions(${TEST_TARGET} PUBLIC BUILD_CONFIG_DEBUG OX_DEBUG_BUILD) add_compile_options(-O3 -MMD -MP -Wall -ggdb) elseif(CMAKE_BUILD_TYPE STREQUAL "Release") target_compile_definitions(${RUNTIME_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD) target_compile_definitions(${DEBUGGER_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD) target_compile_definitions(${ASSEMBLER_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD) target_compile_definitions(${TOOLS_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD) + target_compile_definitions(${TEST_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD) add_compile_options(-MMD -MP -Wall) endif() @@ -148,6 +171,7 @@ if (WIN32) target_link_libraries(${ASSEMBLER_TARGET} mingw32) target_link_libraries(${TOOLS_TARGET} mingw32) target_link_libraries(${DEBUGGER_TARGET} mingw32 SDL3 SDL3_ttf SDL3_image) + target_link_libraries(${TEST_TARGET} mingw32) endif (WIN32) if (APPLE) @@ -157,6 +181,7 @@ if (APPLE) target_link_libraries(${ASSEMBLER_TARGET} Boost::regex) target_link_libraries(${TOOLS_TARGET} Boost::regex) target_link_libraries(${DEBUGGER_TARGET} Boost::regex) + target_link_libraries(${TEST_TARGET} Boost::regex) find_package(PkgConfig REQUIRED) pkg_check_modules(SDL3 REQUIRED sdl3) pkg_check_modules(SDL3_IMAGE REQUIRED sdl3-image) @@ -197,10 +222,12 @@ elseif (UNIX) target_link_libraries(${ASSEMBLER_TARGET} xcb xcb-randr boost_regex) target_link_libraries(${TOOLS_TARGET} xcb xcb-randr boost_regex) target_link_libraries(${DEBUGGER_TARGET} xcb xcb-randr boost_regex SDL3 SDL3_image) + target_link_libraries(${TEST_TARGET} xcb xcb-randr boost_regex) endif () target_link_libraries(${RUNTIME_TARGET} ostd ogfx) target_link_libraries(${DEBUGGER_TARGET} ostd ogfx) target_link_libraries(${ASSEMBLER_TARGET} ostd) target_link_libraries(${TOOLS_TARGET} ostd) +target_link_libraries(${TEST_TARGET} ostd) #----------------------------------------------------------------------------------------- diff --git a/other/build.nr b/other/build.nr index 7f8f011..ec63514 100644 --- a/other/build.nr +++ b/other/build.nr @@ -1 +1 @@ -7 +9 diff --git a/src/RAM_test.cpp b/src/RAM_test.cpp new file mode 100644 index 0000000..f4aa8b7 --- /dev/null +++ b/src/RAM_test.cpp @@ -0,0 +1,422 @@ +/* + DragonV2 RAM test harness. + + Single-threaded tests prove the bit-level correctness of the + load/store/shift/mask logic. Multi-threaded tests prove the + atomicity claims: under hammering contention, no update is lost + and no neighbour byte is corrupted. + + Build (example, adjust include paths to your tree): + g++ -std=c++20 -O2 -pthread RAM.cpp RAM_test.cpp -o ram_test + then run: + ./ram_test +*/ + +#include "hardware/RAM.hpp" + +#include +#include +#include +#include +#include +#include + +using dragon::hw::RAM; + +// --- tiny test framework ------------------------------------------------------ + +static int g_passed = 0; +static int g_failed = 0; +static bool g_currentFailed = false; + +#define TEST(name) \ + do { \ + const char* __test_name = #name; \ + g_currentFailed = false; \ + std::cout << "[ RUN ] " << __test_name << "\n"; \ + auto __start = std::chrono::steady_clock::now(); \ + name(); \ + auto __end = std::chrono::steady_clock::now(); \ + auto __ms = std::chrono::duration_cast(__end - __start).count(); \ + if (g_currentFailed) { ++g_failed; std::cout << "[ FAIL ] " << __test_name << " (" << __ms << " ms)\n"; } \ + else { ++g_passed; std::cout << "[ PASS ] " << __test_name << " (" << __ms << " ms)\n"; } \ + } while (0) + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + g_currentFailed = true; \ + std::cout << " CHECK failed: " #cond " at " << __FILE__ << ":" << __LINE__ << "\n"; \ + } \ + } while (0) + +#define CHECK_EQ(a, b) \ + do { \ + auto __a = (a); auto __b = (b); \ + if (!(__a == __b)) { \ + g_currentFailed = true; \ + std::cout << " CHECK_EQ failed: " #a " == " #b \ + << " got 0x" << std::hex << (u64)__a << " vs 0x" << (u64)__b << std::dec \ + << " at " << __FILE__ << ":" << __LINE__ << "\n"; \ + } \ + } while (0) + + +// --- single-threaded correctness --------------------------------------------- + +// If you store, then load, do you get what you stored back? This is the +// most basic sanity check and proves the shift/mask math is right at the +// bit level. Multi-threading can't fix bit-level bugs. +static void test_roundtrip_u8(void) +{ + RAM ram(1024); + for (u32 a = 0; a < 256; ++a) ram.store_u8(a, static_cast(a ^ 0x5A)); + for (u32 a = 0; a < 256; ++a) CHECK_EQ(ram.load_u8(a), static_cast(a ^ 0x5A)); +} + +static void test_roundtrip_u16(void) +{ + RAM ram(1024); + for (u32 a = 0; a < 256; a += 2) ram.store_u16(a, static_cast(0xBEEF ^ a)); + for (u32 a = 0; a < 256; a += 2) CHECK_EQ(ram.load_u16(a), static_cast(0xBEEF ^ a)); +} + +static void test_roundtrip_u32(void) +{ + RAM ram(1024); + for (u32 a = 0; a < 256; a += 4) ram.store_u32(a, 0xDEADBEEFu ^ a); + for (u32 a = 0; a < 256; a += 4) CHECK_EQ(ram.load_u32(a), 0xDEADBEEFu ^ a); +} + +static void test_roundtrip_u64(void) +{ + RAM ram(1024); + for (u32 a = 0; a < 256; a += 8) ram.store_u64(a, 0xCAFEBABEDEADBEEFull ^ a); + for (u32 a = 0; a < 256; a += 8) CHECK_EQ(ram.load_u64(a), 0xCAFEBABEDEADBEEFull ^ a); +} + +// Writing one byte/half-word must not disturb its neighbours within the +// same 64-bit word. This is the property the CAS loop is designed to +// guarantee, but we can prove it single-threaded too: do the writes in a +// known order and check every byte after each step. +static void test_no_neighbour_corruption_singlethread(void) +{ + RAM ram(64); + + // Fill word 0 with a known pattern using a single u64 store. + ram.store_u64(0, 0x0807060504030201ull); + + // Overwrite just byte 3 with a CAS-loop store. + ram.store_u8(3, 0xFF); + + // All other bytes must be untouched. + CHECK_EQ(ram.load_u8(0), 0x01); + CHECK_EQ(ram.load_u8(1), 0x02); + CHECK_EQ(ram.load_u8(2), 0x03); + CHECK_EQ(ram.load_u8(3), 0xFF); + CHECK_EQ(ram.load_u8(4), 0x05); + CHECK_EQ(ram.load_u8(5), 0x06); + CHECK_EQ(ram.load_u8(6), 0x07); + CHECK_EQ(ram.load_u8(7), 0x08); + + // And reading the whole word should show one byte changed. + CHECK_EQ(ram.load_u64(0), 0x08070605FF030201ull); +} + +// Little-endian layout: the byte at address N occupies bits [N*8 .. N*8+7] +// of the host atomic word. If the shift math is backwards, this fails. +static void test_endianness_layout(void) +{ + RAM ram(64); + // Write a u32 = 0x44332211 at address 0. In little-endian, byte 0 should + // be 0x11, byte 1 should be 0x22, byte 2 should be 0x33, byte 3 should be 0x44. + ram.store_u32(0, 0x44332211u); + CHECK_EQ(ram.load_u8(0), 0x11); + CHECK_EQ(ram.load_u8(1), 0x22); + CHECK_EQ(ram.load_u8(2), 0x33); + CHECK_EQ(ram.load_u8(3), 0x44); + + // And reading u16 at addr 0 should give the low half: 0x2211. + CHECK_EQ(ram.load_u16(0), 0x2211); + // u16 at addr 2 should give the high half: 0x4433. + CHECK_EQ(ram.load_u16(2), 0x4433); +} + +// Misaligned and out-of-bounds accesses should fault rather than +// corrupting memory. We can't observe the fault directly (it just prints +// for now), but we can observe that they don't change neighbouring bytes. +static void test_faults_dont_modify_memory(void) +{ + RAM ram(64); + ram.store_u64(0, 0xAAAAAAAAAAAAAAAAull); + + // Misaligned u32 store should be rejected. + ram.store_u32(1, 0x55555555u); + CHECK_EQ(ram.load_u64(0), 0xAAAAAAAAAAAAAAAAull); + + // Out-of-bounds store should be rejected. + ram.store_u8(64, 0x55); + // And the last in-bounds word is unchanged. + CHECK_EQ(ram.load_u64(56), 0x0000000000000000ull); +} + + +// --- multi-threaded atomicity ------------------------------------------------- + +// The classic atomicity proof. N threads each increment the same u32 +// location M times using a CAS retry loop in user code. If atomic stores +// were not actually atomic (or if our CAS loop were broken), updates +// would be lost and the final count would be less than N*M. +// +// Note this test uses store_u32 indirectly via a read-modify-write pattern +// from user code, which is NOT atomic by itself. We instead drive the +// guarantee directly: we expect each store_u32 to commit as a single +// SC-ordered update. The right test for this is "concurrent disjoint +// writes preserve neighbours" (next test). What this test actually +// stresses is that the RAM survives concurrent access on the same word +// without crashing or producing impossible values. +static void test_concurrent_same_word_smoke(void) +{ + RAM ram(64); + ram.store_u32(0, 0); + + constexpr int kThreads = 8; + constexpr int kIters = 100000; + + std::vector threads; + threads.reserve(kThreads); + std::atomic ready { 0 }; + std::atomic go { false }; + + for (int t = 0; t < kThreads; ++t) { + threads.emplace_back([&, t]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { /* spin */ } + for (int i = 0; i < kIters; ++i) { + // Each thread writes its own ID into addr 0. + ram.store_u32(0, static_cast(t)); + u32 v = ram.load_u32(0); + // The only valid values are 0..kThreads-1 + CHECK(v < static_cast(kThreads)); + } + }); + } + + while (ready.load(std::memory_order_seq_cst) < kThreads) { /* spin */ } + go.store(true, std::memory_order_seq_cst); + + for (auto& th : threads) th.join(); + + u32 final = ram.load_u32(0); + CHECK(final < static_cast(kThreads)); +} + +// THE critical test for sub-word atomicity. 8 threads each write a +// distinct constant value to a distinct byte of the same 64-bit word, +// hammering on it for many iterations. After they all stop, the word +// must contain exactly one byte from each thread, with no corruption. +// +// If the CAS loop is wrong, some writer's byte will get clobbered by a +// neighbour's read-modify-write cycle and the final word will contain +// the wrong byte for one of the slots. +static void test_concurrent_neighbour_byte_writes(void) +{ + RAM ram(64); + ram.store_u64(0, 0); // start clean + + constexpr int kIters = 200000; + + std::vector threads; + std::atomic ready { 0 }; + std::atomic go { false }; + + // Each thread owns one byte slot (0..7) and a unique 8-bit value to write. + for (u32 slot = 0; slot < 8; ++slot) { + const u8 my_value = static_cast(0x10 + slot); // 0x10, 0x11, ..., 0x17 + threads.emplace_back([&, slot, my_value]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { /* spin */ } + for (int i = 0; i < kIters; ++i) { + ram.store_u8(slot, my_value); + } + }); + } + + while (ready.load(std::memory_order_seq_cst) < 8) { /* spin */ } + go.store(true, std::memory_order_seq_cst); + for (auto& th : threads) th.join(); + + // After the dust settles, each byte slot must hold the value its owner wrote. + for (u32 slot = 0; slot < 8; ++slot) { + CHECK_EQ(ram.load_u8(slot), static_cast(0x10 + slot)); + } +} + +// Same property, scaled up: across many words, many threads, mixed widths. +// One thread per byte slot across 16 words. If anything is broken +// anywhere in the shift/mask/CAS pipeline, this should expose it. +static void test_concurrent_many_words(void) +{ + constexpr u32 kWords = 16; + RAM ram(kWords * 8); + for (u32 w = 0; w < kWords; ++w) ram.store_u64(w * 8, 0); + + constexpr int kIters = 50000; + std::vector threads; + std::atomic ready { 0 }; + std::atomic go { false }; + + for (u32 w = 0; w < kWords; ++w) { + for (u32 slot = 0; slot < 8; ++slot) { + const u32 addr = w * 8 + slot; + const u8 val = static_cast((w * 17 + slot * 31) & 0xFF); + threads.emplace_back([&, addr, val]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { /* spin */ } + for (int i = 0; i < kIters; ++i) { + ram.store_u8(addr, val); + } + }); + } + } + + const int total = static_cast(kWords * 8); + while (ready.load(std::memory_order_seq_cst) < total) { /* spin */ } + go.store(true, std::memory_order_seq_cst); + for (auto& th : threads) th.join(); + + for (u32 w = 0; w < kWords; ++w) { + for (u32 slot = 0; slot < 8; ++slot) { + const u8 expected = static_cast((w * 17 + slot * 31) & 0xFF); + CHECK_EQ(ram.load_u8(w * 8 + slot), expected); + } + } +} + +// Mixed-width contention. Two threads compete on the same 64-bit word +// but at different widths: one writes a u32 to the low half, another +// writes a u16 to byte offset 4 (inside the high half), another writes +// individual bytes to offsets 6 and 7. After many iterations the final +// state must be self-consistent (each writer's last value visible in its +// own slot). +static void test_concurrent_mixed_widths(void) +{ + RAM ram(64); + ram.store_u64(0, 0); + + constexpr int kIters = 100000; + std::atomic ready { 0 }; + std::atomic go { false }; + std::vector threads; + + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + for (int i = 0; i < kIters; ++i) ram.store_u32(0, 0xAABBCCDDu); + }); + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + for (int i = 0; i < kIters; ++i) ram.store_u16(4, 0xEEFFu); + }); + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + for (int i = 0; i < kIters; ++i) ram.store_u8(6, 0x11); + }); + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + for (int i = 0; i < kIters; ++i) ram.store_u8(7, 0x22); + }); + + while (ready.load(std::memory_order_seq_cst) < 4) { } + go.store(true, std::memory_order_seq_cst); + for (auto& th : threads) th.join(); + + // Each writer wrote the same value every iteration so each slot must hold its writer's value. + CHECK_EQ(ram.load_u32(0), 0xAABBCCDDu); + CHECK_EQ(ram.load_u16(4), 0xEEFFu); + CHECK_EQ(ram.load_u8 (6), 0x11); + CHECK_EQ(ram.load_u8 (7), 0x22); +} + +// Readers + writers. If the load path were non-atomic (e.g. read-modify +// of underlying storage during a load) you could see torn values. With +// seq_cst atomics, every load sees exactly one consistent prior store. +static void test_concurrent_readers_writers(void) +{ + RAM ram(64); + ram.store_u64(0, 0); + + constexpr int kIters = 200000; + std::atomic ready { 0 }; + std::atomic go { false }; + std::atomic stop { false }; + std::vector threads; + + // Two writers alternate between two known patterns. + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + for (int i = 0; i < kIters; ++i) ram.store_u64(0, 0x1111111111111111ull); + }); + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + for (int i = 0; i < kIters; ++i) ram.store_u64(0, 0x2222222222222222ull); + }); + + // Readers should never observe anything other than the two patterns. + std::atomic bad_observations { 0 }; + for (int r = 0; r < 4; ++r) { + threads.emplace_back([&]() { + ready.fetch_add(1, std::memory_order_seq_cst); + while (!go.load(std::memory_order_seq_cst)) { } + while (!stop.load(std::memory_order_seq_cst)) { + u64 v = ram.load_u64(0); + if (v != 0x1111111111111111ull && v != 0x2222222222222222ull && v != 0) + bad_observations.fetch_add(1, std::memory_order_seq_cst); + } + }); + } + + while (ready.load(std::memory_order_seq_cst) < 6) { } + go.store(true, std::memory_order_seq_cst); + + // Wait for writers to finish, then stop readers. + threads[0].join(); + threads[1].join(); + stop.store(true, std::memory_order_seq_cst); + for (size_t i = 2; i < threads.size(); ++i) threads[i].join(); + + CHECK_EQ(bad_observations.load(), 0); +} + +// --- main -------------------------------------------------------------------- + +int main(void) +{ + std::cout << "DragonV2 RAM test harness\n"; + std::cout << "=========================\n\n"; + + std::cout << "--- single-threaded correctness ---\n"; + TEST(test_roundtrip_u8); + TEST(test_roundtrip_u16); + TEST(test_roundtrip_u32); + TEST(test_roundtrip_u64); + TEST(test_no_neighbour_corruption_singlethread); + TEST(test_endianness_layout); + TEST(test_faults_dont_modify_memory); + + std::cout << "\n--- multi-threaded atomicity ---\n"; + TEST(test_concurrent_same_word_smoke); + TEST(test_concurrent_neighbour_byte_writes); + TEST(test_concurrent_many_words); + TEST(test_concurrent_mixed_widths); + TEST(test_concurrent_readers_writers); + + std::cout << "\n=========================\n"; + std::cout << "Passed: " << g_passed << " Failed: " << g_failed << "\n"; + return g_failed == 0 ? 0 : 1; +} diff --git a/src/hardware/VirtualCPU.cpp b/src/hardware/CPU.cpp similarity index 93% rename from src/hardware/VirtualCPU.cpp rename to src/hardware/CPU.cpp index 87adbf1..432d7b0 100644 --- a/src/hardware/VirtualCPU.cpp +++ b/src/hardware/CPU.cpp @@ -18,13 +18,13 @@ along with DragonV2. If not, see . */ -#include "VirtualCPU.hpp" +#include "CPU.hpp" namespace dragon { namespace hw { - VirtualCPU::VirtualCPU(void) + CPU::CPU(void) { } } diff --git a/src/hardware/VirtualCPU.hpp b/src/hardware/CPU.hpp similarity index 95% rename from src/hardware/VirtualCPU.hpp rename to src/hardware/CPU.hpp index ed74387..a0a9fa5 100644 --- a/src/hardware/VirtualCPU.hpp +++ b/src/hardware/CPU.hpp @@ -26,10 +26,10 @@ namespace dragon { namespace hw { - class VirtualCPU + class CPU { public: - VirtualCPU(void); + CPU(void); private: }; diff --git a/src/hardware/RAM.cpp b/src/hardware/RAM.cpp new file mode 100644 index 0000000..e35bda1 --- /dev/null +++ b/src/hardware/RAM.cpp @@ -0,0 +1,159 @@ +/* + DragonV2 - A collection of useful functionality + Copyright (C) 2026 OmniaX-Dev + + This file is part of DragonV2. + + DragonV2 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DragonV2 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DragonV2. If not, see . +*/ + +#include "RAM.hpp" +#include +#include + +namespace dragon +{ + namespace hw + { + RAM::RAM(u64 size) + { + m_wordCount = (size + 7) / 8; // Round up to multiple of 8 bytes for the word-granular backing + m_size = size; + m_words = new std::atomic[m_wordCount]; + for (u64 i = 0; i < m_wordCount; ++i) + m_words[i].store(0, std::memory_order_relaxed); + } + + RAM::~RAM(void) + { + delete[] m_words; + } + + u64 RAM::load_u64(u32 addr) const + { + if (addr + 8 > m_size || (addr & 7)) + { + fault(addr, 8); + return 0; + } + return m_words[addr >> 3].load(std::memory_order_seq_cst); + } + + u32 RAM::load_u32(u32 addr) const + { + if (addr + 4 > m_size || (addr & 3)) + { + fault(addr, 4); + return 0; + } + u64 word = m_words[addr >> 3].load(std::memory_order_seq_cst); + u32 shift = (addr & 4) * 8; // 0 or 32 + return static_cast(word >> shift); + } + + u16 RAM::load_u16(u32 addr) const + { + if (addr + 2 > m_size || (addr & 1)) + { + fault(addr, 2); + return 0; + } + u64 word = m_words[addr >> 3].load(std::memory_order_seq_cst); + u32 shift = (addr & 6) * 8; // 0, 16, 32, or 48 + return static_cast(word >> shift); + } + + u8 RAM::load_u8(u32 addr) const + { + if (addr >= m_size) + { + fault(addr, 1); + return 0; + } + u64 word = m_words[addr >> 3].load(std::memory_order_seq_cst); + u32 shift = (addr & 7) * 8; + return static_cast(word >> shift); + } + + void RAM::store_u64(u32 addr, u64 value) + { + if (addr + 8 > m_size || (addr & 7)) + { + fault(addr, 8); + return; + } + m_words[addr >> 3].store(value, std::memory_order_seq_cst); + } + + void RAM::store_u32(u32 addr, u32 value) + { + if (addr + 4 > m_size || (addr & 3)) + { + fault(addr, 4); + return; + } + u32 shift = (addr & 4) * 8; + u64 mask = u64(0xFFFFFFFFu) << shift; + std::atomic& word = m_words[addr >> 3]; + u64 expected = word.load(std::memory_order_relaxed); + u64 desired; + do + { + desired = (expected & ~mask) | (u64(value) << shift); + } while (!word.compare_exchange_weak(expected, desired, std::memory_order_seq_cst, std::memory_order_relaxed)); + } + + void RAM::store_u16(u32 addr, u16 value) + { + if (addr + 2 > m_size || (addr & 1)) + { + fault(addr, 2); + return; + } + u32 shift = (addr & 6) * 8; + u64 mask = u64(0xFFFFu) << shift; + std::atomic& word = m_words[addr >> 3]; + u64 expected = word.load(std::memory_order_relaxed); + u64 desired; + do + { + desired = (expected & ~mask) | (u64(value) << shift); + } while (!word.compare_exchange_weak(expected, desired, std::memory_order_seq_cst, std::memory_order_relaxed)); + } + + void RAM::store_u8(u32 addr, u8 value) + { + if (addr >= m_size) + { + fault(addr, 1); + return; + } + u32 shift = (addr & 7) * 8; + u64 mask = u64(0xFFu) << shift; + std::atomic& word = m_words[addr >> 3]; + u64 expected = word.load(std::memory_order_relaxed); + u64 desired; + do + { + desired = (expected & ~mask) | (u64(value) << shift); + } while (!word.compare_exchange_weak(expected, desired, std::memory_order_seq_cst, std::memory_order_relaxed)); + } + + void RAM::fault(u32 addr, u32 size) const + { + std::cout << "Memory Fault: " << ostd::String::getHexStr(addr, true, 4) << "\n"; + /* TODO: raise guest trap */ + } + } +} diff --git a/src/hardware/RAM.hpp b/src/hardware/RAM.hpp new file mode 100644 index 0000000..d31a27e --- /dev/null +++ b/src/hardware/RAM.hpp @@ -0,0 +1,66 @@ +/* + DragonV2 - A collection of useful functionality + Copyright (C) 2026 OmniaX-Dev + + This file is part of DragonV2. + + DragonV2 is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DragonV2 is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DragonV2. If not, see . +*/ + +#pragma once + +#include +#include + +namespace dragon +{ + namespace hw + { + class RAM + { + public: + explicit RAM(u64 size); + ~RAM(void); + RAM(const RAM&) = delete; + RAM& operator=(const RAM&) = delete; + + // ---- Aligned loads ---- + u64 load_u64(u32 addr) const; + u32 load_u32(u32 addr) const; + u16 load_u16(u32 addr) const; + u8 load_u8 (u32 addr) const; + inline i64 load_i64(u32 addr) const { return cast(load_u64(addr)); } + inline i32 load_i32(u32 addr) const { return cast(load_u32(addr)); } + inline i16 load_i16(u32 addr) const { return cast(load_u16(addr)); } + inline i8 load_i8 (u32 addr) const { return cast(load_u8 (addr)); } + + // ---- Aligned stores ---- + void store_u64(u32 addr, u64 value); + void store_u32(u32 addr, u32 value); + void store_u16(u32 addr, u16 value); + void store_u8 (u32 addr, u8 value); + inline void store_i64(u32 addr, u64 value) { store_u64(addr, cast(value)); } + inline void store_i32(u32 addr, u32 value) { store_u32(addr, cast(value)); } + inline void store_i16(u32 addr, u16 value) { store_u16(addr, cast(value)); } + inline void store_i8 (u32 addr, u8 value) { store_u8 (addr, cast(value)); } + + void fault(u32 addr, u32 size) const; + + private: + std::atomic* m_words; + u64 m_wordCount; + u64 m_size; + }; + } +}