perf(pdf): reduce pan/zoom jitter in viewer dialog
The main cost during a live pan/zoom is the GPU re-sampling the page bitmap every frame under the zoomable modifier's transform. Three changes bring that cost down substantially: - Bitmap.Config.RGB_565 instead of ARGB_8888. PDFs are opaque, so the alpha channel is unused. Halves texture memory and GPU upload bandwidth; visually identical. - HI_RES_MAX_DIM_PX lowered from 6144 to 4096. 4096 stays within the common GPU texture-size limit, keeping rendering hardware-accelerated on mid-range devices. Peak bitmap is now ~24 MB (A4, RGB_565) instead of ~107 MB (ARGB_8888, 6144). - FilterQuality.Medium (bilinear) instead of High (bicubic/Mitchell). High is recomputed per frame during pan/zoom and is the main source of jitter on multi-megapixel sources. At 3072-4096 px base resolution bilinear is effectively indistinguishable. - HI_RES_ZOOM_THRESHOLD raised from 1.2 to 1.5 so we don't trigger a costly re-render for marginal zoom levels where the base is fine. - Cache the ImageBitmap wrapper via remember to avoid per-recomposition allocations.
This commit is contained in:
+3
-1
@@ -256,7 +256,9 @@ private fun renderFirstPage(
|
||||
|
||||
renderer.openPage(0).use { page ->
|
||||
val (renderW, renderH) = cappedRenderSize(page.width, page.height, targetWidthPx)
|
||||
val bitmap = Bitmap.createBitmap(renderW, renderH, Bitmap.Config.ARGB_8888)
|
||||
// RGB_565 halves memory vs ARGB_8888 and is visually identical for
|
||||
// opaque PDF renders.
|
||||
val bitmap = Bitmap.createBitmap(renderW, renderH, Bitmap.Config.RGB_565)
|
||||
bitmap.eraseColor(android.graphics.Color.WHITE)
|
||||
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||
PdfLoadState.Ready(
|
||||
|
||||
+23
-10
@@ -89,16 +89,18 @@ import net.engawapg.lib.zoomable.toggleScale
|
||||
import net.engawapg.lib.zoomable.zoomable
|
||||
|
||||
// Hard ceiling on each base-rendered page bitmap, in pixels. Higher = sharper when
|
||||
// the user pinch-zooms inside the dialog, but each page costs ~maxDim^2 * 4 bytes
|
||||
// of RAM (ARGB_8888). 3072 gives ~26 MB per A4-shaped page.
|
||||
// the user pinch-zooms inside the dialog, but each page costs ~maxDim^2 * 2 bytes
|
||||
// of RAM (RGB_565). 3072 gives ~13 MB per A4-shaped page.
|
||||
private const val VIEWER_MAX_DIM_PX = 3072
|
||||
|
||||
// Hard ceiling for the per-page zoom-aware detail render. When the user zooms in
|
||||
// past HI_RES_ZOOM_THRESHOLD we re-render the current page at
|
||||
// (VIEWER_MAX_DIM_PX * scale) capped at this value. 6144 allows up to 2x sharper
|
||||
// than the base render at peak cost of ~100 MB for one page (ARGB_8888, A4 shape).
|
||||
private const val HI_RES_MAX_DIM_PX = 6144
|
||||
private const val HI_RES_ZOOM_THRESHOLD = 1.2f
|
||||
// (VIEWER_MAX_DIM_PX * scale) capped at this value. 4096 keeps the bitmap within
|
||||
// common GPU texture limits (so drawing stays hardware-accelerated) and caps
|
||||
// memory at ~24 MB for an A4-shaped page (RGB_565). Above 4096 most mid-range
|
||||
// GPUs fall back to software rendering, which is what caused the pan/zoom jitter.
|
||||
private const val HI_RES_MAX_DIM_PX = 4096
|
||||
private const val HI_RES_ZOOM_THRESHOLD = 1.5f
|
||||
private const val HI_RES_DEBOUNCE_MS = 200L
|
||||
|
||||
// Zoom level the viewer animates to when the user double-taps. Matches the
|
||||
@@ -352,13 +354,21 @@ private fun PdfPageView(
|
||||
}
|
||||
|
||||
val current = hiResBitmap ?: baseBitmap
|
||||
// Cache the ImageBitmap wrapper so each recomposition doesn't allocate a new
|
||||
// one and push Compose into thinking the texture changed.
|
||||
val imageBitmap = remember(current) { current?.asImageBitmap() }
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
if (current != null) {
|
||||
if (imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = current.asImageBitmap(),
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
filterQuality = FilterQuality.High,
|
||||
// Medium = bilinear. High is bicubic/Mitchell and gets recomputed on every
|
||||
// frame during pan/zoom, which is the main source of jitter when the
|
||||
// source bitmap is several megapixels. Bilinear on a 3072-4096 px source
|
||||
// looks effectively identical on-screen.
|
||||
filterQuality = FilterQuality.Medium,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -388,7 +398,10 @@ private suspend fun renderPageCatching(
|
||||
withContext(Dispatchers.IO) {
|
||||
handle.renderer.openPage(pageIndex).use { page ->
|
||||
val (width, height) = cappedRenderSize(page.width, page.height, maxDim)
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
// RGB_565 uses half the memory of ARGB_8888 and is indistinguishable
|
||||
// for PDFs (opaque white background, no alpha). Halves GPU texture
|
||||
// upload size and sampling cost during pan/zoom.
|
||||
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
bmp.eraseColor(android.graphics.Color.WHITE)
|
||||
page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||
bmp
|
||||
|
||||
Reference in New Issue
Block a user