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