| @@ -0,0 +1,361 @@ |
| | 1 | /* |
| | 2 | * Resolume DXV encoder |
| | 3 | * Copyright (C) 2024 Connor Worley <connorbworley@gmail.com> |
| | 4 | * |
| | 5 | * This file is part of FFmpeg. |
| | 6 | * |
| | 7 | * FFmpeg is free software; you can redistribute it and/or |
| | 8 | * modify it under the terms of the GNU Lesser General Public |
| | 9 | * License as published by the Free Software Foundation; either |
| | 10 | * version 2.1 of the License, or (at your option) any later version. |
| | 11 | * |
| | 12 | * FFmpeg is distributed in the hope that it will be useful, |
| | 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| | 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| | 15 | * Lesser General Public License for more details. |
| | 16 | * |
| | 17 | * You should have received a copy of the GNU Lesser General Public |
| | 18 | * License along with FFmpeg; if not, write to the Free Software |
| | 19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| | 20 | */ |
| | 21 | |
| | 22 | #include <stdint.h> |
| | 23 | |
| | 24 | #include "libavutil/crc.h" |
| | 25 | #include "libavutil/imgutils.h" |
| | 26 | #include "libavutil/opt.h" |
| | 27 | |
| | 28 | #include "bytestream.h" |
| | 29 | #include "codec_internal.h" |
| | 30 | #include "encode.h" |
| | 31 | #include "texturedsp.h" |
| | 32 | |
| | 33 | #define DXV_HEADER_LENGTH 12 |
| | 34 | |
| | 35 | /* |
| | 36 | * DXV uses LZ-like back-references to avoid copying words that have already |
| | 37 | * appeared in the decompressed stream. Using a simple hash table (HT) |
| | 38 | * significantly speeds up the lookback process while encoding. |
| | 39 | */ |
| | 40 | #define LOOKBACK_HT_ELEMS 0x40000 |
| | 41 | #define LOOKBACK_WORDS 0x20202 |
| | 42 | |
| | 43 | enum DXVTextureFormat { |
| | 44 | DXV_FMT_DXT1 = MKBETAG('D', 'X', 'T', '1'), |
| | 45 | }; |
| | 46 | |
| | 47 | typedef struct HTEntry { |
| | 48 | uint32_t key; |
| | 49 | uint32_t pos; |
| | 50 | } HTEntry; |
| | 51 | |
| | 52 | static void ht_init(HTEntry *ht) |
| | 53 | { |
| | 54 | for (size_t i = 0; i < LOOKBACK_HT_ELEMS; i++) { |
| | 55 | ht[i].pos = -1; |
| | 56 | } |
| | 57 | } |
| | 58 | |
| | 59 | static uint32_t ht_lookup_and_upsert(HTEntry *ht, AVCRC *hash_ctx, |
| | 60 | uint32_t key, uint32_t pos) |
| | 61 | { |
| | 62 | uint32_t ret = -1; |
| | 63 | size_t hash = av_crc(hash_ctx, 0, (uint8_t*)&key, 4) % LOOKBACK_HT_ELEMS; |
| | 64 | for (size_t i = hash; i < hash + LOOKBACK_HT_ELEMS; i++) { |
| | 65 | size_t wrapped_index = i % LOOKBACK_HT_ELEMS; |
| | 66 | HTEntry *entry = &ht[wrapped_index]; |
| | 67 | if (entry->key == key || entry->pos == -1) { |
| | 68 | ret = entry->pos; |
| | 69 | entry->key = key; |
| | 70 | entry->pos = pos; |
| | 71 | break; |
| | 72 | } |
| | 73 | } |
| | 74 | return ret; |
| | 75 | } |
| | 76 | |
| | 77 | static void ht_delete(HTEntry *ht, AVCRC *hash_ctx, |
| | 78 | uint32_t key, uint32_t pos) |
| | 79 | { |
| | 80 | HTEntry *removed_entry = NULL; |
| | 81 | size_t removed_hash; |
| | 82 | size_t hash = av_crc(hash_ctx, 0, (uint8_t*)&key, 4) % LOOKBACK_HT_ELEMS; |
| | 83 | |
| | 84 | for (size_t i = hash; i < hash + LOOKBACK_HT_ELEMS; i++) { |
| | 85 | size_t wrapped_index = i % LOOKBACK_HT_ELEMS; |
| | 86 | HTEntry *entry = &ht[wrapped_index]; |
| | 87 | if (entry->pos == -1) |
| | 88 | return; |
| | 89 | if (removed_entry) { |
| | 90 | size_t candidate_hash = av_crc(hash_ctx, 0, (uint8_t*)&entry->key, 4) % LOOKBACK_HT_ELEMS; |
| | 91 | if ((wrapped_index > removed_hash && (candidate_hash <= removed_hash || candidate_hash > wrapped_index)) || |
| | 92 | (wrapped_index < removed_hash && (candidate_hash <= removed_hash && candidate_hash > wrapped_index))) { |
| | 93 | *removed_entry = *entry; |
| | 94 | entry->pos = -1; |
| | 95 | removed_entry = entry; |
| | 96 | removed_hash = wrapped_index; |
| | 97 | } |
| | 98 | } else if (entry->key == key) { |
| | 99 | if (entry->pos <= pos) { |
| | 100 | entry->pos = -1; |
| | 101 | removed_entry = entry; |
| | 102 | removed_hash = wrapped_index; |
| | 103 | } else { |
| | 104 | return; |
| | 105 | } |
| | 106 | } |
| | 107 | } |
| | 108 | } |
| | 109 | |
| | 110 | typedef struct DXVEncContext { |
| | 111 | AVClass *class; |
| | 112 | |
| | 113 | TextureDSPContext texdsp; |
| | 114 | PutByteContext pbc; |
| | 115 | |
| | 116 | uint8_t *tex_data; // Compressed texture |
| | 117 | int64_t tex_size; // Texture size |
| | 118 | |
| | 119 | /* Optimal number of slices for parallel decoding */ |
| | 120 | int slice_count; |
| | 121 | |
| | 122 | TextureDSPThreadContext enc; |
| | 123 | |
| | 124 | enum DXVTextureFormat tex_fmt; |
| | 125 | int (*compress_tex)(AVCodecContext *avctx); |
| | 126 | |
| | 127 | AVCRC *crc_ctx; |
| | 128 | |
| | 129 | HTEntry color_lookback_ht[LOOKBACK_HT_ELEMS]; |
| | 130 | HTEntry lut_lookback_ht[LOOKBACK_HT_ELEMS]; |
| | 131 | } DXVEncContext; |
| | 132 | |
| | 133 | static int compress_texture_thread(AVCodecContext *avctx, void *arg, |
| | 134 | int slice, int thread_nb) |
| | 135 | { |
| | 136 | DXVEncContext *ctx = avctx->priv_data; |
| | 137 | AVFrame *frame = arg; |
| | 138 | |
| | 139 | if (ctx->enc.tex_funct) { |
| | 140 | ctx->enc.tex_data.out = ctx->tex_data; |
| | 141 | ctx->enc.frame_data.in = frame->data[0]; |
| | 142 | ctx->enc.stride = frame->linesize[0]; |
| | 143 | return ff_texturedsp_compress_thread(avctx, &ctx->enc, slice, thread_nb); |
| | 144 | } else { |
| | 145 | /* unimplemented: YCoCg formats */ |
| | 146 | return AVERROR_INVALIDDATA; |
| | 147 | } |
| | 148 | |
| | 149 | return 0; |
| | 150 | } |
| | 151 | |
| | 152 | /* Converts an index offset value to a 2-bit opcode and pushes it to a stream. |
| | 153 | * Inverse of CHECKPOINT in dxv.c. */ |
| | 154 | #define PUSH_OP(x) \ |
| | 155 | do { \ |
| | 156 | if (state == 16) { \ |
| | 157 | if (bytestream2_get_bytes_left_p(pbc) < 4) { \ |
| | 158 | return AVERROR_INVALIDDATA; \ |
| | 159 | } \ |
| | 160 | value = (uint32_t*)pbc->buffer; \ |
| | 161 | bytestream2_put_le32(pbc, 0); \ |
| | 162 | state = 0; \ |
| | 163 | } \ |
| | 164 | if (idx >= 0x102 * x) { \ |
| | 165 | op = 3; \ |
| | 166 | bytestream2_put_le16(pbc, (idx / x) - 0x102); \ |
| | 167 | } else if (idx >= 2 * x) { \ |
| | 168 | op = 2; \ |
| | 169 | bytestream2_put_byte(pbc, (idx / x) - 2); \ |
| | 170 | } else if (idx == x) { \ |
| | 171 | op = 1; \ |
| | 172 | } else { \ |
| | 173 | op = 0; \ |
| | 174 | } \ |
| | 175 | *value |= (op << (state * 2)); \ |
| | 176 | state++; \ |
| | 177 | } while (0) |
| | 178 | |
| | 179 | static int dxv_compress_dxt1(AVCodecContext *avctx) |
| | 180 | { |
| | 181 | DXVEncContext *ctx = avctx->priv_data; |
| | 182 | PutByteContext *pbc = &ctx->pbc; |
| | 183 | uint32_t *value; |
| | 184 | uint32_t color, lut, idx, color_idx, lut_idx, prev_pos, state = 16, pos = 2, op = 0; |
| | 185 | |
| | 186 | ht_init(ctx->color_lookback_ht); |
| | 187 | ht_init(ctx->lut_lookback_ht); |
| | 188 | |
| | 189 | bytestream2_put_le32(pbc, AV_RL32(ctx->tex_data)); |
| | 190 | bytestream2_put_le32(pbc, AV_RL32(ctx->tex_data + 4)); |
| | 191 | |
| | 192 | ht_lookup_and_upsert(ctx->color_lookback_ht, ctx->crc_ctx, AV_RL32(ctx->tex_data), 0); |
| | 193 | ht_lookup_and_upsert(ctx->lut_lookback_ht, ctx->crc_ctx, AV_RL32(ctx->tex_data + 4), 1); |
| | 194 | |
| | 195 | while (pos + 2 <= ctx->tex_size / 4) { |
| | 196 | idx = 0; |
| | 197 | |
| | 198 | color = AV_RL32(ctx->tex_data + pos * 4); |
| | 199 | prev_pos = ht_lookup_and_upsert(ctx->color_lookback_ht, ctx->crc_ctx, color, pos); |
| | 200 | color_idx = prev_pos != -1 ? pos - prev_pos : 0; |
| | 201 | if (pos >= LOOKBACK_WORDS) { |
| | 202 | uint32_t old_pos = pos - LOOKBACK_WORDS; |
| | 203 | uint32_t old_color = AV_RL32(ctx->tex_data + old_pos * 4); |
| | 204 | ht_delete(ctx->color_lookback_ht, ctx->crc_ctx, old_color, old_pos); |
| | 205 | } |
| | 206 | pos++; |
| | 207 | |
| | 208 | lut = AV_RL32(ctx->tex_data + pos * 4); |
| | 209 | if (color_idx && lut == AV_RL32(ctx->tex_data + (pos - color_idx) * 4)) { |
| | 210 | idx = color_idx; |
| | 211 | } else { |
| | 212 | idx = 0; |
| | 213 | prev_pos = ht_lookup_and_upsert(ctx->lut_lookback_ht, ctx->crc_ctx, lut, pos); |
| | 214 | lut_idx = prev_pos != -1 ? pos - prev_pos : 0; |
| | 215 | } |
| | 216 | if (pos >= LOOKBACK_WORDS) { |
| | 217 | uint32_t old_pos = pos - LOOKBACK_WORDS; |
| | 218 | uint32_t old_lut = AV_RL32(ctx->tex_data + old_pos * 4); |
| | 219 | ht_delete(ctx->lut_lookback_ht, ctx->crc_ctx, old_lut, old_pos); |
| | 220 | } |
| | 221 | pos++; |
| | 222 | |
| | 223 | PUSH_OP(2); |
| | 224 | |
| | 225 | if (!idx) { |
| | 226 | idx = color_idx; |
| | 227 | PUSH_OP(2); |
| | 228 | if (!idx) |
| | 229 | bytestream2_put_le32(pbc, color); |
| | 230 | |
| | 231 | idx = lut_idx; |
| | 232 | PUSH_OP(2); |
| | 233 | if (!idx) |
| | 234 | bytestream2_put_le32(pbc, lut); |
| | 235 | } |
| | 236 | } |
| | 237 | |
| | 238 | return 0; |
| | 239 | } |
| | 240 | |
| | 241 | static int dxv_encode(AVCodecContext *avctx, AVPacket *pkt, |
| | 242 | const AVFrame *frame, int *got_packet) |
| | 243 | { |
| | 244 | DXVEncContext *ctx = avctx->priv_data; |
| | 245 | PutByteContext *pbc = &ctx->pbc; |
| | 246 | int ret; |
| | 247 | |
| | 248 | /* unimplemented: needs to depend on compression ratio of tex format */ |
| | 249 | /* under DXT1, we need 3 words to encode load ops for 32 words. |
| | 250 | * the first 2 words of the texture do not need load ops. */ |
| | 251 | ret = ff_alloc_packet(avctx, pkt, DXV_HEADER_LENGTH + ctx->tex_size + AV_CEIL_RSHIFT(ctx->tex_size - 8, 7) * 12); |
| | 252 | if (ret < 0) |
| | 253 | return ret; |
| | 254 | |
| | 255 | avctx->execute2(avctx, compress_texture_thread, (void*)frame, NULL, ctx->enc.slice_count); |
| | 256 | |
| | 257 | bytestream2_init_writer(pbc, pkt->data, pkt->size); |
| | 258 | |
| | 259 | bytestream2_put_le32(pbc, ctx->tex_fmt); |
| | 260 | bytestream2_put_byte(pbc, 4); |
| | 261 | bytestream2_put_byte(pbc, 0); |
| | 262 | bytestream2_put_byte(pbc, 0); |
| | 263 | bytestream2_put_byte(pbc, 0); |
| | 264 | /* Fill in compressed size later */ |
| | 265 | bytestream2_skip_p(pbc, 4); |
| | 266 | |
| | 267 | ret = ctx->compress_tex(avctx); |
| | 268 | if (ret < 0) |
| | 269 | return ret; |
| | 270 | |
| | 271 | AV_WL32(pkt->data + 8, bytestream2_tell_p(pbc) - DXV_HEADER_LENGTH); |
| | 272 | av_shrink_packet(pkt, bytestream2_tell_p(pbc)); |
| | 273 | |
| | 274 | *got_packet = 1; |
| | 275 | return 0; |
| | 276 | } |
| | 277 | |
| | 278 | static av_cold int dxv_init(AVCodecContext *avctx) |
| | 279 | { |
| | 280 | DXVEncContext *ctx = avctx->priv_data; |
| | 281 | int ret = av_image_check_size(avctx->width, avctx->height, 0, avctx); |
| | 282 | |
| | 283 | if (ret < 0) { |
| | 284 | av_log(avctx, AV_LOG_ERROR, "Invalid image size %dx%d.\n", |
| | 285 | avctx->width, avctx->height); |
| | 286 | return ret; |
| | 287 | } |
| | 288 | |
| | 289 | ff_texturedspenc_init(&ctx->texdsp); |
| | 290 | |
| | 291 | switch (ctx->tex_fmt) { |
| | 292 | case DXV_FMT_DXT1: |
| | 293 | ctx->compress_tex = dxv_compress_dxt1; |
| | 294 | ctx->enc.tex_funct = ctx->texdsp.dxt1_block; |
| | 295 | ctx->enc.tex_ratio = 8; |
| | 296 | break; |
| | 297 | default: |
| | 298 | av_log(avctx, AV_LOG_ERROR, "Invalid format %08X\n", ctx->tex_fmt); |
| | 299 | return AVERROR_INVALIDDATA; |
| | 300 | } |
| | 301 | ctx->enc.raw_ratio = 16; |
| | 302 | ctx->tex_size = FFALIGN(avctx->width, 16) / TEXTURE_BLOCK_W * |
| | 303 | FFALIGN(avctx->height, 16) / TEXTURE_BLOCK_H * |
| | 304 | ctx->enc.tex_ratio; |
| | 305 | ctx->enc.slice_count = av_clip(avctx->thread_count, 1, FFALIGN(avctx->height, 16) / TEXTURE_BLOCK_H); |
| | 306 | |
| | 307 | ctx->tex_data = av_malloc(ctx->tex_size); |
| | 308 | if (!ctx->tex_data) { |
| | 309 | return AVERROR(ENOMEM); |
| | 310 | } |
| | 311 | |
| | 312 | ctx->crc_ctx = (AVCRC*)av_crc_get_table(AV_CRC_32_IEEE); |
| | 313 | if (!ctx->crc_ctx) { |
| | 314 | av_log(avctx, AV_LOG_ERROR, "Could not initialize CRC table.\n"); |
| | 315 | return AVERROR_BUG; |
| | 316 | } |
| | 317 | |
| | 318 | return 0; |
| | 319 | } |
| | 320 | |
| | 321 | static av_cold int dxv_close(AVCodecContext *avctx) |
| | 322 | { |
| | 323 | DXVEncContext *ctx = avctx->priv_data; |
| | 324 | |
| | 325 | av_freep(&ctx->tex_data); |
| | 326 | |
| | 327 | return 0; |
| | 328 | } |
| | 329 | |
| | 330 | #define OFFSET(x) offsetof(DXVEncContext, x) |
| | 331 | #define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM |
| | 332 | static const AVOption options[] = { |
| | 333 | { "format", NULL, OFFSET(tex_fmt), AV_OPT_TYPE_INT, { .i64 = DXV_FMT_DXT1 }, DXV_FMT_DXT1, DXV_FMT_DXT1, FLAGS, "format" }, |
| | 334 | { "dxt1", "DXT1 (Normal Quality, No Alpha)", 0, AV_OPT_TYPE_CONST, { .i64 = DXV_FMT_DXT1 }, 0, 0, FLAGS, "format" }, |
| | 335 | { NULL }, |
| | 336 | }; |
| | 337 | |
| | 338 | static const AVClass dxvenc_class = { |
| | 339 | .class_name = "DXV encoder", |
| | 340 | .option = options, |
| | 341 | .version = LIBAVUTIL_VERSION_INT, |
| | 342 | }; |
| | 343 | |
| | 344 | const FFCodec ff_dxv_encoder = { |
| | 345 | .p.name = "dxv", |
| | 346 | CODEC_LONG_NAME("Resolume DXV"), |
| | 347 | .p.type = AVMEDIA_TYPE_VIDEO, |
| | 348 | .p.id = AV_CODEC_ID_DXV, |
| | 349 | .init = dxv_init, |
| | 350 | FF_CODEC_ENCODE_CB(dxv_encode), |
| | 351 | .close = dxv_close, |
| | 352 | .priv_data_size = sizeof(DXVEncContext), |
| | 353 | .p.capabilities = AV_CODEC_CAP_DR1 | |
| | 354 | AV_CODEC_CAP_SLICE_THREADS | |
| | 355 | AV_CODEC_CAP_FRAME_THREADS, |
| | 356 | .p.priv_class = &dxvenc_class, |
| | 357 | .p.pix_fmts = (const enum AVPixelFormat[]) { |
| | 358 | AV_PIX_FMT_RGBA, AV_PIX_FMT_NONE, |
| | 359 | }, |
| | 360 | .caps_internal = FF_CODEC_CAP_INIT_CLEANUP, |
| | 361 | }; |