// license:BSD-3-Clause
// copyright-holders:Patrick Mackinlay

/*
 * An implementation of the Fairchild/Intergraph CLIPPER CPU family.
 *
 * Primary source: http://bitsavers.org/pdf/fairchild/clipper/Clipper_Instruction_Set_Oct85.pdf
 *
 * TODO:
 *   - unimplemented C400 instructions (cdb, cnvx[ds]w, loadts, waitd)
 *   - correct boot logic
 *   - instruction timing
 *   - big endian support (not present in the wild)
 */

#include "emu.h"
#include "debugger.h"
#include "clipper.h"
#include "clipperd.h"

#define LOG_GENERAL   (1U << 0)
#define LOG_EXCEPTION (1U << 1)
#define LOG_SYSCALLS  (1U << 2)

//#define VERBOSE (LOG_GENERAL | LOG_EXCEPTION)
#define VERBOSE (LOG_SYSCALLS)

#include "logmacro.h"

// convenience macros for frequently used instruction fields
#define R1 (m_info.r1)
#define R2 (m_info.r2)

#define BIT31(x) BIT(x, 31)
#define BIT63(x) BIT(x, 63)

// macros for computing and setting condition codes
#define FLAGS(C,V,Z,N) \
	m_psw = (m_psw & ~(PSW_C | PSW_V | PSW_Z | PSW_N)) | (((C) << 3) | ((V) << 2) | ((Z) << 1) | ((N) << 0))

#define FLAGS_ADD(op2, op1, result) FLAGS(                                        \
	(BIT31(op2) && BIT31(op1)) || (!BIT31(result) && (BIT31(op2) || BIT31(op1))), \
	(BIT31(op2) == BIT31(op1)) && (BIT31(result) != BIT31(op2)),                  \
	result == 0, BIT31(result))

#define FLAGS_SUB(op2, op1, result) FLAGS(                                         \
	(!BIT31(op2) && BIT31(op1)) || (BIT31(result) && (!BIT31(op2) || BIT31(op1))), \
	(BIT31(op2) != BIT31(op1)) && (BIT31(result) != BIT31(op2)),                   \
	result == 0, BIT31(result))

DEFINE_DEVICE_TYPE(CLIPPER_C100, clipper_c100_device, "clipper_c100", "C100 CLIPPER")
DEFINE_DEVICE_TYPE(CLIPPER_C300, clipper_c300_device, "clipper_c300", "C300 CLIPPER")
DEFINE_DEVICE_TYPE(CLIPPER_C400, clipper_c400_device, "clipper_c400", "C400 CLIPPER")

clipper_c100_device::clipper_c100_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
	: clipper_device(mconfig, CLIPPER_C100, tag, owner, clock, ENDIANNESS_LITTLE, SSW_ID_C1R1)
	, m_icammu(*this, "^cammu_i")
	, m_dcammu(*this, "^cammu_d")
{
}

clipper_c300_device::clipper_c300_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
	: clipper_device(mconfig, CLIPPER_C300, tag, owner, clock, ENDIANNESS_LITTLE, SSW_ID_C3R1)
	, m_icammu(*this, "^cammu_i")
	, m_dcammu(*this, "^cammu_d")
{
}

clipper_c400_device::clipper_c400_device(const machine_config &mconfig, const char *tag, device_t *owner, u32 clock)
	: clipper_device(mconfig, CLIPPER_C400, tag, owner, clock, ENDIANNESS_LITTLE, SSW_ID_C4R4)
	, m_db_pc(0)
	, m_cammu(*this, "^cammu")
{
}

clipper_device::clipper_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, u32 clock, const endianness_t endianness, const u32 cpuid)
	: cpu_device(mconfig, type, tag, owner, clock)
	, m_main_config("main", endianness, 32, 32, 0)
	, m_io_config("io", endianness, 32, 32, 0)
	, m_boot_config("boot", endianness, 32, 32, 0)
	, m_icount(0)
	, m_psw(endianness == ENDIANNESS_BIG ? PSW_BIG : 0)
	, m_ssw(cpuid)
	, m_r(m_rs)
	, m_ru{0}
	, m_rs{0}
	, m_f{0}
	, m_fp_pc(0)
	, m_fp_dst(0)
	, m_info{0}
{
}

// rotate helpers to replace MSVC intrinsics
inline u32 rotl32(u32 x, u8 shift)
{
  shift &= 31;
  return (x << shift) | (x >> ((32 - shift) & 31));
}

inline u32 rotr32(u32 x, u8 shift)
{
  shift &= 31;
  return (x >> shift) | (x << ((32 - shift) & 31));
}

inline u64 rotl64(u64 x, u8 shift)
{
  shift &= 63;
  return (x << shift) | (x >> ((64 - shift) & 63));
}

inline u64 rotr64(u64 x, u8 shift)
{
  shift &= 63;
  return (x >> shift) | (x << ((64 - shift) & 63));
}

void clipper_device::device_start()
{
	// configure the cammu address spaces
	get_dcammu().set_spaces(space(0), space(1), space(2));
	get_icammu().set_spaces(space(0), space(1), space(2));

	// set our instruction counter
	set_icountptr(m_icount);

	// program-visible cpu state
	save_item(NAME(m_pc));
	save_item(NAME(m_psw));
	save_item(NAME(m_ssw));
	save_item(NAME(m_ru));
	save_item(NAME(m_rs));
	save_item(NAME(m_f));
	save_item(NAME(m_fp_pc));
	save_item(NAME(m_fp_dst));

	// non-visible cpu state
	save_item(NAME(m_wait));
	save_item(NAME(m_nmi));
	save_item(NAME(m_irq));
	save_item(NAME(m_ivec));
	save_item(NAME(m_exception));

	state_add(STATE_GENPC, "GENPC", m_pc).noshow();
	state_add(STATE_GENPCBASE, "CURPC", m_pc).noshow();
	state_add(STATE_GENFLAGS, "GENFLAGS", m_psw).mask(0xf).formatstr("%4s").noshow();

	state_add(CLIPPER_PC, "pc", m_pc);
	state_add(CLIPPER_PSW, "psw", m_psw);
	state_add(CLIPPER_SSW, "ssw", m_ssw);

	// integer regsters
	for (int i = 0; i < get_ireg_count(); i++)
		state_add(CLIPPER_UREG + i, util::string_format("ur%d", i).c_str(), m_ru[i]);
	for (int i = 0; i < get_ireg_count(); i++)
		state_add(CLIPPER_SREG + i, util::string_format("sr%d", i).c_str(), m_rs[i]);

	// floating point registers
	for (int i = 0; i < get_freg_count(); i++)
		state_add(CLIPPER_FREG + i, util::string_format("f%d", i).c_str(), m_f[i]);
}

void clipper_c400_device::device_start()
{
	clipper_device::device_start();

	save_item(NAME(m_db_pc));
}

void clipper_device::device_reset()
{
	/*
	 * From C300 documentation, on reset:
	 *   psw: T cleared, BIG set from hardware, others undefined
	 *   ssw: EI, TP, M, U, K, KU, UU, P cleared, ID set from hardware, others undefined
	 */

	// clear the psw and ssw
	set_psw(0);
	set_ssw(0);

	// FIXME: figure out how to branch to the boot code properly
	m_pc = 0x7f100000;

	m_wait = false;
	m_nmi = CLEAR_LINE;
	m_irq = CLEAR_LINE;
	m_ivec = 0;
	m_exception = 0;
}

void clipper_device::state_string_export(const device_state_entry &entry, std::string &str) const
{
	switch (entry.index())
	{
	case STATE_GENFLAGS:
		str = string_format("%c%c%c%c",
			PSW(C) ? 'C' : '.',
			PSW(V) ? 'V' : '.',
			PSW(Z) ? 'Z' : '.',
			PSW(N) ? 'N' : '.');
		break;
	}
}

