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;