init
This commit is contained in:
commit
43d0ac9630
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
build/
|
||||||
|
lua/bin
|
||||||
|
native/riscv/ref
|
||||||
|
|
||||||
|
.cache/
|
||||||
|
.vscode/
|
|
@ -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
|
|
@ -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)?
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
add_library()
|
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
global:
|
||||||
|
gmod13_open;
|
||||||
|
gmod13_close;
|
||||||
|
|
||||||
|
local: *;
|
||||||
|
};
|
|
@ -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)
|
|
@ -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
|
|
@ -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{};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
|
||||||
|
namespace riscv {
|
||||||
|
|
||||||
|
/** The CPU core. There will be one of these in a [System]. */
|
||||||
|
struct CPU {
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue