【问题标题】:Interfaces vs disciminated union for SVG curvesSVG曲线的接口与区分联合
【发布时间】:2018-12-22 14:26:58
【问题描述】:

对于可缩放矢量图形的 GPU 渲染器的预处理步骤之一,我正在处理 SVG 曲线(四种类型:直线、二次和三次贝塞尔曲线以及椭圆弧)。其中一个步骤是在交叉点上进行曲线细分,作为填充双连接边列表的前置算法。

以前,我将每种类型的曲线存储在一个单独的结构中(LineQuadraticBezierCubicBezierEllipticArc),这意味着,要对它们进行操作,我需要编写相同的(类似)这些组合中的每一个的代码(导致代码的高达 10 次)。现在,我想尝试一些不同的东西。我有两个选择:使用接口ICurve,或使用具有Typestruct Curve 并重新组合所有操作。

在曲线上运行的代码如下所示(禁止重复):

// Reunite all the shapes
var curves = new List<ICurve>(path.PathCommands.Length);

foreach (var cmd in path.PathCommands)
    /* generate the curves by evaluating the path commands */;

// Reunite all intersections to subdivide the curves
var curveRootSets = new SortedSet<float>[curves.Count];
for (int i = 0; i < curveRootSets.Length; i++)
    curveRootSets[i] = new SortedSet<float>(new[] { 1f }, Half.Comparer);

// Get all intersections
for (int i = 0; i < curves.Count; i++)
    for (int j = i+1; i < curves.Count; j++)
        foreach (var pair in CurveIntersectionPairs(curves[i], curves[j]))
        {
            curveRootSets[i].Add(pair.A);
            curveRootSets[j].Add(pair.B);
        }

// Account for possibly-duplicate curves
foreach (var set in curveRootSets) set.Remove(0f);

// Subdivide the curves
var curvesSubdiv = curves.Zip(curveRootSets, delegate (ICurve curve, SortedSet<float> set)
{
    float v = 0f;
    var list = new List<ICurve>();
    foreach (var l in set)
    {
        list.Add(curve.Subcurve(v, l));
        v = l;
    }
    return list;
}).Aggregate(Enumerable.Empty<ICurve>(), Enumerable.Concat);

我采用的第一种方法是定义一个interface ICurve 和必要的合同,以允许它在细分阶段和 DCEL 阶段都工作。界面如下所示:

public interface ICurve
{
    // Evaluates the curve at parameter "t"
    Vector2 At(float t);

    // The derivative of the curve (aka the "velocity curve")
    ICurve Derivative { get; }

    // Gets the curve that maps [0,1] to [l,r] on this curve
    ICurve Subcurve(float l, float r);

    // The bounding box of the curve
    FRectangle BoundingBox { get; }

    // The measure of how much counterclockwise the curve is
    // (aka double the area swept by it and the segments that
    // connect the endpoints to the origin)
    float Winding { get; }

    // Does the curve degenerate to a single point?
    bool IsDegenerate { get; }
}

然后我会让每个结构实现接口:

public struct Line : ICurve { /* ... */ }
public struct QuadraticBezier : ICurve { /* ... */ }
public struct CubicBezier : ICurve { /* ... */ }
public struct EllipticArc : ICurve { /* ... */ }

这很神奇,每个类都正确实现了契约方法,因此我可以抽象地使用ICurves,甚至可以根据 SVG 规范的要求实现更多曲线。但是我读过一些关于接口装箱/拆箱的恐怖故事(因为它们是引用类型)以及在堆上分配的结构,这些结构将分散在内存中。我想我可以通过设置struct Curvepublic readonly CurveType Type 以及所有必要的字段来变得更好(关于缓存一致性、间接性和其他方式):

public enum CurveType { Line, QuadraticBezier, CubicBezier, EllipticArc }

public partial struct Curve
{
    // four Vector2's are sufficient for now
    readonly Vector2 A, B, C, D;

    public readonly CurveType Type;

    // Evaluates the curve at parameter "t"
    public Vector2 At(float t)
    {
        // "Dispatch" the function based on the type
        switch (Type)
        {
            case CurveType.Line: return LineAt(t);
            case CurveType.QuadraticBezier: return QuadraticBezierAt(t);
            case CurveType.CubicBezier: return CubicBezierAt(t);
            case CurveType.EllipticArc: return EllipticArcAt(t);
            default: return new Vector2(float.NaN, float.NaN);
        }
    }

    // Other contract methods implemented similarly ...
}

由于structs 是值类型,我可以使用List&lt;Curve&gt;(在内部使用数组)在本地分配它们,并从缓存局部性中受益(至少一点)。但是,我会失去“开放继承”的能力(我认为这不是问题,因为在今天的 SVG 上使用的曲线很少)。

但也许我过度设计了它?我是 C# 的新手(使用 C++ 已经有几年了),我不知道接口会给值类型带来的复杂性,也许接口提供的动态调度会比“手动调度”更快。因此,我想询问您对这些具体方法的了解。也许还有另一个我不知道的更好的选择?

请记住,我正在准备将其用于处理真实世界的 SVG 文件,这意味着每条路径可能有数十或数百条曲线,并且之后必须对其进行细分。当我开始生成包含每条曲线的基元、分配填充和 SVG 的其他细微差别时,我可以重用这些结构。

【问题讨论】:

  • 如果你寻求表演,使用固定大小的数组而不是List,没有foreach而是常规for。避免使用linq,而是制作数组副本。您的结构大小是否超过 16 个字节?如果是,您将失去堆叠优势。 (stackoverflow.com/questions/1082311/…)

标签: c#


【解决方案1】:

问题在于试图利用值类型来利用类型差异。这在 C# 中是一个很大的不,不;你不能这样做。遗憾的是,没有办法拥有List&lt;T&gt;,其中T 是一个可以接受所有不同值类型的结构。 c# 中的类型变化必须保持同一性,也就是说,根据定义,与值类型不兼容。

显而易见的第一个选项是您已经想出的:ICurve,但这会导致拳击处罚可能非常昂贵。

第二个选项实际上是让你的曲线引用类型,但这可能会给 GC 带来很大的收集压力。

第三个选项是实现单个结构Curve,并在创建时定义它真正属于其状态的类型。这可能是更好的选择,您已经确定这是一个可行的选择。

【讨论】:

  • 有时我会想念 C++ 的联合和内存管理功能。最好的方法是使用所有类型的 union
  • 我想我在帖子中说明了您的第三个选项。 struct Curveenum CurveType.
  • @JoaoBapt 是的,这似乎是最好的选择,但测试实际上是真正找出答案的方法。 #2 是一个选项,只有测试才能告诉您收集压力是否存在问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-05
  • 2022-01-13
  • 2015-10-20
  • 1970-01-01
  • 2014-04-20
  • 1970-01-01
相关资源
最近更新 更多