a decently improved architecture

This commit is contained in:
Aubrey 2025-03-23 20:33:38 -06:00
parent 5c87063c48
commit 6d238b24c1
No known key found for this signature in database
30 changed files with 2061 additions and 924 deletions

3
.envrc
View file

@ -1 +1,2 @@
use flake
use flake
TMPDIR=/tmp

1115
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

7
Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[workspace]
resolver = "3"
members = ["rust"]
[profile.release]
panic = "abort"
debug-assertions = true

View file

@ -2,114 +2,126 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "2.1.10"
id("fabric-loom") version "1.9-SNAPSHOT"
id("maven-publish")
kotlin("jvm") version "2.1.10"
id("fabric-loom") version "1.9-SNAPSHOT"
id("maven-publish")
id("com.google.protobuf") version "0.9.4"
}
version = project.property("mod_version") as String
group = project.property("maven_group") as String
base {
archivesName.set(project.property("archives_base_name") as String)
archivesName.set(project.property("archives_base_name") as String)
}
val targetJavaVersion = 21
java {
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
// if it is present.
// If you remove this line, sources will not be generated.
withSourcesJar()
}
loom {
mods {
register("golemcomputers") {
sourceSet("main")
}
mods {
register("golemcomputers") {
sourceSet("main")
}
}
}
repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
// for more information about repositories.
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.30.0"
}
generateProtoTasks {
file("./proto/all.proto")
}
}
configurations {
create("rustlib") {
isCanBeConsumed = false
isCanBeResolved = true
}
create("rustlib") {
isCanBeConsumed = false
isCanBeResolved = true
}
}
dependencies {
// To change the versions see the gradle.properties file
minecraft("com.mojang:minecraft:${project.property("minecraft_version")}")
mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2")
modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}")
modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}")
// To change the versions see the gradle.properties file
minecraft("com.mojang:minecraft:${project.property("minecraft_version")}")
mappings("net.fabricmc:yarn:${project.property("yarn_mappings")}:v2")
modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}")
modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}")
modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}")
add("rustlib", project(":rust"))
implementation("com.github.jhg023:Pbbl:1.0.2")
modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}")
add("rustlib", project(":rust"))
implementation("com.github.jhg023:Pbbl:1.0.2")
implementation("com.google.protobuf:protobuf-java:4.30.0")
protobuf(files("proto/"))
}
tasks.processResources {
inputs.property("version", project.version)
inputs.property("minecraft_version", project.property("minecraft_version"))
inputs.property("loader_version", project.property("loader_version"))
filteringCharset = "UTF-8"
inputs.property("version", project.version)
inputs.property("minecraft_version", project.property("minecraft_version"))
inputs.property("loader_version", project.property("loader_version"))
filteringCharset = "UTF-8"
from(configurations.getByName("rustlib"))
from(configurations.getByName("rustlib"))
filesMatching("fabric.mod.json") {
expand(
"version" to project.version,
"minecraft_version" to project.property("minecraft_version"),
"loader_version" to project.property("loader_version"),
"kotlin_loader_version" to project.property("kotlin_loader_version")
)
}
filesMatching("fabric.mod.json") {
expand(
"version" to project.version,
"minecraft_version" to project.property("minecraft_version"),
"loader_version" to project.property("loader_version"),
"kotlin_loader_version" to project.property("kotlin_loader_version")
)
}
}
tasks.withType<JavaCompile>().configureEach {
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
// If Javadoc is generated, this must be specified in that task too.
options.encoding = "UTF-8"
options.release.set(targetJavaVersion)
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
// If Javadoc is generated, this must be specified in that task too.
options.encoding = "UTF-8"
options.release.set(targetJavaVersion)
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.fromTarget(targetJavaVersion.toString()))
compilerOptions.jvmTarget.set(JvmTarget.fromTarget(targetJavaVersion.toString()))
}
tasks.jar {
from("LICENSE") {
rename { "${it}_${project.base.archivesName}" }
}
from("LICENSE") {
rename { "${it}_${project.base.archivesName}" }
}
}
// configure the maven publication
publishing {
publications {
create<MavenPublication>("mavenJava") {
artifactId = project.property("archives_base_name") as String
from(components["java"])
}
publications {
create<MavenPublication>("mavenJava") {
artifactId = project.property("archives_base_name") as String
from(components["java"])
}
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
// See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
repositories {
// Add repositories to publish to here.
// Notice: This block does NOT have the same function as the block in the top level.
// The repositories here will be used for publishing your artifact, not for
// retrieving dependencies.
}
}

View file

@ -32,6 +32,7 @@
cmake
ninja
pkg-config
protobuf
#needed for minecraft to function properly
libGL

28
proto/all.proto Normal file
View file

@ -0,0 +1,28 @@
syntax = "proto3";
package computer;
option java_package = "ca.sanae.golemcomputers.computer";
option java_outer_classname = "NativeProtobuf";
message ComputerInit {
repeated ComponentInit components = 1;
uint32 primary_ram_pages = 2;
sint32 primary_rom_index = 3;
}
message ComponentInit {
oneof data {
CoreComponentInit core = 1;
ChatComponentInit chat = 2;
}
}
message CoreComponentInit {
uint32 instructions_per_tick = 1;
}
message ChatComponentInit {
sint32 java_index = 1;
}

View file

@ -10,15 +10,16 @@ crate-type = ["cdylib", "rlib"]
bitfield-struct = "0.10.1"
disarm64 = "0.1.24"
jni = "0.21.1"
nodit = "0.9.2"
num-derive = "0.4.2"
num-traits = "0.2.19"
oneshot = "0.1.10"
pollster = "0.4.0"
prost = "0.13.5"
smol = "2.0.2"
static_assertions = "1.1.0"
unicorn-engine = { version = "2.1.1", default-features = false, features = ["arch_aarch64"], git = "https://github.com/Sanae6/unicorn/", branch = "cpreg" }
zerocopy = { version = "0.8.20", features = ["derive"] }
[profile.release]
panic = "abort"
debug-assertions = true
[build-dependencies]
prost-build = "0.13.5"

View file

@ -1,20 +1,18 @@
val task = tasks.register<Exec>("default") {
commandLine("cargo", "build", "--release")
val library = System.mapLibraryName("golem_computers")
val libraryPath = "$workingDir/target/release/$library"
val libraryPath = "$workingDir/../target/release/$library"
outputs.file(libraryPath)
inputs.dir("$workingDir/src")
doLast {
if (!file(libraryPath).exists()) {
throw GradleException("the library wasn't properly build! expected $libraryPath")
throw GradleException("the library wasn't properly built! expected $libraryPath")
}
}
}
configurations {
create("default") {
}
create("default")
}
artifacts.add("default", task)

6
rust/build.rs Normal file
View file

@ -0,0 +1,6 @@
use std::io::Result;
fn main() -> Result<()> {
prost_build::compile_protos(&["../proto/all.proto"], &["../proto"])?;
Ok(())
}

37
rust/examples/condvar.rs Normal file
View file

@ -0,0 +1,37 @@
use std::{
sync::{Arc, Condvar, Mutex},
thread,
time::Duration,
};
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair_clone1 = Arc::clone(&pair);
let pair_clone2 = Arc::clone(&pair);
thread::spawn(move || {
let (lock, cvar) = &*pair_clone1;
let mut started = lock.lock().unwrap();
*started = true;
std::thread::sleep(Duration::from_secs(3));
cvar.notify_all();
});
thread::spawn(move || {
let (lock, cvar) = &*pair_clone2;
let mut started = lock.lock().unwrap();
while !*started {
println!("waiting 2");
started = cvar.wait(started).unwrap();
}
println!("Thread 2 proceeding...");
});
let (lock, cvar) = &*pair;
let mut started = lock.lock().unwrap();
while !*started {
println!("waiting 1");
started = cvar.wait(started).unwrap();
}
println!("Thread 1 proceeding...");
}

View file

@ -1,251 +0,0 @@
use std::{
mem::offset_of,
ops::{Deref, DerefMut, Range},
pin::Pin,
sync::{atomic::Ordering, Arc},
};
use bitfield_struct::bitfield;
use jni::JavaVM;
use unicorn_engine::Unicorn;
use zerocopy::{
big_endian::U16 as U16BE,
little_endian::{U16, U32, U64},
FromBytes, Immutable, IntoBytes, KnownLayout,
};
use crate::{
align_up, component::{Component, ComponentInterface}, core::CpuContext, memory_map::RAM_START, overlapping, range_of_field, unsync_cell::UnsyncCell, unsync_read
};
use super::ComponentDma;
pub struct ComponentIoResult {
component_index: usize,
direction: ComponentIoDirection,
}
pub enum ComponentIoDirection {
ToComponent,
ToGuest,
}
impl Component {
pub fn perform_write<D>(
&self,
range: Range<usize>,
writer: impl FnOnce(&UnsyncCell<ComponentInterface>),
ram: Arc<UnsyncCell<[u8]>>,
java_vm: JavaVM,
) -> Option<Pin<Box<dyn Future<Output = ComponentIoResult>>>> {
let Self::Populated {
read_transfer_active,
write_transfer_active,
info,
interface,
own_index,
java_component,
..
} = self
else {
return None;
};
if overlapping(&range, &range_of_field!(ComponentInterface, info)) {
return None;
}
let read_range = range_of_field!(ComponentInterface, read_dma);
if overlapping(&range, &read_range) {
if read_transfer_active.load(Ordering::Acquire) || read_range != range {
return None;
}
writer(interface);
let dma: ComponentDma = unsync_read!(interface, ComponentInterface, read_dma);
if dma.dma_enabled()
&& read_transfer_active.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire).is_ok()
{
if !overlapping(
&(dma.address..dma.address + dma.size()),
&(RAM_START..RAM_START + ram.len() as u64),
) {
read_transfer_active.store(false, Ordering::Release);
return None;
}
let bytes_per_chunk: u32 = info.bytes_per_tick / 5;
let java_component =
let index = return Some(Box::pin(async move {
let java_component = java_component;
ComponentIoResult { component_index: 0, direction: todo!() }
}));
}
return None;
}
// let write_range = range_of_field!(ComponentInterface, write_dma);
// if write_transfer_active && overlapping(range.clone(), write_range) {
// return;
// }
return None;
}
}
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
struct ComponentMmioHeader {
component_count: U32,
/// version bump when component access method (offset table) changes or [ComponentInfo] is non-additively changed
version: U16,
control: U16BE,
}
#[bitfield(u16)]
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
pub struct ComponentMmioControl {
interrupt_added: bool,
interrupt_removed: bool,
#[bits(2)]
_reserved: U16,
/// read-write, indicates the core to fire interrupts on. indicating an invalid core means no core will receive the interrupt
#[bits(4)]
interrupt_firing_core: u8,
#[bits(8)]
_reserved: U16,
}
pub fn calculate_components_start(component_count: usize) -> usize {
OFFSETS_START + align_up(component_count as u64 * 4 + 16, 0x1000) as usize
}
fn read_bytes_to_u64(bytes: &[u8]) -> u64 {
match bytes.len() {
1 => bytes[0] as u64,
2 => U16::read_from_bytes(bytes).unwrap().get() as u64,
4 => U32::read_from_bytes(bytes).unwrap().get() as u64,
8 => U64::read_from_bytes(bytes).unwrap().get() as u64,
16 => todo!("128 bit reads are currently neither implemented here nor supported by unicorn hooks"),
_ => panic!("expected byte slice of primitive size"),
}
}
fn write_bytes_from_u64(bytes: &mut [u8], value: u64) {
match bytes.len() {
1 => bytes[0] = value.try_into().unwrap(),
2 => U16::new(value.try_into().unwrap()).write_to(bytes).unwrap(),
4 => U32::new(value.try_into().unwrap()).write_to(bytes).unwrap(),
8 => U64::new(value).write_to(bytes).unwrap(),
16 => todo!("128 bit writes are currently neither implemented here nor supported by unicorn hooks"),
_ => panic!("expected byte slice of primitive size"),
}
}
const OFFSETS_START: usize = 0x200;
const INTERFACE_INTERVAL: usize = 0x800;
pub fn component_mmio_read(vcpu: &mut Unicorn<CpuContext>, offset: u64, size: usize) -> u64 {
let offset = offset as usize;
let component_count = vcpu.get_data().components.len();
if offset < OFFSETS_START {
// mmio header
println!("reading from mmio start!");
let header = ComponentMmioHeader {
component_count: U32::new(component_count as u32),
version: U16::new(0),
control: U16BE::new(vcpu.get_data().component_control.read().into_bits()),
};
if offset > size_of::<ComponentMmioHeader>() {
return 0;
}
let mut region = [0; size_of::<ComponentMmioHeader>() + size_of::<u64>()];
header.write_to_prefix(&mut region).unwrap();
let range = offset as usize..(offset + size) as usize;
return read_bytes_to_u64(&region[range]);
}
if offset < vcpu.get_data().component_data_start {
// offsets
let range = offset as usize..(offset + size) as usize;
let components_range = OFFSETS_START..(size_of::<u32>() * component_count);
let range = range.start - components_range.start as usize..range.end - components_range.start as usize;
let first_offset_index = range.start >> 2;
let offset_buffer: [U32; 3] = std::array::from_fn(|index| {
(first_offset_index + index < component_count as usize)
.then_some(U32::new(
((first_offset_index + index) * INTERFACE_INTERVAL as usize) as u32,
))
.unwrap_or_default()
});
return read_bytes_to_u64(&offset_buffer.as_bytes()[range]);
}
// info region
let offset = offset - vcpu.get_data().component_data_start;
let index = offset / INTERFACE_INTERVAL as usize;
let offset = offset - (index * INTERFACE_INTERVAL as usize);
let Some(Component::Populated { interface, .. }) = vcpu.get_data().components.get(index) else {
return 0;
};
match size {
1 => interface.read_into::<u8>(offset).into(),
2 => interface.read_into::<U16>(offset).into(),
4 => interface.read_into::<U32>(offset).into(),
8 => interface.read_into::<U64>(offset).into(),
_ => unreachable!(),
}
}
pub fn component_mmio_write(vcpu: &mut Unicorn<CpuContext>, offset: u64, size: usize, value: u64) {
let offset = offset as usize;
let range = offset..offset + size;
if offset < OFFSETS_START {
if overlapping(
&range,
&(offset_of!(ComponentMmioHeader, control)..size_of::<ComponentMmioHeader>()),
) {
// control included
let mut buffer = [0; size_of::<ComponentMmioHeader>() + size_of::<u64>()];
write_bytes_from_u64(&mut buffer[range], value);
let header = ComponentMmioHeader::ref_from_prefix(&buffer).unwrap().0;
vcpu.get_data().component_control.write(ComponentMmioControl::from_bits(header.control.get()));
}
return;
}
if offset < vcpu.get_data().component_data_start {
return;
}
let offset = offset - vcpu.get_data().component_data_start;
let index = offset / INTERFACE_INTERVAL as usize;
let offset = offset - (index * INTERFACE_INTERVAL as usize);
let Some(component) = vcpu.get_data().components.get(index) else {
return;
};
if offset > size_of::<ComponentInterface>() {
return;
}
component.perform_write(
range,
|interface| match size {
1 => interface.write_from(offset, &(value as u8)),
2 => interface.write_from(offset, &U16::new(value as u16)),
4 => interface.write_from(offset, &U32::new(value as u32)),
8 => interface.write_from(offset, &U64::new(value as u64)),
_ => unreachable!(),
},
vcpu.get_data().ram,
);
}

