mirror of
https://github.com/FFmpeg/FFmpeg.git
synced 2026-11-06 05:17:42 +00:00
Add single-image ASTC and KTX 1.0 muxers and demuxers for the libastcenc codec. Validate the KTX layout, ASTC dimensions, color profile, and compressed payload boundaries before exposing a packet to the decoder. Map the KTX 1.0 2D linear and sRGB ASTC formats to the corresponding libastcenc profiles, and reject unsupported HDR, 3D, array, cubemap, and mipmap layouts instead of producing ambiguous image streams. The KTX demuxer turns the KTXorientation key into display metadata, and the KTX muxer writes the key back, so a stream copied from a flipped texture keeps its row order. Its srgb option follows the encoder's color profile by default, and an explicit value that contradicts a known profile is rejected instead of silently relabelling the texture. Signed-off-by: Jun Zhao <barryjzhao@tencent.com>
296 lines
12 KiB
C
296 lines
12 KiB
C
/*
|
|
* KTX 1.0 muxer for ASTC
|
|
* 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
|
|
* KTX 1.0 muxer for ASTC textures.
|
|
*
|
|
* Writes the KTX 1.0 header (64 bytes) followed by a uint32 image size and
|
|
* the raw ASTC bitstream. The block size is recovered from the encoder's
|
|
* .astc-style extradata; the sRGB vs linear GL enum is chosen by the
|
|
* "srgb" muxer option.
|
|
*/
|
|
|
|
#include "avformat.h"
|
|
#include "avio.h"
|
|
#include "mux.h"
|
|
#include "libavcodec/defs.h"
|
|
#include "libavutil/intreadwrite.h"
|
|
#include "libavutil/opt.h"
|
|
|
|
#define ASTC_HEADER_SIZE 16
|
|
static const uint8_t astc_magic[4] = { 0x13, 0xAB, 0xA1, 0x5C };
|
|
|
|
/* ASTC GL internal format enums (2D), linear base 0x93B0, sRGB = +0x20.
|
|
* Order matches the astc-encoder ASTC_FORMATS table. */
|
|
static const int astc_gl_linear[14] = {
|
|
0x93B0, 0x93B1, 0x93B2, 0x93B3, 0x93B4, 0x93B5, 0x93B6, 0x93B7,
|
|
0x93B8, 0x93B9, 0x93BA, 0x93BB, 0x93BC, 0x93BD
|
|
};
|
|
static const int astc_gl_srgb[14] = {
|
|
0x93D0, 0x93D1, 0x93D2, 0x93D3, 0x93D4, 0x93D5, 0x93D6, 0x93D7,
|
|
0x93D8, 0x93D9, 0x93DA, 0x93DB, 0x93DC, 0x93DD
|
|
};
|
|
/* Block sizes paired with the enum tables above. */
|
|
static const int astc_bx[14] = { 4, 5, 5, 6, 6, 8, 8, 8, 10, 10, 10, 10, 12, 12 };
|
|
static const int astc_by[14] = { 4, 4, 5, 5, 6, 5, 6, 8, 5, 6, 8, 10, 10, 12 };
|
|
|
|
typedef struct KTXMuxerContext {
|
|
AVClass *class;
|
|
int srgb; /* -1 = auto (follow the encoder profile) */
|
|
int wrote_image;
|
|
} KTXMuxerContext;
|
|
|
|
static const char ktx_orientation_key[] = "KTXorientation";
|
|
|
|
/* Derive the stored row order from the stream's display matrix. The demuxer
|
|
* converts KTXorientation into exactly such a matrix, so a stream copied from
|
|
* a flipped KTX keeps its orientation instead of being relabelled. Any other
|
|
* transform is rejected rather than silently dropped: KTXorientation only has
|
|
* room for axis flips, and applying a rotation or a scale to the pixels would
|
|
* mean decoding and re-encoding the texture. */
|
|
static int ktx_orientation_from_side_data(AVFormatContext *s, AVStream *st,
|
|
int *hflip, int *vflip)
|
|
{
|
|
const AVPacketSideData *sd;
|
|
const int32_t *matrix;
|
|
|
|
*hflip = *vflip = 0;
|
|
sd = av_packet_side_data_get(st->codecpar->coded_side_data,
|
|
st->codecpar->nb_coded_side_data,
|
|
AV_PKT_DATA_DISPLAYMATRIX);
|
|
if (!sd || sd->size < 9 * sizeof(*matrix))
|
|
return 0;
|
|
|
|
matrix = (const int32_t *)sd->data;
|
|
/* The layout av_display_matrix_flip() produces: the 2x2 part (a, b, c, d)
|
|
* is diagonal with unit scales, negative when the axis is flipped, the
|
|
* translation (x, y) is zero, and the perspective terms (u, v, w) are the
|
|
* identity ones. Note that a, b, c, d, x and y are 16.16 fixed-point while
|
|
* u, v and w are 2.30. */
|
|
if (matrix[1] || matrix[2] || matrix[3] || matrix[5] ||
|
|
matrix[6] || matrix[7] || matrix[8] != 1 << 30 ||
|
|
FFABSU(matrix[0]) != 65536 || FFABSU(matrix[4]) != 65536) {
|
|
av_log(s, AV_LOG_ERROR, "Unsupported display matrix: KTXorientation "
|
|
"only records axis flips, not rotations, scales, translations "
|
|
"or perspective.\n");
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
*hflip = matrix[0] < 0;
|
|
*vflip = matrix[4] < 0;
|
|
return 0;
|
|
}
|
|
|
|
static int ktx_write_header(AVFormatContext *s)
|
|
{
|
|
KTXMuxerContext *ctx = s->priv_data;
|
|
AVStream *st = s->streams[0];
|
|
const uint8_t *ed = st->codecpar->extradata;
|
|
uint8_t bx = 0, by = 0;
|
|
int glfmt = 0, i, srgb, profile_srgb, ret;
|
|
int hflip, vflip, kv_size, kv_padded;
|
|
uint8_t hdr[64];
|
|
char kv[32];
|
|
char orientation[8];
|
|
|
|
if (st->codecpar->extradata_size < ASTC_HEADER_SIZE) {
|
|
av_log(s, AV_LOG_ERROR, ".ktx muxer requires 16-byte extradata "
|
|
"(block size) from the encoder.\n");
|
|
return AVERROR(EINVAL);
|
|
}
|
|
if (st->codecpar->width <= 0 || st->codecpar->height <= 0 ||
|
|
st->codecpar->width > 0xFFFFFF || st->codecpar->height > 0xFFFFFF) {
|
|
av_log(s, AV_LOG_ERROR, "Invalid image dimensions %dx%d.\n",
|
|
st->codecpar->width, st->codecpar->height);
|
|
return AVERROR(EINVAL);
|
|
}
|
|
if (memcmp(ed, astc_magic, sizeof(astc_magic)) ||
|
|
AV_RL24(ed + 7) != st->codecpar->width ||
|
|
AV_RL24(ed + 10) != st->codecpar->height ||
|
|
AV_RL24(ed + 13) != 1) {
|
|
av_log(s, AV_LOG_ERROR, ".ktx muxer received invalid ASTC extradata.\n");
|
|
return AVERROR_INVALIDDATA;
|
|
}
|
|
bx = ed[4];
|
|
by = ed[5];
|
|
|
|
/* ASTC bitstreams do not record whether they hold LDR or HDR endpoints,
|
|
* and the KTX 1.0 GL enums do not express it either: the linear ASTC
|
|
* formats can also carry HDR endpoint encodings. Writing HDR through this
|
|
* muxer is simply not implemented, so reject the profiles that name an HDR
|
|
* endpoint format and point at the .astc container. A stream that only
|
|
* reports a linear colour space is not known to be HDR and is still
|
|
* written as a linear texture. */
|
|
if (st->codecpar->profile == AV_PROFILE_ASTC_HDR_RGB_LDR_A ||
|
|
st->codecpar->profile == AV_PROFILE_ASTC_HDR) {
|
|
av_log(s, AV_LOG_ERROR,
|
|
"Writing HDR ASTC to KTX 1.0 is not supported; use the .astc "
|
|
"container for HDR output.\n");
|
|
return AVERROR(EINVAL);
|
|
}
|
|
/* codecpar->profile publishes the colour space (and, when it is known, the
|
|
* endpoint format) of the stream. Only the profiles that name a colour
|
|
* space determine the GL enum here; a stream without profile information
|
|
* leaves it undetermined. */
|
|
switch (st->codecpar->profile) {
|
|
case AV_PROFILE_ASTC_LDR_SRGB:
|
|
profile_srgb = 1;
|
|
break;
|
|
case AV_PROFILE_ASTC_LDR:
|
|
case AV_PROFILE_ASTC_LINEAR_ANY:
|
|
profile_srgb = 0;
|
|
break;
|
|
default:
|
|
profile_srgb = -1;
|
|
break;
|
|
}
|
|
|
|
/* Auto keeps the color space the encoder was configured with, so that
|
|
* remuxing a linear KTX does not silently flip it to sRGB; a stream
|
|
* without profile information falls back to the sRGB ASTC format. */
|
|
srgb = ctx->srgb >= 0 ? ctx->srgb :
|
|
profile_srgb >= 0 ? profile_srgb : 1;
|
|
|
|
/* An explicit request must not contradict a known encoder profile. */
|
|
if (ctx->srgb >= 0 && profile_srgb >= 0 && profile_srgb != ctx->srgb) {
|
|
av_log(s, AV_LOG_ERROR,
|
|
"KTX srgb option does not match the encoder profile.\n");
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
/* Writing 3D ASTC blocks is not implemented: only the 2D GL internal
|
|
* formats are emitted. */
|
|
if (ed[6] != 1) {
|
|
av_log(s, AV_LOG_ERROR,
|
|
"Writing 3D ASTC blocks to KTX is not supported; use the .astc "
|
|
"container for 3D block output.\n");
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
for (i = 0; i < 14; i++) {
|
|
if (astc_bx[i] == bx && astc_by[i] == by) {
|
|
glfmt = srgb ? astc_gl_srgb[i] : astc_gl_linear[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!glfmt) {
|
|
av_log(s, AV_LOG_ERROR, "Unsupported ASTC block size %dx%d for KTX.\n", bx, by);
|
|
return AVERROR(EINVAL);
|
|
}
|
|
|
|
/* The stored rows are passed through unchanged, so describe them as they
|
|
* are: the encoder writes them top-down, left-to-right, unless the input
|
|
* brings its own orientation from a previous KTX. */
|
|
ret = ktx_orientation_from_side_data(s, st, &hflip, &vflip);
|
|
if (ret < 0)
|
|
return ret;
|
|
memcpy(orientation, "S=r,T=d", sizeof(orientation));
|
|
orientation[2] = hflip ? 'l' : 'r';
|
|
orientation[6] = vflip ? 'u' : 'd';
|
|
|
|
memcpy(kv, ktx_orientation_key, sizeof(ktx_orientation_key));
|
|
memcpy(kv + sizeof(ktx_orientation_key), orientation, sizeof(orientation));
|
|
kv_size = sizeof(ktx_orientation_key) + sizeof(orientation);
|
|
kv_padded = (kv_size + 3) & ~3;
|
|
|
|
memset(hdr, 0, sizeof(hdr));
|
|
/* KTX 1.0 file identifier: 0xAB 'K' 'T' 'X' ' ' '1' '1' 0xBB 0x0D 0x0A 0x1A 0x0A */
|
|
hdr[0] = 0xAB; hdr[1] = 'K'; hdr[2] = 'T'; hdr[3] = 'X';
|
|
hdr[4] = ' '; hdr[5] = '1'; hdr[6] = '1'; hdr[7] = 0xBB;
|
|
hdr[8] = 0x0D; hdr[9] = 0x0A; hdr[10] = 0x1A; hdr[11] = 0x0A;
|
|
AV_WL32(hdr + 12, 0x04030201); /* endianness */
|
|
/* gl_type(16)=0, gl_type_size(20)=1, gl_format(24)=0 */
|
|
AV_WL32(hdr + 20, 1);
|
|
AV_WL32(hdr + 28, glfmt); /* gl_internal_format */
|
|
AV_WL32(hdr + 32, 0x1908); /* gl_base_internal_format = GL_RGBA */
|
|
AV_WL32(hdr + 36, st->codecpar->width); /* pixel_width */
|
|
AV_WL32(hdr + 40, st->codecpar->height); /* pixel_height */
|
|
/* pixel_depth(44)=0, array(48)=0 */
|
|
AV_WL32(hdr + 52, 1); /* number_of_faces */
|
|
AV_WL32(hdr + 56, 1); /* number_of_mipmap_levels */
|
|
AV_WL32(hdr + 60, 4 + kv_padded); /* bytes_of_key_value_data */
|
|
|
|
avio_write(s->pb, hdr, sizeof(hdr));
|
|
|
|
/* Self-describe the row convention so readers do not have to guess
|
|
* whether the texture is stored top-down or bottom-up. */
|
|
avio_wl32(s->pb, kv_size);
|
|
avio_write(s->pb, kv, kv_size);
|
|
for (i = kv_size; i % 4; i++)
|
|
avio_w8(s->pb, 0);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ktx_write_packet(AVFormatContext *s, AVPacket *pkt)
|
|
{
|
|
KTXMuxerContext *ctx = s->priv_data;
|
|
|
|
/* The container holds a single image; FF_OFMT_FLAG_MAX_ONE_OF_EACH only
|
|
* limits the number of streams, so packets must be rejected here. */
|
|
if (ctx->wrote_image) {
|
|
av_log(s, AV_LOG_ERROR,
|
|
".ktx muxer supports a single image per file.\n");
|
|
return AVERROR(EINVAL);
|
|
}
|
|
ctx->wrote_image = 1;
|
|
|
|
avio_wl32(s->pb, pkt->size); /* imageSize */
|
|
avio_write(s->pb, pkt->data, pkt->size);
|
|
return 0;
|
|
}
|
|
|
|
#define OFFSET(x) offsetof(KTXMuxerContext, x)
|
|
#define VE AV_OPT_FLAG_ENCODING_PARAM
|
|
static const AVOption ktx_options[] = {
|
|
{ "srgb", "GL internal format color space", OFFSET(srgb),
|
|
AV_OPT_TYPE_INT, { .i64 = -1 }, -1, 1, VE, .unit = "srgb" },
|
|
{ "auto", "Follow the encoder profile", 0, AV_OPT_TYPE_CONST, { .i64 = -1 }, 0, 0, VE, .unit = "srgb" },
|
|
{ "linear", "Write the linear ASTC format", 0, AV_OPT_TYPE_CONST, { .i64 = 0 }, 0, 0, VE, .unit = "srgb" },
|
|
{ "srgb", "Write the sRGB ASTC format", 0, AV_OPT_TYPE_CONST, { .i64 = 1 }, 0, 0, VE, .unit = "srgb" },
|
|
{ NULL },
|
|
};
|
|
|
|
static const AVClass ktx_muxer_class = {
|
|
.class_name = "ktx_muxer",
|
|
.item_name = av_default_item_name,
|
|
.option = ktx_options,
|
|
.version = LIBAVUTIL_VERSION_INT,
|
|
};
|
|
|
|
const FFOutputFormat ff_ktx_muxer = {
|
|
.p.name = "ktx",
|
|
.p.long_name = NULL_IF_CONFIG_SMALL("KTX 1.0 (Khronos Texture) for ASTC"),
|
|
.p.mime_type = "image/ktx",
|
|
.p.extensions = "ktx",
|
|
.p.audio_codec = AV_CODEC_ID_NONE,
|
|
.p.video_codec = AV_CODEC_ID_ASTC,
|
|
.p.subtitle_codec = AV_CODEC_ID_NONE,
|
|
.flags_internal = FF_OFMT_FLAG_MAX_ONE_OF_EACH |
|
|
FF_OFMT_FLAG_ONLY_DEFAULT_CODECS,
|
|
.priv_data_size = sizeof(KTXMuxerContext),
|
|
.p.priv_class = &ktx_muxer_class,
|
|
.write_header = ktx_write_header,
|
|
.write_packet = ktx_write_packet,
|
|
.p.flags = AVFMT_NOTIMESTAMPS,
|
|
};
|