diff --git a/BizHawk.Client.EmuHawk/tools/VirtualPads/controls/components/AnalogStickPanel.cs b/BizHawk.Client.EmuHawk/tools/VirtualPads/controls/components/AnalogStickPanel.cs index c8a512088d..ddbf3e11a3 100644 --- a/BizHawk.Client.EmuHawk/tools/VirtualPads/controls/components/AnalogStickPanel.cs +++ b/BizHawk.Client.EmuHawk/tools/VirtualPads/controls/components/AnalogStickPanel.cs @@ -19,7 +19,7 @@ namespace BizHawk.Client.EmuHawk get => _x; set { - _x = _rangeX.Constrain(value); + _x = value.ConstrainWithin(_rangeX); SetAnalog(); } } @@ -29,7 +29,7 @@ namespace BizHawk.Client.EmuHawk get => _y; set { - _y = _rangeY.Constrain(value); + _y = value.ConstrainWithin(_rangeY); SetAnalog(); } } @@ -56,24 +56,22 @@ namespace BizHawk.Client.EmuHawk public void SetRangeX(float[] range) { - _actualRangeX.Min = (int) range[0]; - _actualRangeX.Max = (int) range[2]; - + _actualRangeX.Start = (int) range[0]; + _actualRangeX.EndInclusive = (int) range[2]; Rerange(); } public void SetRangeY(float[] range) { - _actualRangeY.Min = (int) range[0]; - _actualRangeY.Max = (int) range[2]; - + _actualRangeY.Start = (int) range[0]; + _actualRangeY.EndInclusive = (int) range[2]; Rerange(); } - private readonly MutableIntRange _rangeX = new MutableIntRange(-128, 127); - private readonly MutableIntRange _rangeY = new MutableIntRange(-128, 127); - private readonly MutableIntRange _actualRangeX = new MutableIntRange(-128, 127); - private readonly MutableIntRange _actualRangeY = new MutableIntRange(-128, 127); + private readonly MutableRange _rangeX = new MutableRange(-128, 127); + private readonly MutableRange _rangeY = new MutableRange(-128, 127); + private RangeStruct _actualRangeX = new RangeStruct { Start = -128, EndInclusive = 127 }; + private RangeStruct _actualRangeY = new RangeStruct { Start = -128, EndInclusive = 127 }; private bool _reverseX; private bool _reverseY; @@ -83,15 +81,13 @@ namespace BizHawk.Client.EmuHawk _reverseX = _userRangePercentageX < 0; _reverseY = _userRangePercentageY < 0; - var midX = (_actualRangeX.Min + _actualRangeX.Max) / 2.0; - var halfRangeX = (_reverseX ? -1 : 1) * (_actualRangeX.Max - _actualRangeX.Min) * _userRangePercentageX / 200.0; - _rangeX.Min = (int) (midX - halfRangeX); - _rangeX.Max = (int) (midX + halfRangeX); + var midX = (_actualRangeX.Start + _actualRangeX.EndInclusive) / 2.0; + var halfRangeX = (_reverseX ? -1 : 1) * (_actualRangeX.EndInclusive - _actualRangeX.Start) * _userRangePercentageX / 200.0; + _rangeX.Overwrite((int) (midX - halfRangeX), (int) (midX + halfRangeX)); - var midY = (_actualRangeY.Min + _actualRangeY.Max) / 2.0; - var halfRangeY = (_reverseY ? -1 : 1) * (_actualRangeY.Max - _actualRangeY.Min) * _userRangePercentageY / 200.0; - _rangeY.Min = (int) (midY - halfRangeY); - _rangeY.Max = (int) (midY + halfRangeY); + var midY = (_actualRangeY.Start + _actualRangeY.EndInclusive) / 2.0; + var halfRangeY = (_reverseY ? -1 : 1) * (_actualRangeY.EndInclusive - _actualRangeY.Start) * _userRangePercentageY / 200.0; + _rangeY.Overwrite((int) (midY - halfRangeY), (int) (midY + halfRangeY)); // re-constrain after changing ranges X = X; @@ -108,12 +104,12 @@ namespace BizHawk.Client.EmuHawk /// /// min + (max - i) == max - (i - min) == min + max - i /// - private int MaybeReversedInX(int i) => _reverseX ? _rangeX.Min + _rangeX.Max - i : i; + private int MaybeReversedInX(int i) => _reverseX ? _rangeX.Start + _rangeX.EndInclusive - i : i; /// - private int MaybeReversedInY(int i) => _reverseY ? _rangeY.Min + _rangeY.Max - i : i; + private int MaybeReversedInY(int i) => _reverseY ? _rangeY.Start + _rangeY.EndInclusive - i : i; - private int PixelSizeX => (int)(_rangeX.GetCount() * ScaleX); - private int PixelSizeY => (int)(_rangeY.GetCount() * ScaleY); + private int PixelSizeX => (int)(_rangeX.Count() * ScaleX); + private int PixelSizeY => (int)(_rangeY.Count() * ScaleY); private int PixelMinX => (Size.Width - PixelSizeX) / 2; private int PixelMinY => (Size.Height - PixelSizeY) / 2; private int PixelMidX => PixelMinX + PixelSizeX / 2; @@ -122,16 +118,16 @@ namespace BizHawk.Client.EmuHawk private int PixelMaxY => PixelMinY + PixelSizeY - 1; private int RealToGfxX(int val) => - PixelMinX + ((MaybeReversedInX(_rangeX.Constrain(val)) - _rangeX.Min) * ScaleX).RoundToInt(); + PixelMinX + ((MaybeReversedInX(val.ConstrainWithin(_rangeX)) - _rangeX.Start) * ScaleX).RoundToInt(); private int RealToGfxY(int val) => - PixelMinY + ((MaybeReversedInY(_rangeY.Constrain(val)) - _rangeY.Min) * ScaleY).RoundToInt(); + PixelMinY + ((MaybeReversedInY(val.ConstrainWithin(_rangeY)) - _rangeY.Start) * ScaleY).RoundToInt(); private int GfxToRealX(int val) => - MaybeReversedInX(_rangeX.Constrain(_rangeX.Min + ((val - PixelMinX) / ScaleX).RoundToInt())); + MaybeReversedInX((_rangeX.Start + ((val - PixelMinX) / ScaleX).RoundToInt()).ConstrainWithin(_rangeX)); private int GfxToRealY(int val) => - MaybeReversedInY(_rangeY.Constrain(_rangeY.Min + ((val - PixelMinY) / ScaleY).RoundToInt())); + MaybeReversedInY((_rangeY.Start + ((val - PixelMinY) / ScaleY).RoundToInt()).ConstrainWithin(_rangeY)); private readonly Pen _blackPen = new Pen(Brushes.Black); private readonly Pen _bluePen = new Pen(Brushes.Blue, 2); @@ -200,7 +196,7 @@ namespace BizHawk.Client.EmuHawk var pX = (int)_previous.GetFloat(XName); var pY = (int)_previous.GetFloat(YName); e.Graphics.DrawLine(_grayPen, PixelMidX, PixelMidY, RealToGfxX(pX), RealToGfxY(pY)); - e.Graphics.DrawImage(_grayDot, RealToGfxX(pX) - 3, RealToGfxY(_rangeY.Max) - RealToGfxY(pY) - 3); + e.Graphics.DrawImage(_grayDot, RealToGfxX(pX) - 3, RealToGfxY(_rangeY.EndInclusive) - RealToGfxY(pY) - 3); } // Line diff --git a/BizHawk.Common/BizHawk.Common.csproj b/BizHawk.Common/BizHawk.Common.csproj index 30e8052490..f5dd02f8a7 100644 --- a/BizHawk.Common/BizHawk.Common.csproj +++ b/BizHawk.Common/BizHawk.Common.csproj @@ -84,10 +84,10 @@ - + diff --git a/BizHawk.Common/Extensions/NumberExtensions.cs b/BizHawk.Common/Extensions/NumberExtensions.cs index e12201950a..6484a15fe3 100644 --- a/BizHawk.Common/Extensions/NumberExtensions.cs +++ b/BizHawk.Common/Extensions/NumberExtensions.cs @@ -122,5 +122,17 @@ namespace BizHawk.Common.NumberExtensions } public static int RoundToInt(this float f) => (int) Math.Round(f); + + /// 2^-53 + private const double ExtremelySmallNumber = 1.1102230246251565E-16; + + /// + public static bool HawkFloatEquality(this double d, double other, double ε = ExtremelySmallNumber) => Math.Abs(other - d) < ε; + + /// 2^-24 + private const float ReallySmallNumber = 5.96046448E-08f; + + /// don't use this in cores without picking a suitable ε + public static bool HawkFloatEquality(this float f, float other, float ε = ReallySmallNumber) => Math.Abs(other - f) < ε; } } diff --git a/BizHawk.Common/MutableIntRange.cs b/BizHawk.Common/MutableIntRange.cs deleted file mode 100644 index 227e15b362..0000000000 --- a/BizHawk.Common/MutableIntRange.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; - -namespace BizHawk.Common -{ - public class MutableIntRange - { - private int _min; - private int _max; - - /// (from setter) > - public int Min - { - get => _min; - set - { - if (_max < value) throw new ArgumentException(); - _min = value; - } - } - - /// (from setter) < - public int Max - { - get => _max; - set - { - if (value < _min) throw new ArgumentException(); - _max = value; - } - } - - public MutableIntRange(int min, int max) - { - _min = min; - Max = max; // set property instead of field to validate and possibly throw an ArgumentException - } - - public int Constrain(int i) => i < _min ? _min : i > _max ? _max : i; - - /// true if i is in the inclusive range .., false otherwise - public bool Covers(int i) => _min <= i && i <= _max; - - public uint GetCount() => (uint) ((long) _max - _min + 1); - - /// true if i is in the exclusive range .., false otherwise - /// - /// You probably want - /// - public bool StrictContains(int i) => _min < i && i < _max; - } -} \ No newline at end of file diff --git a/BizHawk.Common/Ranges.cs b/BizHawk.Common/Ranges.cs new file mode 100644 index 0000000000..d65526d213 --- /dev/null +++ b/BizHawk.Common/Ranges.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using BizHawk.Common.NumberExtensions; + +namespace BizHawk.Common +{ + public struct RangeStruct where T : unmanaged, IComparable + { + public T Start; + + public T EndInclusive; + } + + /// represents a closed, inclusive range of () + public interface Range where T : unmanaged, IComparable + { + T Start { get; } + + T EndInclusive { get; } + } + + public class RangeImpl : Range where T : unmanaged, IComparable + { + protected RangeStruct r; + + /// . < ., or is float/double and either bound is NaN + internal RangeImpl(RangeStruct range) + { + r = ValidatedOrThrow(range); + } + + /// < , or is / and either bound is + public RangeImpl(T start, T endInclusive) : this(new RangeStruct { Start = start, EndInclusive = endInclusive }) {} + + public T Start => r.Start; + + public T EndInclusive => r.EndInclusive; + + internal RangeStruct CopyStruct() => r; + + protected static RangeStruct ValidatedOrThrow(RangeStruct range) where T1 : unmanaged, IComparable + { + if (range is RangeStruct fr && (float.IsNaN(fr.Start) || float.IsNaN(fr.EndInclusive)) + || range is RangeStruct dr && (double.IsNaN(dr.Start) || double.IsNaN(dr.EndInclusive))) + { + throw new ArgumentException("range bound is NaN", nameof(range)); + } + if (range.EndInclusive.CompareTo(range.Start) < 0) throw new ArgumentException("range end < start", nameof(range)); + return range; + } + } + + /// represents a closed, inclusive range of () which can be grown or shrunk + /// inheriting reduces code duplication in + public class MutableRange : RangeImpl where T : unmanaged, IComparable + { + /// + internal MutableRange(RangeStruct range) : base(range) {} + + /// + public MutableRange(T start, T endInclusive) : base(new RangeStruct { Start = start, EndInclusive = endInclusive }) {} + + /// (from setter) > + public new T Start + { + get => r.Start; + set => r.Start = r.EndInclusive.CompareTo(value) < 0 + ? throw new ArgumentException("attempted to set start > end", nameof(value)) + : value; + } + + /// (from setter) < + public new T EndInclusive + { + get => r.EndInclusive; + set => r.EndInclusive = value.CompareTo(r.Start) < 0 + ? throw new ArgumentException("attempted to set end < start", nameof(value)) + : value; + } + + public void Overwrite(T start, T endInclusive) => r = ValidatedOrThrow(new RangeStruct { Start = start, EndInclusive = endInclusive }); + } + + /// contains most of the logic for ranges + /// non-generic overloads are used where the method requires an increment or decrement + public static class RangeExtensions + { + private const string EXCL_RANGE_ARITH_EXC_TEXT = "exclusive range end is min value of integral type"; + + /// if it's contained in , or else whichever bound of is closest to + public static T ConstrainWithin(this T value, Range range) where T : unmanaged, IComparable => value.CompareTo(range.Start) < 0 + ? range.Start + : range.EndInclusive.CompareTo(value) < 0 + ? range.EndInclusive + : value; + + /// true iff is contained in ( is considered to be in the range if it's exactly equal to either bound) + /// + public static bool Contains(this Range range, T value) where T : unmanaged, IComparable => !(value.CompareTo(range.Start) < 0 || range.EndInclusive.CompareTo(value) < 0); + + public static byte Count(this Range range) => (byte) (range.EndInclusive - range.Start + (byte) 1U); + + public static uint Count(this Range range) => (uint) (1L + range.EndInclusive - range.Start); + + public static ulong Count(this Range range) => throw new NotImplementedException("TODO fancy math"); + + public static byte Count(this Range range) => (byte) (1 + range.EndInclusive - range.Start); + + public static ushort Count(this Range range) => (ushort) (1 + range.EndInclusive - range.Start); + + public static uint Count(this Range range) => range.EndInclusive - range.Start + 1U; + + public static ulong Count(this Range range) => range.EndInclusive - range.Start + 1U; + + public static ushort Count(this Range range) => (ushort) (range.EndInclusive - range.Start + (ushort) 1U); + + /// TODO is this faster or slower than the yield return algorithm used in ? + public static IEnumerable Enumerate(this Range range) => Enumerable.Range(range.Start, range.Count()).Select(i => (byte) i); + + /// + public static IEnumerable Enumerate(this Range range, double step) + { + var d = range.Start; + while (d < range.EndInclusive) + { + yield return d; + d += step; + } + if (d.HawkFloatEquality(range.EndInclusive)) yield return d; + } + + /// beware precision errors + public static IEnumerable Enumerate(this Range range, float step) + { + var f = range.Start; + while (f < range.EndInclusive) + { + yield return f; + f += step; + } + if (f.HawkFloatEquality(range.EndInclusive)) yield return f; + } + + public static IEnumerable Enumerate(this Range range) + { + var i = range.Start; + while (i < range.EndInclusive) yield return i++; + yield return i; + } + + public static IEnumerable Enumerate(this Range range) + { + var l = range.Start; + while (l < range.EndInclusive) yield return l++; + yield return l; + } + + public static IEnumerable Enumerate(this Range range) => Enumerable.Range(range.Start, range.Count()).Select(i => (sbyte) i); + + public static IEnumerable Enumerate(this Range range) => Enumerable.Range(range.Start, range.Count()).Select(i => (short) i); + + public static IEnumerable Enumerate(this Range range) + { + var i = range.Start; + while (i < range.EndInclusive) yield return i++; + yield return i; + } + + public static IEnumerable Enumerate(this Range range) + { + var l = range.Start; + while (l < range.EndInclusive) yield return l++; + yield return l; + } + + public static IEnumerable Enumerate(this Range range) => Enumerable.Range(range.Start, range.Count()).Select(i => (ushort) i); + + public static Range GetImmutableCopy(this MutableRange range) where T : unmanaged, IComparable => new MutableRange(range.CopyStruct()); + + public static MutableRange GetMutableCopy(this Range range) where T : unmanaged, IComparable => range is RangeImpl impl + ? new MutableRange(impl.CopyStruct()) // copied by value when using the implementations in this file + : new MutableRange(range.Start, range.EndInclusive); + + /// + public static MutableRange MutableRangeTo(this T start, T endInclusive) where T : unmanaged, IComparable => new MutableRange(start, endInclusive); + + /// + public static MutableRange MutableRangeToExcluding(this byte start, byte endExclusive) => endExclusive == byte.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, (byte) (endExclusive - 1U)); + + /// + public static MutableRange MutableRangeToExcluding(this int start, int endExclusive) => endExclusive == int.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, endExclusive - 1); + + /// + public static MutableRange MutableRangeToExcluding(this long start, long endExclusive) => endExclusive == long.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, endExclusive - 1L); + + /// + public static MutableRange MutableRangeToExcluding(this sbyte start, sbyte endExclusive) => endExclusive == sbyte.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, (sbyte) (endExclusive - 1)); + + /// + public static MutableRange MutableRangeToExcluding(this short start, short endExclusive) => endExclusive == short.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, (short) (endExclusive - 1)); + + /// + public static MutableRange MutableRangeToExcluding(this uint start, uint endExclusive) => endExclusive == uint.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, endExclusive - 1U); + + /// + public static MutableRange MutableRangeToExcluding(this ulong start, ulong endExclusive) => endExclusive == ulong.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, endExclusive - 1UL); + + /// + public static MutableRange MutableRangeToExcluding(this ushort start, ushort endExclusive) => endExclusive == ushort.MinValue + ? throw new ArgumentException(EXCL_RANGE_ARITH_EXC_TEXT, nameof(endExclusive)) + : new MutableRange(start, (ushort) (endExclusive - 1U)); + + /// + public static Range RangeTo(this T start, T endInclusive) where T : unmanaged, IComparable => new RangeImpl(start, endInclusive); + + /// + public static Range RangeToExcluding(this byte start, byte endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// (empty ranges where = are not permitted) + /// is min value of integral type (therefore ) + public static Range RangeToExcluding(this int start, int endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// + public static Range RangeToExcluding(this long start, long endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// + public static Range RangeToExcluding(this sbyte start, sbyte endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// + public static Range RangeToExcluding(this short start, short endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// + public static Range RangeToExcluding(this uint start, uint endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// + public static Range RangeToExcluding(this ulong start, ulong endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// + public static Range RangeToExcluding(this ushort start, ushort endExclusive) => MutableRangeToExcluding(start, endExclusive); + + /// true iff is strictly contained in ( is considered to be OUTSIDE the range if it's exactly equal to either bound) + /// + public static bool StrictlyBoundedBy(this T value, Range range) where T : unmanaged, IComparable => range.Start.CompareTo(value) < 0 && value.CompareTo(range.EndInclusive) < 0; + } +} \ No newline at end of file diff --git a/BizHawk.Emulation.Cores/Consoles/PC Engine/VPC.cs b/BizHawk.Emulation.Cores/Consoles/PC Engine/VPC.cs index 040c62f328..9cba317c28 100644 --- a/BizHawk.Emulation.Cores/Consoles/PC Engine/VPC.cs +++ b/BizHawk.Emulation.Cores/Consoles/PC Engine/VPC.cs @@ -357,15 +357,15 @@ namespace BizHawk.Emulation.Cores.PCEngine // clear inter-sprite priority buffer Array.Clear(InterSpritePriorityBuffer, 0, FrameWidth); - var testRange = new MutableIntRange(0, vdc.ActiveLine + 1); + var testRange = new MutableRange(0, vdc.ActiveLine + 1); for (int i = 0; i < 64; i++) { int y = (vdc.SpriteAttributeTable[(i * 4) + 0] & 1023) - 64; int x = (vdc.SpriteAttributeTable[(i * 4) + 1] & 1023) - 32; ushort flags = vdc.SpriteAttributeTable[(i * 4) + 3]; byte height = heightTable[(flags >> 12) & 3]; - testRange.Min = vdc.ActiveLine - height; - if (!testRange.StrictContains(y)) continue; + testRange.Start = vdc.ActiveLine - height; + if (!y.StrictlyBoundedBy(testRange)) continue; int patternNo = (((vdc.SpriteAttributeTable[(i * 4) + 2]) >> 1) & 0x1FF); int paletteBase = 256 + ((flags & 15) * 16);