View file

@ -1,120 +0,0 @@
pub mod mmio;
use std::{
cell::RefCell,
mem::offset_of,
ops::{Deref, DerefMut, Range, RangeBounds},
pin::Pin,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc, LazyLock, Mutex,
},
};
use bitfield_struct::bitfield;
use jni::objects::GlobalRef;
use unicorn_engine::Unicorn;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
use crate::{
core::CpuContext, memory_map::RAM_START, overlapping, range_of_field, subtract_range, unsync_cell::UnsyncCell,
unsync_read,
};
#[derive(Default)]
pub enum Component {
#[default]
Empty,
Populated {
own_index: usize,
java_component: GlobalRef,
read_transfer_active: AtomicBool,
write_transfer_active: AtomicBool,
info: ComponentInfo,
interface: UnsyncCell<ComponentInterface>,
},
}
#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct ComponentInterface {
// read only
pub info: ComponentInfo,
// read-write
control: ComponentControl,
_restart_device: u8,
_reserved: [u8; 6],
read_dma: ComponentDma,
write_dma: ComponentDma,
}
#[bitfield(u8)]
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
pub struct ComponentControl {
interrupt_when_added: bool,
interrupt_when_removed: bool,
#[bits(2)]
_reserved: u8,
#[bits(4)]
interrupt_firing_core: u8,
}
#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct ComponentDma {
pub upper_word: ComponentDmaUpper,
pub address: u64,
}
impl Deref for ComponentDma {
type Target = ComponentDmaUpper;
fn deref(&self) -> &Self::Target {
&self.upper_word
}
}
impl DerefMut for ComponentDma {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.upper_word
}
}
#[bitfield(u64)]
#[derive(FromBytes, IntoBytes, Immutable, KnownLayout)]
pub struct ComponentDmaUpper {
dma_enabled: bool,
#[bits(3)]
_reserved: u8,
/// read-write, indicates the core to fire interrupts on. indicating an invalid core means no core will receive the interrupt
#[bits(4)]
interrupt_firing_core: u8,
#[bits(56)]
size: u64,
}
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct ComponentInfo {
/// a null terminated string, the type name of the component
pub type_name: [u8; 256],
/// a null terminated string, the protocol identifier for how to talk to the component
pub protocol: [u8; 256],
pub bytes_per_tick: u32,
pub version: u32,
}
impl Default for ComponentInfo {
fn default() -> Self {
Self {
type_name: [0; 256],
protocol: [0; 256],
bytes_per_tick: 0,
version: 0,
}
}
}

