r68k icon indicating copy to clipboard operation
r68k copied to clipboard

Host callbacks

Open marhel opened this issue 7 years ago • 11 comments

As discussed in #54 some communication with the host is sometimes needed. However, which communication is relevant?

kstenerud/Musashi has the following host callbacks:

Interrupt Acknowledge
Breakpoint Acknowledge (68010+)
RESET instruction (not external reset)
PC changed (by a jump)
Set function code (encodes supervisor/user + program/data)
Instruction Hook (before every instruction)

MAME Musashi has the following host callbacks:

Interrupt Acknowledge
Breakpoint Acknowledge (68010+)
RESET instruction (for resetting external devices, not the CPU)
CMPI.L #v instruction
RTE instruction
TAS instruction write (allow/disallow writeback)

r68k already does interrupt acknowledge using a trait (see 63a00a9b), and I'm thinking RESET might belong there as well, as it is also dealing with external devices/peripherals. Function code callbacks are irrelevant in MAME or r68k as those are already sent as parameters to the memory access functions.

I realize TAS callback is needed for accuracy of emulation, but fail to see the relevance of CMPI.L or RTE instruction callbacks. I should probably try to find the MAME commit adding the support to see the discussion there.

marhel avatar Nov 28 '16 08:11 marhel

Having a callback happen on trap instructions would be nice.

emoon avatar Nov 28 '16 08:11 emoon

I know that EASy68k uses TRAP #15 for input/output, with a trap task stored in D0, so that seems a relevant use case, thanks.

Would a slightly more general exception callback work? This would cover not just TRAP but address errors, bus errors, or basically whenever any of the exception vectors are taken? I'm thinking for simple programs, one might want to actually panic!(...) on encountering a bus error, for example.

marhel avatar Nov 28 '16 09:11 marhel

Yeah that might work for example on Amiga you have load a library and then you call like this

     ; a0 = library ptr
     ; setup some input to function here
     jsr -400(a0) ; calls library function lib printf

In my case I want to patch that some how so I can call some native code for doing printf and such.

emoon avatar Nov 28 '16 09:11 emoon

OK, I implemented experimental support for an exception callback where you can completely override the handling yourself for any exception.

It works like this: If any of the known CPU exceptions occurs (address error, illegal instruction, traps, interrupts etc), the callback is called;

    fn exception_callback(core: &mut Core, ex: Exception) -> Result<Cycles>

If the return value is an Err, then the normal exception handling routines of the CPU will be called (on the returned Err), but if it's an Ok, they won't, and after deducting the cycles returned, execution simply continues with the next instruction as if the exception vector was followed, and the exception routine has returned.

This means you can just start execution at an entry point, without even setting up exception vectors at all, and for example handle TRAPs as you see fit.

As for your particular case, you would be able to setup a TRAP followed by an RTS at that address, and then handle the TRAP in your exception callback, emulating whatever library function you wanted.

However, I'm now also thinking that it could perhaps be even simpler to add a similar override capability to BSR/JSR, say a subroutine_callback, where you can allow the jump to pass through normally, or if the jump happens to be to a known library subroutine, completely override the normal BSR/JSR handling, and act as if the routine was jumped to, executed and returned.

If this would cause too much overhead for normal processing (without the subroutine callback set), it would be possible to modify the instruction set directly, and simply swap in different BSR/JSR handlers depending on if we want this capability or not.

As there are many different ways to call a subroutine beside BSR/JSR, perhaps just overriding those would be inadequate, what do you think?

marhel avatar Nov 29 '16 08:11 marhel

Override of BSR/JSR sounds interesting for sure but in my case I think having traps is easier. Because otherwise one would have to figure out inside the BSR/JSR callback what should be done.

So for me traps and the code above will be just fine, it would be nice if one could have have it as a generic though so one can supply user data to it.

something like

fn <T>exception_callback(user_data: &mut T, core: &mut Core, ex: Exception) -> Result<Cycles>

Or can user-data be put inside Core already? Anyway something like this will be needed as you likely may need to set some state in your program depending on the exception callback.

emoon avatar Nov 29 '16 08:11 emoon

I was also thinking if you ran multiple cores with the same callback, that you'd probably like some kind of user_data to differentiate the cores, unless you used the memory address of the Core as a lookup value in some dictionary. I could just add a user_data field to Core. Does it need to be generic, or would a simple u32 be good enough?

I'm having a bit of an issue making the Core signature even more convoluted, and making Core into a trait would need to wrap all register access into functions. Sure those could probably be inlined by a smart optimizer, but I would want to verify that.

marhel avatar Nov 29 '16 08:11 marhel

It would be very limited if you couldn't change the state of your program depending on what happens inside the callback. I could imagine having a an API that looks like this:

   let mut my_prog = MyProg::new();
   let mut core = r68k::core::new();
   // setup core with code etc

   loop {
     core.step_once(&mut my_prog); // step one instruction in the core
   }

step_once here needs to be a generic so when a trap happens it can be tunneled to an exception callback. I would assume step_once can be quite small and just call internal code with in the core to do actual execution etc but if a trap happens it can call the callback code with the data.

emoon avatar Nov 29 '16 08:11 emoon

Ok, I see your point now, my_prog would need to be global/static in order to be accessible from the exception_callback! And we know how Rust feels about globals.

I'll rethink a bit.

marhel avatar Nov 29 '16 08:11 marhel

Indeed. I would rather not have it as a global as they are a pain in Rust and that is usually a good thing :)

emoon avatar Nov 29 '16 09:11 emoon

Ok, so this has been implemented in 53ac2ab5 like I described earlier, but also taking your feedback into account.

How it works

There's now a Callbacks trait;

pub trait Callbacks {
   fn exception_callback(&mut self, core: &mut Core, ex: Exception) -> Result<Cycles>;
}

that can be provided when executing cycles "with state";

pub fn execute_with_state<T: Callbacks>(&mut self, cycles: i32, state: &mut T) -> Cycles

and the exception_callback will then be called on state when any CPU exception occurs;

match state.exception_callback(self, ex) {

If the return value of the exception_callback is an Err, then the normal exception handling routines of the CPU will be called (on the returned Err, which can be original exception (the ex-value provided as a parameter) or a different one), but if it's an Ok, they won't, and after deducting the cycles returned, execution simply continues with the next instruction as if the exception vector was followed, and the exception routine has returned.

This means you can just start execution at an entry point, without even setting up exception vectors at all, and for example handle TRAPs as you see fit, and the struct implementing Callbacks can contain any user state you desire.

If this is good enough, I'll close this issue.

marhel avatar Jan 08 '17 23:01 marhel

Just digged into the need for a callback for CMPI.L and RTE, and it's in MAME to support emulation of a rather devious SEGA encryption scheme using the Hitachi FD1094 CPU which is a 68000 modified to run encrypted code. So I see no need for this unless someone wants to use r68k for SEGA emulation.

marhel avatar Jul 31 '17 14:07 marhel