Added first draft of RAM class

This commit is contained in:
OmniaX-Dev 2026-05-18 13:42:41 +02:00
parent 0073ef91f9
commit 8b94e8f55c
8 changed files with 687 additions and 13 deletions

View file

@ -18,9 +18,9 @@
"reveal": "always", "reveal": "always",
}, },
{ {
"label": "build dbg", "label": "build gdb",
"command": "./build", "command": "./build",
"args": ["dbg"], "args": ["gdb"],
"cwd": "$ZED_WORKTREE_ROOT", "cwd": "$ZED_WORKTREE_ROOT",
"use_new_terminal": false, "use_new_terminal": false,
"allow_concurrent_runs": false, "allow_concurrent_runs": false,

View file

@ -31,12 +31,14 @@ list(APPEND INCLUDE_DIRS
list(APPEND RUNTIME_SOURCE_FILES list(APPEND RUNTIME_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/src/runtime/runtime_main.cpp ${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 list(APPEND DEBUGGER_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/src/debugger/debugger_main.cpp ${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 list(APPEND ASSEMBLER_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/src/assembler/assembler_main.cpp ${CMAKE_CURRENT_LIST_DIR}/src/assembler/assembler_main.cpp
@ -44,6 +46,10 @@ list(APPEND ASSEMBLER_SOURCE_FILES
list(APPEND TOOLS_SOURCE_FILES list(APPEND TOOLS_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/src/tools/tools_main.cpp ${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 #Targets
@ -51,7 +57,7 @@ list(APPEND TOOLS_SOURCE_FILES
set(RUNTIME_TARGET dvm) set(RUNTIME_TARGET dvm)
add_executable(${RUNTIME_TARGET} ${RUNTIME_SOURCE_FILES}) add_executable(${RUNTIME_TARGET} ${RUNTIME_SOURCE_FILES})
if(WIN32) 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() endif()
target_include_directories(${RUNTIME_TARGET} PUBLIC ${INCLUDE_DIRS}) target_include_directories(${RUNTIME_TARGET} PUBLIC ${INCLUDE_DIRS})
@ -59,7 +65,7 @@ target_include_directories(${RUNTIME_TARGET} PUBLIC ${INCLUDE_DIRS})
set(DEBUGGER_TARGET ddb) set(DEBUGGER_TARGET ddb)
add_executable(${DEBUGGER_TARGET} ${DEBUGGER_SOURCE_FILES}) add_executable(${DEBUGGER_TARGET} ${DEBUGGER_SOURCE_FILES})
if(WIN32) 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() endif()
target_include_directories(${DEBUGGER_TARGET} PUBLIC ${INCLUDE_DIRS}) target_include_directories(${DEBUGGER_TARGET} PUBLIC ${INCLUDE_DIRS})
@ -67,7 +73,7 @@ target_include_directories(${DEBUGGER_TARGET} PUBLIC ${INCLUDE_DIRS})
set(ASSEMBLER_TARGET dasm) set(ASSEMBLER_TARGET dasm)
add_executable(${ASSEMBLER_TARGET} ${ASSEMBLER_SOURCE_FILES}) add_executable(${ASSEMBLER_TARGET} ${ASSEMBLER_SOURCE_FILES})
if(WIN32) 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() endif()
target_include_directories(${ASSEMBLER_TARGET} PUBLIC ${INCLUDE_DIRS}) target_include_directories(${ASSEMBLER_TARGET} PUBLIC ${INCLUDE_DIRS})
@ -75,14 +81,23 @@ target_include_directories(${ASSEMBLER_TARGET} PUBLIC ${INCLUDE_DIRS})
set(TOOLS_TARGET dtools) set(TOOLS_TARGET dtools)
add_executable(${TOOLS_TARGET} ${TOOLS_SOURCE_FILES}) add_executable(${TOOLS_TARGET} ${TOOLS_SOURCE_FILES})
if(WIN32) 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() endif()
target_include_directories(${TOOLS_TARGET} PUBLIC ${INCLUDE_DIRS}) 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(${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(${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(${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(${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) if (WIN32)
set(MSYS2_UCRT64 "C:/msys64/ucrt64") 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"
"${MSYS2_UCRT64}/include/c++/13.2.0/x86_64-w64-mingw32" "${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() 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(${DEBUGGER_TARGET} PUBLIC BUILD_CONFIG_DEBUG OX_DEBUG_BUILD)
target_compile_definitions(${ASSEMBLER_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(${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) add_compile_options(-O3 -MMD -MP -Wall -ggdb)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release") elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
target_compile_definitions(${RUNTIME_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD) 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(${DEBUGGER_TARGET} PUBLIC BUILD_CONFIG_RELEASE OX_RELEASE_BUILD)
target_compile_definitions(${ASSEMBLER_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(${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) add_compile_options(-MMD -MP -Wall)
endif() endif()
@ -148,6 +171,7 @@ if (WIN32)
target_link_libraries(${ASSEMBLER_TARGET} mingw32) target_link_libraries(${ASSEMBLER_TARGET} mingw32)
target_link_libraries(${TOOLS_TARGET} mingw32) target_link_libraries(${TOOLS_TARGET} mingw32)
target_link_libraries(${DEBUGGER_TARGET} mingw32 SDL3 SDL3_ttf SDL3_image) target_link_libraries(${DEBUGGER_TARGET} mingw32 SDL3 SDL3_ttf SDL3_image)
target_link_libraries(${TEST_TARGET} mingw32)
endif (WIN32) endif (WIN32)
if (APPLE) if (APPLE)
@ -157,6 +181,7 @@ if (APPLE)
target_link_libraries(${ASSEMBLER_TARGET} Boost::regex) target_link_libraries(${ASSEMBLER_TARGET} Boost::regex)
target_link_libraries(${TOOLS_TARGET} Boost::regex) target_link_libraries(${TOOLS_TARGET} Boost::regex)
target_link_libraries(${DEBUGGER_TARGET} Boost::regex) target_link_libraries(${DEBUGGER_TARGET} Boost::regex)
target_link_libraries(${TEST_TARGET} Boost::regex)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL3 REQUIRED sdl3) pkg_check_modules(SDL3 REQUIRED sdl3)
pkg_check_modules(SDL3_IMAGE REQUIRED sdl3-image) 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(${ASSEMBLER_TARGET} xcb xcb-randr boost_regex)
target_link_libraries(${TOOLS_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(${DEBUGGER_TARGET} xcb xcb-randr boost_regex SDL3 SDL3_image)
target_link_libraries(${TEST_TARGET} xcb xcb-randr boost_regex)
endif () endif ()
target_link_libraries(${RUNTIME_TARGET} ostd ogfx) target_link_libraries(${RUNTIME_TARGET} ostd ogfx)
target_link_libraries(${DEBUGGER_TARGET} ostd ogfx) target_link_libraries(${DEBUGGER_TARGET} ostd ogfx)
target_link_libraries(${ASSEMBLER_TARGET} ostd) target_link_libraries(${ASSEMBLER_TARGET} ostd)
target_link_libraries(${TOOLS_TARGET} ostd) target_link_libraries(${TOOLS_TARGET} ostd)
target_link_libraries(${TEST_TARGET} ostd)
#----------------------------------------------------------------------------------------- #-----------------------------------------------------------------------------------------

View file

@ -1 +1 @@
7 9

422
src/RAM_test.cpp Normal file
View file

@ -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 <atomic>
#include <chrono>
#include <cstdio>
#include <iostream>
#include <thread>
#include <vector>
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<std::chrono::milliseconds>(__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<u8>(a ^ 0x5A));
for (u32 a = 0; a < 256; ++a) CHECK_EQ(ram.load_u8(a), static_cast<u8>(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<u16>(0xBEEF ^ a));
for (u32 a = 0; a < 256; a += 2) CHECK_EQ(ram.load_u16(a), static_cast<u16>(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<std::thread> threads;
threads.reserve(kThreads);
std::atomic<int> ready { 0 };
std::atomic<bool> 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<u32>(t));
u32 v = ram.load_u32(0);
// The only valid values are 0..kThreads-1
CHECK(v < static_cast<u32>(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<u32>(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<std::thread> threads;
std::atomic<int> ready { 0 };
std::atomic<bool> 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<u8>(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<u8>(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<std::thread> threads;
std::atomic<int> ready { 0 };
std::atomic<bool> 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<u8>((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<int>(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<u8>((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<int> ready { 0 };
std::atomic<bool> go { false };
std::vector<std::thread> 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<int> ready { 0 };
std::atomic<bool> go { false };
std::atomic<bool> stop { false };
std::vector<std::thread> 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<int> 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;
}

View file

@ -18,13 +18,13 @@
along with DragonV2. If not, see <https://www.gnu.org/licenses/>. along with DragonV2. If not, see <https://www.gnu.org/licenses/>.
*/ */
#include "VirtualCPU.hpp" #include "CPU.hpp"
namespace dragon namespace dragon
{ {
namespace hw namespace hw
{ {
VirtualCPU::VirtualCPU(void) CPU::CPU(void)
{ {
} }
} }

View file

@ -26,10 +26,10 @@ namespace dragon
{ {
namespace hw namespace hw
{ {
class VirtualCPU class CPU
{ {
public: public:
VirtualCPU(void); CPU(void);
private: private:
}; };

159
src/hardware/RAM.cpp Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "RAM.hpp"
#include <iostream>
#include <ostd/string/String.hpp>
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<u64>[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<u32>(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<u16>(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<u8>(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<u64>& 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<u64>& 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<u64>& 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 */
}
}
}

66
src/hardware/RAM.hpp Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <ostd/data/Types.hpp>
#include <atomic>
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<i64>(load_u64(addr)); }
inline i32 load_i32(u32 addr) const { return cast<i32>(load_u32(addr)); }
inline i16 load_i16(u32 addr) const { return cast<i16>(load_u16(addr)); }
inline i8 load_i8 (u32 addr) const { return cast<i8 >(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<i64>(value)); }
inline void store_i32(u32 addr, u32 value) { store_u32(addr, cast<i32>(value)); }
inline void store_i16(u32 addr, u16 value) { store_u16(addr, cast<i16>(value)); }
inline void store_i8 (u32 addr, u8 value) { store_u8 (addr, cast<i8 >(value)); }
void fault(u32 addr, u32 size) const;
private:
std::atomic<u64>* m_words;
u64 m_wordCount;
u64 m_size;
};
}
}