Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic emulator core thread operation #344

Merged
merged 16 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 122 additions & 9 deletions src/agent.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,136 @@
//! The agent responsible for running the emulator core on the worker thread and communication functionalities.

use futures::{SinkExt, StreamExt};
use crate::agent::messages::{Command, MipsStateUpdate};
use crate::emulation_core::architectures::{DatapathRef, DatapathUpdate};
use crate::emulation_core::datapath::Datapath;
use crate::emulation_core::mips::datapath::MipsDatapath;
use crate::emulation_core::mips::registers::GpRegisterType;
use futures::{FutureExt, SinkExt, StreamExt};
use gloo_console::log;
use std::time::Duration;
use yew::platform::time::sleep;
use yew_agent::prelude::*;

pub mod datapath_communicator;
pub mod datapath_reducer;
pub mod messages;

/// The main logic for the emulation core agent. All code within this function runs on a worker thread as opposed to
/// the UI thread.
#[reactor(EmulationCoreAgent)]
pub async fn emulation_core_agent(mut scope: ReactorScope<i32, i32>) {
pub async fn emulation_core_agent(scope: ReactorScope<Command, DatapathUpdate>) {
log!("Hello world!");
let mut num = 1;
scope.send(num).await.unwrap();
let mut state = EmulatorCoreAgentState::new(scope);
loop {
let msg = scope.next().await;
log!("Got message: ", msg);
num += 1;
scope.send(num).await.unwrap();
log!("Sent.");
let execution_delay = state.get_delay();

// Part 1: Delay/Command Handling
if state.executing {
futures::select! {
// If we get a message, handle the command before attempting to execute.
msg = state.scope.next() => match msg {
Some(msg) => state.handle_command(msg),
None => return,
},
// Delay to slow execution down to the intended speed.
_ = sleep(Duration::from_millis(execution_delay)).fuse() => {},
}
} else {
match state.scope.next().await {
Some(msg) => state.handle_command(msg),
None => return,
}
}

// Part 2: Execution
// Execute a single instruction if the emulator core should be executing.
state.execute();

// Part 3: Processing State/Sending Updates to UI
// TODO: This is a very naive implementation. Optimization is probably a good idea.
match state.current_datapath.as_datapath_ref() {
DatapathRef::MIPS(datapath) => {
let state_update =
DatapathUpdate::MIPS(MipsStateUpdate::UpdateState(datapath.state.clone()));
let register_update =
DatapathUpdate::MIPS(MipsStateUpdate::UpdateRegisters(datapath.registers));
let memory_update =
DatapathUpdate::MIPS(MipsStateUpdate::UpdateMemory(datapath.memory.clone()));
state.scope.send(state_update).await.unwrap();
state.scope.send(register_update).await.unwrap();
state.scope.send(memory_update).await.unwrap();
}
}
}
}

struct EmulatorCoreAgentState {
current_datapath: Box<dyn Datapath<RegisterData = u64, RegisterEnum = GpRegisterType>>,
pub scope: ReactorScope<Command, DatapathUpdate>,
speed: u32,
executing: bool,
}

impl EmulatorCoreAgentState {
pub fn new(scope: ReactorScope<Command, DatapathUpdate>) -> EmulatorCoreAgentState {
EmulatorCoreAgentState {
current_datapath: Box::<MipsDatapath>::default(),
scope,
speed: 0,
executing: false,
}
}

pub fn handle_command(&mut self, command: Command) {
match command {
Command::SetCore(_architecture) => {
todo!() // Implement once we have a RISCV datapath
}
Command::Initialize(initial_pc, mem) => {
self.current_datapath.initialize(initial_pc, mem).unwrap();
}
Command::SetExecuteSpeed(speed) => {
self.speed = speed;
}
Command::SetRegister(register, value) => {
self.current_datapath.set_register_by_str(&register, value);
}
Command::SetMemory(ptr, data) => {
self.current_datapath.set_memory(ptr, &data);
}
Command::Execute => {
self.executing = true;
}
Command::ExecuteInstruction => {
self.current_datapath.execute_instruction();
}
Command::ExecuteStage => {
self.current_datapath.execute_stage();
}
Command::Pause => {
self.executing = false;
}
Command::Reset => {
self.current_datapath.reset();
}
}
}

pub fn execute(&mut self) {
// Skip the execution phase if the emulator core is not currently executing.
if !self.executing {
return;
}
self.current_datapath.execute_instruction();
}

/// Returns the delay between CPU cycles in milliseconds for the current execution speed. Will return zero if the
/// execution speed is zero.
pub fn get_delay(&self) -> u64 {
if self.speed == 0 {
0
} else {
(1000 / self.speed).into()
}
}
}
83 changes: 74 additions & 9 deletions src/agent/datapath_communicator.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use crate::agent::datapath_reducer::DatapathReducer;
use crate::agent::messages::Command;
use crate::agent::EmulationCoreAgent;
use crate::emulation_core::architectures::AvailableDatapaths;
use futures::stream::{SplitSink, SplitStream};
use futures::FutureExt;
use futures::SinkExt;
use futures::StreamExt;
use gloo_console::log;
use gloo_console::warn;
use std::cell::RefCell;
use yew::UseForceUpdateHandle;
use yew::UseReducerDispatcher;
use yew_agent::reactor::ReactorBridge;

/// This struct provides an abstraction over all communication with the worker thread. Any commands to the worker
Expand All @@ -15,7 +18,7 @@ use yew_agent::reactor::ReactorBridge;
/// The DatapathCommunicator will also handle receiving information about the state of the emulation core and maintain
/// internal state that can be displayed by the UI.
pub struct DatapathCommunicator {
writer: RefCell<SplitSink<ReactorBridge<EmulationCoreAgent>, i32>>,
writer: RefCell<SplitSink<ReactorBridge<EmulationCoreAgent>, Command>>,
reader: RefCell<SplitStream<ReactorBridge<EmulationCoreAgent>>>,
}

Expand All @@ -29,6 +32,8 @@ impl PartialEq for &DatapathCommunicator {
}

impl DatapathCommunicator {
// General operational functions

/// Initialize the DatapathCommunicator using a bridge.
pub fn new(bridge: ReactorBridge<EmulationCoreAgent>) -> DatapathCommunicator {
let (write, read) = bridge.split();
Expand All @@ -42,7 +47,10 @@ impl DatapathCommunicator {
/// from the main app component. After updating internal state, the component this was called from will be force
/// updated.
#[allow(clippy::await_holding_refcell_ref)]
pub async fn listen_for_updates(&self, update_handle: UseForceUpdateHandle) {
pub async fn listen_for_updates(
&self,
dispatcher_handle: UseReducerDispatcher<DatapathReducer>,
) {
let mut reader = match self.reader.try_borrow_mut() {
Ok(reader) => reader,
Err(_) => {
Expand All @@ -55,21 +63,78 @@ impl DatapathCommunicator {
log!("Waiting...");
let update = reader.next().await;
log!(format!("Got update {:?}", update));
if update.is_none() {
return;
match update {
None => return,
Some(update) => dispatcher_handle.dispatch(update),
}
update_handle.force_update();
}
}

/// Sends a test message to the worker thread.
pub fn send_test_message(&self) {
fn send_message(&self, command: Command) {
let mut writer = self.writer.borrow_mut();
writer
.send(1)
// The
.send(command)
// The logic for sending a message is synchronous but the API for writing to a SplitSink is asynchronous,
// so we attempt to resolve the future immediately so we can expose a synchronous API for sending commands.
// If the future doesn't return immediately, there's serious logic changes that need to happen so we just
// log an error message and panic.
.now_or_never()
.expect("Send function did not immediately return, async logic needed.")
.expect("Sending test message error")
}

// Wrapper functions for commands

/// Sets the current emulation core to the provided architecture.
pub fn set_core(&self, _architecture: AvailableDatapaths) {
todo!()
}

/// Resets and loads the parsed/assembled instructions provided into the current emulator core.
pub fn initialize(&self, initial_pc: usize, instructions: Vec<u8>) {
self.send_message(Command::Initialize(initial_pc, instructions));
}

/// Sets the execution speed of the emulator core to the provided speed in hz. If set to zero, the emulator core
/// will execute as fast as possible.
pub fn set_execute_speed(&self, speed: u32) {
self.send_message(Command::SetExecuteSpeed(speed));
}

/// Sets the register with the provided name to the provided value.
pub fn set_register(&self, register: String, data: u64) {
self.send_message(Command::SetRegister(register, data));
}

/// Copies the contents of `data` to the emulator core's memory at `ptr`. Copies until either the end of `data` or
/// the end of the emulaot core's memory.
pub fn set_memory(&self, ptr: usize, data: Vec<u8>) {
self.send_message(Command::SetMemory(ptr, data));
}

/// Executes the emulator core at the current set speed.
pub fn execute(&self) {
self.send_message(Command::Execute);
}

/// Executes a single instruction on the emulator core and pauses.
pub fn execute_instruction(&self) {
self.send_message(Command::ExecuteInstruction);
}

/// Executes a single stage on the emulator core and pauses.
pub fn execute_stage(&self) {
self.send_message(Command::ExecuteStage);
}

/// Pauses the core. Does nothing if the emulator core is already paused.
pub fn pause_core(&self) {
self.send_message(Command::Pause);
}

/// Resets the current core to its default state.
pub fn reset(&self) {
self.send_message(Command::Reset);
}
}
58 changes: 58 additions & 0 deletions src/agent/datapath_reducer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::agent::messages::MipsStateUpdate;
use crate::emulation_core::architectures::AvailableDatapaths::MIPS;
use crate::emulation_core::architectures::{AvailableDatapaths, DatapathUpdate};
use crate::emulation_core::mips::datapath::DatapathState;
use crate::emulation_core::mips::memory::Memory;
use crate::emulation_core::mips::registers::GpRegisters;
use std::rc::Rc;
use yew::Reducible;

pub struct DatapathReducer {
pub current_architecture: AvailableDatapaths,
pub mips: MipsState,
}

#[derive(Default)]
pub struct MipsState {
pub state: DatapathState,
pub registers: GpRegisters,
pub memory: Memory,
}

impl Default for DatapathReducer {
fn default() -> Self {
Self {
current_architecture: MIPS,
mips: MipsState::default(),
}
}
}

impl Reducible for DatapathReducer {
type Action = DatapathUpdate;

fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
Rc::from(match action {
DatapathUpdate::MIPS(update) => Self {
current_architecture: AvailableDatapaths::MIPS,
mips: match update {
MipsStateUpdate::UpdateState(state) => MipsState {
state,
registers: self.mips.registers,
memory: self.mips.memory.clone(),
},
MipsStateUpdate::UpdateRegisters(registers) => MipsState {
state: self.mips.state.clone(),
registers,
memory: self.mips.memory.clone(),
},
MipsStateUpdate::UpdateMemory(memory) => MipsState {
state: self.mips.state.clone(),
registers: self.mips.registers,
memory,
},
},
},
})
}
}
28 changes: 28 additions & 0 deletions src/agent/messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use crate::emulation_core::architectures::AvailableDatapaths;
use crate::emulation_core::mips::datapath::DatapathState;
use crate::emulation_core::mips::memory::Memory;
use crate::emulation_core::mips::registers::GpRegisters;
use serde::{Deserialize, Serialize};

/// Commands sent from the UI thread to the worker thread.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Command {
SetCore(AvailableDatapaths),
Initialize(usize, Vec<u8>),
SetExecuteSpeed(u32),
SetRegister(String, u64),
SetMemory(usize, Vec<u8>),
Execute,
ExecuteInstruction,
ExecuteStage,
Pause,
Reset,
}

/// Information about the emulator core's state sent from the worker thread to the UI thread.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum MipsStateUpdate {
UpdateState(DatapathState),
UpdateRegisters(GpRegisters),
UpdateMemory(Memory),
}
Loading
Loading