mirror of
https://git.ffmpeg.org/ffmpeg.git
synced 2026-10-24 02:09:03 +03:00
Add the ASTC codec ID and descriptor together with encoder and decoder wrappers for Arm's astc-encoder library. Support the four ASTC profiles, 2D and 3D block footprints, LDR RGB/RGBA input, and floating-point HDR input without clamping the decoded result to 8-bit. Keep the ASTC image dimensions and compressed block sizes bounded by the format and packet limits, and pass KTX-provided color profile metadata to the decoder when no explicit profile was selected. libastcenc is distributed under the Apache License 2.0, so the library is only available when FFmpeg is built with --enable-version3; list it with the other libraries that require that option. Signed-off-by: Jun Zhao <barryjzhao@tencent.com>
304 lines
11 KiB
C
304 lines
11 KiB
C
/*
|
|
* 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,
|
|
};
|