#include "composer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include int dma_heap_alloc(const char *heap_name, size_t size, int *fd); #define MAX_TILES 25 typedef struct { int x, y, w, h; } TileRect; /* Cache of RGA-imported dmabuf handles. importbuffer_fd/releasebuffer_handle * are driver round-trips; doing both per tile per frame costs measurable CPU. * Each entry keeps its own dup'd fd so the fd number can never be recycled * and alias a different buffer while cached. * * DRIVER LIMIT: the RGA driver only keeps a bounded number of imported * handles; once that table is full every new importbuffer_fd returns EINVAL * (and never makes it into the cache, so the table stays full forever = * permanent black tiles). The cache size must stay well below the driver * table limit so a fresh import always has a free slot. 128 was observed to * wedge the table on 8 RV-streams; 64 still failed on the live site; 32 keeps * every buffer that is *currently being blitted* cached (the LRU re-imports * the rest) while leaving the driver table mostly empty. */ typedef struct { int fd; /* our own dup (never the caller's fd) */ ino_t ino; /* dmabuf anon-inode identity */ int w, h, stride, vstride; rga_buffer_handle_t handle; uint64_t last_use; } RgaCacheEnt; #define RGA_CACHE_SIZE 32 struct Composer { int output_w; int output_h; int num_streams; int letterbox; TileRect tiles[MAX_TILES]; RgaCacheEnt rga_cache[RGA_CACHE_SIZE]; uint64_t rga_clock; /* Per-tile health counters for diagnosing flicker: a tile that fails to * import its source buffer keeps the dark clear = a black flashing tile. */ uint64_t src_ok[MAX_TILES]; uint64_t src_import_fail[MAX_TILES]; uint64_t src_improcess_fail[MAX_TILES]; uint64_t dst_fail; /* Emergency low-address mirror per tile (invalid <= 0). See comment * on mirror_refresh(). */ int mirror_fd[MAX_TILES]; uint64_t mirror_hits; }; int composer_grid_rects(int out_w, int out_h, int n, int *xs, int *ys, int *ws, int *hs, int y_off) { if (!xs || !ys || !ws || !hs) return -1; if (n < 0) n = 0; if (n > MAX_TILES) n = MAX_TILES; if (y_off < 0) y_off = 0; if (y_off > out_h) y_off = out_h; int usable_h = out_h - y_off; int cols = 1, rows = 1; if (n <= 1) { cols = 1; rows = 1; } else if (n <= 2) { cols = 2; rows = 1; } else if (n <= 4) { cols = 2; rows = 2; } else if (n <= 6) { cols = 3; rows = 2; } else if (n <= 9) { cols = 3; rows = 3; } else if (n <= 12) { cols = 4; rows = 3; } else if (n <= 16) { cols = 4; rows = 4; } else { cols = 5; rows = 5; } int grid_w = out_w / cols; int grid_h = usable_h / rows; if (grid_h < 1) grid_h = 1; for (int i = 0; i < n; i++) { int col = i % cols; int row = i / cols; int w = grid_w; int h = grid_h; /* Edge column/row extends to the output edge. */ if (col == cols - 1) w = out_w - col * grid_w; if (row == rows - 1) h = out_h - y_off - row * grid_h; xs[i] = col * grid_w; ys[i] = y_off + row * grid_h; ws[i] = w; hs[i] = h; } return n; } int composer_flow_rects(int out_w, int out_h, const int *row_cols, int nrows, int *xs, int *ys, int *ws, int *hs, int y_off) { if (!row_cols || !xs || !ys || !ws || !hs) return -1; if (nrows <= 0) return 0; if (nrows > MAX_TILES) nrows = MAX_TILES; if (y_off < 0) y_off = 0; if (y_off > out_h) y_off = out_h; int usable_h = out_h - y_off; if (usable_h <= 0) return 0; int total = 0; for (int r = 0; r < nrows; r++) { if (row_cols[r] <= 0) continue; total += row_cols[r]; } if (total <= 0) return 0; if (total > MAX_TILES) return -1; /* Row heights: inversely proportional to the number of cells in the row, * so a row with fewer cells ends up taller (a 1-cell "main" row + a * multi-cell "thumbnail" row is the common case). The last row always * extends to the bottom edge so the canvas is fully filled. Each row * height is forced even (NV12 needs even row strides). */ int *rh = NULL; double *wcol = NULL; if (nrows > 1) { rh = calloc((size_t)nrows, sizeof(int)); wcol = calloc((size_t)nrows, sizeof(double)); if (!rh || !wcol) { free(rh); free(wcol); return -1; } double wsum = 0.0; for (int r = 0; r < nrows; r++) if (row_cols[r] > 0) { wcol[r] = 1.0 / (double)row_cols[r]; wsum += wcol[r]; } if (wsum <= 0.0) { free(rh); free(wcol); return 0; } /* All rows except the last: proportional to 1/cols, rounded even, * leaving room for the last row to take whatever is left. */ int sum = 0; for (int r = 0; r < nrows - 1; r++) { if (row_cols[r] <= 0) { rh[r] = 0; continue; } int h = (int)(usable_h * wcol[r] / wsum); if (h & 1) h--; if (h < 2) h = 2; rh[r] = h; sum += h; } /* Last row fills exactly the remaining height (full canvas). */ int last_h = usable_h - sum; if (last_h & 1) { last_h++; for (int r = nrows - 2; r >= 0 && (last_h & 1); r--) if (rh[r] > 2) { rh[r] -= 2; last_h += 2; } } if (last_h < 2 && row_cols[nrows - 1] > 0) last_h = 2; rh[nrows - 1] = last_h; } int idx = 0; int y = y_off; for (int r = 0; r < nrows; r++) { int cols = row_cols[r]; int rheight = rh ? rh[r] : (usable_h - (y - y_off)); /* single row: fill all */ if (cols <= 0 || rheight < 2) continue; if (r == nrows - 1) rheight = usable_h - (y - y_off); /* last row to bottom edge */ for (int c = 0; c < cols; c++) { int w = out_w / cols; int x = c * w; if (c == cols - 1) w = out_w - x; /* last cell to right edge */ if (w < 2) w = 2; if (x < 0) x = 0; xs[idx] = x; ys[idx] = y; ws[idx] = w; hs[idx] = rheight; idx++; } y += rheight; } free(wcol); free(rh); return idx; } int composer_split_rects(int out_w, int out_h, int y_off, const int *top_ids, int top_n, double top_ratio, const int *bottom_ids, int bottom_n, int actual_bottom, int *xs, int *ys, int *ws, int *hs, int *sid) { if (!top_ids || !bottom_ids || !xs || !ys || !ws || !hs || !sid) return -1; if (top_n <= 0 || top_n > MAX_TILES) return -1; if (y_off < 0) y_off = 0; if (y_off > out_h) y_off = out_h; if (top_ratio < 0.05) top_ratio = 0.05; if (top_ratio > 0.95) top_ratio = 0.95; if (actual_bottom < 0) actual_bottom = 0; if (actual_bottom > bottom_n) actual_bottom = bottom_n; if (actual_bottom > MAX_TILES - top_n) actual_bottom = MAX_TILES - top_n; int usable_h = out_h - y_off; if (usable_h <= 0) return 0; /* Top row: height proportional to top_ratio, even dimensions */ int top_h = (int)(usable_h * top_ratio); top_h &= ~1; /* even for NV12 */ if (top_h < 2) top_h = 2; if (top_h > usable_h - 2) top_h = usable_h - 2; int bot_h = usable_h - top_h; if (bot_h < 2) bot_h = 2; int n = 0; /* Top row: top_n cameras, equal width, full row width */ int cell_w = out_w / top_n; for (int i = 0; i < top_n && n < MAX_TILES; i++) { xs[n] = i * cell_w; ys[n] = y_off; ws[n] = (i == top_n - 1) ? out_w - i * cell_w : cell_w; hs[n] = top_h; sid[n] = top_ids[i]; n++; } /* Bottom row: actual_bottom cameras, equal width, centered */ if (actual_bottom > 0) { int bot_w = out_w / actual_bottom; int total_bot_w = bot_w * actual_bottom; int bot_x_start = (out_w - total_bot_w) / 2; bot_x_start &= ~1; /* even offset for NV12 */ for (int i = 0; i < actual_bottom && n < MAX_TILES; i++) { xs[n] = bot_x_start + i * bot_w; ys[n] = y_off + top_h; ws[n] = (i == actual_bottom - 1) ? out_w - bot_x_start - i * bot_w : bot_w; hs[n] = bot_h; sid[n] = bottom_ids[i]; n++; } } return n; } int composer_region_rects(int out_w, int out_h, const int *row_cols, int nrows, const int *cells, int *region_x, int *region_y, int *region_w, int *region_h, int *region_sid, int y_off) { if (!row_cols || !cells || !region_x || !region_y || !region_w || !region_h || !region_sid) return -1; if (nrows <= 0 || nrows > MAX_TILES) return 0; if (y_off < 0) y_off = 0; if (y_off > out_h) y_off = out_h; int usable_h = out_h - y_off; if (usable_h <= 0) return 0; /* ---- per-row geometry (inverse cell-count height, fill to bottom) ---- */ int rh[MAX_TILES]; int ysum = 0; if (nrows > 1) { double wcol[MAX_TILES], wsum = 0.0; for (int r = 0; r < nrows; r++) if (row_cols[r] > 0) { wcol[r] = 1.0 / (double)row_cols[r]; wsum += wcol[r]; } if (wsum <= 0.0) return 0; for (int r = 0; r < nrows - 1; r++) { int h = (int)(usable_h * wcol[r] / wsum); if (h & 1) h--; if (h < 2) h = 2; rh[r] = h; ysum += h; } int last_h = usable_h - ysum; if (last_h & 1) { last_h++; for (int r = nrows - 2; r >= 0 && (last_h & 1); r--) if (rh[r] > 2) { rh[r] -= 2; last_h += 2; } } if (last_h < 2) last_h = 2; rh[nrows - 1] = last_h; } else { rh[0] = usable_h; } /* ---- assign every cell a label bucket ---- */ /* We'll accumulate pixel spans per label directly. */ int lb_minx[MAX_TILES], lb_miny[MAX_TILES]; int lb_maxx[MAX_TILES], lb_maxy[MAX_TILES]; int lb_init[MAX_TILES]; int nlabels = 0; int lbl[MAX_TILES]; for (int i = 0; i < MAX_TILES; i++) lb_init[i] = 0; int idx = 0; int row_top = y_off; for (int r = 0; r < nrows; r++) { int cols = row_cols[r]; int rheight = rh[r]; if (cols <= 0 || rheight < 2) continue; for (int c = 0; c < cols; c++) { int lab = cells[idx]; /* flat, row-major */ int cellw = out_w / cols; int cellx = c * cellw; if (c == cols - 1) cellw = out_w - cellx; if (cellw < 2) cellw = 2; int cell_bot = row_top + rheight; if (lab >= 0) { int li = -1; for (int k = 0; k < nlabels; k++) if (lbl[k] == lab) { li = k; break; } if (li < 0) { if (nlabels >= MAX_TILES) { idx++; continue; } li = nlabels; lbl[li] = lab; lb_minx[li] = cellx; lb_miny[li] = row_top; lb_maxx[li] = cellx + cellw; lb_maxy[li] = cell_bot; lb_init[li] = 1; nlabels++; } else { if (cellx < lb_minx[li]) lb_minx[li] = cellx; if (row_top < lb_miny[li]) lb_miny[li] = row_top; if (cellx + cellw > lb_maxx[li]) lb_maxx[li] = cellx + cellw; if (cell_bot > lb_maxy[li]) lb_maxy[li] = cell_bot; } } idx++; } row_top += rheight; } /* dedupe/merge: same label already merged during accumulation */ int n = 0; for (int k = 0; k < nlabels; k++) { if (!lb_init[k]) continue; region_x[n] = lb_minx[k]; region_y[n] = lb_miny[k]; region_w[n] = lb_maxx[k] - lb_minx[k]; region_h[n] = lb_maxy[k] - lb_miny[k]; if (region_w[n] < 2) region_w[n] = 2; if (region_h[n] < 2) region_h[n] = 2; region_sid[n] = lbl[k]; n++; } return n; } Composer *composer_create(int output_width, int output_height, int num_streams, int y_off) { Composer *comp = calloc(1, sizeof(Composer)); if (!comp) return NULL; comp->output_w = output_width; comp->output_h = output_height; comp->num_streams = num_streams > MAX_TILES ? MAX_TILES : num_streams; /* Pin RGA to the RGA3 core (64-bit addressing). The RGA2 cores are * 32-bit only and refuse any dmabuf whose physical pages sit above 4G * ("rga_mm: RGA_MMU unsupported memory larger than 4G!"), which is what * made importbuffer_fd return EINVAL and black out tiles on the live * site. RGA3 has no such limitation and is faster. If the driver/kernel * rejects it we keep the default scheduler. */ IM_STATUS sc = imconfig(IM_CONFIG_SCHEDULER_CORE, IM_SCHEDULER_RGA3_CORE0); if (sc == IM_STATUS_SUCCESS) fprintf(stderr, "composer: RGA scheduler pinned to RGA3 (64-bit addressing)\n"); else fprintf(stderr, "composer: imconfig(RGA3_CORE0) failed st=%d, using default scheduler\n", sc); int xs[MAX_TILES], ys[MAX_TILES], ws[MAX_TILES], hs[MAX_TILES]; int n = composer_grid_rects(output_width, output_height, comp->num_streams, xs, ys, ws, hs, y_off); for (int i = 0; i < n; i++) { comp->tiles[i].x = xs[i]; comp->tiles[i].y = ys[i]; comp->tiles[i].w = ws[i]; comp->tiles[i].h = hs[i]; } return comp; } void composer_invalidate_all(Composer *comp) { if (!comp) return; for (int i = 0; i < RGA_CACHE_SIZE; i++) { RgaCacheEnt *e = &comp->rga_cache[i]; if (e->handle) releasebuffer_handle(e->handle); if (e->fd > 0) close(e->fd); memset(e, 0, sizeof(*e)); } } void composer_destroy(Composer *comp) { composer_invalidate_all(comp); for (int i = 0; i < MAX_TILES; i++) if (comp->mirror_fd[i] > 0) close(comp->mirror_fd[i]); free(comp); } void composer_get_tile_layout(Composer *comp, int stream_idx, int *x, int *y, int *w, int *h) { if (stream_idx < 0 || stream_idx >= MAX_TILES) { *x = *y = *w = *h = 0; return; } *x = comp->tiles[stream_idx].x; *y = comp->tiles[stream_idx].y; *w = comp->tiles[stream_idx].w; *h = comp->tiles[stream_idx].h; } int composer_set_tiles(Composer *comp, const int *x, const int *y, const int *w, const int *h, int n) { if (!comp || !x || !y || !w || !h) return -1; if (n < 0) n = 0; if (n > MAX_TILES) n = MAX_TILES; for (int i = 0; i < n; i++) { comp->tiles[i].x = x[i]; comp->tiles[i].y = y[i]; comp->tiles[i].w = w[i]; comp->tiles[i].h = h[i]; } return 0; } void composer_set_letterbox(Composer *comp, int enable) { if (comp) comp->letterbox = enable ? 1 : 0; } static void rga_cache_evict_one(Composer *comp) { RgaCacheEnt *oldest = NULL; for (int i = 0; i < RGA_CACHE_SIZE; i++) { RgaCacheEnt *e = &comp->rga_cache[i]; if (!e->fd) continue; if (!oldest || e->last_use < oldest->last_use) oldest = e; } if (!oldest) return; if (oldest->handle) releasebuffer_handle(oldest->handle); close(oldest->fd); memset(oldest, 0, sizeof(*oldest)); } /* Wrap fd for RGA use, importing into a cached handle when the fast * wrapbuffer_fd path is unavailable. On return *out_handle is nonzero only * when the caller must releasebuffer_handle it (legacy one-shot import); * cached handles are owned by the composer and must not be released. */ static rga_buffer_t import_nv12(Composer *comp, int fd, int w, int h, int stride, int vstride, rga_buffer_handle_t *out_handle) { *out_handle = 0; rga_buffer_t buf = wrapbuffer_fd_t(fd, w, h, stride, vstride, RK_FORMAT_YCbCr_420_SP); if (buf.handle != 0) return buf; im_handle_param_t param; memset(¶m, 0, sizeof(param)); param.width = w; param.height = h; param.format = RK_FORMAT_YCbCr_420_SP; /* Identity of the underlying dmabuf: its anon-inode number is unique * per buffer instance, immune to fd-number recycling. */ struct stat st; ino_t ino = (fstat(fd, &st) == 0) ? st.st_ino : 0; if (ino != 0) { for (int i = 0; i < RGA_CACHE_SIZE; i++) { RgaCacheEnt *e = &comp->rga_cache[i]; if (e->fd > 0 && e->handle && e->ino == ino && e->w == w && e->h == h && e->stride == stride && e->vstride == vstride) { e->last_use = ++comp->rga_clock; return wrapbuffer_handle_t(e->handle, w, h, stride, vstride, RK_FORMAT_YCbCr_420_SP); } } } /* Prepare a free cache slot BEFORE importing. If the table is full when * importbuffer_fd runs the driver refuses the new handle with EINVAL and * it never gets cached (stale entries then keep the table full forever). * Evicting one old handle up front guarantees the import has room. */ int slot = -1; if (ino != 0) { for (int i = 0; i < RGA_CACHE_SIZE; i++) if (!comp->rga_cache[i].fd) { slot = i; break; } if (slot < 0) { rga_cache_evict_one(comp); for (int i = 0; i < RGA_CACHE_SIZE; i++) if (!comp->rga_cache[i].fd) { slot = i; break; } } } rga_buffer_handle_t handle = importbuffer_fd(fd, ¶m); if (handle == 0) { /* Import failed even with room for it (or a wedged table): report * and let the caller's self-heal invalidate fire instead of CACHING * the failure. Rate-limit the line: boards that map part of DDR above * 4G keep the same two high-bank decode buffers failing every frame * (the composer's low-4G mirror then rescues them), so a per-frame * print floods; the counts still live in composer_dump_stats(). */ static struct timeval last_err = {0, 0}, now; gettimeofday(&now, NULL); if (last_err.tv_sec == 0 || now.tv_sec - last_err.tv_sec >= 2) { last_err = now; fprintf(stderr, "import_nv12: importbuffer_fd failed for fd=%d ino=%llu %dx%d stride=%d errno=%d(%s)\n", fd, (unsigned long long)ino, w, h, stride, errno, strerror(errno)); } memset(&buf, 0, sizeof(buf)); return buf; } /* Pin in the cache with our own dup: while our dup stays open the kernel * cannot hand that fd number to anyone else, so stale hits are impossible. */ if (ino != 0 && slot >= 0) { int mydup = fcntl(fd, F_DUPFD_CLOEXEC, 3); if (mydup >= 0) { RgaCacheEnt *e = &comp->rga_cache[slot]; e->fd = mydup; e->ino = ino; e->w = w; e->h = h; e->stride = stride; e->vstride = vstride; e->handle = handle; e->last_use = ++comp->rga_clock; return wrapbuffer_handle_t(handle, w, h, stride, vstride, RK_FORMAT_YCbCr_420_SP); } /* dup failed: hand the handle back one-shot (slot stays free). */ } /* could not cache: hand out one-shot ownership to the caller */ *out_handle = handle; return wrapbuffer_handle_t(handle, w, h, stride, vstride, RK_FORMAT_YCbCr_420_SP); } /* Emergency low-address mirror: the board maps part of DDR above 4G and the * RGA driver refuses any dmabuf whose physical pages sit above 4G * ("rga_mm: RGA_MMU unsupported memory larger than 4G!") — importbuffer_fd * then returns EINVAL and the tile goes black. When a tile's import fails we * memcpy its latest frame into a per-tile system-uncached-dma32 buffer * (guaranteed <4G) and hand RGA that instead. Only failing tiles pay the * copy cost; healthy ones keep the zero-copy path. */ static int mirror_refresh(Composer *comp, int idx, int src_fd, int stride, int vstride) { size_t size = (size_t)stride * (size_t)vstride * 3 / 2; if (comp->mirror_fd[idx] <= 0) { if (dma_heap_alloc("system-uncached-dma32", size, &comp->mirror_fd[idx]) < 0 && dma_heap_alloc("cma", size, &comp->mirror_fd[idx]) < 0) { fprintf(stderr, "mirror: dma32+cma heap alloc of %zuB failed\n", size); return -1; } } void *src = mmap(NULL, size, PROT_READ, MAP_SHARED, src_fd, 0); if (src == MAP_FAILED) { fprintf(stderr, "mirror: mmap src fd=%d failed (%s)\n", src_fd, strerror(errno)); return -1; } void *dst = mmap(NULL, size, PROT_WRITE, MAP_SHARED, comp->mirror_fd[idx], 0); if (dst == MAP_FAILED) { fprintf(stderr, "mirror: mmap mirror fd=%d failed (%s)\n", comp->mirror_fd[idx], strerror(errno)); munmap(src, size); return -1; } memcpy(dst, src, size); munmap(dst, size); munmap(src, size); struct dma_buf_sync sync = { .flags = DMA_BUF_SYNC_RW }; ioctl(comp->mirror_fd[idx], DMA_BUF_IOCTL_SYNC, &sync); return 0; } int composer_compose(Composer *comp, int *src_fds, int *src_widths, int *src_heights, int *src_strides, int *src_vstrides, int num_src, int dst_fd, int dst_stride) { int n = num_src; if (n > MAX_TILES) n = MAX_TILES; if (n <= 0) return 0; rga_buffer_handle_t dst_handle = 0; rga_buffer_t dst_buf = import_nv12(comp, dst_fd, comp->output_w, comp->output_h, dst_stride, comp->output_h, &dst_handle); if (dst_buf.handle == 0) { /* Output import failed: the RGA handle table is likely stale (this * driver returns EINVAL for every import once it wedges). Drop every * cached import and retry once before giving up on this frame. */ composer_invalidate_all(comp); usleep(1000); dst_buf = import_nv12(comp, dst_fd, comp->output_w, comp->output_h, dst_stride, comp->output_h, &dst_handle); if (dst_buf.handle == 0) { comp->dst_fail++; return -1; } } rga_buffer_t pat; memset(&pat, 0, sizeof(pat)); im_rect prect; memset(&prect, 0, sizeof(prect)); int src_fail_frame = 0; for (int i = 0; i < n; i++) { if (src_fds[i] < 0) continue; int vstride = (src_vstrides && src_vstrides[i] > 0) ? src_vstrides[i] : src_heights[i]; rga_buffer_handle_t src_handle = 0; rga_buffer_t src_buf = import_nv12(comp, src_fds[i], src_widths[i], src_heights[i], src_strides[i], vstride, &src_handle); if (src_buf.handle == 0) { /* Likely landed on a >4G physical page (RGA refuses it). Copy the * frame into the per-tile low-address mirror and import that. */ if (mirror_refresh(comp, i, src_fds[i], src_strides[i], vstride) == 0) { src_handle = 0; src_buf = import_nv12(comp, comp->mirror_fd[i], src_widths[i], src_heights[i], src_strides[i], vstride, &src_handle); if (src_buf.handle == 0) fprintf(stderr, "mirror: import mirror fd=%d still failed for tile %d\n", comp->mirror_fd[i], i); } if (src_buf.handle == 0) { comp->src_import_fail[i]++; src_fail_frame++; continue; } comp->mirror_hits++; } comp->src_ok[i]++; im_rect src_rect = {0, 0, src_widths[i], src_heights[i]}; im_rect dst_rect; int tw = comp->tiles[i].w, th = comp->tiles[i].h; if (comp->letterbox) { /* Preserve aspect ratio: fit inside the tile, centered, with * black bars. NV12 needs even dims/offsets. */ int sw = src_widths[i], sh = src_heights[i]; double sc = ((double)tw / sw < (double)th / sh) ? (double)tw / sw : (double)th / sh; int dw = (int)(sw * sc); int dh = (int)(sh * sc); if (dw > tw) dw = tw; if (dh > th) dh = th; dw &= ~1; dh &= ~1; if (dw < 2) dw = 2; if (dh < 2) dh = 2; dst_rect.x = comp->tiles[i].x + (tw - dw) / 2; dst_rect.y = comp->tiles[i].y + (th - dh) / 2; if (dst_rect.x & 1) dst_rect.x--; if (dst_rect.y & 1) dst_rect.y--; if (dst_rect.x < 0) dst_rect.x = 0; if (dst_rect.y < 0) dst_rect.y = 0; dst_rect.width = dw; dst_rect.height = dh; } else { dst_rect.x = comp->tiles[i].x; dst_rect.y = comp->tiles[i].y; dst_rect.width = tw; dst_rect.height = th; } int ret = improcess(src_buf, dst_buf, pat, src_rect, dst_rect, prect, IM_SYNC); if (ret != IM_STATUS_SUCCESS) { comp->src_improcess_fail[i]++; fprintf(stderr, "RGA improcess failed for tile %d: ret=%d (fail=%llu)\n", i, ret, (unsigned long long)comp->src_improcess_fail[i]); } if (src_handle) releasebuffer_handle(src_handle); } if (dst_handle) releasebuffer_handle(dst_handle); if (src_fail_frame > 0) { static struct timeval last_iva = {0, 0}; struct timeval now; gettimeofday(&now, NULL); if (last_iva.tv_sec == 0 || now.tv_sec - last_iva.tv_sec >= 30) { last_iva = now; composer_invalidate_all(comp); fprintf(stderr, "compose: %d src import(s) failed this frame; " "invalidated RGA cache for self-heal\n", src_fail_frame); } } return 0; } void composer_dump_stats(const Composer *comp) { if (!comp) return; int any = 0; for (int i = 0; i < MAX_TILES; i++) if (comp->src_import_fail[i] || comp->src_improcess_fail[i]) { any = 1; break; } if (!any && !comp->dst_fail) { fprintf(stderr, "compose stats: all tiles clean (%llu dst-fail)\n", (unsigned long long)comp->dst_fail); return; } for (int i = 0; i < MAX_TILES; i++) { if (comp->src_ok[i] || comp->src_import_fail[i] || comp->src_improcess_fail[i]) fprintf(stderr, "compose stats tile %d: ok=%llu import_fail=%llu improcess_fail=%llu\n", i, (unsigned long long)comp->src_ok[i], (unsigned long long)comp->src_import_fail[i], (unsigned long long)comp->src_improcess_fail[i]); } if (comp->dst_fail) fprintf(stderr, "compose stats dst: import_fail=%llu\n", (unsigned long long)comp->dst_fail); if (comp->mirror_hits) fprintf(stderr, "compose stats mirror: rescued=%llu frames via low-4G copy\n", (unsigned long long)comp->mirror_hits); }