How do I make an event handler that can mutate state with only one thread?

53 Views Asked by At

TL;DR: How do I make a request-response system that has multiple structs communicating with each other in a mutable way with only one thread?

I'm developing NES emulator for hobby project. I'd like to represent hardware structure in project's hierarchy and types. So I have structs that represents each chips.

In real hardware, they will communicate through wires and react to the signal which can mutate their own states. However this means shared mutable state.

Currently the struct hierarchy is like this. NES owns everything below:

NES:
- CPU6502
- SystemBus
- PPU
- Memory
- CatridgeConnector
- some other chips
How about Rc<RefCell>

Sharing mutable reference using Rc<RefCell<T>> was the first approach and it indeed worked. But it introduces so much boilerplate code and I think it's part of bottleneck (although just a hunch). And this doesn't reflect hardware structure very well. Each chip shouldn't care what it's connected to. I'd like to avoid mentioning other chip's type in struct's field.

How about borrowing and returning ownership back and forth?

So idea is this: For example, when CPU6502 wants to communicate with Memory, it'll return message that says it wants to access memory at a specific location. However this requires significant refactoring. With current hierarchy, NES will call function in CPU6502 to execute a cycle. Say it returns message to NES, then NES would order Memory to do its thing and so on.

However CPU6502 can read from memory multiple times in single function call. Returning to NES each time it needs to communicate with other chip is not possible. This behavior is not 100% accurate in real hardware but it is how it is now.

Channel? Event(or Message) System?

Current approach I'm trying is using bidirectional_channel. If CPU6502 sends request to Memory, it should respond with u8. However I have only one thread, there is nothing that handles the request. I could use multiple threads but it seems like overkill. It is just reading a single value at specific index from byte buffer. Handling multiple threads, even if it's light like tokio, would be more expensive compared to small operations that just does this: self.buffer[address - start].

And that was just reading. CPU6502 needs to write to Memory too, so mutability is also necessary.

Above was just example. All communications between each chip requires very small operation, like setting a single u16, doing bit-wise operation on u8. So I don't think multiple threads is a good idea.

1

There are 1 best solutions below

0
Chayim Friedman On

You can have channel-like functionality with callbacks: by type erasure, you can avoid having each component to know the other components, and by centralizing the components access under Nes, you can avoid the Rc. The RefCell is harder to get rid of.

use std::cell::RefCell;

pub struct Requester<Params, Ret> {
    responder: fn(&Nes, Params) -> Ret,
}

impl<Params, Ret> Requester<Params, Ret> {
    pub fn new(responder: fn(&Nes, Params) -> Ret) -> Self {
        Self { responder }
    }

    pub fn request(&self, nes: &Nes, params: Params) -> Ret {
        (self.responder)(nes, params)
    }
}

pub struct Address(pub u8);

pub struct Cpu6502 {
    memory_requester: Requester<Address, u8>,
}

impl Cpu6502 {
    pub fn new(memory_requester: Requester<Address, u8>) -> Self {
        Self { memory_requester }
    }

    pub fn execute_cycle(&mut self, nes: &Nes) {
        let data = self.memory_requester.request(nes, Address(0));
    }
}

pub struct Memory {}

impl Memory {
    pub fn new() -> Self {
        Self {}
    }

    pub fn request_address(&self, nes: &Nes, addr: Address) -> u8 {
        unimplemented!()
    }
}

pub struct Nes {
    cpu6502: RefCell<Cpu6502>,
    memory: RefCell<Memory>,
}

impl Nes {
    pub fn new() -> Self {
        let cpu6502 = RefCell::new(Cpu6502::new(Requester::new(|nes, addr| {
            nes.memory.borrow().request_address(nes, addr)
        })));
        let memory = RefCell::new(Memory::new());
        Self { cpu6502, memory }
    }

    pub fn execute_cycle(&self) {
        self.cpu6502.borrow_mut().execute_cycle(self);
    }
}

If a component only requires a shared reference for all of its operations, you can avoid wrapping it in a RefCell.