| /* | |
| * 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 "profiles.h" | |
| #include "libavutil/intreadwrite.h" | |
| #include "libavutil/mem.h" | |
| #include "libavutil/opt.h" | |
| /* .astc container magic (little-endian 0x5CA1AB13). */ | |
| #define ASTC_MAGIC 0x5CA1AB13 | |
| #define ASTC_HEADER_SIZE 16 | |
| #define ASTC_BLOCK_BYTES 16 | |
| typedef struct LibAstcDecContext { | |
| AVClass *class; | |
| struct astcenc_config cfg; | |
| struct astcenc_context *ctx; | |
| int block_x; | |
| int block_y; | |
| int block_z; | |
| int is_hdr; | |
| int dec_profile; /* astcenc_profile, set via private option, -1 = auto */ | |
| } LibAstcDecContext; | |
| static av_cold int astcdec_configure(AVCodecContext *avctx, | |
| enum astcenc_profile prf) | |
| { | |
| LibAstcDecContext *s = avctx->priv_data; | |
| enum astcenc_error err; | |
| if (s->ctx) { | |
| astcenc_context_free(s->ctx); | |
| s->ctx = NULL; | |
| } | |
| err = astcenc_config_init(prf, s->block_x, s->block_y, s->block_z, | |
| 0, ASTCENC_FLG_DECOMPRESS_ONLY, &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, 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); | |
| } | |
| s->is_hdr = (prf == ASTCENC_PRF_HDR || prf == ASTCENC_PRF_HDR_RGB_LDR_A); | |
| /* Leave avctx->profile untouched: it is a property of the stream, set by | |
| * the container, while the profile resolved here is an internal decision. | |
| * avformat_find_stream_info() copies this context back into the stream | |
| * parameters, so publishing the resolved profile would, for example, turn | |
| * a linear KTX into a stream the KTX muxer refuses to copy. */ | |
| avctx->pix_fmt = s->is_hdr ? AV_PIX_FMT_RGBAF16 : AV_PIX_FMT_RGBA; | |
| return 0; | |
| } | |
| static av_cold int libastcdec_init(AVCodecContext *avctx) | |
| { | |
| LibAstcDecContext *s = avctx->priv_data; | |
| const uint8_t *ed = avctx->extradata; | |
| 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; | |
| } | |
| /* .astc header layout (all little-endian): | |
| * [0-3] magic | |
| * [4-6] block dimensions block_x, block_y, block_z (block_z > 1 | |
| * selects a 3D block footprint, e.g. 4x4x4) | |
| * [7-9] image width (dim_x, texels) | |
| * [10-12] image height (dim_y, texels) | |
| * [13-15] image depth (dim_z, texel slices; 1 for a 2D image) | |
| * block_z (the block footprint) is independent of dim_z (the image | |
| * slice count), so a 3D block can still describe a 2D image. */ | |
| if (AV_RL32(ed) != ASTC_MAGIC || AV_RL24(ed + 13) != 1) { | |
| av_log(avctx, AV_LOG_ERROR, "Invalid ASTC header.\n"); | |
| return AVERROR_INVALIDDATA; | |
| } | |
| s->block_x = ed[4]; | |
| s->block_y = ed[5]; | |
| s->block_z = ed[6]; | |
| avctx->width = AV_RL24(ed + 7); | |
| avctx->height = AV_RL24(ed + 10); | |
| /* Zero footprints would divide by zero in the block accounting below; the | |
| * .astc demuxer rejects them, but the decoder must not trust its input. */ | |
| if (!s->block_x || !s->block_y || !s->block_z || | |
| avctx->width <= 0 || avctx->height <= 0) { | |
| av_log(avctx, AV_LOG_ERROR, | |
| "Invalid ASTC header: block %dx%dx%d, image %dx%d.\n", | |
| s->block_x, s->block_y, s->block_z, | |
| avctx->width, avctx->height); | |
| return AVERROR_INVALIDDATA; | |
| } | |
| /* The .astc header does not record the intended decoding profile. | |
| * Individual blocks identify their endpoint encodings, but that does not | |
| * establish the sRGB versus linear interpretation. Use the container | |
| * metadata or -dec_profile; without either, a raw .astc input defaults to | |
| * ldr-srgb. A stream that records no profile at all keeps the | |
| * AV_PROFILE_UNKNOWN default and lands in the fallback branch below. */ | |
| enum astcenc_profile prf = (enum astcenc_profile)s->dec_profile; | |
| if (s->dec_profile < 0) { | |
| switch (avctx->profile) { | |
| case AV_PROFILE_ASTC_LDR_SRGB: | |
| prf = ASTCENC_PRF_LDR_SRGB; | |
| break; | |
| case AV_PROFILE_ASTC_LDR: | |
| prf = ASTCENC_PRF_LDR; | |
| break; | |
| case AV_PROFILE_ASTC_HDR_RGB_LDR_A: | |
| prf = ASTCENC_PRF_HDR_RGB_LDR_A; | |
| break; | |
| case AV_PROFILE_ASTC_HDR: | |
| prf = ASTCENC_PRF_HDR; | |
| break; | |
| case AV_PROFILE_ASTC_LINEAR_ANY: | |
| /* A linear texture may hold HDR endpoint encodings, so sample it | |
| * with HDR precision; LDR endpoint blocks decode correctly through | |
| * the same profile instead of coming back as the magenta error | |
| * colour. */ | |
| prf = ASTCENC_PRF_HDR_RGB_LDR_A; | |
| break; | |
| default: | |
| /* No container information (a raw .astc stream): the documented | |
| * LDR sRGB fallback. HDR needs an explicit -dec_profile. */ | |
| prf = ASTCENC_PRF_LDR_SRGB; | |
| break; | |
| } | |
| } | |
| return astcdec_configure(avctx, prf); | |
| } | |
| static int libastcdec_decode(AVCodecContext *avctx, AVFrame *frame, | |
| int *got_frame, AVPacket *avpkt) | |
| { | |
| LibAstcDecContext *s = avctx->priv_data; | |
| const int w = avctx->width; | |
| const int h = avctx->height; | |
| const int elem = s->is_hdr ? 2 : 1; | |
| size_t row_bytes, buf_size, block_count, expected; | |
| if (av_size_mult(w, 4 * elem, &row_bytes) < 0 || | |
| av_size_mult(h, row_bytes, &buf_size) < 0 || | |
| av_size_mult((w - 1) / s->block_x + 1, | |
| (h - 1) / s->block_y + 1, &block_count) < 0 || | |
| av_size_mult(block_count, ASTC_BLOCK_BYTES, &expected) < 0) | |
| return AVERROR_INVALIDDATA; | |
| /* dim_z is always 1 (2D input), so blocks_z is always 1. */ | |
| 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; | |
| /* A single ASTC image is exactly the blocks its geometry needs; accepting | |
| * a larger packet would silently ignore the trailing data below. */ | |
| if ((size_t)avpkt->size != expected) { | |
| av_log(avctx, AV_LOG_ERROR, | |
| "ASTC packet size %d does not match the %zu bytes required for " | |
| "a %dx%d image with %dx%d blocks.\n", | |
| avpkt->size, expected, w, h, s->block_x, s->block_y); | |
| return AVERROR_INVALIDDATA; | |
| } | |
| frame->width = w; | |
| frame->height = h; | |
| if ((ret = ff_get_buffer(avctx, frame, 0)) < 0) | |
| return ret; | |
| /* astcenc writes a tightly packed image. If the frame has no line padding | |
| * we can decode straight into it; otherwise use a temporary buffer and | |
| * copy row by row. */ | |
| if (frame->linesize[0] == row_bytes) { | |
| data_ptrs[0] = frame->data[0]; | |
| } else { | |
| out_buf = av_malloc(buf_size); | |
| if (!out_buf) | |
| return AVERROR(ENOMEM); | |
| data_ptrs[0] = out_buf; | |
| } | |
| img.dim_x = w; | |
| img.dim_y = h; | |
| img.dim_z = 1; | |
| img.data_type = s->is_hdr ? ASTCENC_TYPE_F16 : ASTCENC_TYPE_U8; | |
| 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; | |
| } | |
| if (out_buf) { | |
| for (int y = 0; y < h; y++) | |
| memcpy(frame->data[0] + (size_t)y * frame->linesize[0], | |
| out_buf + (size_t)y * row_bytes, row_bytes); | |
| } | |
| *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 = -1 }, -1, 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_RGBAF16), | |
| .p.profiles = NULL_IF_CONFIG_SMALL(ff_astc_profiles), | |
| .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, | |
| }; |