1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.ink.rendering.android.canvas.internal
18 
19 import android.graphics.BlendMode
20 import android.graphics.Canvas
21 import android.graphics.ColorSpace as AndroidColorSpace
22 import android.graphics.Matrix
23 import android.graphics.Mesh as AndroidMesh
24 import android.graphics.MeshSpecification
25 import android.graphics.Paint
26 import android.graphics.RectF
27 import android.os.Build
28 import android.os.SystemClock
29 import androidx.annotation.RequiresApi
30 import androidx.annotation.Size
31 import androidx.annotation.VisibleForTesting
32 import androidx.collection.MutableObjectLongMap
33 import androidx.ink.brush.BrushPaint
34 import androidx.ink.brush.ExperimentalInkCustomBrushApi
35 import androidx.ink.brush.color.Color as ComposeColor
36 import androidx.ink.brush.color.colorspace.ColorSpaces as ComposeColorSpaces
37 import androidx.ink.geometry.AffineTransform
38 import androidx.ink.geometry.BoxAccumulator
39 import androidx.ink.geometry.Mesh as InkMesh
40 import androidx.ink.geometry.MeshAttributeUnpackingParams
41 import androidx.ink.geometry.MeshFormat
42 import androidx.ink.geometry.populateMatrix
43 import androidx.ink.nativeloader.NativeLoader
44 import androidx.ink.nativeloader.UsedByNative
45 import androidx.ink.rendering.android.TextureBitmapStore
46 import androidx.ink.rendering.android.canvas.CanvasStrokeRenderer
47 import androidx.ink.strokes.InProgressStroke
48 import androidx.ink.strokes.Stroke
49 import androidx.ink.strokes.StrokeInput
50 import java.util.WeakHashMap
51 
52 /**
53  * Renders Ink objects using [Canvas.drawMesh]. This is the most fully-featured and performant
54  * [Canvas] Ink renderer.
55  *
56  * This is not thread safe, so if it must be used from multiple threads, the caller is responsible
57  * for synchronizing access. If it is being used in two very different contexts where there are
58  * unlikely to be cached mesh data in common, the easiest solution to thread safety is to have two
59  * different instances of this object.
60  */
61 @OptIn(ExperimentalInkCustomBrushApi::class)
62 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
63 internal class CanvasMeshRenderer(
64     textureStore: TextureBitmapStore = TextureBitmapStore { null },
65     /** Monotonic time with a non-epoch zero time. */
66     private val getDurationTimeMillis: () -> Long = SystemClock::elapsedRealtime,
67 ) : CanvasStrokeRenderer {
68 
69     /** Caches [Paint] objects so these can be reused between strokes with the same [BrushPaint]. */
70     private val paintCache = BrushPaintCache(textureStore)
71 
72     /**
73      * Caches [android.graphics.Mesh] instances so that they can be reused between calls to
74      * [Canvas.drawMesh], greatly improving performance.
75      *
76      * On Android U, a bug in [android.graphics.Mesh] uniform handling causes a mesh rendered twice
77      * with different uniform values to overwrite the first draw's uniform values with the second
78      * draw's values. Therefore, [MeshData] tracks the most recent uniform data that a mesh has been
79      * drawn with, and if the next draw differs from the previous draw, a new
80      * [android.graphics.Mesh] will be created to satisfy it. This allows the typical use case,
81      * where a stroke is drawn the same way frame to frame, to remain fast and reuse cached
82      * [android.graphics.Mesh] instances. But less typical use cases, like animations that change
83      * the color or scale/rotation of strokes, will still work but will be a little slower.
84      *
85      * On Android V+, this bug has been fixed, so the same [android.graphics.Mesh] can be reused
86      * with different uniform values even within the same frame. Therefore, the extra data in
87      * [MeshData] is ignored, and it will just contain the values that were first used when
88      * rendering the associated [InkMesh].
89      */
90     private val inkMeshToAndroidMesh = WeakHashMap<InkMesh, MeshData>()
91 
92     /**
93      * On Android U, this holds strong references to [android.graphics.Mesh] instances that were
94      * recently used in [draw], so that they aren't garbage collected too soon causing their
95      * underlying memory to be freed before the render thread can use it. Otherwise, the render
96      * thread may use the memory after it is freed, leading to undefined behavior (typically a
97      * crash). The values of this map are the last time (according to [getDurationTimeMillis]) that
98      * the corresponding mesh has been drawn, so that its contents can be periodically evicted to
99      * keep memory usage under control.
100      *
101      * On Android V+, this bug has been fixed, so this map will remain empty.
102      */
103     private val recentlyDrawnMeshesToLastDrawTimeMillis = MutableObjectLongMap<AndroidMesh>()
104 
105     /**
106      * The last time that [recentlyDrawnMeshesToLastDrawTimeMillis] was checked for old meshes that
107      * can be cleaned up.
108      */
109     private var recentlyDrawnMeshesLastCleanupTimeMillis = Long.MIN_VALUE
110 
111     /**
112      * Cached [android.graphics.Mesh]es so that they can be reused between calls to
113      * [Canvas.drawMesh], greatly improving performance. Each [InProgressStroke] maps to a list of
114      * [InProgressMeshData] objects, one for each brush coat. Because [InProgressStroke] is mutable,
115      * this cache is based not just on the existence of data, but whether that data's version number
116      * matches that of the [InProgressStroke].
117      */
118     private val inProgressStrokeToAndroidMeshes =
119         WeakHashMap<InProgressStroke, List<InProgressMeshData>>()
120 
121     /**
122      * Caches [ShaderMetadata]s so that when two [MeshFormat] objects have equivalent packed
123      * representations (see [MeshFormat.isPackedEquivalent]), the same [ShaderMetadata] object can
124      * be used instead of reconstructed. This is a list instead of a map because:
125      * 1. There should never be more than ~9 unique values of [MeshFormat], so a linear scan is not
126      *    an undue cost when constructing a new [android.graphics.Mesh].
127      * 2. [MeshFormat] does not implement `hashCode` and `equals` in a way that would be relevant
128      *    here, since we only care about the packed representation for this use case. This could be
129      *    worked around with a wrapper type of [MeshFormat] that is specific to the packed
130      *    representation, but it didn't seem worth the extra effort.
131      *
132      * @See [obtainShaderMetadata] for the management of this cache.
133      */
134     private val meshFormatToPackedShaderMetadata = ArrayList<Pair<MeshFormat, ShaderMetadata>>()
135 
136     /**
137      * Holds a mapping from [MeshFormat] to [ShaderMetadata], so that when two [MeshFormat] objects
138      * are equivalent when it comes to their unpacked representation (see
139      * [MeshFormat.isUnpackedEquivalent]), then the same [MeshSpecification] object can be used
140      * instead of reconstructed. This is a list instead of a map because
141      * 1. There should never be more than ~9 unique values of [MeshFormat], so a linear scan is not
142      *    an undue cost when constructing a new [android.graphics.Mesh].
143      * 2. [MeshFormat] does not implement `hashCode` and `equals` in a way that would be relevant
144      *    here, since we only care about the unpacked representation for this use case.
145      *
146      * @See [obtainShaderMetadata] for the management of this cache.
147      */
148     private val meshFormatToUnpackedShaderMetadata = ArrayList<Pair<MeshFormat, ShaderMetadata>>()
149 
150     /** Scratch [Matrix] used for draw calls taking an [AffineTransform]. */
151     private val scratchMatrix = Matrix()
152 
153     /** Scratch space used as the argument to [Matrix.getValues]. */
154     private val matrixValuesScratchArray = FloatArray(9)
155 
156     /** Scratch space used to hold the scale/skew components of a [Matrix] in column-major order. */
157     @Size(4) private val objectToCanvasLinearComponentScratch = FloatArray(4)
158 
159     /** Allocated once and reused for performance, passed to [AndroidMesh.setFloatUniform]. */
160     private val colorRgbaScratchArray = FloatArray(4)
161 
162     // First and last inputs for the stroke being rendered, reused so that we don't need to allocate
163     // new ones for every stroke.
164     private val scratchFirstInput = StrokeInput()
165     private val scratchLastInput = StrokeInput()
166 
drawnull167     override fun draw(
168         canvas: Canvas,
169         stroke: Stroke,
170         strokeToScreenTransform: AffineTransform,
171         textureAnimationProgress: Float,
172     ) {
173         strokeToScreenTransform.populateMatrix(scratchMatrix)
174         draw(canvas, stroke, scratchMatrix, textureAnimationProgress)
175     }
176 
177     /**
178      * Draw a [Stroke] to the [Canvas].
179      *
180      * @param canvas The [Canvas] to draw to.
181      * @param stroke The [Stroke] to draw.
182      * @param strokeToScreenTransform The transform [Matrix] to convert from [Stroke] coordinates to
183      *   the coordinates of pixels on the screen used to display the stroke. It is important to pass
184      *   this here to be applied internally rather than applying it to [canvas] in calling code, to
185      *   ensure anti-aliasing has the information it needs to render properly. Also, any additional
186      *   transforms applied to [canvas] must only be translations. If you are rendering in a
187      *   [android.view.View] where it (or one of its ancestors) is rotated or scaled within its
188      *   parent, or if you are applying rotation or scaling transforms to [canvas] yourself, then
189      *   care must be taken so that these transformations are reflected in the
190      *   [strokeToScreenTransform]. Without this, anti-aliasing at the edge of strokes will not
191      *   render properly.
192      * @param textureAnimationProgress The animation progress value for the stroke's animated
193      *   textures, if any.
194      */
drawnull195     override fun draw(
196         canvas: Canvas,
197         stroke: Stroke,
198         strokeToScreenTransform: Matrix,
199         textureAnimationProgress: Float,
200     ) {
201         require(strokeToScreenTransform.isAffine) { "strokeToScreenTransform must be affine" }
202         if (stroke.inputs.isEmpty()) return // nothing to draw
203         stroke.inputs.populate(0, scratchFirstInput)
204         stroke.inputs.populate(stroke.inputs.size - 1, scratchLastInput)
205         for (coatIndex in 0 until stroke.brush.family.coats.size) {
206             val meshes = stroke.shape.renderGroupMeshes(coatIndex)
207             if (meshes.isEmpty()) continue
208             val brushPaint = stroke.brush.family.coats[coatIndex].paint
209             val textureMapping = getTextureMapping(brushPaint)
210             val numTextureAnimationFrames = getNumTextureAnimationFrames(brushPaint)
211             val blendMode = finalBlendMode(brushPaint)
212             // A white paint color ensures that the paint color doesn't affect how the paint texture
213             // is blended with the mesh coloring.
214             val androidPaint =
215                 paintCache.obtain(
216                     brushPaint,
217                     ComposeColor.White,
218                     stroke.brush.size,
219                     scratchFirstInput,
220                     scratchLastInput,
221                 )
222             for (mesh in meshes) {
223                 drawFromStroke(
224                     canvas,
225                     mesh,
226                     strokeToScreenTransform,
227                     stroke.brush.composeColor,
228                     textureMapping,
229                     textureAnimationProgress,
230                     numTextureAnimationFrames,
231                     blendMode,
232                     androidPaint,
233                 )
234             }
235         }
236     }
237 
238     /** Draw an [InkMesh] as if it is part of a stroke. */
drawFromStrokenull239     private fun drawFromStroke(
240         canvas: Canvas,
241         inkMesh: InkMesh,
242         meshToCanvasTransform: Matrix,
243         brushColor: ComposeColor,
244         textureMapping: BrushPaint.TextureMapping,
245         textureAnimationProgress: Float,
246         numTextureAnimationFrames: Int,
247         blendMode: BlendMode,
248         paint: Paint,
249     ) {
250         fillObjectToCanvasLinearComponent(
251             meshToCanvasTransform,
252             objectToCanvasLinearComponentScratch
253         )
254         val cachedMeshData = inkMeshToAndroidMesh[inkMesh]
255         val uniformBugFixed = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
256         val androidMesh =
257             if (
258                 cachedMeshData == null ||
259                     (!uniformBugFixed &&
260                         !cachedMeshData.areUniformsEquivalent(
261                             brushColor,
262                             objectToCanvasLinearComponentScratch,
263                             textureAnimationProgress,
264                             numTextureAnimationFrames,
265                         ))
266             ) {
267                 val newMesh =
268                     createAndroidMesh(inkMesh) ?: return // Nothing to draw if the mesh is empty.
269                 updateAndroidMesh(
270                     newMesh,
271                     inkMesh.format,
272                     objectToCanvasLinearComponentScratch,
273                     brushColor,
274                     inkMesh.vertexAttributeUnpackingParams,
275                     textureMapping,
276                     textureAnimationProgress,
277                     numTextureAnimationFrames,
278                 )
279                 inkMeshToAndroidMesh[inkMesh] =
280                     MeshData.create(
281                         newMesh,
282                         brushColor,
283                         objectToCanvasLinearComponentScratch,
284                         textureAnimationProgress,
285                         numTextureAnimationFrames,
286                     )
287                 newMesh
288             } else {
289                 if (uniformBugFixed) {
290                     // Update the uniform values unconditionally because it's inexpensive after the
291                     // bug fix.
292                     // Before the bug fix, there's no need to update the uniforms since changed
293                     // uniform values
294                     // could have caused the mesh to be recreated above.
295                     updateAndroidMesh(
296                         cachedMeshData.androidMesh,
297                         inkMesh.format,
298                         objectToCanvasLinearComponentScratch,
299                         brushColor,
300                         inkMesh.vertexAttributeUnpackingParams,
301                         textureMapping,
302                         textureAnimationProgress,
303                         numTextureAnimationFrames,
304                     )
305                 }
306                 cachedMeshData.androidMesh
307             }
308 
309         canvas.drawMesh(androidMesh, blendMode, paint)
310 
311         if (!uniformBugFixed) {
312             val currentTimeMillis = getDurationTimeMillis()
313             // Before the `androidMesh` variable goes out of scope, save it as a hard reference
314             // (temporarily) as a workaround for the Android U bug where drawMesh would not hand off
315             // or
316             // share ownership of the mesh data properly so data could be used by the render thread
317             // after
318             // being freed and cause a crash.
319             saveRecentlyDrawnAndroidMesh(androidMesh, currentTimeMillis)
320             // Clean up meshes that were previously saved as hard references, but shouldn't be saved
321             // forever otherwise we'll run out of memory. Anything purged by this will only be kept
322             // around
323             // if its associated InkMesh is still referenced, due to their presence in
324             // `inkMeshToAndroidMesh`.
325             cleanUpRecentlyDrawnAndroidMeshes(currentTimeMillis)
326         }
327     }
328 
drawnull329     override fun draw(
330         canvas: Canvas,
331         inProgressStroke: InProgressStroke,
332         strokeToScreenTransform: AffineTransform,
333         textureAnimationProgress: Float,
334     ) {
335         strokeToScreenTransform.populateMatrix(scratchMatrix)
336         draw(canvas, inProgressStroke, scratchMatrix, textureAnimationProgress)
337     }
338 
drawnull339     override fun draw(
340         canvas: Canvas,
341         inProgressStroke: InProgressStroke,
342         strokeToScreenTransform: Matrix,
343         textureAnimationProgress: Float,
344     ) {
345         val brush =
346             checkNotNull(inProgressStroke.brush) {
347                 "Attempting to draw an InProgressStroke that has not been started."
348             }
349         require(strokeToScreenTransform.isAffine) { "strokeToScreenTransform must be affine" }
350         val inputCount = inProgressStroke.getInputCount()
351         if (inputCount == 0) return // nothing to draw
352         inProgressStroke.populateInput(scratchFirstInput, 0)
353         inProgressStroke.populateInput(scratchLastInput, inputCount - 1)
354         fillObjectToCanvasLinearComponent(
355             strokeToScreenTransform,
356             objectToCanvasLinearComponentScratch
357         )
358         val brushCoatCount = inProgressStroke.getBrushCoatCount()
359         for (coatIndex in 0 until brushCoatCount) {
360             val brushPaint = brush.family.coats[coatIndex].paint
361             val textureMapping = getTextureMapping(brushPaint)
362             val numTextureAnimationFrames = getNumTextureAnimationFrames(brushPaint)
363             val blendMode = finalBlendMode(brushPaint)
364             val androidPaint =
365                 paintCache.obtain(
366                     brushPaint,
367                     ComposeColor.White,
368                     brush.size,
369                     scratchFirstInput,
370                     scratchLastInput,
371                 )
372             val inProgressMeshData = obtainInProgressMeshData(inProgressStroke, coatIndex)
373             for (meshIndex in 0 until inProgressMeshData.androidMeshes.size) {
374                 val androidMesh = inProgressMeshData.androidMeshes[meshIndex] ?: continue
375                 updateAndroidMesh(
376                     androidMesh,
377                     inProgressStroke.getMeshFormat(coatIndex, meshIndex),
378                     objectToCanvasLinearComponentScratch,
379                     brush.composeColor,
380                     attributeUnpackingParams = null,
381                     textureMapping,
382                     textureAnimationProgress,
383                     numTextureAnimationFrames,
384                 )
385                 canvas.drawMesh(androidMesh, blendMode, androidPaint)
386             }
387         }
388     }
389 
390     /** Create a new [AndroidMesh] for the given [InkMesh]. */
createAndroidMeshnull391     private fun createAndroidMesh(inkMesh: InkMesh): AndroidMesh? {
392         val bounds = inkMesh.bounds ?: return null // Nothing to render with an empty mesh.
393         val meshSpec = obtainShaderMetadata(inkMesh.format, isPacked = true).meshSpecification
394         return AndroidMesh(
395             meshSpec,
396             AndroidMesh.TRIANGLES,
397             inkMesh.rawVertexData,
398             inkMesh.vertexCount,
399             inkMesh.rawTriangleIndexData,
400             RectF(bounds.xMin, bounds.yMin, bounds.xMax, bounds.yMax),
401         )
402     }
403 
404     /**
405      * Update [androidMesh] with the information that might have changed since the previous call to
406      * [drawFromStroke] with the [InkMesh]. This is intended to be so low cost that it can be called
407      * on every draw call.
408      */
updateAndroidMeshnull409     private fun updateAndroidMesh(
410         androidMesh: AndroidMesh,
411         meshFormat: MeshFormat,
412         @Size(min = 4) meshToCanvasLinearComponent: FloatArray,
413         brushColor: ComposeColor,
414         attributeUnpackingParams: List<MeshAttributeUnpackingParams>?,
415         textureMapping: BrushPaint.TextureMapping,
416         textureAnimationProgress: Float,
417         numTextureAnimationFrames: Int,
418     ) {
419         val isPacked = attributeUnpackingParams != null
420         var colorUniformName = INVALID_NAME
421         var positionUnpackingParamsUniformName = INVALID_NAME
422         var positionAttributeIndex = INVALID_ATTRIBUTE_INDEX
423         var sideDerivativeUnpackingParamsUniformName = INVALID_NAME
424         var sideDerivativeAttributeIndex = INVALID_ATTRIBUTE_INDEX
425         var forwardDerivativeUnpackingParamsUniformName = INVALID_NAME
426         var forwardDerivativeAttributeIndex = INVALID_ATTRIBUTE_INDEX
427         var objectToCanvasLinearComponentUniformName = INVALID_NAME
428         var textureMappingName = INVALID_NAME
429         var textureAnimationProgressName = INVALID_NAME
430         var numTextureAnimationFramesName = INVALID_NAME
431 
432         for ((id, name, unpackingIndex) in
433             obtainShaderMetadata(meshFormat, isPacked).uniformMetadata) {
434             when (id) {
435                 UniformId.OBJECT_TO_CANVAS_LINEAR_COMPONENT ->
436                     objectToCanvasLinearComponentUniformName = name
437                 UniformId.BRUSH_COLOR -> colorUniformName = name
438                 UniformId.POSITION_UNPACKING_TRANSFORM -> {
439                     check(isPacked) {
440                         "Unpacking transform uniform is only supported for packed meshes."
441                     }
442                     positionUnpackingParamsUniformName = name
443                     positionAttributeIndex = unpackingIndex
444                 }
445                 UniformId.SIDE_DERIVATIVE_UNPACKING_TRANSFORM -> {
446                     check(isPacked) {
447                         "Unpacking transform uniform is only supported for packed meshes."
448                     }
449                     sideDerivativeUnpackingParamsUniformName = name
450                     sideDerivativeAttributeIndex = unpackingIndex
451                 }
452                 UniformId.FORWARD_DERIVATIVE_UNPACKING_TRANSFORM -> {
453                     check(isPacked) {
454                         "Unpacking transform uniform is only supported for packed meshes."
455                     }
456                     forwardDerivativeUnpackingParamsUniformName = name
457                     forwardDerivativeAttributeIndex = unpackingIndex
458                 }
459                 UniformId.TEXTURE_MAPPING -> textureMappingName = name
460                 UniformId.TEXTURE_ANIMATION_PROGRESS -> textureAnimationProgressName = name
461                 UniformId.NUM_TEXTURE_ANIMATION_FRAMES -> numTextureAnimationFramesName = name
462             }
463         }
464         // Color and object-to-canvas uniforms are required for all meshes.
465         check(objectToCanvasLinearComponentUniformName != INVALID_NAME)
466         check(colorUniformName != INVALID_NAME)
467         // Unpacking transform uniforms are required for and only for packed meshes.
468         check(
469             !isPacked ||
470                 (positionUnpackingParamsUniformName != INVALID_NAME &&
471                     sideDerivativeUnpackingParamsUniformName != INVALID_NAME &&
472                     forwardDerivativeUnpackingParamsUniformName != INVALID_NAME)
473         )
474 
475         androidMesh.setFloatUniform(
476             objectToCanvasLinearComponentUniformName,
477             meshToCanvasLinearComponent[0],
478             meshToCanvasLinearComponent[1],
479             meshToCanvasLinearComponent[2],
480             meshToCanvasLinearComponent[3],
481         )
482 
483         // Don't use setColorUniform because it does some color space conversion that we don't want.
484         // Instead, set the uniform as an array of 4 floats, but ensure that the color is in the
485         // same
486         // color space that the MeshSpecification is configured to operate in. In
487         // LinearExtendedSrgb,
488         // "linear" means the values are not gamma-encoded, "extended" means they are not clamped to
489         // [0, 1], and "sRGB" means that we use the same color primaries as in ordinary sRGB.
490         androidMesh.setFloatUniform(
491             colorUniformName,
492             colorRgbaScratchArray.also {
493                 brushColor
494                     .convert(ComposeColorSpaces.LinearExtendedSrgb)
495                     .fillFloatArray(colorRgbaScratchArray)
496             },
497         )
498 
499         androidMesh.setIntUniform(textureMappingName, textureMapping.value)
500         androidMesh.setFloatUniform(textureAnimationProgressName, textureAnimationProgress)
501         androidMesh.setIntUniform(numTextureAnimationFramesName, numTextureAnimationFrames)
502 
503         if (!isPacked) return
504 
505         attributeUnpackingParams!!.let {
506             val positionParams = it[positionAttributeIndex]
507             androidMesh.setFloatUniform(
508                 positionUnpackingParamsUniformName,
509                 positionParams.xOffset,
510                 positionParams.xScale,
511                 positionParams.yOffset,
512                 positionParams.yScale,
513             )
514 
515             val sideDerivativeParams = it[sideDerivativeAttributeIndex]
516             androidMesh.setFloatUniform(
517                 sideDerivativeUnpackingParamsUniformName,
518                 sideDerivativeParams.xOffset,
519                 sideDerivativeParams.xScale,
520                 sideDerivativeParams.yOffset,
521                 sideDerivativeParams.yScale,
522             )
523 
524             val forwardDerivativeParams = it[forwardDerivativeAttributeIndex]
525             androidMesh.setFloatUniform(
526                 forwardDerivativeUnpackingParamsUniformName,
527                 forwardDerivativeParams.xOffset,
528                 forwardDerivativeParams.xScale,
529                 forwardDerivativeParams.yOffset,
530                 forwardDerivativeParams.yScale,
531             )
532         }
533     }
534 
fillObjectToCanvasLinearComponentnull535     private fun fillObjectToCanvasLinearComponent(
536         objectToCanvasTransform: Matrix,
537         @Size(min = 4) objectToCanvasLinearComponent: FloatArray,
538     ) {
539         require(objectToCanvasTransform.isAffine) { "objectToCanvasTransform must be affine" }
540         objectToCanvasTransform.getValues(matrixValuesScratchArray)
541         objectToCanvasLinearComponent.let {
542             it[0] = matrixValuesScratchArray[Matrix.MSCALE_X]
543             it[1] = matrixValuesScratchArray[Matrix.MSKEW_Y]
544             it[2] = matrixValuesScratchArray[Matrix.MSKEW_X]
545             it[3] = matrixValuesScratchArray[Matrix.MSCALE_Y]
546         }
547     }
548 
obtainInProgressMeshDatanull549     private fun obtainInProgressMeshData(
550         inProgressStroke: InProgressStroke,
551         coatIndex: Int,
552     ): InProgressMeshData {
553         val cachedMeshDatas = inProgressStrokeToAndroidMeshes[inProgressStroke]
554         if (
555             cachedMeshDatas != null && cachedMeshDatas.size == inProgressStroke.getBrushCoatCount()
556         ) {
557             val inProgressMeshData = cachedMeshDatas[coatIndex]
558             if (inProgressMeshData.version == inProgressStroke.version) {
559                 return inProgressMeshData
560             }
561         }
562         val inProgressMeshDatas = computeInProgressMeshDatas(inProgressStroke)
563         inProgressStrokeToAndroidMeshes[inProgressStroke] = inProgressMeshDatas
564         return inProgressMeshDatas[coatIndex]
565     }
566 
computeInProgressMeshDatasnull567     private fun computeInProgressMeshDatas(
568         inProgressStroke: InProgressStroke
569     ): List<InProgressMeshData> =
570         buildList() {
571             for (coatIndex in 0 until inProgressStroke.getBrushCoatCount()) {
572                 val androidMeshes =
573                     buildList() {
574                         for (meshIndex in
575                             0 until inProgressStroke.getMeshPartitionCount(coatIndex)) {
576                             add(createAndroidMesh(inProgressStroke, coatIndex, meshIndex))
577                         }
578                     }
579                 add(InProgressMeshData(inProgressStroke.version, androidMeshes))
580             }
581         }
582 
583     /**
584      * Create a new [AndroidMesh] for the unpacked mesh at [meshIndex] in brush coat [coatIndex] of
585      * the given [inProgressStroke].
586      */
587     @VisibleForTesting
createAndroidMeshnull588     internal fun createAndroidMesh(
589         inProgressStroke: InProgressStroke,
590         coatIndex: Int,
591         meshIndex: Int,
592     ): AndroidMesh? {
593         val vertexCount = inProgressStroke.getVertexCount(coatIndex, meshIndex)
594         if (vertexCount < 3) {
595             // Fail gracefully when mesh doesn't contain enough vertices for a full triangle.
596             return null
597         }
598         val bounds = BoxAccumulator().apply { inProgressStroke.populateMeshBounds(coatIndex, this) }
599         if (bounds.isEmpty()) return null // Empty mesh; nothing to render.
600         return AndroidMesh(
601             obtainShaderMetadata(
602                     inProgressStroke.getMeshFormat(coatIndex, meshIndex),
603                     isPacked = false
604                 )
605                 .meshSpecification,
606             AndroidMesh.TRIANGLES,
607             inProgressStroke.getRawVertexBuffer(coatIndex, meshIndex),
608             vertexCount,
609             inProgressStroke.getRawTriangleIndexBuffer(coatIndex, meshIndex),
610             bounds.box?.let { RectF(it.xMin, it.yMin, it.xMax, it.yMax) } ?: return null,
611         )
612     }
613 
614     /**
615      * Returns a [ShaderMetadata] compatible with the [isPacked] state of the given [MeshFormat]'s
616      * vertex format. This may be newly created, or an internally cached value.
617      *
618      * This method manages read and write access to both [meshFormatToPackedShaderMetadata] and
619      * [meshFormatToUnpackedShaderMetadata]
620      */
621     @VisibleForTesting
obtainShaderMetadatanull622     internal fun obtainShaderMetadata(meshFormat: MeshFormat, isPacked: Boolean): ShaderMetadata {
623         val meshFromatToShaderMetaData =
624             if (isPacked) meshFormatToPackedShaderMetadata else meshFormatToUnpackedShaderMetadata
625         // Check the cache first.
626         return getCachedValue(meshFormat, meshFromatToShaderMetaData, isPacked)
627             ?: createShaderMetadata(meshFormat, isPacked).also {
628                 // Populate the cache before returning the newly-created ShaderMetadata.
629                 meshFromatToShaderMetaData.add(Pair(meshFormat, it))
630             }
631     }
632 
633     /**
634      * Returns true when the [stroke]'s [inputs] are empty, or [MeshFormat] is compatible with the
635      * native Skia `MeshSpecificationData`.
636      */
canDrawnull637     internal fun canDraw(stroke: Stroke): Boolean {
638         for (groupIndex in 0 until stroke.shape.getRenderGroupCount()) {
639             if (stroke.shape.renderGroupMeshes(groupIndex).isEmpty()) continue
640             val format = stroke.shape.renderGroupFormat(groupIndex)
641             if (!nativeIsMeshFormatRenderable(format.nativePointer, isPacked = true)) {
642                 return false
643             }
644         }
645         return true
646     }
647 
createShaderMetadatanull648     private fun createShaderMetadata(meshFormat: MeshFormat, isPacked: Boolean): ShaderMetadata {
649         // Fill "out" parameter arrays with invalid data, to fail fast in case anything goes wrong.
650         val attributeTypesOut = IntArray(MAX_ATTRIBUTES) { Type.INVALID_NATIVE_VALUE }
651         val attributeOffsetsBytesOut = IntArray(MAX_ATTRIBUTES) { INVALID_OFFSET }
652         val attributeNamesOut = Array(MAX_ATTRIBUTES) { INVALID_NAME }
653         val vertexStrideBytesOut = intArrayOf(INVALID_VERTEX_STRIDE)
654         val varyingTypesOut = IntArray(MAX_VARYINGS) { Type.INVALID_NATIVE_VALUE }
655         val varyingNamesOut = Array(MAX_VARYINGS) { INVALID_NAME }
656         val uniformIdsOut = IntArray(MAX_UNIFORMS) { UniformId.INVALID_NATIVE_VALUE }
657         val uniformUnpackingIndicesOut = IntArray(MAX_UNIFORMS) { INVALID_ATTRIBUTE_INDEX }
658         val uniformNamesOut = Array(MAX_UNIFORMS) { INVALID_NAME }
659         val vertexShaderOut = arrayOf("unset vertex shader")
660         val fragmentShaderOut = arrayOf("unset fragment shader")
661         fillSkiaMeshSpecData(
662             meshFormat.nativePointer,
663             isPacked,
664             attributeTypesOut,
665             attributeOffsetsBytesOut,
666             attributeNamesOut,
667             vertexStrideBytesOut,
668             varyingTypesOut,
669             varyingNamesOut,
670             uniformIdsOut,
671             uniformUnpackingIndicesOut,
672             uniformNamesOut,
673             vertexShaderOut,
674             fragmentShaderOut,
675         )
676         val attributes = mutableListOf<MeshSpecification.Attribute>()
677         for (attrIndex in 0 until MAX_ATTRIBUTES) {
678             val type = Type.fromNativeValue(attributeTypesOut[attrIndex]) ?: break
679             val offset = attributeOffsetsBytesOut[attrIndex]
680             val name = attributeNamesOut[attrIndex]
681             attributes.add(MeshSpecification.Attribute(type.meshSpecValue, offset, name))
682         }
683         val varyings = mutableListOf<MeshSpecification.Varying>()
684         for (varyingIndex in 0 until MAX_VARYINGS) {
685             val type = Type.fromNativeValue(varyingTypesOut[varyingIndex]) ?: break
686             val name = varyingNamesOut[varyingIndex]
687             varyings.add(MeshSpecification.Varying(type.meshSpecValue, name))
688         }
689         val uniforms = mutableListOf<UniformMetadata>()
690         for (uniformIndex in 0 until MAX_UNIFORMS) {
691             val id = UniformId.fromNativeValue(uniformIdsOut[uniformIndex]) ?: break
692             val name = uniformNamesOut[uniformIndex]
693             val attributeIndex = uniformUnpackingIndicesOut[uniformIndex]
694             uniforms.add(UniformMetadata(id, name, attributeIndex))
695         }
696 
697         return ShaderMetadata(
698             meshSpecification =
699                 MeshSpecification.make(
700                     attributes.toTypedArray(),
701                     validVertexStrideBytes(vertexStrideBytesOut[0]),
702                     varyings.toTypedArray(),
703                     vertexShaderOut[0],
704                     fragmentShaderOut[0],
705                     // The shaders output linear, premultiplied, non-clamped sRGB colors.
706                     AndroidColorSpace.get(AndroidColorSpace.Named.LINEAR_EXTENDED_SRGB),
707                     MeshSpecification.ALPHA_TYPE_PREMULTIPLIED,
708                 ),
709             uniformMetadata = uniforms,
710         )
711     }
712 
validVertexStrideBytesnull713     private fun validVertexStrideBytes(vertexStride: Int): Int {
714         // MeshSpecification.make is documented to accept a vertex stride between 1 and 1024 bytes
715         // (inclusive), but its only supported vertex attribute types are in multiples of 4 bytes,
716         // so
717         // its true lower bound is 4 bytes.
718         require(vertexStride in 4..1024)
719         return vertexStride
720     }
721 
722     /**
723      * Retrieves data analogous to [MeshSpecification] from native code. It makes use of "out"
724      * parameters to return this data, as it is tedious (and therefore error-prone) to construct and
725      * return complex objects from JNI. These "out" parameters are all arrays, as those are well
726      * supported by JNI, especially primitive arrays.
727      *
728      * @param meshFormatNativePointer The pointer address of a [MeshFormat].
729      * @param isPacked Whether to fill the mesh spec with properties describing a packed format (as
730      *   in ink::Mesh) or an unpacked format (as in ink::MutableMesh).
731      * @param attributeTypesOut An array that can hold at least [MAX_ATTRIBUTES] values. It will
732      *   contain the resulting attribute types aligning with [Type.nativeValue]. The number of
733      *   attributes will be determined by the first index of this array with an invalid value, and
734      *   that attribute count will determine the number of entries to look at in
735      *   [attributeOffsetsBytesOut] and [attributeNamesOut]. See
736      *   [MeshSpecification.Attribute.getType].
737      * @param attributeOffsetsBytesOut An array that can hold at least [MAX_ATTRIBUTES] values.
738      *   Specifies the layout of each vertex of the raw data for a mesh, where each vertex is a
739      *   contiguous chunk of memory and each attribute is located at a particular number of bytes
740      *   (offset) from the beginning of that vertex's chunk of memory.
741      * @param attributeNamesOut The names of each attribute, referenced in the shader code.
742      * @param vertexStrideBytesOut In the raw data of the mesh vertices, the number of bytes between
743      *   the start of each vertex. See [attributeOffsetsBytesOut] for how each attribute is laid
744      *   out.
745      * @param varyingTypesOut An array that can hold at least [MAX_VARYINGS] values. It will contain
746      *   the resulting varying types aligning with [Type.nativeValue]. The number of varyings will
747      *   be determined by the first index of this array with an invalid value, and that varying
748      *   count will determine the number of entries to look at in [varyingNamesOut]. See
749      *   [MeshSpecification.Varying.getType].
750      * @param varyingNamesOut The names of each varying, referenced in the shader code.
751      * @param vertexShaderOut An array with at least one element that will be filled in by the
752      *   string vertex shader code.
753      * @param fragmentShaderOut An array with at least one element that will be filled in by the
754      *   string fragment shader code.
755      * @throws IllegalArgumentException If an unrecognized format was passed in, i.e. when
756      *   [nativeIsMeshFormatRenderable] returns false.
757      */
758     @UsedByNative
fillSkiaMeshSpecDatanull759     private external fun fillSkiaMeshSpecData(
760         meshFormatNativePointer: Long,
761         isPacked: Boolean,
762         attributeTypesOut: IntArray,
763         attributeOffsetsBytesOut: IntArray,
764         attributeNamesOut: Array<String>,
765         vertexStrideBytesOut: IntArray,
766         varyingTypesOut: IntArray,
767         varyingNamesOut: Array<String>,
768         uniformIdsOut: IntArray,
769         uniformUnpackingIndicesOut: IntArray,
770         uniformNamesOut: Array<String>,
771         vertexShaderOut: Array<String>,
772         fragmentShaderOut: Array<String>,
773     )
774 
775     /**
776      * Constructs native [MeshFormat] from [meshFormatNativePointer] and checks whether it is
777      * compatible with the native Skia `MeshSpecificationData`.
778      *
779      * @param isPacked checks whether [meshFormat] describes a packed format (as in native
780      *   ink::Mesh) or an unpacked format (as in native ink::MutableMesh).
781      *
782      * [fillSkiaMeshSpecData] throws IllegalArgumentException when this method returns false.
783      */
784     @UsedByNative
785     private external fun nativeIsMeshFormatRenderable(
786         meshFormatNativePointer: Long,
787         isPacked: Boolean,
788     ): Boolean
789 
790     private fun saveRecentlyDrawnAndroidMesh(androidMesh: AndroidMesh, currentTimeMillis: Long) {
791         recentlyDrawnMeshesToLastDrawTimeMillis[androidMesh] = currentTimeMillis
792     }
793 
cleanUpRecentlyDrawnAndroidMeshesnull794     private fun cleanUpRecentlyDrawnAndroidMeshes(currentTimeMillis: Long) {
795         if (
796             recentlyDrawnMeshesLastCleanupTimeMillis + EVICTION_SCAN_PERIOD_MS > currentTimeMillis
797         ) {
798             return
799         }
800         recentlyDrawnMeshesToLastDrawTimeMillis.removeIf { _, lastDrawTimeMillis ->
801             lastDrawTimeMillis + MESH_STRONG_REFERENCE_DURATION_MS < currentTimeMillis
802         }
803         recentlyDrawnMeshesLastCleanupTimeMillis = currentTimeMillis
804     }
805 
806     @VisibleForTesting
getRecentlyDrawnAndroidMeshesCountnull807     internal fun getRecentlyDrawnAndroidMeshesCount(): Int {
808         return recentlyDrawnMeshesToLastDrawTimeMillis.size
809     }
810 
fillFloatArraynull811     private fun ComposeColor.fillFloatArray(@Size(min = 4) outRgba: FloatArray) {
812         outRgba[0] = this.red
813         outRgba[1] = this.green
814         outRgba[2] = this.blue
815         outRgba[3] = this.alpha
816     }
817 
818     private class MeshData
819     private constructor(
820         val androidMesh: AndroidMesh,
821         val brushColor: ComposeColor,
822         /** Do not modify! */
823         @Size(4) val objectToCanvasLinearComponent: FloatArray,
824         val textureAnimationProgress: Float,
825         val numTextureAnimationFrames: Int,
826     ) {
827 
areUniformsEquivalentnull828         fun areUniformsEquivalent(
829             otherBrushColor: ComposeColor,
830             @Size(4) otherObjectToCanvasLinearComponent: FloatArray,
831             otherTextureAnimationProgress: Float,
832             otherNumTextureAnimationFrames: Int,
833         ): Boolean {
834             return otherBrushColor == brushColor &&
835                 otherObjectToCanvasLinearComponent.contentEquals(objectToCanvasLinearComponent) &&
836                 otherNumTextureAnimationFrames == numTextureAnimationFrames &&
837                 // Ignore animation progress if there's only one frame (no animation).
838                 (numTextureAnimationFrames == 1 ||
839                     // We intentionally compare animation progress floats by equality. The apparent
840                     // alternative would be converting to a frame index, but two particles in the
841                     // same stroke
842                     // may have different animation offsets, which may not be quantized to frame
843                     // index
844                     // boundaries. So there is no global "frame index" that is guaranteed to be
845                     // meaningful for
846                     // all particles.
847                     otherTextureAnimationProgress == textureAnimationProgress)
848         }
849 
850         companion object {
createnull851             fun create(
852                 androidMesh: AndroidMesh,
853                 brushColor: ComposeColor,
854                 @Size(4) objectToCanvasLinearComponent: FloatArray,
855                 textureAnimationProgress: Float,
856                 numTextureAnimationFrames: Int,
857             ): MeshData {
858                 val copied = FloatArray(4)
859                 System.arraycopy(
860                     /* src = */ objectToCanvasLinearComponent,
861                     /* srcPos = */ 0,
862                     /* dest = */ copied,
863                     /* destPos = */ 0,
864                     /* length = */ 4,
865                 )
866                 return MeshData(
867                     androidMesh,
868                     brushColor,
869                     copied,
870                     textureAnimationProgress,
871                     numTextureAnimationFrames,
872                 )
873             }
874         }
875     }
876 
877     /**
878      * Contains the [android.graphics.Mesh] data for an [InProgressStroke], along with metadata used
879      * to verify if that data is still valid.
880      */
881     private data class InProgressMeshData(
882         /** If this does not match [InProgressStroke.version], the data is invalid. */
883         val version: Long,
884         /**
885          * At each index, the [android.graphics.Mesh] for the corresponding partition index of the
886          * [InProgressStroke], or `null` if that partition is empty.
887          */
888         val androidMeshes: List<AndroidMesh?>,
889     )
890 
891     companion object {
<lambda>null892         init {
893             NativeLoader.load()
894         }
895 
896         /**
897          * On Android U, how long to hold a reference to an [android.graphics.Mesh] after it has
898          * been drawn with [Canvas.drawMesh]. This is an imperfect workaround for a bug in the
899          * native layer where the render thread is not given ownership of the mesh data to prevent
900          * it from being freed before the render thread uses it for drawing.
901          */
902         private const val MESH_STRONG_REFERENCE_DURATION_MS = 5000
903         private const val EVICTION_SCAN_PERIOD_MS = 2000
904 
905         /** All the metadata about values sent to the shader for a given mesh. Used for caching. */
906         internal data class ShaderMetadata(
907             val meshSpecification: MeshSpecification,
908             val uniformMetadata: List<UniformMetadata>,
909         )
910 
911         internal data class UniformMetadata(
912             val id: UniformId,
913             val name: String,
914             val unpackingAttributeIndex: Int,
915         )
916 
917         internal enum class UniformId(val nativeValue: Int) {
918             /**
919              * The 2x2 linear component of the affine transformation from mesh / "object"
920              * coordinates to the canvas. This requires that the [meshToCanvasTransform] matrix used
921              * during drawing is an affine transform. Set it with [AndroidMesh.setFloatUniform]. It
922              * is a `float4` with the following expected entries:
923              * - `[0]`: `matrixValues[Matrix.MSCALE_X]`
924              * - `[1]`: `matrixValues[Matrix.MSKEW_X]`
925              * - `[2]`: `matrixValues[Matrix.MSKEW_Y]`
926              * - `[3]`: `matrixValues[Matrix.MSCALE_Y]`
927              */
928             OBJECT_TO_CANVAS_LINEAR_COMPONENT(0),
929 
930             /**
931              * The [Color] of the Stroke's brush, which will be combined with per-vertex color
932              * shifts in the shaders. Set it with [AndroidMesh.setColorUniform]. Must be specified
933              * for every format.
934              */
935             BRUSH_COLOR(1),
936 
937             /**
938              * The transform parameters to convert packed [InkMesh] coordinates into actual
939              * ("object") coordinates. Set it with [AndroidMesh.setFloatUniform]. Must be specified
940              * for packed meshes only. It is a `float4` with the following entries:
941              * - `[0]`: x offset
942              * - `[1]`: x scale
943              * - `[2]`: y offset
944              * - `[3]`: y scale
945              */
946             POSITION_UNPACKING_TRANSFORM(2),
947 
948             /**
949              * The transform parameters to convert packed [InkMesh] side-derivative attribute values
950              * into their unpacked values. Set it with [AndroidMesh.setFloatUniform]. Must be
951              * specified for packed meshes only. It is a `float4` with the following entries:
952              * - `[0]`: x offset
953              * - `[1]`: x scale
954              * - `[2]`: y offset
955              * - `[3]`: y scale
956              */
957             SIDE_DERIVATIVE_UNPACKING_TRANSFORM(3),
958 
959             /**
960              * The transform parameters to convert packed [InkMesh] forward-derivative attribute
961              * values into their unpacked values. Set it with [AndroidMesh.setFloatUniform]. Must be
962              * specified for packed meshes only. It is a `float4` with the following entries:
963              * - `[0]`: x offset
964              * - `[1]`: x scale
965              * - `[2]`: y offset
966              * - `[3]`: y scale
967              */
968             FORWARD_DERIVATIVE_UNPACKING_TRANSFORM(4),
969 
970             /**
971              * The integer value of the [BrushPaint.TextureMapping] mode used for this brush coat.
972              * Set it with [AndroidMesh.setIntUniform]. Must be specified for every format. It is an
973              * `int`.
974              */
975             // TODO: b/375203215 - Get rid of this uniform once we are able to mix tiling and
976             // winding
977             // textures in a single [BrushPaint].
978             TEXTURE_MAPPING(5),
979 
980             /**
981              * The current progress of the texture animation. It is a `float` in the range [0, 1].
982              *
983              * We must pass both animation progress and number of frames to the shader, rather than
984              * computing a frame index from these on the CPU and passing only that. Why? Each
985              * particle in a stroke can have a different progress offset, and these offsets are not
986              * quantized to animation frame boundaries. Therefore the conversion to frame indices
987              * depends on both the stroke-wide progress and the per-particle offset, the latter of
988              * which is only available in the vertex shader.
989              */
990             TEXTURE_ANIMATION_PROGRESS(6),
991 
992             /**
993              * The number of frames in the texture animation. It is an `int`, and must be at
994              * least 1.
995              */
996             NUM_TEXTURE_ANIMATION_FRAMES(7);
997 
998             companion object {
999                 const val INVALID_NATIVE_VALUE = -1
1000 
fromNativeValuenull1001                 fun fromNativeValue(nativeValue: Int): UniformId? {
1002                     for (type in UniformId.values()) {
1003                         if (type.nativeValue == nativeValue) return type
1004                     }
1005                     return null
1006                 }
1007             }
1008         }
1009 
1010         //
1011         // [MeshSpecification] limits the number of attributes to 8 and the number of varyings to 6
1012         // (see
1013         // https://developer.android.com/reference/android/graphics/MeshSpecification). This
1014         // limitation
1015         // comes from Skia (see https://api.skia.org/classSkMeshSpecification.html).
1016         private const val MAX_ATTRIBUTES = 8
1017         private const val MAX_VARYINGS = 6
1018         // [MeshSpecification] doesn't seem to place any clear limit on the number of uniforms, so
1019         // this
1020         // value is just the size we choose to use for our array. Currently it is set to the actual
1021         // number of uniforms we happen to use right now.
1022         private const val MAX_UNIFORMS = 8
1023 
1024         private const val INVALID_OFFSET = -1
1025         private const val INVALID_VERTEX_STRIDE = -1
1026         private const val INVALID_NAME = ")"
1027         private const val INVALID_ATTRIBUTE_INDEX = -1
1028 
1029         internal enum class Type(val nativeValue: Int, val meshSpecValue: Int) {
1030             FLOAT(0, MeshSpecification.TYPE_FLOAT),
1031             FLOAT2(1, MeshSpecification.TYPE_FLOAT2),
1032             FLOAT3(2, MeshSpecification.TYPE_FLOAT3),
1033             FLOAT4(3, MeshSpecification.TYPE_FLOAT4),
1034             UBYTE4(4, MeshSpecification.TYPE_UBYTE4);
1035 
1036             // Note that we don't include `INT` here (even though we use that as a uniform type)
1037             // because
1038             // there is no [MeshSpecification] constant for it. On the native side, the `kInt`
1039             // enumerator
1040             // is assigned a value of 5.
1041 
1042             companion object {
1043                 const val INVALID_NATIVE_VALUE = -1
1044 
fromNativeValuenull1045                 fun fromNativeValue(nativeValue: Int): Type? {
1046                     for (type in Type.values()) {
1047                         if (type.nativeValue == nativeValue) return type
1048                     }
1049                     return null
1050                 }
1051             }
1052         }
1053 
1054         /**
1055          * Returns the [T] associated with [key] in [cache]. If [isPacked] is false, keys are
1056          * considered equivalent if their unpacked format is the same; if true, if their packed
1057          * format is the same. This provides a map-like getter interface for a cache implemented as
1058          * a list of key-value pairs. Returns `null` if no equivalent key is found.
1059          */
getCachedValuenull1060         private fun <T> getCachedValue(
1061             key: MeshFormat,
1062             cache: ArrayList<Pair<MeshFormat, T>>,
1063             isPacked: Boolean,
1064         ): T? {
1065             for ((format, item) in cache) {
1066                 if (isPacked && format.isPackedEquivalent(key)) {
1067                     return item
1068                 } else if (!isPacked && format.isUnpackedEquivalent(key)) {
1069                     return item
1070                 }
1071             }
1072             return null
1073         }
1074 
finalBlendModenull1075         private fun finalBlendMode(brushPaint: BrushPaint): BlendMode =
1076             brushPaint.textureLayers.lastOrNull()?.let { it.blendMode.toBlendMode() }
1077                 ?: BlendMode.MODULATE
1078 
1079         /**
1080          * Returns the texture mapping mode used by the [BrushPaint]. (This is actually specified
1081          * separately in each texture layer, but currently, we require all texture layers in the
1082          * same paint to use the same texture mapping mode.)
1083          */
getTextureMappingnull1084         private fun getTextureMapping(brushPaint: BrushPaint): BrushPaint.TextureMapping =
1085             brushPaint.textureLayers.firstOrNull()?.let { it.mapping }
1086                 ?: BrushPaint.TextureMapping.TILING
1087 
1088         /**
1089          * Returns the number of texture animation frames in the [BrushPaint]. (This is actually
1090          * specified separately in each texture layer, but currently, we require all texture layers
1091          * in the same paint to have the same number of animation frames.)
1092          */
getNumTextureAnimationFramesnull1093         private fun getNumTextureAnimationFrames(brushPaint: BrushPaint): Int =
1094             brushPaint.textureLayers.firstOrNull()?.let { it.animationFrames } ?: 1
1095 
1096         private val MeshAttributeUnpackingParams.xOffset
1097             get() = components[0].offset
1098 
1099         private val MeshAttributeUnpackingParams.xScale
1100             get() = components[0].scale
1101 
1102         private val MeshAttributeUnpackingParams.yOffset
1103             get() = components[1].offset
1104 
1105         private val MeshAttributeUnpackingParams.yScale
1106             get() = components[1].scale
1107     }
1108 }
1109