Tencent / Tencent/libpag

Android: PAGImage.FromTexture renders blank when an editable image layer has a minimum animated scale below 0.4

Open
#3,707 0 comments 0 reactions 1 assignee View on GitHub

@domchen is already working on this.

Since Aug 28, 2026.

Dominant language
HTML
Stars
5.8k
Forks
531
Avg merge
8d 55m
Merged PRs (30d)
17

Description

Which Version of libpag are you using?

libpag 4.5.85

What Platform are you on?

Android 11, Pixel 2 XL, Adreno 540.

The issue was reproduced using the classic .pag pipeline with PAGFile, PAGPlayer, PAGSurface.FromTexture(), and PAGImage.FromTexture().

Expected Behavior

Image

A texture supplied through PAGImage.FromTexture() should render normally after replacing an editable image layer.

In the attached PAG file, the affected image layer has an effective transform scale factor animated from 0.0 to 1.0 (0% to 100%). At progress 1.0, its current effective scale factor is 1.0, so the replacement texture should be fully visible.

This should work for both opaque and transparent RGBA textures, just as it does when the same image is supplied through PAGImage.FromBitmap().

Actual Behavior

Image

The replacement image renders completely blank when all of the following conditions are met:

  1. The image is supplied through PAGImage.FromTexture().
  2. The editable image layer has an effective animated minimum scale below 0.4. In the attached reproduction, its scale reaches exactly 0.0.
  3. The PAG file is rendered through the classic PAGFile / PAGPlayer pipeline.

The external texture is created and consumed on the same current EGL context. It remains valid until after PAGPlayer.flush() and all snapshot/readback operations complete. It is a GL_TEXTURE_2D RGBA texture, and no GL error is reported during texture upload or rendering.

The problem is independent of alpha:

  • An opaque red RGBA texture is blank.
  • A texture containing transparent pixels is also blank.
  • The same opaque texture works when replacing another editable layer whose animated scale does not reach the mipmap threshold.
  • PAGImage.FromBitmap() works on the affected layer.
  • The original embedded image is visible in PAGViewer.
  • At progress 1.0, the affected layer has valid bounds and a current effective scale factor of 1.0.

Observed matrix comparison:

Working layer:
progress 0.0: effective scale is approximately 1.0
progress 1.0: effective scale is approximately 1.0
FromTexture result: visible

Affected layer:
progress 0.0: effective scale = 0.0
progress 1.0: effective scale = 1.0
FromTexture result at progress 1.0: blank
FromBitmap result at progress 1.0: visible

Generating mipmaps on the supplied GL texture with glGenerateMipmap() and setting GL_TEXTURE_MIN_FILTER to GL_LINEAR_MIPMAP_LINEAR does not resolve the blank output.

Reducing the external texture dimensions also does not resolve it because the authored animation still reaches an effective scale of zero.

Once triggered by the minimum scale, the replacement remains blank for the entire timeline, including frames where its current effective scale is greater than 0.4.

Code Example

val pagFile = requireNotNull(PAGFile.Load(pagFilePath))

val pagImage = requireNotNull(
    PAGImage.FromTexture(
        textureId,
        GLES20.GL_TEXTURE_2D,
        textureWidth,
        textureHeight,
        false,
    )
).apply {
    setScaleMode(PAGScaleMode.Zoom)
}

pagFile.replaceImageByName("animated_image.png", pagImage)

val pagSurface = requireNotNull(
    PAGSurface.FromTexture(
        outputTextureId,
        pagFile.width(),
        pagFile.height(),
        false,
    )
)

val player = PAGPlayer().apply {
    setComposition(pagFile)
    setSurface(pagSurface)
    setScaleMode(PAGScaleMode.Zoom)
    setProgress(1.0)
}

val changed = player.flush()

For comparison, changing only the image construction makes the affected layer visible:

val pagImage = requireNotNull(
    PAGImage.FromBitmap(replacementBitmap)
)

The same opaque red content, layer name, PAG file, progress, player, and output surface are used in both tests.

Investigation

The behavior appears to be caused by the classic renderer's mipmap path.

RenderCache::getAssetImageInternal() calls makeMipmapped(true) when the effective minimum scale is below MIPMAP_ENABLED_THRESHOLD:

https://github.com/Tencent/libpag/blob/main/src/rendering/caches/RenderCache.cpp

auto scaleFactor = stage->getAssetMinScale(assetID);
if (scaleFactor < MIPMAP_ENABLED_THRESHOLD) {
  image = image->makeMipmapped(true);
}
assetImages[assetID] = image;

A TGFX texture-backed image cannot currently create a mipmapped copy through this call because TextureImage::onMakeMipmapped() returns nullptr:

https://github.com/Tencent/tgfx/blob/main/src/core/images/TextureImage.h

std::shared_ptr<Image> onMakeMipmapped(bool) const override {
  return nullptr;
}

Based on the reproduction matrix and the current source code, our current hypothesis is that the texture-backed replacement becomes a null asset image when its effective minimum scale falls below 0.4.

We would appreciate confirmation from the maintainers that this is the code path responsible for the blank output.

The PAGX renderer already avoids calling makeMipmapped(true) for texture-backed images:

https://github.com/Tencent/libpag/commit/682a8ebc26f48bf642f517050b3767ba109da34d

if (image && !image->isTextureBacked()) {
  image = image->makeMipmapped(true);
}

Would it be appropriate to apply the same guard to the classic RenderCache path, or preserve the original image when makeMipmapped(true) returns nullptr?

Related Issue

This appears related to #2740 because both cases involve mipmap handling for textures supplied through PAGImage.FromTexture():

https://github.com/Tencent/libpag/issues/2740

However, the symptom is different:

  • #2740 reports the texture filter being changed to GL_LINEAR, resulting in flickering while downscaling.
  • This issue produces a completely blank replacement image because mipmapped image creation appears to return nullptr.

PAG File

from_texture_min_scale_repro.pag.zip

Attached:

  • from_texture_min_scale_repro.pag
  • Rendered output at progress 1.0 using PAGImage.FromTexture().
  • Rendered output at progress 1.0 using PAGImage.FromBitmap().

The PAG file contains:

  1. One editable image layer with an effective scale of 0.0 to 1.0 that works with PAGImage.FromTexture().
  2. One editable image layer animated from an effective scale of 0.0 to 1.0 that becomes blank with PAGImage.FromTexture().

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.