View file

@ -0,0 +1,72 @@
use std::{mem::offset_of, sync::{mpsc, Arc}};
use bitfield_struct::bitfield;
use zerocopy::{
little_endian::{U32, U64},
FromBytes, IntoBytes, KnownLayout,
};
use crate::{component_name, core::CoreMessage, overlapping, unsync_cell::UnsyncCell};
use super::Component;
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout)]
#[repr(C)]
pub struct Registers {
pub reset_address: U64,
pub flags: U32,
}
#[bitfield(u32)]
pub struct Flags {
should_reset: bool,
#[bits(31)]
_reserved: u32,
}
pub struct CoreComponent {
registers: Arc<UnsyncCell<Registers>>,
message_sender: mpsc::Sender<CoreMessage>,
}
impl CoreComponent {
pub fn new(registers: Arc<UnsyncCell<Registers>>, message_sender: mpsc::Sender<CoreMessage>) -> Self {
Self {
registers,
message_sender,
}
}
}
impl Component for CoreComponent {
fn name() -> [u8; 0x30] {
component_name!(b"core")
}
fn memory_map_size(&self) -> u64 {
size_of::<Registers>() as u64
}
fn read(&self, _: &mut jni::JNIEnv, offset: u64, data: &mut [u8]) {
assert!((offset + data.len() as u64) < self.memory_map_size() as u64);
self.registers.read_into_byte_slice(offset as usize, data);
}
fn write(&self, _: &mut jni::JNIEnv, offset: u64, data: &[u8]) {
assert!((offset + data.len() as u64) < self.memory_map_size() as u64);
self.registers.write_from_byte_slice(offset as usize, data);
if overlapping(
&(offset..offset + data.len() as u64),
&(offset_of!(Registers, flags) as u64..size_of::<Registers>() as u64),
) {
let mut registers = self.registers.read();
let flags = Flags::from_bits(registers.flags.get());
if flags.should_reset() {
let _ = self.message_sender.send(CoreMessage::Reset);
registers.flags.set(flags.with_should_reset(false).into_bits());
self.registers.write(registers);
}
}
}
}

View file

@ -0,0 +1,50 @@
use std::sync::Mutex;
use jni::{
objects::{GlobalRef, JValueGen},
JNIEnv,
};
use crate::component_name;
use super::Component;
pub struct DebugChat {
buffer: Mutex<String>,
object: GlobalRef,
}
impl DebugChat {
pub(crate) fn new(component: GlobalRef) -> Self {
Self {
buffer: Default::default(),
object: component,
}
}
}
impl Component for DebugChat {
fn name() -> [u8; 0x30] {
component_name!(b"chat")
}
fn memory_map_size(&self) -> u64 {
1
}
fn write(&self, env: &mut JNIEnv, _: u64, data: &[u8]) {
let mut string = self.buffer.lock().unwrap();
if data[0] == 0 {
let jni_string = env.new_string(string.drain(..).collect::<String>()).expect("failed to create string");
env
.call_method(
self.object.clone(),
"sendChatMessage",
"(Ljava_lang_String;)V",
&[JValueGen::Object(&jni_string)],
)
.expect("failed to call...");
return;
}
string.push(data[0].into());
}
}

View file

@ -0,0 +1,29 @@
use bitfield_struct::bitfield;
use crate::component_name;
use super::Component;
#[bitfield(u8)]
struct InterruptEntry {
#[bits(4)]
affinity: u8,
masked: bool,
fired: bool,
#[bits(2)]
_unused: u8,
}
pub struct InterruptController {
_interrupt_entries: [InterruptEntry; 32],
}
impl Component for InterruptController {
fn name() -> [u8; 0x30] {
component_name!(b"interrupt_controller")
}
fn memory_map_size(&self) -> u64 {
0
}
}

View file

@ -0,0 +1,66 @@
use core::CoreComponent;
use debug_chat::DebugChat;
use jni::JNIEnv;
pub mod core;
pub mod debug_chat;
pub mod interrupt_controller;
pub mod storage;
pub trait Component: Sized {
fn name() -> [u8; 0x30];
fn memory_map_size(&self) -> u64;
fn write(&self, env: &mut JNIEnv, offset: u64, data: &[u8]) {
let _ = (env, offset, data);
}
fn read(&self, env: &mut JNIEnv, offset: u64, data: &mut [u8]) {
let _ = (env, offset, data);
}
fn has_interrupt(&self) -> bool {
false
}
}
pub enum AllComponent {
Core(CoreComponent),
DebugChat(DebugChat),
}
impl AllComponent {
pub fn name(&self) -> [u8; 0x30] {
match self {
AllComponent::Core(_) => CoreComponent::name(),
AllComponent::DebugChat(_) => DebugChat::name(),
}
}
pub fn memory_map_size(&self) -> u64 {
match self {
AllComponent::Core(core_component) => core_component.memory_map_size(),
AllComponent::DebugChat(debug_chat) => debug_chat.memory_map_size(),
}
}
pub fn write(&self, env: &mut JNIEnv, offset: u64, data: &[u8]) {
match self {
AllComponent::Core(core_component) => core_component.write(env, offset, data),
AllComponent::DebugChat(debug_chat) => debug_chat.write(env, offset, data),
}
}
pub fn read(&self, env: &mut JNIEnv, offset: u64, data: &mut [u8]) {
match self {
AllComponent::Core(core_component) => core_component.read(env, offset, data),
AllComponent::DebugChat(debug_chat) => debug_chat.read(env, offset, data),
}
}
pub fn has_interrupt(&self) -> bool {
match self {
AllComponent::Core(core_component) => core_component.has_interrupt(),
AllComponent::DebugChat(debug_chat) => debug_chat.has_interrupt(),
}
}
}

View file

@ -0,0 +1,2 @@
pub struct Storage {
}

View file

