1
0
mirror of https://git.ffmpeg.org/ffmpeg.git synced 2026-10-24 02:09:03 +03:00
Files
ffmpeg/libavcodec/libastcenc.c
Jun Zhao 6e9100c648 lavc: add ASTC encoder/decoder via libastcenc
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>
2026-09-14 12:44:07 +00:00

412 lines
16 KiB
C

/*
* 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 <limits.h>
#include "avcodec.h"
#include "codec_internal.h"
#include "encode.h"
#include "profiles.h"
#include "libavutil/intreadwrite.h"
#include "libavutil/pixdesc.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 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 perceptual;
int alpha_weight;
} LibAstcEncContext;
static av_cold int libastcenc_init(AVCodecContext *avctx)
{
LibAstcEncContext *s = avctx->priv_data;
enum astcenc_profile prf;
unsigned int flags = 0;
enum astcenc_error err;
/* An explicitly set private profile option overrides the public
* AVCodecContext.profile field. AV_PROFILE_ASTC_LINEAR_ANY describes how a
* container should interpret a stream and never selects an encoding
* profile, so it is rejected here. */
if (s->profile >= 0) {
prf = (enum astcenc_profile)s->profile;
} else {
switch (avctx->profile) {
case AV_PROFILE_UNKNOWN:
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;
default:
av_log(avctx, AV_LOG_ERROR,
"Unsupported ASTC encoding profile %d.\n", avctx->profile);
return AVERROR(EINVAL);
}
}
/* 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. */
int is_float = (avctx->pix_fmt == AV_PIX_FMT_RGBAF16 ||
avctx->pix_fmt == AV_PIX_FMT_RGBAF32 ||
avctx->pix_fmt == AV_PIX_FMT_GBRAPF32);
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 rgbaf16) for HDR.\n");
return AVERROR(EINVAL);
}
s->profile = (int)prf;
/* Publish the profile the container records, as one of the public ASTC
* profile values rather than as the astcenc enum, whose ordering is not
* part of the FFmpeg API. */
switch (prf) {
case ASTCENC_PRF_LDR_SRGB:
avctx->profile = AV_PROFILE_ASTC_LDR_SRGB;
break;
case ASTCENC_PRF_LDR:
avctx->profile = AV_PROFILE_ASTC_LDR;
break;
case ASTCENC_PRF_HDR_RGB_LDR_A:
avctx->profile = AV_PROFILE_ASTC_HDR_RGB_LDR_A;
break;
case ASTCENC_PRF_HDR:
avctx->profile = AV_PROFILE_ASTC_HDR;
break;
default:
avctx->profile = AV_PROFILE_UNKNOWN;
break;
}
if (avctx->width <= 0 || avctx->height <= 0 ||
avctx->width > 0xFFFFFF || avctx->height > 0xFFFFFF) {
av_log(avctx, AV_LOG_ERROR, "Invalid image dimensions %dx%d.\n",
avctx->width, avctx->height);
return AVERROR(EINVAL);
}
/* Accept exactly "WxH" or "WxHxD". Scanning stops at the third literal 'x'
* when it is absent, so trailing garbage such as "8x8junk" has to be
* caught by checking that the whole string was consumed. */
int consumed = 0;
s->block_z = 1;
if (sscanf(s->block_size_str, "%dx%dx%d%n", &s->block_x, &s->block_y,
&s->block_z, &consumed) != 3) {
consumed = 0;
s->block_z = 1;
sscanf(s->block_size_str, "%dx%d%n", &s->block_x, &s->block_y,
&consumed);
}
if (!consumed || s->block_size_str[consumed]) {
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->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;
}
/* astcenc normalizes the requested footprint, e.g. a zero block depth is
* clamped to one, so publish the validated footprint instead of the parsed
* one. Both the .astc header below and the block accounting in encode()
* must agree with what the library actually compresses with. */
s->block_x = s->cfg.block_x;
s->block_y = s->cfg.block_y;
s->block_z = s->cfg.block_z;
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);
}
/* 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. The buffer is padded so consumers that read
* it as a bitstream do not over-read. */
avctx->extradata = av_mallocz(ASTC_HEADER_SIZE + AV_INPUT_BUFFER_PADDING_SIZE);
if (!avctx->extradata)
return AVERROR(ENOMEM);
avctx->extradata_size = ASTC_HEADER_SIZE;
{
uint8_t *h = avctx->extradata;
/* .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. */
AV_WL32(h, ASTC_MAGIC);
h[4] = (uint8_t)s->block_x;
h[5] = (uint8_t)s->block_y;
h[6] = (uint8_t)s->block_z;
AV_WL24(h + 7, avctx->width);
AV_WL24(h + 10, avctx->height);
AV_WL24(h + 13, 1); /* image depth is 1: 2D image (3D blocks stay 2D) */
}
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;
size_t blocks_x, blocks_y, block_count, out_size;
if (w != avctx->width || h != avctx->height) {
av_log(avctx, AV_LOG_ERROR,
"Frame size %dx%d does not match configured %dx%d.\n",
w, h, avctx->width, avctx->height);
return AVERROR_INVALIDDATA;
}
blocks_x = (w - 1) / bx + 1;
blocks_y = (h - 1) / by + 1;
if (av_size_mult(blocks_x, blocks_y, &block_count) < 0 ||
av_size_mult(block_count, ASTC_BLOCK_BYTES, &out_size) < 0 ||
out_size > INT_MAX)
return AVERROR(EINVAL);
struct astcenc_swizzle swz = {
ASTCENC_SWZ_R, ASTCENC_SWZ_G, ASTCENC_SWZ_B, ASTCENC_SWZ_A
};
uint8_t *buf = NULL;
uint8_t *packed; /* buffer handed to astcenc, buf or the frame itself */
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; /* GBRAPF32 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_RGBAF16: type = ASTCENC_TYPE_F16; elem = 2; planar = 0; const_alpha = 0; break;
case AV_PIX_FMT_RGBAF32: type = ASTCENC_TYPE_F32; elem = 4; planar = 0; const_alpha = 0; break;
case AV_PIX_FMT_GBRAPF32: 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);
}
size_t row_bytes, buf_size;
if (av_size_mult(w, 4 * elem, &row_bytes) < 0 ||
av_size_mult(h, row_bytes, &buf_size) < 0)
return AVERROR(EINVAL);
/* Packed input whose lines already have the size astcenc expects needs no
* repacking, so it can be consumed in place. */
if (!planar && !const_alpha && frame->linesize[0] == (int)row_bytes) {
packed = frame->data[0];
} else {
buf = av_malloc(buf_size);
if (!buf)
return AVERROR(ENOMEM);
packed = buf;
if (planar) {
/* GBRAPF32 -> 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] = packed;
img.data = data_ptrs;
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[xD] (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 (overrides AVCodecContext.profile)",
OFFSET(profile), AV_OPT_TYPE_INT, { .i64 = -1 }, -1, 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" },
{ "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,
CODEC_PIXFMTS(AV_PIX_FMT_RGBA, AV_PIX_FMT_RGB24,
AV_PIX_FMT_RGBAF16, AV_PIX_FMT_RGBAF32, AV_PIX_FMT_GBRAPF32),
.p.profiles = NULL_IF_CONFIG_SMALL(ff_astc_profiles),
.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,
};