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