// SPDX-License-Identifier: MIT #include "FEXCore/Utils/LogManager.h" #include "Interface/Core/Interpreter/Fallbacks/FallbackOpHandler.h" #include "Interface/IR/IR.h" #include "Interface/IR/IREmitter.h" #include "Interface/IR/Passes.h" #include "Interface/IR/PassManager.h" #include "FEXCore/IR/IR.h" #include "FEXCore/Utils/Profiler.h" #include "FEXCore/Core/HostFeatures.h" #include "Interface/Core/Addressing.h" #include #include #include #include // This file adds a pass to process X87 stack instructions. // These instructions are marked in IR.json with `X87: true` and are generated // by X87 guest instructions. // The way is works is that there's a virtual stack `StackData`, where we load and store // and apply the operations in a block of code. Once the block finishes, we emit the necessary operations // that we recorded onto the virtual stack. This allows us to save a lot of code movement // to and from stack registers, top management and valid flags. It also allows us to // perform memcpy optimizations like the one performed in STORESTACKMEM. // // By default we run on the fast path - i.e. we assume all values are in the stack and we have a complete // stack overview. However, if we encounter a value that's not in the virtual stack - maybe it was added // to the stack in a previous block, we move onto the slow path which loads and stores values to the stack // registers. // Once in a slow path, we won't return to the fast pass until the beginning of the following block. namespace FEXCore::IR { // FIXME(pmatos): copy from OpcodeDispatcher.h static uint32_t MMBaseOffset() { return static_cast(offsetof(Core::CPUState, mm[0][0])); } // Similar helper to the one in OpcodeDispatcher.h except we do not // need to handle flags, etc. template static void DeriveOp(Ref& RefV, IROps NewOp, IREmitter::IRPair Expr) { Expr.first->Header.Op = NewOp; RefV = Expr; } enum class StackSlot { UNUSED, INVALID, VALID }; // FixedSizeStack is a model of the x87 Stack where each element in this // fixed size stack lives at an offset from top. The top of the stack is at // index 0. template class FixedSizeStack { public: struct StackSlotEntry final { StackSlot Type = StackSlot::UNUSED; T Value = T::Invalid; }; static constexpr uint8_t size = 8; // Real top as an offset from stored top value (or the one at the beginning of the block) // For example, if we start and push a value to our simulated stack, because we don't // update top straight away the TopOffset is 1. // If SlowPath is true, then TopOffset is always zero. int8_t TopOffset = 0; FixedSizeStack() = default; void push(const T& Value) { rotate(); buffer.front() = {StackSlot::VALID, Value}; } // Rotate the elements with the direction controlled by Right void rotate(bool Right = true) { if (Right) { std::rotate(buffer.begin(), buffer.end() - 1, buffer.end()); TopOffset++; } else { std::rotate(buffer.begin(), buffer.begin() + 1, buffer.end()); TopOffset--; } } void pop() { buffer.front() = {StackSlot::INVALID, T::Invalid}; rotate(false); } const StackSlotEntry& top(size_t Offset = 0) const { return buffer[Offset]; } void setTop(const T& Value, size_t Offset = 0) { buffer[Offset] = {StackSlot::VALID, Value}; } bool isValid(size_t Offset) const { return buffer[Offset].Type == StackSlot::VALID; } void clear() { buffer.fill({StackSlot::UNUSED, T::Invalid}); TopOffset = 0; } void dump() const { LogMan::Msg::DFmt("-- Stack"); for (size_t i = 0; i < buffer.size(); i++) { const auto& [Valid, Element] = buffer[i]; if (Valid == StackSlot::VALID) { LogMan::Msg::DFmt("| ST{}: 0x{:x}", i, (uintptr_t)(Element.StackDataNode)); } else if (Valid == StackSlot::INVALID) { LogMan::Msg::DFmt("| ST{}: INVALID", i); } } LogMan::Msg::DFmt("--"); } void setTagInvalid(size_t Index) { buffer[Index].Type = StackSlot::INVALID; } // Returns a mask to set in AbridgedTagWord uint8_t getValidMask() const { uint8_t Mask = 0; for (size_t i = 0; i < buffer.size(); i++) { if (buffer[i].Type == StackSlot::VALID) { Mask |= 1U << i; } } return Mask; } // Returns a mask to set in AbridgedTagWord uint8_t getInvalidMask() const { uint8_t Mask = 0; for (size_t i = 0; i < buffer.size(); i++) { if (buffer[i].Type == StackSlot::INVALID) { Mask |= 1U << i; } } return Mask; } private: std::array buffer {}; }; class X87StackOptimization final : public Pass { public: X87StackOptimization(const FEXCore::HostFeatures& Features, OpSize GPROpSize) : Features(Features) , GPROpSize(GPROpSize) { FEX_CONFIG_OPT(ReducedPrecision, X87REDUCEDPRECISION); ReducedPrecisionMode = ReducedPrecision; } void Run(IREmitter* Emit) override; private: const FEXCore::HostFeatures& Features; const OpSize GPROpSize; bool ReducedPrecisionMode; FEX_CONFIG_OPT(DisableVixlIndirectCalls, DISABLE_VIXL_INDIRECT_RUNTIME_CALLS); // Helpers Ref RotateRight8(uint32_t V, Ref Amount); void F80SplitStore_Helper(const IROp_StoreStackMem* Op, Ref StackNode, Ref AddrNode, Ref Offset, OpSize Align, MemOffsetType OffsetType, uint8_t OffsetScale) { IREmit->_StoreMemFPR(OpSize::i64Bit, StackNode, AddrNode, Offset, Align, OffsetType, OffsetScale); auto Upper = IREmit->_VExtractToGPR(OpSize::i128Bit, OpSize::i64Bit, StackNode, 1); // Store the Upper part of the register (the remaining 2 bytes) into memory. AddressMode A {.Base = AddrNode, .Index = Op->Offset.IsInvalid() ? nullptr : Offset, .Offset = 8, .IndexType = MemOffsetType::SXTX, .IndexScale = OffsetScale, .AddrSize = OpSize::i64Bit}; A = SelectAddressMode(IREmit, A, GPROpSize, Features.SupportsTSOImm9, false, false, OpSize::i16Bit); IREmit->_StoreMemGPR(OpSize::i16Bit, Upper, A.Base, A.Index, OpSize::i64Bit, MemOffsetType::SXTX, A.IndexScale); } void Store80BitToMem(const IROp_StoreStackMem* Op, Ref StackNode, Ref AddrNode, Ref Offset, OpSize Align, MemOffsetType OffsetType, uint8_t OffsetScale) { if (Features.SupportsSVE()) { AddressMode A {.Base = AddrNode, .Index = Op->Offset.IsInvalid() ? nullptr : Offset, .IndexType = MemOffsetType::SXTX, .IndexScale = OffsetScale, .AddrSize = OpSize::i64Bit}; AddrNode = LoadEffectiveAddress(IREmit, A, GPROpSize, false); IREmit->_StoreMemX87SVEOptPredicate(OpSize::i128Bit, OpSize::i16Bit, StackNode, AddrNode); } else { F80SplitStore_Helper(Op, StackNode, AddrNode, Offset, Align, OffsetType, OffsetScale); } } void StoreStackMem_Helper(const IRListView& IR, const IROp_StoreStackMem* Op, Ref StackNode) { LOGMAN_THROW_A_FMT(!ReducedPrecisionMode, "Full precision mode expected."); Ref AddrNode = IR.GetNode(Op->Addr); Ref Offset = IR.GetNode(Op->Offset); OpSize Align = Op->Align; MemOffsetType OffsetType = Op->OffsetType; uint8_t OffsetScale = Op->OffsetScale; // Normal Precision Mode switch (Op->StoreSize) { case OpSize::i32Bit: case OpSize::i64Bit: { StackNode = IREmit->_F80CVT(Op->StoreSize, StackNode); IREmit->_StoreMemFPR(Op->StoreSize, StackNode, AddrNode, Offset, Align, OffsetType, OffsetScale); break; } case OpSize::f80Bit: { Store80BitToMem(Op, StackNode, AddrNode, Offset, Align, OffsetType, OffsetScale); break; } default: ERROR_AND_DIE_FMT("Unsupported x87 size"); } } // Performs a store to memory from a value the stack passed in as StackNode. // This is the version dealing with the reduced precision case. void StoreStackMem_Reduced_Helper(const IRListView& IR, const IROp_StoreStackMem* Op, Ref StackNode) { LOGMAN_THROW_A_FMT(ReducedPrecisionMode, "Reduced precision mode expected."); Ref AddrNode = IR.GetNode(Op->Addr); Ref Offset = IR.GetNode(Op->Offset); OpSize Align = Op->Align; MemOffsetType OffsetType = Op->OffsetType; uint8_t OffsetScale = Op->OffsetScale; switch (Op->StoreSize) { case OpSize::i32Bit: { StackNode = IREmit->_Float_FToF(OpSize::i32Bit, OpSize::i64Bit, StackNode); [[fallthrough]]; } case OpSize::i64Bit: { IREmit->_StoreMemFPR(Op->StoreSize, StackNode, AddrNode, Offset, Align, OffsetType, OffsetScale); break; } case OpSize::f80Bit: { StackNode = IREmit->_F80CVTTo(StackNode, OpSize::i64Bit); Store80BitToMem(Op, StackNode, AddrNode, Offset, Align, OffsetType, OffsetScale); break; } default: ERROR_AND_DIE_FMT("Unsupported x87 size"); } } // Handles a Unary operation. // Takes the op we are handling, the Node for the reduced precision case and the node for the normal case. // Depending on the type of Op64, we might need to pass a couple of extra constant arguments, this happens // when VFOp64 is true. void HandleUnop(IROps Op64, bool VFOp64, IROps Op80); void HandleBinopValue(IROps Op64, bool VFOp64, IROps Op80, uint8_t DestStackOffset, bool MarkDestValid, uint8_t StackOffset, Ref ValueNode, bool Reverse = false); void HandleBinopStack(IROps Op64, bool VFOp64, IROps Op80, uint8_t DestStackOffset, uint8_t StackOffset1, uint8_t StackOffset2, bool Reverse = false); // Top Management Helpers /// Set the valid tag for Value as valid (if Valid is true), or invalid (if Valid is false). void SetX87ValidTag(uint8_t Offset, bool Valid); // Generates slow code to load/store a value from an offset from the top of the stack Ref LoadStackValueAtOffset_Slow(uint8_t Offset = 0); void StoreStackValueAtOffset_Slow(Ref Value, uint8_t Offset = 0, bool SetValid = true); // Update Top value in slow path for a pop void UpdateTopForPop_Slow(bool InvalidateTag = true); void UpdateTopForPush_Slow(); // Synchronizes the current simulated stack with the actual values. // Returns a new value for Top, that's synchronized between the simulated stack // and the actual FPU stack. Ref SynchronizeStackValues(); // Moves us from the fast to the slow path if ShouldMigrate is true. void MigrateToSlowPathIf(bool ShouldMigrate); // Top Cache Management Ref GetTopWithCache_Slow(); Ref GetOffsetTopWithCache_Slow(uint8_t Offset, bool Reverse = false); Ref GetOffsetTopAddressWithCache_Slow(uint8_t Offset); void SetTopWithCache_Slow(Ref Value); Ref GetX87ValidTag_Slow(uint8_t Offset); // Resets fields to initial values void Reset(); struct StackMemberInfo { constexpr StackMemberInfo() = default; constexpr StackMemberInfo(Ref Data) : StackDataNode(Data) {} constexpr StackMemberInfo(Ref Data, Ref Source, OpSize Size) : StackDataNode(Data) , Source({Size, Source}) {} Ref StackDataNode {}; // Reference to the data in the Stack. // This is the source data node in the stack format, possibly converted to 64/80 bits. struct StackMemberData final { OpSize Size; Ref Node; }; static const StackMemberInfo Invalid; // Tuple is only valid if we have information about the Source of the Stack Data Node. // In it's valid then OpSize is the original source size and Ref is the original source node. std::optional Source {}; }; // StackData, TopCache need to be always properly set to ensure // they reflect the current state of the FPU. This sync only makes sense while // taking the fast path. Once in the slow path, these don't make sense anymore // and we are syncing everything. // Index on vector is offset to top value at start of block // If slow path is true, then StackData is always empty. FixedSizeStack StackData; void InvalidateCaches(); void InvalidateCachedRegs(); // Path Migration helper management std::optional MigrateToSlowPath_IfInvalid(uint8_t Offset = 0); Ref LoadStackValue(uint8_t Offset = 0); void StoreStackValue(Ref Value, uint8_t Offset = 0, bool SetValid = false); void StackPop(); // Cache for Constants // ConstantPoll[i] has IREmit->_Constant(i); std::array ConstantPool {}; Ref GetConstant(ssize_t Offset); // Cached value for Top // If slowpath is false, then TopCache is nullptr. bool FlushTopPending = false; std::array FlushValuesPending {}; bool FlushValidPending = false; void FlushCachedRegs(); Ref GetFTW(); Ref FTWCached {}; std::array TopOffsetCache {}; std::array TopOffsetAddressCache {}; std::array TopValueCache {}; std::array TopValidCache {}; // Are we on the slow path? // Once we enter the slow path, we never come out. // This just simplifies the code atm. If there's a need to return to the fast path in the future // we can implement that but I would expect that there would be very few cases where that's necessary. // On the slow path TopCache is always the last obtained version of top. // TopOffset is ignored bool SlowPath = false; // Keeping IREmitter not to pass arguments around IREmitter* IREmit = nullptr; }; inline const X87StackOptimization::StackMemberInfo X87StackOptimization::StackMemberInfo::Invalid {nullptr}; inline void X87StackOptimization::InvalidateCaches() { InvalidateCachedRegs(); ConstantPool.fill(nullptr); } inline void X87StackOptimization::InvalidateCachedRegs() { FlushCachedRegs(); FTWCached = {}; TopOffsetCache.fill(nullptr); TopOffsetAddressCache.fill(nullptr); TopValueCache.fill(nullptr); TopValidCache.fill(StackSlot::UNUSED); } inline void X87StackOptimization::Reset() { SlowPath = false; StackData.clear(); InvalidateCaches(); } inline Ref X87StackOptimization::GetConstant(ssize_t Offset) { if (Offset < 0 || Offset >= X87StackOptimization::ConstantPool.size()) { // not dealt by pool return IREmit->_Constant(Offset); } if (ConstantPool[Offset] == nullptr) { ConstantPool[Offset] = IREmit->_Constant(Offset); } return ConstantPool[Offset]; } inline void X87StackOptimization::MigrateToSlowPathIf(bool ShouldMigrate) { if (ShouldMigrate && !SlowPath) { SynchronizeStackValues(); StackData.clear(); SlowPath = true; } } inline Ref X87StackOptimization::GetTopWithCache_Slow() { if (!TopOffsetCache[0]) { TopOffsetCache[0] = IREmit->_LoadContextGPR(OpSize::i8Bit, offsetof(FEXCore::Core::CPUState, flags) + FEXCore::X86State::X87FLAG_TOP_LOC); } return TopOffsetCache[0]; } inline Ref X87StackOptimization::GetOffsetTopWithCache_Slow(uint8_t Offset, bool Reverse) { if (Reverse) { Offset = 8 - Offset; } Offset &= 7; if (TopOffsetCache[Offset]) { return TopOffsetCache[Offset]; } auto* OffsetTop = GetTopWithCache_Slow(); if (Offset != 0) { OffsetTop = IREmit->_And(OpSize::i32Bit, IREmit->Add(OpSize::i32Bit, OffsetTop, Offset), GetConstant(7)); // GetTopWithCache_Slow already sets the cache so we don't need to set it here for offset == 0 TopOffsetCache[Offset] = OffsetTop; } return OffsetTop; } inline Ref X87StackOptimization::GetOffsetTopAddressWithCache_Slow(uint8_t Offset) { if (TopOffsetAddressCache[Offset]) { return TopOffsetAddressCache[Offset]; } Ref OffsetRef = GetOffsetTopWithCache_Slow(Offset); TopOffsetAddressCache[Offset] = IREmit->_FormContextAddress(OpSize::i64Bit, OffsetRef, 16); return TopOffsetAddressCache[Offset]; } inline void X87StackOptimization::SetTopWithCache_Slow(Ref Value) { InvalidateCachedRegs(); TopOffsetCache[0] = Value; FlushTopPending = true; } inline Ref X87StackOptimization::GetFTW() { if (!FTWCached) { FTWCached = IREmit->_LoadContextGPR(OpSize::i8Bit, offsetof(FEXCore::Core::CPUState, AbridgedFTW)); } return FTWCached; } inline void X87StackOptimization::SetX87ValidTag(uint8_t Offset, bool Valid) { TopValidCache[Offset] = Valid ? StackSlot::VALID : StackSlot::INVALID; FlushValidPending = true; } inline Ref X87StackOptimization::GetX87ValidTag_Slow(uint8_t Offset) { switch (TopValidCache[Offset]) { case StackSlot::UNUSED: return IREmit->_And(OpSize::i32Bit, IREmit->_Lshr(OpSize::i32Bit, GetFTW(), GetOffsetTopWithCache_Slow(Offset)), GetConstant(1)); case StackSlot::INVALID: return GetConstant(0); case StackSlot::VALID: return GetConstant(1); } } inline Ref X87StackOptimization::LoadStackValueAtOffset_Slow(uint8_t Offset) { OrderedNode* TopOffsetAddress = GetOffsetTopAddressWithCache_Slow(Offset); auto Size = ReducedPrecisionMode ? OpSize::i64Bit : OpSize::i128Bit; if (!TopValueCache[Offset]) { TopValueCache[Offset] = IREmit->_LoadMemFPR(Size, TopOffsetAddress, IREmit->_InlineConstant(MMBaseOffset()), Size, MemOffsetType::SXTX, 1); } return TopValueCache[Offset]; } inline void X87StackOptimization::StoreStackValueAtOffset_Slow(Ref Value, uint8_t Offset, bool SetValid) { TopValueCache[Offset] = Value; FlushValuesPending[Offset] = true; // mark it valid // In some cases we might already know it has been previously set as valid so we don't need to do it again if (SetValid) { SetX87ValidTag(Offset, true); } } inline Ref X87StackOptimization::RotateRight8(uint32_t V, Ref Amount) { return IREmit->_Lshr(OpSize::i32Bit, GetConstant(V | (V << 8)), Amount); } inline std::optional X87StackOptimization::MigrateToSlowPath_IfInvalid(uint8_t Offset) { const auto& [Valid, StackMember] = StackData.top(Offset); MigrateToSlowPathIf(Valid != StackSlot::VALID); if (Valid == StackSlot::VALID) { return StackMember; } return {}; } inline Ref X87StackOptimization::LoadStackValue(uint8_t Offset) { const auto& StackValue = MigrateToSlowPath_IfInvalid(Offset); return SlowPath ? LoadStackValueAtOffset_Slow(Offset) : StackValue->StackDataNode; } inline void X87StackOptimization::StoreStackValue(Ref Value, uint8_t Offset, bool SetValid) { if (SlowPath) { StoreStackValueAtOffset_Slow(Value, Offset, SetValid); } else { StackData.setTop(StackMemberInfo {Value}, Offset); } } inline void X87StackOptimization::StackPop() { if (SlowPath) { UpdateTopForPop_Slow(); } else { StackData.pop(); } } void X87StackOptimization::HandleUnop(IROps Op64, bool VFOp64, IROps Op80) { Ref St0 = LoadStackValue(); Ref Value {}; if (ReducedPrecisionMode) { if (VFOp64) { DeriveOp(Value, Op64, IREmit->_VFSqrt(OpSize::i64Bit, OpSize::i64Bit, St0)); } else { DeriveOp(Value, Op64, IREmit->_F64SIN(St0)); } } else { DeriveOp(Value, Op80, IREmit->_F80SQRT(St0)); } StoreStackValue(Value); } void X87StackOptimization::HandleBinopValue(IROps Op64, bool VFOp64, IROps Op80, uint8_t DestStackOffset, bool MarkDestValid, uint8_t StackOffset, Ref ValueNode, bool Reverse) { LOGMAN_THROW_A_FMT(!Reverse || VFOp64, "There are no reverse operations using non VFOp64 ops"); auto StackNode = LoadStackValue(StackOffset); Ref Node = {}; if (ReducedPrecisionMode) { if (Reverse) { DeriveOp(Node, Op64, IREmit->_VFAdd(OpSize::i64Bit, OpSize::i64Bit, ValueNode, StackNode)); } else { if (VFOp64) { DeriveOp(Node, Op64, IREmit->_VFAdd(OpSize::i64Bit, OpSize::i64Bit, StackNode, ValueNode)); } else { DeriveOp(Node, Op64, IREmit->_F64FPREM(StackNode, ValueNode)); } } } else { if (Reverse) { DeriveOp(Node, Op80, IREmit->_F80Add(ValueNode, StackNode)); } else { DeriveOp(Node, Op80, IREmit->_F80Add(StackNode, ValueNode)); } } StoreStackValue(Node, DestStackOffset, MarkDestValid && StackOffset != DestStackOffset); } void X87StackOptimization::HandleBinopStack(IROps Op64, bool VFOp64, IROps Op80, uint8_t DestStackOffset, uint8_t StackOffset1, uint8_t StackOffset2, bool Reverse) { auto StackNode = LoadStackValue(StackOffset2); HandleBinopValue(Op64, VFOp64, Op80, DestStackOffset, StackOffset2 != DestStackOffset, StackOffset1, StackNode, Reverse); } inline void X87StackOptimization::UpdateTopForPop_Slow(bool InvalidateTag) { const auto PopContainer = [](auto& container) { const auto begin = std::begin(container); std::rotate(begin, std::next(begin), std::end(container)); }; if (InvalidateTag) { SetX87ValidTag(0, false); } // Pop the top of the x87 stack GetOffsetTopWithCache_Slow(1); PopContainer(TopOffsetCache); PopContainer(TopOffsetAddressCache); PopContainer(TopValueCache); PopContainer(FlushValuesPending); PopContainer(TopValidCache); FlushTopPending = true; } inline void X87StackOptimization::UpdateTopForPush_Slow() { const auto PushContainer = [](auto& container) { const auto end = std::end(container); std::rotate(std::begin(container), std::prev(end), end); }; // Push the top of the x87 stack GetOffsetTopWithCache_Slow(1, true); PushContainer(TopOffsetCache); PushContainer(TopOffsetAddressCache); PushContainer(TopValueCache); PushContainer(FlushValuesPending); PushContainer(TopValidCache); FlushTopPending = true; } void X87StackOptimization::FlushCachedRegs() { if (FlushTopPending) { IREmit->_StoreContextGPR(OpSize::i8Bit, TopOffsetCache[0], offsetof(FEXCore::Core::CPUState, flags) + FEXCore::X86State::X87FLAG_TOP_LOC); FlushTopPending = false; } auto Size = ReducedPrecisionMode ? OpSize::i64Bit : OpSize::i128Bit; for (size_t i = 0; i < FlushValuesPending.size(); i++) { if (FlushValuesPending[i]) { OrderedNode* TopOffsetAddress = GetOffsetTopAddressWithCache_Slow(i); IREmit->_StoreMemFPR(Size, TopValueCache[i], TopOffsetAddress, IREmit->_InlineConstant(MMBaseOffset()), Size, MemOffsetType::SXTX, 1); // store FlushValuesPending[i] = false; } } if (FlushValidPending) { uint8_t ValidMask = 0; uint8_t InvalidMask = 0; for (auto It = TopValidCache.rbegin(); It != TopValidCache.rend(); It++) { ValidMask <<= 1; InvalidMask <<= 1; if (*It == StackSlot::VALID) { ValidMask |= 1; } else if (*It == StackSlot::INVALID) { InvalidMask |= 1; } } if (ValidMask || InvalidMask) { Ref NewFTW = [&]() { if (ValidMask == 0xff || InvalidMask == 0xff) { // If InvalidMask == 0xff then ValidMask = 0 return GetConstant(ValidMask); } else { Ref NewFTW = GetFTW(); Ref RotAmount {}; if (std::popcount(ValidMask) == 1) { uint8_t BitIdx = std::countr_zero(ValidMask); Ref RegMask = IREmit->_Lshl(OpSize::i32Bit, GetConstant(1), GetOffsetTopWithCache_Slow(BitIdx)); NewFTW = IREmit->_Or(OpSize::i32Bit, NewFTW, RegMask); } else if (ValidMask) { RotAmount = IREmit->_Sub(OpSize::i32Bit, GetConstant(8), GetTopWithCache_Slow()); // perform a rotate right on mask by top NewFTW = IREmit->_Or(OpSize::i32Bit, NewFTW, RotateRight8(ValidMask, RotAmount)); } if (std::popcount(InvalidMask) == 1) { uint8_t BitIdx = std::countr_zero(InvalidMask); Ref RegMask = IREmit->_Lshl(OpSize::i32Bit, GetConstant(1), GetOffsetTopWithCache_Slow(BitIdx)); NewFTW = IREmit->_Andn(OpSize::i32Bit, NewFTW, RegMask); } else if (InvalidMask) { if (!RotAmount) { RotAmount = IREmit->_Sub(OpSize::i32Bit, GetConstant(8), GetTopWithCache_Slow()); } NewFTW = IREmit->_Andn(OpSize::i32Bit, NewFTW, RotateRight8(InvalidMask, RotAmount)); } return NewFTW; } }(); IREmit->_StoreContextGPR(OpSize::i8Bit, NewFTW, offsetof(FEXCore::Core::CPUState, AbridgedFTW)); FTWCached = NewFTW; } FlushValidPending = false; } } // We synchronize stack values in a few occasions but one of the most important of those, // is when we move from fast to a slow path and need to make sure that the context is properly // written. Ref X87StackOptimization::SynchronizeStackValues() { if (SlowPath) { return GetTopWithCache_Slow(); } // Store new top which is now the original top minus recorded top offset // Careful with underflow wraparound. const auto TopOffset = StackData.TopOffset; if (TopOffset != 0) { Ref NewTop = GetOffsetTopWithCache_Slow(TopOffset, true); SetTopWithCache_Slow(NewTop); } StackData.TopOffset = 0; // Before leaving we need to write the current values in the stack to // context so that the values are correct. Copy SourceDataNode in the // stack to the respective mmX register. Ref TopValue = GetTopWithCache_Slow(); for (size_t i = 0; i < StackData.size; ++i) { const auto& [Valid, StackMember] = StackData.top(i); if (Valid == StackSlot::VALID) { StoreStackValueAtOffset_Slow(StackMember.StackDataNode, i, false); } } { // Set valid tags uint8_t ValidMask = StackData.getValidMask(); uint8_t InvalidMask = StackData.getInvalidMask(); for (auto& Elem : TopValidCache) { Elem = (ValidMask & 1) ? StackSlot::VALID : ((InvalidMask & 1) ? StackSlot::INVALID : StackSlot::UNUSED); ValidMask >>= 1; InvalidMask >>= 1; } FlushValidPending = true; } return TopValue; } void X87StackOptimization::Run(IREmitter* Emit) { FEXCORE_PROFILE_SCOPED("PassManager::x87StackOpt"); auto CurrentIR = Emit->ViewIR(); auto* HeaderOp = CurrentIR.GetHeader(); LOGMAN_THROW_A_FMT(HeaderOp->Header.Op == OP_IRHEADER, "First op wasn't IRHeader"); if (!HeaderOp->HasX87) { // If there is no x87 in this, just early exit. return; } // Initialize IREmit member IREmit = Emit; // Run optimization proper for (auto [BlockNode, BlockHeader] : CurrentIR.GetBlocks()) { auto BlockIROp = BlockHeader->CW(); // Each time we deal with a new block we need to start over. // The optimization should run per-block Reset(); IREmit->SetCurrentCodeBlock(BlockNode); for (auto [CodeNode, IROp] : CurrentIR.GetCode(BlockNode)) { if (!LoweredX87(IROp->Op)) { continue; } IREmit->SetWriteCursor(CodeNode); switch (IROp->Op) { case OP_F80ADDSTACK: { const auto* Op = IROp->C(); HandleBinopStack(OP_VFADD, true, OP_F80ADD, Op->SrcStack1, Op->SrcStack1, Op->SrcStack2); break; } case OP_F80SUBSTACK: { const auto* Op = IROp->C(); HandleBinopStack(OP_VFSUB, true, OP_F80SUB, Op->DstStack, Op->SrcStack1, Op->SrcStack2); break; } case OP_F80MULSTACK: { const auto* Op = IROp->C(); HandleBinopStack(OP_VFMUL, true, OP_F80MUL, Op->SrcStack1, Op->SrcStack1, Op->SrcStack2); break; } case OP_F80DIVSTACK: { const auto* Op = IROp->C(); HandleBinopStack(OP_VFDIV, true, OP_F80DIV, Op->DstStack, Op->SrcStack1, Op->SrcStack2); break; } case OP_F80FPREMSTACK: { HandleBinopStack(OP_F64FPREM, false, OP_F80FPREM, 0, 0, 1); break; } case OP_F80FPREM1STACK: { HandleBinopStack(OP_F64FPREM1, false, OP_F80FPREM1, 0, 0, 1); break; } case OP_F80SCALESTACK: { HandleBinopStack(OP_F64SCALE, false, OP_F80SCALE, 0, 0, 1); break; } case OP_F80FYL2XSTACK: { HandleBinopStack(OP_F64FYL2X, false, OP_F80FYL2X, 1, 0, 1); StackPop(); break; } case OP_F80FYL2XP1STACK: { HandleBinopStack(OP_F64FYL2XP1, false, OP_F80FYL2XP1, 1, 0, 1); StackPop(); break; } case OP_F80ATANSTACK: { HandleBinopStack(OP_F64ATAN, false, OP_F80ATAN, 1, 1, 0); StackPop(); break; } case OP_F80ADDVALUE: { const auto* Op = IROp->C(); HandleBinopValue(OP_VFADD, true, OP_F80ADD, 0, true, Op->SrcStack, CurrentIR.GetNode(Op->X80Src)); break; } case OP_F80SUBRVALUE: case OP_F80SUBVALUE: { const auto* Op = IROp->C(); HandleBinopValue(OP_VFSUB, true, OP_F80SUB, 0, true, Op->SrcStack, CurrentIR.GetNode(Op->X80Src), IROp->Op == OP_F80SUBRVALUE); break; } case OP_F80DIVRVALUE: case OP_F80DIVVALUE: { const auto* Op = IROp->C(); HandleBinopValue(OP_VFDIV, true, OP_F80DIV, 0, true, Op->SrcStack, CurrentIR.GetNode(Op->X80Src), IROp->Op == OP_F80DIVRVALUE); break; } case OP_F80MULVALUE: { const auto* Op = IROp->C(); HandleBinopValue(OP_VFMUL, true, OP_F80MUL, 0, true, Op->SrcStack, CurrentIR.GetNode(Op->X80Src)); break; } case OP_F80SQRTSTACK: { HandleUnop(OP_VFSQRT, true, OP_F80SQRT); break; } case OP_F80SINSTACK: { HandleUnop(OP_F64SIN, false, OP_F80SIN); break; } case OP_F80COSSTACK: { HandleUnop(OP_F64COS, false, OP_F80COS); break; } case OP_F80F2XM1STACK: { HandleUnop(OP_F64F2XM1, false, OP_F80F2XM1); break; } case OP_F80PTANSTACK: { HandleUnop(OP_F64TAN, false, OP_F80TAN); Ref OneConst {}; if (ReducedPrecisionMode) { OneConst = IREmit->_VCastFromGPR(OpSize::i64Bit, OpSize::i64Bit, GetConstant(0x3FF0000000000000)); } else { OneConst = IREmit->_LoadNamedVectorConstant(OpSize::i128Bit, NamedVectorConstant::NAMED_VECTOR_X87_ONE); } if (SlowPath) { UpdateTopForPush_Slow(); StoreStackValueAtOffset_Slow(OneConst); } else { StackData.push(StackMemberInfo {OneConst}); } break; } case OP_F80SINCOSSTACK: { Ref St0 = LoadStackValue(); Ref SinValue {}; Ref CosValue {}; if (ReducedPrecisionMode) { SinValue = IREmit->_F64SIN(St0); CosValue = IREmit->_F64COS(St0); } #ifdef VIXL_SIMULATOR else if (DisableVixlIndirectCalls() == 0) { SinValue = IREmit->_F80SIN(St0); CosValue = IREmit->_F80COS(St0); } #endif else { SinValue = IREmit->_AllocateFPR(OpSize::i128Bit, OpSize::i128Bit); CosValue = IREmit->_AllocateFPR(OpSize::i128Bit, OpSize::i128Bit); IREmit->_F80SINCOS(St0, SinValue, CosValue); } // Push values if (SlowPath) { StoreStackValueAtOffset_Slow(SinValue, 0, false); UpdateTopForPush_Slow(); StoreStackValueAtOffset_Slow(CosValue, 0, true); } else { StackData.setTop(StackMemberInfo {SinValue}); StackData.push(StackMemberInfo {CosValue}); } break; } case OP_INITSTACK: { StackData.clear(); InvalidateCachedRegs(); break; } case OP_INVALIDATESTACK: { const auto* Op = IROp->C(); auto Offset = Op->StackLocation; if (Offset != 0xff) { // invalidate single offset if (SlowPath) { SetX87ValidTag(Offset, false); } else { StackData.setTagInvalid(Offset); } } else { // invalidate all if (SlowPath) { TopValidCache.fill(StackSlot::INVALID); FlushValidPending = true; } else { for (size_t i = 0; i < StackData.size; i++) { StackData.setTagInvalid(i); } } } break; } case OP_PUSHSTACK: { const auto* Op = IROp->C(); auto* SourceNode = CurrentIR.GetNode(Op->X80Src); if (SlowPath) { UpdateTopForPush_Slow(); StoreStackValueAtOffset_Slow(SourceNode); } else { if (Op->OriginalValue.IsInvalid()) { // No original value to track - just push the converted data StackData.push(StackMemberInfo {SourceNode}); } else { auto* OriginalNode = CurrentIR.GetNode(Op->OriginalValue); StackData.push(StackMemberInfo {SourceNode, OriginalNode, Op->LoadSize}); } } break; } case OP_COPYPUSHSTACK: { const auto* Op = IROp->C(); auto Offset = Op->StackLocation; auto Value = MigrateToSlowPath_IfInvalid(Offset); if (SlowPath) { Ref St0 = LoadStackValueAtOffset_Slow(Offset); UpdateTopForPush_Slow(); StoreStackValueAtOffset_Slow(St0); } else { StackData.push(*Value); } break; } case OP_READSTACKVALUE: { const auto* Op = IROp->C(); auto Offset = Op->StackLocation; Ref NewValue = LoadStackValue(Offset); IREmit->ReplaceUsesWithAfter(CodeNode, NewValue, CodeNode); break; } case OP_STACKVALIDTAG: { // Returns 0 if value is valid and 1 otherwise. const auto* Op = IROp->C(); auto Offset = Op->StackLocation; auto Value = MigrateToSlowPath_IfInvalid(Offset); Ref Tag {}; if (SlowPath) { Tag = GetX87ValidTag_Slow(Offset); } else { Tag = Value ? GetConstant(1) : GetConstant(0); } IREmit->ReplaceUsesWithAfter(CodeNode, Tag, CodeNode); break; } case OP_STORESTACKMEM: { const auto* Op = IROp->C(); const auto& Value = MigrateToSlowPath_IfInvalid(); Ref StackNode = SlowPath ? LoadStackValueAtOffset_Slow() : Value->StackDataNode; Ref AddrNode = CurrentIR.GetNode(Op->Addr); Ref Offset = CurrentIR.GetNode(Op->Offset); OpSize Align = Op->Align; MemOffsetType OffsetType = Op->OffsetType; uint8_t OffsetScale = Op->OffsetScale; // On the fast path we can optimize memory copies. // If we are doing: // fld dword [rax] // fst dword [rbx] // We can optimize this to: // ldr w2, [x0] // str w2, [x1] // or similar. As long as the source size and dest size are one and the same. // This will avoid any conversions between source and stack element size and conversion back. OpSize StoreSize = Op->StoreSize; LOGMAN_THROW_A_FMT(Op->StoreSize == OpSize::i32Bit || Op->StoreSize == OpSize::i64Bit || Op->StoreSize == OpSize::f80Bit, "Invalid store size in x87 store stack mem"); if (!SlowPath && Value->Source && Value->Source->Size == StoreSize) { Ref SourceValue = Value->Source->Node; if (Op->StoreSize == OpSize::f80Bit) { Store80BitToMem(Op, SourceValue, AddrNode, Offset, Align, OffsetType, OffsetScale); } else { IREmit->_StoreMemFPR(StoreSize, SourceValue, AddrNode, Offset, Align, OffsetType, OffsetScale); } break; } if (ReducedPrecisionMode) { StoreStackMem_Reduced_Helper(CurrentIR, Op, StackNode); break; } StoreStackMem_Helper(CurrentIR, Op, StackNode); break; } case OP_STORESTACKTOSTACK: { // stores top of stack in another place in stack. const auto* Op = IROp->C(); auto Offset = Op->StackLocation; auto Value = MigrateToSlowPath_IfInvalid(); // Need to store st0 to stack location - basically a copy. if (SlowPath) { StoreStackValueAtOffset_Slow(LoadStackValueAtOffset_Slow(), Offset); } else { StackData.setTop(*Value, Offset); } break; } case OP_POPSTACKDESTROY: { StackPop(); break; } case OP_F80STACKXCHANGE: { const auto* Op = IROp->C(); auto Offset = Op->SrcStack; if (Offset == 0) { // No-op break; } const auto [ValidTop, StackMemberTop] = StackData.top(0); const auto [ValidOffset, StackMemberOffset] = StackData.top(Offset); if (ValidTop != StackSlot::VALID || ValidOffset != StackSlot::VALID) { // Slow path: do actual memory operations Ref ValueTop = LoadStackValue(); Ref ValueOffset = LoadStackValue(Offset); StoreStackValue(ValueOffset, 0, true); StoreStackValue(ValueTop, Offset, true); } else { // Fast path: swap complete StackMemberInfo preserving Source metadata StackData.setTop(StackMemberOffset, 0); StackData.setTop(StackMemberTop, Offset); } break; } case OP_F80STACKCHANGESIGN: { Ref Value = LoadStackValue(); // We need a couple of intermediate instructions to change the sign // of a value Ref ResultNode {}; if (ReducedPrecisionMode) { ResultNode = IREmit->_VFNeg(OpSize::i64Bit, OpSize::i64Bit, Value); } else { Ref HelperNode = IREmit->_LoadNamedVectorConstant(OpSize::i128Bit, IR::NamedVectorConstant::NAMED_VECTOR_F80_SIGN_MASK); ResultNode = IREmit->_VXor(OpSize::i128Bit, Value, HelperNode); } StoreStackValue(ResultNode); break; } case OP_F80STACKABS: { Ref Value = LoadStackValue(); Ref ResultNode {}; if (ReducedPrecisionMode) { ResultNode = IREmit->_VFAbs(OpSize::i64Bit, OpSize::i64Bit, Value); } else { // Intermediate insts Ref HelperNode = IREmit->_LoadNamedVectorConstant(OpSize::i128Bit, IR::NamedVectorConstant::NAMED_VECTOR_F80_SIGN_MASK); ResultNode = IREmit->_VAndn(OpSize::i128Bit, Value, HelperNode); } StoreStackValue(ResultNode); break; } case OP_F80CMPSTACK: { const auto* Op = IROp->C(); auto Offset = Op->SrcStack; Ref StackValue1 = LoadStackValue(); Ref StackValue2 = LoadStackValue(Offset); Ref CmpNode {}; if (ReducedPrecisionMode) { CmpNode = IREmit->_FCmp(OpSize::i64Bit, StackValue1, StackValue2); } else { CmpNode = IREmit->_F80Cmp(StackValue1, StackValue2); } IREmit->ReplaceUsesWithAfter(CodeNode, CmpNode, CodeNode); break; } case OP_F80STACKTEST: { const auto* Op = IROp->C(); auto Offset = Op->SrcStack; auto StackNode = LoadStackValue(Offset); Ref ZeroConst = IREmit->_VCastFromGPR(ReducedPrecisionMode ? OpSize::i64Bit : OpSize::i128Bit, OpSize::i64Bit, GetConstant(0)); Ref CmpNode {}; if (ReducedPrecisionMode) { CmpNode = IREmit->_FCmp(OpSize::i64Bit, StackNode, ZeroConst); } else { CmpNode = IREmit->_F80Cmp(StackNode, ZeroConst); } IREmit->ReplaceUsesWithAfter(CodeNode, CmpNode, CodeNode); break; } case OP_F80CMPVALUE: { const auto* Op = IROp->C(); const auto& Value = CurrentIR.GetNode(Op->X80Src); auto StackNode = LoadStackValue(); Ref CmpNode {}; if (ReducedPrecisionMode) { CmpNode = IREmit->_FCmp(OpSize::i64Bit, StackNode, Value); } else { CmpNode = IREmit->_F80Cmp(StackNode, Value); } IREmit->ReplaceUsesWithAfter(CodeNode, CmpNode, CodeNode); break; } case OP_SYNCSTACKTOSLOW: { // This synchronizes stack values but doesn't necessarily moves us off the FastPath! Ref NewTop = SynchronizeStackValues(); FlushCachedRegs(); IREmit->ReplaceUsesWithAfter(CodeNode, NewTop, CodeNode); break; } case OP_STACKFORCESLOW: { MigrateToSlowPathIf(true); InvalidateCachedRegs(); break; } case OP_INCSTACKTOP: { if (SlowPath) { UpdateTopForPop_Slow(false); } else { StackData.rotate(false); } break; } case OP_DECSTACKTOP: { if (SlowPath) { UpdateTopForPush_Slow(); } else { StackData.rotate(true); } break; } case OP_F80ROUNDSTACK: { Ref St0 = LoadStackValue(); Ref Value {}; if (ReducedPrecisionMode) { Value = IREmit->_Vector_FToI(OpSize::i64Bit, OpSize::i64Bit, St0, RoundMode::Host); } else { Value = IREmit->_F80Round(St0); } StoreStackValue(Value); break; } case OP_F80VBSLSTACK: { const auto* Op = IROp->C(); auto StackOffset1 = Op->SrcStack1; auto StackOffset2 = Op->SrcStack2; Ref Value1 = LoadStackValue(StackOffset1); Ref Value2 = LoadStackValue(StackOffset2); Ref StackNode = IREmit->_VBSL(OpSize::i128Bit, CurrentIR.GetNode(Op->VectorMask), Value1, Value2); StoreStackValue(StackNode, 0, StackOffset1 && StackOffset2); break; } default: LOGMAN_THROW_A_FMT(false, "IROp was expected to be lowered"); } IREmit->Remove(CodeNode); } auto Last = CurrentIR.at(BlockIROp->Last); --Last; auto [LastCodeNode, LastIROp] = Last(); LOGMAN_THROW_A_FMT(IsBlockExit(LastIROp->Op), "must be exit"); IREmit->SetWriteCursorBefore(LastCodeNode); SynchronizeStackValues(); FlushCachedRegs(); } } fextl::unique_ptr CreateX87StackOptimizationPass(const HostFeatures& Features, OpSize GPROpSize) { return fextl::make_unique(Features, GPROpSize); } } // namespace FEXCore::IR