use crate::execution_kernel::TaskOwned;
use crate::guest_procedure::GuestIsa;
use ppc::{PpcCpu, PpcImportAction, PpcNativeReturnGpr3};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
pub(crate) use crate::execution_kernel::{CallId, ContinuationPhase, ExecutionTaskId};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct GuestCallTarget {
pub(crate) isa: GuestIsa,
pub(crate) entry: u32,
pub(crate) rtoc: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct M68kCallOrigin {
return_pc: u32,
final_sp: u32,
result: Option<M68kResultTarget>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct PowerPcCallOrigin {
return_pc: u32,
final_pc: u32,
restore_rtoc: u32,
return_gpr3: GuestCallReturnPolicy,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct M68kExecution {
entry: u32,
initial_sp: u32,
return_pc: u32,
final_sp: u32,
registers: M68kRegisterState,
result: Option<M68kResultSource>,
started: bool,
}
#[derive(Clone, Debug)]
struct PowerPcExecution {
arguments: PowerPcArguments,
return_pc: Option<u32>,
parked_cpu: Option<Box<PpcCpu>>,
completed: Option<PowerPcReturnState>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GuestCallOrigin {
M68k(M68kCallOrigin),
PowerPc(PowerPcCallOrigin),
}
#[derive(Clone, Debug)]
struct GuestCallFrame {
target: GuestCallTarget,
origin: GuestCallOrigin,
m68k_execution: Option<M68kExecution>,
powerpc_execution: Option<PowerPcExecution>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PendingM68kExecution {
pub(crate) entry: u32,
pub(crate) initial_sp: u32,
pub(crate) return_pc: u32,
pub(crate) final_sp: u32,
pub(crate) registers: M68kRegisterState,
pub(crate) result: Option<M68kResultSource>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct M68kRegisterState {
pub(crate) data: [u32; 8],
pub(crate) address: [u32; 7],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum M68kResultSource {
Data(u8),
Address(u8),
Memory {
address: u32,
size: u8,
},
SpecialCase {
selector: u8,
arguments: PowerPcArguments,
stack_result: Option<u32>,
},
}
pub(crate) const MAX_POWERPC_GUEST_ARGUMENTS: usize = 13;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PowerPcArguments {
values: [u32; MAX_POWERPC_GUEST_ARGUMENTS],
len: u8,
}
impl PowerPcArguments {
pub(crate) fn from_slice(values: &[u32]) -> Option<Self> {
if values.len() > MAX_POWERPC_GUEST_ARGUMENTS {
return None;
}
let mut arguments = Self {
values: [0; MAX_POWERPC_GUEST_ARGUMENTS],
len: u8::try_from(values.len()).ok()?,
};
arguments.values[..values.len()].copy_from_slice(values);
Some(arguments)
}
pub(crate) fn as_slice(&self) -> &[u32] {
&self.values[..usize::from(self.len)]
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum M68kResultTarget {
Data { index: u8, size: u8 },
Address { index: u8, size: u8 },
Ccr { mask: u8 },
Memory { address: u32, size: u8 },
SpecialCase { selector: u8, scratch: u32 },
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GuestCallReturnPolicy {
Preserve,
Mask(u32),
Set(u32),
ZeroOrSet { zero: u32, nonzero: u32 },
CrBit(u8),
XerCa,
XerOv,
}
impl From<PpcNativeReturnGpr3> for GuestCallReturnPolicy {
fn from(policy: PpcNativeReturnGpr3) -> Self {
match policy {
PpcNativeReturnGpr3::Preserve => Self::Preserve,
PpcNativeReturnGpr3::Mask(mask) => Self::Mask(mask),
PpcNativeReturnGpr3::Set(value) => Self::Set(value),
PpcNativeReturnGpr3::ZeroOrSet { zero, nonzero } => Self::ZeroOrSet { zero, nonzero },
PpcNativeReturnGpr3::CrBit(bit_index) => Self::CrBit(bit_index),
PpcNativeReturnGpr3::XerCa => Self::XerCa,
PpcNativeReturnGpr3::XerOv => Self::XerOv,
}
}
}
impl From<GuestCallReturnPolicy> for PpcNativeReturnGpr3 {
fn from(policy: GuestCallReturnPolicy) -> Self {
match policy {
GuestCallReturnPolicy::Preserve => Self::Preserve,
GuestCallReturnPolicy::Mask(mask) => Self::Mask(mask),
GuestCallReturnPolicy::Set(value) => Self::Set(value),
GuestCallReturnPolicy::ZeroOrSet { zero, nonzero } => Self::ZeroOrSet { zero, nonzero },
GuestCallReturnPolicy::CrBit(bit_index) => Self::CrBit(bit_index),
GuestCallReturnPolicy::XerCa => Self::XerCa,
GuestCallReturnPolicy::XerOv => Self::XerOv,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PowerPcReturnState {
pub(crate) gpr3: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct PendingPowerPcExecution {
pub(crate) target: GuestCallTarget,
pub(crate) arguments: PowerPcArguments,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct M68kResume {
pub(crate) return_pc: u32,
pub(crate) final_sp: u32,
pub(crate) result: Option<M68kResultTarget>,
pub(crate) powerpc: PowerPcReturnState,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct M68kCallRequest {
pub(crate) entry: u32,
pub(crate) initial_sp: u32,
pub(crate) final_sp: u32,
pub(crate) registers: M68kRegisterState,
pub(crate) result: Option<M68kResultSource>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GuestCallArguments {
None,
PowerPc(PowerPcArguments),
M68k(M68kCallRequest),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct GuestCallRequest {
pub(crate) task: ExecutionTaskId,
pub(crate) target: GuestCallTarget,
pub(crate) arguments: GuestCallArguments,
}
impl GuestCallRequest {
pub(crate) fn new(target: GuestCallTarget) -> Self {
Self::for_task(ExecutionTaskId::APPLICATION, target)
}
pub(crate) fn for_task(task: ExecutionTaskId, target: GuestCallTarget) -> Self {
Self {
task,
target,
arguments: GuestCallArguments::None,
}
}
pub(crate) fn with_powerpc_arguments(mut self, arguments: PowerPcArguments) -> Self {
self.arguments = GuestCallArguments::PowerPc(arguments);
self
}
pub(crate) fn with_m68k_request(mut self, request: M68kCallRequest) -> Self {
self.arguments = GuestCallArguments::M68k(request);
self
}
fn with_task(mut self, task: ExecutionTaskId) -> Self {
self.task = task;
self
}
}
impl TaskOwned for GuestCallRequest {
fn task(&self) -> ExecutionTaskId {
self.task
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GuestCallContinuation {
ReturnToM68k {
return_pc: u32,
final_sp: u32,
result: Option<M68kResultTarget>,
},
ReturnToPowerPc {
return_pc: u32,
final_pc: u32,
restore_rtoc: u32,
return_gpr3: GuestCallReturnPolicy,
},
}
impl GuestCallContinuation {
pub(crate) const fn to_m68k(
return_pc: u32,
final_sp: u32,
result: Option<M68kResultTarget>,
) -> Self {
Self::ReturnToM68k {
return_pc,
final_sp,
result,
}
}
pub(crate) fn to_powerpc(
return_pc: u32,
final_pc: u32,
restore_rtoc: u32,
return_gpr3: impl Into<GuestCallReturnPolicy>,
) -> Self {
Self::ReturnToPowerPc {
return_pc,
final_pc,
restore_rtoc,
return_gpr3: return_gpr3.into(),
}
}
}
pub(crate) type ContinuationStore =
crate::execution_kernel::ContinuationStore<GuestCallRequest, GuestCallContinuation>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GuestCallEffect {
CallGuest {
request: GuestCallRequest,
continuation: GuestCallContinuation,
},
}
impl GuestCallEffect {
pub(crate) const fn call_guest(
request: GuestCallRequest,
continuation: GuestCallContinuation,
) -> Self {
Self::CallGuest {
request,
continuation,
}
}
fn with_task(self, task: ExecutionTaskId) -> Self {
match self {
Self::CallGuest {
request,
continuation,
} => Self::CallGuest {
request: request.with_task(task),
continuation,
},
}
}
pub(crate) fn request(self) -> GuestCallRequest {
match self {
Self::CallGuest { request, .. } => request,
}
}
pub(crate) fn into_ppc_import_action(self) -> Option<PpcImportAction> {
let Self::CallGuest {
request,
continuation:
GuestCallContinuation::ReturnToPowerPc {
return_pc,
final_pc,
restore_rtoc,
return_gpr3,
},
} = self
else {
return None;
};
if request.target.isa != GuestIsa::PowerPc
|| !matches!(request.arguments, GuestCallArguments::None)
{
return None;
}
Some(PpcImportAction::CallNative {
entry: request.target.entry,
rtoc: request.target.rtoc,
return_pc,
final_pc,
restore_rtoc,
return_gpr3: return_gpr3.into(),
})
}
pub(crate) fn from_ppc_import_action(action: PpcImportAction) -> Option<Self> {
let PpcImportAction::CallNative {
entry,
rtoc,
return_pc,
final_pc,
restore_rtoc,
return_gpr3,
} = action
else {
return None;
};
Some(Self::call_guest(
GuestCallRequest::new(GuestCallTarget {
isa: GuestIsa::PowerPc,
entry,
rtoc,
}),
GuestCallContinuation::to_powerpc(
return_pc,
final_pc,
restore_rtoc,
GuestCallReturnPolicy::from(return_gpr3),
),
))
}
fn into_frame(self) -> Option<GuestCallFrame> {
let Self::CallGuest {
request,
continuation,
} = self;
let target = request.target;
match (target.isa, request.arguments, continuation) {
(
GuestIsa::M68k,
GuestCallArguments::None,
GuestCallContinuation::ReturnToM68k {
return_pc,
final_sp,
result,
},
) => Some(GuestCallFrame {
target,
origin: GuestCallOrigin::M68k(M68kCallOrigin {
return_pc,
final_sp,
result,
}),
m68k_execution: None,
powerpc_execution: None,
}),
(
GuestIsa::PowerPc,
GuestCallArguments::PowerPc(arguments),
GuestCallContinuation::ReturnToM68k {
return_pc,
final_sp,
result,
},
) => Some(GuestCallFrame {
target,
origin: GuestCallOrigin::M68k(M68kCallOrigin {
return_pc,
final_sp,
result,
}),
m68k_execution: None,
powerpc_execution: Some(PowerPcExecution {
arguments,
return_pc: None,
parked_cpu: None,
completed: None,
}),
}),
(
GuestIsa::PowerPc,
GuestCallArguments::None,
GuestCallContinuation::ReturnToPowerPc {
return_pc,
final_pc,
restore_rtoc,
return_gpr3,
},
) => Some(GuestCallFrame {
target,
origin: GuestCallOrigin::PowerPc(PowerPcCallOrigin {
return_pc,
final_pc,
restore_rtoc,
return_gpr3,
}),
m68k_execution: None,
powerpc_execution: None,
}),
(
GuestIsa::M68k,
GuestCallArguments::M68k(request),
GuestCallContinuation::ReturnToPowerPc {
return_pc,
final_pc,
restore_rtoc,
return_gpr3,
},
) => Some(GuestCallFrame {
target,
origin: GuestCallOrigin::PowerPc(PowerPcCallOrigin {
return_pc,
final_pc,
restore_rtoc,
return_gpr3,
}),
m68k_execution: Some(M68kExecution {
entry: request.entry,
initial_sp: request.initial_sp,
return_pc,
final_sp: request.final_sp,
registers: request.registers,
result: request.result,
started: false,
}),
powerpc_execution: None,
}),
_ => None,
}
}
}
pub(crate) fn format_ppc_import_action(action: &PpcImportAction) -> String {
match action {
PpcImportAction::Return(value) => format!("return(${:08X})", value),
PpcImportAction::ReturnPreserve => "return-preserve".to_string(),
PpcImportAction::ReturnPreserveWithExtraCycles(extra_cycles) => {
format!("return-preserve+{}cycles", extra_cycles)
}
PpcImportAction::ReturnWithExtraCycles(value, extra_cycles) => {
format!("return(${:08X})+{}cycles", value, extra_cycles)
}
PpcImportAction::Continue => "continue".to_string(),
PpcImportAction::Yield(cycles) => format!("yield({cycles}cycles)"),
PpcImportAction::CallNative {
entry,
rtoc,
return_pc,
final_pc,
restore_rtoc,
..
} => format!(
"call-native(entry=${:08X},rtoc=${:08X},return_pc=${:08X},final_pc=${:08X},restore_rtoc=${:08X})",
entry, rtoc, return_pc, final_pc, restore_rtoc
),
PpcImportAction::RaiseException(exception) => format!("exception({:?})", exception),
PpcImportAction::Halt => "halt".to_string(),
}
}
impl PartialEq for GuestCallFrame {
fn eq(&self, other: &Self) -> bool {
self.target == other.target
&& self.origin == other.origin
&& self.m68k_execution == other.m68k_execution
&& match (&self.powerpc_execution, &other.powerpc_execution) {
(None, None) => true,
(Some(left), Some(right)) => {
left.arguments == right.arguments
&& left.return_pc == right.return_pc
&& left.parked_cpu.is_some() == right.parked_cpu.is_some()
&& left.completed == right.completed
}
_ => false,
}
}
}
impl Eq for GuestCallFrame {}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ExecutionTaskCalls {
kernel: ContinuationStore,
frames: HashMap<CallId, GuestCallFrame>,
}
impl Default for ExecutionTaskCalls {
fn default() -> Self {
Self {
kernel: ContinuationStore::default(),
frames: HashMap::new(),
}
}
}
impl ExecutionTaskCalls {
fn is_empty(&self) -> bool {
self.kernel.is_empty()
}
}
#[derive(Debug, Default)]
pub(crate) struct SharedGuestCallStack(Rc<RefCell<ExecutionTaskCalls>>);
impl PartialEq for SharedGuestCallStack {
fn eq(&self, other: &Self) -> bool {
*self.0.borrow() == *other.0.borrow()
}
}
impl Eq for SharedGuestCallStack {}
impl Clone for SharedGuestCallStack {
fn clone(&self) -> Self {
Self(Rc::new(RefCell::new(self.0.borrow().clone())))
}
}
impl SharedGuestCallStack {
fn all_tasks_idle(&self) -> bool {
self.0.borrow().is_empty()
}
pub(crate) fn shared_handle(&self) -> Self {
Self(Rc::clone(&self.0))
}
pub(crate) fn attach_to(&mut self, process_calls: &Self) {
if Rc::ptr_eq(&self.0, &process_calls.0) {
return;
}
assert!(
self.all_tasks_idle() || process_calls.all_tasks_idle(),
"cannot attach two active guest-procedure continuation stacks"
);
let pending = std::mem::take(&mut *self.0.borrow_mut());
self.0 = Rc::clone(&process_calls.0);
if !pending.is_empty() {
*self.0.borrow_mut() = pending;
}
}
#[cfg(test)]
pub(crate) fn is_empty(&self) -> bool {
self.all_tasks_idle()
}
pub(crate) fn depth(&self) -> usize {
self.0.borrow().kernel.depth()
}
pub(crate) fn current_task(&self) -> ExecutionTaskId {
self.0.borrow().kernel.current_task()
}
pub(crate) fn task_is_empty(&self, task: ExecutionTaskId) -> bool {
self.0.borrow().kernel.task_is_empty(task)
}
pub(crate) fn switch_to_task(&self, task: ExecutionTaskId) {
self.0.borrow().kernel.switch_to_task(task);
}
pub(crate) fn remove_task(&self, task: ExecutionTaskId) -> bool {
let tasks = self.0.borrow_mut();
if tasks.kernel.retire_task(task).is_err() {
return false;
}
true
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.0.borrow().kernel.len()
}
#[cfg(test)]
pub(crate) fn task_depth(&self, task: ExecutionTaskId) -> usize {
self.0.borrow().kernel.task_depth(task)
}
fn top_frame(&self) -> Option<(CallId, GuestCallFrame)> {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = tasks.kernel.peek(task)?;
let frame = tasks.frames.get(&semantic.call_id())?;
Some((semantic.call_id(), frame.clone()))
}
fn submit_effect(&self, effect: GuestCallEffect) -> Option<CallId> {
let effect = effect.with_task(self.current_task());
let GuestCallEffect::CallGuest {
request,
continuation,
} = effect;
let Some(frame) = GuestCallEffect::call_guest(request, continuation).into_frame() else {
return None;
};
let task = self.current_task();
let mut tasks = self.0.borrow_mut();
let call_id = tasks.kernel.submit(task, request, continuation).ok()?;
tasks.frames.insert(call_id, frame);
Some(call_id)
}
fn push_effect(&self, effect: GuestCallEffect) -> bool {
self.submit_effect(effect).is_some()
}
pub(crate) fn begin_m68k(
&self,
target: GuestCallTarget,
return_pc: u32,
final_sp: u32,
) -> bool {
debug_assert_eq!(target.isa, GuestIsa::M68k);
self.push_effect(GuestCallEffect::call_guest(
GuestCallRequest::new(target),
GuestCallContinuation::to_m68k(return_pc, final_sp, None),
))
}
pub(crate) fn begin_m68k_to_powerpc(
&self,
target: GuestCallTarget,
arguments: PowerPcArguments,
return_pc: u32,
final_sp: u32,
result: Option<M68kResultTarget>,
) -> bool {
if self.has_powerpc_from_m68k() {
return false;
}
debug_assert_eq!(target.isa, GuestIsa::PowerPc);
self.push_effect(GuestCallEffect::call_guest(
GuestCallRequest::new(target).with_powerpc_arguments(arguments),
GuestCallContinuation::to_m68k(return_pc, final_sp, result),
))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn begin_powerpc_to_m68k(
&self,
target: GuestCallTarget,
entry: u32,
initial_sp: u32,
return_pc: u32,
final_sp: u32,
registers: M68kRegisterState,
result: Option<M68kResultSource>,
final_pc: u32,
restore_rtoc: u32,
return_gpr3: impl Into<GuestCallReturnPolicy>,
) -> bool {
debug_assert_eq!(target.isa, GuestIsa::M68k);
self.push_effect(GuestCallEffect::call_guest(
GuestCallRequest::new(target).with_m68k_request(M68kCallRequest {
entry,
initial_sp,
final_sp,
registers,
result,
}),
GuestCallContinuation::to_powerpc(return_pc, final_pc, restore_rtoc, return_gpr3),
))
}
pub(crate) fn pending_powerpc_from_m68k(&self) -> Option<PendingPowerPcExecution> {
let (_, frame) = self.top_frame()?;
let GuestCallOrigin::M68k(_) = frame.origin else {
return None;
};
let execution = frame.powerpc_execution.as_ref()?;
(execution.return_pc.is_none() && execution.completed.is_none()).then_some(
PendingPowerPcExecution {
target: frame.target,
arguments: execution.arguments,
},
)
}
pub(crate) fn activate_powerpc_from_m68k(
&self,
cpu: &mut PpcCpu,
return_pc: u32,
) -> Option<PendingPowerPcExecution> {
let (task, call_id, target, arguments) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = tasks.kernel.peek(task)?;
let frame = tasks.frames.get(&semantic.call_id())?;
let GuestCallOrigin::M68k(_) = frame.origin else {
return None;
};
let execution = frame.powerpc_execution.as_ref()?;
if execution.return_pc.is_some() || execution.completed.is_some() {
return None;
}
(task, semantic.call_id(), frame.target, execution.arguments)
};
let mut tasks = self.0.borrow_mut();
tasks.kernel.activate(task, call_id).ok()?;
let frame = tasks
.frames
.get_mut(&call_id)
.expect("semantic continuation must have an adapter frame");
let execution = frame
.powerpc_execution
.as_mut()
.expect("validated PowerPC transition must have an execution payload");
execution.parked_cpu = Some(Box::new(cpu.clone()));
execution.return_pc = Some(return_pc);
Some(PendingPowerPcExecution { target, arguments })
}
pub(crate) fn has_powerpc_from_m68k(&self) -> bool {
self.top_frame().is_some_and(|(_, frame)| {
matches!(frame.origin, GuestCallOrigin::M68k(_)) && frame.powerpc_execution.is_some()
})
}
pub(crate) fn suspended_m68k_context_depth(&self) -> usize {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
tasks
.kernel
.task_states(task)
.iter()
.filter_map(|semantic| tasks.frames.get(&semantic.call_id()))
.filter(|frame| {
matches!(frame.origin, GuestCallOrigin::M68k(_))
&& frame
.powerpc_execution
.as_ref()
.is_some_and(|execution| execution.return_pc.is_some())
})
.count()
}
pub(crate) fn complete_powerpc_for_m68k(&self, cpu: &mut PpcCpu) -> bool {
let (task, call_id, parked_cpu) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = match tasks.kernel.peek(task) {
Some(semantic) => semantic,
None => return false,
};
let frame = match tasks.frames.get(&semantic.call_id()) {
Some(frame) => frame,
None => return false,
};
let GuestCallOrigin::M68k(_) = frame.origin else {
return false;
};
let Some(execution) = frame.powerpc_execution.as_ref() else {
return false;
};
if execution.completed.is_some() || execution.return_pc != Some(cpu.pc) {
return false;
}
let Some(parked_cpu) = execution.parked_cpu.as_ref() else {
return false;
};
(task, semantic.call_id(), parked_cpu.clone())
};
let result = PowerPcReturnState { gpr3: cpu.gpr[3] };
let elapsed_time_base = cpu.time_base();
let mut tasks = self.0.borrow_mut();
if tasks
.kernel
.complete(task, call_id, Some(result.gpr3))
.is_err()
{
return false;
}
let frame = tasks
.frames
.get_mut(&call_id)
.expect("semantic continuation must have an adapter frame");
let execution = frame
.powerpc_execution
.as_mut()
.expect("validated PowerPC transition must have an execution payload");
execution.parked_cpu.take();
execution.completed = Some(result);
*cpu = *parked_cpu;
cpu.set_time_base(elapsed_time_base);
true
}
pub(crate) fn peek_m68k_resume(&self) -> Option<M68kResume> {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = tasks.kernel.peek(task)?;
let frame = tasks.frames.get(&semantic.call_id())?;
let GuestCallOrigin::M68k(origin) = frame.origin else {
return None;
};
let powerpc = frame.powerpc_execution.as_ref()?.completed?;
Some(M68kResume {
return_pc: origin.return_pc,
final_sp: origin.final_sp,
result: origin.result,
powerpc,
})
}
pub(crate) fn retire_m68k_resume(&self) -> bool {
let (task, call_id) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let Some(semantic) = tasks.kernel.peek(task) else {
return false;
};
let Some(frame) = tasks.frames.get(&semantic.call_id()) else {
return false;
};
if !matches!(frame.origin, GuestCallOrigin::M68k(_))
|| frame
.powerpc_execution
.as_ref()
.and_then(|execution| execution.completed)
.is_none()
{
return false;
}
(task, semantic.call_id())
};
let mut tasks = self.0.borrow_mut();
if tasks.kernel.retire(task, call_id).is_err() {
return false;
}
tasks.frames.remove(&call_id).is_some()
}
#[cfg(test)]
pub(crate) fn take_m68k_resume(&self) -> Option<M68kResume> {
let resume = self.peek_m68k_resume()?;
self.retire_m68k_resume().then_some(resume)
}
pub(crate) fn activate_m68k(&self) -> Option<PendingM68kExecution> {
let (task, call_id, pending, started) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = tasks.kernel.peek(task)?;
let frame = tasks.frames.get(&semantic.call_id())?;
let execution = frame.m68k_execution.as_ref()?;
let pending = PendingM68kExecution {
entry: execution.entry,
initial_sp: execution.initial_sp,
return_pc: execution.return_pc,
final_sp: execution.final_sp,
registers: execution.registers,
result: execution.result,
};
(task, semantic.call_id(), pending, execution.started)
};
if started {
return Some(pending);
}
let mut tasks = self.0.borrow_mut();
tasks.kernel.activate(task, call_id).ok()?;
let frame = tasks
.frames
.get_mut(&call_id)
.expect("semantic continuation must have an adapter frame");
frame
.m68k_execution
.as_mut()
.expect("validated 68k transition must have an execution payload")
.started = true;
Some(pending)
}
pub(crate) fn active_m68k(&self) -> Option<PendingM68kExecution> {
let (_, frame) = self.top_frame()?;
let execution = frame.m68k_execution?;
execution.started.then_some(PendingM68kExecution {
entry: execution.entry,
initial_sp: execution.initial_sp,
return_pc: execution.return_pc,
final_sp: execution.final_sp,
registers: execution.registers,
result: execution.result,
})
}
pub(crate) fn has_m68k_execution(&self) -> bool {
self.top_frame()
.is_some_and(|(_, frame)| frame.m68k_execution.is_some())
}
pub(crate) fn externalize_powerpc_action(
&self,
cpu: &mut PpcCpu,
action: PpcImportAction,
) -> PpcImportAction {
let Some(effect) = GuestCallEffect::from_ppc_import_action(action) else {
return action;
};
let Some(call_id) = self.submit_effect(effect) else {
return action;
};
let task = self.current_task();
{
let tasks = self.0.borrow_mut();
if tasks.kernel.activate(task, call_id).is_err() {
let _ = tasks.kernel.cancel_pending(task, call_id);
drop(tasks);
self.0.borrow_mut().frames.remove(&call_id);
return action;
}
}
let target = effect.request().target;
cpu.pc = target.entry;
let GuestCallEffect::CallGuest { continuation, .. } = effect;
let GuestCallContinuation::ReturnToPowerPc { return_pc, .. } = continuation else {
unreachable!("PPC action conversion always has a PowerPC continuation");
};
cpu.lr = return_pc;
cpu.gpr[2] = target.rtoc;
PpcImportAction::Continue
}
pub(crate) fn complete_powerpc(&self, cpu: &mut PpcCpu) -> bool {
let (task, call_id, origin) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = match tasks.kernel.peek(task) {
Some(semantic) => semantic,
None => return false,
};
let frame = match tasks.frames.get(&semantic.call_id()) {
Some(frame) => frame,
None => return false,
};
let GuestCallOrigin::PowerPc(origin) = frame.origin else {
return false;
};
if frame.target.isa != GuestIsa::PowerPc || cpu.pc != origin.return_pc {
return false;
}
(task, semantic.call_id(), origin)
};
let mut tasks = self.0.borrow_mut();
if tasks
.kernel
.complete(task, call_id, Some(cpu.gpr[3]))
.is_err()
{
return false;
}
let _ = tasks
.kernel
.retire(task, call_id)
.expect("completed native continuation must retire transactionally");
tasks
.frames
.remove(&call_id)
.expect("semantic continuation must have an adapter frame");
Self::apply_powerpc_return(cpu, origin);
true
}
pub(crate) fn complete_m68k_for_powerpc(
&self,
post_call_pc: u32,
final_sp: u32,
result: Option<u32>,
cpu: &mut PpcCpu,
) -> bool {
let (task, call_id, origin) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let semantic = match tasks.kernel.peek(task) {
Some(semantic) => semantic,
None => return false,
};
let frame = match tasks.frames.get(&semantic.call_id()) {
Some(frame) => frame,
None => return false,
};
let GuestCallOrigin::PowerPc(origin) = frame.origin else {
return false;
};
let Some(execution) = frame.m68k_execution else {
return false;
};
let result_required = match execution.result {
None => Some(false),
Some(M68kResultSource::SpecialCase { selector, .. }) => {
let proc_info = crate::mixed_mode::proc_info::SPECIAL_CASE
| (u32::from(selector) << crate::mixed_mode::special_case::SELECTOR_PHASE);
crate::mixed_mode::native_special_case_signature(proc_info).map(|signature| {
signature.result != crate::mixed_mode::NativeSpecialCaseResult::Void
})
}
Some(_) => Some(true),
};
if !execution.started
|| post_call_pc != execution.return_pc
|| final_sp != execution.final_sp
|| result_required.is_none()
|| result.is_some() != result_required.unwrap_or(false)
{
return false;
}
(task, semantic.call_id(), origin)
};
let mut tasks = self.0.borrow_mut();
if tasks.kernel.complete(task, call_id, result).is_err() {
return false;
}
let _ = tasks
.kernel
.retire(task, call_id)
.expect("completed 68k continuation must retire transactionally");
tasks
.frames
.remove(&call_id)
.expect("semantic continuation must have an adapter frame");
if let Some(result) = result {
cpu.gpr[3] = result;
}
Self::apply_powerpc_return(cpu, origin);
true
}
fn apply_powerpc_return(cpu: &mut PpcCpu, origin: PowerPcCallOrigin) {
match origin.return_gpr3 {
GuestCallReturnPolicy::Preserve => {}
GuestCallReturnPolicy::Mask(mask) => cpu.gpr[3] &= mask,
GuestCallReturnPolicy::Set(value) => cpu.gpr[3] = value,
GuestCallReturnPolicy::ZeroOrSet { zero, nonzero } => {
cpu.gpr[3] = if cpu.gpr[3] == 0 { zero } else { nonzero };
}
GuestCallReturnPolicy::CrBit(bit_index) => {
cpu.gpr[3] = u32::from(cpu.cr_bit(bit_index));
}
GuestCallReturnPolicy::XerCa => cpu.gpr[3] = u32::from(cpu.xer_ca()),
GuestCallReturnPolicy::XerOv => cpu.gpr[3] = u32::from(cpu.xer_ov()),
}
cpu.gpr[2] = origin.restore_rtoc;
cpu.lr = origin.final_pc;
cpu.pc = origin.final_pc;
}
pub(crate) fn complete_m68k(&self, post_trap_pc: u32, final_sp: u32) -> bool {
let (task, call_id) = {
let tasks = self.0.borrow();
let task = tasks.kernel.current_task();
let Some(semantic) = tasks.kernel.peek(task) else {
return false;
};
let Some(frame) = tasks.frames.get(&semantic.call_id()) else {
return false;
};
if frame.target.isa != GuestIsa::M68k
|| !matches!(
frame.origin,
GuestCallOrigin::M68k(origin)
if origin.return_pc.wrapping_add(2) == post_trap_pc
&& origin.final_sp == final_sp
)
{
return false;
}
(task, semantic.call_id())
};
let mut tasks = self.0.borrow_mut();
let phase = tasks
.kernel
.peek(task)
.expect("semantic continuation must have an adapter frame")
.phase();
if phase == ContinuationPhase::Pending && tasks.kernel.activate(task, call_id).is_err() {
return false;
}
if tasks.kernel.complete(task, call_id, None).is_err() {
return false;
}
let _ = tasks
.kernel
.retire(task, call_id)
.expect("completed 68k continuation must retire transactionally");
tasks
.frames
.remove(&call_id)
.expect("semantic continuation must have an adapter frame");
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::GuestAddressSpace;
use ppc::PpcRunResult;
const RETURN_PC: u32 = 0x01f0_4000;
fn native_action(
entry: u32,
final_pc: u32,
return_gpr3: PpcNativeReturnGpr3,
) -> PpcImportAction {
GuestCallEffect::call_guest(
GuestCallRequest::new(GuestCallTarget {
isa: GuestIsa::PowerPc,
entry,
rtoc: entry + 0x100,
}),
GuestCallContinuation::to_powerpc(RETURN_PC, final_pc, final_pc + 0x100, return_gpr3),
)
.into_ppc_import_action()
.expect("native PowerPC request should adapt to CallNative")
}
#[test]
fn explicit_shared_handles_share_live_frames_while_clone_is_detached() {
let calls = SharedGuestCallStack::default();
let shared = calls.shared_handle();
let detached = calls.clone();
calls.begin_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x1000,
rtoc: 0,
},
0x2000,
0x3000,
);
assert_eq!(calls.len(), 1);
assert_eq!(shared.len(), 1);
assert!(detached.is_empty());
assert!(shared.complete_m68k(0x2002, 0x3000));
assert!(calls.is_empty());
}
#[test]
fn execution_tasks_keep_independent_continuation_stacks() {
let calls = SharedGuestCallStack::default();
calls.begin_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x1000,
rtoc: 0,
},
0x2000,
0x3000,
);
let worker = ExecutionTaskId::from_thread_id(7);
calls.switch_to_task(worker);
assert_eq!(calls.depth(), 0);
calls.begin_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x4000,
rtoc: 0,
},
0x5000,
0x6000,
);
assert!(!calls.remove_task(worker));
assert!(calls.complete_m68k(0x5002, 0x6000));
calls.switch_to_task(ExecutionTaskId::APPLICATION);
assert_eq!(calls.depth(), 1);
assert!(calls.complete_m68k(0x2002, 0x3000));
assert!(calls.remove_task(worker));
assert!(calls.is_empty());
}
#[test]
fn global_stack_queries_include_suspended_tasks() {
let calls = SharedGuestCallStack::default();
let worker = ExecutionTaskId::from_thread_id(7);
calls.switch_to_task(worker);
calls.begin_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x4000,
rtoc: 0,
},
0x5000,
0x6000,
);
calls.switch_to_task(ExecutionTaskId::APPLICATION);
assert_eq!(calls.depth(), 0, "the active task has no continuation");
assert_eq!(calls.len(), 1, "the process still has a suspended frame");
assert!(
!calls.is_empty(),
"suspended tasks keep the owner non-empty"
);
}
#[test]
fn attachment_preserves_pending_frames_when_the_process_owner_is_empty() {
let process_calls = SharedGuestCallStack::default();
let mut adapter_calls = SharedGuestCallStack::default();
adapter_calls.begin_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x1000,
rtoc: 0,
},
0x2000,
0x3000,
);
adapter_calls.attach_to(&process_calls);
assert_eq!(process_calls.len(), 1);
assert!(adapter_calls.complete_m68k(0x2002, 0x3000));
assert!(process_calls.is_empty());
}
#[test]
fn powerpc_action_is_externalized_and_restored_by_the_process_owner() {
let calls = SharedGuestCallStack::default();
let mut cpu = PpcCpu::new();
let action = calls.externalize_powerpc_action(
&mut cpu,
native_action(0x1000, 0x2000, PpcNativeReturnGpr3::Mask(0xff)),
);
assert_eq!(action, PpcImportAction::Continue);
assert_eq!((cpu.pc, cpu.lr, cpu.gpr[2]), (0x1000, RETURN_PC, 0x1100));
assert_eq!(calls.len(), 1);
cpu.pc = RETURN_PC;
cpu.gpr[3] = 0x1234;
assert!(calls.complete_powerpc(&mut cpu));
assert_eq!((cpu.pc, cpu.lr, cpu.gpr[2]), (0x2000, 0x2000, 0x2100));
assert_eq!(cpu.gpr[3], 0x34);
assert!(calls.is_empty());
}
#[test]
fn powerpc_continuation_survives_across_cpu_execution_slices() {
let calls = SharedGuestCallStack::default();
let mut cpu = PpcCpu::new();
let mut memory = GuestAddressSpace::new();
memory.add_region(0x1000, 0x4e80_0020u32.to_be_bytes().to_vec());
memory.add_region(RETURN_PC, vec![0; 4]);
calls.externalize_powerpc_action(
&mut cpu,
native_action(0x1000, 0x2000, PpcNativeReturnGpr3::Preserve),
);
assert_eq!(
cpu.run_with_imports(&mut memory, 1, 0, RETURN_PC, 1, |_, _, _| {
PpcImportAction::Halt
}),
PpcRunResult::CycleLimit { cycles: 1 }
);
assert_eq!(cpu.pc, RETURN_PC);
assert_eq!(calls.len(), 1);
assert_eq!(
cpu.run_with_imports(&mut memory, 1, 0, RETURN_PC, 1, |_, cpu, _| {
assert!(calls.complete_powerpc(cpu));
PpcImportAction::Continue
}),
PpcRunResult::CycleLimit { cycles: 1 }
);
assert_eq!(cpu.pc, 0x2000);
assert!(calls.is_empty());
}
#[test]
fn nested_powerpc_calls_complete_in_lifo_order() {
let calls = SharedGuestCallStack::default();
let mut cpu = PpcCpu::new();
calls.externalize_powerpc_action(
&mut cpu,
native_action(0x1000, 0x2000, PpcNativeReturnGpr3::Preserve),
);
calls.externalize_powerpc_action(
&mut cpu,
native_action(0x3000, 0x4000, PpcNativeReturnGpr3::Set(7)),
);
cpu.pc = RETURN_PC;
assert!(calls.complete_powerpc(&mut cpu));
assert_eq!((cpu.pc, cpu.gpr[3]), (0x4000, 7));
assert_eq!(calls.len(), 1);
cpu.pc = RETURN_PC;
assert!(calls.complete_powerpc(&mut cpu));
assert_eq!(cpu.pc, 0x2000);
assert!(calls.is_empty());
}
#[test]
fn reverse_transition_parks_and_restores_the_native_context_and_elapsed_time() {
let calls = SharedGuestCallStack::default();
let arguments = PowerPcArguments::from_slice(&[1, 2, 3]).unwrap();
assert!(calls.begin_m68k_to_powerpc(
GuestCallTarget {
isa: GuestIsa::PowerPc,
entry: 0x1000,
rtoc: 0x2000,
},
arguments,
0x3000,
0x4000,
Some(M68kResultTarget::Data { index: 2, size: 2 }),
));
let mut cpu = PpcCpu::new();
cpu.pc = 0x5000;
cpu.lr = 0x6000;
cpu.gpr[1] = 0x7000;
cpu.gpr[2] = 0x8000;
cpu.gpr[3] = 0x9000;
cpu.set_time_base(10);
assert_eq!(
calls.activate_powerpc_from_m68k(&mut cpu, RETURN_PC),
Some(PendingPowerPcExecution {
target: GuestCallTarget {
isa: GuestIsa::PowerPc,
entry: 0x1000,
rtoc: 0x2000,
},
arguments,
})
);
assert!(calls
.activate_powerpc_from_m68k(&mut cpu, RETURN_PC)
.is_none());
cpu.pc = RETURN_PC;
cpu.lr = 0xaaaa;
cpu.gpr[1] = 0xbbbb;
cpu.gpr[2] = 0xcccc;
cpu.gpr[3] = 0x1234_5678;
cpu.set_time_base(50);
assert!(!calls.complete_powerpc(&mut cpu));
assert!(calls.complete_powerpc_for_m68k(&mut cpu));
assert_eq!((cpu.pc, cpu.lr), (0x5000, 0x6000));
assert_eq!(
(cpu.gpr[1], cpu.gpr[2], cpu.gpr[3]),
(0x7000, 0x8000, 0x9000)
);
assert_eq!(cpu.time_base(), 50);
let resume = calls.take_m68k_resume().unwrap();
assert_eq!((resume.return_pc, resume.final_sp), (0x3000, 0x4000));
assert_eq!(resume.powerpc.gpr3, 0x1234_5678);
assert!(calls.is_empty());
}
#[test]
fn deeper_cross_isa_transition_completes_in_lifo_order() {
let calls = SharedGuestCallStack::default();
assert!(calls.begin_m68k_to_powerpc(
GuestCallTarget {
isa: GuestIsa::PowerPc,
entry: 0x1000,
rtoc: 0x2000,
},
PowerPcArguments::from_slice(&[]).unwrap(),
0x3000,
0x4000,
None,
));
assert_eq!(calls.suspended_m68k_context_depth(), 0);
let mut cpu = PpcCpu::new();
assert!(calls
.activate_powerpc_from_m68k(&mut cpu, RETURN_PC)
.is_some());
assert_eq!(calls.suspended_m68k_context_depth(), 1);
assert!(calls.begin_powerpc_to_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x5000,
rtoc: 0,
},
0x5000,
0x6000,
0x7000,
0x6004,
M68kRegisterState::default(),
None,
0x8000,
0x9000,
PpcNativeReturnGpr3::Preserve,
));
assert_eq!(calls.len(), 2);
assert!(!calls.has_powerpc_from_m68k());
assert!(calls.has_m68k_execution());
assert!(calls.activate_m68k().is_some());
assert!(calls.begin_m68k_to_powerpc(
GuestCallTarget {
isa: GuestIsa::PowerPc,
entry: 0xa000,
rtoc: 0xb000,
},
PowerPcArguments::from_slice(&[0xc000]).unwrap(),
0xd000,
0xe000,
None,
));
assert!(calls
.activate_powerpc_from_m68k(&mut cpu, RETURN_PC + 4)
.is_some());
assert_eq!(calls.suspended_m68k_context_depth(), 2);
assert!(calls.begin_powerpc_to_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0xf000,
rtoc: 0,
},
0xf000,
0x1_0000,
0x1_1000,
0x1_0004,
M68kRegisterState::default(),
None,
0x1_2000,
0x1_3000,
PpcNativeReturnGpr3::Preserve,
));
assert!(calls.activate_m68k().is_some());
assert!(!calls.complete_m68k_for_powerpc(0x7000, 0x6004, None, &mut cpu));
assert!(calls.complete_m68k_for_powerpc(0x1_1000, 0x1_0004, None, &mut cpu));
assert_eq!(calls.suspended_m68k_context_depth(), 2);
cpu.pc = RETURN_PC + 4;
assert!(calls.complete_powerpc_for_m68k(&mut cpu));
let nested_resume = calls.take_m68k_resume().unwrap();
assert_eq!(
(nested_resume.return_pc, nested_resume.final_sp),
(0xd000, 0xe000)
);
assert_eq!(calls.suspended_m68k_context_depth(), 1);
assert!(!calls.complete_m68k_for_powerpc(0x7004, 0x6004, None, &mut cpu));
assert!(calls.complete_m68k_for_powerpc(0x7000, 0x6004, None, &mut cpu));
assert_eq!(calls.len(), 1);
assert!(calls.has_powerpc_from_m68k());
assert_eq!(calls.suspended_m68k_context_depth(), 1);
cpu.pc = RETURN_PC;
cpu.gpr[3] = 0x1234_5678;
assert!(calls.complete_powerpc_for_m68k(&mut cpu));
let resume = calls.take_m68k_resume().unwrap();
assert_eq!((resume.return_pc, resume.final_sp), (0x3000, 0x4000));
assert_eq!(resume.powerpc.gpr3, 0x1234_5678);
assert_eq!(calls.suspended_m68k_context_depth(), 0);
assert!(calls.is_empty());
}
#[test]
fn cross_isa_frame_activates_once_and_restores_its_native_caller() {
let calls = SharedGuestCallStack::default();
let mut cpu = PpcCpu::new();
cpu.gpr[2] = 0xaaaa_0000;
assert!(calls.begin_powerpc_to_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x1000,
rtoc: 0,
},
0x2000,
0x3000,
0x4000,
0x3004,
M68kRegisterState::default(),
None,
0x5000,
0x6000,
PpcNativeReturnGpr3::Preserve,
));
assert!(calls.active_m68k().is_none());
assert!(calls.has_m68k_execution());
assert_eq!(
calls.activate_m68k(),
Some(PendingM68kExecution {
entry: 0x2000,
initial_sp: 0x3000,
return_pc: 0x4000,
final_sp: 0x3004,
registers: M68kRegisterState::default(),
result: None,
})
);
assert_eq!(calls.active_m68k(), calls.activate_m68k());
assert!(!calls.complete_m68k_for_powerpc(0x4000, 0x3000, None, &mut cpu));
assert!(calls.complete_m68k_for_powerpc(0x4000, 0x3004, None, &mut cpu));
assert_eq!((cpu.pc, cpu.lr, cpu.gpr[2]), (0x5000, 0x5000, 0x6000));
assert!(calls.is_empty());
}
#[test]
fn m68k_completion_requires_a_result_exactly_when_the_callback_abi_does() {
use crate::mixed_mode::special_case;
let mut cpu = PpcCpu::new();
for (selector, supplied_result) in [
(special_case::HIGH_HOOK as u8, None),
(special_case::EOL_HOOK as u8, Some(1)),
] {
let calls = SharedGuestCallStack::default();
assert!(calls.begin_powerpc_to_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x1000,
rtoc: 0,
},
0x2000,
0x3000,
0x4000,
0x3004,
M68kRegisterState::default(),
Some(M68kResultSource::SpecialCase {
selector,
arguments: PowerPcArguments::from_slice(&[]).unwrap(),
stack_result: None,
}),
0x5000,
0x6000,
PpcNativeReturnGpr3::Preserve,
));
assert!(calls.activate_m68k().is_some());
let wrong_result = supplied_result.map_or(Some(1), |_| None);
assert!(!calls.complete_m68k_for_powerpc(0x4000, 0x3004, wrong_result, &mut cpu));
assert!(calls.complete_m68k_for_powerpc(0x4000, 0x3004, supplied_result, &mut cpu,));
}
}
#[test]
fn powerpc_completion_preserves_every_native_result_policy() {
for (policy, input, expected) in [
(PpcNativeReturnGpr3::Preserve, 0x1234, 0x1234),
(PpcNativeReturnGpr3::Mask(0xff), 0x1234, 0x34),
(PpcNativeReturnGpr3::Set(9), 0x1234, 9),
(
PpcNativeReturnGpr3::ZeroOrSet {
zero: 10,
nonzero: 11,
},
0,
10,
),
(
PpcNativeReturnGpr3::ZeroOrSet {
zero: 10,
nonzero: 11,
},
1,
11,
),
(PpcNativeReturnGpr3::CrBit(2), 0, 1),
(PpcNativeReturnGpr3::XerCa, 0, 1),
(PpcNativeReturnGpr3::XerOv, 0, 1),
] {
let calls = SharedGuestCallStack::default();
let mut cpu = PpcCpu::new();
cpu.set_cr_bit(2, true);
cpu.set_xer_ca(true);
cpu.xer |= 1 << 30;
calls.externalize_powerpc_action(&mut cpu, native_action(0x1000, 0x2000, policy));
cpu.pc = RETURN_PC;
cpu.gpr[3] = input;
assert!(calls.complete_powerpc(&mut cpu));
assert_eq!(cpu.gpr[3], expected, "policy {policy:?}");
}
}
#[test]
fn completion_never_consumes_the_other_architectures_frame() {
let calls = SharedGuestCallStack::default();
let mut cpu = PpcCpu::new();
calls.begin_m68k(
GuestCallTarget {
isa: GuestIsa::M68k,
entry: 0x1000,
rtoc: 0,
},
0x2000,
0x3000,
);
cpu.pc = RETURN_PC;
assert!(!calls.complete_powerpc(&mut cpu));
assert_eq!(calls.len(), 1);
assert!(calls.complete_m68k(0x2002, 0x3000));
calls.externalize_powerpc_action(
&mut cpu,
native_action(0x4000, 0x5000, PpcNativeReturnGpr3::Preserve),
);
assert!(!calls.complete_m68k(0x5000, 0x6000));
assert_eq!(calls.len(), 1);
}
}