mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
267 lines
11 KiB
C#
267 lines
11 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.Intrinsics;
|
|
using System.Runtime.Intrinsics.X86;
|
|
using System.Threading.Tasks;
|
|
using Robust.Client.ComponentTrees;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.Graphics;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Threading;
|
|
using Robust.Shared.Utility;
|
|
|
|
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<SpriteData> _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<int>.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<SpriteData> list)
|
|
{
|
|
var query = _entityManager.GetEntityQuery<TransformComponent>();
|
|
var viewScale = eye.Scale * view.RenderScale * new Vector2(EyeManager.PixelsPerMeter, -EyeManager.PixelsPerMeter);
|
|
var treeData = new BatchData()
|
|
{
|
|
Sys = _entityManager.EntitySysManager.GetEntitySystem<TransformSystem>(),
|
|
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 };
|
|
var xformSystem = _entitySystemManager.GetEntitySystem<SharedTransformSystem>();
|
|
|
|
foreach (var (treeOwner, comp) in _entitySystemManager.GetEntitySystem<SpriteTreeSystem>().GetIntersectingTrees(map, worldBounds))
|
|
{
|
|
var treeXform = query.GetComponent(treeOwner);
|
|
var bounds = xformSystem.GetInvWorldMatrix(treeOwner).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.Tree.QueryAabb(ref list,
|
|
static (ref RefList<SpriteData> state, in ComponentTreeEntry<SpriteComponent> value) =>
|
|
{
|
|
ref var entry = ref state.AllocAdd();
|
|
entry.Uid = value.Uid;
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void ProcessSprites(
|
|
RefList<SpriteData> 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.GetRelativePositionRotation(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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is effectively a specialized combination of a <see cref="Matrix3.TransformBox(in Box2Rotated)"/> and <see cref="Box2Rotated.CalcBoundingBox()"/>.
|
|
/// </summary>
|
|
[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);
|
|
|
|
var boxVec = Unsafe.As<Box2, Vector128<float>>(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<Vector2, Vector128<float>>(ref Unsafe.AsRef(in offset)); // upper undefined
|
|
var scaleVec = Unsafe.As<Vector2, Vector128<float>>(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<float> 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<Vector128<float>, Box2>(ref lbrt);
|
|
}
|
|
|
|
private struct SpriteData
|
|
{
|
|
public EntityUid Uid;
|
|
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<TransformComponent> 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<int>
|
|
{
|
|
private readonly RefList<SpriteData> _drawList;
|
|
|
|
public SpriteDrawingOrderComparer(RefList<SpriteData> 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.Uid.CompareTo(b.Uid);
|
|
}
|
|
}
|
|
}
|