Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,69 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "LottieMetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "LottieMetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.LottieMetalSources</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>LottieMetal</string>
"""
)
apple_resource_bundle(
name = "LottieMetalSourcesBundle",
infoplists = [
":LottieMetalSourcesBundleInfoPlist",
],
resources = [
":LottieMetalSources",
],
)
swift_library(
name = "LottieMetal",
module_name = "LottieMetal",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
data = [
":LottieMetalSourcesBundle",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
"//submodules/Display",
"//submodules/AnimatedStickerNode",
"//submodules/GZip",
"//submodules/MetalEngine",
"//submodules/LottieCpp",
"//submodules/Components/HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,871 @@
#include <metal_stdlib>
using namespace metal;
typedef struct
{
packed_float2 position;
packed_float2 texCoord;
} QuadVertex;
typedef struct
{
packed_float2 position;
} Vertex;
typedef struct
{
float4 position [[position]];
float2 texCoord;
float2 transformedPosition;
} QuadOut;
typedef struct
{
float4 position [[position]];
float direction;
} FillVertexOut;
float calculateNormalDirection(float2 a, float2 b, float2 c) {
float2 ab = b - a;
float2 ac = c - a;
return ab.x * ac.y - ab.y * ac.x;
}
vertex QuadOut quad_vertex_shader(
device QuadVertex const *vertices [[buffer(0)]],
uint vertexId [[vertex_id]],
device matrix<float, 4> const &transform [[buffer(1)]]
) {
QuadVertex in = vertices[vertexId];
QuadOut out;
float4 position = transform * float4(float2(in.position), 0.0, 1.0);
out.position = position;
out.texCoord = in.texCoord;
out.transformedPosition = (transform * float4(float2(in.position), 0.0, 1.0)).xy;
return out;
}
vertex FillVertexOut fill_vertex_shader(
device Vertex const *vertices [[buffer(0)]],
uint vertexId [[vertex_id]],
device matrix<float, 4> const &transform [[buffer(1)]],
device packed_float2 const &baseVertex [[buffer(2)]]
) {
FillVertexOut out;
uint triangleIndex = vertexId / 3;
uint vertexInTriangleIndex = vertexId % 3;
//[0, 1], [1, 2], [2, 3]...
//0, 1, 2
float2 sourcePosition;
float2 v1 = float2(vertices[triangleIndex].position);
float2 v2 = float2(vertices[triangleIndex + 1].position);
sourcePosition = select(
select(
v2,
v1,
vertexInTriangleIndex == 1
),
baseVertex,
vertexInTriangleIndex == 0
);
float normalDirection = calculateNormalDirection(baseVertex, v1, v2);
float4 position = transform * float4(sourcePosition, 0.0, 1.0);
out.position = position;
out.direction = sign(normalDirection);
return out;
}
struct ShapeOut {
half4 color [[color(1)]];
};
fragment ShapeOut fragment_shader(
FillVertexOut in [[stage_in]],
ShapeOut current,
device const int32_t &mode [[buffer(1)]]
) {
ShapeOut out = current;
if (mode == 0) {
half result = select(out.color.r, half(127.0 / 255.0), out.color.r == 0.0);
result += half(in.direction) * 3.0 / 255.0;
out.color.r = result;
} else {
out.color.r = out.color.r == 0.0 ? 1.0 : 0.0;
}
return out;
}
fragment ShapeOut clear_mask_fragment(
QuadOut in [[stage_in]]
) {
ShapeOut out;
out.color = half4(0.0);
return out;
}
struct ColorOut {
half4 color [[color(0)]];
};
fragment ColorOut merge_color_fill_fragment_shader(
ShapeOut colorIn,
device const float4 &color [[buffer(0)]],
device const int32_t &mode [[buffer(1)]]
) {
ColorOut out;
half4 sampledColor = half4(color);
sampledColor.r = sampledColor.r * sampledColor.a;
sampledColor.g = sampledColor.g * sampledColor.a;
sampledColor.b = sampledColor.b * sampledColor.a;
if (mode == 0) {
half diff = abs(colorIn.color.r - 127.0 / 255.0);
float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
float outColorFactor = select(
0.0,
diffSelect,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
} else {
float outColorFactor = select(
0.0,
1.0,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
}
if (out.color.a == 0.0) {
//discard_fragment();
}
return out;
}
typedef struct
{
packed_float4 color;
float location;
} GradientColorStop;
float linearGradientStep(float edge0, float edge1, float x) {
float t = clamp((x - edge0) / (edge1 - edge0), float(0), float(1));
return t;
}
fragment ColorOut merge_linear_gradient_fill_fragment_shader(
QuadOut quadIn [[stage_in]],
ShapeOut colorIn,
device const GradientColorStop *colorStops [[buffer(0)]],
device const int32_t &mode [[buffer(1)]],
device const uint &numColorStops [[buffer(2)]],
device const packed_float2 &localStartPosition [[buffer(3)]],
device const packed_float2 &localEndPosition [[buffer(4)]]
) {
ColorOut out;
float4 sourceColor;
if (numColorStops <= 1) {
sourceColor = colorStops[0].color;
} else {
float2 localPixelPosition = quadIn.transformedPosition.xy;
float2 gradientVector = normalize(localEndPosition - localStartPosition);
float2 pointVector = localPixelPosition - localStartPosition;
float pixelDistance = dot(pointVector, gradientVector) / dot(gradientVector, gradientVector);
float gradientLength = length(localEndPosition - localStartPosition);
float pixelValue = clamp(pixelDistance / gradientLength, 0.0, 1.0);
sourceColor = mix(colorStops[0].color, colorStops[1].color, linearGradientStep(
colorStops[0].location,
colorStops[1].location,
pixelValue
));
for (int i = 1; i < (int)numColorStops - 1; i++) {
sourceColor = mix(sourceColor, colorStops[i + 1].color, linearGradientStep(
colorStops[i].location,
colorStops[i + 1].location,
pixelValue
));
}
}
half4 sampledColor = half4(sourceColor);
sampledColor.r = sampledColor.r * sampledColor.a;
sampledColor.g = sampledColor.g * sampledColor.a;
sampledColor.b = sampledColor.b * sampledColor.a;
if (mode == 0) {
half diff = abs(colorIn.color.r - 127.0 / 255.0);
float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
float outColorFactor = select(
0.0,
diffSelect,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
} else {
float outColorFactor = select(
0.0,
1.0,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
}
if (out.color.a == 0.0) {
//discard_fragment();
}
return out;
}
fragment ColorOut merge_radial_gradient_fill_fragment_shader(
QuadOut quadIn [[stage_in]],
ShapeOut colorIn,
device const GradientColorStop *colorStops [[buffer(0)]],
device const int32_t &mode [[buffer(1)]],
device const uint &numColorStops [[buffer(2)]],
device const packed_float2 &localStartPosition [[buffer(3)]],
device const packed_float2 &localEndPosition [[buffer(4)]]
) {
ColorOut out;
float4 sourceColor;
if (numColorStops <= 1) {
sourceColor = colorStops[0].color;
} else {
float pixelDistance = distance(quadIn.transformedPosition.xy, localStartPosition);
float gradientLength = length(localEndPosition - localStartPosition);
float pixelValue = clamp(pixelDistance / gradientLength, 0.0, 1.0);
sourceColor = colorStops[0].color;
for (int i = 0; i < (int)numColorStops - 1; i++) {
float currentStopLocation = colorStops[i].location;
float nextStopLocation = colorStops[i + 1].location;
float4 nextStopColor = colorStops[i + 1].color;
sourceColor = mix(sourceColor, nextStopColor, linearGradientStep(
currentStopLocation,
nextStopLocation,
pixelValue
));
}
}
half4 sampledColor = half4(sourceColor);
sampledColor.r = sampledColor.r * sampledColor.a;
sampledColor.g = sampledColor.g * sampledColor.a;
sampledColor.b = sampledColor.b * sampledColor.a;
if (mode == 0) {
half diff = abs(colorIn.color.r - 127.0 / 255.0);
float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
float outColorFactor = select(
0.0,
diffSelect,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
} else {
float outColorFactor = select(
0.0,
1.0,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
}
if (out.color.a == 0.0) {
//discard_fragment();
}
return out;
}
typedef struct {
packed_float2 position;
} StrokePositionIn;
typedef struct {
packed_float2 point;
} StrokePointIn;
typedef struct {
float id;
} StrokeRoundJoinVertexIn;
typedef struct {
packed_float4 position;
} StrokeMiterJoinVertexIn;
typedef struct {
packed_float3 position;
} StrokeBevelJoinVertexIn;
typedef struct {
packed_float2 position;
} StrokeCapVertexIn;
typedef struct
{
float4 position [[position]];
} StrokeVertexOut;
fragment ColorOut stroke_fragment_shader(
StrokeVertexOut in [[stage_in]],
device const float4 &color [[buffer(0)]]
) {
ColorOut out;
half4 result = half4(color);
result.r *= result.a;
result.g *= result.a;
result.b *= result.a;
out.color = result;
return out;
}
typedef struct {
int32_t bufferOffset; // 4
packed_float2 start; // 4 * 2
packed_float2 end; // 4 * 2
packed_float2 cp1; // 4 * 2
packed_float2 cp2; // 4 * 2
float offset; // 4
} BezierInputItem;
kernel void evaluateBezier(
device BezierInputItem const *inputItems [[buffer(0)]],
device float *vertexData [[buffer(1)]],
device uint const &itemCount [[buffer(2)]],
uint2 index [[ thread_position_in_grid ]]
) {
if (index.x >= itemCount) {
return;
}
BezierInputItem item = inputItems[index.x];
float2 p0 = item.start;
float2 p1 = item.cp1;
float2 p2 = item.cp2;
float2 p3 = item.end;
float t = (((float)index.y) + 1.0) / (8.0);
float oneMinusT = 1.0 - t;
float2 value = oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3;
vertexData[item.bufferOffset + 2 * index.y] = value.x;
vertexData[item.bufferOffset + 2 * index.y + 1] = value.y;
}
fragment half4 quad_offscreen_fragment(
QuadOut in [[stage_in]],
texture2d<half, access::sample> texture[[texture(0)]],
device float const &opacity [[buffer(1)]]
) {
constexpr sampler s(address::clamp_to_edge, filter::linear);
half4 color = texture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
color *= half(opacity);
return color;
}
fragment half4 quad_offscreen_fragment_with_mask(
QuadOut in [[stage_in]],
texture2d<half, access::sample> texture[[texture(0)]],
texture2d<half, access::sample> maskTexture[[texture(1)]],
device float const &opacity [[buffer(1)]],
device uint const &maskMode [[buffer(2)]]
) {
constexpr sampler s(address::clamp_to_edge, filter::linear);
half4 color = texture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
half4 maskColor = maskTexture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
if (maskMode == 0) {
color *= maskColor.a;
} else {
color *= 1.0 - maskColor.a;
}
color *= half(opacity);
return color;
}
bool myIsNan(float val) {
return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;
}
bool isLinePointInvalid(float4 p) {
return p.w == 0.0 || myIsNan(p.x);
}
// Adapted from https://github.com/rreusser/regl-gpu-lines
vertex StrokeVertexOut strokeTerminalVertex(
uint instanceId [[instance_id]],
uint index [[vertex_id]],
device StrokePointIn const *points [[buffer(0)]],
device matrix<float, 4> const &transform [[buffer(1)]],
device packed_float2 const &_vertCnt2 [[buffer(2)]],
device packed_float2 const &_capJoinRes2 [[buffer(3)]],
device uint const &isJoinRound [[buffer(4)]],
device uint const &isCapRound [[buffer(5)]],
device float const &miterLimit [[buffer(6)]],
device float const &width [[buffer(7)]]
) {
const float2 ROUND_CAP_SCALE = float2(1.0, 1.0);
const float2 SQUARE_CAP_SCALE = float2(2.0, 2.0 / sqrt(3.0));
float2 _capScale = isCapRound ? ROUND_CAP_SCALE : SQUARE_CAP_SCALE;
const float pi = 3.141592653589793;
float2 xyB = points[instanceId * 3 + 0].point;
float2 xyC = points[instanceId * 3 + 1].point;
float2 xyD = points[instanceId * 3 + 2].point;
StrokeVertexOut out;
float4 pB = float4(xyB, 0.0, 1.0);
float4 pC = float4(xyC, 0.0, 1.0);
float4 pD = float4(xyD, 0.0, 1.0);
// A sensible default for early returns
out.position = pB;
bool aInvalid = false;
bool bInvalid = isLinePointInvalid(pB);
bool cInvalid = isLinePointInvalid(pC);
bool dInvalid = isLinePointInvalid(pD);
// Vertex count for each part (first half of join, second (mirrored) half). Note that not all of
// these vertices may be used, for example if we have enough for a round cap but only draw a miter
// join.
float2 v = _vertCnt2 + 3.0;
// Total vertex count
float N = dot(v, float2(1));
// If we're past the first half-join and half of the segment, then we swap all vertices and start
// over from the opposite end.
bool mirror = index >= v.x;
// When rendering dedicated endpoints, this allows us to insert an end cap *alone* (without the attached
// segment and join)
if (dInvalid && mirror) {
return out;
}
// Convert to screen-pixel coordinates
// Save w so we can perspective re-multiply at the end to get varyings depth-correct
float pw = mirror ? pC.w : pB.w;
pB = float4(float3(pB.xy, pB.z) / pB.w, 1);
pC = float4(float3(pC.xy, pC.z) / pC.w, 1);
pD = float4(float3(pD.xy, pD.z) / pD.w, 1);
// If it's a cap, mirror A back onto C to accomplish a round
float4 pA = pC;
// Reject if invalid or if outside viewing planes
if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {
return out;
}
// Swap everything computed so far if computing mirrored half
if (mirror) {
float4 vTmp = pC; pC = pB; pB = vTmp;
vTmp = pD; pD = pA; pA = vTmp;
bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;
}
bool isCap = !mirror;
// Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a
// degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.
if (aInvalid) { pA = 2.0 * pB - pC; }
if (dInvalid) { pD = 2.0 * pC - pB; }
bool roundOrCap = isJoinRound || isCap;
// Tangent and normal vectors
float2 tBC = pC.xy - pB.xy;
float lBC = length(tBC);
tBC /= lBC;
float2 nBC = float2(-tBC.y, tBC.x);
float2 tAB = pB.xy - pA.xy;
float lAB = length(tAB);
if (lAB > 0.0) tAB /= lAB;
float2 nAB = float2(-tAB.y, tAB.x);
float2 tCD = pD.xy - pC.xy;
float lCD = length(tCD);
if (lCD > 0.0) tCD /= lCD;
float2 nCD = float2(-tCD.y, tCD.x);
// Clamp for safety, since we take the arccos
float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);
// This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation
// of the middle segment. The fix appears straightforward, but this took a few hours to get right.
const float tol = 1e-4;
float mirrorSign = mirror ? -1.0 : 1.0;
float dirB = -dot(tBC, nAB);
float dirC = dot(tBC, nCD);
bool bCollinear = abs(dirB) < tol;
bool cCollinear = abs(dirC) < tol;
bool bIsHairpin = bCollinear && cosB < 0.0;
// bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;
dirB = bCollinear ? -mirrorSign : sign(dirB);
dirC = cCollinear ? -mirrorSign : sign(dirC);
float2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
// Compute our primary "join index", that is, the index starting at the very first point of the join.
// The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
float i = mirror ? N - index : index;
// Decide the resolution of whichever feature we're drawing. n is twice the number of points used since
// that's the only form in which we use this number.
float res = (isCap ? _capJoinRes2.x : _capJoinRes2.y);
// Shift the index to send unused vertices to an index below zero, which will then just get clamped to
// zero and result in repeated points, i.e. degenerate triangles.
i -= max(0.0, (mirror ? _vertCnt2.y : _vertCnt2.x) - res);
// Use the direction to offset the index by one. This has the effect of flipping the winding number so
// that it's always consistent no matter which direction the join turns.
i += (dirB < 0.0 ? -1.0 : 0.0);
// Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly
// in the middle, where the mirrored and unmirrored halves meet.
i -= mirror ? 1.0 : 0.0;
// Clamp to zero and repeat unused excess vertices.
i = max(0.0, i);
// Start with a default basis pointing along the segment with normal vector outward
float2 xBasis = tBC;
float2 yBasis = nBC * dirB;
// Default point is 0 along the segment, 1 (width unit) normal to it
float2 xy = float2(0);
if (i == res + 1.0) {
// pick off this one specific index to be the interior miter point
// If not div-by-zero, then sinB / (1 + cosB)
float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;
xy = float2(min(abs(m), min(lBC, lAB) / width), -1);
} else {
// Draw half of a join
float m2 = dot(miter, miter);
float lm = sqrt(m2);
yBasis = miter / lm;
xBasis = dirB * float2(yBasis.y, -yBasis.x);
bool isBevel = 1.0 > miterLimit * m2;
if (((int)i) % 2 == 0) {
// Outer joint points
if (roundOrCap || i != 0.0) {
// Round joins
float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);
xy = float2(cos(theta), sin(theta));
if (isCap) {
// A special multiplier factor for turning 3-point rounds into square caps (but leave the
// y == 0.0 point unaffected)
if (xy.y > 0.001) xy *= _capScale;
}
} else {
// Miter joins
yBasis = bIsHairpin ? float2(0) : miter;
xy.y = isBevel ? 1.0 : 1.0 / m2;
}
} else {
// Offset the center vertex position to get bevel SDF correct
if (isBevel && !roundOrCap) {
xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);
}
}
}
// Point offset from main vertex position
float2 dP = float2x2(xBasis, yBasis) * xy;
out.position = pB;
out.position.xy += width * dP;
out.position *= pw;
out.position = transform * out.position;
return out;
}
vertex StrokeVertexOut strokeInnerVertex(
uint instanceId [[instance_id]],
uint index [[vertex_id]],
device StrokePointIn const *points [[buffer(0)]],
device matrix<float, 4> const &transform [[buffer(1)]],
device packed_float2 const &_vertCnt2 [[buffer(2)]],
device packed_float2 const &_capJoinRes2 [[buffer(3)]],
device uint const &isJoinRound [[buffer(4)]],
device uint const &isCapRound [[buffer(5)]],
device float const &miterLimit [[buffer(6)]],
device float const &width [[buffer(7)]]
) {
const float2 ROUND_CAP_SCALE = float2(1.0, 1.0);
const float2 SQUARE_CAP_SCALE = float2(2.0, 2.0 / sqrt(3.0));
float2 _capScale = isCapRound ? ROUND_CAP_SCALE : SQUARE_CAP_SCALE;
const float pi = 3.141592653589793;
float2 xyA = points[instanceId + 0].point;
float2 xyB = points[instanceId + 1].point;
float2 xyC = points[instanceId + 2].point;
float2 xyD = points[instanceId + 3].point;
StrokeVertexOut out;
float4 pA = float4(xyA, 0.0, 1.0);
float4 pB = float4(xyB, 0.0, 1.0);
float4 pC = float4(xyC, 0.0, 1.0);
float4 pD = float4(xyD, 0.0, 1.0);
// A sensible default for early returns
out.position = pB;
bool aInvalid = isLinePointInvalid(pA);
bool bInvalid = isLinePointInvalid(pB);
bool cInvalid = isLinePointInvalid(pC);
bool dInvalid = isLinePointInvalid(pD);
// Vertex count for each part (first half of join, second (mirrored) half). Note that not all of
// these vertices may be used, for example if we have enough for a round cap but only draw a miter
// join.
float2 v = _vertCnt2 + 3.0;
// Total vertex count
float N = dot(v, float2(1));
// If we're past the first half-join and half of the segment, then we swap all vertices and start
// over from the opposite end.
bool mirror = index >= v.x;
// When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached
// segment and join)
// Convert to screen-pixel coordinates
// Save w so we can perspective re-multiply at the end to get varyings depth-correct
float pw = mirror ? pC.w : pB.w;
pA = float4(float3(pA.xy, pA.z) / pA.w, 1);
pB = float4(float3(pB.xy, pB.z) / pB.w, 1);
pC = float4(float3(pC.xy, pC.z) / pC.w, 1);
pD = float4(float3(pD.xy, pD.z) / pD.w, 1);
// If it's a cap, mirror A back onto C to accomplish a round
// Reject if invalid or if outside viewing planes
if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {
return out;
}
// Swap everything computed so far if computing mirrored half
if (mirror) {
float4 vTmp = pC; pC = pB; pB = vTmp;
vTmp = pD; pD = pA; pA = vTmp;
bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;
}
const bool isCap = false;
// Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a
// degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.
if (aInvalid) { pA = 2.0 * pB - pC; }
if (dInvalid) { pD = 2.0 * pC - pB; }
bool roundOrCap = isJoinRound || isCap;
// Tangent and normal vectors
float2 tBC = pC.xy - pB.xy;
float lBC = length(tBC);
tBC /= lBC;
float2 nBC = float2(-tBC.y, tBC.x);
float2 tAB = pB.xy - pA.xy;
float lAB = length(tAB);
if (lAB > 0.0) tAB /= lAB;
float2 nAB = float2(-tAB.y, tAB.x);
float2 tCD = pD.xy - pC.xy;
float lCD = length(tCD);
if (lCD > 0.0) tCD /= lCD;
float2 nCD = float2(-tCD.y, tCD.x);
// Clamp for safety, since we take the arccos
float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);
// This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation
// of the middle segment. The fix appears straightforward, but this took a few hours to get right.
const float tol = 1e-4;
float mirrorSign = mirror ? -1.0 : 1.0;
float dirB = -dot(tBC, nAB);
float dirC = dot(tBC, nCD);
bool bCollinear = abs(dirB) < tol;
bool cCollinear = abs(dirC) < tol;
bool bIsHairpin = bCollinear && cosB < 0.0;
// bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;
dirB = bCollinear ? -mirrorSign : sign(dirB);
dirC = cCollinear ? -mirrorSign : sign(dirC);
float2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
// Compute our primary "join index", that is, the index starting at the very first point of the join.
// The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
float i = mirror ? N - index : index;
// Decide the resolution of whichever feature we're drawing. n is twice the number of points used since
// that's the only form in which we use this number.
float res = (isCap ? _capJoinRes2.x : _capJoinRes2.y);
// Shift the index to send unused vertices to an index below zero, which will then just get clamped to
// zero and result in repeated points, i.e. degenerate triangles.
i -= max(0.0, (mirror ? _vertCnt2.y : _vertCnt2.x) - res);
// Use the direction to offset the index by one. This has the effect of flipping the winding number so
// that it's always consistent no matter which direction the join turns.
i += (dirB < 0.0 ? -1.0 : 0.0);
// Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly
// in the middle, where the mirrored and unmirrored halves meet.
i -= mirror ? 1.0 : 0.0;
// Clamp to zero and repeat unused excess vertices.
i = max(0.0, i);
// Start with a default basis pointing along the segment with normal vector outward
float2 xBasis = tBC;
float2 yBasis = nBC * dirB;
// Default point is 0 along the segment, 1 (width unit) normal to it
float2 xy = float2(0);
if (i == res + 1.0) {
// pick off this one specific index to be the interior miter point
// If not div-by-zero, then sinB / (1 + cosB)
float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;
xy = float2(min(abs(m), min(lBC, lAB) / width), -1);
} else {
// Draw half of a join
float m2 = dot(miter, miter);
float lm = sqrt(m2);
yBasis = miter / lm;
xBasis = dirB * float2(yBasis.y, -yBasis.x);
bool isBevel = 1.0 > miterLimit * m2;
if (((int)i) % 2 == 0) {
// Outer joint points
if (roundOrCap || i != 0.0) {
// Round joins
float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);
xy = float2(cos(theta), sin(theta));
if (isCap) {
// A special multiplier factor for turning 3-point rounds into square caps (but leave the
// y == 0.0 point unaffected)
if (xy.y > 0.001) xy *= _capScale;
}
} else {
// Miter joins
yBasis = bIsHairpin ? float2(0) : miter;
xy.y = isBevel ? 1.0 : 1.0 / m2;
}
} else {
// Offset the center vertex position to get bevel SDF correct
if (isBevel && !roundOrCap) {
xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);
}
}
}
// Point offset from main vertex position
float2 dP = float2x2(xBasis, yBasis) * xy;
// The varying generation code handles clamping, if needed
out.position = pB;
out.position.xy += width * dP;
out.position *= pw;
out.position = transform * out.position;
return out;
}
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
struct MetalEngineRectangle {
float2 origin;
float2 size;
};
struct MetalEngineQuadVertexOut {
float4 position [[position]];
float2 uv;
};
vertex MetalEngineQuadVertexOut blitVertex(
const device MetalEngineRectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
MetalEngineQuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = float2(quadVertex.x, 1.0 - quadVertex.y);
return out;
}
fragment half4 blitFragment(
MetalEngineQuadVertexOut in [[stage_in]],
texture2d<half> texture [[ texture(0) ]]
) {
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
half4 color = texture.sample(sampler, in.uv);
return half4(color.r, color.g, color.b, color.a);
}
@@ -0,0 +1,375 @@
import Foundation
import MetalKit
import LottieCpp
/*private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
final class PathFrameState {
struct RenderItem {
enum Content {
case fill(PathRenderFillState)
case stroke(PathRenderStrokeState)
case offscreen(surface: Surface, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?)
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer, canvasSize: CGSize) {
switch self {
case let .fill(fill):
fill.encode(context: context, encoder: encoder, buffer: buffer)
case let .stroke(stroke):
stroke.encode(context: context, encoder: encoder, buffer: buffer)
case let .offscreen(surface, rect, transform, opacity, mask):
surface.encode(context: context, encoder: encoder, canvasSize: canvasSize, rect: rect, transform: transform, opacity: opacity, mask: mask)
}
}
}
let content: Content
init(content: Content) {
self.content = content
}
}
final class MaskSurface {
enum Mode {
case regular
case inverse
}
let surface: Surface
let mode: Mode
init(surface: Surface, mode: Mode) {
self.surface = surface
self.mode = mode
}
}
final class Surface {
let width: Int
let height: Int
private let msaaSampleCount: Int
private var texture: MTLTexture?
private(set) var items: [RenderItem] = []
init(width: Int, height: Int, msaaSampleCount: Int) {
self.width = width
self.height = height
self.msaaSampleCount = msaaSampleCount
}
func add(fill: PathRenderFillState) {
self.items.append(RenderItem(content: .fill(fill)))
}
func add(stroke: PathRenderStrokeState) {
self.items.append(RenderItem(content: .stroke(stroke)))
}
func add(surface: Surface, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?) {
self.items.append(RenderItem(content: .offscreen(surface: surface, rect: rect, transform: transform, opacity: opacity, mask: mask)))
}
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, canvasSize: CGSize, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?) {
guard let texture = self.texture else {
print("Trying to encode offscreen blit pass, but no texture is present")
return
}
if mask != nil {
encoder.setRenderPipelineState(context.drawOffscreenWithMaskPipelineState)
} else {
encoder.setRenderPipelineState(context.drawOffscreenPipelineState)
}
let identityTransform = CATransform3DIdentity
var identityTransformMatrix = SIMD16<Float>(
Float(identityTransform.m11), Float(identityTransform.m12), Float(identityTransform.m13), Float(identityTransform.m14),
Float(identityTransform.m21), Float(identityTransform.m22), Float(identityTransform.m23), Float(identityTransform.m24),
Float(identityTransform.m31), Float(identityTransform.m32), Float(identityTransform.m33), Float(identityTransform.m34),
Float(identityTransform.m41), Float(identityTransform.m42), Float(identityTransform.m43), Float(identityTransform.m44)
)
let boundingBox = rect.applying(CATransform3DGetAffineTransform(transform))
var quadVertices: [SIMD4<Float>] = [
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.minY), 0.0, 0.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.maxY), 1.0, 1.0)
]
encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
encoder.setFragmentTexture(texture, index: 0)
if let mask {
guard let maskTexture = mask.surface.texture else {
print("Trying to encode offscreen blit pass, but no mask texture is present")
return
}
encoder.setFragmentTexture(maskTexture, index: 1)
}
var opacity = opacity
encoder.setFragmentBytes(&opacity, length: 4, index: 1)
if let mask {
var maskMode: UInt32 = mask.mode == .regular ? 0 : 1;
encoder.setFragmentBytes(&maskMode, length: 4, index: 2)
}
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
}
func offscreenTextureDescriptor() -> MTLTextureDescriptor {
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.width = self.width
textureDescriptor.height = self.height
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.storageMode = .private
textureDescriptor.usage = [.renderTarget, .shaderRead]
return textureDescriptor
}
func offscreenTempTextureDescriptor() -> MTLTextureDescriptor {
let tempTextureDescriptor = MTLTextureDescriptor()
tempTextureDescriptor.sampleCount = self.msaaSampleCount
if self.msaaSampleCount == 1 {
tempTextureDescriptor.textureType = .type2D
} else {
tempTextureDescriptor.textureType = .type2DMultisample
}
tempTextureDescriptor.width = self.width
tempTextureDescriptor.height = self.height
tempTextureDescriptor.pixelFormat = .bgra8Unorm
tempTextureDescriptor.storageMode = .private
tempTextureDescriptor.usage = [.renderTarget, .shaderRead]
return tempTextureDescriptor
}
func calculateOffscreenHeapMemorySize(device: MTLDevice) -> Int {
var result = 0
var sizeAndAlign = device.heapTextureSizeAndAlign(descriptor: self.offscreenTextureDescriptor())
result += sizeAndAlign.size
sizeAndAlign = device.heapTextureSizeAndAlign(descriptor: self.offscreenTempTextureDescriptor())
result += sizeAndAlign.size * 2
for item in self.items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
result += surface.calculateOffscreenHeapMemorySize(device: device)
if let mask {
result += mask.surface.calculateOffscreenHeapMemorySize(device: device)
}
}
}
return result
}
func encodeOffscreen(context: PathRenderContext, heap: MTLHeap, commandBuffer: MTLCommandBuffer, materializedBuffer: MTLBuffer, canvasSize: CGSize) {
guard let resultTexture = heap.makeTexture(descriptor: self.offscreenTextureDescriptor()) else {
return
}
for item in self.items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
if let mask {
mask.surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
}
self.texture = resultTexture
guard let offscreenTexture = heap.makeTexture(descriptor: self.offscreenTempTextureDescriptor()) else {
return
}
guard let tempTexture = heap.makeTexture(descriptor: self.offscreenTempTextureDescriptor()) else {
return
}
let offscreenRenderPassDescriptor = MTLRenderPassDescriptor()
if msaaSampleCount == 1 {
offscreenRenderPassDescriptor.colorAttachments[0].texture = resultTexture
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .store
} else {
offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .multisampleResolve
offscreenRenderPassDescriptor.colorAttachments[0].resolveTexture = resultTexture
}
offscreenRenderPassDescriptor.colorAttachments[0].loadAction = .clear
offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
offscreenRenderPassDescriptor.colorAttachments[1].texture = tempTexture
offscreenRenderPassDescriptor.colorAttachments[1].loadAction = .clear
offscreenRenderPassDescriptor.colorAttachments[1].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
offscreenRenderPassDescriptor.colorAttachments[1].storeAction = .dontCare
if self.msaaSampleCount == 4 {
offscreenRenderPassDescriptor.setSamplePositions([
MTLSamplePosition(x: 0.25, y: 0.25),
MTLSamplePosition(x: 0.75, y: 0.25),
MTLSamplePosition(x: 0.75, y: 0.75),
MTLSamplePosition(x: 0.25, y: 0.75)
])
}
guard let offscreenRenderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: offscreenRenderPassDescriptor) else {
return
}
for item in self.items {
item.content.encode(context: context, encoder: offscreenRenderEncoder, buffer: materializedBuffer, canvasSize: canvasSize)
}
offscreenRenderEncoder.endEncoding()
}
}
let msaaSampleCount: Int
let buffer: PathRenderBuffer
let bezierDataBuffer: PathRenderBuffer
private var surfaceStack: [Surface] = []
private var materializedBuffer: MTLBuffer?
private var materializedBezierIndexBuffer: MTLBuffer?
init(width: Int, height: Int, msaaSampleCount: Int, buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer) {
self.msaaSampleCount = msaaSampleCount
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.surfaceStack.append(Surface(width: width, height: height, msaaSampleCount: msaaSampleCount))
}
func pushOffscreen(width: Int, height: Int) {
self.surfaceStack.append(Surface(width: width, height: height, msaaSampleCount: self.msaaSampleCount))
}
func popOffscreen(rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface? = nil) {
self.surfaceStack[self.surfaceStack.count - 2].add(surface: self.surfaceStack[self.surfaceStack.count - 1], rect: rect, transform: transform, opacity: opacity, mask: mask)
self.surfaceStack.removeLast()
}
func popOffscreenMask(mode: MaskSurface.Mode) -> MaskSurface {
return MaskSurface(
surface: self.surfaceStack.removeLast(),
mode: mode
)
}
func add(fill: PathRenderFillState) {
self.surfaceStack.last!.add(fill: fill)
}
func add(stroke: PathRenderStrokeState) {
self.surfaceStack.last!.add(stroke: stroke)
}
func prepare(heap: MTLHeap) {
if self.buffer.length == 0 {
return
}
var bufferOptions: MTLResourceOptions = [.storageModeShared, .cpuCacheModeWriteCombined]
if #available(iOS 13.0, *) {
bufferOptions.insert(.hazardTrackingModeTracked)
}
guard let materializedBuffer = heap.makeBuffer(length: self.buffer.length, options: bufferOptions) else {
print("Could not create materialized buffer")
return
}
materializedBuffer.label = "materializedBuffer"
self.materializedBuffer = materializedBuffer
memcpy(materializedBuffer.contents(), self.buffer.memory, self.buffer.length)
if self.bezierDataBuffer.length != 0 {
guard let materializedBezierIndexBuffer = heap.makeBuffer(length: self.bezierDataBuffer.length, options: bufferOptions) else {
print("Could not create materialized bezier index buffer")
return
}
self.materializedBezierIndexBuffer = materializedBezierIndexBuffer
materializedBezierIndexBuffer.label = "materializedBezierIndexBuffer"
memcpy(materializedBezierIndexBuffer.contents(), self.bezierDataBuffer.memory, self.bezierDataBuffer.length)
}
}
func calculateOffscreenHeapMemorySize(device: MTLDevice) -> Int {
var result = 0
for item in self.surfaceStack[0].items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
result += surface.calculateOffscreenHeapMemorySize(device: device)
if let mask {
result += mask.surface.calculateOffscreenHeapMemorySize(device: device)
}
}
}
return result
}
func encodeOffscreen(context: PathRenderContext, heap: MTLHeap, commandBuffer: MTLCommandBuffer, canvasSize: CGSize) {
guard let materializedBuffer = self.materializedBuffer else {
return
}
assert(self.surfaceStack.count == 1)
for item in self.surfaceStack[0].items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
if let mask {
mask.surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
}
}
func encodeRender(context: PathRenderContext, encoder: MTLRenderCommandEncoder, canvasSize: CGSize) {
guard let materializedBuffer = self.materializedBuffer else {
return
}
assert(self.surfaceStack.count == 1)
for item in self.surfaceStack[0].items {
item.content.encode(context: context, encoder: encoder, buffer: materializedBuffer, canvasSize: canvasSize)
}
}
func encodeCompute(context: PathRenderContext, computeEncoder: MTLComputeCommandEncoder) {
guard let materializedBuffer = self.materializedBuffer, let materializedBezierIndexBuffer = self.materializedBezierIndexBuffer else {
return
}
let itemSize = 4 + 4 * 4 * 2 + 4
let itemCount = self.bezierDataBuffer.length / itemSize
computeEncoder.setComputePipelineState(context.prepareBezierPipelineState)
let threadGroupWidth = 16
let threadGroupHeight = 8
computeEncoder.useResource(materializedBuffer, usage: .write)
computeEncoder.setBuffer(materializedBezierIndexBuffer, offset: 0, index: 0)
computeEncoder.setBuffer(materializedBuffer, offset: 0, index: 1)
var itemCountSize: UInt32 = UInt32(itemCount)
computeEncoder.setBytes(&itemCountSize, length: 4, index: 2)
let dispatchSize = alignUp(size: itemCount, align: threadGroupWidth)
computeEncoder.dispatchThreadgroups(MTLSize(width: dispatchSize, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: threadGroupHeight, depth: 1))
}
}
*/
@@ -0,0 +1,77 @@
import Foundation
import MetalKit
import LottieCpp
final class PathRenderBuffer {
private(set) var memory: UnsafeMutableRawPointer
private(set) var capacity: Int = 8 * 1024 * 1024
private(set) var length: Int = 0
init() {
self.memory = malloc(self.capacity)!
}
func reset() {
self.length = 0
}
func append(bytes: UnsafeRawPointer, length: Int) {
assert(length % 4 == 0)
if self.length + length > self.capacity {
self.capacity = self.capacity * 2
preconditionFailure()
}
memcpy(self.memory.advanced(by: self.length), bytes, length)
self.length += length
}
func appendZero(count: Int) {
if self.length + length > self.capacity {
self.capacity = self.capacity * 2
preconditionFailure()
}
self.length += count
}
func append(float: Float) {
var value: Float = float
self.append(bytes: &value, length: 4)
}
func append(float2: SIMD2<Float>) {
var value: SIMD2<Float> = float2
self.append(bytes: &value, length: 4 * 2)
}
func append(float3: SIMD3<Float>) {
var value = float3.x
self.append(bytes: &value, length: 4)
value = float3.y
self.append(bytes: &value, length: 4)
value = float3.z
self.append(bytes: &value, length: 4)
}
func append(int: Int32) {
var value = int
self.append(bytes: &value, length: 4)
}
func appendBezierData(
bufferOffset: Int,
start: SIMD2<Float>,
end: SIMD2<Float>,
cp1: SIMD2<Float>,
cp2: SIMD2<Float>,
offset: Float
) {
self.append(int: Int32(bufferOffset))
self.append(float2: start)
self.append(float2: end)
self.append(float2: cp1)
self.append(float2: cp2)
self.append(float: offset)
}
}
@@ -0,0 +1,208 @@
import Foundation
import MetalKit
import LottieCpp
final class PathRenderContext {
let device: MTLDevice
let msaaSampleCount: Int
let prepareBezierPipelineState: MTLComputePipelineState
let shapePipelineState: MTLRenderPipelineState
let clearPipelineState: MTLRenderPipelineState
let mergeColorFillPipelineState: MTLRenderPipelineState
let mergeLinearGradientFillPipelineState: MTLRenderPipelineState
let mergeRadialGradientFillPipelineState: MTLRenderPipelineState
let strokeTerminalPipelineState: MTLRenderPipelineState
let strokeInnerPipelineState: MTLRenderPipelineState
let drawOffscreenPipelineState: MTLRenderPipelineState
let drawOffscreenWithMaskPipelineState: MTLRenderPipelineState
let maximumThreadGroupWidth: Int
init?(device: MTLDevice, msaaSampleCount: Int) {
self.device = device
self.msaaSampleCount = msaaSampleCount
self.maximumThreadGroupWidth = device.maxThreadsPerThreadgroup.width
guard let library = metalLibrary(device: device) else {
return nil
}
guard let quadVertexFunction = library.makeFunction(name: "quad_vertex_shader") else {
print("Unable to find vertex function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let shapeVertexFunction = library.makeFunction(name: "fill_vertex_shader") else {
print("Unable to find vertex function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let shapeFragmentFunction = library.makeFunction(name: "fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let clearFragmentFunction = library.makeFunction(name: "clear_mask_fragment") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let mergeColorFillFragmentFunction = library.makeFunction(name: "merge_color_fill_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let mergeLinearGradientFillFragmentFunction = library.makeFunction(name: "merge_linear_gradient_fill_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let mergeRadialGradientFillFragmentFunction = library.makeFunction(name: "merge_radial_gradient_fill_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let strokeFragmentFunction = library.makeFunction(name: "stroke_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let strokeTerminalVertexFunction = library.makeFunction(name: "strokeTerminalVertex") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let strokeInnerVertexFunction = library.makeFunction(name: "strokeInnerVertex") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let prepareBezierPipelineFunction = library.makeFunction(name: "evaluateBezier") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let quadOffscreenFragmentFunction = library.makeFunction(name: "quad_offscreen_fragment") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let quadOffscreenWithMaskFragmentFunction = library.makeFunction(name: "quad_offscreen_fragment_with_mask") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
self.prepareBezierPipelineState = try! device.makeComputePipelineState(function: prepareBezierPipelineFunction)
let shapePipelineDescriptor = MTLRenderPipelineDescriptor()
shapePipelineDescriptor.vertexFunction = shapeVertexFunction
shapePipelineDescriptor.fragmentFunction = shapeFragmentFunction
shapePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
shapePipelineDescriptor.colorAttachments[0].writeMask = []
shapePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
shapePipelineDescriptor.colorAttachments[1].writeMask = [.all]
shapePipelineDescriptor.rasterSampleCount = msaaSampleCount
guard let shapePipelineState = try? device.makeRenderPipelineState(descriptor: shapePipelineDescriptor) else {
preconditionFailure()
}
self.shapePipelineState = shapePipelineState
let clearPipelineDescriptor = MTLRenderPipelineDescriptor()
clearPipelineDescriptor.vertexFunction = quadVertexFunction
clearPipelineDescriptor.fragmentFunction = clearFragmentFunction
clearPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
clearPipelineDescriptor.colorAttachments[0].writeMask = []
clearPipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
clearPipelineDescriptor.colorAttachments[1].writeMask = .all
clearPipelineDescriptor.rasterSampleCount = msaaSampleCount
guard let clearPipelineState = try? device.makeRenderPipelineState(descriptor: clearPipelineDescriptor) else {
preconditionFailure()
}
self.clearPipelineState = clearPipelineState
let mergePipelineDescriptor = MTLRenderPipelineDescriptor()
mergePipelineDescriptor.vertexFunction = quadVertexFunction
mergePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
mergePipelineDescriptor.colorAttachments[0].writeMask = [.all]
mergePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
mergePipelineDescriptor.rasterSampleCount = msaaSampleCount
mergePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
mergePipelineDescriptor.colorAttachments[1].writeMask = []
mergePipelineDescriptor.fragmentFunction = mergeColorFillFragmentFunction
guard let mergeColorFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
preconditionFailure()
}
self.mergeColorFillPipelineState = mergeColorFillPipelineState
mergePipelineDescriptor.fragmentFunction = mergeLinearGradientFillFragmentFunction
guard let mergeLinearGradientFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
preconditionFailure()
}
self.mergeLinearGradientFillPipelineState = mergeLinearGradientFillPipelineState
mergePipelineDescriptor.fragmentFunction = mergeRadialGradientFillFragmentFunction
guard let mergeRadialGradientFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
preconditionFailure()
}
self.mergeRadialGradientFillPipelineState = mergeRadialGradientFillPipelineState
let strokePipelineDescriptor = MTLRenderPipelineDescriptor()
strokePipelineDescriptor.fragmentFunction = strokeFragmentFunction
strokePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
strokePipelineDescriptor.colorAttachments[0].writeMask = [.all]
strokePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
strokePipelineDescriptor.colorAttachments[0].rgbBlendOperation = mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation
strokePipelineDescriptor.colorAttachments[0].alphaBlendOperation = mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation
strokePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor
strokePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor
strokePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor
strokePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor
strokePipelineDescriptor.rasterSampleCount = msaaSampleCount
strokePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
strokePipelineDescriptor.colorAttachments[1].writeMask = []
strokePipelineDescriptor.vertexFunction = strokeTerminalVertexFunction
guard let strokeTerminalPipelineState = try? device.makeRenderPipelineState(descriptor: strokePipelineDescriptor) else {
preconditionFailure()
}
self.strokeTerminalPipelineState = strokeTerminalPipelineState
strokePipelineDescriptor.vertexFunction = strokeInnerVertexFunction
guard let strokeInnerPipelineState = try? device.makeRenderPipelineState(descriptor: strokePipelineDescriptor) else {
preconditionFailure()
}
self.strokeInnerPipelineState = strokeInnerPipelineState
let drawOffscreenPipelineDescriptor = MTLRenderPipelineDescriptor()
drawOffscreenPipelineDescriptor.vertexFunction = quadVertexFunction
drawOffscreenPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
drawOffscreenPipelineDescriptor.colorAttachments[0].writeMask = [.all]
drawOffscreenPipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
drawOffscreenPipelineDescriptor.colorAttachments[1].writeMask = []
drawOffscreenPipelineDescriptor.rasterSampleCount = msaaSampleCount
drawOffscreenPipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
drawOffscreenPipelineDescriptor.colorAttachments[0].rgbBlendOperation = mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation
drawOffscreenPipelineDescriptor.colorAttachments[0].alphaBlendOperation = mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation
drawOffscreenPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor
drawOffscreenPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor
drawOffscreenPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor
drawOffscreenPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor
drawOffscreenPipelineDescriptor.fragmentFunction = quadOffscreenFragmentFunction
guard let drawOffscreenPipelineState = try? device.makeRenderPipelineState(descriptor: drawOffscreenPipelineDescriptor) else {
preconditionFailure()
}
self.drawOffscreenPipelineState = drawOffscreenPipelineState
drawOffscreenPipelineDescriptor.fragmentFunction = quadOffscreenWithMaskFragmentFunction
guard let drawOffscreenWithMaskPipelineState = try? device.makeRenderPipelineState(descriptor: drawOffscreenPipelineDescriptor) else {
preconditionFailure()
}
self.drawOffscreenWithMaskPipelineState = drawOffscreenWithMaskPipelineState
}
}
@@ -0,0 +1,277 @@
import Foundation
import MetalKit
import simd
import LottieCpp
/*enum PathShading {
final class Gradient {
enum GradientType {
case linear
case radial
}
struct ColorStop {
var color: LottieColor
var location: Float
init(color: LottieColor, location: Float) {
self.color = color
self.location = location
}
}
let gradientType: GradientType
let colorStops: [ColorStop]
let start: SIMD2<Float>
let end: SIMD2<Float>
init(gradientType: GradientType, colorStops: [ColorStop], start: SIMD2<Float>, end: SIMD2<Float>) {
self.gradientType = gradientType
self.colorStops = colorStops
self.start = start
self.end = end
}
}
case color(LottieColor)
case gradient(Gradient)
}
final class PathRenderSubpathFillState {
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
let bufferOffset: Int
private(set) var vertexCount: Int = 0
private var firstPosition: SIMD2<Float>
private var lastPosition: SIMD2<Float>
private(set) var minPosition: SIMD2<Float>
private(set) var maxPosition: SIMD2<Float>
private var isClosed: Bool = false
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, point: SIMD2<Float>) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.bufferOffset = buffer.length
self.firstPosition = point
self.lastPosition = point
self.minPosition = point
self.maxPosition = point
self.add(point: point)
}
func add(point: SIMD2<Float>) {
self.buffer.append(float2: point)
self.minPosition.x = min(self.minPosition.x, point.x)
self.minPosition.y = min(self.minPosition.y, point.y)
self.maxPosition.x = max(self.maxPosition.x, point.x)
self.maxPosition.y = max(self.maxPosition.y, point.y)
self.lastPosition = point
self.vertexCount += 1
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
let stepCount = 8
self.bezierDataBuffer.appendBezierData(
bufferOffset: self.buffer.length / 4,
start: self.lastPosition,
end: point,
cp1: cp1,
cp2: cp2,
offset: 0.0
)
self.buffer.appendZero(count: 4 * 2 * stepCount)
self.vertexCount += stepCount
let (curveMin, curveMax) = bezierBounds(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point)
self.minPosition.x = min(self.minPosition.x, curveMin.x)
self.minPosition.y = min(self.minPosition.y, curveMin.y)
self.maxPosition.x = max(self.maxPosition.x, curveMax.x)
self.maxPosition.y = max(self.maxPosition.y, curveMax.y)
self.lastPosition = point
}
func close() {
if self.isClosed {
assert(false)
} else {
self.isClosed = true
if self.lastPosition != self.firstPosition {
self.add(point: self.firstPosition)
}
}
}
}
final class PathRenderFillState {
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
private let fillRule: LottieFillRule
private let shading: PathShading
private let transform: CATransform3D
private var currentSubpath: PathRenderSubpathFillState?
private(set) var subpaths: [PathRenderSubpathFillState] = []
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, fillRule: LottieFillRule, shading: PathShading, transform: CATransform3D) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.fillRule = fillRule
self.shading = shading
self.transform = transform
}
func begin(point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.close()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
self.currentSubpath = PathRenderSubpathFillState(buffer: self.buffer, bezierDataBuffer: self.bezierDataBuffer, point: point)
}
func addLine(to point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.add(point: point)
}
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.addCurve(to: point, cp1: cp1, cp2: cp2)
}
}
func close() {
if let currentSubpath = self.currentSubpath {
currentSubpath.close()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
}
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer) {
if self.subpaths.isEmpty {
return
}
var minPosition: SIMD2<Float> = self.subpaths[0].minPosition
var maxPosition: SIMD2<Float> = self.subpaths[0].maxPosition
for subpath in self.subpaths {
minPosition.x = min(minPosition.x, subpath.minPosition.x)
minPosition.y = min(minPosition.y, subpath.minPosition.y)
maxPosition.x = max(maxPosition.x, subpath.maxPosition.x)
maxPosition.y = max(maxPosition.y, subpath.maxPosition.y)
}
let localBoundingBox = CGRect(x: CGFloat(minPosition.x), y: CGFloat(minPosition.y), width: CGFloat(maxPosition.x - minPosition.x), height: CGFloat(maxPosition.y - minPosition.y))
if localBoundingBox.isEmpty {
return
}
var transformMatrix = simd_float4x4(
SIMD4<Float>(Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14)),
SIMD4<Float>(Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24)),
SIMD4<Float>(Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34)),
SIMD4<Float>(Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44))
)
let identityTransform = CATransform3DIdentity
var identityTransformMatrix = SIMD16<Float>(
Float(identityTransform.m11), Float(identityTransform.m12), Float(identityTransform.m13), Float(identityTransform.m14),
Float(identityTransform.m21), Float(identityTransform.m22), Float(identityTransform.m23), Float(identityTransform.m24),
Float(identityTransform.m31), Float(identityTransform.m32), Float(identityTransform.m33), Float(identityTransform.m34),
Float(identityTransform.m41), Float(identityTransform.m42), Float(identityTransform.m43), Float(identityTransform.m44)
)
let transform = CATransform3DGetAffineTransform(self.transform)
let boundingBox = localBoundingBox.applying(transform)
let baseVertex = boundingBox.origin.applying(transform.inverted())
encoder.setRenderPipelineState(context.clearPipelineState)
var quadVertices: [SIMD4<Float>] = [
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.minY), 0.0, 0.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.maxY), 1.0, 1.0)
]
encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
encoder.setRenderPipelineState(context.shapePipelineState)
encoder.setVertexBytes(&transformMatrix, length: 4 * 4 * 4, index: 1)
var baseVertexData = SIMD2<Float>(Float(baseVertex.x), Float(baseVertex.y))
encoder.setVertexBytes(&baseVertexData, length: 4 * 2, index: 2)
var modeBytes: Int32 = self.fillRule == .winding ? 0 : 1
encoder.setFragmentBytes(&modeBytes, length: 4, index: 1)
for subpath in self.subpaths {
encoder.setVertexBuffer(buffer, offset: subpath.bufferOffset, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: (subpath.vertexCount - 1) * 3)
}
encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
switch self.shading {
case let .color(color):
encoder.setRenderPipelineState(context.mergeColorFillPipelineState)
var colorVector = SIMD4(Float(color.r), Float(color.g), Float(color.b), Float(color.a))
encoder.setFragmentBytes(&colorVector, length: MemoryLayout<SIMD4<Float>>.size, index: 0)
case let .gradient(gradient):
switch gradient.gradientType {
case .linear:
encoder.setRenderPipelineState(context.mergeLinearGradientFillPipelineState)
case .radial:
encoder.setRenderPipelineState(context.mergeRadialGradientFillPipelineState)
}
var modeBytes: Int32 = self.fillRule == .winding ? 0 : 1
encoder.setFragmentBytes(&modeBytes, length: 4, index: 1)
let colorStopSize = 4 * 4 + 4
var colorStopsData = Data(count: colorStopSize * gradient.colorStops.count)
colorStopsData.withUnsafeMutableBytes { buffer in
let bytes = buffer.baseAddress!.assumingMemoryBound(to: Float.self)
for i in 0 ..< gradient.colorStops.count {
let colorStop = gradient.colorStops[i]
bytes[i * 5 + 0] = Float(colorStop.color.r)
bytes[i * 5 + 1] = Float(colorStop.color.g)
bytes[i * 5 + 2] = Float(colorStop.color.b)
bytes[i * 5 + 3] = Float(colorStop.color.a)
bytes[i * 5 + 4] = colorStop.location
}
encoder.setFragmentBytes(buffer.baseAddress!, length: buffer.count, index: 0)
}
var numColorStops: UInt32 = UInt32(gradient.colorStops.count)
encoder.setFragmentBytes(&numColorStops, length: 4, index: 2)
var startPosition = transformMatrix * SIMD4<Float>(gradient.start.x, gradient.start.y, 0.0, 1.0)
encoder.setFragmentBytes(&startPosition, length: 4 * 2, index: 3)
var endPosition = transformMatrix * SIMD4<Float>(gradient.end.x, gradient.end.y, 0.0, 1.0)
encoder.setFragmentBytes(&endPosition, length: 4 * 2, index: 4)
}
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
}
}
*/
@@ -0,0 +1,425 @@
import Foundation
import MetalKit
import LottieCpp
/*func evaluateBezier(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>, t: Float) -> SIMD2<Float> {
let t2 = t * t
let t3 = t * t * t
let A = (3 * t2 - 3 * t3)
let B = (3 * t3 - 6 * t2 + 3 * t)
let C = (3 * t2 - t3 - 3 * t + 1)
let value = t3 * p3 + A * p2 + B * p1 + C * p0
return value
}
func evaluateBezier(p0: Float, p1: Float, p2: Float, p3: Float, t: Float) -> Float {
let oneMinusT = 1.0 - t
let value = oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3
return value
}
func solveQuadratic(p0: Float, p1: Float, p2: Float, p3: Float) -> (Float, Float) {
let i = p1 - p0
let j = p2 - p1
let k = p3 - p2
let a = (3 * i) - (6 * j) + (3 * k)
let b = (6 * j) - (6 * i)
let c = (3 * i)
let sqrtPart = (b * b) - (4 * a * c)
let hasSolution = sqrtPart >= 0
if !hasSolution {
return (.nan, .nan)
}
let t1 = (-b + sqrt(sqrtPart)) / (2 * a)
let t2 = (-b - sqrt(sqrtPart)) / (2 * a)
var s1: Float = .nan
var s2: Float = .nan
if t1 >= 0.0 && t1 <= 1.0 {
s1 = evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t1)
}
if t2 >= 0.0 && t2 <= 1.0 {
s2 = evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t2)
}
return (s1, s2)
}
func bezierBounds(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>) -> (minPosition: SIMD2<Float>, maxPosition: SIMD2<Float>) {
let (solX1, solX2) = solveQuadratic(p0: p0.x, p1: p1.x, p2: p2.x, p3: p3.x)
let (solY1, solY2) = solveQuadratic(p0: p0.y, p1: p1.y, p2: p2.y, p3: p3.y)
var minX = min(p0.x, p3.x)
var maxX = max(p0.x, p3.x)
if !solX1.isNaN {
minX = min(minX, solX1)
maxX = max(maxX, solX1)
}
if !solX2.isNaN {
minX = min(minX, solX2)
maxX = max(maxX, solX2)
}
var minY = min(p0.y, p3.y)
var maxY = max(p0.y, p3.y)
if !solY1.isNaN {
minY = min(minY, solY1)
maxY = max(maxY, solY1)
}
if !solY2.isNaN {
minY = min(minY, solY2)
maxY = max(maxY, solY2)
}
return (SIMD2<Float>(minX, minY), SIMD2<Float>(maxX, maxY))
}
final class PathRenderSubpathStrokeState {
struct TerminalState {
var bufferOffset: Int
var segmentCount: Int
}
enum UnresolvedPosition {
case position(SIMD2<Float>)
case curve(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>, t: Float)
func resolve() -> SIMD2<Float> {
switch self {
case let .position(value):
return value
case let .curve(p0, p1, p2, p3, t):
return evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t)
}
}
}
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
let bufferOffset: Int
private(set) var vertexCount: Int = 0
private(set) var terminalState: TerminalState?
private(set) var curveJoinVertexRanges: [Range<Int>] = []
private var firstPosition: SIMD2<Float>
private var secondPosition: UnresolvedPosition
private var thirdPosition: UnresolvedPosition
private var lastPosition: SIMD2<Float>
private var lastMinus1Position: UnresolvedPosition
private var lastMinus2Position: UnresolvedPosition
private(set) var isClosed: Bool = false
private(set) var isCompleted: Bool = false
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, point: SIMD2<Float>) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.bufferOffset = buffer.length
self.firstPosition = point
self.secondPosition = .position(point)
self.thirdPosition = .position(point)
self.lastPosition = point
self.lastMinus1Position = .position(point)
self.lastMinus2Position = .position(point)
self.add(point: point)
}
func add(point: SIMD2<Float>) {
self.buffer.append(float2: point)
self.lastMinus2Position = self.lastMinus1Position
self.lastMinus1Position = .position(self.lastPosition)
self.lastPosition = point
self.vertexCount += 1
if self.vertexCount == 2 {
self.secondPosition = .position(point)
} else if self.vertexCount == 3 {
self.thirdPosition = .position(point)
}
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
let stepCount = 8
self.bezierDataBuffer.appendBezierData(
bufferOffset: self.buffer.length / 4,
start: self.lastPosition,
end: point,
cp1: cp1,
cp2: cp2,
offset: 0.0
)
self.buffer.appendZero(count: 4 * 2 * stepCount)
if self.vertexCount == 1 {
self.secondPosition = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(1) / Float(stepCount))
self.thirdPosition = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(2) / Float(stepCount))
}
self.vertexCount += stepCount
self.lastMinus2Position = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(stepCount - 2) / Float(stepCount))
self.lastMinus1Position = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(stepCount - 1) / Float(stepCount))
self.lastPosition = point
}
func close() {
if self.isClosed {
assert(false)
} else {
self.isClosed = true
if self.lastPosition != self.firstPosition {
self.add(point: self.firstPosition)
}
}
}
func complete() {
if self.isCompleted {
assert(false)
} else {
if self.isClosed {
if self.vertexCount >= 3 {
self.buffer.append(float2: self.secondPosition.resolve())
self.buffer.append(float2: self.thirdPosition.resolve())
self.vertexCount += 2
}
} else {
if self.vertexCount == 2 {
let terminalBufferOffset = self.buffer.length
let resolvedSecond = self.secondPosition.resolve()
self.buffer.append(float2: self.firstPosition)
self.buffer.append(float2: self.firstPosition * 0.5 + resolvedSecond * 0.5)
self.buffer.append(float2: resolvedSecond)
self.buffer.append(float2: resolvedSecond)
self.buffer.append(float2: self.firstPosition * 0.5 + resolvedSecond * 0.5)
self.buffer.append(float2: self.firstPosition)
self.terminalState = TerminalState(bufferOffset: terminalBufferOffset, segmentCount: 2)
} else if self.vertexCount >= 3 {
let terminalBufferOffset = self.buffer.length
self.buffer.append(float2: self.firstPosition)
self.buffer.append(float2: self.secondPosition.resolve())
self.buffer.append(float2: self.thirdPosition.resolve())
self.buffer.append(float2: self.lastPosition)
self.buffer.append(float2: self.lastMinus1Position.resolve())
self.buffer.append(float2: self.lastMinus2Position.resolve())
self.terminalState = TerminalState(bufferOffset: terminalBufferOffset, segmentCount: 2)
}
}
}
}
}
final class PathRenderStrokeState {
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
private let lineWidth: Float
private let lineJoin: CGLineJoin
private let lineCap: CGLineCap
private let miterLimit: Float
private let color: LottieColor
private let transform: CATransform3D
private var currentSubpath: PathRenderSubpathStrokeState?
private(set) var subpaths: [PathRenderSubpathStrokeState] = []
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, lineWidth: Float, lineJoin: CGLineJoin, lineCap: CGLineCap, miterLimit: Float, color: LottieColor, transform: CATransform3D) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.lineWidth = lineWidth
self.lineJoin = lineJoin
self.lineCap = lineCap
self.miterLimit = miterLimit
self.color = color
self.transform = transform
}
func begin(point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.complete()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
self.currentSubpath = PathRenderSubpathStrokeState(buffer: self.buffer, bezierDataBuffer: self.bezierDataBuffer, point: point)
}
func addLine(to point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.add(point: point)
}
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.addCurve(to: point, cp1: cp1, cp2: cp2)
}
}
func close() {
if let currentSubpath = self.currentSubpath {
currentSubpath.close()
currentSubpath.complete()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
}
func complete() {
if let currentSubpath = self.currentSubpath {
currentSubpath.complete()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
}
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer) {
if self.subpaths.isEmpty {
return
}
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
var colorVector = SIMD4(Float(color.r), Float(color.g), Float(color.b), Float(color.a))
encoder.setFragmentBytes(&colorVector, length: MemoryLayout<SIMD4<Float>>.size, index: 0)
var transformMatrix = SIMD16<Float>(
Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14),
Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24),
Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34),
Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44)
)
encoder.setVertexBytes(&transformMatrix, length: 4 * 4 * 4, index: 1)
let capRes2: Float
switch self.lineCap {
case .butt:
capRes2 = 2.0
case .square:
capRes2 = 6.0
case .round:
capRes2 = 24.0
@unknown default:
capRes2 = 2.0
}
let joinRes2: Float = self.lineJoin == .round ? 16.0 : 2.0
func computeCount(isEndpoints: Bool, insertCaps: Bool) -> SIMD2<Float> {
if insertCaps {
if isEndpoints {
return SIMD2<Float>(capRes2, max(capRes2, joinRes2))
} else {
return SIMD2<Float>(max(capRes2, joinRes2), max(capRes2, joinRes2))
}
} else {
if isEndpoints {
return SIMD2<Float>(capRes2, joinRes2)
} else {
return SIMD2<Float>(joinRes2, joinRes2)
}
}
}
var hasTerminalStates = false
for subpath in self.subpaths {
let segmentCount = subpath.vertexCount - 1
if segmentCount <= 0 {
continue
}
if subpath.vertexCount >= 4 {
encoder.setRenderPipelineState(context.strokeInnerPipelineState)
encoder.setVertexBufferOffset(subpath.bufferOffset, index: 0)
var vertCnt2 = computeCount(isEndpoints: false, insertCaps: false)
encoder.setVertexBytes(&vertCnt2, length: 4 * 2, index: 2)
var capJoinRes2 = SIMD2<Float>(capRes2, joinRes2)
encoder.setVertexBytes(&capJoinRes2, length: 4 * 2, index: 3)
var isRoundJoinValue: UInt32 = self.lineJoin == .round ? 1 : 0
encoder.setVertexBytes(&isRoundJoinValue, length: 4, index: 4)
var isRoundCapValue: UInt32 = self.lineCap == .round ? 1 : 0
encoder.setVertexBytes(&isRoundCapValue, length: 4, index: 5)
var miterLimitValue: Float = self.lineJoin == .miter ? self.miterLimit : 1.0
encoder.setVertexBytes(&miterLimitValue, length: 4, index: 6)
var lineWidthValue: Float = self.lineWidth * 0.5
encoder.setVertexBytes(&lineWidthValue, length: 4, index: 7)
let vertexCount = 6 + Int(vertCnt2.x) + Int(vertCnt2.y) + 2
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount, instanceCount: subpath.vertexCount - 4 + 1, baseInstance: 0)
}
if subpath.terminalState != nil {
hasTerminalStates = true
}
}
if hasTerminalStates {
encoder.setRenderPipelineState(context.strokeTerminalPipelineState)
for subpath in self.subpaths {
let segmentCount = subpath.vertexCount - 1
if segmentCount <= 0 {
continue
}
if !subpath.isClosed {
if let terminalState = subpath.terminalState {
encoder.setVertexBufferOffset(terminalState.bufferOffset, index: 0)
var vertCnt2 = computeCount(isEndpoints: true, insertCaps: false)
encoder.setVertexBytes(&vertCnt2, length: 4 * 2, index: 2)
var capJoinRes2 = SIMD2<Float>(capRes2, joinRes2)
encoder.setVertexBytes(&capJoinRes2, length: 4 * 2, index: 3)
var isRoundJoinValue: UInt32 = self.lineJoin == .round ? 1 : 0
encoder.setVertexBytes(&isRoundJoinValue, length: 4, index: 4)
var isRoundCapValue: UInt32 = self.lineCap == .round ? 1 : 0
encoder.setVertexBytes(&isRoundCapValue, length: 4, index: 5)
var miterLimitValue: Float = self.lineJoin == .miter ? self.miterLimit : 1.0
encoder.setVertexBytes(&miterLimitValue, length: 4, index: 6)
var lineWidthValue: Float = self.lineWidth * 0.5
encoder.setVertexBytes(&lineWidthValue, length: 4, index: 7)
let vertexCount = 6 + Int(vertCnt2.x) + Int(vertCnt2.y) + 2
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount, instanceCount: terminalState.segmentCount, baseInstance: 0)
}
}
}
}
}
}*/
@@ -0,0 +1,3 @@
import Foundation
import LottieCpp