void clipper_device::execute_run()
{
	// check for non-maskable and prioritised interrupts
	if (m_nmi)
	{
		// acknowledge non-maskable interrupt
		standard_irq_callback(INPUT_LINE_NMI);

		LOGMASKED(LOG_EXCEPTION, "non-maskable interrupt\n");
		m_pc = intrap(EXCEPTION_INTERRUPT_BASE, m_pc);
	}
	else if (SSW(EI) && m_irq)
	{
		LOGMASKED(LOG_EXCEPTION, "prioritised interrupt vector 0x%02x\n", m_ivec);

		// allow equal/higher priority interrupts
		if ((m_ivec & IVEC_LEVEL) <= SSW(IL))
		{
			// acknowledge interrupt
			standard_irq_callback(INPUT_LINE_IRQ0);

			m_pc = intrap(EXCEPTION_INTERRUPT_BASE + m_ivec * 8, m_pc);

			LOGMASKED(LOG_EXCEPTION, "transferring control to vector 0x%02x address 0x%08x\n", m_ivec, m_pc);
		}
	}

	while (m_icount > 0)
	{
		debugger_instruction_hook(m_pc);

		if (m_wait)
		{
			m_icount = 0;
			continue;
		}

		// fetch and decode an instruction
		if (decode_instruction())
		{
			softfloat_exceptionFlags = 0;

			// execute instruction
			execute_instruction();

			// check floating point exceptions
			if (softfloat_exceptionFlags)
				fp_exception();
		}

		if (m_exception)
		{
			debugger_exception_hook(m_exception);

			/*
			 * For traced instructions which are interrupted or cause traps, the TP
			 * flag is set by hardware when the interrupt or trap occurs to ensure
			 * that the trace trap will occur immediately after the interrupt or other
			 * trap has been serviced.
			 */
			// FIXME: don't know why/when the trace pending flag is needed
			if (PSW(T))
				m_ssw |= SSW_TP;

			switch (m_exception)
			{
				// data memory trap group
			case EXCEPTION_D_CORRECTED_MEMORY_ERROR:
			case EXCEPTION_D_UNCORRECTABLE_MEMORY_ERROR:
			case EXCEPTION_D_ALIGNMENT_FAULT:
			case EXCEPTION_D_PAGE_FAULT:
			case EXCEPTION_D_READ_PROTECT_FAULT:
			case EXCEPTION_D_WRITE_PROTECT_FAULT:

				// instruction memory trap group
			case EXCEPTION_I_CORRECTED_MEMORY_ERROR:
			case EXCEPTION_I_UNCORRECTABLE_MEMORY_ERROR:
			case EXCEPTION_I_ALIGNMENT_FAULT:
			case EXCEPTION_I_PAGE_FAULT:
			case EXCEPTION_I_EXECUTE_PROTECT_FAULT:

				// illegal operation trap group
			case EXCEPTION_ILLEGAL_OPERATION:
			case EXCEPTION_PRIVILEGED_INSTRUCTION:
				// return address is faulting instruction
				m_pc = intrap(m_exception, m_info.pc);
				break;

			default:
				// return address is following instruction
				m_pc = intrap(m_exception, m_pc);
				break;
			}
		}

		// FIXME: trace trap logic not working properly yet
		//else if (PSW(T))
		//  m_pc = intrap(EXCEPTION_TRACE, m_pc);

		// FIXME: some instructions take longer (significantly) than one cycle
		// and also the timings are often slower for the C100 and C300
		m_icount -= 4;
	}
}

void clipper_device::execute_set_input(int inputnum, int state)
{
	if (state)
		m_wait = false;

	switch (inputnum)
	{
	case INPUT_LINE_IRQ0:
		m_irq = state;
		break;

	case INPUT_LINE_NMI:
		m_nmi = state;
		break;
	}
}

device_memory_interface::space_config_vector clipper_device::memory_space_config() const
{
	return space_config_vector {
		std::make_pair(0, &m_main_config),
		std::make_pair(1, &m_io_config),
		std::make_pair(2, &m_boot_config)
	};
}

bool clipper_device::memory_translate(int spacenum, int intention, offs_t &address)
{
	return ((intention & TRANSLATE_TYPE_MASK) == TRANSLATE_FETCH ? get_icammu() : get_dcammu()).memory_translate(m_ssw, spacenum, intention, address);
}

void clipper_device::set_exception(u16 data)
{
	LOGMASKED(LOG_EXCEPTION, "external exception 0x%04x triggered\n", data);

	// check if corrected memory errors are masked
	if (!SSW(ECM) && (data == EXCEPTION_D_CORRECTED_MEMORY_ERROR || data == EXCEPTION_I_CORRECTED_MEMORY_ERROR))
		return;

	m_exception = data;
}

/*
 * Fetch and decode an instruction and compute an effective address (for
 * instructions with addressing modes). The results are contained in the m_info
 * structure to simplify passing between here and execute_instruction().
 */
bool clipper_device::decode_instruction()
{
	// record the current instruction address
	m_info.pc = m_pc;

	// fetch and decode the primary parcel
	if (!get_icammu().fetch<u16>(m_ssw, m_pc + 0, [this](u16 insn) {
		m_info.opcode = insn >> 8;
		m_info.subopcode = insn & 0xff;
		m_info.r1 = (insn & 0x00f0) >> 4;
		m_info.r2 = insn & 0x000f;
	}))
		return false;

	// initialise the other fields
	m_info.imm = 0;
	m_info.macro = 0;
	m_info.address = 0;

	// default instruction size is 2 bytes
	int size = 2;

	if ((m_info.opcode & 0xf8) == 0x38)
	{
		// fetch 16 bit immediate and sign extend
		if (!get_icammu().fetch<s16>(m_ssw, m_pc + 2, [this](s32 v) { m_info.imm = v; }))
			return false;
		size = 4;
	}
	else if ((m_info.opcode & 0xd3) == 0x83)
	{
		// instruction has an immediate operand, either 16 or 32 bit
		if (m_info.subopcode & 0x80)
		{
			// fetch 16 bit immediate and sign extend
			if (!get_icammu().fetch<s16>(m_ssw, m_pc + 2, [this](s32 v) { m_info.imm = v; }))
				return false;
			size = 4;
		}
		else
		{
			// fetch 32 bit immediate
			if (!get_icammu().fetch<u32>(m_ssw, m_pc + 2, [this](u32 v) { m_info.imm = v; }))
				return false;
			size = 6;
		}
	}
	else if ((m_info.opcode & 0xc0) == 0x40)
	{
		// instructions with addresses
		if (m_info.opcode & 0x01)
		{
			// instructions with complex modes
			switch (m_info.subopcode & 0xf0)
			{
			case ADDR_MODE_PC32:
				if (!get_icammu().fetch<u32>(m_ssw, m_pc + 2, [this](u32 v) { m_info.address = m_pc + v; }))
					return false;
				size = 6;
				break;

			case ADDR_MODE_ABS32:
				if (!get_icammu().fetch<u32>(m_ssw, m_pc + 2, [this](u32 v) { m_info.address = v; }))
					return false;
				size = 6;
				break;

			case ADDR_MODE_REL32:
				if (!get_icammu().fetch<u16>(m_ssw, m_pc + 2, [this](u16 v) { m_info.r2 = v & 0xf; }))
					return false;

				if (!get_icammu().fetch<u32>(m_ssw, m_pc + 4, [this](u32 v) { m_info.address = m_r[m_info.subopcode & 0xf] + v; }))
					return false;
				size = 8;
				break;

			case ADDR_MODE_PC16:
				if (!get_icammu().fetch<s16>(m_ssw, m_pc + 2, [this](s16 v) { m_info.address = m_pc + v; }))
					return false;
				size = 4;
				break;

			case ADDR_MODE_REL12:
				if (!get_icammu().fetch<s16>(m_ssw, m_pc + 2, [this](s16 v) {
					m_info.r2 = v & 0xf;
					m_info.address = m_r[m_info.subopcode & 0xf] + (v >> 4);
				}))
					return false;
				size = 4;
				break;

			case ADDR_MODE_ABS16:
				if (!get_icammu().fetch<s16>(m_ssw, m_pc + 2, [this](s32 v) { m_info.address = v; }))
					return false;
				size = 4;
				break;

			case ADDR_MODE_PCX:
				if (!get_icammu().fetch<u16>(m_ssw, m_pc + 2, [this](u16 v) {
					m_info.r2 = v & 0xf;
					m_info.address = m_pc + m_r[(v >> 4) & 0xf];
				}))
					return false;
				size = 4;
				break;

			case ADDR_MODE_RELX:
				if (!get_icammu().fetch<u16>(m_ssw, m_pc + 2, [this](u16 v) {
					m_info.r2 = v & 0xf;
					m_info.address = m_r[m_info.subopcode & 0xf] + m_r[(v >> 4) & 0xf];
				}))
					return false;
				size = 4;
				break;

			default:
				m_exception = EXCEPTION_ILLEGAL_OPERATION;
				return false;
			}
		}
		else
			// relative addressing mode
			m_info.address = m_r[m_info.r1];
	}
	else if ((m_info.opcode & 0xfd) == 0xb4)
	{
		// macro instructions
		if (!get_icammu().fetch<u16>(m_ssw, m_pc + 2, [this](u16 v) { m_info.macro = v; }))
			return false;
		size = 4;
	}

	// instruction fetch and decode complete
	m_pc = m_pc + size;

	return true;
}

