blob: 4526ad6b0de1624825eeebcec9417f21bbda005d [file] [edit]
/*
* 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,
};