@ -1,86 +1,114 @@
use std::sync::{Arc, OnceLock, RwLock};
pub(crate) mod proto {
include!(concat!(env!("OUT_DIR"), "/computer.rs"));
}
use std::sync::{mpsc, Arc};
use jni::{
objects::{JByteBuffer, JIntArray, JObject, JString, JValue},
objects::{JByteArray, JByteBuffer, JObject, JObjectArray, JValue},
signature::{Primitive, ReturnType},
strings::JNIString,
sys::{jint, jlong},
sys::jlong,
JNIEnv,
};
use prost::Message;
use proto::component_init::Data;
use zerocopy::{little_endian::U64, FromZeros};
use crate::{
assert_positive,
component::{mmio::ComponentMmioControl, Component, ComponentInfo, ComponentInterface},
core::CoreHandle,
align_up_to_page,
components::{
core::{CoreComponent, Registers},
debug_chat::DebugChat,
AllComponent,
},
core::{spawn_core_thread, Core, CoreMessage},
device_bus::{DeviceBus, ROM_START},
unsync_cell::UnsyncCell,
};
pub struct Computer {
pub components: Arc<[RwLock<Component>]>,
pub component_control: Arc<UnsyncCell<ComponentMmioControl>>,
pub shared_ram: Arc<UnsyncCell<[u8]>>,
pub shared_rom: Arc<UnsyncCell<[u8]>>,
cores: Vec<mpsc::Sender<CoreMessage>>,
}
pub cores: OnceLock<Box<[CoreHandle]>>,
fn try_get_object<'local>(
env: &mut JNIEnv<'local>,
init_objects: &JObjectArray<'local>,
index: i32,
) -> jni::errors::Result<Option<JObject<'local>>> {
if index < 0 {
return Ok(None);
}
env.get_object_array_element(&init_objects, index).map(Some)
}
impl Computer {
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_new")]
extern "C" fn new<'local>(
unsafe extern "C" fn new<'local>(
mut env: JNIEnv<'local>,
java_computer: JObject<'local>,
component_capacity: i32,
memory_size: i32,
core_speeds: JIntArray<'local>,
init_data: JByteArray<'local>,
init_objects: JObjectArray<'local>,
) {
let (component_capacity, memory_size): (usize, usize) = (
assert_positive!(env, component_capacity),
assert_positive!(env, memory_size),
);
let init_data = env.convert_byte_array(&init_data).expect("unable to convert byte array to vec");
let init = proto::ComputerInit::decode(init_data.as_slice()).expect("failed to decode init data from java!");
let ram = UnsyncCell::new_zeroed_box(init.primary_ram_pages as usize * 0x1000);
let core_count = env.get_array_length(&core_speeds).unwrap() as usize;
if core_count > 16 {
env.throw("no more than 16 cores are allowed").unwrap();
return;
}
println!("got to computer construction!");
let computer = Box::new(Self {
components: {
let mut components = Vec::with_capacity(component_capacity);
components.extend(std::iter::from_fn(|| Default::default()).take(component_capacity));
Arc::from(components.into_boxed_slice())
},
component_control: Arc::new(UnsyncCell::new(ComponentMmioControl::new())),
shared_ram: Arc::from(UnsyncCell::new_zeroed_box(memory_size as usize)),
shared_rom: Arc::from(UnsyncCell::new_zeroed_box(0x1000000 as usize)),
// late initalized, see below
cores: OnceLock::new(),
});
println!("core construction");
let mut speeds = [0; 16];
env.get_int_array_region(core_speeds, 0, &mut speeds[..core_count]).unwrap();
println!("core speeds acquired {:?}", &speeds[..core_count]);
let Ok(cores) = speeds
.into_iter()
.take(core_count)
.enumerate()
.map(|(core_index, speed)| {
Ok(CoreHandle::new(
&computer,
core_index,
assert_positive!(env, speed, Err(())),
env.get_java_vm().unwrap(),
))
let rom = try_get_object(&mut env, &init_objects, init.primary_rom_index)
.expect("failed to get rom from java")
.map(|rom| {
if !env.is_instance_of(&rom, "java/nio/ByteBuffer").expect("failed to perform instanceOf") {
panic!("rom was not a byte buffer")
}
let rom_byte_buf = JByteBuffer::from(rom);
let capacity = env.get_direct_buffer_capacity(&rom_byte_buf).expect("unable to extract capacity from rom");
let rom = UnsyncCell::new_zeroed_box(align_up_to_page(capacity));
rom.update_from_jni(&mut env, &rom_byte_buf).unwrap();
rom
})
.collect::<Result<Vec<_>, _>>()
else {
return;
};
.expect("no rom was provided");
let _ = computer.cores.set(cores.into_boxed_slice());
let mut core_init_datas = Vec::new();
let components = init
.components
.into_iter()
.map(|component| match component.data.unwrap() {
Data::Core(init) => {
let registers = Arc::new(UnsyncCell::new(Registers {
reset_address: U64::new(ROM_START),
..Registers::new_zeroed()
}));
let (sender, receiver) = mpsc::channel();
core_init_datas.push((init, registers.clone(), sender.clone(), receiver));
AllComponent::Core(CoreComponent::new(registers, sender))
}
Data::Chat(init) => {
let component = try_get_object(&mut env, &init_objects, init.java_index)
.expect("failed to get chat component from java")
.expect("chat component object was not provided");
println!("storing computer info in field!");
let component = env.new_global_ref(component).unwrap();
AllComponent::DebugChat(DebugChat::new(component))
}
})
.collect();
let device_bus = Arc::new(DeviceBus::new(ram, rom, components));
let cores = core_init_datas
.into_iter()
.map(|(init, registers, sender, receiver)| {
spawn_core_thread(
Core::new(registers, device_bus.clone(), init.instructions_per_tick),
env.get_java_vm().unwrap(),
receiver,
);
sender
})
.collect();
let computer = Box::new(Self { cores });
env
.set_field(
@ -104,67 +132,6 @@ impl Computer {
.unwrap() as *mut _
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_insertComponentNative")]
unsafe extern "C" fn insert_component<'local>(
mut env: JNIEnv<'local>,
computer: JObject<'local>,
component_index: jint,
component_object: JObject<'local>,
type_name: JString<'local>,
protocol: JString<'local>,
bytes_per_tick: u32,
version: u32,
) {
// Safety: the responsibility of asserting the pointer is valid is on the java caller
let computer = unsafe { &*Self::from_jni(&mut env, computer) };
let component_index: usize = assert_positive!(env, component_index);
let mut make_info_string = |string: JString<'local>| {
let mut bytes = [0u8; 256];
let jstring = env.get_string(&string).unwrap();
let length = bytes.len().min(jstring.to_bytes().len());
bytes[..length].copy_from_slice(&jstring.to_bytes()[..length]);
bytes
};
let type_name = make_info_string(type_name);
let protocol = make_info_string(protocol);
let info = ComponentInfo {
type_name,
protocol,
bytes_per_tick,
version,
};
let java_component = env.new_global_ref(component_object).unwrap();
*computer.components[component_index].write().unwrap() = Component::Populated {
own_index: component_index,
java_component,
read_transfer_active: false.into(),
write_transfer_active: false.into(),
info,
interface: ComponentInterface::default().into(),
};
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_updateRomTest")]
unsafe extern "C" fn update_rom_test<'local>(
mut env: JNIEnv<'local>,
computer: JObject<'local>,
bytes: JByteBuffer<'local>,
) {
// Safety: the responsibility of asserting the pointer is valid is on the java caller
let computer = unsafe { &*Self::from_jni(&mut env, computer) };
computer.shared_rom.update_from_jni(&mut env, &bytes).expect("failed to update rom");
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_updateRomFile")]
unsafe extern "C" fn update_rom_file<'local>(env: JNIEnv<'local>, computer: JObject<'local>) {
drop((env, computer));
todo!()
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_free")]
unsafe extern "C" fn free<'local>(mut env: JNIEnv<'local>, computer: JObject<'local>) {
let computer = Self::from_jni(&mut env, computer);
@ -172,31 +139,23 @@ impl Computer {
drop(unsafe { Box::from_raw(computer) })
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_wakeCoreAt")]
unsafe extern "C" fn wake_core_at<'local>(
mut env: JNIEnv<'local>,
computer: JObject<'local>,
core: jint,
address: jlong,
) {
let (core, address): (usize, u64) = (assert_positive!(env, core), assert_positive!(env, address));
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_resetCore")]
unsafe extern "C" fn reset_core<'local>(mut env: JNIEnv<'local>, computer: JObject<'local>, core: u32, wake: bool) {
// Safety: the responsibility of asserting the pointer is valid is on the java caller
let computer = unsafe { &*Self::from_jni(&mut env, computer) };
let Some(core) = computer.cores.get().unwrap().get(core) else {
env.throw("core index was out of bounds").unwrap();
return;
};
core.wake_at(address);
let core = computer.cores.get(core as usize).unwrap();
core.send(CoreMessage::Reset).unwrap();
if wake {
core.send(CoreMessage::Wake).unwrap();
}
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_Computer_tick")]
unsafe extern "C" fn tick<'local>(mut env: JNIEnv<'local>, computer: JObject<'local>) {
// Safety: the responsibility of asserting the pointer is valid is on the java caller
let computer = unsafe { &*Self::from_jni(&mut env, computer) };
let cores = &computer.cores.get().unwrap();
for core in cores.iter() {
core.tick();
for core in &computer.cores {
core.send(CoreMessage::Tick).unwrap();
}
}
}

View file

@ -1,103 +1,122 @@
use std::{
cell::{OnceCell, RefCell},
pin::Pin,
rc::Rc,
sync::{mpsc, Arc, RwLock},
};
use std::sync::{mpsc, Arc};
use disarm64::decoder;
use jni::{JNIEnv, JavaVM};
use jni::{AttachGuard, JNIEnv, JavaVM};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
use unicorn_engine::{Arch, ArmCpRegister, HookType, Mode, Permission, Query, RegisterARM64, Unicorn};
use unicorn_engine::{Arch, HookType, Mode, Unicorn};
use zerocopy::IntoBytes;
use crate::{component::{mmio::{calculate_components_start, component_mmio_read, component_mmio_write, ComponentMmioControl}, Component, ComponentIoResult}, computer::Computer, unsync_cell::UnsyncCell};
use crate::{components::core::Registers, device_bus::DeviceBus, unsync_cell::UnsyncCell};
pub struct CoreHandle {
message_sender: mpsc::Sender<CpuMessage>,
pub struct Core {
registers: Arc<UnsyncCell<Registers>>,
bus: Arc<DeviceBus>,
instructions_per_tick: u32,
}
/*
todo REWRITEEEEEEE RAAAAH
*/
pub struct CpuContext<'a> {
component_data_start: usize,
components: Arc<[RwLock<Component>]>,
/// see comment on [Computer]
component_control: Arc<UnsyncCell<ComponentMmioControl>>,
message_sender: mpsc::Sender<CpuMessage>,
component_access_futures: Vec<Pin<Box<dyn Future<Output = ComponentIoResult>>>>,
ram: Arc<UnsyncCell<[u8]>>,
stopped: bool,
_env: JNIEnv<'a>,
impl Core {
pub fn new(registers: Arc<UnsyncCell<Registers>>, bus: Arc<DeviceBus>, instructions_per_tick: u32) -> Self {
Self {
registers,
bus,
instructions_per_tick,
}
}
}
enum CpuMessage {
pub struct CoreContext<'a> {
core: Core,
env: AttachGuard<'a>,
is_sleeping: bool,
}
impl<'a> CoreContext<'a> {
pub fn get_env(&mut self) -> &mut JNIEnv<'a> {
&mut self.env
}
}
pub enum CoreMessage {
Tick,
Wake(Option<u64>),
Reset,
Wake,
Sleep,
}
impl CoreHandle {
pub fn new(computer: &Computer, core_index: usize, instructions_per_tick: u32, java_vm: JavaVM) -> Self {
let (components, component_control) = (computer.components.clone(), computer.component_control.clone());
let (started_up_sender, started_up_receiver) = oneshot::channel();
pub fn spawn_core_thread(core: Core, jvm: JavaVM, message_receiver: mpsc::Receiver<CoreMessage>) {
std::thread::spawn(|| core_main(core, jvm, message_receiver));
}
let (message_sender, message_receiver) = mpsc::channel();
let memories = (computer.shared_ram.clone(), computer.shared_rom.clone());
fn core_main(core: Core, jvm: JavaVM, message_receiver: mpsc::Receiver<CoreMessage>) {
let env = jvm.attach_current_thread().unwrap();
let mut uc = Unicorn::new_with_data(
Arch::ARM64,
Mode::LITTLE_ENDIAN,
CoreContext {
core,
env,
is_sleeping: true,
},
)
.unwrap();
std::thread::spawn({
let message_sender = message_sender.clone();
let bus = uc.get_data().core.bus.clone();
bus.map(&mut uc);
uc.add_intr_hook(interrupt_handler).unwrap();
uc
.add_mem_hook(
HookType::MEM_INVALID,
u64::MIN,
u64::MAX,
|vcpu, mem_type, address, _, value| {
eprintln!("memory exception: {mem_type:?} at {address:X} (value: {value:X})");
move || {
let env = java_vm.attach_current_thread_permanently().unwrap();
vcpu.emu_stop().unwrap();
true
},
)
.unwrap();
let context = CpuContext {
component_data_start: calculate_components_start(components.len()),
components,
component_control,
message_sender,
component_access_futures: Vec::new(),
ram: memories.0.clone(),
_env: env,
stopped: true,
};
thread(
core_index,
instructions_per_tick,
memories,
context,
started_up_sender,
message_receiver,
)
let try_fetch_message = |stopped| {
if stopped {
message_receiver.recv().map(Some).map_err(|_| ())
} else {
Ok(message_receiver.try_recv().ok())
}
};
let mut instructions_left = 0;
loop {
while let Some(message) = try_fetch_message(uc.get_data().is_sleeping || instructions_left == 0).transpose() {
let Ok(message) = message else { return };
match message {
CoreMessage::Tick => {
if uc.get_data().is_sleeping {
continue;
}
println!("ticking");
instructions_left = uc.get_data().core.instructions_per_tick as usize;
}
CoreMessage::Reset => {
let reset_address = uc.get_data().core.registers.read().reset_address.get();
println!("reset! address: {reset_address:08X}");
uc.set_pc(reset_address).expect("error while executing cpu")
}
CoreMessage::Wake => uc.get_data_mut().is_sleeping = false,
CoreMessage::Sleep => uc.get_data_mut().is_sleeping = true,
}
});
let _ = started_up_receiver.recv().expect("failed to start up core");
CoreHandle { message_sender }
}
#[unsafe(export_name = "Java_ca_sanae_golemcomputers_computer_RustNative_coreFree")]
unsafe extern "C" fn free<'local>(_env: JNIEnv<'local>, core: *const Self) {
// Safety: the responsibility of verifying the pointer is valid is on the java caller
drop(unsafe { Arc::from_raw(core) })
}
pub fn tick(&self) {
self.message_sender.send(CpuMessage::Tick).expect("cpu thread panicked :(");
}
pub fn wake_at(&self, start: u64) {
self.message_sender.send(CpuMessage::Wake(Some(start))).unwrap();
}
println!("executing");
uc.emu_start(
uc.pc_read().expect("error while executing cpu"),
0,
0,
instructions_left.min(1000),
)
.expect("error while executing cpu");
instructions_left = instructions_left.saturating_sub(1000);
}
}
@ -126,110 +145,29 @@ enum Exception {
Unaligned = 22,
}
fn thread(
core_index: usize,
instructions_per_tick: u32,
(shared_ram, shared_rom): (Arc<UnsyncCell<[u8]>>, Arc<UnsyncCell<[u8]>>),
context: CpuContext,
started_up_sender: oneshot::Sender<()>,
message_receiver: mpsc::Receiver<CpuMessage>,
) -> Result<(), ()> {
let mut vcpu =
Unicorn::new_with_data(Arch::ARM64, Mode::LITTLE_ENDIAN, context).expect("failed to spawn unicorn engine");
fn interrupt_handler(uc: &mut Unicorn<CoreContext>, kind: u32) {
let exception = Exception::from_u32(kind).unwrap();
const MPIDR_EL1: ArmCpRegister = ArmCpRegister::from_manual(0b11, 0b000, 0b0000, 0b0000, 0b101);
vcpu.reg_write_cp(CP_REG, MPIDR_EL1, core_index as u64).unwrap();
vcpu
.mmio_map(
0x2000_0000,
0x1000,
Some(component_mmio_read),
Some(component_mmio_write),
)
.expect("failed to map mmio for core");
shared_rom
.map(&mut vcpu, 0x4000_0000, None, Permission::READ | Permission::EXEC)
.expect("failed to map memory for rom");
shared_ram.map(&mut vcpu, 0x8000_0000, None, Permission::ALL).expect("failed to map memory for rom");
use RegisterARM64::*;
vcpu
.add_intr_hook(|vcpu, excp| {
println!("interrupt: {excp}");
let exception = Exception::from_u32(excp).unwrap();
let pc = vcpu.pc_read().unwrap();
println!("exception: {exception:?}");
let mut opcode = 0u32;
if let Ok(_) = vcpu.mem_read(pc, opcode.as_mut_bytes()) {
if let Some(opcode) = decoder::decode(opcode) {
let mut inst = String::new();
disarm64::format_insn::format_insn_pc(pc, &mut inst, &opcode).unwrap();
println!("pc {pc:08X} {inst}");
} else {
println!("pc {pc:08X} - op parse failed {opcode:08X}");
}
} else {
println!("pc {pc:08X} - can't read");
}
println!("x0 {:08X}", vcpu.reg_read(X0).unwrap());
println!("x1 {:08X}", vcpu.reg_read(X1).unwrap());
let syndrome = vcpu.query(Query::SYNDROME).unwrap();
println!("0x{syndrome:08X}");
vcpu.get_data_mut().stopped = true;
vcpu.emu_stop().unwrap();
})
.unwrap();
vcpu
.add_mem_hook(
HookType::MEM_INVALID,
u64::MIN,
u64::MAX,
|vcpu, mem_type, address, _, value| {
eprintln!("memory exception: {mem_type:?} at {address:X} (value: {value:X})");
vcpu.emu_stop().unwrap();
true
},
)
.unwrap();
started_up_sender.send(()).expect("panicked when trying to core");
let try_fetch_message = |stopped| {
if stopped {
message_receiver.recv().map(Some).map_err(|_| ())
let pc = uc.pc_read().unwrap();
println!("exception: {exception:?}");
let mut opcode = 0u32;
if let Ok(_) = uc.mem_read(pc, opcode.as_mut_bytes()) {
if let Some(opcode) = decoder::decode(opcode) {
let mut inst = String::new();
disarm64::format_insn::format_insn_pc(pc, &mut inst, &opcode).unwrap();
println!("pc {pc:08X} {inst}");
} else {
Ok(message_receiver.try_recv().ok())
println!("pc {pc:08X} - op parse failed {opcode:08X}");
}
};
let mut instructions_left = 0;
loop {
while let Some(message) = try_fetch_message(vcpu.get_data().stopped || instructions_left == 0)? {
match message {
CpuMessage::Tick => {
if vcpu.get_data().stopped {
continue;
}
}
CpuMessage::Wake(address) => {
if let Some(address) = address {
vcpu.set_pc(address).unwrap();
println!("waking at {address}");
}
vcpu.get_data_mut().stopped = false;
let pc = vcpu.pc_read().unwrap();
println!("executing {pc:X}");
vcpu.emu_start(pc, 0, 0, instructions_per_tick as usize).expect("error while executing cpu");
}
CpuMessage::Sleep => vcpu.get_data_mut().stopped = true,
}
}
vcpu.emu_start(vcpu.pc_read().unwrap(), 0, 0, instructions_left.min(1000)).expect("error while executing cpu");
instructions_left = instructions_left.saturating_sub(1000);
} else {
println!("pc {pc:08X} - can't read");
}
use unicorn_engine::RegisterARM64::*;
println!("X0: {:X}", uc.reg_read(X0).unwrap());
println!("X1: {:X}", uc.reg_read(X1).unwrap());
println!("X2: {:X}", uc.reg_read(X2).unwrap());
// if an exception occurs, we don't want to execute anymore
uc.get_data_mut().is_sleeping = true;
}

174
rust/src/device_bus.rs Normal file
View file

@ -0,0 +1,174 @@
use std::{num::NonZeroUsize, sync::Arc};
use bitfield_struct::bitfield;
use nodit::{interval::ie, Interval, NoditMap, OverlapError};
use unicorn_engine::{Permission, Unicorn};
use zerocopy::{little_endian::U32, FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use crate::{align_up_to_page, components::AllComponent, core::CoreContext, unsync_cell::UnsyncCell};
pub const COMPONENT_TABLE_START: u64 = 0x1000_0000;
pub const COMPONENTS_START: u64 = 0x2000_0000;
pub const ROM_START: u64 = 0x4000_0000;
pub const RAM_START: u64 = 0x8000_0000;
enum Device {
Rom(Box<UnsyncCell<[u8]>>),
Ram(Box<UnsyncCell<[u8]>>),
ComponentTable,
Component(AllComponent),
}
#[bitfield(u32)]
struct ComponentEntryHeader {
#[bits(31)]
_padding: u32,
filled: bool,
}
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
struct ComponentTableEntry {
header: U32,
address: U32,
name: [u8; 0x30],
}
struct ComponentTable(Box<[u8]>);
pub struct DeviceBus {
devices: NoditMap<u64, Interval<u64>, Device>,
component_table: ComponentTable,
}
fn assert_non_overlapping(result: Result<(), OverlapError<Device>>) {
if result.is_err() {
panic!("insert overlapped!")
}
}
impl DeviceBus {
pub fn new(ram: Box<UnsyncCell<[u8]>>, rom: Box<UnsyncCell<[u8]>>, components: Vec<AllComponent>) -> Self {
let mut devices = NoditMap::new();
assert_non_overlapping(devices.insert_strict(ie(ROM_START, ROM_START + rom.len() as u64), Device::Rom(rom)));
assert_non_overlapping(devices.insert_strict(ie(RAM_START, RAM_START + ram.len() as u64), Device::Ram(ram)));
let mut component_table = {
let mut table = Box::new_uninit_slice(align_up_to_page(components.len() * size_of::<ComponentTableEntry>()));
table.zero();
unsafe { table.assume_init() }
};
assert_non_overlapping(devices.insert_strict(
ie(
COMPONENT_TABLE_START,
COMPONENT_TABLE_START + component_table.len() as u64,
),
Device::ComponentTable,
));
{
let component_table = <[ComponentTableEntry]>::mut_from_bytes_with_elems(
&mut component_table[..components.len() * size_of::<ComponentTableEntry>()],
components.len(),
)
.unwrap();
let mut component_offset = COMPONENTS_START;
for (index, component) in components.into_iter().enumerate() {
let start = component_offset;
let end = align_up_to_page(component_offset + component.memory_map_size());
println!("component offset: {start:X}..{end:X} and {}", u32::MAX);
assert!(end < ROM_START);
component_offset = end;
component_table[index] = ComponentTableEntry {
header: U32::new(ComponentEntryHeader::new().with_filled(true).into_bits()),
address: U32::new(start as u32),
name: component.name(),
};
assert_non_overlapping(devices.insert_strict(ie(start, end), Device::Component(component)));
}
}
Self {
devices,
component_table: ComponentTable(component_table),
}
}
pub fn map(self: Arc<Self>, uc: &mut Unicorn<CoreContext>) {
for (interval, device) in self.devices.iter() {
let (address, size) = (interval.start(), (interval.end() - interval.start()) as usize + 1);
println!("address, size {address:X}, {size:X} {:X?}", interval);
match device {
Device::Rom(memory) => unsafe {
uc.mem_map_ptr(
address,
size,
Permission::READ | Permission::WRITE,
memory.as_mut_slice_ptr() as _,
)
.unwrap();
},
Device::Ram(memory) => unsafe {
uc.mem_map_ptr(
address,
size,
Permission::READ | Permission::WRITE | Permission::EXEC,
memory.as_mut_slice_ptr() as _,
)
.unwrap();
},
Device::ComponentTable => unsafe {
println!("component table range {address:X} + {size:X}");
uc.mem_map_ptr(address, size, Permission::READ, self.component_table.0.as_ptr() as _).unwrap()
},
Device::Component(_) => {
fn calculate_size(component: &AllComponent, offset: u64, size: usize) -> Option<NonZeroUsize> {
if offset >= component.memory_map_size() {
return None;
}
let offset = offset as usize;
NonZeroUsize::new(usize::min(offset + size, component.memory_map_size() as usize) - offset)
}
let bus_read = self.clone();
let bus_write = self.clone();
uc.mmio_map(
address,
size,
Some(move |uc: &mut Unicorn<CoreContext>, offset: u64, size: usize| {
let Device::Component(component) = bus_read.devices.get_at_point(address).unwrap() else {
unreachable!("device was not a component")
};
let Some(size) = calculate_size(component, offset, size).map(NonZeroUsize::get) else {
return 0;
};
let mut data = 0u64;
component.read(uc.get_data_mut().get_env(), offset, &mut data.as_mut_bytes()[..size]);
data
}),
Some(
move |uc: &mut Unicorn<CoreContext>, offset: u64, size: usize, data: u64| {
let Device::Component(component) = bus_write.devices.get_at_point(address).unwrap() else {
unreachable!("device was not a component")
};
let Some(size) = calculate_size(component, offset, size).map(NonZeroUsize::get) else {
return;
};
component.write(uc.get_data_mut().get_env(), offset, &data.to_le_bytes()[..size]);
},
),
)
.unwrap()
}
}
}
}
}

View file

@ -1,6 +1,5 @@
#![deny(unsafe_op_in_unsafe_fn)]
#![feature(sync_unsafe_cell)]
#![feature(generic_const_exprs)]
use std::ops::Range;
@ -8,12 +7,13 @@ use jni::{
sys::{jint, JNI_VERSION_1_8},
JNIEnv,
};
use num_traits::PrimInt;
pub mod component;
pub mod components;
mod computer;
mod core;
pub mod core;
mod device_bus;
pub mod unsync_cell;
mod memory_map;
#[macro_export]
macro_rules! assert_positive {
@ -45,6 +45,30 @@ macro_rules! assert_positive {
}};
}
#[macro_export]
macro_rules! component_name {
($value: expr) => {{
const DATA: [u8; 0x30] = {
let name = { $value };
if name.len() >= 0x30 {
panic!("name length must be less than 64 characters")
}
let mut new_name = [0; 0x30];
let mut index = name.len();
loop {
index -= 1;
new_name[index] = name[index];
if index == 0 {
break;
}
}
new_name
};
DATA.clone()
}};
}
#[macro_export]
macro_rules! range_of_field {
($type: ty, $field: ident) => {{
@ -68,19 +92,19 @@ fn overlapping<Idx: PartialOrd>(a: &Range<Idx>, b: &Range<Idx>) -> bool {
a.contains(&b.start) && a.contains(&b.end)
}
fn subtract_range(a: &Range<usize>, b: &Range<usize>) -> Range<usize> {
a.start - b.start..a.end - b.end
// fn subtract_range(a: &Range<usize>, b: &Range<usize>) -> Range<usize> {
// a.start - b.start..a.end - b.end
// }
pub fn align_up_to_page<T: PrimInt>(value: T) -> T {
align_up(value, T::one().unsigned_shl(12))
}
pub fn align_up(value: u64, alignment: u64) -> u64 {
(value + alignment - 1) & !(alignment - 1)
}
struct UnicornWrap {
pub fn align_up<T: PrimInt>(value: T, alignment: T) -> T {
(value + alignment - T::one()) & !(alignment - T::one())
}
#[unsafe(no_mangle)]
pub extern "system" fn JNI_OnLoad<'local>(_: JNIEnv<'local>, _: usize) -> jint {
extern "system" fn JNI_OnLoad<'local>(_: JNIEnv<'local>, _: usize) -> jint {
JNI_VERSION_1_8
}

View file

@ -1,3 +0,0 @@
pub const COMPONENTS_START: u64 = 0x2000_0000;
pub const ROM_START: u64 = 0x4000_0000;
pub const RAM_START: u64 = 0x8000_0000;

View file

@ -2,7 +2,7 @@ use std::cell::SyncUnsafeCell;
use jni::{objects::JByteBuffer, JNIEnv};
use unicorn_engine::{uc_error, Permission, Unicorn};
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
use zerocopy::{FromBytes, FromZeros, IntoBytes, KnownLayout};
/// A simple replacement for [std::cell::Cell] in Sync contexts which explicitly does not try to synchronize the contents.
/// This is used for contexts where unsynchronized access is explicitly fine.
@ -19,6 +19,12 @@ impl<T> UnsyncCell<T> {
}
}
impl<T> UnsyncCell<[T]> {
pub fn as_mut_slice_ptr(&self) -> *mut [T] {
self.0.get()
}
}
impl<T: Sized + Copy> UnsyncCell<T> {
pub fn new(value: T) -> Self {
Self(SyncUnsafeCell::new(value))
@ -35,24 +41,36 @@ impl<T: Sized + Copy> UnsyncCell<T> {
impl<T: Sized + Copy + IntoBytes> UnsyncCell<T> {
// N must be the same as the size of U
pub fn read_into<U: FromBytes>(&self, offset: usize) -> U
where
[u8; size_of::<U>()]:,
{
let mut buffer = [0; size_of::<U>()];
let end = (offset + size_of::<U>()).min(size_of::<T>());
assert!(end - offset <= size_of::<U>());
// Safety: both pointers are valid for 0..(end - offset)
unsafe { (self.as_mut_ptr() as *const u8).copy_to(buffer.as_mut_ptr(), end - offset) };
U::read_from_bytes(&buffer).expect("N was not the same as the size of U")
// pub fn read_into<U: FromBytes>(&self, offset: usize) -> U
// where
// [u8; size_of::<U>()]:,
// {
// let mut buffer = [0; size_of::<U>()];
// let end = (offset + size_of::<U>()).min(size_of::<T>());
// assert!(end - offset <= size_of::<U>());
// // Safety: both pointers are valid for 0..(end - offset)
// unsafe { (self.as_mut_ptr() as *const u8).copy_to(buffer.as_mut_ptr(), end - offset) };
// U::read_from_bytes(&buffer).expect("N was not the same as the size of U")
// }
pub fn read_into_byte_slice(&self, offset: usize, data: &mut [u8]) {
let end = (offset + data.len()).min(size_of::<T>());
assert!(end - offset <= data.len());
unsafe { (self.as_mut_ptr() as *const u8).copy_to(data.as_mut_ptr(), end - offset) };
}
}
impl<T: Sized + Copy + FromBytes> UnsyncCell<T> {
pub fn write_from<U: IntoBytes + Immutable>(&self, offset: usize, value: &U) {
let end = (offset + size_of::<U>()).min(size_of::<T>());
assert!(end - offset <= size_of::<U>());
unsafe { (self.as_mut_ptr() as *mut u8).copy_from(value.as_bytes().as_ptr(), end - offset) };
// pub fn write_from<U: IntoBytes + Immutable>(&self, offset: usize, value: &U) {
// let end = (offset + size_of::<U>()).min(size_of::<T>());
// assert!(end - offset <= size_of::<U>());
// unsafe { (self.as_mut_ptr() as *mut u8).copy_from(value.as_bytes().as_ptr(), end - offset) };
// }
pub fn write_from_byte_slice(&self, offset: usize, data: &[u8]) {
let end = (offset + data.len()).min(size_of::<T>());
assert!(end - offset <= data.len());
unsafe { (self.as_mut_ptr() as *mut u8).copy_from(data.as_ptr(), end - offset) };
}
}
@ -69,7 +87,7 @@ impl<T: Sized + Copy> UnsyncCell<[T]> {
memory.zero();
// Safety: UnsafeCell transparently wraps the slice, meaning the two types are semantically identical
unsafe { std::mem::transmute::<Box<[T]>, Box<Self>>(memory.assume_init()) }
unsafe { std::mem::transmute::<Box<[T]>, Box<UnsyncCell<[T]>>>(memory.assume_init()) }
}
fn as_mut_ptr(&self) -> *mut [T] {
@ -80,6 +98,23 @@ impl<T: Sized + Copy> UnsyncCell<[T]> {
self.as_mut_ptr().len()
}
pub fn map<D>(
&self,
vcpu: &mut Unicorn<D>,
address: u64,
size: Option<usize>,
permissions: Permission,
) -> Result<(), uc_error> {
let size = size.unwrap_or(self.len());
if size > self.len() {
return Err(uc_error::ARG);
}
println!("mapping {permissions:?} at {address:08X} for {size:08X}");
unsafe { vcpu.mem_map_ptr(address, size, permissions, self.as_mut_ptr() as *mut _) }
}
}
impl UnsyncCell<[u8]> {
pub fn update_from_slice(&self, bytes: &[u8]) -> Result<(), usize> {
if self.len() < bytes.len() {
return Err(bytes.len() - self.len());
@ -107,21 +142,6 @@ impl<T: Sized + Copy> UnsyncCell<[T]> {
Ok(())
}
pub fn map<D>(
&self,
vcpu: &mut Unicorn<D>,
address: u64,
size: Option<usize>,
permissions: Permission,
) -> Result<(), uc_error> {
let size = size.unwrap_or(self.len());
if size > self.len() {
return Err(uc_error::ARG);
}
println!("mapping {permissions:?} at {address:08X} for {size:08X}");
unsafe { vcpu.mem_map_ptr(address, size, permissions, self.as_mut_ptr() as *mut _) }
}
}
impl<T: Clone + Copy> Clone for UnsyncCell<T> {

View file

@ -3,6 +3,7 @@ pluginManagement {
maven("https://maven.fabricmc.net/") {
name = "Fabric"
}
gradlePluginPortal()
mavenCentral()
}

View file

@ -4,10 +4,12 @@ import ca.sanae.golemcomputers.GolemComputers
import com.mojang.serialization.MapCodec
import net.minecraft.block.BlockState
import net.minecraft.block.BlockWithEntity
import net.minecraft.block.CommandBlock
import net.minecraft.block.entity.BlockEntity
import net.minecraft.block.entity.BlockEntityTicker
import net.minecraft.block.entity.BlockEntityType
import net.minecraft.entity.player.PlayerEntity
import net.minecraft.server.command.SayCommand
import net.minecraft.sound.BlockSoundGroup
import net.minecraft.text.Text
import net.minecraft.util.ActionResult
@ -51,8 +53,9 @@ class GolemBlock :
if (world.isClient()) {
return ActionResult.SUCCESS
}
world.server!!.playerManager.broadcast(Text.literal("Hello world!"), false)
blockEntity.incrementClicks();
player.sendMessage(Text.literal("You've clicked the block for the ${blockEntity.getClicks()}th time."), true);
// player.sendMessage(Text.literal("You've clicked the block for the ${blockEntity.getClicks()}th time."), true);
return ActionResult.SUCCESS_SERVER
}

View file

@ -2,6 +2,7 @@ package ca.sanae.golemcomputers.blocks
import ca.sanae.golemcomputers.GolemComputers
import ca.sanae.golemcomputers.computer.Computer
import ca.sanae.golemcomputers.computer.components.ChatComponent
import net.fabricmc.api.EnvType
import net.fabricmc.api.Environment
import net.minecraft.block.BlockState
@ -10,6 +11,8 @@ import net.minecraft.nbt.NbtCompound
import net.minecraft.registry.RegistryWrapper
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import java.nio.channels.FileChannel
import kotlin.io.path.Path
class GolemBlockEntity(pos: BlockPos?, state: BlockState?) :
BlockEntity(GolemComputers.GOLEM_BLOCK_ENTITY, pos, state) {
@ -34,7 +37,17 @@ class GolemBlockEntity(pos: BlockPos?, state: BlockState?) :
}
override fun setWorld(world: World?) {
computer = if (!world!!.isClient) Computer(world) else null
val path = Path("/home/aubrey/Projects/golem-software/assembly.bin")
val channel = FileChannel.open(path);
val buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size())
if (!world!!.isClient) {
val computer = Computer.Builder(world, buffer, 1u)
.addChat(ChatComponent(world.server!!))
.addCore(1000)
.build()
computer.resetCore(0, true);
this.computer = computer;
}
super.setWorld(world)
}

View file

@ -1,62 +1,66 @@
package ca.sanae.golemcomputers.computer
import ca.sanae.golemcomputers.computer.RustNative.Companion.cleaner
import ca.sanae.golemcomputers.computer.NativeProtobuf.ChatComponentInit
import ca.sanae.golemcomputers.computer.NativeProtobuf.ComponentInit
import ca.sanae.golemcomputers.computer.NativeProtobuf.ComputerInit
import ca.sanae.golemcomputers.computer.NativeProtobuf.CoreComponentInit
import ca.sanae.golemcomputers.computer.RustNative.cleaner
import ca.sanae.golemcomputers.computer.components.ChatComponent
import ca.sanae.golemcomputers.computer.components.Component
import net.minecraft.world.World
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.nio.file.Files
import kotlin.io.path.Path
class Computer(val world: World) {
class Computer private constructor(val world: World, init: ComputerInit, initObjects: Array<Any>) {
@Suppress("unused")
private val address: Long = 0
init {
new(1, 0x1000, intArrayOf(0x10000))
new(init.toByteArray(), initObjects)
cleaner.register(this) {
free()
}
val path = Path("/home/aubrey/Projects/golem-software/assembly.bin")
val channel = FileChannel.open(path);
val buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size())
val wakeAddress = buffer.getUnsignedInt();
updateRomTest(buffer.rewind())
println("waking at 0x${wakeAddress.toString(16)}")
insertComponent(0, ChatComponent(this))
wakeCoreAt(0, wakeAddress)
}
fun ByteBuffer.getUnsignedInt(): Long {
return Integer.reverseBytes(this.getInt()).toLong() and 0xFFFFFFFFL
}
fun insertComponent(coreIndex: Int, component: Component) {
insertComponentNative(
coreIndex,
component,
component.info.typeName.toString(),
component.info.protocol.toString(),
component.info.bytesPerTick,
component.info.version
)
}
private external fun new(componentCapacity: Int, memorySize: Int, coreIps: IntArray): Long
private external fun new(toByteArray: ByteArray, initObjects: Array<Any>): Long
private external fun free()
private external fun updateRomTest(data: ByteBuffer)
private external fun wakeCoreAt(coreIndex: Int, address: Long)
private external fun insertComponentNative(
index: Int,
component: Component,
typeName: String,
protocol: String,
bytesPerTick: UInt,
version: UInt
)
external fun resetCore(core: Int, wakeCore: Boolean)
external fun tick();
class Builder(val world: World, rom: ByteBuffer, ramPages: UInt) {
private val objects: ArrayList<Any> = ArrayList()
private val builder: ComputerInit.Builder = ComputerInit.newBuilder()
init {
builder.setPrimaryRomIndex(addObject(rom))
builder.setPrimaryRamPages(ramPages.toInt())
}
private fun addObject(value: Any): Int {
val index = objects.size
objects.add(value)
return index
}
fun addCore(instructionsPerTick: Int): Builder {
builder.addComponents(
ComponentInit.newBuilder()
.setCore(CoreComponentInit.newBuilder().setInstructionsPerTick(instructionsPerTick).build())
)
return this
}
fun addChat(chat: ChatComponent): Builder {
builder.addComponents(
ComponentInit.newBuilder().setChat(ChatComponentInit.newBuilder().setJavaIndex(addObject(chat)).build())
)
return this
}
fun build(): Computer {
return Computer(world, builder.build(), objects.toArray())
}
}
}

View file

@ -6,27 +6,24 @@ import java.lang.ref.Cleaner
import java.nio.channels.Channels
class RustNative private constructor() {
companion object {
val cleaner: Cleaner = Cleaner.create()
private val instance: RustNative = RustNative();
object RustNative {
val cleaner: Cleaner = Cleaner.create()
init {
val nativeLibraryName = System.mapLibraryName("golem_computers")
val tempFile: File = File.createTempFile("extracted_", nativeLibraryName)
init {
val nativeLibraryName = System.mapLibraryName("golem_computers")
val tempFile: File = File.createTempFile("extracted_", nativeLibraryName)
val classLoader = RustNative::class.java.getClassLoader()
val classLoader = RustNative::class.java.getClassLoader()
Channels.newChannel(classLoader.getResourceAsStream(nativeLibraryName)!!).use { src ->
FileOutputStream(tempFile).channel.use { dst ->
dst.transferFrom(src, 0, Long.MAX_VALUE)
}
Channels.newChannel(classLoader.getResourceAsStream(nativeLibraryName)!!).use { src ->
FileOutputStream(tempFile).channel.use { dst ->
dst.transferFrom(src, 0, Long.MAX_VALUE)
}
System.load(tempFile.absolutePath)
}
System.load(tempFile.absolutePath)
}
fun isInitialized(): Boolean {
return true
}
fun isInitialized(): Boolean {
return true
}
}

View file

@ -1,29 +1,13 @@
package ca.sanae.golemcomputers.computer.components
import ca.sanae.golemcomputers.GolemComputers
import ca.sanae.golemcomputers.computer.Computer
import kotlinx.io.bytestring.decodeToString
import kotlinx.io.bytestring.getByteString
import net.minecraft.server.MinecraftServer
import net.minecraft.text.Text
import java.nio.ByteBuffer
class ChatComponent(private val computer: Computer) :
Component(GolemComputers.id("chat"), GolemComputers.id("string_stream"), 10000, 0u) {
var chatBuffer: String = ""
override fun gotByteChunk(buffer: ByteBuffer) {
chatBuffer += buffer.getByteString().decodeToString()
while (true) {
val index = chatBuffer.indexOf('\u0000')
if (index == -1) break
val (message, new) = chatBuffer.splitAtIndex(index)
chatBuffer = new
computer.world.server!!.sendMessage(Text.of(message))
class ChatComponent(val server: MinecraftServer) {
@Suppress("unused")
fun sendChatMessage(string: String) {
server.execute {
server.playerManager.broadcast(Text.of(string), true)
}
}
override fun restarted() {
chatBuffer = ""
}
fun String.splitAtIndex(index: Int) = take(index) to substring(index)
}

View file

@ -1,31 +0,0 @@
package ca.sanae.golemcomputers.computer.components
import com.github.pbbl.heap.ByteBufferPool
import com.jogamp.common.util.LFRingbuffer
import com.jogamp.common.util.Ringbuffer
import net.minecraft.util.Identifier
import java.nio.ByteBuffer
abstract class Component(val info: ComponentInfo) {
@Suppress("unused")
private val address: Long = 0
private val pool = ByteBufferPool()
abstract fun gotByteChunk(buffer: ByteBuffer)
private external fun writeBytesNative(buffer: ByteBuffer): Boolean
fun writeBytes(lambda: ((size: Int) -> ByteBuffer) -> ByteBuffer) {
synchronized(pool) {
val buffer = lambda(pool::take)
val overflowed = writeBytesNative(buffer)
pool.give(buffer)
if (overflowed) {
overflowShutdown()
}
}
}
// called when the component runs out of runway to store unread data
open fun overflowShutdown() {}
abstract fun restarted()
data class ComponentInfo(val typeName: Identifier, val protocol: Identifier, val bytesPerTick: UInt, val version: UInt)
}