This commit is contained in:
Lily Tsuru 2023-07-16 01:58:32 -04:00
commit 43d0ac9630
16 changed files with 529 additions and 0 deletions

44
.clang-format Executable file
View File

@ -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

12
.editorconfig Normal file
View File

@ -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

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
build/
lua/bin
native/riscv/ref
.cache/
.vscode/

13
README.md Normal file
View File

@ -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

19
ideas.md Normal file
View File

@ -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)?

10
native/CMakeLists.txt Normal file
View File

@ -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)

View File

@ -0,0 +1,3 @@
add_library()

View File

@ -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.

7
native/gmsv_lcpu/abi.ver Normal file
View File

@ -0,0 +1,7 @@
{
global:
gmod13_open;
gmod13_close;
local: *;
};

View File

@ -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)

11
native/riscv/README.md Normal file
View File

@ -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

View File

@ -0,0 +1,92 @@
#include <riscv/Types.hpp>
#include <vector>
#include <unordered_map>
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<Device&> FindDeviceForAddress(AddressT address) const;
CPU* attachedCpu{};
// TODO: if this ends up being a hotpath replace with robinhood unordered map
std::unordered_map<AddressT, Device*> mapped_devices;
// this is essentially a flat map
// we don't particularly need hashing :p
//struct AttachedDevice {
// AddressT baseAddress{};
// Device* device{};
//};
//std::vector<AttachedDevice> mapped_devices{};
};
}

View File

@ -0,0 +1,11 @@
namespace riscv {
/** The CPU core. There will be one of these in a [System]. */
struct CPU {
};
}

View File

@ -0,0 +1,89 @@
//! Common types
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdint>
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 <class T>
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 <class T>
struct OptionalRef<T&> {
constexpr OptionalRef() : ptr(nullptr) {
}
// trigger explicit null construction
constexpr OptionalRef(Nullref_t) : OptionalRef() {
}
constexpr OptionalRef(T& ref) : ptr(&ref) {
}
// polymorphic downconstruction from another OptionalRef<U>
template <class U>
constexpr OptionalRef(const OptionalRef<U&>& 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

95
native/riscv/src/Bus.cpp Normal file
View File

@ -0,0 +1,95 @@
#include <riscv/Bus.hpp>
#include <algorithm>
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::Device&> 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

View File

@ -0,0 +1,98 @@
#include <riscv/Bus.hpp>
#include "riscv/Types.hpp"
#include <span>
namespace riscv {
namespace {
template<bool Rom>
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<u16*>(memory)[OffsetToIndex<u16>(offset)];
}
u32 PeekWord(AddressT offset) override {
return std::bit_cast<u32*>(memory)[OffsetToIndex<u32>(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<u16*>(memory)[OffsetToIndex<u16>(offset)] = value;
} else {
// TODO: trap here
}
}
void PokeWord(AddressT offset, u32 value) override {
if constexpr(!Rom) {
std::bit_cast<u32*>(memory)[OffsetToIndex<u32>(offset)] = value;
} else {
// TODO: trap here
}
}
private:
/// helper used for implementing stuff
template<class T>
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<false>;
using RomDevice = BasicMemoryDevice<true>;
}
//Bus::Device* NewRam()
}