diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 01308a696..1a17065dd 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -146,6 +146,7 @@ END TEMPLATE--> * Most usages of x86 SIMD intrinsics have been replaced with cross-platform versions using the new .NET cross-platform intrinsics. * This reduces code to maintain and improves performance on ARM. * Tiny optimization to rendering code. + * Sprite processing & bounding box calculations should be slightly faster now. * `RobustSerializer` no longer needs to be called from threads with an active IoC context. * This makes it possible to use from thread pool threads without `IoCManager.InitThread`. * Removed finalizer dispose from `Overlay`. diff --git a/Robust.Client/GameObjects/Components/Renderable/ISpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/ISpriteComponent.cs index 7ddd9df24..866c73473 100644 --- a/Robust.Client/GameObjects/Components/Renderable/ISpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/ISpriteComponent.cs @@ -222,6 +222,6 @@ namespace Robust.Client.GameObjects /// /// Calculate the rotated sprite bounding box in world-space coordinates. /// - Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, IEye? eye = null); + Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, Angle eye); } } diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs index 3f10cdce8..e408314d2 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteBoundsOverlay.cs @@ -84,7 +84,7 @@ namespace Robust.Client.GameObjects foreach (var (sprite, xform) in comp.SpriteTree.QueryAabb(localAABB)) { var (worldPos, worldRot) = xform.GetWorldPositionRotation(); - var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot); + var bounds = sprite.CalculateRotatedBoundingBox(worldPos, worldRot, _eyeManager.CurrentEye.Rotation); // Get scaled down bounds used to indicate the "south" of a sprite. var localBound = bounds.Box; diff --git a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs index 49317e8e8..165275b95 100644 --- a/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs +++ b/Robust.Client/GameObjects/Components/Renderable/SpriteComponent.cs @@ -1474,7 +1474,7 @@ namespace Robust.Client.GameObjects } /// - public Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, IEye? eye = null) + public Box2Rotated CalculateRotatedBoundingBox(Vector2 worldPosition, Angle worldRotation, Angle eyeRot) { // fast check for empty sprites if (!Visible || Layers.Count == 0) @@ -1491,21 +1491,23 @@ namespace Robust.Client.GameObjects if (worldRotation.Theta < 0) worldRotation = new Angle(worldRotation.Theta + Math.Tau); - eye ??= eyeManager.CurrentEye; - // Next, what we do is take the box2 and apply the sprite's transform, and then the entity's transform. We // could do this via Matrix3.TransformBox, but that only yields bounding boxes. So instead we manually // transform our box by the combination of these matrices: + Angle finalRotation = NoRotation + ? Rotation - eyeRot + : Rotation + worldRotation; + + // slightly faster path if offset == 0 (true for 99.9% of sprites) + if (Offset == Vector2.Zero) + return new Box2Rotated(Bounds.Translated(worldPosition), finalRotation, worldPosition); + var adjustedOffset = NoRotation - ? (-eye.Rotation).RotateVec(Offset) + ? (-eyeRot).RotateVec(Offset) : worldRotation.RotateVec(Offset); Vector2 position = adjustedOffset + worldPosition; - Angle finalRotation = NoRotation - ? Rotation - eye.Rotation - : Rotation + worldRotation; - return new Box2Rotated(Bounds.Translated(position), finalRotation, position); } diff --git a/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs b/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs index 70fb1ae5c..a1feceb35 100644 --- a/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/RenderingTreeSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using JetBrains.Annotations; +using Robust.Client.Graphics; using Robust.Client.Physics; using Robust.Shared; using Robust.Shared.Configuration; @@ -21,6 +22,7 @@ namespace Robust.Client.GameObjects public sealed class RenderingTreeSystem : EntitySystem { [Dependency] private readonly TransformSystem _xformSystem = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; internal const string LoggerSawmill = "rendertree"; @@ -375,7 +377,7 @@ namespace Robust.Client.GameObjects private Box2 SpriteAabbFunc(SpriteComponent value, TransformComponent xform, Vector2 worldPos, Angle worldRot, EntityQuery xforms) { - var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot); + var bounds = value.CalculateRotatedBoundingBox(worldPos, worldRot, _eyeManager.CurrentEye.Rotation); var tree = GetRenderTree(value.Owner, xform, xforms); return tree == null ? bounds.CalcBoundingBox() : _xformSystem.GetInvWorldMatrix(tree.Owner, xforms).TransformBox(bounds); diff --git a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs index c5c9b65f7..c2742ddd2 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.HLR.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.HLR.cs @@ -1,20 +1,17 @@ using System; using System.Buffers; using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Threading; +using OpenToolkit.Graphics.OpenGL4; using Robust.Client.GameObjects; using Robust.Client.ResourceManagement; -using Robust.Shared.Map; -using Robust.Shared.Maths; -using Robust.Shared.Utility; -using OpenToolkit.Graphics.OpenGL4; using Robust.Client.UserInterface.CustomControls; using Robust.Shared; using Robust.Shared.Enums; -using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; using Robust.Shared.Profiling; -using Robust.Shared.Physics; +using Robust.Shared.Utility; namespace Robust.Client.Graphics.Clyde { @@ -25,11 +22,6 @@ namespace Robust.Client.Graphics.Clyde { public ClydeDebugLayers DebugLayers { get; set; } - private readonly RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRotation, Box2 spriteScreenBB)> - _drawingSpriteList - = - new(); - // TODO allow this scale to be passed with PostShader as variable /// /// Some shaders that enlarge the final sprite, like emission or highlight effects, need to use a slightly larger render target. @@ -234,25 +226,14 @@ namespace Robust.Client.Graphics.Clyde } RenderOverlays(viewport, OverlaySpace.WorldSpaceBelowEntities, worldAABB, worldBounds); - - var screenSize = viewport.Size; - - ProcessSpriteEntities(mapId, viewport, eye, worldBounds, _drawingSpriteList); - var worldOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceEntities); - // We use a separate list for indexing so that the sort is faster. - var indexList = ArrayPool.Shared.Rent(_drawingSpriteList.Count); - - for (var i = 0; i < _drawingSpriteList.Count; i++) - { - indexList[i] = i; - } + GetSprites(mapId, viewport, eye, worldBounds, out var indexList); + var screenSize = viewport.Size; var overlayIndex = 0; - RenderTexture? entityPostRenderTarget = null; - Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList)); + RenderTexture? entityPostRenderTarget = null; bool flushed = false; for (var i = 0; i < _drawingSpriteList.Count; i++) { @@ -262,7 +243,7 @@ namespace Robust.Client.Graphics.Clyde { var overlay = worldOverlays[overlayIndex]; - if (overlay.ZIndex > entry.sprite.DrawDepth) + if (overlay.ZIndex > entry.Sprite.DrawDepth) { flushed = false; break; @@ -278,10 +259,10 @@ namespace Robust.Client.Graphics.Clyde } Vector2i roundedPos = default; - if (entry.sprite.PostShader != null) + if (entry.Sprite.PostShader != null) { // get the size of the sprite on screen, scaled slightly to allow for shaders that increase the final sprite size. - var screenSpriteSize = (Vector2i)(entry.spriteScreenBB.Size * PostShadeScale).Rounded(); + var screenSpriteSize = (Vector2i)(entry.SpriteScreenBB.Size * PostShadeScale).Rounded(); // I'm not 100% sure why it works, but without it post-shader // can be lower or upper by 1px than original sprite depending on sprite rotation or scale @@ -292,14 +273,14 @@ namespace Robust.Client.Graphics.Clyde screenSpriteSize.Y++; bool exit = false; - if (entry.sprite.GetScreenTexture) + if (entry.Sprite.GetScreenTexture) { FlushRenderQueue(); var tex = CopyScreenTexture(viewport.RenderTarget); if (tex == null) exit = true; else - entry.sprite.PostShader.SetParameter("SCREEN_TEXTURE", tex); + entry.Sprite.PostShader.SetParameter("SCREEN_TEXTURE", tex); } // check that sprite size is valid @@ -332,20 +313,20 @@ namespace Robust.Client.Graphics.Clyde // Calculate viewport so that the entity thinks it's drawing to the same position, // which is necessary for light application, // but it's ACTUALLY drawing into the center of the render target. - roundedPos = (Vector2i) entry.spriteScreenBB.Center; + roundedPos = (Vector2i) entry.SpriteScreenBB.Center; var flippedPos = new Vector2i(roundedPos.X, screenSize.Y - roundedPos.Y); flippedPos -= entityPostRenderTarget.Size / 2; _renderHandle.Viewport(Box2i.FromDimensions(-flippedPos, screenSize)); - if (entry.sprite.RaiseShaderEvent) - _entityManager.EventBus.RaiseLocalEvent(entry.sprite.Owner, - new BeforePostShaderRenderEvent(entry.sprite, viewport), false); + if (entry.Sprite.RaiseShaderEvent) + _entityManager.EventBus.RaiseLocalEvent(entry.Sprite.Owner, + new BeforePostShaderRenderEvent(entry.Sprite, viewport), false); } } - entry.sprite.Render(_renderHandle.DrawingHandleWorld, eye.Rotation, in entry.worldRotation, in entry.worldPos); + entry.Sprite.Render(_renderHandle.DrawingHandleWorld, eye.Rotation, in entry.WorldRot, in entry.WorldPos); - if (entry.sprite.PostShader != null && entityPostRenderTarget != null) + if (entry.Sprite.PostShader != null && entityPostRenderTarget != null) { var oldProj = _currentMatrixProj; var oldView = _currentMatrixView; @@ -353,7 +334,7 @@ namespace Robust.Client.Graphics.Clyde _renderHandle.UseRenderTarget(viewport.RenderTarget); _renderHandle.Viewport(Box2i.FromDimensions(Vector2i.Zero, screenSize)); - _renderHandle.UseShader(entry.sprite.PostShader); + _renderHandle.UseShader(entry.Sprite.PostShader); CalcScreenMatrices(viewport.Size, out var proj, out var view); _renderHandle.SetProjView(proj, view); _renderHandle.SetModelTransform(Matrix3.Identity); @@ -390,41 +371,6 @@ namespace Robust.Client.Graphics.Clyde FlushRenderQueue(); } - [MethodImpl(MethodImplOptions.NoInlining)] - private void ProcessSpriteEntities(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds, - RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRot, Box2 spriteScreenBB)> list) - { - var xforms = _entityManager.GetEntityQuery(); - var xformSys = _entityManager.EntitySysManager.GetEntitySystem(); - - // matrix equivalent for Viewport.WorldToLocal() - var worldToLocal = view.GetWorldToLocalMatrix(); - var spriteState = (list, xformSys, xforms, eye, worldToLocal); - - foreach (var comp in _entitySystemManager.GetEntitySystem().GetRenderTrees(map, worldBounds)) - { - var bounds = xforms.GetComponent(comp.Owner).InvWorldMatrix.TransformBox(worldBounds); - - comp.SpriteTree.QueryAabb(ref spriteState, static - (ref (RefList<(SpriteComponent sprite, Vector2 worldPos, Angle worldRot, Box2 spriteScreenBB)> list, - TransformSystem xformSys, - EntityQuery xforms, - IEye eye, - Matrix3 worldToLocal) state, - in ComponentTreeEntry value) => - { - ref var entry = ref state.list.AllocAdd(); - entry.sprite = value.Component; - (entry.worldPos, entry.worldRot) = state.xformSys.GetWorldPositionRotation(value.Transform, state.xforms); - - var spriteWorldBB = entry.sprite.CalculateRotatedBoundingBox(entry.worldPos, entry.worldRot, state.eye); - entry.spriteScreenBB = state.worldToLocal.TransformBox(spriteWorldBB); - return true; - - }, bounds, true); - } - } - private void DrawSplash(IRenderHandle handle) { // Clear screen to black for splash. diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs index a7ed1fd81..f47cba965 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs @@ -1008,45 +1008,6 @@ namespace Robust.Client.Graphics.Clyde PointList, } - private sealed class SpriteDrawingOrderComparer : IComparer - { - private readonly RefList<(SpriteComponent, Vector2, Angle, Box2)> _drawList; - - public SpriteDrawingOrderComparer(RefList<(SpriteComponent, Vector2, Angle, Box2)> drawList) - { - _drawList = drawList; - } - - public int Compare(int x, int y) - { - var a = _drawList[x]; - var b = _drawList[y]; - - var cmp = a.Item1.DrawDepth.CompareTo(b.Item1.DrawDepth); - if (cmp != 0) - { - return cmp; - } - - cmp = a.Item1.RenderOrder.CompareTo(b.Item1.RenderOrder); - - if (cmp != 0) - { - return cmp; - } - - // compare the top of the sprite's BB for y-sorting. Because screen coordinates are flipped, the "top" of the BB is actually the "bottom". - cmp = a.Item4.Top.CompareTo(b.Item4.Top); - - if (cmp != 0) - { - return cmp; - } - - return a.Item1.Owner.CompareTo(b.Item1.Owner); - } - } - private readonly struct FullStoredRendererState { public readonly Matrix3 ProjMatrix; diff --git a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs new file mode 100644 index 000000000..51396da3b --- /dev/null +++ b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs @@ -0,0 +1,264 @@ +using Robust.Client.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Threading; +using Robust.Shared.Utility; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Threading.Tasks; + +namespace Robust.Client.Graphics.Clyde; + +// this partial class contains code specific to querying, processing & sorting sprites. +internal partial class Clyde +{ + [Shared.IoC.Dependency] private readonly IParallelManager _parMan = default!; + private readonly RefList _drawingSpriteList = new(); + private const int _spriteProcessingBatchSize = 25; + + private void GetSprites(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds, out int[] indexList) + { + ProcessSpriteEntities(map, view, eye, worldBounds, _drawingSpriteList); + + // We use a separate list for indexing sprites so that the sort is faster. + indexList = ArrayPool.Shared.Rent(_drawingSpriteList.Count); + + // populate index list + for (var i = 0; i < _drawingSpriteList.Count; i++) + indexList[i] = i; + + // sort index list + // TODO better sorting? parallel merge sort? + Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList)); + } + + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ProcessSpriteEntities(MapId map, Viewport view, IEye eye, Box2Rotated worldBounds, RefList list) + { + var query = _entityManager.GetEntityQuery(); + var viewScale = eye.Scale * view.RenderScale * (EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter); + var treeData = new BatchData() + { + Sys = _entityManager.EntitySysManager.GetEntitySystem(), + Query = query, + ViewRotation = eye.Rotation, + ViewScale = viewScale, + PreScaleViewOffset = view.Size / 2f / viewScale, + ViewPosition = eye.Position.Position + eye.Offset + }; + + // We need to batch the actual tree query, or alternatively we need just get the list of sprites and then + // parallelize the rotation & bounding box calculations. + var index = 0; + var added = 0; + var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount }; + foreach (var comp in _entitySystemManager.GetEntitySystem().GetRenderTrees(map, worldBounds)) + { + var treeOwner = comp.Owner; + var treeXform = query.GetComponent(comp.Owner); + var bounds = treeXform.InvWorldMatrix.TransformBox(worldBounds); + DebugTools.Assert(treeXform.MapUid == treeXform.ParentUid || !treeXform.ParentUid.IsValid()); + + treeData = treeData with + { + TreeOwner = treeOwner, + TreePos = treeXform.LocalPosition, + TreeRot = treeXform.LocalRotation, + Sin = MathF.Sin((float)treeXform.LocalRotation), + Cos = MathF.Cos((float)treeXform.LocalRotation), + }; + + comp.SpriteTree.QueryAabb(ref list, + static (ref RefList state, in ComponentTreeEntry value) => + { + ref var entry = ref state.AllocAdd(); + entry.Sprite = value.Component; + entry.Xform = value.Transform; + return true; + }, bounds, true); + + // Get bounding boxes & world positions + added = list.Count - index; + var batches = added/_spriteProcessingBatchSize; + + // TODO also do sorting here & use a merge sort later on for y-sorting? + if (batches > 1) + Parallel.For(0, batches, opts, (i) => ProcessSprites(list, index + i * _spriteProcessingBatchSize, _spriteProcessingBatchSize, treeData)); + else + batches = 0; + + var remainder = added - _spriteProcessingBatchSize * batches; + if (remainder > 0) + ProcessSprites(list, index + batches * _spriteProcessingBatchSize, remainder, treeData); + + index += batches * _spriteProcessingBatchSize + remainder; + } + } + + /// + /// This function computes a sprites world position, rotation, and screen-space bounding box. The position & + /// rotation are required in general, but the bounding box is only really needed for y-sorting & if the + /// sprite has a post processing shader. + /// + private void ProcessSprites( + RefList list, + int startIndex, + int count, + in BatchData batch) + { + for (int i = startIndex; i < startIndex + count; i++) + { + ref var data = ref list[i]; + DebugTools.Assert(data.Sprite.Visible); + + // To help explain the remainder of this function, it should be functionally equivalent to the following + // three lines of code, but has been expanded & simplified to speed up the calculation: + // + // (data.WorldPos, data.WorldRot) = batch.Sys.GetWorldPositionRotation(data.Xform, batch.Query); + // var spriteWorldBB = data.Sprite.CalculateRotatedBoundingBox(data.WorldPos, data.WorldRot, batch.ViewRotation); + // data.SpriteScreenBB = Viewport.GetWorldToLocalMatrix().TransformBox(spriteWorldBB); + + var (pos, rot) = batch.Sys.GetParentRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query); + pos = new Vector2( + batch.TreePos.X + batch.Cos * pos.X - batch.Sin * pos.Y, + batch.TreePos.Y + batch.Sin * pos.X + batch.Cos * pos.Y); + + rot += batch.TreeRot; + data.WorldRot = rot; + data.WorldPos = pos; + + var finalRotation = (float) (data.Sprite.NoRotation + ? data.Sprite.Rotation + : data.Sprite.Rotation + rot + batch.ViewRotation); + + // false for 99.9% of sprites + if (data.Sprite.Offset != Vector2.Zero) + { + pos += data.Sprite.NoRotation + ? (-batch.ViewRotation).RotateVec(data.Sprite.Offset) + : rot.RotateVec(data.Sprite.Offset); + } + + pos = batch.ViewRotation.RotateVec(pos - batch.ViewPosition); + + // special casing angle = n*pi/2 to avoid box rotation & bounding calculations doesn't seem to give significant speedups. + data.SpriteScreenBB = TransformCenteredBox( + data.Sprite.Bounds, + finalRotation, + pos + batch.PreScaleViewOffset, + batch.ViewScale); + } + } + + /// + /// This is effectively a specialized variant of . It assumes that + /// the initial box is centered at the origin, and automatically applies an offset and scale while computing the + /// bounds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe Box2 TransformCenteredBox(in Box2 box, float angle, in Vector2 offset, in Vector2 scale) + { + // This function is for sprites, which flip the y axis, so here we flip the definition of t and b relative to the normal function. + DebugTools.Assert(scale.Y < 0); + DebugTools.Assert(box.Center.EqualsApprox(Vector2.Zero)); + + var boxVec = Unsafe.As>(ref Unsafe.AsRef(in box)); + + var sin = Vector128.Create(MathF.Sin(angle)); + var cos = Vector128.Create(MathF.Cos(angle)); + var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2)); + var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1)); + var modX = allX * cos - allY * sin; + var modY = allX * sin + allY * cos; + + var offsetVec = Unsafe.As>(ref Unsafe.AsRef(in offset)); // upper undefined + var scaleVec = Unsafe.As>(ref Unsafe.AsRef(in scale)); // upper undefined + offsetVec = Vector128.Shuffle(offsetVec, Vector128.Create(0, 1, 0, 1)); + scaleVec = Vector128.Shuffle(scaleVec, Vector128.Create(0, 1, 0, 1)); + + Vector128 lbrt; + if (Sse.IsSupported) + { + var lrlr = SimdHelpers.MinMaxHorizontalSse(modX); + var btbt = SimdHelpers.MaxMinHorizontalSse(modY); + lbrt = Sse.UnpackLow(lrlr, btbt); + } + else + { + var l = SimdHelpers.MinHorizontal128(allX); + var b = SimdHelpers.MaxHorizontal128(allY); + var r = SimdHelpers.MaxHorizontal128(allX); + var t = SimdHelpers.MinHorizontal128(allY); + lbrt = SimdHelpers.MergeRows128(l, b, r, t); + } + + // offset and scale box. + lbrt = (lbrt + offsetVec) * scaleVec; + + return Unsafe.As, Box2>(ref lbrt); + } + + private struct SpriteData + { + public SpriteComponent Sprite; + public TransformComponent Xform; + public Vector2 WorldPos; + public Angle WorldRot; + public Box2 SpriteScreenBB; + } + + private readonly struct BatchData + { + public TransformSystem Sys { get; init; } + public EntityQuery Query { get; init; } + public Angle ViewRotation { get; init; } + public Vector2 ViewScale { get; init; } + public Vector2 PreScaleViewOffset { get; init; } + public Vector2 ViewPosition { get; init; } + public EntityUid TreeOwner { get; init; } + public Vector2 TreePos { get; init; } + public Angle TreeRot { get; init; } + public float Sin { get; init; } + public float Cos { get; init; } + } + + private sealed class SpriteDrawingOrderComparer : IComparer + { + private readonly RefList _drawList; + + public SpriteDrawingOrderComparer(RefList drawList) + { + _drawList = drawList; + } + + public int Compare(int x, int y) + { + var a = _drawList[x]; + var b = _drawList[y]; + + var cmp = a.Sprite.DrawDepth.CompareTo(b.Sprite.DrawDepth); + if (cmp != 0) + return cmp; + + cmp = a.Sprite.RenderOrder.CompareTo(b.Sprite.RenderOrder); + + if (cmp != 0) + return cmp; + + // compare the top of the sprite's BB for y-sorting. Because screen coordinates are flipped, the "top" of the BB is actually the "bottom". + cmp = a.SpriteScreenBB.Top.CompareTo(b.SpriteScreenBB.Top); + + if (cmp != 0) + return cmp; + + return a.Sprite.Owner.CompareTo(b.Sprite.Owner); + } + } +} diff --git a/Robust.Shared.Maths/Angle.cs b/Robust.Shared.Maths/Angle.cs index 100406455..ac9f3d03e 100644 --- a/Robust.Shared.Maths/Angle.cs +++ b/Robust.Shared.Maths/Angle.cs @@ -104,13 +104,12 @@ namespace Robust.Shared.Maths // No calculation necessery when theta is zero if (Theta == 0) return vec; - var (x, y) = vec; - var cos = Math.Cos(Theta); - var sin = Math.Sin(Theta); - var dx = cos * x - sin * y; - var dy = sin * x + cos * y; + var cos = MathF.Cos((float)Theta); + var sin = MathF.Sin((float)Theta); + var dx = cos * vec.X - sin * vec.Y; + var dy = sin * vec.X + cos * vec.Y; - return new Vector2((float)dx, (float)dy); + return new Vector2(dx, dy); } public bool EqualsApprox(Angle other, double tolerance) diff --git a/Robust.Shared.Maths/Box2Rotated.cs b/Robust.Shared.Maths/Box2Rotated.cs index 5f02ed48a..0ac8147d3 100644 --- a/Robust.Shared.Maths/Box2Rotated.cs +++ b/Robust.Shared.Maths/Box2Rotated.cs @@ -80,12 +80,23 @@ namespace Robust.Shared.Maths allX = modX + originX; allY = modY + originY; - var l = SimdHelpers.MinHorizontal128(allX); - var b = SimdHelpers.MinHorizontal128(allY); - var r = SimdHelpers.MaxHorizontal128(allX); - var t = SimdHelpers.MaxHorizontal128(allY); + // lrlr = vector containing [left right left right] + Vector128 lbrt; - var lbrt = SimdHelpers.MergeRows128(l, b, r, t); + if (Sse.IsSupported) + { + var lrlr = SimdHelpers.MinMaxHorizontalSse(allX); + var btbt = SimdHelpers.MinMaxHorizontalSse(allY); + lbrt = Sse.UnpackLow(lrlr, btbt); + } + else + { + var l = SimdHelpers.MinHorizontal128(allX); + var b = SimdHelpers.MinHorizontal128(allY); + var r = SimdHelpers.MaxHorizontal128(allX); + var t = SimdHelpers.MaxHorizontal128(allY); + lbrt = SimdHelpers.MergeRows128(l, b, r, t); + } return Unsafe.As, Box2>(ref lbrt); } diff --git a/Robust.Shared.Maths/SimdHelpers.cs b/Robust.Shared.Maths/SimdHelpers.cs index 31f2d7b66..b89ca5d97 100644 --- a/Robust.Shared.Maths/SimdHelpers.cs +++ b/Robust.Shared.Maths/SimdHelpers.cs @@ -9,6 +9,34 @@ namespace Robust.Shared.Maths /// internal static class SimdHelpers { + /// A vector with the horizontal minimum and maximum values arranged as { min max min max} . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MinMaxHorizontalSse(Vector128 input) + { + var tmp = Sse.Shuffle(input, input, 0b00_01_10_11); + var min = Sse.Min(tmp, input); + var max = Sse.Max(tmp, input); + tmp = Sse.Shuffle(min, max, 0b01_00_00_01); + min = Sse.Min(tmp, min); + max = Sse.Max(tmp, max); + tmp = Sse.MoveScalar(max, min); // no generic Vector128 equivalent :( + return Sse.Shuffle(tmp, tmp, 0b11_00_11_00); + } + + /// A vector with the horizontal minimum and maximum values arranged as { max min max min} . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MaxMinHorizontalSse(Vector128 input) + { + var tmp = Sse.Shuffle(input, input, 0b00_01_10_11); + var min = Sse.Min(tmp, input); + var max = Sse.Max(tmp, input); + tmp = Sse.Shuffle(min, max, 0b01_00_00_01); + min = Sse.Min(tmp, min); + max = Sse.Max(tmp, max); + tmp = Sse.MoveScalar(max, min); // no generic Vector128 equivalent :( + return Sse.Shuffle(tmp, tmp, 0b00_11_00_11); + } + /// The min value is broadcast to the whole vector. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector128 MinHorizontal128(Vector128 v) diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 12b162a0e..ee79bdfcf 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -731,6 +731,39 @@ public abstract partial class SharedTransformSystem return component.GetWorldPositionRotation(xformQuery); } + /// + /// Returns the position and rotation relative to some entity higher up in the component's transform hierarchy. + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal (Vector2 Position, Angle Rotation) GetParentRelativePositionRotation( + TransformComponent component, + EntityUid relative, + EntityQuery query) + { + var rot = component._localRotation; + var pos = component._localPosition; + var xform = component; + while (xform.ParentUid != relative) + { + if (xform.ParentUid.IsValid() && query.TryGetComponent(xform.ParentUid, out xform)) + { + rot += xform._localRotation; + pos = xform._localRotation.RotateVec(pos) + xform._localPosition; + continue; + } + + // Entity was not actually in the transform hierarchy. This is probably a sign that something is wrong, or that the function is being misused. + Logger.Warning($"Target entity ({ToPrettyString(relative)}) not in transform hierarchy while calling {nameof(GetParentRelativePositionRotation)}."); + var relXform = query.GetComponent(relative); + pos = relXform.InvWorldMatrix.Transform(pos); + rot = rot - relXform.WorldRotation; + break; + } + + return (pos, rot); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetWorldPosition(EntityUid uid, Vector2 worldPos) { diff --git a/Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs b/Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs index b7ecd9caa..fae26a923 100644 --- a/Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs +++ b/Robust.UnitTesting/Shared/Maths/Box2Rotated_Test.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Runtime.Intrinsics.X86; using NUnit.Framework;