void clipper_device::execute_instruction()
{
	switch (m_info.opcode)
	{
	case 0x00: // noop
		break;

	case 0x10:
		// movwp: move word to processor register
		// treated as a noop if target ssw in user mode
		// R1 == 3 means "fast" mode - avoids pipeline flush
		if (R1 == 0)
			set_psw(m_r[R2]);
		else if (!SSW(U) && (R1 == 1 || R1 == 3))
			set_ssw(m_r[R2]);
		// FLAGS: CVZN
		break;
	case 0x11:
		// movpw: move processor register to word
		switch (R1)
		{
		case 0: m_r[R2] = m_psw; break;
		case 1: m_r[R2] = m_ssw; break;
		}
		break;
	case 0x12:
		// calls: call supervisor
		m_exception = EXCEPTION_SUPERVISOR_CALL_BASE + (m_info.subopcode & 0x7f) * 8;
		if (VERBOSE & LOG_SYSCALLS)
			switch (m_info.subopcode & 0x7f)
			{
			case 0x3b: // execve
				LOGMASKED(LOG_SYSCALLS, "execve(\"%s\", [ %s ], envp)\n",
					debug_string(m_r[0]), debug_string_array(m_r[1]));
				break;
			}
		break;
	case 0x13:
		// ret: return from subroutine
		get_dcammu().load<u32>(m_ssw, m_r[R2], [this](u32 v) {
			m_pc = v;
			m_r[R2] += 4;
		});
		// TRAPS: C,U,A,P,R
		break;
	case 0x14:
		// pushw: push word
		get_dcammu().store<u32>(m_ssw, m_r[R1] - 4, m_r[R2]);
		m_r[R1] -= 4;
		// TRAPS: A,P,W
		break;

	case 0x16:
		// popw: pop word
		get_dcammu().load<u32>(m_ssw, m_r[R1], [this](u32 v) {
			m_r[R1] += 4;
			m_r[R2] = v;
		});
		// TRAPS: C,U,A,P,R
		break;

	case 0x20:
		// adds: add single floating
		set_fp(R2, f32_add(get_fp32(R2), get_fp32(R1)), F_IVUX);
		// TRAPS: F_IVUX
		break;
	case 0x21:
		// subs: subtract single floating
		set_fp(R2, f32_sub(get_fp32(R2), get_fp32(R1)), F_IVUX);
		// TRAPS: F_IVUX
		break;
	case 0x22:
		// addd: add double floating
		set_fp(R2, f64_add(get_fp64(R2), get_fp64(R1)), F_IVUX);
		// TRAPS: F_IVUX
		break;
	case 0x23:
		// subd: subtract double floating
		set_fp(R2, f64_sub(get_fp64(R2), get_fp64(R1)), F_IVUX);
		// TRAPS: F_IVUX
		break;
	case 0x24:
		// movs: move single floating
		set_fp(R2, get_fp32(R1), F_NONE);
		break;
	case 0x25:
		// cmps: compare single floating
		FLAGS(0, 0, f32_eq(get_fp32(R2), get_fp32(R1)), f32_lt(get_fp32(R2), get_fp32(R1)));
		// flag unordered
		if (softfloat_exceptionFlags & softfloat_flag_invalid)
			m_psw |= PSW_Z | PSW_N;
		softfloat_exceptionFlags &= F_NONE;
		break;
	case 0x26:
		// movd: move double floating
		set_fp(R2, get_fp64(R1), F_NONE);
		break;
	case 0x27:
		// cmpd: compare double floating
		FLAGS(0, 0, f64_eq(get_fp64(R2), get_fp64(R1)), f64_lt(get_fp64(R2), get_fp64(R1)));
		// flag unordered
		if (softfloat_exceptionFlags & softfloat_flag_invalid)
			m_psw |= PSW_Z | PSW_N;
		softfloat_exceptionFlags &= F_NONE;
		break;
	case 0x28:
		// muls: multiply single floating
		set_fp(R2, f32_mul(get_fp32(R2), get_fp32(R1)), F_IVUX);
		// TRAPS: F_IVUX
		break;
	case 0x29:
		// divs: divide single floating
		set_fp(R2, f32_div(get_fp32(R2), get_fp32(R1)), F_IVDUX);
		// TRAPS: F_IVDUX
		break;
	case 0x2a:
		// muld: multiply double floating
		set_fp(R2, f64_mul(get_fp64(R2), get_fp64(R1)), F_IVUX);
		// TRAPS: F_IVUX
		break;
	case 0x2b:
		// divd: divide double floating
		set_fp(R2, f64_div(get_fp64(R2), get_fp64(R1)), F_IVDUX);
		// TRAPS: F_IVDUX
		break;
	case 0x2c:
		// movsw: move single floating to word
		m_r[R2] = get_fp32(R1).v;
		break;
	case 0x2d:
		// movws: move word to single floating
		set_fp(R2, float32_t{ m_r[R1] }, F_NONE);
		break;
	case 0x2e:
		// movdl: move double floating to longword
		set_64(R2, get_fp64(R1).v);
		break;
	case 0x2f:
		// movld: move longword to double floating
		set_fp(R2, float64_t{ get_64(R1) }, F_NONE);
		break;
	case 0x30:
		// shaw: shift arithmetic word
		if (!BIT31(m_r[R1]))
		{
			// save the bits that will be shifted out plus new sign bit
			const s32 v = s32(m_r[R2]) >> (31 - m_r[R1]);

			m_r[R2] <<= m_r[R1];

			// overflow is set if sign changes during shift
			FLAGS(0, v != 0 && v != -1, m_r[R2] == 0, BIT31(m_r[R2]));
		}
		else
		{
			m_r[R2] = s32(m_r[R2]) >> -m_r[R1];
			FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		}
		// FLAGS: 0VZN
		break;
	case 0x31:
		// shal: shift arithmetic longword
		if (!BIT31(m_r[R1]))
		{
			// save the bits that will be shifted out plus new sign bit
			const s64 v = s64(get_64(R2)) >> (63 - m_r[R1]);

			set_64(R2, get_64(R2) << m_r[R1]);

			// overflow is set if sign changes during shift
			FLAGS(0, v != 0 && v != -1, get_64(R2) == 0, BIT63(get_64(R2)));
		}
		else
		{
			set_64(R2, s64(get_64(R2)) >> -m_r[R1]);
			FLAGS(0, 0, get_64(R2) == 0, BIT63(get_64(R2)));
		}
		// FLAGS: 0VZN
		break;
	case 0x32:
		// shlw: shift logical word
		if (!BIT31(m_r[R1]))
			m_r[R2] <<= m_r[R1];
		else
			m_r[R2] >>= -m_r[R1];
		// FLAGS: 00ZN
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		break;
	case 0x33:
		// shll: shift logical longword
		if (!BIT31(m_r[R1]))
			set_64(R2, get_64(R2) << m_r[R1]);
		else
			set_64(R2, get_64(R2) >> -m_r[R1]);
		// FLAGS: 00ZN
		FLAGS(0, 0, get_64(R2) == 0, BIT63(get_64(R2)));
		break;
	case 0x34:
		// rotw: rotate word
		if (!BIT31(m_r[R1]))
			m_r[R2] = rotl32(m_r[R2], m_r[R1]);
		else
			m_r[R2] = rotr32(m_r[R2], -m_r[R1]);
		// FLAGS: 00ZN
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		break;
	case 0x35:
		// rotl: rotate longword
		if (!BIT31(m_r[R1]))
			set_64(R2, rotl64(get_64(R2), m_r[R1]));
		else
			set_64(R2, rotr64(get_64(R2), -m_r[R1]));
		// FLAGS: 00ZN
		FLAGS(0, 0, get_64(R2) == 0, BIT63(get_64(R2)));
		break;

	case 0x38:
		// shai: shift arithmetic immediate
		if (!BIT31(m_info.imm))
		{
			// save the bits that will be shifted out plus new sign bit
			const s32 v = s32(m_r[R2]) >> (31 - m_info.imm);

			m_r[R2] <<= m_info.imm;

			// overflow is set if sign changes during shift
			FLAGS(0, v != 0 && v != -1, m_r[R2] == 0, BIT31(m_r[R2]));
		}
		else
		{
			m_r[R2] = s32(m_r[R2]) >> -m_info.imm;
			FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		}
		// FLAGS: 0VZN
		// TRAPS: I
		break;
	case 0x39:
		// shali: shift arithmetic longword immediate
		if (!BIT31(m_info.imm))
		{
			// save the bits that will be shifted out plus new sign bit
			const s64 v = s64(get_64(R2)) >> (63 - m_info.imm);

			set_64(R2, get_64(R2) << m_info.imm);

			// overflow is set if sign changes during shift
			FLAGS(0, v != 0 && v != -1, get_64(R2) == 0, BIT63(get_64(R2)));
		}
		else
		{
			set_64(R2, s64(get_64(R2)) >> -m_info.imm);
			FLAGS(0, 0, get_64(R2) == 0, BIT63(get_64(R2)));
		}
		// FLAGS: 0VZN
		// TRAPS: I
		break;
	case 0x3a:
		// shli: shift logical immediate
		if (!BIT31(m_info.imm))
			m_r[R2] <<= m_info.imm;
		else
			m_r[R2] >>= -m_info.imm;
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0x3b:
		// shlli: shift logical longword immediate
		if (!BIT31(m_info.imm))
			set_64(R2, get_64(R2) << m_info.imm);
		else
			set_64(R2, get_64(R2) >> -m_info.imm);
		FLAGS(0, 0, get_64(R2) == 0, BIT63(get_64(R2)));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0x3c:
		// roti: rotate immediate
		if (!BIT31(m_info.imm))
			m_r[R2] = rotl32(m_r[R2], m_info.imm);
		else
			m_r[R2] = rotr32(m_r[R2], -m_info.imm);
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0x3d:
		// rotli: rotate longword immediate
		if (!BIT31(m_info.imm))
			set_64(R2, rotl64(get_64(R2), m_info.imm));
		else
			set_64(R2, rotr64(get_64(R2), -m_info.imm));
		FLAGS(0, 0, get_64(R2) == 0, BIT63(get_64(R2)));
		// FLAGS: 00ZN
		// TRAPS: I
		break;

	case 0x44:
	case 0x45:
		// call: call subroutine
		if (get_dcammu().store<u32>(m_ssw, m_r[R2] - 4, m_pc))
		{
			m_pc = m_info.address;
			m_r[R2] -= 4;
		}
		// TRAPS: A,P,W
		break;

	case 0x48:
	case 0x49:
		// b*: branch on condition
		if (evaluate_branch())
			m_pc = m_info.address;
		// TRAPS: A,I
		break;

	case 0x4c:
	case 0x4d:
		// bf*: branch on floating exception
		// FIXME: documentation is not clear, implementation is guesswork
		switch (R2)
		{
		case BF_ANY:
			// bfany: floating any exception
			if (m_psw & (PSW_FI | PSW_FV | PSW_FD | PSW_FU | PSW_FX))
				m_pc = m_info.address;
			break;
		case BF_BAD:
			// bfbad: floating bad result
			if (m_psw & (PSW_FI | PSW_FD))
				m_pc = m_info.address;
			break;
		default:
			// reserved
			// FIXME: not sure if this should trigger an exception?
			m_exception = EXCEPTION_ILLEGAL_OPERATION;
			break;
		}
		break;

	case 0x60:
	case 0x61:
		// loadw: load word
		get_dcammu().load<u32>(m_ssw, m_info.address, [this](u32 v) { m_r[R2] = v; });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x62:
	case 0x63:
		// loada: load address
		m_r[R2] = m_info.address;
		// TRAPS: I
		break;
	case 0x64:
	case 0x65:
		// loads: load single floating
		get_dcammu().load<u32>(m_ssw, m_info.address, [this](u32 v) { set_fp(R2, float32_t{ v }, F_NONE); });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x66:
	case 0x67:
		// loadd: load double floating
		get_dcammu().load<u64>(m_ssw, m_info.address, [this](u64 v) { set_fp(R2, float64_t{ v }, F_NONE); });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x68:
	case 0x69:
		// loadb: load byte
		get_dcammu().load<s8>(m_ssw, m_info.address, [this](s32 v) { m_r[R2] = v; });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x6a:
	case 0x6b:
		// loadbu: load byte unsigned
		get_dcammu().load<u8>(m_ssw, m_info.address, [this](u32 v) { m_r[R2] = v; });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x6c:
	case 0x6d:
		// loadh: load halfword
		get_dcammu().load<s16>(m_ssw, m_info.address, [this](s32 v) { m_r[R2] = v; });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x6e:
	case 0x6f:
		// loadhu: load halfword unsigned
		get_dcammu().load<u16>(m_ssw, m_info.address, [this](u32 v) { m_r[R2] = v; });
		// TRAPS: C,U,A,P,R,I
		break;
	case 0x70:
	case 0x71:
		// storw: store word
		get_dcammu().store<u32>(m_ssw, m_info.address, m_r[R2]);
		// TRAPS: A,P,W,I
		break;
	case 0x72:
	case 0x73:
		// tsts: test and set
		get_dcammu().modify<u32>(m_ssw, m_info.address, [this](u32 v) {
			m_r[R2] = v;
			return v | 0x80000000U;
		});
		// TRAPS: C,U,A,P,R,W,I
		break;
	case 0x74:
	case 0x75:
		// stors: store single floating
		get_dcammu().store<u32>(m_ssw, m_info.address, get_fp32(R2).v);
		// TRAPS: A,P,W,I
		break;
	case 0x76:
	case 0x77:
		// stord: store double floating
		get_dcammu().store<u64>(m_ssw, m_info.address, get_fp64(R2).v);
		// TRAPS: A,P,W,I
		break;
	case 0x78:
	case 0x79:
		// storb: store byte
		get_dcammu().store<u8>(m_ssw, m_info.address, m_r[R2]);
		// TRAPS: A,P,W,I
		break;

	case 0x7c:
	case 0x7d:
		// storh: store halfword
		get_dcammu().store<u16>(m_ssw, m_info.address, m_r[R2]);
		// TRAPS: A,P,W,I
		break;

	case 0x80:
		// addw: add word
		{
			const u32 result = m_r[R2] + m_r[R1];

			FLAGS_ADD(m_r[R2], m_r[R1], result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;

	case 0x82:
		// addq: add quick
		{
			const u32 result = m_r[R2] + m_info.r1;

			FLAGS_ADD(m_r[R2], m_info.r1, result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;
	case 0x83:
		// addi: add immediate
		{
			const u32 result = m_r[R2] + m_info.imm;

			FLAGS_ADD(m_r[R2], m_info.imm, result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		// TRAPS: I
		break;
	case 0x84:
		// movw: move word
		m_r[R2] = m_r[R1];
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		break;

	case 0x86:
		// loadq: load quick
		m_r[R2] = m_info.r1;
		FLAGS(0, 0, m_r[R2] == 0, 0);
		// FLAGS: 00Z0
		break;
	case 0x87:
		// loadi: load immediate
		m_r[R2] = m_info.imm;
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0x88:
		// andw: and word
		m_r[R2] &= m_r[R1];
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		break;

	case 0x8b:
		// andi: and immediate
		m_r[R2] &= m_info.imm;
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0x8c:
		// orw: or word
		m_r[R2] |= m_r[R1];
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		break;

	case 0x8f:
		// ori: or immediate
		m_r[R2] |= m_info.imm;
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0x90:
		// addwc: add word with carry
		{
			const u32 result = m_r[R2] + m_r[R1] + (PSW(C) ? 1 : 0);

			FLAGS_ADD(m_r[R2], m_r[R1], result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;
	case 0x91:
		// subwc: subtract word with carry
		{
			const u32 result = m_r[R2] - m_r[R1] - (PSW(C) ? 1 : 0);

			FLAGS_SUB(m_r[R2], m_r[R1], result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;

	case 0x93:
		// negw: negate word
		{
			const u32 result = -m_r[R1];

			FLAGS(
				m_r[R1] != 0,
				s32(m_r[R1]) == INT32_MIN,
				result == 0,
				BIT31(result));

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;

	case 0x98:
		// mulw: multiply word
		{
			const s64 product = mul_32x32(m_r[R1], m_r[R2]);
			m_r[R2] = s32(product);
			FLAGS(0, (u64(product) >> 32) != (BIT31(product) ? ~u32(0) : 0), 0, 0);
			// FLAGS: 0V00
		}
		break;
	case 0x99:
		// mulwx: multiply word extended
		{
			const s64 product = mul_32x32(m_r[R1], m_r[R2]);
			set_64(R2, product);
			FLAGS(0, (u64(product) >> 32) != (BIT31(product) ? ~u32(0) : 0), 0, 0);
			// FLAGS: 0V00
		}
		break;
	case 0x9a:
		// mulwu: multiply word unsigned
		{
			const u64 product = mulu_32x32(m_r[R1], m_r[R2]);
			m_r[R2] = u32(product);
			FLAGS(0, (product >> 32) != 0, 0, 0);
			// FLAGS: 0V00
		}
		break;
	case 0x9b:
		// mulwux: multiply word unsigned extended
		{
			const u64 product = mulu_32x32(m_r[R1], m_r[R2]);
			set_64(R2, product);
			FLAGS(0, (product >> 32) != 0, 0, 0);
			// FLAGS: 0V00
		}
		break;
	case 0x9c:
		// divw: divide word
		if (m_r[R1] != 0)
		{
			// FLAGS: 0V00
			FLAGS(0, s32(m_r[R2]) == INT32_MIN && s32(m_r[R1]) == -1, 0, 0);
			m_r[R2] = s32(m_r[R2]) / s32(m_r[R1]);
		}
		else
			// TRAPS: D
			m_exception = EXCEPTION_INTEGER_DIVIDE_BY_ZERO;
		break;
	case 0x9d:
		// modw: modulus word
		if (m_r[R1] != 0)
		{
			// FLAGS: 0V00
			FLAGS(0, s32(m_r[R2]) == INT32_MIN && s32(m_r[R1]) == -1, 0, 0);
			m_r[R2] = s32(m_r[R2]) % s32(m_r[R1]);
		}
		else
			// TRAPS: D
			m_exception = EXCEPTION_INTEGER_DIVIDE_BY_ZERO;
		break;
	case 0x9e:
		// divwu: divide word unsigned
		if (m_r[R1] != 0)
		{
			m_r[R2] = m_r[R2] / m_r[R1];
			// FLAGS: 0000
			FLAGS(0, 0, 0, 0);
		}
		else
			// TRAPS: D
			m_exception = EXCEPTION_INTEGER_DIVIDE_BY_ZERO;
		break;
	case 0x9f:
		// modwu: modulus word unsigned
		if (m_r[R1] != 0)
		{
			m_r[R2] = m_r[R2] % m_r[R1];
			// FLAGS: 0000
			FLAGS(0, 0, 0, 0);
		}
		else
			// TRAPS: D
			m_exception = EXCEPTION_INTEGER_DIVIDE_BY_ZERO;
		break;
	case 0xa0:
		// subw: subtract word
		{
			const u32 result = m_r[R2] - m_r[R1];

			FLAGS_SUB(m_r[R2], m_r[R1], result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;

	case 0xa2:
		// subq: subtract quick
		{
			const u32 result = m_r[R2] - m_info.r1;

			FLAGS_SUB(m_r[R2], m_info.r1, result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		break;
	case 0xa3:
		// subi: subtract immediate
		{
			const u32 result = m_r[R2] - m_info.imm;

			FLAGS_SUB(m_r[R2], m_info.imm, result);

			m_r[R2] = result;
		}
		// FLAGS: CVZN
		// TRAPS: I
		break;
	case 0xa4:
		// cmpw: compare word
		{
			const u32 result = m_r[R2] - m_r[R1];

			FLAGS_SUB(m_r[R2], m_r[R1], result);
		}
		// FLAGS: CVZN
		break;

	case 0xa6:
		// cmpq: compare quick
		{
			const u32 result = m_r[R2] - m_info.r1;

			FLAGS_SUB(m_r[R2], m_info.r1, result);
		}
		// FLAGS: CVZN
		break;
	case 0xa7:
		// cmpi: compare immediate
		{
			const u32 result = m_r[R2] - m_info.imm;

			FLAGS_SUB(m_r[R2], m_info.imm, result);
		}
		// FLAGS: CVZN
		// TRAPS: I
		break;
	case 0xa8:
		// xorw: exclusive or word
		m_r[R2] ^= m_r[R1];
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		break;

	case 0xab:
		// xori: exclusive or immediate
		m_r[R2] ^= m_info.imm;
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		// TRAPS: I
		break;
	case 0xac:
		// notw: not word
		m_r[R2] = ~m_r[R1];
		FLAGS(0, 0, m_r[R2] == 0, BIT31(m_r[R2]));
		// FLAGS: 00ZN
		break;

	case 0xae:
		// notq: not quick
		m_r[R2] = ~R1;
		FLAGS(0, 0, 0, 1);
		// FLAGS: 0001
		break;

	case 0xb4:
		// unprivileged macro instructions
		switch (m_info.subopcode)
		{
		case 0x00: case 0x01: case 0x02: case 0x03:
		case 0x04: case 0x05: case 0x06: case 0x07:
		case 0x08: case 0x09: case 0x0a: case 0x0b:
		case 0x0c:
			// savew0..savew12: push registers rN:r14

			// store ri at sp - 4 * (15 - i)
			for (int i = R2; i < 15 && !m_exception; i++)
				get_dcammu().store<u32>(m_ssw, m_r[15] - 4 * (15 - i), m_r[i]);

			// decrement sp after push to allow restart on exceptions
			if (!m_exception)
				m_r[15] -= 4 * (15 - R2);
			// TRAPS: A,P,W
			break;
		// NOTE: the movc, initc and cmpc macro instructions are implemented in a very basic way because
		// at some point they will need to be improved to deal with possible exceptions (e.g. page faults)
		// that may occur during execution. The implementation here is intended to allow the instructions
		// to be "continued" after such exceptions.
		case 0x0d:
			// movc: copy r0 bytes from r1 to r2

			while (m_r[0])
			{
				get_dcammu().load<u8>(m_ssw, m_r[1], [this](u8 byte) { get_dcammu().store<u8>(m_ssw, m_r[2], byte); });

				if (m_exception)
					break;

				m_r[0]--;
				m_r[1]++;
				m_r[2]++;
			}
			// TRAPS: C,U,P,R,W
			break;
		case 0x0e:
			// initc: initialise r0 bytes at r1 with value in r2
			while (m_r[0])
			{
				if (!get_dcammu().store<u8>(m_ssw, m_r[1], m_r[2]))
					break;

				m_r[0]--;
				m_r[1]++;
				m_r[2] = rotr32(m_r[2], 8);
			}
			// TRAPS: P,W
			break;
		case 0x0f:
			// cmpc: compare r0 bytes at r1 with r2

			// set condition codes assuming strings match
			FLAGS(0, 0, 1, 0);

			while (m_r[0])
			{
				// read and compare bytes (as signed 32 bit integers)
				get_dcammu().load<s8>(m_ssw, m_r[1], [this](s32 byte1)
				{
					get_dcammu().load<s8>(m_ssw, m_r[2], [this, byte1](s32 byte2)
					{
						const s32 result = byte2 - byte1;

						FLAGS_SUB(byte2, byte1, result);
					});
				});

				// abort on exception or mismatch
				if (m_exception || !PSW(Z))
					break;

				m_r[0]--;
				m_r[1]++;
				m_r[2]++;
			}
			// TRAPS: C,U,P,R
			break;
		case 0x10: case 0x11: case 0x12: case 0x13:
		case 0x14: case 0x15: case 0x16: case 0x17:
		case 0x18: case 0x19: case 0x1a: case 0x1b:
		case 0x1c:
			// restwN..restw12: pop registers rN:r14

			// load ri from sp + 4 * (i - N)
			for (int i = R2; i < 15 && !m_exception; i++)
				get_dcammu().load<u32>(m_ssw, m_r[15] + 4 * (i - R2), [this, i](u32 v) { m_r[i] = v; });

			// increment sp after pop to allow restart on exceptions
			if (!m_exception)
				m_r[15] += 4 * (15 - R2);
			// TRAPS: C,U,A,P,R
			break;

		case 0x20: case 0x21: case 0x22: case 0x23:
		case 0x24: case 0x25: case 0x26: case 0x27:
			// saved0..saved7: push registers fN:f7

			// store fi at sp - 8 * (8 - i)
			for (int i = m_info.subopcode & 0x7; i < 8 && !m_exception; i++)
				get_dcammu().store<u64>(m_ssw, m_r[15] - 8 * (8 - i), get_fp64(i).v);

			// decrement sp after push to allow restart on exceptions
			if (!m_exception)
				m_r[15] -= 8 * (8 - (m_info.subopcode & 0x7));
			// TRAPS: A,P,W
			break;
		case 0x28: case 0x29: case 0x2a: case 0x2b:
		case 0x2c: case 0x2d: case 0x2e: case 0x2f:
			// restd0..restd7: pop registers fN:f7

			// load fi from sp + 8 * (i - N)
			for (int i = m_info.subopcode & 0x7; i < 8 && !m_exception; i++)
				get_dcammu().load<u64>(m_ssw, m_r[15] + 8 * (i - (m_info.subopcode & 0x7)), [this, i](u64 v) { set_fp(i, float64_t{ v }, F_NONE); });

			// increment sp after pop to allow restart on exceptions
			if (!m_exception)
				m_r[15] += 8 * (8 - (m_info.subopcode & 0x7));
			// TRAPS: C,U,A,P,R
			break;
		case 0x30:
			// cnvsw: convert single floating to word
			m_fp_pc = m_info.pc;

			m_r[m_info.macro & 0xf] = f32_to_i32(get_fp32((m_info.macro >> 4) & 0xf), softfloat_roundingMode, true);
			// TRAPS: F_IX
			softfloat_exceptionFlags &= F_IX;
			break;
		case 0x31:
			// cnvrsw: convert rounding single floating to word (non-IEEE +0.5/-0.5 rounding)
			m_fp_pc = m_info.pc;

			if (f32_lt(get_fp32((m_info.macro >> 4) & 0xf), float32_t{ 0 }))
				m_r[m_info.macro & 0xf] = f32_to_i32(f32_sub(get_fp32((m_info.macro >> 4) & 0xf),
					f32_div(i32_to_f32(1), i32_to_f32(2))), softfloat_round_minMag, true);
			else
				m_r[m_info.macro & 0xf] = f32_to_i32(f32_add(get_fp32((m_info.macro >> 4) & 0xf),
					f32_div(i32_to_f32(1), i32_to_f32(2))), softfloat_round_minMag, true);
			// TRAPS: F_IX
			softfloat_exceptionFlags &= F_IX;
			break;
		case 0x32:
			// cnvtsw: convert truncating single floating to word
			m_fp_pc = m_info.pc;

			m_r[m_info.macro & 0xf] = f32_to_i32(get_fp32((m_info.macro >> 4) & 0xf), softfloat_round_minMag, true);
			// TRAPS: F_IX
			softfloat_exceptionFlags &= F_IX;
			break;
		case 0x33:
			// cnvws: convert word to single floating
			set_fp(m_info.macro & 0xf, i32_to_f32(m_r[(m_info.macro >> 4) & 0xf]), F_X);
			// TRAPS: F_X
			break;
		case 0x34:
			// cnvdw: convert double floating to word
			m_fp_pc = m_info.pc;

			m_r[m_info.macro & 0xf] = f64_to_i32(get_fp64((m_info.macro >> 4) & 0xf), softfloat_roundingMode, true);
			// TRAPS: F_IX
			softfloat_exceptionFlags &= F_IX;
			break;
		case 0x35:
			// cnvrdw: convert rounding double floating to word (non-IEEE +0.5/-0.5 rounding)
			m_fp_pc = m_info.pc;

			if (f64_lt(get_fp64((m_info.macro >> 4) & 0xf), float64_t{ 0 }))
				m_r[m_info.macro & 0xf] = f64_to_i32(f64_sub(get_fp64((m_info.macro >> 4) & 0xf),
					f64_div(i32_to_f64(1), i32_to_f64(2))), softfloat_round_minMag, true);
			else
				m_r[m_info.macro & 0xf] = f64_to_i32(f64_add(get_fp64((m_info.macro >> 4) & 0xf),
					f64_div(i32_to_f64(1), i32_to_f64(2))), softfloat_round_minMag, true);
			// TRAPS: F_IX
			softfloat_exceptionFlags &= F_IX;
			break;
		case 0x36:
			// cnvtdw: convert truncating double floating to word
			m_fp_pc = m_info.pc;

			m_r[m_info.macro & 0xf] = f64_to_i32(get_fp64((m_info.macro >> 4) & 0xf), softfloat_round_minMag, true);
			// TRAPS: F_IX
			softfloat_exceptionFlags &= F_IX;
			break;
		case 0x37:
			// cnvwd: convert word to double floating
			set_fp(m_info.macro & 0xf, i32_to_f64(m_r[(m_info.macro >> 4) & 0xf]), F_NONE);
			break;
		case 0x38:
			// cnvsd: convert single to double floating
			set_fp(m_info.macro & 0xf, f32_to_f64(get_fp32((m_info.macro >> 4) & 0xf)), F_I);
			// TRAPS: F_I
			break;
		case 0x39:
			// cnvds: convert double to single floating
			set_fp(m_info.macro & 0xf, f64_to_f32(get_fp64((m_info.macro >> 4) & 0xf)), F_IVUX);
			// TRAPS: F_IVUX
			break;
		case 0x3a:
			// negs: negate single floating
			set_fp(m_info.macro & 0xf, f32_mul(get_fp32((m_info.macro >> 4) & 0xf), i32_to_f32(-1)), F_NONE);
			break;
		case 0x3b:
			// negd: negate double floating
			set_fp(m_info.macro & 0xf, f64_mul(get_fp64((m_info.macro >> 4) & 0xf), i32_to_f64(-1)), F_NONE);
			break;
		case 0x3c:
			/*
			 * This implementation for scalbd and scalbs is a bit opaque, but
			 * essentially we check if the integer value is within range, and
			 * directly create a floating constant representing 2^n or NaN
			 * respectively, which is used as an input to a multiply, producing
			 * the desired result. While doing an actual multiply is both
			 * inefficient and unnecessary, it's a tidy way to ensure the
			 * correct exception flags are set.
			 */
			// scalbs: scale by, single floating
			set_fp(m_info.macro & 0xf, f32_mul(get_fp32(m_info.macro & 0xf),
				((s32(m_r[(m_info.macro >> 4) & 0xf]) > -127 && s32(m_r[(m_info.macro >> 4) & 0xf]) < 128)
					? float32_t{ u32(s32(m_r[(m_info.macro >> 4) & 0xf]) + 127) << 23 }
					: float32_t{ ~u32(0) })), F_IVUX);
			// TRAPS: F_IVUX
			break;
		case 0x3d:
			// scalbd: scale by, double floating
			set_fp(m_info.macro & 0xf, f64_mul(get_fp64(m_info.macro & 0xf),
				(s32(m_r[(m_info.macro >> 4) & 0xf]) > -1023 && s32(m_r[(m_info.macro >> 4) & 0xf]) < 1024)
					? float64_t{ u64(s32(m_r[(m_info.macro >> 4) & 0xf]) + 1023) << 52 }
					: float64_t{ ~u64(0) }), F_IVUX);
			// TRAPS: F_IVUX
			break;
		case 0x3e:
			// trapfn: trap floating unordered
			// TRAPS: I
			if (PSW(Z) && PSW(N))
				m_exception = EXCEPTION_ILLEGAL_OPERATION;
			break;
		case 0x3f:
			// loadfs: load floating status
			m_r[(m_info.macro >> 4) & 0xf] = m_fp_pc;
			m_f[m_info.macro & 0xf] = m_fp_dst;
			m_ssw |= SSW_FRD;
			break;

		default:
			m_exception = EXCEPTION_ILLEGAL_OPERATION;
			break;
		}
		break;

	case 0xb6:
		// privileged macro instructions
		if (!SSW(U))
		{
			switch (m_info.subopcode)
			{
			case 0x00:
				// movus: move user to supervisor
				m_rs[m_info.macro & 0xf] = m_ru[(m_info.macro >> 4) & 0xf];
				FLAGS(0, 0, m_rs[m_info.macro & 0xf] == 0, BIT31(m_rs[m_info.macro & 0xf]));
				// FLAGS: 00ZN
				// TRAPS: S
				break;
			case 0x01:
				// movsu: move supervisor to user
				m_ru[m_info.macro & 0xf] = m_rs[(m_info.macro >> 4) & 0xf];
				FLAGS(0, 0, m_ru[m_info.macro & 0xf] == 0, BIT31(m_ru[m_info.macro & 0xf]));
				// FLAGS: 00ZN
				// TRAPS: S
				break;
			case 0x02:
				// saveur: save user registers
				for (int i = 0; i < 16 && !m_exception; i++)
					get_dcammu().store<u32>(m_ssw, m_rs[(m_info.macro >> 4) & 0xf] - 4 * (i + 1), m_ru[15 - i]);

				if (!m_exception)
					m_rs[(m_info.macro >> 4) & 0xf] -= 64;
				// TRAPS: A,P,W,S
				break;
			case 0x03:
				// restur: restore user registers
				for (int i = 0; i < 16 && !m_exception; i++)
					get_dcammu().load<u32>(m_ssw, m_rs[(m_info.macro >> 4) & 0xf] + 4 * i, [this, i](u32 v) { m_ru[i] = v; });

				if (!m_exception)
					m_rs[(m_info.macro >> 4) & 0xf] += 64;
				// TRAPS: C,U,A,P,R,S
				break;
			case 0x04:
				// reti: restore psw, ssw and pc from supervisor stack
				m_pc = reti();
				// TRAPS: S
				break;
			case 0x05:
				// wait: wait for interrupt
				m_wait = true;
				// TRAPS: S
				break;

			default:
				m_exception = EXCEPTION_ILLEGAL_OPERATION;
				break;
			}
		}
		else
			m_exception = EXCEPTION_PRIVILEGED_INSTRUCTION;
		break;

	default:
		m_exception = EXCEPTION_ILLEGAL_OPERATION;
		break;
	}
}

u32 clipper_device::reti()
{
	u32 new_psw = 0, new_ssw = 0, new_pc = 0;

	// pop the psw, ssw and pc from the supervisor stack
	if (!get_dcammu().load<u32>(m_ssw, m_rs[(m_info.macro >> 4) & 0xf] + 0, [&new_psw](u32 v) { new_psw = v; }))
		fatalerror("reti unrecoverable fault 0x%04x pop psw address 0x%08x pc 0x%08x\n", m_exception, m_rs[(m_info.macro >> 4) & 0xf] + 0, m_info.pc);

	if (!get_dcammu().load<u32>(m_ssw, m_rs[(m_info.macro >> 4) & 0xf] + 4, [&new_ssw](u32 v) { new_ssw = v; }))
		fatalerror("reti unrecoverable fault 0x%04x pop ssw address 0x%08x pc 0x%08x\n", m_exception, m_rs[(m_info.macro >> 4) & 0xf] + 4, m_info.pc);

	if (!get_dcammu().load<u32>(m_ssw, m_rs[(m_info.macro >> 4) & 0xf] + 8, [&new_pc](u32 v) { new_pc = v; }))
		fatalerror("reti unrecoverable fault 0x%04x pop pc address 0x%08x pc 0x%08x\n", m_exception, m_rs[(m_info.macro >> 4) & 0xf] + 8, m_info.pc);

	LOGMASKED(LOG_EXCEPTION, "reti r%d ssp 0x%08x pc 0x%08x ssw 0x%08x psw 0x%08x new_pc 0x%08x new_ssw 0x%08x new_psw 0x%08x\n",
		(m_info.macro >> 4) & 0xf, m_rs[(m_info.macro >> 4) & 0xf], m_info.pc, m_ssw, m_psw, new_pc, new_ssw, new_psw);

	// adjust the stack pointer
	m_rs[(m_info.macro >> 4) & 0xf] += 12;

	// restore the psw and ssw
	set_psw(new_psw);
	set_ssw(new_ssw);

	// return the restored pc
	return new_pc;
}

/*
 * Common entry point for transferring control in the event of an interrupt or
 * exception. Reading between the lines, it appears this logic was implemented
 * using the macro instruction ROM and a special macro instruction (intrap).
 */
u32 clipper_device::intrap(const u16 vector, const u32 old_pc)
{
	const u32 old_ssw = m_ssw;
	const u32 old_psw = m_psw;
	u32 new_pc = 0, new_ssw = 0;

	// clear ssw bits to enable supervisor memory access
	m_ssw &= ~(SSW_U | SSW_K | SSW_UU | SSW_KU);

	// clear exception state
	m_exception = 0;

	// fetch next pc and ssw from interrupt vector
	if (!get_dcammu().load<u32>(m_ssw, vector + 0, [&new_pc](u32 v) { new_pc = v; }))
		fatalerror("intrap unrecoverable fault 0x%04x load pc address 0x%08x pc 0x%08x\n", m_exception, vector + 0, old_pc);
	if (!get_dcammu().load<u32>(m_ssw, vector + 4, [&new_ssw](u32 v) { new_ssw = v; }))
		fatalerror("intrap unrecoverable fault 0x%04x load ssw address 0x%08x pc 0x%08x\n", m_exception, vector + 4, old_pc);

	LOGMASKED(LOG_EXCEPTION, "intrap vector 0x%04x pc 0x%08x ssp 0x%08x new_pc 0x%08x new_ssw 0x%08x\n", vector, old_pc, m_rs[15], new_pc, new_ssw);

	// derive cts and mts from vector
	u32 source = 0;
	switch (vector)
	{
		// data memory trap group
	case EXCEPTION_D_CORRECTED_MEMORY_ERROR:
	case EXCEPTION_D_UNCORRECTABLE_MEMORY_ERROR:
	case EXCEPTION_D_ALIGNMENT_FAULT:
	case EXCEPTION_D_PAGE_FAULT:
	case EXCEPTION_D_READ_PROTECT_FAULT:
	case EXCEPTION_D_WRITE_PROTECT_FAULT:

		// instruction memory trap group
	case EXCEPTION_I_CORRECTED_MEMORY_ERROR:
	case EXCEPTION_I_UNCORRECTABLE_MEMORY_ERROR:
	case EXCEPTION_I_ALIGNMENT_FAULT:
	case EXCEPTION_I_PAGE_FAULT:
	case EXCEPTION_I_EXECUTE_PROTECT_FAULT:
		source = (vector & MTS_VMASK) << (MTS_SHIFT - MTS_VSHIFT);
		break;

		// integer arithmetic trap group
	case EXCEPTION_INTEGER_DIVIDE_BY_ZERO:
		source = CTS_DIVIDE_BY_ZERO;
		break;

		// illegal operation trap group
	case EXCEPTION_ILLEGAL_OPERATION:
		source = CTS_ILLEGAL_OPERATION;
		break;
	case EXCEPTION_PRIVILEGED_INSTRUCTION:
		source = CTS_PRIVILEGED_INSTRUCTION;
		break;

		// diagnostic trap group
	case EXCEPTION_TRACE:
		source = CTS_TRACE_TRAP;
		break;
	}

	// push pc, ssw and psw onto supervisor stack
	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0x4, old_pc))
		fatalerror("intrap unrecoverable fault 0x%04x push pc ssp 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0x4, old_pc);

	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0x8, old_ssw))
		fatalerror("intrap unrecoverable fault 0x%04x push ssw ssp 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0x8, old_pc);

	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0xc, (old_psw & ~(PSW_CTS | PSW_MTS)) | source))
		fatalerror("intrap unrecoverable fault 0x%04x push psw ssp 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0xc, old_pc);

	// decrement supervisor stack pointer
	m_rs[15] -= 12;

	// set ssw from vector and previous mode
	set_ssw((new_ssw & ~SSW_P) | (old_ssw & SSW_U) << 1);

	// clear psw
	set_psw(0);

	// return new pc from trap vector
	return new_pc;
}

u32 clipper_c400_device::intrap(const u16 vector, const u32 old_pc)
{
	const u32 old_ssw = m_ssw;
	const u32 old_psw = m_psw;
	u32 new_pc = 0, new_ssw = 0;

	// clear ssw bits to enable supervisor memory access
	m_ssw &= ~(SSW_U | SSW_K | SSW_UU | SSW_KU);

	// clear exception state
	m_exception = 0;

	// fetch ssw and pc from interrupt vector (C400 reversed wrt C100/C300)
	if (!get_dcammu().load<u32>(m_ssw, vector + 0, [&new_ssw](u32 v) { new_ssw = v; }))
		fatalerror("intrap unrecoverable fault 0x%04x load ssw address 0x%08x pc 0x%08x\n", m_exception, vector + 0, old_pc);
	if (!get_dcammu().load<u32>(m_ssw, vector + 4, [&new_pc](u32 v) { new_pc = v; }))
		fatalerror("intrap unrecoverable fault 0x%04x load pc address 0x%08x pc 0x%08x\n", m_exception, vector + 4, old_pc);

	LOGMASKED(LOG_EXCEPTION, "intrap vector 0x%04x pc 0x%08x ssp 0x%08x new_pc 0x%08x new_ssw 0x%08x\n", vector, old_pc, m_rs[15], new_pc, new_ssw);

	// derive cts and mts from vector
	u32 source = 0;
	switch (vector)
	{
		// data memory trap group
	case EXCEPTION_D_CORRECTED_MEMORY_ERROR:
	case EXCEPTION_D_UNCORRECTABLE_MEMORY_ERROR:
	case EXCEPTION_D_ALIGNMENT_FAULT:
	case EXCEPTION_D_PAGE_FAULT:
	case EXCEPTION_D_READ_PROTECT_FAULT:
	case EXCEPTION_D_WRITE_PROTECT_FAULT:

		// instruction memory trap group
	case EXCEPTION_I_CORRECTED_MEMORY_ERROR:
	case EXCEPTION_I_UNCORRECTABLE_MEMORY_ERROR:
	case EXCEPTION_I_ALIGNMENT_FAULT:
	case EXCEPTION_I_PAGE_FAULT:
	case EXCEPTION_I_EXECUTE_PROTECT_FAULT:
		source = (vector & MTS_VMASK) << (MTS_SHIFT - MTS_VSHIFT);
		break;

		// integer arithmetic trap group
	case EXCEPTION_INTEGER_DIVIDE_BY_ZERO:
		source = CTS_DIVIDE_BY_ZERO;
		break;

		// illegal operation trap group
	case EXCEPTION_ILLEGAL_OPERATION:
		source = CTS_ILLEGAL_OPERATION;
		break;
	case EXCEPTION_PRIVILEGED_INSTRUCTION:
		source = CTS_PRIVILEGED_INSTRUCTION;
		break;

		// diagnostic trap group
	case EXCEPTION_TRACE:
		source = CTS_TRACE_TRAP;
		break;
	}

	// push pc, ssw and psw onto supervisor stack
	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0x4, old_pc))
		fatalerror("intrap unrecoverable fault 0x%04x push pc ssp 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0x4, old_pc);

	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0x8, old_ssw))
		fatalerror("intrap unrecoverable fault 0x%04x push ssw ssp 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0x8, old_pc);

	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0xc, (old_psw & ~(PSW_CTS | PSW_MTS)) | source))
		fatalerror("intrap unrecoverable fault 0x%04x push psw ssp 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0xc, old_pc);

	// TODO: push pc1
	// TODO: push pc2

	// push delayed branch pc onto supervisor stack
	if (!get_dcammu().store<u32>(m_ssw, m_rs[15] - 0x18, m_db_pc))
		fatalerror("intrap unrecoverable fault 0x%04x push db_pc address 0x%08x pc 0x%08x\n", m_exception, m_rs[15] - 0x18, old_pc);

	// decrement supervisor stack pointer
	m_rs[15] -= 24;

	// set ssw from vector and previous mode
	set_ssw((new_ssw & ~SSW_P) | (old_ssw & SSW_U) << 1);

	// clear psw
	set_psw(0);

	// return new pc from trap vector
	return new_pc;
}

bool clipper_device::evaluate_branch() const
{
	switch (m_info.r2)
	{
	case BRANCH_T:
		return true;

	case BRANCH_LT:
		return (!PSW(V) && !PSW(Z) && !PSW(N))
			|| (PSW(V) && !PSW(Z) && PSW(N));

	case BRANCH_LE:
		return (!PSW(V) && !PSW(N))
			|| (PSW(V) && !PSW(Z) && PSW(N));

	case BRANCH_EQ:
		return PSW(Z) && !PSW(N);

	case BRANCH_GT:
		return (!PSW(V) && !PSW(Z) && PSW(N))
			|| (PSW(V) && !PSW(N));

	case BRANCH_GE:
		return (PSW(V) && !PSW(N))
			|| (!PSW(V) && !PSW(Z) && PSW(N))
			|| (PSW(Z) && !PSW(N));

	case BRANCH_NE:
		return (!PSW(Z))
			|| (PSW(Z) && PSW(N));

	case BRANCH_LTU:
		return (!PSW(C) && !PSW(Z));

	case BRANCH_LEU:
		return !PSW(C);

	case BRANCH_GTU:
		return PSW(C);

	case BRANCH_GEU:
		return PSW(C) || PSW(Z);

	case BRANCH_V:
		return PSW(V);
	case BRANCH_NV:
		return !PSW(V);

	case BRANCH_N:
		return !PSW(Z) && PSW(N);
	case BRANCH_NN:
		return !PSW(N);

	case BRANCH_FN:
		return PSW(Z) && PSW(N);
	}

	return false;
}

void clipper_device::set_psw(const u32 psw)
{
	// retain read-only endianness field
	m_psw = (m_psw & PSW_BIG) | (psw & ~PSW_BIG);

	// set the softfloat rounding mode based on the psw rounding mode
	switch (PSW(FR))
	{
	case FR_0: softfloat_roundingMode = softfloat_round_near_even; break;
	case FR_1: softfloat_roundingMode = softfloat_round_max; break;
	case FR_2: softfloat_roundingMode = softfloat_round_min; break;
	case FR_3: softfloat_roundingMode = softfloat_round_minMag; break;
	}
}

void clipper_device::set_ssw(const u32 ssw)
{
	// retain read-only id field
	m_ssw = (m_ssw & SSW_ID) | (ssw & ~SSW_ID);

	// select the register file
	m_r = SSW(U) ? m_ru : m_rs;
}

void clipper_device::fp_exception()
{
	u16 exception = 0;

	/*
	 * Set the psw floating exception flags, and identify any enabled
	 * exceptions. The order here is important, but since the documentation
	 * doesn't explicitly specify, this is a guess. Simply put, exceptions
	 * are considered in sequence with an increasing order of priority.
	 */
	if (softfloat_exceptionFlags & softfloat_flag_inexact)
	{
		m_psw |= PSW_FX;
		if (PSW(EFX))
			exception = EXCEPTION_FLOATING_INEXACT;
	}
	if (softfloat_exceptionFlags & softfloat_flag_underflow)
	{
		m_psw |= PSW_FU;
		if (PSW(EFU))
			exception = EXCEPTION_FLOATING_UNDERFLOW;
	}
	if (softfloat_exceptionFlags & softfloat_flag_overflow)
	{
		m_psw |= PSW_FV;
		if (PSW(EFV))
			exception = EXCEPTION_FLOATING_OVERFLOW;
	}
	if (softfloat_exceptionFlags & softfloat_flag_infinite)
	{
		m_psw |= PSW_FD;
		if (PSW(EFD))
			exception = EXCEPTION_FLOATING_DIVIDE_BY_ZERO;
	}
	if (softfloat_exceptionFlags & softfloat_flag_invalid)
	{
		m_psw |= PSW_FI;
		if (PSW(EFI))
			exception = EXCEPTION_FLOATING_INVALID_OPERATION;
	}

	// trigger a floating point exception
	if (PSW(EFT) && exception)
		m_exception = exception;
}

void clipper_c400_device::execute_instruction()
{
	// update delay slot pointer
	switch (PSW(DSP))
	{
	case DSP_S1:
		// take delayed branch
		m_psw &= ~PSW_DSP;
		m_pc = m_db_pc;
		return;

	case DSP_SALL:
		// one delay slot still active
		m_psw &= ~PSW_DSP;
		m_psw |= DSP_S1;
		break;

	case DSP_SETUP:
		// two delay slots active
		m_psw &= ~PSW_DSP;
		m_psw |= DSP_SALL;
		break;
	}

	// if executing a delay slot instruction, test for valid type
	if (PSW(DSP))
	{
		switch (m_info.opcode)
		{
		case 0x13: // ret
		case 0x44: // call
		case 0x45:
		case 0x48: // b*
		case 0x49:
		case 0x4a: // cdb
		case 0x4b:
		case 0x4c: // cdbeq
		case 0x4d:
		case 0x4e: // cdbne
		case 0x4f:
		case 0x50: // db*
		case 0x51:
			// TODO: this should throw some kind of illegal instruction trap, not abort
			fatalerror("instruction type 0x%02x invalid in branch delay slot pc 0x%08x\n", m_info.opcode, m_info.pc);

		default:
			break;
		}
	}

	switch (m_info.opcode)
	{
	case 0x46:
	case 0x47:
		// loadd2: load double floating double
		// TODO: 128-bit load
		get_dcammu().load<u64>(m_ssw, m_info.address + 0, [this](u64 v) { set_fp(R2 + 0, float64_t{ v }, F_NONE); });
		get_dcammu().load<u64>(m_ssw, m_info.address + 8, [this](u64 v) { set_fp(R2 + 1, float64_t{ v }, F_NONE); });
		// TRAPS: C,U,A,P,R,I
		break;

	case 0x4a:
	case 0x4b:
		// cdb: compare and delayed branch?
		// emulate.h: "cdb is special because it does not support all addressing modes", 2-3 parcels
		fatalerror("cdb pc 0x%08x\n", m_info.pc);
	case 0x4c:
	case 0x4d:
		// cdbeq: compare and delayed branch if equal?
		if (m_r[R2] == 0)
		{
			m_psw |= DSP_SETUP;
			m_db_pc = m_info.address;
		}
		break;
	case 0x4e:
	case 0x4f:
		// cdbne: compare and delayed branch if not equal?
		if (m_r[R2] != 0)
		{
			m_psw |= DSP_SETUP;
			m_db_pc = m_info.address;
		}
		break;
	case 0x50:
	case 0x51:
		// db*: delayed branch on condition
		if (evaluate_branch())
		{
			m_psw |= DSP_SETUP;
			m_db_pc = m_info.address;
		}
		break;

	case 0xb0:
		// abss: absolute value single floating?
		if (f32_lt(get_fp32(R1), float32_t{ 0 }))
			set_fp(R2, f32_mul(get_fp32(R1), i32_to_f32(-1)), F_IVUX);
		else
			set_fp(R2, get_fp32(R1), F_IVUX);
		break;

	case 0xb2:
		// absd: absolute value double floating?
		if (f64_lt(get_fp64(R1), float64_t{ 0 }))
			set_fp(R2, f64_mul(get_fp64(R1), i32_to_f64(-1)), F_IVUX);
		else
			set_fp(R2, get_fp64(R1), F_IVUX);
		break;

	case 0xb4:
		// unprivileged macro instructions
		switch (m_info.subopcode)
		{
		case 0x44:
			// cnvxsw: ??
			fatalerror("cnvxsw pc 0x%08x\n", m_info.pc);
		case 0x46:
			// cnvxdw: ??
			fatalerror("cnvxdw pc 0x%08x\n", m_info.pc);

		default:
			clipper_device::execute_instruction();
			break;
		}
		break;

	case 0xb6:
		// privileged macro instructions
		if (!SSW(U))
		{
			switch (m_info.subopcode)
			{
			case 0x07:
				// loadts: unknown?
				fatalerror("loadts pc 0x%08x\n", m_info.pc);

			default:
				clipper_device::execute_instruction();
				break;
			}
		}
		else
			m_exception = EXCEPTION_PRIVILEGED_INSTRUCTION;
		break;

	case 0xbc:
		// waitd:
		if (!SSW(U))
			; // TODO: don't know what this instruction does
		else
			m_exception = EXCEPTION_PRIVILEGED_INSTRUCTION;
		break;

	case 0xc0:
		// s*: set register on condition
		m_r[R1] = evaluate_branch() ? 1 : 0;
		break;

	default:
		clipper_device::execute_instruction();
		break;
	}
}

std::unique_ptr<util::disasm_interface> clipper_device::create_disassembler()
{
	return std::make_unique<clipper_disassembler>();
}

std::string clipper_device::debug_string(u32 pointer)
{
	auto const suppressor(machine().disable_side_effects());

	std::string s("");

	while (true)
	{
		char c;

		if (!get_dcammu().load<u8>(m_ssw, pointer++, [&c](u8 v) { c = v; }))
			break;

		if (c == '\0')
			break;

		s += c;
	}

	return s;
}

std::string clipper_device::debug_string_array(u32 array_pointer)
{
	auto const suppressor(machine().disable_side_effects());

	std::string s("");

	while (true)
	{
		u32 string_pointer;

		if (!get_dcammu().load<u32>(m_ssw, array_pointer, [&string_pointer](u32 v) { string_pointer = v; }))
			break;

		if (string_pointer == 0)
			break;

		if (!s.empty())
			s += ", ";

		s += '\"' + debug_string(string_pointer) + '\"';
		array_pointer += 4;
	}

	return s;
}
