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:
@@ -18,6 +18,7 @@ static inline long long nowMs(void) {
|
|||||||
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "AssPack.h"
|
||||||
#include "ass/ass.h"
|
#include "ass/ass.h"
|
||||||
|
|
||||||
#define LOG_TAG "SubtitleRenderer"
|
#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
|
// Packing/composite policy lives in AssPack.c (pure C, desktop-testable); this
|
||||||
// texture is far above the worst real frame measured (a 4K-rendered full-screen
|
// file owns the JNI boundary, buffer plumbing and logging.
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int imageListHasOutput(ASS_Image* image) {
|
static int imageListHasOutput(ASS_Image* image) {
|
||||||
for (ASS_Image* img = image; img != NULL; img = img->next) {
|
for (ASS_Image* img = image; img != NULL; img = img->next) {
|
||||||
@@ -293,11 +267,12 @@ static int truncationLogCounter = 0;
|
|||||||
// [5]=hasOutput [6]=pageCount
|
// [5]=hasOutput [6]=pageCount
|
||||||
// [7 .. 7+MAX-1] = pageHeights[pageCount]
|
// [7 .. 7+MAX-1] = pageHeights[pageCount]
|
||||||
// [7+MAX .. 7+2*MAX-1] = pageQuadCounts[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(
|
static jint writeAtlasHeader(
|
||||||
JNIEnv* env, jintArray headerBuf, int atlasWidth, int quadCount, int changed, int truncated, int requiredPages,
|
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];
|
int hdr[ASS_HEADER_INTS];
|
||||||
memset(hdr, 0, sizeof(hdr));
|
memset(hdr, 0, sizeof(hdr));
|
||||||
hdr[0] = atlasWidth;
|
hdr[0] = atlasWidth;
|
||||||
@@ -307,36 +282,32 @@ static jint writeAtlasHeader(
|
|||||||
hdr[4] = requiredPages;
|
hdr[4] = requiredPages;
|
||||||
hdr[5] = hasOutput;
|
hdr[5] = hasOutput;
|
||||||
hdr[6] = pageCount;
|
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 + 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);
|
(*env)->SetIntArrayRegion(env, headerBuf, 0, ASS_HEADER_INTS, hdr);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renders a frame into the provided atlas + vertex direct ByteBuffers.
|
// Renders a frame into the provided atlas + vertex direct ByteBuffers.
|
||||||
//
|
//
|
||||||
// - atlasBuf holds one or more vertically-stacked ALPHA_8 *pages*, each atlasMaxW ×
|
// - In the common ATLAS mode, atlasBuf holds one or more vertically-stacked ALPHA_8
|
||||||
// atlasMaxH (row stride atlasMaxW); page p starts at byte offset p*atlasMaxW*atlasMaxH.
|
// *pages*, each atlasMaxW × atlasMaxH (row stride atlasMaxW); page p starts at byte
|
||||||
// The buffer's capacity bounds how many pages this render may fill; AssAtlasFrame
|
// offset p*atlasMaxW*atlasMaxH. The buffer's capacity bounds how many pages this
|
||||||
// reports pageHeights (rows worth uploading per page) and requiredPages.
|
// 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)
|
// - 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
|
// floats = 48 floats = 192 bytes per quad). Must match BYTES_PER_QUAD/VERTEX in
|
||||||
// AssSubtitleAtlasPipeline.kt. UVs are page-local, normalized against atlasMaxW ×
|
// AssSubtitleAtlasPipeline.kt. Vertices are emitted in libass's painter order.
|
||||||
// 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).
|
|
||||||
//
|
//
|
||||||
// Never fails on content size: when the frame needs more pages than the buffer holds
|
// Never drops content for size: when the frame needs more capacity than the buffer
|
||||||
// (requiredPages > pageHeights.size) the caller grows the buffer and re-renders; any
|
// holds (requiredPages > pageHeights.size) the caller grows the buffer and re-renders;
|
||||||
// genuinely undrawable tiles (past MAX_ATLAS_PAGES / the vertex budget) are dropped
|
// frames too dense for the paged atlas flatten into the RGBA composite (see AssPack.c).
|
||||||
// and counted in truncated.
|
|
||||||
//
|
//
|
||||||
// Returns 0 for missing buffers/handles (the caller maps that to a null frame); 1 when
|
// 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,
|
// 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,
|
ANDROID_LOG_WARN, LOG_TAG, "slow render t=%lldms: ass=%lldms (changed=%d, hasOutput=%d)", (long long)time,
|
||||||
tAss - t0, changed, hasOutput);
|
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) {
|
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,
|
ANDROID_LOG_WARN, LOG_TAG, "slow render t=%lldms: ass=%lldms (changed=%d, no output)", (long long)time,
|
||||||
tAss - t0, changed);
|
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);
|
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);
|
(long long)atlasCap);
|
||||||
return 0;
|
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.
|
AssPackResult pack;
|
||||||
// tiles[] stays in list order (= blend/painter order for emission); keys[] is
|
if (!ass_pack_frame(image, atlasPixels, (size_t)atlasCap, atlasMaxW, atlasMaxH, vertices, (size_t)vertexCap, &pack)) {
|
||||||
// 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);
|
|
||||||
return 0;
|
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
|
// Warn only for genuinely-unrecoverable loss. A frame that needs more capacity than
|
||||||
// (byte-identical to the prior single-page packer when the frame fits one page).
|
// the buffer currently holds is recoverable: the caller grows the buffer and
|
||||||
qsort(keys, (size_t)n, sizeof(TileSortKey), compareTileKeysByHeightDesc);
|
// re-renders, so the first (discarded) render's truncated > 0 is a false alarm, not
|
||||||
int cursorX = 0, cursorY = 0, rowH = 0, packedH = 0, accepted = 0;
|
// data loss. With the composite fallback, unrecoverable truncation should be
|
||||||
for (int i = 0; i < n; i++) {
|
// unreachable for real content.
|
||||||
PackTile* t = &tiles[keys[i].idx];
|
const int maxQuads = (int)(vertexCap / 192);
|
||||||
if (accepted >= maxQuads) break;
|
const int recoverableGrow =
|
||||||
int cx = cursorX, cy = cursorY, rh = rowH;
|
pack.requiredPages <= ASS_PACK_MAX_PAGES && (pack.mode == ASS_PACK_MODE_COMPOSITE || pack.totalTiles <= maxQuads);
|
||||||
if (cx + t->tw > atlasMaxW) {
|
if (pack.truncated > 0 && !recoverableGrow && (truncationLogCounter++ & 63) == 0) {
|
||||||
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) {
|
|
||||||
__android_log_print(
|
__android_log_print(
|
||||||
ANDROID_LOG_WARN, LOG_TAG, "atlas truncation: %d of %d tiles dropped (atlas %dx%d, need %d pages have %d)",
|
ANDROID_LOG_WARN, LOG_TAG, "atlas truncation: %d of %d tiles dropped (atlas %dx%d, need %d pages have %lld)",
|
||||||
truncated, n, atlasMaxW, atlasMaxH, requiredPages, providedPages);
|
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)
|
// 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();
|
const long long tEnd = nowMs();
|
||||||
if (tEnd - t0 > 40) {
|
if (tEnd - t0 > 40) {
|
||||||
__android_log_print(
|
__android_log_print(
|
||||||
ANDROID_LOG_WARN, LOG_TAG,
|
ANDROID_LOG_WARN, LOG_TAG,
|
||||||
"slow render t=%lldms: total=%lldms ass=%lldms pack+copy=%lldms images=%d srcPx=%lldk "
|
"slow render t=%lldms: total=%lldms ass=%lldms pack+copy=%lldms images=%d srcPx=%lldk "
|
||||||
"atlas=%dx%d pages=%d quads=%d",
|
"atlas=%dx%d pages=%d quads=%d mode=%d",
|
||||||
(long long)time, tEnd - t0, tAss - t0, tEnd - tAss, n, srcPixels / 1000, atlasMaxW, atlasMaxH, pageCount, qi);
|
(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);
|
// ATLAS: atlasWidth is the full row stride (GLES2 can't upload with stride ≠ width)
|
||||||
// pageHeights/pageQuadCounts describe the per-page upload + draw ranges.
|
// 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(
|
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) ---
|
// --- AssFrameTimestamps (EGL_ANDROID_get_frame_timestamps) ---
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -44,7 +44,7 @@ set_target_properties(ass PROPERTIES
|
|||||||
IMPORTED_LOCATION "${LIBASS_ARCHIVE}")
|
IMPORTED_LOCATION "${LIBASS_ARCHIVE}")
|
||||||
target_include_directories(ass INTERFACE "${LIBASS_ROOT}/include")
|
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})
|
add_dependencies(${CMAKE_PROJECT_NAME} libass_prebuilt_${LIBASS_ABI_TARGET})
|
||||||
# HarfBuzz brings C++; link the shared STL that the app already packages.
|
# 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.)
|
# (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
|
* 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.
|
* 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
|
* In the common [MODE_ATLAS] the atlas may span more than one ALPHA_8 *page* — a heavy
|
||||||
* sub-pixels than a single GL-max texture holds. Pages are vertically stacked in the
|
* full-screen sign can produce more sub-pixels than a single GL-max texture holds. Pages
|
||||||
* atlas ByteBuffer (page `p` at byte offset `p * atlasWidth * atlasMaxHeight`), each its
|
* are vertically stacked in the atlas ByteBuffer (page `p` at byte offset
|
||||||
* own texture, and are drawn in turn. Quads are emitted in libass painter order and page
|
* `p * atlasWidth * atlasMaxHeight`), each its own texture, and are drawn in turn. Quads
|
||||||
* assignment is monotonic in that order, so each page's quads form one contiguous run in
|
* are emitted in libass painter order and page assignment is monotonic in that order, so
|
||||||
* the vertex stream ([pageQuadCounts]); the runner uploads page `p`, then draws its run.
|
* 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
|
* Built in Kotlin by [AssRender.renderFrameAtlas] from the int[] header the native
|
||||||
* renderer fills (see `writeAtlasHeader` in AssKt.c) — never constructed from JNI, so
|
* renderer fills (see `writeAtlasHeader` in AssKt.c) — never constructed from JNI, so
|
||||||
* the minifier may obfuscate it freely without breaking the native boundary.
|
* the minifier may obfuscate it freely without breaking the native boundary.
|
||||||
*
|
*
|
||||||
* @param atlasWidth atlas row stride in pixels (= the allocated width; same for every
|
* @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 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;
|
* @param pageQuadCounts quads on each page, contiguous in the vertex stream in this order;
|
||||||
* `size` = page count, `sum` = [quadCount]
|
* `size` = page count, `sum` = [quadCount]
|
||||||
* @param quadCount total quads; the vertex buffer holds [quadCount] * 6 vertices
|
* @param quadCount total quads; the vertex buffer holds [quadCount] * 6 vertices
|
||||||
* @param changed libass change flag (0 = no change, 1 = positions, 2 = content)
|
* @param changed libass change flag (0 = no change, 1 = positions, 2 = content)
|
||||||
* @param truncated images dropped because the frame needed more than [requiredPages]
|
* @param truncated images dropped because the frame needed more capacity than the
|
||||||
* pages of capacity or exceeded the vertex budget; the frame is
|
* buffer holds; the frame is incomplete but never stale. Recoverable
|
||||||
* incomplete but never stale (should be unreachable for real content)
|
* by growing to [requiredPages]; with the composite fallback,
|
||||||
* @param requiredPages pages this frame needs to render completely. When it exceeds
|
* unrecoverable truncation should be unreachable for real content
|
||||||
* [pageHeights].size the caller must grow the atlas buffer and
|
* @param requiredPages pages of buffer capacity this frame needs to render completely.
|
||||||
* re-render; the rendered pages are still valid in the meantime.
|
* 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
|
* @param hasOutput true when libass reported at least one visible image for this
|
||||||
* timestamp, even when [changed] is 0 and the buffers were not
|
* timestamp, even when [changed] is 0 and the buffers were not
|
||||||
* rewritten. false means this timestamp should be blank.
|
* 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(
|
class AssAtlasFrame(
|
||||||
val atlasWidth: Int,
|
val atlasWidth: Int,
|
||||||
@@ -40,8 +51,17 @@ class AssAtlasFrame(
|
|||||||
val changed: Int,
|
val changed: Int,
|
||||||
val truncated: Int,
|
val truncated: Int,
|
||||||
val requiredPages: 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. */
|
/** Number of atlas pages this frame occupies. */
|
||||||
val pageCount: Int get() = pageHeights.size
|
val pageCount: Int get() = pageHeights.size
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ class AssRender(nativeAss: Long, private val lock: ReentrantLock) {
|
|||||||
|
|
||||||
companion object {
|
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 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
|
@JvmStatic
|
||||||
external fun nativeAssRenderInit(ass: Long): Long
|
external fun nativeAssRenderInit(ass: Long): Long
|
||||||
@@ -175,7 +175,8 @@ class AssRender(nativeAss: Long, private val lock: ReentrantLock) {
|
|||||||
changed = header[2],
|
changed = header[2],
|
||||||
truncated = header[3],
|
truncated = header[3],
|
||||||
requiredPages = header[4],
|
requiredPages = header[4],
|
||||||
hasOutput = header[5] != 0
|
hasOutput = header[5] != 0,
|
||||||
|
mode = header[7 + 2 * MAX_ATLAS_PAGES]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-10
@@ -58,9 +58,11 @@ internal object AssAtlasPipelineConfig {
|
|||||||
* Hard cap on vertically-stacked atlas pages per slot. A frame whose packed
|
* 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
|
* 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
|
* 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
|
* buffer grows on demand toward this cap. 4 covers the worst frame measured for
|
||||||
* letter needs 3); past it tiles are dropped and counted in `truncated`. Must match
|
* ordinary typesetting (a 4K letter needs 3); frames too dense even for that
|
||||||
* MAX_ATLAS_PAGES in AssKt.c.
|
* (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
|
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)
|
var frame = render.renderFrameAtlas(timeMs, payload.atlasBuf, slots.atlasW, slots.atlasH, payload.vertexBuf)
|
||||||
?: return null
|
?: return null
|
||||||
// A frame overflows one atlas page only on dense full-screen typesetting. When it
|
// 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
|
// does — multi-page atlas or an RGBA composite rect needing more than one page —
|
||||||
// — libass's caches make the re-render cheap, and the slot keeps the larger buffer
|
// 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.
|
// 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) {
|
if (frame.requiredPages > payload.pageCapacity && payload.pageCapacity < AssAtlasPipelineConfig.MAX_ATLAS_PAGES) {
|
||||||
payload.growAtlas(
|
payload.growAtlas(
|
||||||
@@ -794,7 +797,8 @@ private class AtlasLibassThread(
|
|||||||
"releaseLeadMs=${request.releaseLeadNs / 1_000_000} seq=${payload.contentSeq} waitMs=$waitMs budgetMs=$budgetMs " +
|
"releaseLeadMs=${request.releaseLeadNs / 1_000_000} seq=${payload.contentSeq} waitMs=$waitMs budgetMs=$budgetMs " +
|
||||||
"libassMs=$lastLibassMs lockWaitMs=${assHandler.render?.lastLockWaitMs} " +
|
"libassMs=$lastLibassMs lockWaitMs=${assHandler.render?.lastLockWaitMs} " +
|
||||||
"specHit=${outcome.specHit} changed=${payload.frame.changed} output=${payload.frame.hasOutput} quads=${payload.frame.quadCount} " +
|
"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()
|
""".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 = """
|
private val fragmentShaderCode = """
|
||||||
precision mediump float;
|
precision mediump float;
|
||||||
varying vec2 v_TexCoord;
|
varying vec2 v_TexCoord;
|
||||||
varying vec4 v_Color;
|
varying vec4 v_Color;
|
||||||
uniform sampler2D u_Texture;
|
uniform sampler2D u_Texture;
|
||||||
|
uniform float u_Rgba;
|
||||||
void main() {
|
void main() {
|
||||||
float mask = texture2D(u_Texture, v_TexCoord).a;
|
vec4 texel = texture2D(u_Texture, v_TexCoord);
|
||||||
float alpha = v_Color.a * mask;
|
float alpha = v_Color.a * texel.a;
|
||||||
gl_FragColor = vec4(v_Color.rgb * alpha, alpha);
|
gl_FragColor = mix(vec4(v_Color.rgb * alpha, alpha), texel, u_Rgba);
|
||||||
}
|
}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
|
||||||
@@ -1423,11 +1431,18 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
|
|||||||
private var allocatedPages = 0
|
private var allocatedPages = 0
|
||||||
private var vertexBufferId = 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 aPosition = 0
|
||||||
private var aTexCoord = 0
|
private var aTexCoord = 0
|
||||||
private var aColor = 0
|
private var aColor = 0
|
||||||
private var uTexture = 0
|
private var uTexture = 0
|
||||||
private var uSurfaceSize = 0
|
private var uSurfaceSize = 0
|
||||||
|
private var uRgba = 0
|
||||||
|
|
||||||
private var atlasAllocatedW = 0
|
private var atlasAllocatedW = 0
|
||||||
private var atlasAllocatedH = 0
|
private var atlasAllocatedH = 0
|
||||||
@@ -1477,6 +1492,7 @@ private class AtlasRenderer(private val assHandler: AssHandler) {
|
|||||||
aColor = glProgram.getAttributeArrayLocationAndEnable("a_Color")
|
aColor = glProgram.getAttributeArrayLocationAndEnable("a_Color")
|
||||||
uTexture = glProgram.getUniformLocation("u_Texture")
|
uTexture = glProgram.getUniformLocation("u_Texture")
|
||||||
uSurfaceSize = glProgram.getUniformLocation("u_SurfaceSize")
|
uSurfaceSize = glProgram.getUniformLocation("u_SurfaceSize")
|
||||||
|
uRgba = glProgram.getUniformLocation("u_Rgba")
|
||||||
|
|
||||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
||||||
GLES20.glUniform1i(uTexture, 0)
|
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(aPosition, 2, GLES20.GL_FLOAT, false, stride, 0)
|
||||||
GLES20.glVertexAttribPointer(aTexCoord, 2, GLES20.GL_FLOAT, false, stride, 8)
|
GLES20.glVertexAttribPointer(aTexCoord, 2, GLES20.GL_FLOAT, false, stride, 8)
|
||||||
GLES20.glVertexAttribPointer(aColor, 4, GLES20.GL_FLOAT, false, stride, 16)
|
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
|
// 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
|
// stream (page assignment is monotonic in painter order). Upload + draw each in
|
||||||
// turn, which reproduces the libass blend order across pages.
|
// turn, which reproduces the libass blend order across pages.
|
||||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
|
|
||||||
var quadOffset = 0
|
var quadOffset = 0
|
||||||
for (p in 0 until frame.pageCount) {
|
for (p in 0 until frame.pageCount) {
|
||||||
val pageQuads = frame.pageQuadCounts[p]
|
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
|
/** Uploads page [page]'s packed rows from the stacked atlas buffer into the
|
||||||
* currently-bound page texture. */
|
* currently-bound page texture. */
|
||||||
private fun uploadPage(atlasBuf: ByteBuffer, page: Int, atlasW: Int, pageH: Int) {
|
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)
|
atlasTexIds.fill(0)
|
||||||
allocatedPages = 0
|
allocatedPages = 0
|
||||||
}
|
}
|
||||||
|
if (rgbaTexId != 0) {
|
||||||
|
val tex = intArrayOf(rgbaTexId)
|
||||||
|
GLES20.glDeleteTextures(1, tex, 0)
|
||||||
|
rgbaTexId = 0
|
||||||
|
rgbaAllocW = 0
|
||||||
|
rgbaAllocH = 0
|
||||||
|
}
|
||||||
if (vertexBufferId != 0) {
|
if (vertexBufferId != 0) {
|
||||||
val buf = intArrayOf(vertexBufferId)
|
val buf = intArrayOf(vertexBufferId)
|
||||||
GLES20.glDeleteBuffers(1, buf, 0)
|
GLES20.glDeleteBuffers(1, buf, 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user