@@ -16,12 +16,14 @@ FPURegCache::FPURegCache(Jit64& jit) : RegCache{jit}

void FPURegCache::StoreRegister(preg_t preg, const OpArg& new_loc)
{
m_emitter->MOVAPD(new_loc, m_regs[preg].Location().GetSimpleReg());
ASSERT_MSG(DYNA_REC, m_regs[preg].IsBound(), "Unbound register - %zu", preg);
m_emitter->MOVAPD(new_loc, m_regs[preg].Location()->GetSimpleReg());
}

void FPURegCache::LoadRegister(preg_t preg, X64Reg new_loc)
{
m_emitter->MOVAPD(new_loc, m_regs[preg].Location());
ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - %zu", preg);
m_emitter->MOVAPD(new_loc, m_regs[preg].Location().value());
}

const X64Reg* FPURegCache::GetAllocationOrder(size_t* count) const
@@ -16,12 +16,14 @@ GPRRegCache::GPRRegCache(Jit64& jit) : RegCache{jit}

void GPRRegCache::StoreRegister(preg_t preg, const OpArg& new_loc)
{
m_emitter->MOV(32, new_loc, m_regs[preg].Location());
ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - %zu", preg);
m_emitter->MOV(32, new_loc, m_regs[preg].Location().value());
}

void GPRRegCache::LoadRegister(preg_t preg, X64Reg new_loc)
{
m_emitter->MOV(32, ::Gen::R(new_loc), m_regs[preg].Location());
ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - %zu", preg);
m_emitter->MOV(32, ::Gen::R(new_loc), m_regs[preg].Location().value());
}

OpArg GPRRegCache::GetDefaultLocation(preg_t preg) const
@@ -56,7 +58,7 @@ void GPRRegCache::SetImmediate32(preg_t preg, u32 imm_value, bool dirty)

BitSet32 GPRRegCache::GetRegUtilization() const
{
return m_jit.js.op->gprInReg;
return m_jit.js.op->gprInUse;
}

BitSet32 GPRRegCache::CountRegsIn(preg_t preg, u32 lookahead) const
@@ -314,6 +314,7 @@ bool RegCache::SanityCheck() const
switch (m_regs[i].GetLocationType())
{
case PPCCachedReg::LocationType::Default:
case PPCCachedReg::LocationType::Discarded:
case PPCCachedReg::LocationType::SpeculativeImmediate:
case PPCCachedReg::LocationType::Immediate:
break;
@@ -322,7 +323,7 @@ bool RegCache::SanityCheck() const
if (m_regs[i].IsLocked() || m_regs[i].IsRevertable())
return false;

Gen::X64Reg xr = m_regs[i].Location().GetSimpleReg();
Gen::X64Reg xr = m_regs[i].Location()->GetSimpleReg();
if (m_xregs[xr].IsLocked())
return false;
if (m_xregs[xr].Contents() != i)
@@ -380,6 +381,29 @@ RCForkGuard RegCache::Fork()
return RCForkGuard{*this};
}

void RegCache::Discard(BitSet32 pregs)
{
ASSERT_MSG(
DYNA_REC,
std::none_of(m_xregs.begin(), m_xregs.end(), [](const auto& x) { return x.IsLocked(); }),
"Someone forgot to unlock a X64 reg");

for (preg_t i : pregs)
{
ASSERT_MSG(DYNA_REC, !m_regs[i].IsLocked(),
"Someone forgot to unlock PPC reg %zu (X64 reg %i).", i, RX(i));
ASSERT_MSG(DYNA_REC, !m_regs[i].IsRevertable(), "Register transaction is in progress!");

if (m_regs[i].IsBound())
{
X64Reg xr = RX(i);
m_xregs[xr].Unbind();
}

m_regs[i].SetDiscarded();
}
}

