| /* | |
| * astc-encoder wrapper for FFmpeg (ASTC image encoder) | |
| * | |
| * Copyright (C) 2026 Jun Zhao | |
| * | |
| * This file is part of FFmpeg. | |
| * | |
| * FFmpeg is free software; you can redistribute it and/or | |
| * modify it under the terms of the GNU Lesser General Public | |
| * License as published by the Free Software Foundation; either | |
| * version 2.1 of the License, or (at your option) any later version. | |
| * | |
| * FFmpeg is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| * Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public | |
| * License along with FFmpeg; if not, write to the Free Software | |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |
| */ | |
| /** | |
| * @file | |
| * ASTC encoder using ARM's astc-encoder library (astcenc C API). | |
| * | |
| * Output is the raw ASTC bitstream (16 bytes per block). The container | |
| * (.astc / .ktx) is added by the corresponding FFmpeg muxer. | |
| */ | |
| #include <astcenc/astcenc.h> | |
| #include <limits.h> | |
| #include "avcodec.h" | |
| #include "codec_internal.h" | |
| #include "encode.h" | |
| #include "profiles.h" | |
| #include "libavutil/intreadwrite.h" | |
| #include "libavutil/pixdesc.h" | |
| #include "libavutil/mem.h" | |
| #include "libavutil/opt.h" | |
| /* .astc container magic (little-endian 0x5CA1AB13). */ | |
| #define ASTC_MAGIC 0x5CA1AB13 | |
| #define ASTC_HEADER_SIZE 16 | |
| #define ASTC_BLOCK_BYTES 16 | |
| typedef struct LibAstcEncContext { | |
| AVClass *class; | |
| /* astc-encoder state */ | |
| struct astcenc_config cfg; | |
| struct astcenc_context *ctx; | |
| /* options */ | |
| char *block_size_str; | |
| int block_x; | |
| int block_y; | |
| int block_z; | |
| int profile; /* astcenc_profile */ | |
| float quality; /* 0..100 */ | |
| int perceptual; | |
| int alpha_weight; | |
| } LibAstcEncContext; | |
| static av_cold int libastcenc_init(AVCodecContext *avctx) | |
| { | |
| LibAstcEncContext *s = avctx->priv_data; | |
| enum astcenc_profile prf; | |
| unsigned int flags = 0; | |
| enum astcenc_error err; | |
| /* An explicitly set private profile option overrides the public | |
| * AVCodecContext.profile field. AV_PROFILE_ASTC_LINEAR_ANY describes how a | |
| * container should interpret a stream and never selects an encoding | |
| * profile, so it is rejected here. */ | |
| if (s->profile >= 0) { | |
| prf = (enum astcenc_profile)s->profile; | |
| } else { | |
| switch (avctx->profile) { | |
| case AV_PROFILE_UNKNOWN: | |
| case AV_PROFILE_ASTC_LDR_SRGB: | |
| prf = ASTCENC_PRF_LDR_SRGB; | |
| break; | |
| case AV_PROFILE_ASTC_LDR: | |
| prf = ASTCENC_PRF_LDR; | |
| break; | |
| case AV_PROFILE_ASTC_HDR_RGB_LDR_A: | |
| prf = ASTCENC_PRF_HDR_RGB_LDR_A; | |
| break; | |
| case AV_PROFILE_ASTC_HDR: | |
| prf = ASTCENC_PRF_HDR; | |
| break; | |
| default: | |
| av_log(avctx, AV_LOG_ERROR, | |
| "Unsupported ASTC encoding profile %d.\n", avctx->profile); | |
| return AVERROR(EINVAL); | |
| } | |
| } | |
| /* Resolve profile vs input pixel format. Float/half input must be encoded | |
| * with an HDR profile (auto-promoted here); 8-bit input cannot drive an HDR | |
| * profile (it would be degenerate -> near-black). This runs at init (not in | |
| * encode) because pix_fmt is known at open time; the resolved profile is | |
| * propagated via avctx->profile (-> codecpar->profile) so muxers such as | |
| * the KTX writer can reject HDR without needing to inspect extradata. */ | |
| int is_float = (avctx->pix_fmt == AV_PIX_FMT_RGBAF16 || | |
| avctx->pix_fmt == AV_PIX_FMT_RGBAF32 || | |
| avctx->pix_fmt == AV_PIX_FMT_GBRAPF32); | |
| if (is_float) { | |
| if (prf == ASTCENC_PRF_LDR || prf == ASTCENC_PRF_LDR_SRGB) { | |
| prf = ASTCENC_PRF_HDR_RGB_LDR_A; | |
| av_log(avctx, AV_LOG_INFO, | |
| "Float/half input: auto-promoted profile to HDR_RGB_LDR_A.\n"); | |
| } | |
| } else if (prf == ASTCENC_PRF_HDR || prf == ASTCENC_PRF_HDR_RGB_LDR_A) { | |
| av_log(avctx, AV_LOG_ERROR, | |
| "HDR profile selected but input is 8-bit; use float/half " | |
| "input (e.g. -pix_fmt rgbaf16) for HDR.\n"); | |
| return AVERROR(EINVAL); | |
| } | |
| s->profile = (int)prf; | |
| /* Publish the profile the container records, as one of the public ASTC | |
| * profile values rather than as the astcenc enum, whose ordering is not | |
| * part of the FFmpeg API. */ | |
| switch (prf) { | |
| case ASTCENC_PRF_LDR_SRGB: | |
| avctx->profile = AV_PROFILE_ASTC_LDR_SRGB; | |
| break; | |
| case ASTCENC_PRF_LDR: | |
| avctx->profile = AV_PROFILE_ASTC_LDR; | |
| break; | |
| case ASTCENC_PRF_HDR_RGB_LDR_A: | |
| avctx->profile = AV_PROFILE_ASTC_HDR_RGB_LDR_A; | |
| break; | |
| case ASTCENC_PRF_HDR: | |
| avctx->profile = AV_PROFILE_ASTC_HDR; | |
| break; | |
| default: | |
| avctx->profile = AV_PROFILE_UNKNOWN; | |
| break; | |
| } | |
| if (avctx->width <= 0 || avctx->height <= 0 || | |
| avctx->width > 0xFFFFFF || avctx->height > 0xFFFFFF) { | |
| av_log(avctx, AV_LOG_ERROR, "Invalid image dimensions %dx%d.\n", | |
| avctx->width, avctx->height); | |
| return AVERROR(EINVAL); | |
| } | |
| /* Accept exactly "WxH" or "WxHxD". Scanning stops at the third literal 'x' | |
| * when it is absent, so trailing garbage such as "8x8junk" has to be | |
| * caught by checking that the whole string was consumed. */ | |
| int consumed = 0; | |
| s->block_z = 1; | |
| if (sscanf(s->block_size_str, "%dx%dx%d%n", &s->block_x, &s->block_y, | |
| &s->block_z, &consumed) != 3) { | |
| consumed = 0; | |
| s->block_z = 1; | |
| sscanf(s->block_size_str, "%dx%d%n", &s->block_x, &s->block_y, | |
| &consumed); | |
| } | |
| if (!consumed || s->block_size_str[consumed]) { | |
| av_log(avctx, AV_LOG_ERROR, "Invalid block_size '%s' " | |
| "(expected e.g. 8x8 or 6x6x6).\n", s->block_size_str); | |
| return AVERROR(EINVAL); | |
| } | |
| if (s->perceptual) | |
| flags |= ASTCENC_FLG_USE_PERCEPTUAL; | |
| if (s->alpha_weight) | |
| flags |= ASTCENC_FLG_USE_ALPHA_WEIGHT; | |
| err = astcenc_config_init(prf, s->block_x, s->block_y, s->block_z, | |
| s->quality, flags, &s->cfg); | |
| if (err != ASTCENC_SUCCESS) { | |
| av_log(avctx, AV_LOG_ERROR, "astcenc_config_init failed: %s\n", | |
| astcenc_get_error_string(err)); | |
| return AVERROR_UNKNOWN; | |
| } | |
| /* astcenc normalizes the requested footprint, e.g. a zero block depth is | |
| * clamped to one, so publish the validated footprint instead of the parsed | |
| * one. Both the .astc header below and the block accounting in encode() | |
| * must agree with what the library actually compresses with. */ | |
| s->block_x = s->cfg.block_x; | |
| s->block_y = s->cfg.block_y; | |
| s->block_z = s->cfg.block_z; | |
| err = astcenc_context_alloc(&s->cfg, 1, &s->ctx, NULL); | |
| if (err != ASTCENC_SUCCESS) { | |
| av_log(avctx, AV_LOG_ERROR, "astcenc_context_alloc failed: %s\n", | |
| astcenc_get_error_string(err)); | |
| return AVERROR(ENOMEM); | |
| } | |
| /* Publish a 16-byte .astc-style header as extradata so the .astc/.ktx | |
| * muxers can write the container header and the decoder can recover the | |
| * block size and dimensions. The buffer is padded so consumers that read | |
| * it as a bitstream do not over-read. */ | |
| avctx->extradata = av_mallocz(ASTC_HEADER_SIZE + AV_INPUT_BUFFER_PADDING_SIZE); | |
| if (!avctx->extradata) | |
| return AVERROR(ENOMEM); | |
| avctx->extradata_size = ASTC_HEADER_SIZE; | |
| { | |
| uint8_t *h = avctx->extradata; | |
| /* .astc header layout (all little-endian): | |
| * [0-3] magic | |
| * [4-6] block dimensions block_x, block_y, block_z (block_z > 1 | |
| * selects a 3D block footprint, e.g. 4x4x4) | |
| * [7-9] image width (dim_x, texels) | |
| * [10-12] image height (dim_y, texels) | |
| * [13-15] image depth (dim_z, texel slices; 1 for a 2D image) | |
| * block_z (the block footprint) is independent of dim_z (the image | |
| * slice count), so a 3D block can still describe a 2D image. */ | |
| AV_WL32(h, ASTC_MAGIC); | |
| h[4] = (uint8_t)s->block_x; | |
| h[5] = (uint8_t)s->block_y; | |
| h[6] = (uint8_t)s->block_z; | |
| AV_WL24(h + 7, avctx->width); | |
| AV_WL24(h + 10, avctx->height); | |
| AV_WL24(h + 13, 1); /* image depth is 1: 2D image (3D blocks stay 2D) */ | |
| } | |
| return 0; | |
| } | |
| static int libastcenc_encode(AVCodecContext *avctx, AVPacket *pkt, | |
| const AVFrame *frame, int *got_packet) | |
| { | |
| LibAstcEncContext *s = avctx->priv_data; | |
| const int w = frame->width; | |
| const int h = frame->height; | |
| const int bx = s->block_x; | |
| const int by = s->block_y; | |
| size_t blocks_x, blocks_y, block_count, out_size; | |
| if (w != avctx->width || h != avctx->height) { | |
| av_log(avctx, AV_LOG_ERROR, | |
| "Frame size %dx%d does not match configured %dx%d.\n", | |
| w, h, avctx->width, avctx->height); | |
| return AVERROR_INVALIDDATA; | |
| } | |
| blocks_x = (w - 1) / bx + 1; | |
| blocks_y = (h - 1) / by + 1; | |
| if (av_size_mult(blocks_x, blocks_y, &block_count) < 0 || | |
| av_size_mult(block_count, ASTC_BLOCK_BYTES, &out_size) < 0 || | |
| out_size > INT_MAX) | |
| return AVERROR(EINVAL); | |
| struct astcenc_swizzle swz = { | |
| ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A | |
| }; | |
| uint8_t *buf = NULL; | |
| uint8_t *packed; /* buffer handed to astcenc, buf or the frame itself */ | |
| void *data_ptrs[1]; | |
| struct astcenc_image img; | |
| enum astcenc_error err; | |
| int ret; | |
| /* Map input pixel format -> astcenc data type and component layout. | |
| * astcenc always consumes a tightly packed, RGBA-ordered buffer. */ | |
| int elem; /* bytes per component */ | |
| int planar; /* GBRAPF32 stores G,B,R,A on separate planes */ | |
| int const_alpha; /* RGB24: synthesize opaque alpha */ | |
| enum astcenc_type type; | |
| switch (frame->format) { | |
| case AV_PIX_FMT_RGBA: type = ASTCENC_TYPE_U8; elem = 1; planar = 0; const_alpha = 0; break; | |
| case AV_PIX_FMT_RGB24: type = ASTCENC_TYPE_U8; elem = 1; planar = 0; const_alpha = 1; break; | |
| case AV_PIX_FMT_RGBAF16: type = ASTCENC_TYPE_F16; elem = 2; planar = 0; const_alpha = 0; break; | |
| case AV_PIX_FMT_RGBAF32: type = ASTCENC_TYPE_F32; elem = 4; planar = 0; const_alpha = 0; break; | |
| case AV_PIX_FMT_GBRAPF32: type = ASTCENC_TYPE_F32; elem = 4; planar = 1; const_alpha = 0; break; | |
| default: | |
| av_log(avctx, AV_LOG_ERROR, "Unsupported input pixel format %s.\n", | |
| av_get_pix_fmt_name(frame->format)); | |
| return AVERROR(EINVAL); | |
| } | |
| size_t row_bytes, buf_size; | |
| if (av_size_mult(w, 4 * elem, &row_bytes) < 0 || | |
| av_size_mult(h, row_bytes, &buf_size) < 0) | |
| return AVERROR(EINVAL); | |
| /* Packed input whose lines already have the size astcenc expects needs no | |
| * repacking, so it can be consumed in place. */ | |
| if (!planar && !const_alpha && frame->linesize[0] == (int)row_bytes) { | |
| packed = frame->data[0]; | |
| } else { | |
| buf = av_malloc(buf_size); | |
| if (!buf) | |
| return AVERROR(ENOMEM); | |
| packed = buf; | |
| if (planar) { | |
| /* GBRAPF32 -> packed RGBA float. */ | |
| for (int y = 0; y < h; y++) { | |
| const float *g = (const float *)(frame->data[0] + (size_t)y * frame->linesize[0]); | |
| const float *b = (const float *)(frame->data[1] + (size_t)y * frame->linesize[1]); | |
| const float *r = (const float *)(frame->data[2] + (size_t)y * frame->linesize[2]); | |
| const float *a = (const float *)(frame->data[3] + (size_t)y * frame->linesize[3]); | |
| float *dst = (float *)(buf + (size_t)y * row_bytes); | |
| for (int x = 0; x < w; x++) { | |
| dst[4*x+0] = r[x]; dst[4*x+1] = g[x]; | |
| dst[4*x+2] = b[x]; dst[4*x+3] = a[x]; | |
| } | |
| } | |
| } else if (const_alpha) { | |
| for (int y = 0; y < h; y++) { | |
| const uint8_t *src = frame->data[0] + (size_t)y * frame->linesize[0]; | |
| uint8_t *dst = buf + (size_t)y * row_bytes; | |
| for (int x = 0; x < w; x++) { | |
| dst[4*x+0] = src[3*x+0]; dst[4*x+1] = src[3*x+1]; | |
| dst[4*x+2] = src[3*x+2]; dst[4*x+3] = 255; | |
| } | |
| } | |
| } else { | |
| /* packed RGBA / RGBA16 / RGBA32: copy row-by-row to drop line padding. */ | |
| for (int y = 0; y < h; y++) | |
| memcpy(buf + (size_t)y * row_bytes, | |
| frame->data[0] + (size_t)y * frame->linesize[0], row_bytes); | |
| } | |
| } | |
| img.dim_x = w; | |
| img.dim_y = h; | |
| img.dim_z = 1; | |
| img.data_type = type; | |
| data_ptrs[0] = packed; | |
| img.data = data_ptrs; | |
| ret = ff_get_encode_buffer(avctx, pkt, out_size, 0); | |
| if (ret < 0) | |
| goto end; | |
| err = astcenc_compress_image(s->ctx, &img, &swz, pkt->data, pkt->size, 0); | |
| if (err != ASTCENC_SUCCESS) { | |
| av_log(avctx, AV_LOG_ERROR, "astcenc_compress_image failed: %s\n", | |
| astcenc_get_error_string(err)); | |
| ret = AVERROR_UNKNOWN; | |
| goto end; | |
| } | |
| *got_packet = 1; | |
| ret = 0; | |
| end: | |
| av_freep(&buf); | |
| return ret; | |
| } | |
| static av_cold int libastcenc_close(AVCodecContext *avctx) | |
| { | |
| LibAstcEncContext *s = avctx->priv_data; | |
| if (s->ctx) { | |
| astcenc_context_free(s->ctx); | |
| s->ctx = NULL; | |
| } | |
| return 0; | |
| } | |
| #define VE AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM | |
| #define OFFSET(x) offsetof(LibAstcEncContext, x) | |
| static const AVOption options[] = { | |
| { "block_size", "ASTC block size WxH[xD] (e.g. 4x4, 8x8, 6x6x6)", OFFSET(block_size_str), | |
| AV_OPT_TYPE_STRING, { .str = "8x8" }, 0, 0, VE }, | |
| { "quality", "Compression quality 0..100 (0=fastest, 100=exhaustive)", | |
| OFFSET(quality), AV_OPT_TYPE_FLOAT, { .dbl = 60.0 }, 0.0, 100.0, VE }, | |
| { "profile", "Color profile (overrides AVCodecContext.profile)", | |
| OFFSET(profile), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, 3, VE, .unit = "profile" }, | |
| { "ldr", "Linear LDR", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_LDR }, 0, 0, VE, .unit = "profile" }, | |
| { "ldr-srgb", "sRGB LDR", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_LDR_SRGB }, 0, 0, VE, .unit = "profile" }, | |
| { "hdr-ldr-a", "HDR RGB, LDR alpha", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_HDR_RGB_LDR_A }, 0, 0, VE, .unit = "profile" }, | |
| { "hdr", "HDR", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_HDR }, 0, 0, VE, .unit = "profile" }, | |
| { "perceptual", "Use perceptual (PSNR-weighted) error metric", OFFSET(perceptual), | |
| AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, | |
| { "alpha_weight", "Enable alpha weighting", OFFSET(alpha_weight), | |
| AV_OPT_TYPE_BOOL, { .i64 = 1 }, 0, 1, VE }, | |
| { NULL }, | |
| }; | |
| static const AVClass libastcenc_class = { | |
| .class_name = "libastcenc", | |
| .item_name = av_default_item_name, | |
| .option = options, | |
| .version = LIBAVUTIL_VERSION_INT, | |
| }; | |
| const FFCodec ff_libastcenc_encoder = { | |
| .p.name = "libastcenc", | |
| CODEC_LONG_NAME("ASTC (Adaptive Scalable Texture Compression) image using astc-encoder"), | |
| .p.type = AVMEDIA_TYPE_VIDEO, | |
| .p.id = AV_CODEC_ID_ASTC, | |
| .p.capabilities = AV_CODEC_CAP_DR1, | |
| CODEC_PIXFMTS(AV_PIX_FMT_RGBA, AV_PIX_FMT_RGB24, | |
| AV_PIX_FMT_RGBAF16, AV_PIX_FMT_RGBAF32, AV_PIX_FMT_GBRAPF32), | |
| .p.profiles = NULL_IF_CONFIG_SMALL(ff_astc_profiles), | |
| .p.priv_class = &libastcenc_class, | |
| .p.wrapper_name = "libastcenc", | |
| .caps_internal = FF_CODEC_CAP_NOT_INIT_THREADSAFE | | |
| FF_CODEC_CAP_INIT_CLEANUP, | |
| .priv_data_size = sizeof(LibAstcEncContext), | |
| .init = libastcenc_init, | |
| FF_CODEC_ENCODE_CB(libastcenc_encode), | |
| .close = libastcenc_close, | |
| }; |