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]

Reply via email to