void RegCache::Flush(BitSet32 pregs)
{
ASSERT_MSG(
@@ -396,6 +420,7 @@ void RegCache::Flush(BitSet32 pregs)
switch (m_regs[i].GetLocationType())
{
case PPCCachedReg::LocationType::Default:
case PPCCachedReg::LocationType::Discarded:
break;
case PPCCachedReg::LocationType::SpeculativeImmediate:
// We can have a cached value without a host register through speculative constants.
@@ -474,8 +499,8 @@ void RegCache::DiscardRegContentsIfCached(preg_t preg)
{
if (m_regs[preg].IsBound())
{
X64Reg xr = m_regs[preg].Location().GetSimpleReg();
m_xregs[xr].SetFlushed();
X64Reg xr = m_regs[preg].Location()->GetSimpleReg();
m_xregs[xr].Unbind();
m_regs[preg].SetFlushed();
}
}
@@ -494,12 +519,15 @@ void RegCache::BindToRegister(preg_t i, bool doLoad, bool makeDirty)

if (doLoad)
{
ASSERT_MSG(DYNA_REC, !m_regs[i].IsDiscarded(), "Attempted to load a discarded value");
LoadRegister(i, xr);
}

ASSERT_MSG(DYNA_REC,
std::none_of(m_regs.begin(), m_regs.end(),
[xr](const auto& r) { return r.Location().IsSimpleReg(xr); }),
[xr](const auto& r) {
return r.Location().has_value() && r.Location()->IsSimpleReg(xr);
}),
"Xreg %i already bound", xr);

m_regs[i].SetBoundTo(xr);
@@ -525,14 +553,15 @@ void RegCache::StoreFromRegister(preg_t i, FlushMode mode)
switch (m_regs[i].GetLocationType())
{
case PPCCachedReg::LocationType::Default:
case PPCCachedReg::LocationType::Discarded:
case PPCCachedReg::LocationType::SpeculativeImmediate:
return;
case PPCCachedReg::LocationType::Bound:
{
X64Reg xr = RX(i);
doStore = m_xregs[xr].IsDirty();
if (mode == FlushMode::Full)
m_xregs[xr].SetFlushed();
m_xregs[xr].Unbind();
break;
}
case PPCCachedReg::LocationType::Immediate:
@@ -635,13 +664,14 @@ float RegCache::ScoreRegister(X64Reg xreg) const

const OpArg& RegCache::R(preg_t preg) const
{
return m_regs[preg].Location();
ASSERT_MSG(DYNA_REC, !m_regs[preg].IsDiscarded(), "Discarded register - %zu", preg);
return m_regs[preg].Location().value();
}

X64Reg RegCache::RX(preg_t preg) const
{
ASSERT_MSG(DYNA_REC, m_regs[preg].IsBound(), "Unbound register - %zu", preg);
return m_regs[preg].Location().GetSimpleReg();
return m_regs[preg].Location()->GetSimpleReg();
}

void RegCache::Lock(preg_t preg)
@@ -707,6 +737,7 @@ void RegCache::Realize(preg_t preg)
}
m_constraints[preg].Realized(RCConstraint::RealizedLoc::Mem);
return;
case PPCCachedReg::LocationType::Discarded:
case PPCCachedReg::LocationType::Bound:
do_bind();
return;
@@ -169,6 +169,7 @@ class RegCache
RCX64Reg Scratch(Gen::X64Reg xr);

RCForkGuard Fork();
void Discard(BitSet32 pregs);
void Flush(BitSet32 pregs = BitSet32::AllTrue(32));
void Revert();
void Commit();
@@ -828,7 +828,12 @@ void JitArm64::DoJit(u32 em_address, JitBlock* b, u32 nextPC)
if (!CanMergeNextInstructions(1) || js.op[1].opinfo->type != ::OpType::Integer)
FlushCarry();

// If we have a register that will never be used again, flush it.
// If we have a register that will never be used again, discard or flush it.
if (!SConfig::GetInstance().bJITRegisterCacheOff)
{
gpr.DiscardRegisters(op.gprDiscardable);
fpr.DiscardRegisters(op.fprDiscardable);
}
gpr.StoreRegisters(~op.gprInUse);
fpr.StoreRegisters(~op.fprInUse);

@@ -24,6 +24,12 @@ void Arm64RegCache::Init(ARM64XEmitter* emitter)
GetAllocationOrder();
}

void Arm64RegCache::DiscardRegisters(BitSet32 regs)
{
for (int j : regs)
DiscardRegister(j);
}

ARM64Reg Arm64RegCache::GetReg()
{
// If we have no registers left, dump the most stale register first
@@ -96,8 +102,8 @@ void Arm64RegCache::FlushMostStaleRegister()
const auto& reg = m_guest_registers[i];
const u32 last_used = reg.GetLastUsed();

if (last_used > most_stale_amount &&
(reg.GetType() != RegType::NotLoaded && reg.GetType() != RegType::Immediate))
if (last_used > most_stale_amount && reg.GetType() != RegType::NotLoaded &&
reg.GetType() != RegType::Discarded && reg.GetType() != RegType::Immediate)
{
most_stale_preg = i;
most_stale_amount = last_used;
@@ -107,6 +113,16 @@ void Arm64RegCache::FlushMostStaleRegister()
FlushRegister(most_stale_preg, false);
}

void Arm64RegCache::DiscardRegister(size_t preg)
{
OpArg& reg = m_guest_registers[preg];
ARM64Reg host_reg = reg.GetReg();

reg.Discard();
if (host_reg != ARM64Reg::INVALID_REG)
UnlockRegister(host_reg);
}

// GPR Cache
constexpr size_t GUEST_GPR_COUNT = 32;
constexpr size_t GUEST_CR_COUNT = 8;
@@ -284,6 +300,9 @@ ARM64Reg Arm64GPRCache::R(const GuestRegInfo& guest_reg)
return host_reg;
}
break;
case RegType::Discarded:
ASSERT_MSG(DYNA_REC, false, "Attempted to read discarded register");
break;
case RegType::NotLoaded: // Register isn't loaded at /all/
{
// This is a bit annoying. We try to keep these preloaded as much as possible
@@ -318,14 +337,18 @@ void Arm64GPRCache::BindToRegister(const GuestRegInfo& guest_reg, bool do_load)
const size_t bitsize = guest_reg.bitsize;

reg.ResetLastUsed();

reg.SetDirty(true);
if (reg.GetType() == RegType::NotLoaded)

const RegType reg_type = reg.GetType();
if (reg_type == RegType::NotLoaded || reg_type == RegType::Discarded)
{
const ARM64Reg host_reg = bitsize != 64 ? GetReg() : EncodeRegTo64(GetReg());
reg.Load(host_reg);
if (do_load)
{
ASSERT_MSG(DYNA_REC, reg_type != RegType::Discarded, "Attempted to load a discarded value");
m_emit->LDR(IndexType::Unsigned, host_reg, PPC_REG, u32(guest_reg.ppc_offset));
}
}
}

@@ -407,10 +430,9 @@ void Arm64FPRCache::Flush(FlushMode mode, PPCAnalyst::CodeOp* op)
{
const RegType reg_type = m_guest_registers[i].GetType();

if (reg_type != RegType::NotLoaded && reg_type != RegType::Immediate)
if (reg_type != RegType::NotLoaded && reg_type != RegType::Discarded &&
reg_type != RegType::Immediate)
{
// XXX: Determine if we can keep a register in the lower 64bits
// Which will allow it to be callee saved.
FlushRegister(i, mode == FlushMode::MaintainState);
}
}
@@ -497,6 +519,9 @@ ARM64Reg Arm64FPRCache::R(size_t preg, RegType type)
}
return host_reg;
}
case RegType::Discarded:
ASSERT_MSG(DYNA_REC, false, "Attempted to read discarded register");
break;
case RegType::NotLoaded: // Register isn't loaded at /all/
{
host_reg = GetReg();
@@ -536,7 +561,7 @@ ARM64Reg Arm64FPRCache::RW(size_t preg, RegType type)
reg.SetDirty(true);

// If not loaded at all, just alloc a new one.
if (reg.GetType() == RegType::NotLoaded)
if (reg.GetType() == RegType::NotLoaded || reg.GetType() == RegType::Discarded)
{
reg.Load(GetReg(), type);
return reg.GetReg();
@@ -637,8 +662,8 @@ void Arm64FPRCache::FlushByHost(ARM64Reg host_reg)
const OpArg& reg = m_guest_registers[i];
const RegType reg_type = reg.GetType();

if ((reg_type != RegType::NotLoaded && reg_type != RegType::Immediate) &&
reg.GetReg() == host_reg)
if (reg_type != RegType::NotLoaded && reg_type != RegType::Discarded &&
reg_type != RegType::Immediate && reg.GetReg() == host_reg)
{
FlushRegister(i, false);
return;
@@ -47,6 +47,7 @@ static_assert(PPCSTATE_OFF(xer_so_ov) < 4096, "STRB can't store xer_so_ov!");
enum class RegType
{
NotLoaded,
Discarded, // Reg is not loaded because we know it won't be read before the next write
Register, // Reg type is register
Immediate, // Reg is really a IMM
LowerPair, // Only the lower pair of a paired register
@@ -86,6 +87,15 @@ class OpArg

m_reg = Arm64Gen::ARM64Reg::INVALID_REG;
}
void Discard()
{
// Invalidate any previous information
m_type = RegType::Discarded;
m_reg = Arm64Gen::ARM64Reg::INVALID_REG;

// Arbitrarily large value that won't roll over on a lot of increments
m_last_used = 0xFFFF;
}
void Flush()
{
// Invalidate any previous information
@@ -143,6 +153,7 @@ class Arm64RegCache
void Init(Arm64Gen::ARM64XEmitter* emitter);

virtual void Start(PPCAnalyst::BlockRegStats& stats) {}
void DiscardRegisters(BitSet32 regs);
// Flushes the register cache in different ways depending on the mode
virtual void Flush(FlushMode mode, PPCAnalyst::CodeOp* op) = 0;

@@ -194,6 +205,7 @@ class Arm64RegCache
// Flushes a guest register by host provided
virtual void FlushByHost(Arm64Gen::ARM64Reg host_reg) = 0;

void DiscardRegister(size_t preg);
virtual void FlushRegister(size_t preg, bool maintain_state) = 0;

// Get available host registers
@@ -196,8 +196,8 @@ static bool CanSwapAdjacentOps(const CodeOp& a, const CodeOp& b)
{
const GekkoOPInfo* a_info = a.opinfo;
const GekkoOPInfo* b_info = b.opinfo;
int a_flags = a_info->flags;
int b_flags = b_info->flags;
u64 a_flags = a_info->flags;
u64 b_flags = b_info->flags;

// can't reorder around breakpoints
if (SConfig::GetInstance().bEnableDebugging &&
@@ -551,6 +551,10 @@ void PPCAnalyzer::SetInstructionStats(CodeBlock* block, CodeOp* code, const Gekk
code->outputFPRF = (opinfo->flags & FL_SET_FPRF) != 0;
code->canEndBlock = (opinfo->flags & FL_ENDBLOCK) != 0;

// TODO: Is it possible to determine that some FPU instructions never cause exceptions?
code->canCauseException =
(opinfo->flags & (FL_LOADSTORE | FL_USE_FPU | FL_PROGRAMEXCEPTION)) != 0;

code->wantsCA = (opinfo->flags & FL_READ_CA) != 0;
code->outputCA = (opinfo->flags & FL_SET_CA) != 0;

@@ -916,7 +920,7 @@ u32 PPCAnalyzer::Analyze(u32 address, CodeBlock* block, CodeBuffer* buffer, std:
// Scan for flag dependencies; assume the next block (or any branch that can leave the block)
// wants flags, to be safe.
bool wantsCR0 = true, wantsCR1 = true, wantsFPRF = true, wantsCA = true;
BitSet32 fprInUse, gprInUse, gprInReg, fprInXmm;
BitSet32 fprInUse, gprInUse, gprDiscardable, fprDiscardable, fprInXmm;
for (int i = block->m_num_instructions - 1; i >= 0; i--)
{
CodeOp& op = code[i];
@@ -939,21 +943,26 @@ u32 PPCAnalyzer::Analyze(u32 address, CodeBlock* block, CodeBuffer* buffer, std:
wantsCA &= !op.outputCA || opWantsCA;
op.gprInUse = gprInUse;
op.fprInUse = fprInUse;
op.gprInReg = gprInReg;
op.gprDiscardable = gprDiscardable;
op.fprDiscardable = fprDiscardable;
op.fprInXmm = fprInXmm;
// TODO: if there's no possible endblocks or exceptions in between, tell the regcache
// we can throw away a register if it's going to be overwritten later.
gprInUse |= op.regsIn;
gprInReg |= op.regsIn;
fprInUse |= op.fregsIn;
if (op.canEndBlock || op.canCauseException)
{
gprDiscardable = BitSet32{};
fprDiscardable = BitSet32{};
}
else
{
gprDiscardable |= op.regsOut;
gprDiscardable &= ~op.regsIn;
if (op.fregOut >= 0)
fprDiscardable[op.fregOut] = true;
fprDiscardable &= ~op.fregsIn;
}
if (strncmp(op.opinfo->opname, "stfd", 4))
fprInXmm |= op.fregsIn;
// For now, we need to count output registers as "used" though; otherwise the flush
// will result in a redundant store (e.g. store to regcache, then store again to
// the same location later).
gprInUse |= op.regsOut;
if (op.fregOut >= 0)
fprInUse[op.fregOut] = true;
}

// Forward scan, for flags that need the other direction for calculation.
@@ -45,13 +45,15 @@ struct CodeOp // 16B
bool outputFPRF;
bool outputCA;
bool canEndBlock;
bool canCauseException;
bool skipLRStack;
bool skip; // followed BL-s for example
// which registers are still needed after this instruction in this block
BitSet32 fprInUse;
BitSet32 gprInUse;
// just because a register is in use doesn't mean we actually need or want it in an x86 register.
BitSet32 gprInReg;
// which registers have values which are known to be unused after this instruction
BitSet32 gprDiscardable;
BitSet32 fprDiscardable;
// we do double stores from GPRs, so we don't want to load a PowerPC floating point register into
// an XMM only to move it again to a GPR afterwards.
BitSet32 fprInXmm;
@@ -7,63 +7,65 @@
#include <array>
#include <cstddef>

#include "Common/CommonTypes.h"
#include "Core/PowerPC/Gekko.h"
#include "Core/PowerPC/Interpreter/Interpreter.h"

// Flags that indicate what an instruction can do.
enum
enum InstructionFlags : u64
{
FL_SET_CR0 = (1 << 0), // Sets CR0.
FL_SET_CR1 = (1 << 1), // Sets CR1.
FL_SET_CRn = (1 << 2), // Encoding decides which CR can be set.
FL_SET_CR0 = (1ull << 0), // Sets CR0.
FL_SET_CR1 = (1ull << 1), // Sets CR1.
FL_SET_CRn = (1ull << 2), // Encoding decides which CR can be set.
FL_SET_CRx = FL_SET_CR0 | FL_SET_CR1 | FL_SET_CRn,
FL_SET_CA = (1 << 3), // Sets the carry flag.
FL_READ_CA = (1 << 4), // Reads the carry flag.
FL_RC_BIT = (1 << 5), // Sets the record bit.
FL_SET_CA = (1ull << 3), // Sets the carry flag.
FL_READ_CA = (1ull << 4), // Reads the carry flag.
FL_RC_BIT = (1ull << 5), // Sets the record bit.
FL_RC_BIT_F =
(1 << 6), // Sets the record bit. Used for floating point instructions that do this.
(1ull << 6), // Sets the record bit. Used for floating point instructions that do this.
FL_ENDBLOCK =
(1 << 7), // Specifies that the instruction can be used as an exit point for a JIT block.
FL_IN_A = (1 << 8), // Uses rA as an input.
FL_IN_A0 = (1 << 9), // Uses rA as an input. Indicates that if rA is zero, the value zero is
// used, not the contents of r0.
FL_IN_B = (1 << 10), // Uses rB as an input.
FL_IN_C = (1 << 11), // Uses rC as an input.
FL_IN_S = (1 << 12), // Uses rS as an input.
(1ull << 7), // Specifies that the instruction can be used as an exit point for a JIT block.
FL_IN_A = (1ull << 8), // Uses rA as an input.
FL_IN_A0 = (1ull << 9), // Uses rA as an input. Indicates that if rA is zero, the value zero is
// used, not the contents of r0.
FL_IN_B = (1ull << 10), // Uses rB as an input.
FL_IN_C = (1ull << 11), // Uses rC as an input.
FL_IN_S = (1ull << 12), // Uses rS as an input.
FL_IN_AB = FL_IN_A | FL_IN_B,
FL_IN_AC = FL_IN_A | FL_IN_C,
FL_IN_ABC = FL_IN_A | FL_IN_B | FL_IN_C,
FL_IN_SB = FL_IN_S | FL_IN_B,
FL_IN_A0B = FL_IN_A0 | FL_IN_B,
FL_IN_A0BC = FL_IN_A0 | FL_IN_B | FL_IN_C,
FL_OUT_D = (1 << 13), // rD is used as a destination.
FL_OUT_A = (1 << 14), // rA is used as a destination.
FL_OUT_D = (1ull << 13), // rD is used as a destination.
FL_OUT_A = (1ull << 14), // rA is used as a destination.
FL_OUT_AD = FL_OUT_A | FL_OUT_D,
FL_TIMER = (1 << 15), // Used only for mftb.
FL_CHECKEXCEPTIONS = (1 << 16), // Used with rfi/rfid.
FL_TIMER = (1ull << 15), // Used only for mftb.
FL_CHECKEXCEPTIONS = (1ull << 16), // Used with rfi/rfid.
FL_EVIL =
(1 << 17), // Historically used to refer to instructions that messed up Super Monkey Ball.
FL_USE_FPU = (1 << 18), // Used to indicate a floating point instruction.
FL_LOADSTORE = (1 << 19), // Used to indicate a load/store instruction.
FL_SET_FPRF = (1 << 20), // Sets bits in the FPRF.
FL_READ_FPRF = (1 << 21), // Reads bits from the FPRF.
FL_SET_OE = (1 << 22), // Sets the overflow flag.
FL_IN_FLOAT_A = (1 << 23), // frA is used as an input.
FL_IN_FLOAT_B = (1 << 24), // frB is used as an input.
FL_IN_FLOAT_C = (1 << 25), // frC is used as an input.
FL_IN_FLOAT_S = (1 << 26), // frS is used as an input.
FL_IN_FLOAT_D = (1 << 27), // frD is used as an input.
(1ull << 17), // Historically used to refer to instructions that messed up Super Monkey Ball.
FL_USE_FPU = (1ull << 18), // Used to indicate a floating point instruction.
FL_LOADSTORE = (1ull << 19), // Used to indicate a load/store instruction.
FL_SET_FPRF = (1ull << 20), // Sets bits in the FPRF.
FL_READ_FPRF = (1ull << 21), // Reads bits from the FPRF.
FL_SET_OE = (1ull << 22), // Sets the overflow flag.
FL_IN_FLOAT_A = (1ull << 23), // frA is used as an input.
FL_IN_FLOAT_B = (1ull << 24), // frB is used as an input.
FL_IN_FLOAT_C = (1ull << 25), // frC is used as an input.
FL_IN_FLOAT_S = (1ull << 26), // frS is used as an input.
FL_IN_FLOAT_D = (1ull << 27), // frD is used as an input.
FL_IN_FLOAT_AB = FL_IN_FLOAT_A | FL_IN_FLOAT_B,
FL_IN_FLOAT_AC = FL_IN_FLOAT_A | FL_IN_FLOAT_C,
FL_IN_FLOAT_ABC = FL_IN_FLOAT_A | FL_IN_FLOAT_B | FL_IN_FLOAT_C,
FL_OUT_FLOAT_D = (1 << 28), // frD is used as a destination.
FL_OUT_FLOAT_D = (1ull << 28), // frD is used as a destination.
// Used in the case of double ops (they don't modify the top half of the output)
FL_INOUT_FLOAT_D = FL_IN_FLOAT_D | FL_OUT_FLOAT_D,
FL_IN_FLOAT_A_BITEXACT = (1 << 29), // The output is based on the exact bits in frA.
FL_IN_FLOAT_B_BITEXACT = (1 << 30), // The output is based on the exact bits in frB.
FL_IN_FLOAT_C_BITEXACT = (1 << 31), // The output is based on the exact bits in frC.
FL_IN_FLOAT_A_BITEXACT = (1ull << 29), // The output is based on the exact bits in frA.
FL_IN_FLOAT_B_BITEXACT = (1ull << 30), // The output is based on the exact bits in frB.
FL_IN_FLOAT_C_BITEXACT = (1ull << 31), // The output is based on the exact bits in frC.
FL_IN_FLOAT_AB_BITEXACT = FL_IN_FLOAT_A_BITEXACT | FL_IN_FLOAT_B_BITEXACT,
FL_IN_FLOAT_BC_BITEXACT = FL_IN_FLOAT_B_BITEXACT | FL_IN_FLOAT_C_BITEXACT,
FL_PROGRAMEXCEPTION = (1ull << 32), // May generate a system exception.
};

enum class OpType
@@ -94,7 +96,7 @@ struct GekkoOPInfo
{
const char* opname;
OpType type;
int flags;
u64 flags;
int numCycles;
u64 runCount;
int compileCount;