Skip to content

Commit

Permalink
Merge pull request #631 from mbbsemu/ge-fpu-fixes
Browse files Browse the repository at this point in the history
Galactic Empire Fixes (FPU + Input)
  • Loading branch information
paladine authored Oct 9, 2024
2 parents 8631d4d + 6040f78 commit 29bb032
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 76 deletions.
29 changes: 24 additions & 5 deletions MBBSEmu.Tests/CPU/FLDCW_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,41 @@ namespace MBBSEmu.Tests.CPU
{
public class FLDCW_Tests : CpuTestBase
{
[Fact]
public void FLDCW_Test()
/// <summary>
/// Loads the FPU Control Word from Memory and sets the FPU Control Word
///
/// We'll verify this by not only setting the Control Word, but also verify
/// the Round Mode is set correctly
///
/// Rounding Flag Conversion:
/// Rounding Mode = (ControlWord >> 10) &amp; 0x3;
/// 0 => MidpointRounding.ToEven
/// 1 => MidpointRounding.ToNegativeInfinity
/// 2 => MidpointRounding.ToPositiveInfinity
/// 3 => MidpointRounding.ToZero
/// </summary>
[Theory]
[InlineData(0x0000, MidpointRounding.ToEven)]
[InlineData(0x0400, MidpointRounding.ToNegativeInfinity)]
[InlineData(0x0800, MidpointRounding.ToPositiveInfinity)]
[InlineData(0x0C00, MidpointRounding.ToZero)]
public void FLDCW_Test(ushort controlWord, MidpointRounding roundingMode)
{
Reset();
mbbsEmuCpuRegisters.Fpu.ControlWord = 0;
CreateDataSegment(new ReadOnlySpan<byte>(), 2);
mbbsEmuMemoryCore.SetWord(2,0, 0xFFFF);
mbbsEmuMemoryCore.SetArray(2, 0, [0, 0, 0]); //Set some junk data ahead of the actual address
mbbsEmuMemoryCore.SetWord(2,3, controlWord);
mbbsEmuCpuRegisters.DS = 2;

var instructions = new Assembler(16);
instructions.fldcw(__word_ptr[0]);
instructions.fldcw(__word_ptr[3]);
CreateCodeSegment(instructions);

mbbsEmuCpuCore.Tick();

Assert.Equal(0xFFFF, mbbsEmuCpuRegisters.Fpu.ControlWord);
Assert.Equal(controlWord, mbbsEmuCpuRegisters.Fpu.ControlWord);
Assert.Equal(roundingMode, mbbsEmuCpuRegisters.Fpu.GetRoundingControl());
}
}
}
37 changes: 30 additions & 7 deletions MBBSEmu.Tests/CPU/FRNDINT_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,41 @@ namespace MBBSEmu.Tests.CPU
{
public class FRNDINT_Tests : CpuTestBase
{
/// <summary>
/// Tests the FRNDINT Instruction which rounds the value in ST(0) to the nearest integer
///
/// We'll test this by setting the FPU Control Mode to Round to different modes and verify.
///
/// Rounding Flag Conversion:
/// Rounding Mode = (ControlWord >> 10) &amp; 0x3;
/// 0 => MidpointRounding.ToEven
/// 1 => MidpointRounding.ToNegativeInfinity
/// 2 => MidpointRounding.ToPositiveInfinity
/// 3 => MidpointRounding.ToZero
/// </summary>
/// <param name="ST0Value"></param>
/// <param name="expectedValue"></param>
[Theory]
[InlineData(2.1d, 2d)]
[InlineData(0.1d, 0d)]
[InlineData(1.9d, 2d)]
[InlineData(-1.9d, -2d)]
[InlineData(0.5d, 1d)]
[InlineData(0.49999999d, 0d)]
public void FRNDINT_Test(double ST0Value, double expectedValue)
//Round to Even
[InlineData(1.5, 2.0, 0x0000)]
[InlineData(-1.5, -2.0, 0x0000)]
//Round to Negative Infinity
[InlineData(1.5, 1.0, 0x0400)]
[InlineData(-1.5, -2.0, 0x0400)]
//Round to Positive Infinity
[InlineData(1.5, 2.0, 0x0800)]
[InlineData(-1.5, -1.0, 0x0800)]
//Round to Zero
[InlineData(1.5, 1.0, 0x0C00)]
[InlineData(-1.5, -1.0, 0x0C00)]
public void FRNDINT_Test(double ST0Value, double expectedValue, ushort controlWord)
{
Reset();
mbbsEmuCpuCore.FpuStack[mbbsEmuCpuRegisters.Fpu.GetStackTop()] = ST0Value;

//Set FPU Control Word to set rounding to even
mbbsEmuCpuRegisters.Fpu.ControlWord = controlWord;

var instructions = new Assembler(16);
instructions.frndint();
CreateCodeSegment(instructions);
Expand Down
18 changes: 9 additions & 9 deletions MBBSEmu.Tests/ExportedModules/Majorbbs/parsin_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,30 @@ public class parsin_Tests : ExportedModuleTestBase
private const ushort PARSIN_ORDINAL = 467;

[Theory]
[InlineData("TEST1 TEST2 TEST3\0", 3, 18)]
[InlineData("TEST1 TEST2 TEST3\0", 3, 17)]
[InlineData("\0", 0, 0)]
[InlineData("A B\0", 2, 4)]
[InlineData("A TEST B\0", 3, 9)]
[InlineData("A TEST B\0", 3, 20)]
[InlineData("A TEST B\0", 3, 34)]
[InlineData("A B\0", 2, 3)]
[InlineData("A TEST B\0", 3, 8)]
[InlineData("A TEST B\0", 3, 19)]
[InlineData("A TEST B\0", 3, 33)]
[InlineData("TEST \0", 1, 5)]
public void parsin_Test(string inputCommand, int expectedMargc, int expectedInputLength)
{
//Reset State
Reset();

//Set Input Values
mbbsEmuMemoryCore.SetArray("INPUT", Encoding.ASCII.GetBytes(inputCommand));
mbbsEmuMemoryCore.SetWord("INPLEN", (ushort)inputCommand.Length);
mbbsEmuMemoryCore.SetWord("INPLEN", (ushort)inputCommand.Replace("\0", string.Empty).Length);

ExecuteApiTest(HostProcess.ExportedModules.Majorbbs.Segment, PARSIN_ORDINAL, new List<FarPtr>());

//Verify Results
var expectedParsedInput = Encoding.ASCII.GetBytes(inputCommand.Replace(' ', '\0'));
var expectedParsedInput = Encoding.ASCII.GetBytes(inputCommand.Replace(' ', '\0')[..expectedInputLength]);
var actualInputLength = mbbsEmuMemoryCore.GetWord("INPLEN");
var actualMargc = mbbsEmuMemoryCore.GetWord("MARGC");

//Just NULL returns a length of 0, but we'll grab & verify the null at that position if the length is zero
var actualInput = mbbsEmuMemoryCore.GetArray("INPUT", actualInputLength == 0 ? (ushort)1 : actualInputLength).ToArray();
var actualInput = mbbsEmuMemoryCore.GetArray("INPUT", actualInputLength).ToArray();

Assert.Equal(expectedMargc, actualMargc); //Verify Correct Number of Commands Parsed
Assert.Equal(expectedInputLength, actualInputLength); //Verify Length is Correct
Expand Down
113 changes: 72 additions & 41 deletions MBBSEmu/CPU/CPUCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ public class CpuCore : CpuRegisters, ICpuCore, IDisposable
private readonly Dictionary<int, IInterruptHandler> _interruptHandlers = new();
private readonly Dictionary<int, IIOPort> _ioPortHandlers = new();

private List<List<FarPtr>> CPUDebugRanges = [];
private List<FarPtr> CPUBreakpoints = [];
private bool CPUDebugBreak = false;
private Dictionary<(FarPtr, ushort), byte[]> WatchedVariables = new ();

public CpuCore(IMessageLogger logger)
{
_logger = logger;
Expand Down Expand Up @@ -160,7 +165,8 @@ public void Dispose()
/// <param name="interruptHandlers"></param>
/// <param name="ioPortHandlers"></param>
public void Reset(IMemoryCore memoryCore,
InvokeExternalFunctionDelegate invokeExternalFunctionDelegate, IEnumerable<IInterruptHandler> interruptHandlers, IDictionary<int, IIOPort> ioPortHandlers)
InvokeExternalFunctionDelegate invokeExternalFunctionDelegate,
IEnumerable<IInterruptHandler> interruptHandlers, IDictionary<int, IIOPort> ioPortHandlers)
{
//Setup Debug Pointers
_currentInstructionPointer = FarPtr.Empty;
Expand Down Expand Up @@ -199,6 +205,53 @@ public void Reset(IMemoryCore memoryCore,
//These two values are the final values popped off on the routine's last RETF
//Seeing a ushort.max for CS and IP tells the routine it's now done
Push(uint.MaxValue);

//Setup Debug

/* ---------------------------
* ## CPU Breakpoints ##
* ---------------------------
* The below list specifies addresses where the CPU will be halted, allowing you to break operation and inspect.
* You can specify any number of offsets in any order. Because this will only work in DEBUG, and is evaluated
* during operation with each instruction, having a large number of breakpoints will slow down the CPU
*/
CPUBreakpoints =
[
//Below would break the CPU at the CS:IP of 0x1:0x100
//new(0x1, 0x100)
];

/* ---------------------------
* ## CPU Debug Ranges ##
* ---------------------------
* The below list specifies a list of address ranges where CPU debug information will be logged to the console
* for each executed instruction (including register debug information as well). Values must be in START->END
* order within their own pair, but pairs can be added to the list in any order.
*/
CPUDebugRanges =
[
//[
// new FarPtr(0x1, 0xD23),
// new FarPtr(0x1, 0xD59)
//],
//[
// new FarPtr(0x1, 0x44B),
// new FarPtr(0x1, 0x475)
//]
];

/* ---------------------------
* ## Watched Memory ##
* ---------------------------
* The below Dictionary specified the addresses that will be watched for changes between CPU ticks. This will
* assist in isolating any issues that might be occurring with memory corruption or in other areas of the code
* not currently being debugged.
*/
WatchedVariables =
[
//{ (new FarPtr(0x9, 0x469), 2), null }
];

}

/// <summary>
Expand Down Expand Up @@ -318,38 +371,6 @@ public void Tick()
_currentOperationSize = GetCurrentOperationSize();

#if DEBUG
/* ---------------------------
* ## CPU Breakpoints ##
* ---------------------------
* The below list specifies addresses where the CPU will be halted, allowing you to break operation and inspect.
* You can specify any number of offsets in any order. Because this will only work in DEBUG, and is evaluated
* during operation with each instruction, having a large number of breakpoints will slow down the CPU
*/
var CPUBreakpoints = new List<FarPtr>
{
//Below would break the CPU at the CS:IP of 0x1:0x100
//new(0x1, 0x100)
};

/* ---------------------------
* ## CPU Debug Ranges ##
* ---------------------------
* The below list specifies a list of address ranges where CPU debug information will be logged to the console
* for each executed instruction (including register debug information as well). Values must be in START->END
* order within their own pair, but pairs can be added to the list in any order.
*/
var CPUDebugRanges = new List<List<FarPtr>>
{
//Below would log debug information for the range of CS:IP 0x1:0x100 to 0x1:0x200
//new()
//{
// new FarPtr(0x1, 0x100),
// new FarPtr(0x1, 0x200)
//}
};

//Set this value to TRUE if you want the CPU to break after each instruction
var CPUDebugBreak = false;

//Evaluate Breakpoints
if (CPUBreakpoints.Contains(_currentInstructionPointer))
Expand All @@ -370,6 +391,17 @@ public void Tick()
_showDebug = false;
}

//Evaluate Watched Variables
foreach (var (address, length) in WatchedVariables.Keys)
{
var value = Memory.GetArray(address.Segment, address.Offset, length);
if (WatchedVariables[(address, length)] == null || !value.SequenceEqual(WatchedVariables[(address, length)]))
{
_logger.Debug($"Value Change @ {address} :: Old: {WatchedVariables[(address, length)]} :: New: {value.ToHexString()}");
WatchedVariables[(address, length)] = value.ToArray();
}
}

_currentInstructionPointer.Offset = Registers.IP;
_currentInstructionPointer.Segment = Registers.CS;
#endif
Expand Down Expand Up @@ -1883,7 +1915,7 @@ private ushort Op_Sar_8()
// in order to inspect bits lost during shift when computing the carry flag, we
// extend the size of the operation first by shifting left and then shifting right.
// So result becomes 0xDDLL where DD is the value and LL are the lost bits
var uresult = (ushort)((((ushort)destination) << 8) >> source);
var uresult = (ushort)((destination << 8) >> source);
Flags_EvaluateCarry(EnumArithmeticOperation.ShiftArithmeticRight, (byte)(uresult & 0x80));

var result = (byte)(uresult >> 8);
Expand All @@ -1910,7 +1942,7 @@ private ushort Op_Sar_16()
// in order to inspect bits lost during shift when computing the carry flag, we
// extend the size of the operation first by shifting left and then shifting right.
// So result becomes 0xDDDDLLLL where DDDD is the value and LLLL are the lost bits
var uresult = (uint)((((uint)destination) << 16) >> source);
var uresult = ((uint)destination << 16) >> source;
Flags_EvaluateCarry(EnumArithmeticOperation.ShiftArithmeticRight, (ushort)(uresult & 0x8000));

var result = (ushort)(uresult >> 16);
Expand Down Expand Up @@ -1950,7 +1982,7 @@ private byte Op_Shr_8()
// in order to inspect bits lost during shift when computing the carry flag, we
// extend the size of the operation first by shifting left and then shifting right.
// So result becomes 0xDDLL where DD is the value and LL are the lost bits
var uresult = (ushort)((((ushort)destination) << 8) >> source);
var uresult = (ushort)((destination << 8) >> source);
Flags_EvaluateCarry(EnumArithmeticOperation.ShiftRight, (byte)(uresult & 0x80));

var result = (byte)(uresult >> 8);
Expand All @@ -1973,7 +2005,7 @@ private ushort Op_Shr_16()
// in order to inspect bits lost during shift when computing the carry flag, we
// extend the size of the operation first by shifting left and then shifting right.
// So result becomes 0xDDDDLLLL where DDDD is the value and LLLL are the lost bits
var uresult = (uint)((((uint)destination) << 16) >> source);
var uresult = ((uint)destination << 16) >> source;
Flags_EvaluateCarry(EnumArithmeticOperation.ShiftRight, (ushort)(uresult & 0x8000));

var result = (ushort)(uresult >> 16);
Expand Down Expand Up @@ -3562,8 +3594,7 @@ private void Op_Fldpi()
[MethodImpl(OpcodeCompilerOptimizations)]
private void Op_Fldcw()
{
var offset = GetOperandOffset(_currentInstruction.Op0Kind);
var newControlWord = Memory.GetWord(Registers.DS, offset);
var newControlWord = GetOperandValueUInt16(_currentInstruction.Op0Kind, EnumOperandType.None);

Registers.Fpu.ControlWord = newControlWord;
}
Expand Down Expand Up @@ -3604,7 +3635,7 @@ private void Op_Fistp()
}

//Round Value from FPU
valueFromFpu = Math.Round(valueFromFpu, MidpointRounding.AwayFromZero);
valueFromFpu = Math.Round(valueFromFpu, Registers.Fpu.GetRoundingControl());

//Safely Cast it to 32-bit Signed Integer
int valueToSave;
Expand Down Expand Up @@ -4172,7 +4203,7 @@ private void Op_Fclex()
[MethodImpl(OpcodeCompilerOptimizations)]
private void Op_Frndint()
{
FpuStack[Registers.Fpu.GetStackTop()] = Math.Round(FpuStack[Registers.Fpu.GetStackTop()], MidpointRounding.AwayFromZero);
FpuStack[Registers.Fpu.GetStackTop()] = Math.Round(FpuStack[Registers.Fpu.GetStackTop()], Registers.Fpu.GetRoundingControl());
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions MBBSEmu/CPU/CpuRegisters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public void SetPointer(FarPtr ptr)
public void PopStackTop() => Registers.Fpu.PopStackTop();
public void PushStackTop() => Registers.Fpu.PushStackTop();
public void ClearExceptions() => Registers.Fpu.ClearExceptions();
public MidpointRounding GetRoundingControl() => Registers.Fpu.GetRoundingControl();

/// <summary>
/// Overridden ToString() to display the current state of the CPU Registers
Expand Down
3 changes: 2 additions & 1 deletion MBBSEmu/CPU/EnumOperandType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum EnumOperandType
{
Destination,
Source,
Count
Count,
None
}
}
6 changes: 1 addition & 5 deletions MBBSEmu/CPU/ICpuRegisters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@ namespace MBBSEmu.CPU
public interface IFpuRegisters
{
ushort StatusWord { get; set; }

ushort ControlWord { get; set; }

void SetFlag(EnumFpuStatusFlags statusFlag);
void ClearFlag(EnumFpuStatusFlags statusFlag);

byte GetStackTop();

void SetStackTop(byte value);

int GetStackPointer(Register register);
void PopStackTop();
void PushStackTop();
void ClearExceptions();
MidpointRounding GetRoundingControl();
}

/// <summary>
Expand Down
16 changes: 16 additions & 0 deletions MBBSEmu/CPU/RegistersStructs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ public struct FpuRegistersStruct

public byte GetStackTop() => (byte) ((StatusWord >> 11) & 0x7);

/// <summary>
/// Gets the FPU Rounding Control by getting the Rounding Control bits from the Control Word and convert it to a C# MidpointRounding enum
/// </summary>
public MidpointRounding GetRoundingControl()
{
var roundingControl = (ControlWord >> 10) & 0x3;
return roundingControl switch
{
0 => MidpointRounding.ToEven,
1 => MidpointRounding.ToNegativeInfinity,
2 => MidpointRounding.ToPositiveInfinity,
3 => MidpointRounding.ToZero,
_ => throw new ArgumentOutOfRangeException()
};
}

public void SetStackTop(byte value)
{
StatusWord &= unchecked((ushort)(~0x3800)); //Zero out the previous value
Expand Down
Loading

0 comments on commit 29bb032

Please sign in to comment.