fix(subtitles): flatten atlas-overflow ASS frames into an RGBA composite

Signs built from hundreds of overlapping paint-stroke drawings (masked
smartphone screens and similar typesetting) sum to far more bitmap area
than the paged ALPHA_8 atlas can hold: the issue sample needs 5 pages of
16M px at 1080p and 19 at 4K against the 4-page cap, so the packer
dropped the painter-order tail - the sign's text and late mask strokes.

Move the packer out of the JNI file into AssPack.c (pure C, compilable
against a desktop libass for verification) and add a composite fallback:
when a frame can never fit MAX_ATLAS_PAGES pages or the vertex budget,
blend the image list CPU-side into one premultiplied RGBA rect over the
union bounding box - O(frame area) instead of O(sum of image areas) -
and draw it as a single quad through a new MODE_COMPOSITE path in the
GL renderer. Oversized composites reuse the existing grow-and-re-render
contract; the atlas fast path is byte-identical for every frame that fits.

Verified with a desktop harness compiling the shipped AssPack.c against
fork libass 0.18.3 and the issue sample: all atlas-mode frames byte-match
the previous packer, the sign's frames composite with zero truncation and
byte-match a reference full-frame blend at 1080p and 4K, and the
multi-page composite grow path round-trips.

close #1868
This commit is contained in:
edde746
2026-08-11 08:41:56 +02:00
parent 3a704a2b9b
commit 83c50d93a2
7 changed files with 528 additions and 311 deletions
+46 -283
View File
@@ -18,6 +18,7 @@ static inline long long nowMs(void) {
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
#include "AssPack.h"
#include "ass/ass.h"
#define LOG_TAG "SubtitleRenderer"
@@ -243,35 +244,8 @@ Java_com_edde746_plezy_libass_AssRender_nativeAssRenderDeinit(JNIEnv* env, jclas
}
}
// Hard cap on atlas pages (see the packing comment below). 4 pages of a GL-max
// texture is far above the worst real frame measured (a 4K-rendered full-screen
// typeset letter needs 3); beyond it tiles are dropped and counted in truncated.
#define MAX_ATLAS_PAGES 4
// A tile is a <= atlasMaxW x atlasMaxH sub-rect of an ASS_Image. A single image
// can exceed one atlas page only when the render frame is larger than a page
// (>4K, or a GPU whose max texture is below the frame) — multi-page can't split
// one image across pages (a quad samples one texture), so tiling does, keeping
// the never-drop guarantee. (#1436 itself was atlas-AREA overflow, fixed by the
// multi-page pack below, not oversized single images.) Tiles are built in list
// order (= libass blend/painter order, preserved for emission); the single-page
// pack runs height-sorted via a separate key array so emission order is untouched.
typedef struct {
ASS_Image* img; // source image (for bitmap/stride/color/dst_x/dst_y)
int ox, oy; // tile offset within the source bitmap
int tw, th; // tile size (<= atlasMaxW x atlasMaxH)
int page; // atlas page the tile is packed into; -1 if dropped for capacity
int sx, sy; // packed slot within the page; valid when page >= 0
} PackTile;
typedef struct {
int th; // tile height (the sort key)
int idx; // index into the build-order tiles[] array
} TileSortKey;
static int compareTileKeysByHeightDesc(const void* a, const void* b) {
return ((const TileSortKey*)b)->th - ((const TileSortKey*)a)->th;
}
// Packing/composite policy lives in AssPack.c (pure C, desktop-testable); this
// file owns the JNI boundary, buffer plumbing and logging.
static int imageListHasOutput(ASS_Image* image) {
for (ASS_Image* img = image; img != NULL; img = img->next) {
@@ -293,11 +267,12 @@ static int truncationLogCounter = 0;
// [5]=hasOutput [6]=pageCount
// [7 .. 7+MAX-1] = pageHeights[pageCount]
// [7+MAX .. 7+2*MAX-1] = pageQuadCounts[pageCount]
#define ASS_HEADER_INTS (7 + 2 * MAX_ATLAS_PAGES)
// [7+2*MAX] = mode (ASS_PACK_MODE_ATLAS | ASS_PACK_MODE_COMPOSITE)
#define ASS_HEADER_INTS (7 + 2 * ASS_PACK_MAX_PAGES + 1)
static jint writeAtlasHeader(
JNIEnv* env, jintArray headerBuf, int atlasWidth, int quadCount, int changed, int truncated, int requiredPages,
int hasOutput, int pageCount, const int* pageHeights, const int* pageQuads) {
int hasOutput, int pageCount, const int* pageHeights, const int* pageQuads, int mode) {
int hdr[ASS_HEADER_INTS];
memset(hdr, 0, sizeof(hdr));
hdr[0] = atlasWidth;
@@ -307,36 +282,32 @@ static jint writeAtlasHeader(
hdr[4] = requiredPages;
hdr[5] = hasOutput;
hdr[6] = pageCount;
for (int i = 0; i < pageCount && i < MAX_ATLAS_PAGES; i++) {
for (int i = 0; i < pageCount && i < ASS_PACK_MAX_PAGES; i++) {
hdr[7 + i] = pageHeights ? pageHeights[i] : 0;
hdr[7 + MAX_ATLAS_PAGES + i] = pageQuads ? pageQuads[i] : 0;
hdr[7 + ASS_PACK_MAX_PAGES + i] = pageQuads ? pageQuads[i] : 0;
}
hdr[7 + 2 * ASS_PACK_MAX_PAGES] = mode;
(*env)->SetIntArrayRegion(env, headerBuf, 0, ASS_HEADER_INTS, hdr);
return 1;
}
// Renders a frame into the provided atlas + vertex direct ByteBuffers.
//
// - atlasBuf holds one or more vertically-stacked ALPHA_8 *pages*, each atlasMaxW ×
// atlasMaxH (row stride atlasMaxW); page p starts at byte offset p*atlasMaxW*atlasMaxH.
// The buffer's capacity bounds how many pages this render may fill; AssAtlasFrame
// reports pageHeights (rows worth uploading per page) and requiredPages.
// - In the common ATLAS mode, atlasBuf holds one or more vertically-stacked ALPHA_8
// *pages*, each atlasMaxW × atlasMaxH (row stride atlasMaxW); page p starts at byte
// offset p*atlasMaxW*atlasMaxH. The buffer's capacity bounds how many pages this
// render may fill; AssAtlasFrame reports pageHeights (rows worth uploading per page)
// and requiredPages. UVs are page-local, normalized against atlasMaxW × atlasMaxH.
// - In COMPOSITE mode (frames whose tiles can never fit ASS_PACK_MAX_PAGES pages or
// the vertex budget) atlasBuf instead starts with one premultiplied RGBA rect of
// atlasWidth × pageHeights[0] pixels, drawn as the single emitted quad (UVs 0..1).
// - vertexBuf holds a per-quad vertex stream (6 vertices × (2 pos + 2 uv + 4 color)
// floats = 48 floats = 192 bytes per quad). Must match BYTES_PER_QUAD/VERTEX in
// AssSubtitleAtlasPipeline.kt. UVs are page-local, normalized against atlasMaxW ×
// atlasMaxH (the per-page texture dims).
// - The common case packs everything into a single height-sorted page (minimizes
// packed height, byte-identical to the prior single-page packer). When that
// overflows (a 4K full-screen sign can exceed one GL-max texture), the packer
// spills into additional pages in list order: page assignment is monotonic in
// libass's painter order, so each page's quads are one contiguous run in the
// vertex stream and drawing the pages in turn reproduces the blend order.
// - Vertices are always emitted in original list order (= libass's painter order).
// AssSubtitleAtlasPipeline.kt. Vertices are emitted in libass's painter order.
//
// Never fails on content size: when the frame needs more pages than the buffer holds
// (requiredPages > pageHeights.size) the caller grows the buffer and re-renders; any
// genuinely undrawable tiles (past MAX_ATLAS_PAGES / the vertex budget) are dropped
// and counted in truncated.
// Never drops content for size: when the frame needs more capacity than the buffer
// holds (requiredPages > pageHeights.size) the caller grows the buffer and re-renders;
// frames too dense for the paged atlas flatten into the RGBA composite (see AssPack.c).
//
// Returns 0 for missing buffers/handles (the caller maps that to a null frame); 1 when
// the header was written. On changed == 0 the header carries (atlasWidth=0, quadCount=0,
@@ -359,7 +330,7 @@ JNIEXPORT jint JNICALL Java_com_edde746_plezy_libass_AssRender_nativeAssRenderFr
ANDROID_LOG_WARN, LOG_TAG, "slow render t=%lldms: ass=%lldms (changed=%d, hasOutput=%d)", (long long)time,
tAss - t0, changed, hasOutput);
}
return writeAtlasHeader(env, headerBuf, 0, 0, changed, 0, 1, hasOutput, 1, NULL, NULL);
return writeAtlasHeader(env, headerBuf, 0, 0, changed, 0, 1, hasOutput, 1, NULL, NULL, ASS_PACK_MODE_ATLAS);
}
if (image == NULL) {
@@ -368,7 +339,7 @@ JNIEXPORT jint JNICALL Java_com_edde746_plezy_libass_AssRender_nativeAssRenderFr
ANDROID_LOG_WARN, LOG_TAG, "slow render t=%lldms: ass=%lldms (changed=%d, no output)", (long long)time,
tAss - t0, changed);
}
return writeAtlasHeader(env, headerBuf, 0, 0, changed, 0, 1, 0, 1, NULL, NULL);
return writeAtlasHeader(env, headerBuf, 0, 0, changed, 0, 1, 0, 1, NULL, NULL, ASS_PACK_MODE_ATLAS);
}
uint8_t* atlasPixels = (uint8_t*)(*env)->GetDirectBufferAddress(env, atlasBuf);
@@ -382,253 +353,45 @@ JNIEXPORT jint JNICALL Java_com_edde746_plezy_libass_AssRender_nativeAssRenderFr
(long long)atlasCap);
return 0;
}
// 48 floats per quad × 4 bytes = 192 bytes/quad
const int maxQuads = (int)(vertexCap / 192);
const size_t pageBytes = (size_t)atlasMaxW * atlasMaxH;
int providedPages = (int)(atlasCap / (jlong)pageBytes);
if (providedPages < 1) return 0; // one page is guaranteed above; keep the page math safe
if (providedPages > MAX_ATLAS_PAGES) providedPages = MAX_ATLAS_PAGES;
// Split every image into <= atlasMaxW x atlasMaxH tiles, then pack the tiles.
// tiles[] stays in list order (= blend/painter order for emission); keys[] is
// sorted by height for the single-page pack so it produces tight rows.
int total = 0;
for (ASS_Image* img = image; img != NULL; img = img->next) {
if (img->w > 0 && img->h > 0) {
int cols = (img->w + atlasMaxW - 1) / atlasMaxW;
int rows = (img->h + atlasMaxH - 1) / atlasMaxH;
total += cols * rows;
}
}
if (total == 0) {
return writeAtlasHeader(env, headerBuf, 0, 0, changed, 0, 1, 0, 1, NULL, NULL);
}
PackTile* tiles = (PackTile*)malloc(sizeof(PackTile) * (size_t)total);
TileSortKey* keys = (TileSortKey*)malloc(sizeof(TileSortKey) * (size_t)total);
if (!tiles || !keys) {
free(tiles);
free(keys);
AssPackResult pack;
if (!ass_pack_frame(image, atlasPixels, (size_t)atlasCap, atlasMaxW, atlasMaxH, vertices, (size_t)vertexCap, &pack)) {
return 0;
}
int n = 0;
long long srcPixels = 0;
for (ASS_Image* img = image; img != NULL; img = img->next) {
if (img->w <= 0 || img->h <= 0) continue;
srcPixels += (long long)img->w * img->h;
for (int oy = 0; oy < img->h; oy += atlasMaxH) {
int th = img->h - oy;
if (th > atlasMaxH) th = atlasMaxH;
for (int ox = 0; ox < img->w; ox += atlasMaxW) {
int tw = img->w - ox;
if (tw > atlasMaxW) tw = atlasMaxW;
tiles[n] = (PackTile){.img = img, .ox = ox, .oy = oy, .tw = tw, .th = th, .page = -1, .sx = -1, .sy = -1};
keys[n] = (TileSortKey){.th = th, .idx = n};
n++;
}
}
}
int pageHeights[MAX_ATLAS_PAGES] = {0};
int pageQuads[MAX_ATLAS_PAGES] = {0};
int pageCount = 1;
int requiredPages = 1;
int truncated = 0;
// Pass 1a: height-sorted single page — the common case, minimal packed height
// (byte-identical to the prior single-page packer when the frame fits one page).
qsort(keys, (size_t)n, sizeof(TileSortKey), compareTileKeysByHeightDesc);
int cursorX = 0, cursorY = 0, rowH = 0, packedH = 0, accepted = 0;
for (int i = 0; i < n; i++) {
PackTile* t = &tiles[keys[i].idx];
if (accepted >= maxQuads) break;
int cx = cursorX, cy = cursorY, rh = rowH;
if (cx + t->tw > atlasMaxW) {
cy += rh;
cx = 0;
rh = 0;
}
if (cy + t->th > atlasMaxH) continue; // doesn't fit a single page
t->page = 0;
t->sx = cx;
t->sy = cy;
cursorX = cx + t->tw;
cursorY = cy;
rowH = (t->th > rh) ? t->th : rh;
if (cy + t->th > packedH) packedH = cy + t->th;
accepted++;
}
if (accepted == n) {
pageHeights[0] = packedH;
pageQuads[0] = accepted;
} else {
// Pass 1b: the frame overflows one page. Re-pack in list order, starting a new
// page whenever a tile won't fit the current one. List order keeps the page
// index monotonic in painter order, so each page's quads stay one contiguous run.
for (int i = 0; i < n; i++) {
tiles[i].page = -1;
tiles[i].sx = -1;
tiles[i].sy = -1;
}
int page = 0, cx = 0, cy = 0, rh = 0, placed = 0;
for (int i = 0; i < n; i++) {
PackTile* t = &tiles[i];
if (cx + t->tw > atlasMaxW) {
cy += rh;
cx = 0;
rh = 0;
}
if (cy + t->th > atlasMaxH) {
page++;
cx = 0;
cy = 0;
rh = 0;
}
if (page + 1 > requiredPages) requiredPages = page + 1;
if (page < providedPages && placed < maxQuads) {
t->page = page;
t->sx = cx;
t->sy = cy;
if (cy + t->th > pageHeights[page]) pageHeights[page] = cy + t->th;
pageQuads[page]++;
placed++;
}
cx += t->tw;
rh = (t->th > rh) ? t->th : rh;
}
pageCount = (requiredPages < providedPages) ? requiredPages : providedPages;
accepted = placed;
truncated = n - placed;
}
// Warn only for genuinely-unrecoverable loss. A frame that needs more pages than
// the buffer currently holds, yet fits within MAX_ATLAS_PAGES and the vertex
// budget, is recoverable: the caller grows the buffer and re-renders, so the
// first (discarded) render's truncated > 0 is a false alarm, not data loss. Tiles
// are only truly lost past the page cap or the vertex budget.
const int recoverableGrow = requiredPages <= MAX_ATLAS_PAGES && n <= maxQuads;
if (truncated > 0 && !recoverableGrow && (truncationLogCounter++ & 63) == 0) {
// Warn only for genuinely-unrecoverable loss. A frame that needs more capacity than
// the buffer currently holds is recoverable: the caller grows the buffer and
// re-renders, so the first (discarded) render's truncated > 0 is a false alarm, not
// data loss. With the composite fallback, unrecoverable truncation should be
// unreachable for real content.
const int maxQuads = (int)(vertexCap / 192);
const int recoverableGrow =
pack.requiredPages <= ASS_PACK_MAX_PAGES && (pack.mode == ASS_PACK_MODE_COMPOSITE || pack.totalTiles <= maxQuads);
if (pack.truncated > 0 && !recoverableGrow && (truncationLogCounter++ & 63) == 0) {
__android_log_print(
ANDROID_LOG_WARN, LOG_TAG, "atlas truncation: %d of %d tiles dropped (atlas %dx%d, need %d pages have %d)",
truncated, n, atlasMaxW, atlasMaxH, requiredPages, providedPages);
ANDROID_LOG_WARN, LOG_TAG, "atlas truncation: %d of %d tiles dropped (atlas %dx%d, need %d pages have %lld)",
pack.truncated, pack.totalTiles, atlasMaxW, atlasMaxH, pack.requiredPages,
(long long)(atlasCap / ((jlong)atlasMaxW * atlasMaxH)));
}
if (accepted == 0) {
free(tiles);
free(keys);
return writeAtlasHeader(env, headerBuf, 0, 0, changed, truncated, requiredPages, 1, 1, NULL, NULL);
}
// Clear only the packed rows of each written page.
for (int p = 0; p < pageCount; p++) {
memset(atlasPixels + (size_t)p * pageBytes, 0, (size_t)atlasMaxW * pageHeights[p]);
}
// Emit tiles in list order (= libass's painter/blend order), copying each placed
// tile into its page slot and emitting its quad. Monotonic page assignment makes
// each page's quads a contiguous run, matching pageQuads[] for the per-page draw.
int qi = 0;
for (int i = 0; i < n; i++) {
PackTile* t = &tiles[i];
if (t->page < 0) continue;
ASS_Image* img = t->img;
const int px = t->sx;
const int py = t->sy;
uint8_t* pageBase = atlasPixels + (size_t)t->page * pageBytes;
for (int y = 0; y < t->th; y++) {
uint8_t* dst = pageBase + (size_t)(py + y) * atlasMaxW + px;
const uint8_t* src = img->bitmap + (size_t)(t->oy + y) * img->stride + t->ox;
memcpy(dst, src, (size_t)t->tw);
}
const float x0 = (float)(img->dst_x + t->ox);
const float y0 = (float)(img->dst_y + t->oy);
const float x1 = x0 + (float)t->tw;
const float y1 = y0 + (float)t->th;
const float u0 = (float)px / (float)atlasMaxW;
const float v0 = (float)py / (float)atlasMaxH;
const float u1 = (float)(px + t->tw) / (float)atlasMaxW;
const float v1 = (float)(py + t->th) / (float)atlasMaxH;
const unsigned int c = img->color;
const float r = (float)((c >> 24) & 0xFFu) / 255.0f;
const float g = (float)((c >> 16) & 0xFFu) / 255.0f;
const float b = (float)((c >> 8) & 0xFFu) / 255.0f;
const float a = (float)(0xFFu - (c & 0xFFu)) / 255.0f;
float* vx = vertices + (size_t)qi * 48;
// 8 floats per vertex: x, y, u, v, r, g, b, a.
// Triangle 1: (x0,y0) (x1,y0) (x0,y1)
vx[0] = x0;
vx[1] = y0;
vx[2] = u0;
vx[3] = v0;
vx[4] = r;
vx[5] = g;
vx[6] = b;
vx[7] = a;
vx[8] = x1;
vx[9] = y0;
vx[10] = u1;
vx[11] = v0;
vx[12] = r;
vx[13] = g;
vx[14] = b;
vx[15] = a;
vx[16] = x0;
vx[17] = y1;
vx[18] = u0;
vx[19] = v1;
vx[20] = r;
vx[21] = g;
vx[22] = b;
vx[23] = a;
// Triangle 2: (x1,y0) (x1,y1) (x0,y1)
vx[24] = x1;
vx[25] = y0;
vx[26] = u1;
vx[27] = v0;
vx[28] = r;
vx[29] = g;
vx[30] = b;
vx[31] = a;
vx[32] = x1;
vx[33] = y1;
vx[34] = u1;
vx[35] = v1;
vx[36] = r;
vx[37] = g;
vx[38] = b;
vx[39] = a;
vx[40] = x0;
vx[41] = y1;
vx[42] = u0;
vx[43] = v1;
vx[44] = r;
vx[45] = g;
vx[46] = b;
vx[47] = a;
qi++;
}
free(tiles);
free(keys);
// Slow-render breakdown: separates libass's own cost (rasterize/blur/shape)
// from this function's packing + memcpy, so device logs attribute the time.
// from this function's packing/compositing, so device logs attribute the time.
const long long tEnd = nowMs();
if (tEnd - t0 > 40) {
__android_log_print(
ANDROID_LOG_WARN, LOG_TAG,
"slow render t=%lldms: total=%lldms ass=%lldms pack+copy=%lldms images=%d srcPx=%lldk "
"atlas=%dx%d pages=%d quads=%d",
(long long)time, tEnd - t0, tAss - t0, tEnd - tAss, n, srcPixels / 1000, atlasMaxW, atlasMaxH, pageCount, qi);
"atlas=%dx%d pages=%d quads=%d mode=%d",
(long long)time, tEnd - t0, tAss - t0, tEnd - tAss, pack.totalTiles, pack.srcPixels / 1000, atlasMaxW,
atlasMaxH, pack.pageCount, pack.quadCount, pack.mode);
}
// atlasWidth is the full row stride (GLES2 can't upload with stride ≠ width);
// pageHeights/pageQuadCounts describe the per-page upload + draw ranges.
// ATLAS: atlasWidth is the full row stride (GLES2 can't upload with stride ≠ width)
// and pageHeights/pageQuadCounts describe the per-page upload + draw ranges.
// COMPOSITE: atlasWidth × pageHeights[0] are the RGBA rect dims for the one quad.
return writeAtlasHeader(
env, headerBuf, atlasMaxW, qi, changed, truncated, requiredPages, 1, pageCount, pageHeights, pageQuads);
env, headerBuf, pack.atlasWidth, pack.quadCount, changed, pack.truncated, pack.requiredPages,
pack.totalTiles > 0 ? 1 : 0, pack.pageCount, pack.pageHeights, pack.pageQuads, pack.mode);
}
// --- AssFrameTimestamps (EGL_ANDROID_get_frame_timestamps) ---
+300
View File
@@ -0,0 +1,300 @@
#include "AssPack.h"
#include <limits.h>
#include <stdlib.h>
#include <string.h>
// A tile is a <= atlasMaxW x atlasMaxH sub-rect of an ASS_Image. A single image
// can exceed one atlas page only when the render frame is larger than a page
// (>4K, or a GPU whose max texture is below the frame) — multi-page can't split
// one image across pages (a quad samples one texture), so tiling does, keeping
// the never-drop guarantee. (#1436 itself was atlas-AREA overflow, fixed by the
// multi-page pack below, not oversized single images.) Tiles are built in list
// order (= libass blend/painter order, preserved for emission); the single-page
// pack runs height-sorted via a separate key array so emission order is untouched.
typedef struct {
ASS_Image* img; // source image (for bitmap/stride/color/dst_x/dst_y)
int ox, oy; // tile offset within the source bitmap
int tw, th; // tile size (<= atlasMaxW x atlasMaxH)
int page; // atlas page the tile is packed into; -1 if dropped for capacity
int sx, sy; // packed slot within the page; valid when page >= 0
} PackTile;
typedef struct {
int th; // tile height (the sort key)
int idx; // index into the build-order tiles[] array
} TileSortKey;
static int compareTileKeysByHeightDesc(const void* a, const void* b) {
return ((const TileSortKey*)b)->th - ((const TileSortKey*)a)->th;
}
// 8 floats per vertex (x, y, u, v, r, g, b, a) x 6 vertices; layout must match
// BYTES_PER_QUAD/VERTEX in AssSubtitleAtlasPipeline.kt.
static void emitQuad(
float* vx, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float r, float g,
float b, float a) {
const float pos[6][2] = {{x0, y0}, {x1, y0}, {x0, y1}, {x1, y0}, {x1, y1}, {x0, y1}};
const float uv[6][2] = {{u0, v0}, {u1, v0}, {u0, v1}, {u1, v0}, {u1, v1}, {u0, v1}};
for (int i = 0; i < 6; i++) {
*vx++ = pos[i][0];
*vx++ = pos[i][1];
*vx++ = uv[i][0];
*vx++ = uv[i][1];
*vx++ = r;
*vx++ = g;
*vx++ = b;
*vx++ = a;
}
}
// Flattens the whole image list into one premultiplied RGBA rect (the union
// bounding box) at the start of `atlasPixels`, emitted as a single quad. The
// blend is libass painter-order src-over, the same math the GL path applies to
// alpha-atlas quads, so output is visually identical — memory just becomes
// O(union area <= frame area) instead of O(sum of image areas). Used for
// frames whose summed image area cannot fit ASS_PACK_MAX_PAGES alpha pages
// (#1868: ~150 overlapping paint-strokes put ~5 pages of tiles at 1080p and
// ~19 at 4K behind a 4-page cap, silently dropping the painter-order tail —
// the sign's text).
static void compositeFrame(
ASS_Image* image, uint8_t* atlasPixels, size_t atlasCap, size_t pageBytes, float* vertices, size_t vertexCap,
AssPackResult* out) {
int ux0 = INT_MAX, uy0 = INT_MAX, ux1 = INT_MIN, uy1 = INT_MIN;
for (ASS_Image* img = image; img != NULL; img = img->next) {
if (img->w <= 0 || img->h <= 0) continue;
if (img->dst_x < ux0) ux0 = img->dst_x;
if (img->dst_y < uy0) uy0 = img->dst_y;
if (img->dst_x + img->w > ux1) ux1 = img->dst_x + img->w;
if (img->dst_y + img->h > uy1) uy1 = img->dst_y + img->h;
}
const int uw = ux1 - ux0;
const int uh = uy1 - uy0;
const size_t rgbaBytes = (size_t)uw * uh * 4;
out->mode = ASS_PACK_MODE_COMPOSITE;
out->requiredPages = (int)((rgbaBytes + pageBytes - 1) / pageBytes);
out->pageCount = 1;
if (rgbaBytes > atlasCap || vertexCap < 192) {
// Buffers too small for the flattened rect: report the needed capacity and
// write nothing — the caller grows and re-renders (same contract as the
// multi-page atlas grow). truncated flags the frame as not presentable.
out->truncated = out->totalTiles;
return;
}
memset(atlasPixels, 0, rgbaBytes);
for (ASS_Image* img = image; img != NULL; img = img->next) {
if (img->w <= 0 || img->h <= 0) continue;
const unsigned int c = img->color;
const unsigned cr = (c >> 24) & 0xFFu;
const unsigned cg = (c >> 16) & 0xFFu;
const unsigned cb = (c >> 8) & 0xFFu;
const unsigned ca = 0xFFu - (c & 0xFFu);
if (ca == 0) continue;
for (int y = 0; y < img->h; y++) {
const uint8_t* src = img->bitmap + (size_t)y * img->stride;
uint8_t* dst = atlasPixels + (((size_t)(img->dst_y - uy0 + y) * uw) + (size_t)(img->dst_x - ux0)) * 4;
for (int x = 0; x < img->w; x++, dst += 4) {
const unsigned a = (src[x] * ca + 127u) / 255u;
if (a == 0) continue;
const unsigned inv = 255u - a;
dst[0] = (uint8_t)((cr * a + dst[0] * inv + 127u) / 255u);
dst[1] = (uint8_t)((cg * a + dst[1] * inv + 127u) / 255u);
dst[2] = (uint8_t)((cb * a + dst[2] * inv + 127u) / 255u);
dst[3] = (uint8_t)((255u * a + dst[3] * inv + 127u) / 255u);
}
}
}
out->atlasWidth = uw;
out->pageHeights[0] = uh;
out->pageQuads[0] = 1;
out->quadCount = 1;
out->truncated = 0;
emitQuad(
vertices, (float)ux0, (float)uy0, (float)(ux0 + uw), (float)(uy0 + uh), 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
1.0f);
}
int ass_pack_frame(
ASS_Image* image, uint8_t* atlasPixels, size_t atlasCap, int atlasMaxW, int atlasMaxH, float* vertices,
size_t vertexCap, AssPackResult* out) {
memset(out, 0, sizeof(*out));
out->mode = ASS_PACK_MODE_ATLAS;
out->requiredPages = 1;
out->pageCount = 1;
// 48 floats per quad x 4 bytes = 192 bytes/quad
const int maxQuads = (int)(vertexCap / 192);
const size_t pageBytes = (size_t)atlasMaxW * atlasMaxH;
int providedPages = (int)(atlasCap / pageBytes);
if (providedPages > ASS_PACK_MAX_PAGES) providedPages = ASS_PACK_MAX_PAGES;
// Split every image into <= atlasMaxW x atlasMaxH tiles, then pack the tiles.
// tiles[] stays in list order (= blend/painter order for emission); keys[] is
// sorted by height for the single-page pack so it produces tight rows.
int total = 0;
for (ASS_Image* img = image; img != NULL; img = img->next) {
if (img->w > 0 && img->h > 0) {
int cols = (img->w + atlasMaxW - 1) / atlasMaxW;
int rows = (img->h + atlasMaxH - 1) / atlasMaxH;
total += cols * rows;
}
}
out->totalTiles = total;
if (total == 0) return 1;
PackTile* tiles = (PackTile*)malloc(sizeof(PackTile) * (size_t)total);
TileSortKey* keys = (TileSortKey*)malloc(sizeof(TileSortKey) * (size_t)total);
if (!tiles || !keys) {
free(tiles);
free(keys);
return 0;
}
int n = 0;
for (ASS_Image* img = image; img != NULL; img = img->next) {
if (img->w <= 0 || img->h <= 0) continue;
out->srcPixels += (long long)img->w * img->h;
for (int oy = 0; oy < img->h; oy += atlasMaxH) {
int th = img->h - oy;
if (th > atlasMaxH) th = atlasMaxH;
for (int ox = 0; ox < img->w; ox += atlasMaxW) {
int tw = img->w - ox;
if (tw > atlasMaxW) tw = atlasMaxW;
tiles[n] = (PackTile){.img = img, .ox = ox, .oy = oy, .tw = tw, .th = th, .page = -1, .sx = -1, .sy = -1};
keys[n] = (TileSortKey){.th = th, .idx = n};
n++;
}
}
}
int truncated = 0;
// Pass 1a: height-sorted single page — the common case, minimal packed height
// (byte-identical to the prior single-page packer when the frame fits one page).
qsort(keys, (size_t)n, sizeof(TileSortKey), compareTileKeysByHeightDesc);
int cursorX = 0, cursorY = 0, rowH = 0, packedH = 0, accepted = 0;
for (int i = 0; i < n; i++) {
PackTile* t = &tiles[keys[i].idx];
if (accepted >= maxQuads) break;
int cx = cursorX, cy = cursorY, rh = rowH;
if (cx + t->tw > atlasMaxW) {
cy += rh;
cx = 0;
rh = 0;
}
if (cy + t->th > atlasMaxH) continue; // doesn't fit a single page
t->page = 0;
t->sx = cx;
t->sy = cy;
cursorX = cx + t->tw;
cursorY = cy;
rowH = (t->th > rh) ? t->th : rh;
if (cy + t->th > packedH) packedH = cy + t->th;
accepted++;
}
if (accepted == n) {
out->pageHeights[0] = packedH;
out->pageQuads[0] = accepted;
} else {
// Pass 1b: the frame overflows one page. Re-pack in list order, starting a new
// page whenever a tile won't fit the current one. List order keeps the page
// index monotonic in painter order, so each page's quads stay one contiguous run.
for (int i = 0; i < n; i++) {
tiles[i].page = -1;
tiles[i].sx = -1;
tiles[i].sy = -1;
}
int requiredPages = 1;
int page = 0, cx = 0, cy = 0, rh = 0, placed = 0;
for (int i = 0; i < n; i++) {
PackTile* t = &tiles[i];
if (cx + t->tw > atlasMaxW) {
cy += rh;
cx = 0;
rh = 0;
}
if (cy + t->th > atlasMaxH) {
page++;
cx = 0;
cy = 0;
rh = 0;
}
if (page + 1 > requiredPages) requiredPages = page + 1;
if (page < providedPages && placed < maxQuads) {
t->page = page;
t->sx = cx;
t->sy = cy;
if (cy + t->th > out->pageHeights[page]) out->pageHeights[page] = cy + t->th;
out->pageQuads[page]++;
placed++;
}
cx += t->tw;
rh = (t->th > rh) ? t->th : rh;
}
if (requiredPages > ASS_PACK_MAX_PAGES || n > maxQuads) {
// The frame can never fit the paged alpha atlas: its tiles exceed the page
// cap or the vertex budget outright. Flatten instead of dropping the
// painter-order tail (#1868).
free(tiles);
free(keys);
memset(out->pageHeights, 0, sizeof(out->pageHeights));
memset(out->pageQuads, 0, sizeof(out->pageQuads));
compositeFrame(image, atlasPixels, atlasCap, pageBytes, vertices, vertexCap, out);
return 1;
}
out->requiredPages = requiredPages;
out->pageCount = (requiredPages < providedPages) ? requiredPages : providedPages;
accepted = placed;
truncated = n - placed;
}
out->truncated = truncated;
if (accepted == 0) {
free(tiles);
free(keys);
memset(out->pageHeights, 0, sizeof(out->pageHeights));
memset(out->pageQuads, 0, sizeof(out->pageQuads));
return 1;
}
// Clear only the packed rows of each written page.
for (int p = 0; p < out->pageCount; p++) {
memset(atlasPixels + (size_t)p * pageBytes, 0, (size_t)atlasMaxW * out->pageHeights[p]);
}
// Emit tiles in list order (= libass's painter/blend order), copying each placed
// tile into its page slot and emitting its quad. Monotonic page assignment makes
// each page's quads a contiguous run, matching pageQuads[] for the per-page draw.
int qi = 0;
for (int i = 0; i < n; i++) {
PackTile* t = &tiles[i];
if (t->page < 0) continue;
ASS_Image* img = t->img;
const int px = t->sx;
const int py = t->sy;
uint8_t* pageBase = atlasPixels + (size_t)t->page * pageBytes;
for (int y = 0; y < t->th; y++) {
uint8_t* dst = pageBase + (size_t)(py + y) * atlasMaxW + px;
const uint8_t* src = img->bitmap + (size_t)(t->oy + y) * img->stride + t->ox;
memcpy(dst, src, (size_t)t->tw);
}
const unsigned int c = img->color;
emitQuad(
vertices + (size_t)qi * 48, (float)(img->dst_x + t->ox), (float)(img->dst_y + t->oy),
(float)(img->dst_x + t->ox + t->tw), (float)(img->dst_y + t->oy + t->th), (float)px / (float)atlasMaxW,
(float)py / (float)atlasMaxH, (float)(px + t->tw) / (float)atlasMaxW, (float)(py + t->th) / (float)atlasMaxH,
(float)((c >> 24) & 0xFFu) / 255.0f, (float)((c >> 16) & 0xFFu) / 255.0f, (float)((c >> 8) & 0xFFu) / 255.0f,
(float)(0xFFu - (c & 0xFFu)) / 255.0f);
qi++;
}
free(tiles);
free(keys);
out->quadCount = qi;
out->atlasWidth = atlasMaxW;
return 1;
}
+57
View File
@@ -0,0 +1,57 @@
// Pure-C frame packer behind the JNI atlas render entry point (AssKt.c).
// Kept free of JNI/Android includes so desktop test harnesses can compile the
// exact shipped packing/composite logic against a host libass build.
#ifndef PLEZY_ASS_PACK_H
#define PLEZY_ASS_PACK_H
#include <stddef.h>
#include <stdint.h>
#include "ass/ass.h"
// Hard cap on atlas pages. 4 pages of a GL-max texture covers the worst real
// frame measured for ordinary typesetting (a 4K-rendered full-screen letter
// needs 3); denser frames (#1868: ~1500 overlapping paint-stroke images whose
// summed area is ~19 pages at 4K) fall back to the RGBA composite below instead
// of dropping tiles. Must match AssAtlasPipelineConfig.MAX_ATLAS_PAGES.
#define ASS_PACK_MAX_PAGES 4
// Result modes. ATLAS: one or more ALPHA_8 pages plus per-quad vertices, the
// common fast path. COMPOSITE: the frame's images were flattened CPU-side into
// a single premultiplied RGBA rect (the union bounding box) drawn as one quad —
// memory is bounded by frame area instead of the sum of per-image areas.
#define ASS_PACK_MODE_ATLAS 0
#define ASS_PACK_MODE_COMPOSITE 1
typedef struct {
int mode; // ASS_PACK_MODE_*
// ATLAS: row stride of every page. COMPOSITE: width of the RGBA rect.
int atlasWidth;
int quadCount;
// Tiles dropped for capacity (frame incomplete). Composite output never
// drops content; it reports truncated == totalTiles only in the
// nothing-written grow request state (quadCount == 0).
int truncated;
// Pages of caller buffer capacity this frame needs to render completely.
// When it exceeds the provided capacity the caller grows and re-renders.
int requiredPages;
int pageCount;
int totalTiles; // tiles the frame splits into (caller-side logging)
long long srcPixels; // summed source image area (caller-side logging)
// ATLAS: packed rows per page / quads per page (contiguous vertex runs).
// COMPOSITE: pageHeights[0] = RGBA rect height, pageQuads[0] = 1.
int pageHeights[ASS_PACK_MAX_PAGES];
int pageQuads[ASS_PACK_MAX_PAGES];
} AssPackResult;
// Packs libass's image list for `atlasPixels`/`vertices` (layout documented at
// the JNI entry point in AssKt.c). Never drops content for size: frames that
// cannot fit ASS_PACK_MAX_PAGES alpha pages (or the vertex budget) flatten into
// an RGBA composite; when the provided buffers are too small for either
// representation, requiredPages tells the caller how much to grow before
// re-rendering, and nothing is written. Returns 0 only on allocation failure.
int ass_pack_frame(
ASS_Image* image, uint8_t* atlasPixels, size_t atlasCap, int atlasMaxW, int atlasMaxH, float* vertices,
size_t vertexCap, AssPackResult* out);
#endif // PLEZY_ASS_PACK_H
+1 -1
View File
@@ -44,7 +44,7 @@ set_target_properties(ass PROPERTIES
IMPORTED_LOCATION "${LIBASS_ARCHIVE}")
target_include_directories(ass INTERFACE "${LIBASS_ROOT}/include")
add_library(${CMAKE_PROJECT_NAME} SHARED AssKt.c SurfaceTxProbe.c)
add_library(${CMAKE_PROJECT_NAME} SHARED AssKt.c AssPack.c SurfaceTxProbe.c)
add_dependencies(${CMAKE_PROJECT_NAME} libass_prebuilt_${LIBASS_ABI_TARGET})
# HarfBuzz brings C++; link the shared STL that the app already packages.
# (SurfaceTxProbe resolves its libandroid/libsync entry points via dlsym, so no extra link.)
@@ -4,33 +4,44 @@ package com.edde746.plezy.libass
* Result of a packed-atlas render. The atlas pixel data is stored in the direct ByteBuffer
* that was passed into [AssRender.renderFrameAtlas]; the vertex stream is in the other.
*
* The atlas may span more than one *page* — a heavy full-screen sign can produce more
* sub-pixels than a single GL-max texture holds. Pages are vertically stacked in the
* atlas ByteBuffer (page `p` at byte offset `p * atlasWidth * atlasMaxHeight`), each its
* own texture, and are drawn in turn. Quads are emitted in libass painter order and page
* assignment is monotonic in that order, so each page's quads form one contiguous run in
* the vertex stream ([pageQuadCounts]); the runner uploads page `p`, then draws its run.
* In the common [MODE_ATLAS] the atlas may span more than one ALPHA_8 *page* — a heavy
* full-screen sign can produce more sub-pixels than a single GL-max texture holds. Pages
* are vertically stacked in the atlas ByteBuffer (page `p` at byte offset
* `p * atlasWidth * atlasMaxHeight`), each its own texture, and are drawn in turn. Quads
* are emitted in libass painter order and page assignment is monotonic in that order, so
* each page's quads form one contiguous run in the vertex stream ([pageQuadCounts]); the
* runner uploads page `p`, then draws its run.
*
* In [MODE_COMPOSITE] the frame was too dense for the paged atlas (its summed image area
* exceeds every page: overlapping paint-stroke signs, #1868) and the native side flattened
* it into one premultiplied RGBA rect of [atlasWidth] × `pageHeights[0]` pixels at the
* start of the atlas ByteBuffer, drawn as the single emitted quad with UVs 0..1.
*
* Built in Kotlin by [AssRender.renderFrameAtlas] from the int[] header the native
* renderer fills (see `writeAtlasHeader` in AssKt.c) — never constructed from JNI, so
* the minifier may obfuscate it freely without breaking the native boundary.
*
* @param atlasWidth atlas row stride in pixels (= the allocated width; same for every
* page; 0 when [changed] == 0)
* page; 0 when [changed] == 0) — or the RGBA rect width in
* [MODE_COMPOSITE]
* @param pageHeights packed height (rows worth uploading) of each page; `size` = page count
* @param pageQuadCounts quads on each page, contiguous in the vertex stream in this order;
* `size` = page count, `sum` = [quadCount]
* @param quadCount total quads; the vertex buffer holds [quadCount] * 6 vertices
* @param changed libass change flag (0 = no change, 1 = positions, 2 = content)
* @param truncated images dropped because the frame needed more than [requiredPages]
* pages of capacity or exceeded the vertex budget; the frame is
* incomplete but never stale (should be unreachable for real content)
* @param requiredPages pages this frame needs to render completely. When it exceeds
* [pageHeights].size the caller must grow the atlas buffer and
* re-render; the rendered pages are still valid in the meantime.
* @param truncated images dropped because the frame needed more capacity than the
* buffer holds; the frame is incomplete but never stale. Recoverable
* by growing to [requiredPages]; with the composite fallback,
* unrecoverable truncation should be unreachable for real content
* @param requiredPages pages of buffer capacity this frame needs to render completely.
* When it exceeds [pageHeights].size the caller must grow the atlas
* buffer and re-render; the rendered pages are still valid in the
* meantime.
* @param hasOutput true when libass reported at least one visible image for this
* timestamp, even when [changed] is 0 and the buffers were not
* rewritten. false means this timestamp should be blank.
* @param mode [MODE_ATLAS] or [MODE_COMPOSITE]; must match the ASS_PACK_MODE_*
* constants in AssPack.h
*/
class AssAtlasFrame(
val atlasWidth: Int,
@@ -40,8 +51,17 @@ class AssAtlasFrame(
val changed: Int,
val truncated: Int,
val requiredPages: Int,
val hasOutput: Boolean
val hasOutput: Boolean,
val mode: Int = MODE_ATLAS
) {
companion object {
/** One or more ALPHA_8 atlas pages, per-quad colors in the vertex stream. */
const val MODE_ATLAS = 0
/** One premultiplied RGBA rect at the start of the atlas buffer, one quad. */
const val MODE_COMPOSITE = 1
}
/** Number of atlas pages this frame occupies. */
val pageCount: Int get() = pageHeights.size
@@ -8,9 +8,9 @@ class AssRender(nativeAss: Long, private val lock: ReentrantLock) {
companion object {
/** Must match MAX_ATLAS_PAGES + the header layout in AssKt.c (`writeAtlasHeader`). */
/** Must match ASS_PACK_MAX_PAGES + the header layout in AssPack.h/AssKt.c (`writeAtlasHeader`). */
private const val MAX_ATLAS_PAGES = 4
private const val HEADER_INTS = 7 + 2 * MAX_ATLAS_PAGES
private const val HEADER_INTS = 7 + 2 * MAX_ATLAS_PAGES + 1
@JvmStatic
external fun nativeAssRenderInit(ass: Long): Long
@@ -175,7 +175,8 @@ class AssRender(nativeAss: Long, private val lock: ReentrantLock) {
changed = header[2],
truncated = header[3],
requiredPages = header[4],
hasOutput = header[5] != 0
hasOutput = header[5] != 0,
mode = header[7 + 2 * MAX_ATLAS_PAGES]
)
}
}
@@ -58,9 +58,11 @@ internal object AssAtlasPipelineConfig {
* Hard cap on vertically-stacked atlas pages per slot. A frame whose packed
* sub-pixels exceed one [ATLAS_PIXEL_BUDGET] texture (a 4K-rendered full-screen
* typeset sign) spills into extra pages so nothing is dropped (#1436); the atlas
* buffer grows on demand toward this cap. 4 covers the worst frame measured (a 4K
* letter needs 3); past it tiles are dropped and counted in `truncated`. Must match
* MAX_ATLAS_PAGES in AssKt.c.
* buffer grows on demand toward this cap. 4 covers the worst frame measured for
* ordinary typesetting (a 4K letter needs 3); frames too dense even for that
* (overlapping paint-stroke signs, #1868) flatten into a single RGBA composite
* ([AssAtlasFrame.MODE_COMPOSITE]) instead of dropping tiles. Must match
* ASS_PACK_MAX_PAGES in AssPack.h.
*/
internal const val MAX_ATLAS_PAGES = 4
@@ -718,8 +720,9 @@ private class AtlasLibassThread(
var frame = render.renderFrameAtlas(timeMs, payload.atlasBuf, slots.atlasW, slots.atlasH, payload.vertexBuf)
?: return null
// A frame overflows one atlas page only on dense full-screen typesetting. When it
// does, grow this slot's buffer to the pages it needs (capped) and render once more
// — libass's caches make the re-render cheap, and the slot keeps the larger buffer
// does — multi-page atlas or an RGBA composite rect needing more than one page —
// grow this slot's buffer to the pages it needs (capped) and render once more:
// libass's caches make the re-render cheap, and the slot keeps the larger buffer
// so the same density never re-grows. The truncated first result is never handed off.
if (frame.requiredPages > payload.pageCapacity && payload.pageCapacity < AssAtlasPipelineConfig.MAX_ATLAS_PAGES) {
payload.growAtlas(
@@ -794,7 +797,8 @@ private class AtlasLibassThread(
"releaseLeadMs=${request.releaseLeadNs / 1_000_000} seq=${payload.contentSeq} waitMs=$waitMs budgetMs=$budgetMs " +
"libassMs=$lastLibassMs lockWaitMs=${assHandler.render?.lastLockWaitMs} " +
"specHit=${outcome.specHit} changed=${payload.frame.changed} output=${payload.frame.hasOutput} quads=${payload.frame.quadCount} " +
"atlas=${payload.frame.atlasWidth}x${payload.frame.atlasHeight} truncated=${payload.frame.truncated}"
"atlas=${payload.frame.atlasWidth}x${payload.frame.atlasHeight} truncated=${payload.frame.truncated} " +
"mode=${payload.frame.mode}"
)
}
}
@@ -1403,15 +1407,19 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
}
""".trimIndent()
// u_Rgba switches the sampling mode: 0 = ALPHA_8 atlas mask tinted by the per-vertex
// color (the common path), 1 = premultiplied RGBA composite sampled directly
// (AssAtlasFrame.MODE_COMPOSITE). Both end premultiplied, matching the blend state.
private val fragmentShaderCode = """
precision mediump float;
varying vec2 v_TexCoord;
varying vec4 v_Color;
uniform sampler2D u_Texture;
uniform float u_Rgba;
void main() {
float mask = texture2D(u_Texture, v_TexCoord).a;
float alpha = v_Color.a * mask;
gl_FragColor = vec4(v_Color.rgb * alpha, alpha);
vec4 texel = texture2D(u_Texture, v_TexCoord);
float alpha = v_Color.a * texel.a;
gl_FragColor = mix(vec4(v_Color.rgb * alpha, alpha), texel, u_Rgba);
}
""".trimIndent()
@@ -1423,11 +1431,18 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
private var allocatedPages = 0
private var vertexBufferId = 0
// Texture for MODE_COMPOSITE frames (premultiplied RGBA rect); allocated lazily on
// the first composite frame and re-specced when the rect outgrows it.
private var rgbaTexId = 0
private var rgbaAllocW = 0
private var rgbaAllocH = 0
private var aPosition = 0
private var aTexCoord = 0
private var aColor = 0
private var uTexture = 0
private var uSurfaceSize = 0
private var uRgba = 0
private var atlasAllocatedW = 0
private var atlasAllocatedH = 0
@@ -1477,6 +1492,7 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
aColor = glProgram.getAttributeArrayLocationAndEnable("a_Color")
uTexture = glProgram.getUniformLocation("u_Texture")
uSurfaceSize = glProgram.getUniformLocation("u_SurfaceSize")
uRgba = glProgram.getUniformLocation("u_Rgba")
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
GLES20.glUniform1i(uTexture, 0)
@@ -1527,11 +1543,22 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
GLES20.glVertexAttribPointer(aPosition, 2, GLES20.GL_FLOAT, false, stride, 0)
GLES20.glVertexAttribPointer(aTexCoord, 2, GLES20.GL_FLOAT, false, stride, 8)
GLES20.glVertexAttribPointer(aColor, 4, GLES20.GL_FLOAT, false, stride, 16)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
if (frame.mode == AssAtlasFrame.MODE_COMPOSITE) {
// The whole frame is one premultiplied RGBA rect (atlasWidth × pageHeights[0])
// at the start of the atlas buffer, drawn as the single emitted quad.
GLES20.glUniform1f(uRgba, 1f)
bindCompositeTexture()
if (!reuseUploads) uploadComposite(payload.atlasBuf, frame.atlasWidth, frame.pageHeights[0])
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, quadCount * 6)
return
}
GLES20.glUniform1f(uRgba, 0f)
// Each atlas page is its own texture; its quads are one contiguous run in the
// stream (page assignment is monotonic in painter order). Upload + draw each in
// turn, which reproduces the libass blend order across pages.
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
var quadOffset = 0
for (p in 0 until frame.pageCount) {
val pageQuads = frame.pageQuadCounts[p]
@@ -1545,6 +1572,48 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
}
}
/** Generates (once) and binds the RGBA composite texture. */
private fun bindCompositeTexture() {
if (rgbaTexId == 0) {
val tex = IntArray(1)
GLES20.glGenTextures(1, tex, 0)
rgbaTexId = tex[0]
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, rgbaTexId)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)
rgbaAllocW = 0
rgbaAllocH = 0
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, rgbaTexId)
}
}
/** Uploads the composite RGBA rect from the start of the stacked atlas buffer into
* the bound composite texture. GLES2 has no UNPACK_ROW_LENGTH, so a sub-image
* update is only stride-correct at the allocated width; otherwise re-spec. */
private fun uploadComposite(atlasBuf: ByteBuffer, width: Int, height: Int) {
if (width <= 0 || height <= 0) return
atlasBuf.clear()
atlasBuf.limit(width * height * 4)
atlasBuf.position(0)
if (width == rgbaAllocW && height <= rgbaAllocH) {
GLES20.glTexSubImage2D(
GLES20.GL_TEXTURE_2D, 0, 0, 0, width, height,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, atlasBuf
)
} else {
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
width, height, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, atlasBuf
)
rgbaAllocW = width
rgbaAllocH = height
}
}
/** Uploads page [page]'s packed rows from the stacked atlas buffer into the
* currently-bound page texture. */
private fun uploadPage(atlasBuf: ByteBuffer, page: Int, atlasW: Int, pageH: Int) {
@@ -1578,6 +1647,13 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
atlasTexIds.fill(0)
allocatedPages = 0
}
if (rgbaTexId != 0) {
val tex = intArrayOf(rgbaTexId)
GLES20.glDeleteTextures(1, tex, 0)
rgbaTexId = 0
rgbaAllocW = 0
rgbaAllocH = 0
}
if (vertexBufferId != 0) {
val buf = intArrayOf(vertexBufferId)
GLES20.glDeleteBuffers(1, buf, 0)