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