blob: c39ef5eabb941a3796c4fd142865be3cf84fb011 [file]
/*
* Copyright 2026 Rive
*/
// Defined as a function because Naga doesn't accept
// "@DS_POLAR_STROKE || @DS_HAIRLINE_STROKE".
INLINE bool isStroke()
{
#ifdef @DS_POLAR_STROKE
if (@DS_POLAR_STROKE)
return true;
#endif
#ifdef @DS_HAIRLINE_STROKE
if (@DS_HAIRLINE_STROKE)
return true;
#endif
return false;
}
#ifdef @VERTEX
ATTR_BLOCK_BEGIN(Attrs)
// No attributes: everything comes from the vertex index.
ATTR_BLOCK_END
#endif
VARYING_BLOCK_BEGIN
NO_PERSPECTIVE VARYING(0, float4, v_paint);
#ifdef @ENABLE_ADVANCED_BLEND
@OPTIONALLY_FLAT VARYING(1, half, v_blendMode);
#endif
#ifdef @ENABLE_MODULATED_IMAGE
NO_PERSPECTIVE VARYING(2, float3, v_image);
#endif
VARYING_BLOCK_END
#ifdef @VERTEX
VERTEX_MAIN(@drawVertexMain, Attrs, attrs, _vertexIdx, _instanceIdx)
{
VARYING_INIT(v_paint, float4);
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_INIT(v_blendMode, half);
#endif
#ifdef @ENABLE_MODULATED_IMAGE
VARYING_INIT(v_image, float3);
#endif
#ifdef @DS_POLAR_STROKE
const bool isPolarStroke = @DS_POLAR_STROKE;
#else
const bool isPolarStroke = false;
#endif
#ifdef @DS_HAIRLINE_STROKE
const bool isHairlineStroke = @DS_HAIRLINE_STROKE;
#else
const bool isHairlineStroke = false;
#endif
// Since there are no actual vertex attrib buffers, Rive smuggles in flags
// via vertex ID. This proves to be a very efficient way of drawing
// different variations of things without changing pipeline state or push
// constants.
bool colorWriteDisabled = (_vertexIdx & VERTEX_FLAG_DISABLE_COLOR_WRITE) != 0;
bool isStrokeDepthPass = (_vertexIdx & VERTEX_FLAG_STROKE_DEPTH_PASS) != 0;
// NOTE: spirv-opt verifiably folds isOuterCubicPatch and isAAPolarStroke
// into a single value because VERTEX_FLAG_OUTER_CUBIC_FILL and
// VERTEX_FLAG_AA_POLAR_STROKE are equal aliases.
bool isOuterCubicPatch = (_vertexIdx & VERTEX_FLAG_OUTER_CUBIC_FILL) != 0;
bool isAAPolarStroke = (_vertexIdx & VERTEX_FLAG_AA_POLAR_STROKE) != 0;
int vertexIdxNoFlags = _vertexIdx & ((1 << VERTEX_FLAGS_SHIFT) - 1);
// The index buffers align per-patch vertex IDs on pow2 strides,
// specifically so this shader can decode _vertexIdx without divides and
// mods. (Using integer division costs 10% total frame perf on PowerVR and
// 3% on Adreno.)
// NOTE: The patches don't actually have pow2 numbers of vertices; the index
// buffers just never reference those vertex IDs between the end of one
// patch and the beginning of another.
int patchIdx, localVertexIdx, patchTessVertexStride;
float outset = .0;
float coverage = 1.;
bool isFillMidpointVertex = false;
if (isStroke())
{
int patchStrideLog2 = DS_STROKE_PATCH_STRIDE_LOG2(isAAPolarStroke);
patchIdx = vertexIdxNoFlags >> patchStrideLog2;
int patchVertexIdx = vertexIdxNoFlags & ((1 << patchStrideLog2) - 1);
int lanesPerSpokeLog2 = DS_STROKE_LANES_PER_SPOKE_LOG2(isAAPolarStroke);
localVertexIdx = patchVertexIdx >> lanesPerSpokeLog2; // spoke
int lane = patchVertexIdx & ((1 << lanesPerSpokeLog2) - 1);
// Lanes run outside to inside, so localVertexIdx is already correct for
// AA polar strokes and outer hairlines.
if (isPolarStroke && !isAAPolarStroke)
{
// Non-AA strokes only draw the solid body band (no AA bands).
// The body band is lanes 1 & 2.
++lane;
}
outset = lane < 2 ? 1. : -1.;
coverage = (lane == 0 || lane == 3) ? .0 : 1.;
patchTessVertexStride = int(MIDPOINT_FAN_PATCH_SEGMENT_SPAN);
}
else
{
int patchStrideLog2 = DS_FILL_PATCH_STRIDE_LOG2(isOuterCubicPatch);
patchIdx = vertexIdxNoFlags >> patchStrideLog2;
int patchVertexIdx = vertexIdxNoFlags & ((1 << patchStrideLog2) - 1);
patchTessVertexStride =
isOuterCubicPatch
// outerCubics tessellate a redundant bowtie vertex at the end
// of every patch (for AA), which is never referenced by the
// depthStencil fill index buffers. But we still need to stride
// over it when indexing into the tessellation texture.
? int(OUTER_CUBIC_PATCH_SEGMENT_SPAN_PLUS_BOWTIE)
: int(MIDPOINT_FAN_PATCH_SEGMENT_SPAN);
isFillMidpointVertex = !isOuterCubicPatch &&
patchVertexIdx == DS_MIDPOINT_VERTEX_IDX;
localVertexIdx = isFillMidpointVertex ? 0 : patchVertexIdx;
}
// Fetch a vertex that definitely belongs to the contour we're drawing.
int vertexIDOnContour = min(localVertexIdx, patchTessVertexStride - 1);
int tessVertexIdx = patchIdx * patchTessVertexStride + vertexIDOnContour;
uint4 tessVertexData = TEXEL_FETCH(
@tessVertexTexture,
tess_texel_coord(tessVertexIdx));
uint contourIDWithFlags = tessVertexData.w;
// Fetch and unpack the contour referenced by the tessellation vertex.
// NOTE: The contourID is guaranteed to be >= 1 at this point, but clamp it
// anyway because in the event of a bug, a buffer load at index "0u - 1" can
// be very serious and hard to catch.
uint contourID = max(contourIDWithFlags & CONTOUR_ID_MASK, 1u);
uint4 contourData = STORAGE_BUFFER_LOAD4(@contourBuffer, contourID - 1u);
float2 midpoint = uintBitsToFloat(contourData.xy);
uint pathID = contourData.z & 0xffffu;
uint vertexIndex0 = contourData.w;
// Fetch and unpack the path.
float2x2 M = make_float2x2(
uintBitsToFloat(STORAGE_BUFFER_LOAD4(@pathBuffer, pathID * 4u)));
uint4 pathData = STORAGE_BUFFER_LOAD4(@pathBuffer, pathID * 4u + 1u);
float2 translate = uintBitsToFloat(pathData.xy);
ushort pathZIndex = cast_uint_to_ushort(pathData.z >> 16u);
float strokeRadius = .0;
if (isPolarStroke)
{
strokeRadius = uintBitsToFloat(
STORAGE_BUFFER_LOAD4(@pathBuffer, pathID * 4u + 2u).x);
}
// Fix the tessellation vertex if we fetched the wrong one in order to
// guarantee we got the correct contour ID and flags, or if we belong to a
// mirrored contour and this vertex has an alternate position when mirrored.
// (Only midpoint-fan fan columns have one; the patch buffer gave outer
// cubics and the midpoint the same vertex either way. Strokes are never
// mirrored, so they only preserve the flag.)
uint mirroredContourFlag = contourIDWithFlags & MIRRORED_CONTOUR_CONTOUR_FLAG;
bool hasAlternateVertexPosition;
if (isPolarStroke)
{
hasAlternateVertexPosition = false;
}
else if (isHairlineStroke)
{
hasAlternateVertexPosition = mirroredContourFlag != 0u;
// A mirrored contour runs backwards, so its outside flips.
if (hasAlternateVertexPosition)
outset = -outset;
}
else
{
hasAlternateVertexPosition = mirroredContourFlag != 0u &&
!isOuterCubicPatch && !isFillMidpointVertex;
}
if (hasAlternateVertexPosition)
localVertexIdx = localVertexIdx - 1;
if (localVertexIdx != vertexIDOnContour)
{
// This can peek one vertex before or after the contour, but the
// tessellator guarantees there is always at least one padding vertex at
// the beginning and end of the data.
int replacementTessVertexIdx = tessVertexIdx + localVertexIdx -
vertexIDOnContour;
uint4 replacementTessVertexData = TEXEL_FETCH(
@tessVertexTexture,
tess_texel_coord(replacementTessVertexIdx));
if ((replacementTessVertexData.w &
(MIRRORED_CONTOUR_CONTOUR_FLAG | 0xffffu)) !=
(contourIDWithFlags & (MIRRORED_CONTOUR_CONTOUR_FLAG | 0xffffu)))
{
// We crossed over into a new contour. Fills are always closed and
// wrap to the first vertex in the contour; a stroke either wraps
// too or stays clamped at the final vertex of the contour.
//
// i.e., "bool isClosed = !isPolarStroke || (midpoint.x != 0.0);"
//
// Unfortunately, we can't word it the obvious way because Naga
// blows up if we emit any sort of operation on a specialization
// constant. Including "!isPolarStroke".
bool isClosed;
if (isPolarStroke)
isClosed = midpoint.x != .0;
else
isClosed = true;
if (isClosed)
{
tessVertexData = TEXEL_FETCH(
@tessVertexTexture,
tess_texel_coord(int(vertexIndex0)));
}
}
else
{
tessVertexData = replacementTessVertexData;
}
// MIRRORED_CONTOUR_CONTOUR_FLAG is not preserved at vertexIndex0.
// Preserve it here. By not preserving this flag, the normal and
// mirrored contour can both share the same contour record.
contourIDWithFlags = (tessVertexData.w & ~MIRRORED_CONTOUR_CONTOUR_FLAG) |
mirroredContourFlag;
}
float2 vertexPosition;
if (isStroke())
{
// Ensure strokes always emit clockwise triangles.
outset *= sign(determinant(M));
// Find the tangent angle of the curve at our vertex.
float theta = unpackTessTheta(tessVertexData.z);
float2 norm = float2(sin(theta), -cos(theta));
float2 origin = uintBitsToFloat(tessVertexData.xy);
float2 vertexOffset = norm;
float2 aaOutsetDir = norm;
// NOTE: 'outset' is only ever -1, 0, or +1.
float aaOutsetMagnitude = (coverage == .0) ? outset : .0;
if (isPolarStroke)
{
// Joins only emanate from the outer side of the stroke (except full
// round joins, which emit both sides like curves).
if ((contourIDWithFlags & LEFT_JOIN_CONTOUR_FLAG) != 0u)
outset = min(outset, .0);
if ((contourIDWithFlags & RIGHT_JOIN_CONTOUR_FLAG) != 0u)
outset = max(outset, .0);
uint joinType = contourIDWithFlags & JOIN_TYPE_MASK;
if (joinType > ROUND_JOIN_CONTOUR_FLAG)
{
bool isTan0 = (contourIDWithFlags & JOIN_TANGENT_0_CONTOUR_FLAG) != 0u;
bool isLeftJoin = (contourIDWithFlags & LEFT_JOIN_CONTOUR_FLAG) != 0u;
// This vertex belongs to a miter or bevel join. Begin by
// finding the bisector, which is the same as norm rotated by
// joinAngle/2. The tessellator already packed cos(joinAngle/2)
// (the miterRatio), so we use that.
float miterRatio = unpackTessMiterJoinRatio(tessVertexData.z);
// Trig identity to find sin(joinAngle/2).
// (miterRatio == cos(joinAngle/2).)
float sinJoinAngleOver2 = sqrt(max(1. - miterRatio * miterRatio, .0));
if (isTan0 == isLeftJoin)
sinJoinAngleOver2 = -sinJoinAngleOver2;
// Rotate norm by joinAngle/2 using a sin/cos rotation matrix.
float2x2 rot = float2x2(
miterRatio,
sinJoinAngleOver2,
-sinJoinAngleOver2,
miterRatio);
float2 bisector = MUL(rot, norm);
// Miter joins that are further away than 4x the stroke radius
// snap to bevel joins. A miter clip is a cap, and never snaps.
bool isBevel = joinType == BEVEL_JOIN_CONTOUR_FLAG ||
(joinType != MITER_CLIP_JOIN_CONTOUR_FLAG &&
miterRatio < .25);
// Bevels have 2 spokes and miters 3; a miter clip has 4,
// carrying an inner pair instead of a single center.
bool isInnerJoinSpoke = (contourIDWithFlags &
JOIN_TANGENT_INNER_CONTOUR_FLAG) != 0u;
if (joinType == MITER_CLIP_JOIN_CONTOUR_FLAG)
{
// For now, square caps are the only miter-clip: a
// 180-degree join whose miterLimit is 1. Simplify the
// miter-clip to only handle this specific case.
vertexOffset = norm + bisector;
}
else if (isInnerJoinSpoke || !isBevel)
{
// Fun little mathematical relationship: distance along the
// bisector to the bevel edge and to the miter corner turn
// out to be reciprocals of one another.
float t = isBevel ? miterRatio : 1. / miterRatio;
vertexOffset = bisector * t;
}
// Bevel spokes, the miter's center spoke, and the miter-clip's
// inner pair all outset their AA in the direction of the
// bisector.
if (isBevel || isInnerJoinSpoke)
aaOutsetDir = bisector;
// Rive always inkbleeds butt caps and bevel joins (partly to
// avoid hairline cracks and partly because it just makes the AA
// easier). This is not necessary for MSAA, but bleed out half a
// pixel anyway in MSAA so that strokes look the same in all
// modes.
if (!isAAPolarStroke && isBevel)
aaOutsetMagnitude = .5 * outset;
}
}
vertexPosition = MUL(M, origin + vertexOffset * (outset * strokeRadius)) +
translate;
if (aaOutsetMagnitude != .0)
{
// Push outward from the stroke edge by 'aaOutsetMagnitude' in both
// screen dimensions.
// Using sign() instead of normalize() makes the AA line offset in
// "Manhattan" distance instead of Euclidean, which is exactly what
// we want for square pixels.
vertexPosition += sign(MUL(aaOutsetDir, inverse(M))) * aaOutsetMagnitude;
}
}
else
{
// depthStencil fills never outset.
float2 origin = isFillMidpointVertex ? midpoint
: uintBitsToFloat(tessVertexData.xy);
vertexPosition = MUL(M, origin) + translate;
}
// Paint matrices operate on the fragment shader's "_fragCoord", which
// counts from memory row 0. A bottom up target needs it flipped into Rive
// pixel space.
float2 fragCoord = vertexPosition;
#ifdef @ENABLE_RENDER_TARGET_BOTTOM_UP
if (uniforms.renderTargetBottomUp != 0u)
{
fragCoord.y = float(uniforms.renderTargetHeight) - fragCoord.y;
}
#endif
#ifdef @ENABLE_CLIP_RECT
if (@ENABLE_CLIP_RECT)
{
// clipRectInverseMatrix transforms from pixel coordinates to a space
// where the clipRect is the normalized rectangle: [-1, -1, 1, 1].
float2x2 clipRectInverseMatrix = make_float2x2(STORAGE_BUFFER_LOAD4(
@paintAuxBuffer,
pathID * PAINT_AUX_ENTRY_ELEMENT_COUNT + 2u));
float4 clipRectInverseTranslate = STORAGE_BUFFER_LOAD4(
@paintAuxBuffer,
pathID * PAINT_AUX_ENTRY_ELEMENT_COUNT + 3u);
set_clip_rect_plane_distances(
clipRectInverseMatrix,
clipRectInverseTranslate.xy,
fragCoord CLIP_CONTEXT_UNPACK);
}
#endif // ENABLE_CLIP_RECT
if (colorWriteDisabled)
{
// No color writes. Skip the paint fetches and, in case the pipeline
// hasn't explicitly disabled color writes, output a fully transparent
// solid color.
v_paint = float4(.0, .0, .0, .0);
#ifdef @ENABLE_MODULATED_IMAGE
v_image = float3(0.0, 0.0, 0.0);
#endif
}
else
{
uint paintType = pathData.z & 0xfu;
bool paintHasAdvancedBlend = false;
#ifdef @ENABLE_ADVANCED_BLEND
if (@ENABLE_ADVANCED_BLEND)
{
uint blendMode = (pathData.z >> 4) & 0xfu;
v_blendMode = float(blendMode);
paintHasAdvancedBlend = blendMode != BLEND_SRC_OVER;
}
#endif
// Unpack the paint once we have a position.
if (paintType == SOLID_COLOR_PAINT_TYPE)
{
v_paint = unpackUnorm4x8(pathData.w);
if (paintHasAdvancedBlend)
{
// Advanced blends take an unmultiplied color, so coverage only
// belongs in alpha.
v_paint.a *= coverage;
}
else
{
v_paint *= coverage;
}
}
else // Gradient.
{
float2x2 paintMatrix = make_float2x2(STORAGE_BUFFER_LOAD4(
@paintAuxBuffer,
pathID * PAINT_AUX_ENTRY_ELEMENT_COUNT));
float4 paintTranslate = STORAGE_BUFFER_LOAD4(
@paintAuxBuffer,
pathID * PAINT_AUX_ENTRY_ELEMENT_COUNT + 1u);
float additivenessComplement = uintBitsToFloat(pathData.w);
v_paint = packGradientData(
fragCoord,
paintMatrix,
paintTranslate.xy,
paintTranslate.zw,
paintType,
additivenessComplement,
coverage);
}
#ifdef @ENABLE_MODULATED_IMAGE
if (@ENABLE_MODULATED_IMAGE && (pathData.z & PAINT_FLAG_HAS_IMAGE) != 0u)
{
float2x2 imageMatrix = make_float2x2(STORAGE_BUFFER_LOAD4(
@paintAuxBuffer,
pathID * PAINT_AUX_ENTRY_ELEMENT_COUNT + 4u));
float4 paintTranslateAndLOD = STORAGE_BUFFER_LOAD4(
@paintAuxBuffer,
pathID * PAINT_AUX_ENTRY_ELEMENT_COUNT + 5u);
float2 imageCoord = MUL(imageMatrix, fragCoord) + paintTranslateAndLOD.xy;
// Add 1 to the LOD because a z value of 0 means "we don't have an
// image"
float imageZ = 1. + paintTranslateAndLOD.z;
if ((pathData.z & PAINT_FLAG_LAYER_MASK) != 0u)
{
// A layer-mask apply. Same sentinel as draw_path.vert: a negative z
// with the mode packed into it (see the comment there).
uint mode = (pathData.z & PAINT_LAYER_MASK_MODE_MASK) >>
PAINT_LAYER_MASK_MODE_SHIFT;
imageZ = -(1. + float(mode));
}
v_image = float3(imageCoord.x, imageCoord.y, imageZ);
}
else
{
v_image = float3(0.0, 0.0, 0.0);
}
#endif
}
float4 pos = RENDER_TARGET_COORD_TO_CLIP_COORD(vertexPosition);
#ifdef @POST_INVERT_Y
pos.y = -pos.y;
#endif
uint depthCoverage8;
if (isStroke())
{
// Quantize depth coverage to 254 discrete values. This way, the color
// pass can be exactly 1 (8-bit) ULP larger, allowing exactly 1 fragment
// to pass. (The one with largest coverage.)
depthCoverage8 = uint(coverage * 254.);
if (!isStrokeDepthPass)
++depthCoverage8;
}
else
{
depthCoverage8 = 0xffu; // Fills always have full coverage.
}
pos.z = packNormalizedDepth(pathZIndex, depthCoverage8);
VARYING_PACK(v_paint);
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_PACK(v_blendMode);
#endif
#ifdef @ENABLE_MODULATED_IMAGE
VARYING_PACK(v_image);
#endif
EMIT_VERTEX(pos);
}
#endif // @VERTEX
#ifdef @FRAGMENT
#ifdef @RENDER_TO_UAV
PLS_MAIN(@drawFragmentMain)
#else
FRAG_DATA_MAIN(half4, @drawFragmentMain)
#endif
{
VARYING_UNPACK(v_paint, float4);
#ifdef @ENABLE_MODULATED_IMAGE
VARYING_UNPACK(v_image, float3);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
VARYING_UNPACK(v_blendMode, half);
#endif
#ifdef @ENABLE_ADVANCED_BLEND
ushort blendMode = cast_half_to_ushort(v_blendMode);
bool paintHasAdvancedBlend = @ENABLE_ADVANCED_BLEND &&
blendMode != BLEND_SRC_OVER;
#else
const bool paintHasAdvancedBlend = false;
#endif
// Unpack the paint color, with coverage modulated in.
half4 color;
if (v_paint.a >= .0) // Is the paint a solid color?
{
// v_paint was sent unmultiplied for advanced-blend draws and
// premultiplied otherwise, matching paintHasAdvancedBlend.
// v_paint is already pre-modulated by coverage.
color = cast_float4_to_half4(v_paint);
}
else // Paint is a gradient.
{
float2 gradTexCoord = getGradientUV(
v_paint,
uniforms.gradTextureYScale,
uniforms.gradTextureYBias);
color = TEXTURE_SAMPLE_LOD(@gradTexture, gradSampler, gradTexCoord, .0);
if (isStroke())
{
color.a *= getGradientCoverage(v_paint);
}
if (!paintHasAdvancedBlend)
{
// Premultipy the gradient if we aren't using advanced blend.
// (The gradient texture is always unmultiplied so that we don't
// lose color data while doing the hardware filter.)
color.rgb *= color.a;
// Only non-advanced blend supports additiveness.
color.a *= getGradientAdditivenessComplement(v_paint);
}
}
#ifdef @ENABLE_MODULATED_IMAGE
if (@ENABLE_MODULATED_IMAGE && v_image.z > 0.0)
{
half lod = v_image.z - 1.;
half4 imageColor = TEXTURE_SAMPLE_DYNAMIC_LOD(
@imageTexture,
imageSampler,
v_image.rg,
lod);
// Images are always premultiplied so the (transparent) background color
// doesn't bleed into the edges during the hardware filter; unmultiply
// to match this draw's convention if needed.
if (paintHasAdvancedBlend)
imageColor = make_half4(unmultiply_rgb(imageColor), imageColor.a);
color *= imageColor;
}
#endif
#if defined(@ENABLE_ADVANCED_BLEND) && !defined(@FIXED_FUNCTION_COLOR_OUTPUT)
// Do the color portion of the blend mode in the shader.
#ifdef @RENDER_TO_UAV
half4 dstColorPremul = PLS_LOAD4F_UAV(@dstColorUav);
#else
half4 dstColorPremul = DST_COLOR_FETCH(@dstColorTexture);
#endif
color.rgb = advanced_color_blend(color.rgb, dstColorPremul, blendMode);
// Premultiply before the hardware srcOver portion of the blend equation.
color.rgb *= color.a;
#endif
color.rgb = add_dither_if_alpha_nonzero(
color.rgb,
color.a,
_fragCoord.xy,
uniforms.ditherScale,
uniforms.ditherBias);
#ifdef @ENABLE_MODULATED_IMAGE
if (@ENABLE_MODULATED_IMAGE && v_image.z < .0)
{
// Layer mask. The texel is coverage, not color, and the pipeline's
// BlendEquation::layerMask computes dst * srcAlpha, so emitting the factor
// as alpha scales the target by it -- "change this layer's opacity, per
// pixel". Nothing above applies: this attenuates a color that is already
// quantized rather than producing a new one.
//
// The mask paint is opaque white, which the vertex shader premultiplied by
// coverage, so v_paint.a is this fragment's coverage (less than 1 only on
// an inkbleed hairline); the factor lerps back to 1, no change, with it.
half4 maskTexel = TEXTURE_SAMPLE_DYNAMIC_LOD(
@imageTexture,
imageSampler,
v_image.rg,
make_half(.0));
uint maskMode = uint(-v_image.z - 1.);
half f = layer_mask_factor(maskTexel, maskMode);
color = make_half4(mix(make_half(1.), f, cast_float_to_half(v_paint.a)));
}
#endif
#ifdef @RENDER_TO_UAV
if (color.a != .0)
{
PLS_STORE4F_UAV(@dstColorUav, blend_src_over(color, dstColorPremul));
}
#endif
#ifdef @RENDER_TO_UAV
EMIT_PLS
#else
EMIT_FRAG_DATA(color);
#endif
}
#endif // FRAGMENT