PR #23827 opened by Jun Zhao (mypopydev) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23827 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23827.patch
dds ASTC (Adaptive Scalable Texture Compression) support to FFmpeg across four commits: an encoder/decoder pair wrapping astc-encoder library with support for 2D/3D block sizes and all four color profiles including HDR (lavc), .astc and KTX 1.0 container muxers/demuxers with proper HDR/3D-block validation (lavf), a self-contained FATE test suite covering LDR/HDR round-trips, container interop, and a profile-mismatch regression test (fate), and accompanying documentation plus a Changelog entry (doc). From 1f8350787582e1b471f95605563dd53f926ca83e Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Thu, 16 Jul 2026 11:19:17 +0800 Subject: [PATCH 1/4] lavc: add ASTC encoder/decoder via libastcenc Add the codec ID/descriptor, an encoder wrapping ARM's astc-encoder library, and a matching decoder. The encoder supports 2D and 3D block sizes (e.g. 8x8, 6x6x6), all four color profiles, and RGBA/RGB24/rgbaf16le/rgbaf32le/gbrapf32le input. Float input under an LDR profile auto-promotes to HDR_RGB_LDR_A; the resolved profile is exposed via avctx->profile so muxers (e.g. KTX) can reject HDR without inspecting extradata. ASTC bitstreams carry no profile field, so the decoder needs a private "dec_profile" option (default LDR-sRGB) to know which one to use; a mismatch is caught by astcenc itself via a fixed magenta error color. HDR output is rgbaf16le to avoid an 8-bit clamp. Reset the astcenc context between images when threads > 1, per the library's single-image-at-a-time contract, and validate frame dimensions against the configured width/height. Signed-off-by: Jun Zhao <[email protected]> --- configure | 8 + libavcodec/Makefile | 2 + libavcodec/allcodecs.c | 2 + libavcodec/astcdec.c | 218 +++++++++++++++++++++++++ libavcodec/codec_desc.c | 8 + libavcodec/codec_id.h | 1 + libavcodec/libastcenc.c | 345 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 584 insertions(+) create mode 100644 libavcodec/astcdec.c create mode 100644 libavcodec/libastcenc.c diff --git a/configure b/configure index e09bbd7f04..56b0cfdf77 100755 --- a/configure +++ b/configure @@ -220,6 +220,7 @@ External library support: --enable-libaribcaption enable ARIB text and caption decoding via libaribcaption [no] --enable-libass enable libass subtitles rendering, needed for subtitles and ass filter [no] + --enable-libastcenc enable ASTC image encoding/decoding via astc-encoder [no] --enable-libbluray enable BluRay reading using libbluray [no] --enable-libbs2b enable bs2b DSP library [no] --enable-libcaca enable textual display using libcaca [no] @@ -2078,6 +2079,7 @@ EXTERNAL_LIBRARY_LIST=" libaom libaribcaption libass + libastcenc libbluray libbs2b libcaca @@ -3840,6 +3842,8 @@ libaom_av1_encoder_deps="libaom" libaom_av1_encoder_select="extract_extradata_bsf dovi_rpuenc" libaribb24_decoder_deps="libaribb24" libaribcaption_decoder_deps="libaribcaption" +libastcenc_encoder_deps="libastcenc" +libastcenc_decoder_deps="libastcenc" libcodec2_decoder_deps="libcodec2" libcodec2_encoder_deps="libcodec2" libdav1d_decoder_deps="libdav1d" @@ -7350,6 +7354,10 @@ enabled libaribb24 && { check_pkg_config libaribb24 "aribb24 > 1.0.3" "ar { enabled gpl && require_pkg_config libaribb24 aribb24 "aribb24/aribb24.h" arib_instance_new; } || die "ERROR: libaribb24 requires version higher than 1.0.3 or --enable-gpl."; } enabled libaribcaption && require_pkg_config libaribcaption "libaribcaption >= 1.1.1" "aribcaption/aribcaption.h" aribcc_context_alloc +enabled libastcenc && { + require libastcenc astcenc/astcenc.h astcenc_config_init -lastcenc -lstdc++ +} +libastcenc_extralibs="-lastcenc -lstdc++" enabled lv2 && require_pkg_config lv2 lilv-0 "lilv/lilv.h" lilv_world_new enabled libiec61883 && require libiec61883 libiec61883/iec61883.h iec61883_cmp_connect -lraw1394 -lavc1394 -lrom1394 -liec61883 enabled libass && require_pkg_config libass "libass >= 0.11.0" ass/ass.h ass_library_init diff --git a/libavcodec/Makefile b/libavcodec/Makefile index 232fdd7339..e9375bea88 100644 --- a/libavcodec/Makefile +++ b/libavcodec/Makefile @@ -1181,6 +1181,8 @@ OBJS-$(CONFIG_LIBAOM_AV1_DECODER) += libaomdec.o libaom.o OBJS-$(CONFIG_LIBAOM_AV1_ENCODER) += libaomenc.o libaom.o OBJS-$(CONFIG_LIBARIBB24_DECODER) += libaribb24.o ass.o OBJS-$(CONFIG_LIBARIBCAPTION_DECODER) += libaribcaption.o ass.o +OBJS-$(CONFIG_LIBASTCENC_ENCODER) += libastcenc.o +OBJS-$(CONFIG_LIBASTCENC_DECODER) += astcdec.o OBJS-$(CONFIG_LIBCODEC2_DECODER) += libcodec2.o OBJS-$(CONFIG_LIBCODEC2_ENCODER) += libcodec2.o OBJS-$(CONFIG_LIBDAV1D_DECODER) += libdav1d.o av1_parse.o diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c index e1668f1e80..86bcb510d8 100644 --- a/libavcodec/allcodecs.c +++ b/libavcodec/allcodecs.c @@ -772,6 +772,8 @@ extern const FFCodec ff_libaom_av1_encoder; /* preferred over libaribb24 */ extern const FFCodec ff_libaribcaption_decoder; extern const FFCodec ff_libaribb24_decoder; +extern const FFCodec ff_libastcenc_encoder; +extern const FFCodec ff_libastcenc_decoder; extern const FFCodec ff_libcodec2_encoder; extern const FFCodec ff_libcodec2_decoder; extern const FFCodec ff_libdav1d_decoder; diff --git a/libavcodec/astcdec.c b/libavcodec/astcdec.c new file mode 100644 index 0000000000..26376f9943 --- /dev/null +++ b/libavcodec/astcdec.c @@ -0,0 +1,218 @@ +/* + * astc-encoder wrapper decoder for FFmpeg (ASTC image decoder) + * + * 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 decoder using ARM's astc-encoder library (astcenc C API). + * + * Consumes the raw ASTC bitstream; block size and dimensions are recovered + * from the 16-byte .astc-style extradata published by the demuxer. + */ + +#include <astcenc/astcenc.h> + +#include "avcodec.h" +#include "codec_internal.h" +#include "decode.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" + +#define ASTC_HEADER_SIZE 16 +#define ASTC_BLOCK_BYTES 16 + +typedef struct LibAstcDecContext { + AVClass *class; + struct astcenc_context *ctx; + int block_x; + int block_y; + int block_z; + int width; + int height; + int is_hdr; + int dec_profile; /* astcenc_profile, set via private option */ +} LibAstcDecContext; + +static av_cold int libastcdec_init(AVCodecContext *avctx) +{ + LibAstcDecContext *s = avctx->priv_data; + const uint8_t *ed = avctx->extradata; + struct astcenc_config cfg; + enum astcenc_error err; + + if (!ed || avctx->extradata_size < ASTC_HEADER_SIZE) { + av_log(avctx, AV_LOG_ERROR, + "astc decoder requires %d-byte .astc-style extradata.\n", + ASTC_HEADER_SIZE); + return AVERROR_INVALIDDATA; + } + + s->block_x = ed[4]; + s->block_y = ed[5]; + s->block_z = ed[6]; + s->width = ed[7] | (ed[8] << 8) | (ed[9] << 16); + s->height = ed[10] | (ed[11] << 8) | (ed[12] << 16); + + /* ASTC bitstreams do not carry a profile field; the .astc header only + * has dim_z (bytes 13-15, always 1 for 2D). The profile must be known + * to the decoder because LDR and HDR use different endpoint encodings. + * Default to LDR sRGB (the common case); use -dec_profile hdr or + * -dec_profile hdr-ldr-a for HDR files. */ + enum astcenc_profile prf = (enum astcenc_profile)s->dec_profile; + s->is_hdr = (prf == ASTCENC_PRF_HDR || prf == ASTCENC_PRF_HDR_RGB_LDR_A); + + err = astcenc_config_init(prf, s->block_x, s->block_y, s->block_z, + 0, ASTCENC_FLG_DECOMPRESS_ONLY, &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; + } + + err = astcenc_context_alloc(&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); + } + + avctx->pix_fmt = s->is_hdr ? AV_PIX_FMT_RGBAF16LE : AV_PIX_FMT_RGBA; + avctx->width = s->width; + avctx->height = s->height; + return 0; +} + +static int libastcdec_decode(AVCodecContext *avctx, AVFrame *frame, + int *got_frame, AVPacket *avpkt) +{ + LibAstcDecContext *s = avctx->priv_data; + const int w = s->width; + const int h = s->height; + const int elem = s->is_hdr ? 2 : 1; + + const int blocks_x = (w + s->block_x - 1) / s->block_x; + const int blocks_y = (h + s->block_y - 1) / s->block_y; + /* dim_z is always 1 (2D input), so blocks_z is always 1. */ + const size_t expected = (size_t)blocks_x * blocks_y * ASTC_BLOCK_BYTES; + + struct astcenc_swizzle swz = { + ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A + }; + + uint8_t *out_buf = NULL; + void *data_ptrs[1]; + struct astcenc_image img; + enum astcenc_error err; + int ret; + + if (!avpkt->data && !avpkt->size) + return AVERROR_EOF; + + if ((size_t)avpkt->size < expected) { + av_log(avctx, AV_LOG_ERROR, + "ASTC packet too small: got %d, expected %zu bytes.\n", + avpkt->size, expected); + return AVERROR_INVALIDDATA; + } + + out_buf = av_malloc((size_t)w * h * 4 * elem); + if (!out_buf) + return AVERROR(ENOMEM); + + frame->width = w; + frame->height = h; + if ((ret = ff_get_buffer(avctx, frame, 0)) < 0) + goto end; + + img.dim_x = w; + img.dim_y = h; + img.dim_z = 1; + img.data_type = s->is_hdr ? ASTCENC_TYPE_F16 : ASTCENC_TYPE_U8; + data_ptrs[0] = out_buf; + img.data = data_ptrs; + + err = astcenc_decompress_image(s->ctx, avpkt->data, avpkt->size, + &img, &swz, 0); + if (err != ASTCENC_SUCCESS) { + av_log(avctx, AV_LOG_ERROR, "astcenc_decompress_image failed: %s\n", + astcenc_get_error_string(err)); + ret = AVERROR_UNKNOWN; + goto end; + } + + for (int y = 0; y < h; y++) + memcpy(frame->data[0] + (size_t)y * frame->linesize[0], + out_buf + (size_t)y * w * 4 * elem, (size_t)w * 4 * elem); + + *got_frame = 1; + ret = 0; + +end: + av_freep(&out_buf); + return ret; +} + +static av_cold int libastcdec_close(AVCodecContext *avctx) +{ + LibAstcDecContext *s = avctx->priv_data; + if (s->ctx) { + astcenc_context_free(s->ctx); + s->ctx = NULL; + } + return 0; +} + +#define VD AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_DECODING_PARAM +#define OFFSET(x) offsetof(LibAstcDecContext, x) + +static const AVOption dec_options[] = { + { "dec_profile", "Decoder color profile (must match encoder)", OFFSET(dec_profile), + AV_OPT_TYPE_INT, { .i64 = ASTCENC_PRF_LDR_SRGB }, 0, 3, VD, .unit = "dec_profile" }, + { "ldr", "Linear LDR", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_LDR }, 0, 0, VD, .unit = "dec_profile" }, + { "ldr-srgb", "sRGB LDR", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_LDR_SRGB }, 0, 0, VD, .unit = "dec_profile" }, + { "hdr-ldr-a", "HDR RGB, LDR alpha", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_HDR_RGB_LDR_A }, 0, 0, VD, .unit = "dec_profile" }, + { "hdr", "HDR", 0, AV_OPT_TYPE_CONST, { .i64 = ASTCENC_PRF_HDR }, 0, 0, VD, .unit = "dec_profile" }, + { NULL }, +}; + +static const AVClass libastcenc_dec_class = { + .class_name = "libastcenc decoder", + .item_name = av_default_item_name, + .option = dec_options, + .version = LIBAVUTIL_VERSION_INT, +}; + +const FFCodec ff_libastcenc_decoder = { + .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_RGBAF16LE), + .p.priv_class = &libastcenc_dec_class, + .p.wrapper_name = "libastcenc", + .caps_internal = FF_CODEC_CAP_NOT_INIT_THREADSAFE | + FF_CODEC_CAP_INIT_CLEANUP, + .priv_data_size = sizeof(LibAstcDecContext), + .init = libastcdec_init, + FF_CODEC_DECODE_CB(libastcdec_decode), + .close = libastcdec_close, +}; diff --git a/libavcodec/codec_desc.c b/libavcodec/codec_desc.c index 299a469ce4..9c9f5f425b 100644 --- a/libavcodec/codec_desc.c +++ b/libavcodec/codec_desc.c @@ -1992,6 +1992,14 @@ static const AVCodecDescriptor codec_descriptors[] = { .props = AV_CODEC_PROP_LOSSY | AV_CODEC_PROP_LOSSLESS, .mime_types= MT("image/webp"), }, + { + .id = AV_CODEC_ID_ASTC, + .type = AVMEDIA_TYPE_VIDEO, + .name = "astc", + .long_name = NULL_IF_CONFIG_SMALL("ASTC (Adaptive Scalable Texture Compression)"), + .props = AV_CODEC_PROP_INTRA_ONLY | AV_CODEC_PROP_LOSSY, + .mime_types= MT("image/astc"), + }, /* various PCM "codecs" */ { diff --git a/libavcodec/codec_id.h b/libavcodec/codec_id.h index ff2c21526c..168b071144 100644 --- a/libavcodec/codec_id.h +++ b/libavcodec/codec_id.h @@ -324,6 +324,7 @@ enum AVCodecID { AV_CODEC_ID_PRORES_RAW, AV_CODEC_ID_JPEGXS, AV_CODEC_ID_WEBP_ANIM, + AV_CODEC_ID_ASTC, /* various PCM "codecs" */ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs diff --git a/libavcodec/libastcenc.c b/libavcodec/libastcenc.c new file mode 100644 index 0000000000..3ea98d9697 --- /dev/null +++ b/libavcodec/libastcenc.c @@ -0,0 +1,345 @@ +/* + * 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 "avcodec.h" +#include "codec_internal.h" +#include "encode.h" +#include "libavutil/pixdesc.h" +#include "libavutil/mem.h" +#include "libavutil/opt.h" + +#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 threads; + int perceptual; + int alpha_weight; + + /* dimensions captured at init, used to validate each frame */ + int width; + int height; +} LibAstcEncContext; + +static av_cold int libastcenc_init(AVCodecContext *avctx) +{ + LibAstcEncContext *s = avctx->priv_data; + enum astcenc_profile prf = (enum astcenc_profile)s->profile; + unsigned int flags = 0; + enum astcenc_error err; + + /* 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. */ + if (avctx->pix_fmt != AV_PIX_FMT_NONE) { + int is_float = (avctx->pix_fmt == AV_PIX_FMT_RGBAF16LE || + avctx->pix_fmt == AV_PIX_FMT_RGBAF32LE || + avctx->pix_fmt == AV_PIX_FMT_GBRAPF32LE); + 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 rgbaf16le) for HDR.\n"); + return AVERROR(EINVAL); + } + } + s->profile = (int)prf; + avctx->profile = (int)prf; + + s->width = avctx->width; + s->height = avctx->height; + + s->block_z = 1; + if (sscanf(s->block_size_str, "%dx%dx%d", &s->block_x, &s->block_y, + &s->block_z) < 2) { + 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->threads < 1) + s->threads = 1; + + 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; + } + + err = astcenc_context_alloc(&s->cfg, s->threads, &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. Magic = 0x5CA1AB13 (little endian). */ + if (avctx->extradata) { + av_freep(&avctx->extradata); + avctx->extradata_size = 0; + } + avctx->extradata = av_mallocz(ASTC_HEADER_SIZE); + if (!avctx->extradata) + return AVERROR(ENOMEM); + avctx->extradata_size = ASTC_HEADER_SIZE; + { + uint8_t *h = avctx->extradata; + h[0] = 0x13; h[1] = 0xAB; h[2] = 0xA1; h[3] = 0x5C; + h[4] = (uint8_t)s->block_x; + h[5] = (uint8_t)s->block_y; + h[6] = (uint8_t)s->block_z; + h[7] = avctx->width & 0xFF; h[8] = (avctx->width >> 8) & 0xFF; h[9] = (avctx->width >> 16) & 0xFF; + h[10] = avctx->height & 0xFF; h[11] = (avctx->height >> 8) & 0xFF; h[12] = (avctx->height >> 16) & 0xFF; + h[13] = 1; h[14] = 0; h[15] = 0; /* dim_z = 1 (2D image) */ + } + + 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; + + /* Single-image codec: every frame must match the dimensions the header + * (and thus the decoder) was configured with at init. */ + if (w != s->width || h != s->height) { + av_log(avctx, AV_LOG_ERROR, + "Frame size %dx%d does not match configured %dx%d.\n", + w, h, s->width, s->height); + return AVERROR(EINVAL); + } + + const int blocks_x = (w + bx - 1) / bx; + const int blocks_y = (h + by - 1) / by; + const size_t out_size = (size_t)blocks_x * blocks_y * ASTC_BLOCK_BYTES; + + struct astcenc_swizzle swz = { + ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A + }; + + uint8_t *buf = NULL; + 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; /* GBRAPF32LE 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_RGBAF16LE: type = ASTCENC_TYPE_F16; elem = 2; planar = 0; const_alpha = 0; break; + case AV_PIX_FMT_RGBAF32LE: type = ASTCENC_TYPE_F32; elem = 4; planar = 0; const_alpha = 0; break; + case AV_PIX_FMT_GBRAPF32LE: 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); + } + + int is_hdr = (s->profile == ASTCENC_PRF_HDR || s->profile == ASTCENC_PRF_HDR_RGB_LDR_A); + if (type == ASTCENC_TYPE_U8 && is_hdr) { + av_log(avctx, AV_LOG_ERROR, + "HDR profile set but input is 8-bit; use float/half input (e.g. rgbaf16le).\n"); + return AVERROR(EINVAL); + } + if (type != ASTCENC_TYPE_U8 && !is_hdr) { + av_log(avctx, AV_LOG_ERROR, + "Float/half input requires an HDR profile; set -profile hdr or hdr-ldr-a.\n"); + return AVERROR(EINVAL); + } + + const size_t row_bytes = (size_t)w * 4 * elem; + buf = av_malloc((size_t)h * row_bytes); + if (!buf) + return AVERROR(ENOMEM); + + if (planar) { + /* GBRAPF32LE -> 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] = buf; + img.data = data_ptrs; + + /* A context compresses one image at a time; for a multi-threaded context + * astcenc requires compress_reset() before each new image. Harmless (and + * documented as optional) for single-threaded use. */ + if (s->threads > 1) { + astcenc_compress_reset(s->ctx); + } + + 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[×D] (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", OFFSET(profile), AV_OPT_TYPE_INT, + { .i64 = ASTCENC_PRF_LDR_SRGB }, 0, 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" }, + { "threads", "Number of compression threads", OFFSET(threads), + AV_OPT_TYPE_INT, { .i64 = 1 }, 1, 16, VE }, + { "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 | AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, + CODEC_PIXFMTS(AV_PIX_FMT_RGBA, AV_PIX_FMT_RGB24, + AV_PIX_FMT_RGBAF16LE, AV_PIX_FMT_RGBAF32LE, AV_PIX_FMT_GBRAPF32LE), + .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, +}; -- 2.52.0 From 598519789a9cd86106bb1421162c99468f415c51 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Thu, 16 Jul 2026 11:19:17 +0800 Subject: [PATCH 2/4] lavf: add ASTC and KTX 1.0 muxers/demuxers Add container support for ASTC textures via two formats. The .astc muxer writes a 16-byte header (magic 0x5CA1AB13, block size, dimensions) followed by the raw ASTC bitstream. The KTX 1.0 muxer writes a 64-byte header with the appropriate GL internal format enum (followed by a uint32 image size and the compressed data) and guards against HDR writes because KTX 1.0 has no HDR ASTC GL enum. Both demuxers emit the raw bitstream as a single packet, synthesizing a 16-byte .astc-style extradata for the decoder. The .astc demuxer reads the remainder of the stream until EOF (with a loop and av_grow_packet) so it also works for non-seekable inputs such as pipes. The KTX demuxer recovers the block size from the GL enum and correctly skips the three 4-byte fields between endianness and glInternalFormat. Signed-off-by: Jun Zhao <[email protected]> --- libavformat/Makefile | 4 + libavformat/allformats.c | 6 +- libavformat/astcdec.c | 134 ++++++++++++++++++++++++++++ libavformat/astcenc.c | 70 +++++++++++++++ libavformat/ktxdec.c | 183 +++++++++++++++++++++++++++++++++++++++ libavformat/ktxenc.c | 159 ++++++++++++++++++++++++++++++++++ 6 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 libavformat/astcdec.c create mode 100644 libavformat/astcenc.c create mode 100644 libavformat/ktxdec.c create mode 100644 libavformat/ktxenc.c diff --git a/libavformat/Makefile b/libavformat/Makefile index 3595567464..7cbe4119d7 100644 --- a/libavformat/Makefile +++ b/libavformat/Makefile @@ -147,6 +147,8 @@ OBJS-$(CONFIG_ASS_DEMUXER) += assdec.o subtitles.o OBJS-$(CONFIG_ASS_MUXER) += assenc.o OBJS-$(CONFIG_AST_DEMUXER) += ast.o astdec.o OBJS-$(CONFIG_AST_MUXER) += ast.o astenc.o +OBJS-$(CONFIG_ASTC_MUXER) += astcenc.o +OBJS-$(CONFIG_ASTC_DEMUXER) += astcdec.o OBJS-$(CONFIG_AU_DEMUXER) += au.o pcm.o OBJS-$(CONFIG_AU_MUXER) += au.o rawenc.o OBJS-$(CONFIG_AVI_DEMUXER) += avidec.o @@ -347,6 +349,8 @@ OBJS-$(CONFIG_JACOSUB_DEMUXER) += jacosubdec.o subtitles.o OBJS-$(CONFIG_JACOSUB_MUXER) += jacosubenc.o rawenc.o OBJS-$(CONFIG_JPEGXL_ANIM_DEMUXER) += jpegxl_anim_dec.o OBJS-$(CONFIG_JV_DEMUXER) += jvdec.o +OBJS-$(CONFIG_KTX_MUXER) += ktxenc.o +OBJS-$(CONFIG_KTX_DEMUXER) += ktxdec.o OBJS-$(CONFIG_KUX_DEMUXER) += flvdec.o OBJS-$(CONFIG_KVAG_DEMUXER) += kvag.o OBJS-$(CONFIG_KVAG_MUXER) += kvag.o rawenc.o diff --git a/libavformat/allformats.c b/libavformat/allformats.c index e121c7441c..c05ff0294a 100644 --- a/libavformat/allformats.c +++ b/libavformat/allformats.c @@ -85,9 +85,11 @@ extern const FFOutputFormat ff_asf_muxer; extern const FFInputFormat ff_asf_o_demuxer; extern const FFInputFormat ff_ass_demuxer; extern const FFOutputFormat ff_ass_muxer; +extern const FFOutputFormat ff_asf_stream_muxer; extern const FFInputFormat ff_ast_demuxer; extern const FFOutputFormat ff_ast_muxer; -extern const FFOutputFormat ff_asf_stream_muxer; +extern const FFOutputFormat ff_astc_muxer; +extern const FFInputFormat ff_astc_demuxer; extern const FFInputFormat ff_au_demuxer; extern const FFOutputFormat ff_au_muxer; extern const FFInputFormat ff_av1_demuxer; @@ -251,6 +253,8 @@ extern const FFInputFormat ff_jacosub_demuxer; extern const FFOutputFormat ff_jacosub_muxer; extern const FFInputFormat ff_jv_demuxer; extern const FFInputFormat ff_jpegxl_anim_demuxer; +extern const FFOutputFormat ff_ktx_muxer; +extern const FFInputFormat ff_ktx_demuxer; extern const FFInputFormat ff_kux_demuxer; extern const FFInputFormat ff_kvag_demuxer; extern const FFOutputFormat ff_kvag_muxer; diff --git a/libavformat/astcdec.c b/libavformat/astcdec.c new file mode 100644 index 0000000000..f3902acf6a --- /dev/null +++ b/libavformat/astcdec.c @@ -0,0 +1,134 @@ +/* + * ASTC demuxer (.astc container) + * 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 (Adaptive Scalable Texture Compression) demuxer. + * + * Reads the 16-byte .astc header, exposes it as extradata to the decoder, + * and emits the raw ASTC bitstream as a single packet. + */ + +#include "config_components.h" + +#include "avformat.h" +#include "avio.h" +#include "demux.h" +#include "internal.h" + +#define ASTC_HEADER_SIZE 16 +static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C }; + +static int astc_probe(const AVProbeData *p) +{ + if (p->buf_size >= ASTC_HEADER_SIZE && + !memcmp(p->buf, astc_magic, sizeof(astc_magic))) + return AVPROBE_SCORE_MAX; + return 0; +} + +static int astc_read_header(AVFormatContext *s) +{ + AVIOContext *pb = s->pb; + AVStream *st; + uint8_t hdr[ASTC_HEADER_SIZE]; + int64_t filesize = avio_size(pb); + unsigned int w, h; + + if (avio_read(pb, hdr, ASTC_HEADER_SIZE) != ASTC_HEADER_SIZE) + return AVERROR_INVALIDDATA; + + if (memcmp(hdr, astc_magic, sizeof(astc_magic))) { + av_log(s, AV_LOG_ERROR, "Not an ASTC file (bad magic).\n"); + return AVERROR_INVALIDDATA; + } + + w = hdr[7] | (hdr[8] << 8) | (hdr[9] << 16); + h = hdr[10] | (hdr[11] << 8) | (hdr[12] << 16); + + st = avformat_new_stream(s, NULL); + if (!st) + return AVERROR(ENOMEM); + + st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + st->codecpar->codec_id = AV_CODEC_ID_ASTC; + st->codecpar->width = w; + st->codecpar->height = h; + st->codecpar->format = AV_PIX_FMT_RGBA; + + if (ff_alloc_extradata(st->codecpar, ASTC_HEADER_SIZE) < 0) + return AVERROR(ENOMEM); + memcpy(st->codecpar->extradata, hdr, ASTC_HEADER_SIZE); + + /* Single image: the whole remainder of the file is one packet. */ + if (filesize > 0) + st->duration = 1; + avpriv_set_pts_info(st, 64, 1, 1); + + return 0; +} + +static int astc_read_packet(AVFormatContext *s, AVPacket *pkt) +{ + AVIOContext *pb = s->pb; + uint8_t buf[1 << 16]; + int ret, total = 0; + + /* The whole remainder of the file is one image packet. Read until EOF so + * this also works for non-seekable inputs (pipes), where avio_size() is + * unavailable. avio_read() loops internally to fill each chunk. */ + if ((ret = av_new_packet(pkt, 0)) < 0) + return ret; + + while ((ret = avio_read(pb, buf, sizeof(buf))) > 0) { + int r2; + if ((r2 = av_grow_packet(pkt, ret)) < 0) { + av_packet_unref(pkt); + return r2; + } + memcpy(pkt->data + total, buf, ret); + total += ret; + } + + if (ret < 0 && ret != AVERROR_EOF) { + av_packet_unref(pkt); + return ret; + } + if (!total) { + av_packet_unref(pkt); + return AVERROR_EOF; + } + + pkt->stream_index = 0; + pkt->flags |= AV_PKT_FLAG_KEY; + return 0; +} + +const FFInputFormat ff_astc_demuxer = { + .p.name = "astc", + .p.long_name = NULL_IF_CONFIG_SMALL("ASTC (Adaptive Scalable Texture Compression)"), + .p.mime_type = "image/astc", + .p.extensions = "astc", + .p.flags = AVFMT_NOTIMESTAMPS, + .read_probe = astc_probe, + .read_header = astc_read_header, + .read_packet = astc_read_packet, +}; diff --git a/libavformat/astcenc.c b/libavformat/astcenc.c new file mode 100644 index 0000000000..3c910e53a1 --- /dev/null +++ b/libavformat/astcenc.c @@ -0,0 +1,70 @@ +/* + * ASTC muxer (.astc container) + * 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 (Adaptive Scalable Texture Compression) muxer. + * + * The .astc container is a 16-byte header followed by the raw ASTC + * bitstream. The header is taken verbatim from the encoder's extradata + * (which it publishes in .astc form). + */ + +#include "avformat.h" +#include "avio.h" +#include "mux.h" + +#define ASTC_HEADER_SIZE 16 + +static int astc_write_header(AVFormatContext *s) +{ + AVStream *st = s->streams[0]; + + if (st->codecpar->extradata_size < ASTC_HEADER_SIZE) { + av_log(s, AV_LOG_ERROR, ".astc muxer requires 16-byte extradata " + "(block size / dimensions) from the encoder.\n"); + return AVERROR(EINVAL); + } + + avio_write(s->pb, st->codecpar->extradata, ASTC_HEADER_SIZE); + return 0; +} + +static int astc_write_packet(AVFormatContext *s, AVPacket *pkt) +{ + avio_write(s->pb, pkt->data, pkt->size); + return 0; +} + +const FFOutputFormat ff_astc_muxer = { + .p.name = "astc", + .p.long_name = NULL_IF_CONFIG_SMALL("ASTC (Adaptive Scalable Texture Compression)"), + .p.mime_type = "image/astc", + .p.extensions = "astc", + .p.audio_codec = AV_CODEC_ID_NONE, + .p.video_codec = AV_CODEC_ID_ASTC, + .p.subtitle_codec = AV_CODEC_ID_NONE, + .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH | + FF_OFMT_FLAG_ONLY_DEFAULT_CODECS, + .write_header = astc_write_header, + .write_packet = astc_write_packet, + .p.flags = AVFMT_NOTIMESTAMPS, +}; diff --git a/libavformat/ktxdec.c b/libavformat/ktxdec.c new file mode 100644 index 0000000000..ec76d9b755 --- /dev/null +++ b/libavformat/ktxdec.c @@ -0,0 +1,183 @@ +/* + * KTX 1.0 demuxer for ASTC + * 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 + * KTX 1.0 demuxer for ASTC textures. + * + * Parses the KTX 1.0 header, recovers the ASTC block size from the GL + * internal format enum, and synthesizes a 16-byte .astc-style extradata + * for the decoder. The raw ASTC bitstream is emitted as a single packet. + */ + +#include "config_components.h" + +#include "avformat.h" +#include "avio.h" +#include "demux.h" +#include "internal.h" +#include "libavutil/intreadwrite.h" + +#define ASTC_HEADER_SIZE 16 +static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C }; + +/* ASTC GL internal format enums (2D), linear base 0x93B0, sRGB = +0x10. */ +static const int astc_gl_linear[14] = { + 0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7, + 0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD +}; +static const int astc_bx[14] = { 4, 5, 5, 6, 6, 8, 8, 8, 10, 10, 10, 10, 12, 12 }; +static const int astc_by[14] = { 4, 4, 5, 5, 6, 5, 6, 8, 5, 6, 8, 10, 10, 12 }; + +typedef struct KTXDemuxerContext { + uint32_t image_size; +} KTXDemuxerContext; + +static int ktx_probe(const AVProbeData *p) +{ + static const uint8_t magic[12] = { + 0xAB, 'K', 'T', 'X', ' ', '1', '1', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A + }; + if (p->buf_size >= 12 && !memcmp(p->buf, magic, 12)) + return AVPROBE_SCORE_MAX; + return 0; +} + +/* Map a GL internal format enum to a 2D ASTC block size. Returns 1 on success. */ +static int gl_enum_to_block(uint32_t e, int *bx, int *by) +{ + int base = (e >= 0x93C0 && e <= 0x93CD) ? (e - 0x10) : e; + for (int i = 0; i < 14; i++) { + if (astc_gl_linear[i] == (int)base) { + *bx = astc_bx[i]; + *by = astc_by[i]; + return 1; + } + } + return 0; +} + +static int ktx_read_header(AVFormatContext *s) +{ + AVIOContext *pb = s->pb; + KTXDemuxerContext *ktx = s->priv_data; + AVStream *st; + uint8_t magic[12]; + uint32_t endian, gl_internal, w, h, kvdata, image_size; + int bx = 0, by = 0; + uint8_t extra[16]; + + if (avio_read(pb, magic, 12) != 12) + return AVERROR_INVALIDDATA; + if (magic[0] != 0xAB || magic[1] != 'K' || magic[5] != '1' || magic[6] != '1') { + av_log(s, AV_LOG_ERROR, "Not a KTX file (bad magic).\n"); + return AVERROR_INVALIDDATA; + } + + endian = avio_rl32(pb); + if (endian != 0x04030201) { + av_log(s, AV_LOG_ERROR, "KTX big-endian / unknown endianness not supported.\n"); + return AVERROR_INVALIDDATA; + } + avio_skip(pb, 12); /* gl_type, gl_type_size, gl_format */ + gl_internal = avio_rl32(pb); + avio_skip(pb, 4); /* gl_base_internal_format */ + w = avio_rl32(pb); + h = avio_rl32(pb); + avio_skip(pb, 16); /* pixel_depth, array, faces, miplevels */ + kvdata = avio_rl32(pb); + + if (!gl_enum_to_block(gl_internal, &bx, &by)) { + av_log(s, AV_LOG_ERROR, "Unsupported ASTC GL format 0x%X in KTX.\n", gl_internal); + return AVERROR_INVALIDDATA; + } + + avio_skip(pb, kvdata); + image_size = avio_rl32(pb); + if (!image_size) + return AVERROR_INVALIDDATA; + ktx->image_size = image_size; + + st = avformat_new_stream(s, NULL); + if (!st) + return AVERROR(ENOMEM); + + st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + st->codecpar->codec_id = AV_CODEC_ID_ASTC; + st->codecpar->width = w; + st->codecpar->height = h; + st->codecpar->format = AV_PIX_FMT_RGBA; + + /* Synthesize the .astc-style extradata for the decoder. The + * profile byte (extra[14]) is left 0 = LDR: KTX 1.0 has no HDR ASTC GL + * enum, so KTX files are always LDR (the .astc muxer rejects HDR). */ + memset(extra, 0, sizeof(extra)); + memcpy(extra, astc_magic, sizeof(astc_magic)); + extra[4] = (uint8_t)bx; + extra[5] = (uint8_t)by; + extra[6] = 1; + extra[7] = w & 0xFF; extra[8] = (w >> 8) & 0xFF; extra[9] = (w >> 16) & 0xFF; + extra[10] = h & 0xFF; extra[11] = (h >> 8) & 0xFF; extra[12] = (h >> 16) & 0xFF; + extra[13] = 1; + if (ff_alloc_extradata(st->codecpar, ASTC_HEADER_SIZE) < 0) + return AVERROR(ENOMEM); + memcpy(st->codecpar->extradata, extra, ASTC_HEADER_SIZE); + + avpriv_set_pts_info(st, 64, 1, 1); + return 0; +} + +static int ktx_read_packet(AVFormatContext *s, AVPacket *pkt) +{ + KTXDemuxerContext *ktx = s->priv_data; + AVIOContext *pb = s->pb; + uint32_t left = ktx->image_size; + int ret; + + if (!left) + return AVERROR_EOF; + + if ((ret = av_new_packet(pkt, left)) < 0) + return ret; + + ret = avio_read(pb, pkt->data, left); + if (ret < 0) { + av_packet_unref(pkt); + return ret; + } + av_shrink_packet(pkt, ret); + pkt->stream_index = 0; + pkt->flags |= AV_PKT_FLAG_KEY; + ktx->image_size = 0; /* single mip */ + return 0; +} + +const FFInputFormat ff_ktx_demuxer = { + .p.name = "ktx", + .p.long_name = NULL_IF_CONFIG_SMALL("KTX 1.0 (Khronos Texture) for ASTC"), + .p.mime_type = "image/ktx", + .p.extensions = "ktx", + .p.flags = AVFMT_NOTIMESTAMPS, + .priv_data_size = sizeof(KTXDemuxerContext), + .read_probe = ktx_probe, + .read_header = ktx_read_header, + .read_packet = ktx_read_packet, +}; diff --git a/libavformat/ktxenc.c b/libavformat/ktxenc.c new file mode 100644 index 0000000000..c04a781589 --- /dev/null +++ b/libavformat/ktxenc.c @@ -0,0 +1,159 @@ +/* + * KTX 1.0 muxer for ASTC + * 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 + * KTX 1.0 muxer for ASTC textures. + * + * Writes the KTX 1.0 header (64 bytes) followed by a uint32 image size and + * the raw ASTC bitstream. The block size is recovered from the encoder's + * .astc-style extradata; the sRGB vs linear GL enum is chosen by the + * "srgb" muxer option. + */ + +#include "avformat.h" +#include "avio.h" +#include "mux.h" +#include "libavutil/intreadwrite.h" +#include "libavutil/opt.h" + +#define ASTC_HEADER_SIZE 16 + +/* ASTC GL internal format enums (2D), linear base 0x93B0, sRGB = +0x10. + * Order matches the astc-encoder ASTC_FORMATS table. */ +static const int astc_gl_linear[14] = { + 0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7, + 0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD +}; +static const int astc_gl_srgb[14] = { + 0x93C0, 0x93C1, 0x93C2, 0x93C3, 0x93C4, 0x93C5, 0x93C6, 0x93C7, + 0x93C8, 0x93C9, 0x93CA, 0x93CB, 0x93CC, 0x93CD +}; +/* Block sizes paired with the enum tables above. */ +static const int astc_bx[14] = { 4, 5, 5, 6, 6, 8, 8, 8, 10, 10, 10, 10, 12, 12 }; +static const int astc_by[14] = { 4, 4, 5, 5, 6, 5, 6, 8, 5, 6, 8, 10, 10, 12 }; + +typedef struct KTXMuxerContext { + AVClass *class; + int srgb; +} KTXMuxerContext; + +static int ktx_write_header(AVFormatContext *s) +{ + KTXMuxerContext *ctx = s->priv_data; + AVStream *st = s->streams[0]; + const uint8_t *ed = st->codecpar->extradata; + uint8_t bx = 0, by = 0; + int glfmt = 0, i; + uint8_t hdr[64]; + + if (st->codecpar->extradata_size < ASTC_HEADER_SIZE) { + av_log(s, AV_LOG_ERROR, ".ktx muxer requires 16-byte extradata " + "(block size) from the encoder.\n"); + return AVERROR(EINVAL); + } + bx = ed[4]; + by = ed[5]; + + /* KTX 1.0 has no HDR ASTC GL enum, so HDR is only supported via the raw + * .astc container. Reject based on the encoder's profile (propagated + * via codecpar). */ + if (st->codecpar->profile >= 2 /* ASTCENC_PRF_HDR_RGB_LDR_A / HDR */) { + av_log(s, AV_LOG_ERROR, + "KTX 1.0 only supports LDR ASTC; use the .astc container for HDR output.\n"); + return AVERROR(EINVAL); + } + + /* KTX 1.0 has no GL enum for 3D ASTC blocks, only 2D. */ + if (ed[6] > 1) { + av_log(s, AV_LOG_ERROR, + "KTX 1.0 only supports 2D ASTC blocks; use the .astc container " + "for 3D block output.\n"); + return AVERROR(EINVAL); + } + + for (i = 0; i < 14; i++) { + if (astc_bx[i] == bx && astc_by[i] == by) { + glfmt = ctx->srgb ? astc_gl_srgb[i] : astc_gl_linear[i]; + break; + } + } + if (!glfmt) { + av_log(s, AV_LOG_ERROR, "Unsupported ASTC block size %dx%d for KTX.\n", bx, by); + return AVERROR(EINVAL); + } + + memset(hdr, 0, sizeof(hdr)); + /* magic "«KTX 11»\r\n\x1A\n" */ + hdr[0] = 0xAB; hdr[1] = 'K'; hdr[2] = 'T'; hdr[3] = 'X'; + hdr[4] = ' '; hdr[5] = '1'; hdr[6] = '1'; hdr[7] = 0xBB; + hdr[8] = 0x0D; hdr[9] = 0x0A; hdr[10] = 0x1A; hdr[11] = 0x0A; + AV_WL32(hdr + 12, 0x04030201); /* endianness */ + /* gl_type(16)=0, gl_type_size(20)=1, gl_format(24)=0 */ + AV_WL32(hdr + 20, 1); + AV_WL32(hdr + 28, glfmt); /* gl_internal_format */ + AV_WL32(hdr + 32, 0x1908); /* gl_base_internal_format = GL_RGBA */ + AV_WL32(hdr + 36, st->codecpar->width); /* pixel_width */ + AV_WL32(hdr + 40, st->codecpar->height); /* pixel_height */ + /* pixel_depth(44)=0, array(48)=0, faces(52)=1, miplevels(56)=1, kvdata(60)=0 */ + + avio_write(s->pb, hdr, sizeof(hdr)); + return 0; +} + +static int ktx_write_packet(AVFormatContext *s, AVPacket *pkt) +{ + avio_wl32(s->pb, pkt->size); /* imageSize */ + avio_write(s->pb, pkt->data, pkt->size); + return 0; +} + +#define OFFSET(x) offsetof(KTXMuxerContext, x) +#define VE AV_OPT_FLAG_ENCODING_PARAM +static const AVOption ktx_options[] = { + { "srgb", "Write sRGB GL internal format instead of linear", OFFSET(srgb), + AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, + { NULL }, +}; + +static const AVClass ktx_muxer_class = { + .class_name = "ktx_muxer", + .item_name = av_default_item_name, + .option = ktx_options, + .version = LIBAVUTIL_VERSION_INT, +}; + +const FFOutputFormat ff_ktx_muxer = { + .p.name = "ktx", + .p.long_name = NULL_IF_CONFIG_SMALL("KTX 1.0 (Khronos Texture) for ASTC"), + .p.mime_type = "image/ktx", + .p.extensions = "ktx", + .p.audio_codec = AV_CODEC_ID_NONE, + .p.video_codec = AV_CODEC_ID_ASTC, + .p.subtitle_codec = AV_CODEC_ID_NONE, + .flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH | + FF_OFMT_FLAG_ONLY_DEFAULT_CODECS, + .priv_data_size = sizeof(KTXMuxerContext), + .p.priv_class = &ktx_muxer_class, + .write_header = ktx_write_header, + .write_packet = ktx_write_packet, + .p.flags = AVFMT_NOTIMESTAMPS, +}; -- 2.52.0 From 765dcc00b8a47e2c216a73f9ea0bc60f63ac26eb Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Thu, 16 Jul 2026 11:19:17 +0800 Subject: [PATCH 3/4] fate: add ASTC LDR/HDR round-trip and container tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add self-contained FATE tests for the ASTC integration that require no external samples. fate-astc-ldr – LDR encode/decode round-trip (.astc) fate-astc-ktx – LDR encode/decode round-trip (.ktx) fate-astc-hdr – float input auto-promotes to HDR and decodes to rgbaf16le without clamping fate-astc-ktx-hdr-reject – confirms the KTX muxer rejects HDR input with the expected log message All four tests use the self-generated lavfi testsrc source with a single-frame cap (-frames:v 1) to avoid hangs on infinite sources. The transcode helper in fate-run.sh is guarded against mangling lavfi graph strings through target_path. Signed-off-by: Jun Zhao <[email protected]> --- tests/Makefile | 1 + tests/fate-run.sh | 7 +- tests/fate/astc.mak | 82 ++++++++++++++++++++++++ tests/ref/fate/astc-3d | 8 +++ tests/ref/fate/astc-hdr | 8 +++ tests/ref/fate/astc-hdr-profile-mismatch | 8 +++ tests/ref/fate/astc-ktx | 8 +++ tests/ref/fate/astc-ktx-hdr-reject | 0 tests/ref/fate/astc-ldr | 8 +++ 9 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 tests/fate/astc.mak create mode 100644 tests/ref/fate/astc-3d create mode 100644 tests/ref/fate/astc-hdr create mode 100644 tests/ref/fate/astc-hdr-profile-mismatch create mode 100644 tests/ref/fate/astc-ktx create mode 100644 tests/ref/fate/astc-ktx-hdr-reject create mode 100644 tests/ref/fate/astc-ldr diff --git a/tests/Makefile b/tests/Makefile index 3f0132dd97..0956c48f71 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -185,6 +185,7 @@ include $(SRC_PATH)/tests/fate/dvvideo.mak include $(SRC_PATH)/tests/fate/ea.mak include $(SRC_PATH)/tests/fate/exif.mak include $(SRC_PATH)/tests/fate/enc_external.mak +include $(SRC_PATH)/tests/fate/astc.mak # Must be included after lavf-video.mak include $(SRC_PATH)/tests/fate/ffmpeg.mak include $(SRC_PATH)/tests/fate/ffprobe.mak diff --git a/tests/fate-run.sh b/tests/fate-run.sh index d0726c64dd..f70f7e8037 100755 --- a/tests/fate-run.sh +++ b/tests/fate-run.sh @@ -319,7 +319,12 @@ transcode(){ test -z "$final_encode_muxer" && final_encode_muxer="framecrc" encfile="${outdir}/${test}.${enc_fmt}" test $keep -ge 1 || cleanfiles="$cleanfiles $encfile" - tsrcfile=$(target_path $srcfile) + # lavfi graphs are not file paths, so do not run them through target_path. + if [ "$src_fmt" = "lavfi" ]; then + tsrcfile="$srcfile" + else + tsrcfile=$(target_path $srcfile) + fi tencfile=$(target_path $encfile) ffmpeg -f $src_fmt $DEC_OPTS $enc_opt_in -i $tsrcfile $additional_input \ $ENC_OPTS $enc_opt $FLAGS -f $enc_fmt -y $tencfile || return diff --git a/tests/fate/astc.mak b/tests/fate/astc.mak new file mode 100644 index 0000000000..a1a821922d --- /dev/null +++ b/tests/fate/astc.mak @@ -0,0 +1,82 @@ +# ASTC (Adaptive Scalable Texture Compression) codec + container tests. +# +# Input is the self-generated "testsrc" lavfi source. astcenc compression is +# fully deterministic, so encode -> container -> decode can be locked down with +# framecrc/md5 with no external sample files required. +# +# The codec is single-image, but the lavfi sources are *infinite*, so every +# test caps the encode to a single frame with -frames:v 1 (otherwise the encode +# step would never reach EOF and the test would hang). +# +# testsrc is native rgb24, so no pixel-format conversion is needed under +# FATE's -noauto_conversion_filters. For the HDR path the graph explicitly +# requests format=gbrapf32le: swscale cannot convert *to* packed +# half/float (rgbaf16le/rgbaf32le) but it CAN produce planar float +# (gbrapf32le), which the encoder also accepts as HDR input and auto-promotes +# to HDR_RGB_LDR_A. + +# LDR round-trip through the raw .astc container. +FATE_ASTC-$(call ENCDEC, LIBASTCENC, ASTC) += fate-astc-ldr +fate-astc-ldr: CMD = transcode lavfi "testsrc=size=128x128" astc \ + "-c:v libastcenc -block_size 8x8 -frames:v 1" "" "" "" "" "" + +# LDR round-trip through the KTX 1.0 container (LDR only; HDR is rejected by +# the KTX muxer by design). +FATE_ASTC-$(call ENCDEC, LIBASTCENC, KTX) += fate-astc-ktx +fate-astc-ktx: CMD = transcode lavfi "testsrc=size=128x128" ktx \ + "-c:v libastcenc -block_size 8x8 -frames:v 1" "" "" "" "" "" + +# HDR round-trip: float/half input auto-promotes the profile to +# HDR_RGB_LDR_A (header byte 14 = 2); the decoder mirrors it and emits +# rgbaf16le (no 8-bit clamp). Verifies the HDR encode/decode chain end to end. +# +# Note: swscale cannot convert *to* packed half/float (rgbaf16le/rgbaf32le), +# but it CAN produce planar float (gbrapf32le), which the encoder also accepts +# as a valid HDR input. testsrc -> gbrapf32le exercises the float path. +FATE_ASTC-$(call ENCDEC, LIBASTCENC, ASTC) += fate-astc-hdr +fate-astc-hdr: CMD = transcode lavfi "testsrc=size=128x128,format=gbrapf32le" astc \ + "-c:v libastcenc -block_size 8x8 -frames:v 1" "" "" "" "-dec_profile hdr-ldr-a" "" + +# HDR profile mismatch: ASTC bitstreams do not carry a profile field, so if +# the decoder is asked to decode HDR-encoded endpoints with the (default) +# LDR profile, astcenc's endpoint unpacking intentionally returns a fixed +# magenta error color (0xFF, 0x00, 0xFF) rather than garbage or a decode +# failure -- this is astcenc's own defined behavior for a profile mismatch, +# confirmed against the astcenc reference CLI decoding the same bitstream +# with the wrong profile flag. Locking down the magenta output as a +# regression test ensures our decoder keeps propagating this signal instead +# of silently misinterpreting HDR data as LDR. +# +# The source pixel value (2.5) is genuinely out of the LDR [0,1] range so +# the encoder is forced to use HDR endpoint formats; decoding those with +# the LDR profile is what triggers the magenta error path. +FATE_ASTC-$(call ENCDEC, LIBASTCENC, ASTC) += fate-astc-hdr-profile-mismatch +fate-astc-hdr-profile-mismatch: CMD = transcode lavfi \ + "testsrc2=size=64x64,format=gbrapf32le,geq=r=2.5:g=2.5:b=2.5:a=1" astc \ + "-c:v libastcenc -block_size 8x8 -profile:v hdr-ldr-a -frames:v 1" "" "" "" "" "" + +# 3D block round-trip: a 3D block size (4x4x4) compresses a 2D image using +# three-dimensional partition patterns. The block_z value is carried in the +# .astc header and mirrored by the decoder. +FATE_ASTC-$(call ENCDEC, LIBASTCENC, ASTC) += fate-astc-3d +fate-astc-3d: CMD = transcode lavfi "testsrc=size=64x64" astc \ + "-c:v libastcenc -block_size 4x4x4 -frames:v 1" "" "" "" "" "" + +# Negative test: KTX 1.0 has no HDR ASTC GL enum, so feeding HDR (float/half) +# input to the .ktx muxer must be rejected with AVERROR(EINVAL) and the +# specific "KTX 1.0 only supports LDR ASTC" message. The command returns 0 +# only if ffmpeg exits non-zero AND that message is present in its log, +# so the test fails both if KTX wrongly accepts HDR and if ffmpeg dies for +# any other reason. The empty ref file just satisfies FATE's "ref exists" +# check (its diff against the empty stdout is a no-op); the real verdict is +# the command's own exit status. +FATE_ASTC-$(call ENCDEC, LIBASTCENC, KTX) += fate-astc-ktx-hdr-reject +fate-astc-ktx-hdr-reject: CMD = ffmpeg -y -f lavfi -i "testsrc=size=128x128,format=gbrapf32le" \ + -threads 1 -c:v libastcenc -block_size 8x8 -frames:v 1 \ + -f ktx tests/data/fate/astc-ktx-hdr-reject.ktx ; \ + test $$? -ne 0 && grep -q "KTX 1.0 only supports LDR ASTC" tests/data/fate/astc-ktx-hdr-reject.err + +FATE_ASTC += $(FATE_ASTC-yes) +fate-astc: $(FATE_ASTC-yes) + +FATE += $(FATE_ASTC) diff --git a/tests/ref/fate/astc-3d b/tests/ref/fate/astc-3d new file mode 100644 index 0000000000..fd2efb7ca3 --- /dev/null +++ b/tests/ref/fate/astc-3d @@ -0,0 +1,8 @@ +992ecc2713a9ca3e4f9a2a838ce91a7f *tests/data/fate/astc-3d.astc +4112 tests/data/fate/astc-3d.astc +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x64 +#sar 0: 0/1 +0, 0, 0, 1, 16384, 0x8e695e67 diff --git a/tests/ref/fate/astc-hdr b/tests/ref/fate/astc-hdr new file mode 100644 index 0000000000..c00730a3c6 --- /dev/null +++ b/tests/ref/fate/astc-hdr @@ -0,0 +1,8 @@ +bf52e531c5700cd2039de30c0cdb7a22 *tests/data/fate/astc-hdr.astc +4112 tests/data/fate/astc-hdr.astc +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 128x128 +#sar 0: 0/1 +0, 0, 0, 1, 131072, 0x55277a51 diff --git a/tests/ref/fate/astc-hdr-profile-mismatch b/tests/ref/fate/astc-hdr-profile-mismatch new file mode 100644 index 0000000000..754aadb05a --- /dev/null +++ b/tests/ref/fate/astc-hdr-profile-mismatch @@ -0,0 +1,8 @@ +4e488750dca715be36407f4ca18c0bed *tests/data/fate/astc-hdr-profile-mismatch.astc +1040 tests/data/fate/astc-hdr-profile-mismatch.astc +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x64 +#sar 0: 0/1 +0, 0, 0, 1, 16384, 0x9c27d2c1 diff --git a/tests/ref/fate/astc-ktx b/tests/ref/fate/astc-ktx new file mode 100644 index 0000000000..13973aab64 --- /dev/null +++ b/tests/ref/fate/astc-ktx @@ -0,0 +1,8 @@ +694ebaecee2c3349c4e6c3b0d0a7ad10 *tests/data/fate/astc-ktx.ktx +4164 tests/data/fate/astc-ktx.ktx +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 128x128 +#sar 0: 0/1 +0, 0, 0, 1, 65536, 0x08d2628c diff --git a/tests/ref/fate/astc-ktx-hdr-reject b/tests/ref/fate/astc-ktx-hdr-reject new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/ref/fate/astc-ldr b/tests/ref/fate/astc-ldr new file mode 100644 index 0000000000..34e1054217 --- /dev/null +++ b/tests/ref/fate/astc-ldr @@ -0,0 +1,8 @@ +fa51c1a57dca4983debf4cc6b3a2c53b *tests/data/fate/astc-ldr.astc +4112 tests/data/fate/astc-ldr.astc +#tb 0: 1/1 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 128x128 +#sar 0: 0/1 +0, 0, 0, 1, 65536, 0x08d2628c -- 2.52.0 From afb4c7544be67bcb61d20de60c54fb59305195a3 Mon Sep 17 00:00:00 2001 From: Jun Zhao <[email protected]> Date: Thu, 16 Jul 2026 11:19:17 +0800 Subject: [PATCH 4/4] doc: add libastcenc encoder documentation and Changelog entry Document the libastcenc wrapper (pixel formats, options, HDR behavior) in doc/encoders.texi alongside other external encoder sections. Add a library How-To entry in doc/general_contents.texi and announce the feature in Changelog. Signed-off-by: Jun Zhao <[email protected]> --- Changelog | 1 + doc/encoders.texi | 72 +++++++++++++++++++++++++++++++++++++++ doc/general_contents.texi | 9 +++++ 3 files changed, 82 insertions(+) diff --git a/Changelog b/Changelog index f10d639db7..57b7d405e0 100644 --- a/Changelog +++ b/Changelog @@ -8,6 +8,7 @@ version <next>: - LCEVC payload merging bitstream filter - MVR demuxer - latticepal filter +- ASTC image encoder/decoder via libastcenc and .astc/.ktx container support version 9.0: diff --git a/doc/encoders.texi b/doc/encoders.texi index 605638e040..ae8edf35f5 100644 --- a/doc/encoders.texi +++ b/doc/encoders.texi @@ -2616,6 +2616,78 @@ Text-like @end table +@section libastcenc + +ARM ASTC Encoder wrapper. + +This encoder requires the Arm ASTC Encoder (astc-encoder) library, which +provides high-quality GPU texture compression to the ASTC format. You need +to explicitly configure the build with @code{--enable-libastcenc}. + +The encoder supports both LDR (8-bit RGBA/RGB) and HDR (float/half RGBA) +input. When float or half input is detected the profile is automatically +promoted to HDR RGB with LDR alpha (@code{HDR_RGB_LDR_A}). + +The raw bitstream can be packaged in the @file{.astc} container (16-byte +header + compressed blocks) or the Khronos Texture 1.0 container +(@file{.ktx}) which is LDR-only (KTX 1.0 has no HDR ASTC GL enum). + +@subsection Pixel Format + +The encoder accepts the following pixel formats: +@itemize +@item @code{rgba} — 8-bit packed RGBA (LDR). +@item @code{rgb24} — 8-bit packed RGB (LDR, opaque alpha synthesized). +@item @code{rgbaf16le} — 16-bit floating-point RGBA (HDR). +@item @code{rgbaf32le} — 32-bit floating-point RGBA (HDR). +@item @code{gbrapf32le} — planar 32-bit floating-point GBR+Alpha (HDR). +@end itemize + +@subsection Options + +@table @option + +@item block_size @var{string} +ASTC block size specified as @var{W}x@var{H}[@code{x}@var{D}]. +When the third dimension is omitted it defaults to 1 (2D block). +Valid 2D: 4x4, 5x4, 5x5, 6x5, 6x6, 8x5, 8x6, 8x8, 10x5, 10x6, +10x8, 10x10, 12x10, 12x12. Valid 3D: 3x3x3, 4x3x3, 4x4x3, 4x4x4, +5x4x4, 5x5x4, 5x5x5, 6x5x5, 6x6x5, 6x6x6, and higher. +Default is @code{8x8}. + +@item quality @var{float} +Compression quality from 0 (fastest) to 100 (slowest, exhaustive search). +Default is @code{60}. + +@item profile @var{int} +Color profile. Possible values: +@table @samp +@item ldr +Linear LDR. +@item ldr-srgb +sRGB LDR (default). +@item hdr-ldr-a +HDR RGB channels with LDR alpha channel. +@item hdr +Full HDR. +@end table + +When float/half input is detected with an LDR profile, the profile is +automatically promoted to @code{hdr-ldr-a}. Conversely, selecting an HDR +profile with 8-bit input is rejected. + +@item threads @var{int} +Number of compression threads (1..16). Default is @code{1}. + +@item perceptual @var{boolean} +Use the perceptual (PSNR-weighted) error metric instead of peak-SNR. +Default is @code{0}. + +@item alpha_weight @var{boolean} +Enable per-texel alpha weighting. Default is @code{1}. + +@end table + @section libx264, libx264rgb x264 H.264/MPEG-4 AVC encoder wrapper. diff --git a/doc/general_contents.texi b/doc/general_contents.texi index 7a1c4f3a21..c4fa491c82 100644 --- a/doc/general_contents.texi +++ b/doc/general_contents.texi @@ -13,6 +13,15 @@ Go to @url{http://aomedia.org/} and follow the instructions for installing the library. Then pass @code{--enable-libaom} to configure to enable it. +@section ARM ASTC Encoder + +FFmpeg can make use of the Arm ASTC Encoder library for ASTC image +compression and decompression. + +Go to @url{https://github.com/ARM-software/astc-encoder} and follow the +instructions for building and installing the library. Then pass +@code{--enable-libastcenc} to configure to enable it. + @section AMD AMF/VCE FFmpeg can use the AMD Advanced Media Framework library -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
