commit 43d0ac96301d45e13871a6e7f13723ff73855c95 Author: modeco80 Date: Sun Jul 16 01:58:32 2023 -0400 init diff --git a/.clang-format b/.clang-format new file mode 100755 index 0000000..011a13d --- /dev/null +++ b/.clang-format @@ -0,0 +1,44 @@ +# Clang-Format file + +BasedOnStyle: Google + +# force T* or T& +DerivePointerAlignment: false +PointerAlignment: Left + +TabWidth: 4 +IndentWidth: 4 +UseTab: Always +IndentPPDirectives: BeforeHash + +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never + +BinPackArguments: true +BinPackParameters: true +BreakConstructorInitializers: BeforeColon +BreakStringLiterals: false + +ColumnLimit: 100 +CompactNamespaces: false + +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ContinuationIndentWidth: 0 + +# turning this on causes major issues with initalizer lists +Cpp11BracedListStyle: false +SpaceBeforeCpp11BracedList: true + +FixNamespaceComments: true + +NamespaceIndentation: All +ReflowComments: true + +SortIncludes: CaseInsensitive +SortUsingDeclarations: true + +SpacesInSquareBrackets: false +SpaceBeforeParens: Never +SpacesBeforeTrailingComments: 1 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..161254b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 + +# yaml sucks +# what else is new +[yml] +indent_style = space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb8fec7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +lua/bin +native/riscv/ref + +.cache/ +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..52c5d2c --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# LCPU + +LCPU is an alternative CPU core addon for GMod/Wiremod. + +It provides: + +- A standard RISC-V architechure (rv32ima) CPU core +- Extensive interoperation with the Wiremod addon +- A text editor and assembler suite based on LLVM (for writing bare-metal LCPU programs in assembly) + +# Building + +TODO: Steps to build the LCPU native module on Windows and Linux diff --git a/ideas.md b/ideas.md new file mode 100644 index 0000000..e18ebb2 --- /dev/null +++ b/ideas.md @@ -0,0 +1,19 @@ +- cpu cores + - controllable paramaters (ram size, ...) + - admin controlled per-user max RAM size (default 16mb) + - possibly override for "respectful" users and admins? + - admin controlled per-user max lcpu chip count (default 4) + - admins can override? + - admin controled global (affects all placed lcpus) scheduler timeslice (? cpus wouldn't block source anyways, the only way they could is if they start posting too many cpu thread -> lua thread callbacks ?) + - upload a raw binary (or elf? we could probably just imitate a "boot loader" and throw it into memory and move the given register to it?) to execute + - or write code as a "lcpu project." in theory exporting out to a Makefile/etc should be possible + - text editor for lcpu projects + + - supported architechures (iniitally. more could be added and a "common" abstraction framework for devices could be agreed on) + - riscv rv32-ima cpu core + - llvm assembler/maybe clang integration? (this might make the addon unwieldly though) + +- wiremod interopability (to obsolete ZCPU at least for my purposes) + - gpio (which uses normal wire stuff) + - console screen + - gpu (if it's not painful to do so)? diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt new file mode 100644 index 0000000..34e4ea3 --- /dev/null +++ b/native/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.15) +project(lcpu-native + DESCRIPTION "Superproject for LCPU GMOD Native Module" +) + +# RISC-V emulation library +add_subdirectory(riscv) + +# Server-side emulation/assembler library +#add_subdirectory(gmsv_lcpu) diff --git a/native/gmsv_lcpu/CMakeLists.txt b/native/gmsv_lcpu/CMakeLists.txt new file mode 100644 index 0000000..31135de --- /dev/null +++ b/native/gmsv_lcpu/CMakeLists.txt @@ -0,0 +1,3 @@ + + +add_library() diff --git a/native/gmsv_lcpu/NOTES.md b/native/gmsv_lcpu/NOTES.md new file mode 100644 index 0000000..4b436a9 --- /dev/null +++ b/native/gmsv_lcpu/NOTES.md @@ -0,0 +1,4 @@ +# Notes + +- gmsv_lcpu doesn't use the upstream cmake buildsystem for gmod lua headers (instead crafting our own) + - this is because, in facepunchs infinite wisdom, they decided to unconditionally build the examples. diff --git a/native/gmsv_lcpu/abi.ver b/native/gmsv_lcpu/abi.ver new file mode 100644 index 0000000..25d1cc0 --- /dev/null +++ b/native/gmsv_lcpu/abi.ver @@ -0,0 +1,7 @@ +{ +global: + gmod13_open; + gmod13_close; + +local: *; +}; diff --git a/native/riscv/CMakeLists.txt b/native/riscv/CMakeLists.txt new file mode 100644 index 0000000..ca0787a --- /dev/null +++ b/native/riscv/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.15) + +project(riscv_emu + DESCRIPTION "rv32-ima emulation library" + LANGUAGES CXX +) + +add_library(riscv + src/Bus.cpp +) + +target_compile_features(riscv PUBLIC cxx_std_20) +target_include_directories(riscv PUBLIC ${PROJECT_SOURCE_DIR}/include) + +add_library(riscv::riscv ALIAS riscv) diff --git a/native/riscv/README.md b/native/riscv/README.md new file mode 100644 index 0000000..e47b0f6 --- /dev/null +++ b/native/riscv/README.md @@ -0,0 +1,11 @@ +# riscv + +This is the RISC-V emulation core that LCPU uses in its native emulation module. + +This is based off [cnlohr/mini-rv32ima](https://github.com/cnlohr/mini-rv32ima), but: + +- Rewritten in C++20 (because I like sanity) +- Cleaned up somewhat +- Moved *ALL* device and MMIO code to seperate interfaces + - Re-implemented the timer device and the UART as said oop interface + - Lua devices use a wrapper which can contain lua callbacks diff --git a/native/riscv/include/riscv/Bus.hpp b/native/riscv/include/riscv/Bus.hpp new file mode 100644 index 0000000..6fdf6c4 --- /dev/null +++ b/native/riscv/include/riscv/Bus.hpp @@ -0,0 +1,92 @@ +#include + +#include +#include + + +namespace riscv { + + struct CPU; + + /// An address bus. + struct Bus { + + struct Device { + virtual ~Device() = default; + + // How many bytes does this device occupy of address space? + virtual AddressT Size() const = 0; + + // Used to allow bus devices to know when they are attached to a memory bus, + // and ultimately, an instance of a System + virtual void Attached(Bus* memoryBus, AddressT baseAddress) = 0; + virtual AddressT BaseAddress() const = 0; // TODO(cleanup): Make this non-virtual? + + + /// Is this device clocked? + virtual bool Clocked() const { return false; } + + /// This function is called during clocks to give clocked devices + /// the ability to update + virtual void Clock() {} + + // TODO(feat): Need to implement ability to generate interrupts + // from devices. This needs to be implemented to facilitate the + // implementation of the timer device as an actual Device implmentation + // instead of poorly hard-coding it into the CPU core logic. + + // Peek() -> reads a value from this device. + virtual u8 PeekByte(AddressT offset) = 0; + virtual u16 PeekShort(AddressT offset) = 0; + virtual u32 PeekWord(AddressT offset) = 0; + + /// Poke() -> Writes a value to this device's space in memory + virtual void PokeByte(AddressT offset, u8 value) = 0; + virtual void PokeShort(AddressT offset, u16 value) = 0; + virtual void PokeWord(AddressT offset, u32 value) = 0; + + }; + + + /// Attach a device to the bus. + /// + /// # Returns + /// This function returns true if the device was able to be put on the bus. + /// This function returns false in the following error cases: + /// - [device] is a null pointer + /// - The provided base address overlaps a already attached device in some way + bool AttachDevice(AddressT baseAddress, Device* device); + + /// Clock all clocked devices. + void Clock(); + + // + u8 PeekByte(AddressT address); + u16 PeekShort(AddressT address); + u32 PeekWord(AddressT address); + + void PokeByte(AddressT address, u8 value); + void PokeShort(AddressT address, u16 value); + void PokeWord(AddressT address, u32 value); + private: + + OptionalRef FindDeviceForAddress(AddressT address) const; + + CPU* attachedCpu{}; + + // TODO: if this ends up being a hotpath replace with robinhood unordered map + std::unordered_map mapped_devices; + + // this is essentially a flat map + // we don't particularly need hashing :p + //struct AttachedDevice { + // AddressT baseAddress{}; + // Device* device{}; + //}; + + //std::vector mapped_devices{}; + }; + + + +} diff --git a/native/riscv/include/riscv/CPU.hpp b/native/riscv/include/riscv/CPU.hpp new file mode 100644 index 0000000..87dbba9 --- /dev/null +++ b/native/riscv/include/riscv/CPU.hpp @@ -0,0 +1,11 @@ + + +namespace riscv { + + /** The CPU core. There will be one of these in a [System]. */ + struct CPU { + + + }; + +} diff --git a/native/riscv/include/riscv/Types.hpp b/native/riscv/include/riscv/Types.hpp new file mode 100644 index 0000000..8e2a2b4 --- /dev/null +++ b/native/riscv/include/riscv/Types.hpp @@ -0,0 +1,89 @@ +//! Common types +#pragma once + +#include +#include +#include + +namespace riscv { + + using u8 = std::uint8_t; + using s8 = std::int8_t; + using u16 = std::uint16_t; + using s16 = std::int16_t; + using u32 = std::uint32_t; + using s32 = std::int32_t; + using u64 = std::uint64_t; + using s64 = std::int64_t; + using usize = std::size_t; + using ssize = std::intptr_t; + + /// A type that can repressent address space or address space offsets. + using AddressT = u32; + + namespace detail { + + struct Nullref_t {}; + + template + struct OptionalRef; // sfinae on non-reference types + + /// Like std::optional, but for references to objects. + /// + /// Additionally, as a bonus, since the repressentation is so simple, this is + /// constexpr-safe by default, so it can be relatively well optimized. + /// + /// # Safety + /// Treat this class like [std::reference_wrapper]. + /// Do *not* give this class invalid references. + template + struct OptionalRef { + constexpr OptionalRef() : ptr(nullptr) { + } + + // trigger explicit null construction + constexpr OptionalRef(Nullref_t) : OptionalRef() { + } + + constexpr OptionalRef(T& ref) : ptr(&ref) { + } + + // polymorphic downconstruction from another OptionalRef + template + constexpr OptionalRef(const OptionalRef& other) : ptr(&other.ptr) { + } + + constexpr T& Value() const { + assert(HasValue() && "Attempt to access OptionalRef without stored value!"); + return *ptr; + } + + constexpr bool HasValue() const { + return ptr != nullptr; + } + + constexpr T& operator*() const { + return Value(); + } + + // unchecked access: DO NOT use this without checking beforehand + constexpr T* operator->() const { + return ptr; + } + + constexpr operator bool() const { + return HasValue(); + } + + private: + T* ptr {}; + }; + + /// Sentinel value to explicitly not populate an OptionalRef. + inline static Nullref_t NullRef {}; + } // namespace detail + + using detail::NullRef; + using detail::OptionalRef; + +} // namespace riscv diff --git a/native/riscv/src/Bus.cpp b/native/riscv/src/Bus.cpp new file mode 100644 index 0000000..ad41afa --- /dev/null +++ b/native/riscv/src/Bus.cpp @@ -0,0 +1,95 @@ +#include + +#include + +namespace riscv { + + bool Bus::AttachDevice(AddressT baseAddress, Device* device) { + if(!device) + return false; + + // Refuse to overlap a device at its base address.. + if(FindDeviceForAddress(baseAddress)) + return false; + // ... or have the end overlap the start of another device. + else if(FindDeviceForAddress(baseAddress + device->Size())) + return false; + + //mapped_devices.push_back({ .baseAddress = baseAddress, .device = device }); + mapped_devices[baseAddress] = device; + + return true; + } + + void Bus::Clock() { + for(auto& device : mapped_devices) { + if(device.second->Clocked()) + device.second->Clock(); + } + } + + u8 Bus::PeekByte(AddressT address) { + if(auto opt = FindDeviceForAddress(address); opt) + return opt->PeekByte(address - opt->BaseAddress()); + return -1; + } + + u16 Bus::PeekShort(AddressT address) { + if(auto opt = FindDeviceForAddress(address); opt) + return opt->PeekShort(address - opt->BaseAddress()); + return -1; + } + + u32 Bus::PeekWord(AddressT address) { + if(auto opt = FindDeviceForAddress(address); opt) + return opt->PeekWord(address - opt->BaseAddress()); + return -1; + } + + void Bus::PokeByte(AddressT address, u8 value) { + if(auto opt = FindDeviceForAddress(address); opt) + return opt->PokeByte(address - opt->BaseAddress(), value); + } + + void Bus::PokeShort(AddressT address, u16 value) { + if(auto opt = FindDeviceForAddress(address); opt) + return opt->PokeShort(address - opt->BaseAddress(), value); + } + + void Bus::PokeWord(AddressT address, u32 value) { + if(auto opt = FindDeviceForAddress(address); opt) + return opt->PokeWord(address - opt->BaseAddress(), value); + } + + OptionalRef Bus::FindDeviceForAddress(AddressT address) const { + /* + for(auto& device : mapped_devices) { + // If the requested address directly matches the base address of a device + // mapped into memory, then we do not even need to consider checking the layout. + if(device.baseAddress == address) + return *device.device; + + // Otherwise, we *do* unfortunately have to do so. + if(address >= device.baseAddress && + address < device.baseAddress + device.device->Size()) + return *device.device; + }*/ + + auto it = std::find_if(mapped_devices.begin(), mapped_devices.end(), [&](const auto& pair) { + return + // We can shorcut the region checking if the requested addess matches base address. + pair.first == address || + // If it doesn't we really can't, though. + (address >= pair.first && + address < pair.first + pair.second->Size()); + }); + + + // No device was found at this address + if(it == mapped_devices.end()) + return NullRef; + else + return *it->second; + } + +} // namespace riscv diff --git a/native/riscv/src/MemoryDevice.cpp b/native/riscv/src/MemoryDevice.cpp new file mode 100644 index 0000000..c463f68 --- /dev/null +++ b/native/riscv/src/MemoryDevice.cpp @@ -0,0 +1,98 @@ +#include +#include "riscv/Types.hpp" + +#include + +namespace riscv { + + namespace { + + + template + struct BasicMemoryDevice : public Bus::Device { + BasicMemoryDevice(usize size) + : memorySize(size) { + memory = new u8[size]; + // TODO(feat): we should have a global panic system which is hooked in + // so that we don't just blindly crash everything + assert(memory && "Out of host memory"); + } + + virtual ~BasicMemoryDevice() { + if(memory) + delete[] memory; + } + + // Implementation of Device interface + + void Attached(Bus* bus, AddressT base) override { + attachedBus = bus; + baseAddress = base; + } + + AddressT BaseAddress() const override { + return baseAddress; + } + + u8 PeekByte(AddressT offset) override { + return memory[offset % memorySize]; + } + + u16 PeekShort(AddressT offset) override { + return std::bit_cast(memory)[OffsetToIndex(offset)]; + } + + u32 PeekWord(AddressT offset) override { + return std::bit_cast(memory)[OffsetToIndex(offset)]; + } + + void PokeByte(AddressT offset, u8 value) override { + if constexpr(!Rom) { + memory[offset % memorySize] = value; + } else { + // TODO: trap here + } + } + + void PokeShort(AddressT offset, u16 value) override { + if constexpr(!Rom) { + std::bit_cast(memory)[OffsetToIndex(offset)] = value; + } else { + // TODO: trap here + } + } + + void PokeWord(AddressT offset, u32 value) override { + if constexpr(!Rom) { + std::bit_cast(memory)[OffsetToIndex(offset)] = value; + } else { + // TODO: trap here + } + } + + private: + + + /// helper used for implementing stuff + template + constexpr usize OffsetToIndex(AddressT offset) { + return (offset % memorySize) / sizeof(T); + } + + // remember what we were attached to via "signal" + Bus* attachedBus{}; + AddressT baseAddress{}; + + u8* memory{}; + usize memorySize{}; + }; + + using RamDevice = BasicMemoryDevice; + using RomDevice = BasicMemoryDevice; + + } + + + //Bus::Device* NewRam() + +}