[FFmpeg-devel] [PATCH] avcodec/vqavideo: Decode 15-bit VQA3 files

Pekka Väänänen pekka.vaananen at iki.fi
Mon Sep 20 22:13:05 EEST 2021


Adds support for 15-bit VQA3 videos used in Westwood Studios' games.

Signed-off-by: Pekka Väänänen <pekka.vaananen at iki.fi>
---
 libavcodec/vqavideo.c      | 279 +++++++++++++++++++++++++++++++++----
 libavformat/westwood_vqa.c |  12 +-
 2 files changed, 262 insertions(+), 29 deletions(-)

diff --git a/libavcodec/vqavideo.c b/libavcodec/vqavideo.c
index 12698dc2e8..e9db6b80a5 100644
--- a/libavcodec/vqavideo.c
+++ b/libavcodec/vqavideo.c
@@ -1,6 +1,7 @@
 /*
  * Westwood Studios VQA Video Decoder
- * Copyright (C) 2003 The FFmpeg project
+ * Copyright (c) 2003 Mike Melanson <melanson at pcisys.net>
+ * Copyright (c) 2021 Pekka Väänänen <pekka.vaananen at iki.fi>
  *
  * This file is part of FFmpeg.
  *
@@ -61,6 +62,11 @@
  * together and the 8-bit pieces together. If most of the vectors are
  * clustered into one group of 256 vectors, most of the 4-bit index pieces
  * should be the same.
+ *
+ * VQA3 introduces a 15-bit high color codebook, delta coding, replaces
+ * the above "split byte" scheme with RLE compression, and extends the
+ * "format80" compression with relative references. In VQA3 the whole
+ * codebook is always updated as a whole without splitting it into pieces.
  */
 
 #include <stdio.h>
@@ -81,7 +87,7 @@
 #define MAX_CODEBOOK_VECTORS 0xFF00
 #define SOLID_PIXEL_VECTORS 0x100
 #define MAX_VECTORS (MAX_CODEBOOK_VECTORS + SOLID_PIXEL_VECTORS)
-#define MAX_CODEBOOK_SIZE (MAX_VECTORS * 4 * 4)
+#define MAX_CODEBOOK_SIZE (MAX_VECTORS * 4 * 4 * sizeof(uint16_t))
 
 #define CBF0_TAG MKBETAG('C', 'B', 'F', '0')
 #define CBFZ_TAG MKBETAG('C', 'B', 'F', 'Z')
@@ -90,6 +96,8 @@
 #define CPL0_TAG MKBETAG('C', 'P', 'L', '0')
 #define CPLZ_TAG MKBETAG('C', 'P', 'L', 'Z')
 #define VPTZ_TAG MKBETAG('V', 'P', 'T', 'Z')
+#define VPTR_TAG MKBETAG('V', 'P', 'T', 'R')
+#define VPRZ_TAG MKBETAG('V', 'P', 'R', 'Z')
 
 typedef struct VqaContext {
 
@@ -104,9 +112,12 @@ typedef struct VqaContext {
     int vector_height;  /* height of individual vector */
     int vqa_version;  /* this should be either 1, 2 or 3 */
 
-    unsigned char *codebook;         /* the current codebook */
+    uint16_t *framebuffer; /* current frame's pixels for 15-bit videos */
+    int framebuffer_size;
+
+    unsigned char *codebook; /* the current codebook */
     int codebook_size;
-    unsigned char *next_codebook_buffer;  /* accumulator for next codebook */
+    unsigned char *next_codebook_buffer; /* accumulator for next codebook */
     int next_codebook_buffer_index;
 
     unsigned char *decode_buffer;
@@ -115,16 +126,15 @@ typedef struct VqaContext {
     /* number of frames to go before replacing codebook */
     int partial_countdown;
     int partial_count;
-
 } VqaContext;
 
 static av_cold int vqa_decode_init(AVCodecContext *avctx)
 {
     VqaContext *s = avctx->priv_data;
     int i, j, codebook_index, ret;
+    int colors;
 
     s->avctx = avctx;
-    avctx->pix_fmt = AV_PIX_FMT_PAL8;
 
     /* make sure the extradata made it */
     if (s->avctx->extradata_size != VQA_HEADER_SIZE) {
@@ -134,17 +144,12 @@ static av_cold int vqa_decode_init(AVCodecContext *avctx)
 
     /* load up the VQA parameters from the header */
     s->vqa_version = s->avctx->extradata[0];
-    switch (s->vqa_version) {
-    case 1:
-    case 2:
-        break;
-    case 3:
-        avpriv_report_missing_feature(avctx, "VQA Version %d", s->vqa_version);
-        return AVERROR_PATCHWELCOME;
-    default:
+
+    if (s->vqa_version < 1 || s->vqa_version > 3) {
         avpriv_request_sample(avctx, "VQA Version %i", s->vqa_version);
-        return AVERROR_PATCHWELCOME;
+        return AVERROR_INVALIDDATA;
     }
+
     s->width = AV_RL16(&s->avctx->extradata[6]);
     s->height = AV_RL16(&s->avctx->extradata[8]);
     if ((ret = ff_set_dimensions(avctx, s->width, s->height)) < 0) {
@@ -155,6 +160,14 @@ static av_cold int vqa_decode_init(AVCodecContext *avctx)
     s->vector_height = s->avctx->extradata[11];
     s->partial_count = s->partial_countdown = s->avctx->extradata[13];
 
+    colors = (s->avctx->extradata[14] << 8) | s->avctx->extradata[15];
+
+    if (colors > 0) {
+        avctx->pix_fmt = AV_PIX_FMT_PAL8;
+    } else {
+        avctx->pix_fmt = AV_PIX_FMT_RGB555LE;
+    }
+
     /* the vector dimensions have to meet very stringent requirements */
     if ((s->vector_width != 4) ||
         ((s->vector_height != 2) && (s->vector_height != 4))) {
@@ -167,6 +180,12 @@ static av_cold int vqa_decode_init(AVCodecContext *avctx)
         return AVERROR_INVALIDDATA;
     }
 
+    /* allocate the framebuffer */
+    s->framebuffer_size = s->width * s->height * sizeof(s->framebuffer[0]);
+    s->framebuffer = av_mallocz(s->framebuffer_size);
+    if (!s->framebuffer)
+        return AVERROR(ENOMEM);
+
     /* allocate codebooks */
     s->codebook_size = MAX_CODEBOOK_SIZE;
     s->codebook = av_malloc(s->codebook_size);
@@ -225,6 +244,7 @@ static int decode_format80(VqaContext *s, int src_size,
     int src_pos;
     unsigned char color;
     int i;
+    int relative = 0;
 
     if (src_size < 0 || src_size > bytestream2_get_bytes_left(&s->gb)) {
         av_log(s->avctx, AV_LOG_ERROR, "Chunk size %d is out of range\n",
@@ -232,6 +252,13 @@ static int decode_format80(VqaContext *s, int src_size,
         return AVERROR_INVALIDDATA;
     }
 
+    /* the "new" scheme makes references relative to destination pointer */
+    if (bytestream2_peek_byte(&s->gb) == 0x00) {
+        relative = 1;
+        bytestream2_get_byte(&s->gb);
+        ff_tlog(s->avctx, "found new format stream ");
+    }
+
     start = bytestream2_tell(&s->gb);
     while (bytestream2_tell(&s->gb) - start < src_size) {
         opcode = bytestream2_get_byte(&s->gb);
@@ -251,7 +278,9 @@ static int decode_format80(VqaContext *s, int src_size,
 
             count   = bytestream2_get_le16(&s->gb);
             src_pos = bytestream2_get_le16(&s->gb);
-            ff_tlog(s->avctx, "(1) copy %X bytes from absolute pos %X\n", count, src_pos);
+            if (relative)
+                src_pos = dest_index - src_pos;
+            ff_tlog(s->avctx, "(1) copy %X bytes from pos %X\n", count, src_pos);
             CHECK_COUNT();
             CHECK_COPY(src_pos);
             for (i = 0; i < count; i++)
@@ -271,7 +300,9 @@ static int decode_format80(VqaContext *s, int src_size,
 
             count = (opcode & 0x3F) + 3;
             src_pos = bytestream2_get_le16(&s->gb);
-            ff_tlog(s->avctx, "(3) copy %X bytes from absolute pos %X\n", count, src_pos);
+            if (relative)
+                src_pos = dest_index - src_pos;
+            ff_tlog(s->avctx, "(3) copy %X bytes from pos %X\n", count, src_pos);
             CHECK_COUNT();
             CHECK_COPY(src_pos);
             for (i = 0; i < count; i++)
@@ -313,7 +344,7 @@ static int decode_format80(VqaContext *s, int src_size,
     return 0; // let's display what we decoded anyway
 }
 
-static int vqa_decode_chunk(VqaContext *s, AVFrame *frame)
+static int vqa_decode_frame_pal8(VqaContext *s, AVFrame *frame)
 {
     unsigned int chunk_type;
     unsigned int chunk_size;
@@ -512,9 +543,8 @@ static int vqa_decode_chunk(VqaContext *s, AVFrame *frame)
                 break;
 
             case 3:
-/* not implemented yet */
-                lines = 0;
-                break;
+                av_log(s->avctx, AV_LOG_ERROR, "VQA3 shouldn't have a color palette");
+                return AVERROR_INVALIDDATA;
             }
 
             while (lines--) {
@@ -596,6 +626,180 @@ static int vqa_decode_chunk(VqaContext *s, AVFrame *frame)
     return 0;
 }
 
+static int vqa_decode_frame_hicolor(VqaContext *s, AVFrame *frame)
+{
+    unsigned int chunk_type;
+    unsigned int chunk_size;
+    unsigned int index = 0;
+    int res;
+
+    int cbf0_chunk = -1;
+    int cbfz_chunk = -1;
+    int vptr_chunk = -1;
+    int vprz_chunk = -1;
+
+    unsigned char *stream;
+
+    while (bytestream2_get_bytes_left(&s->gb) >= 8) {
+        chunk_type = bytestream2_get_be32u(&s->gb);
+        index      = bytestream2_tell(&s->gb);
+        chunk_size = bytestream2_get_be32u(&s->gb);
+
+        switch (chunk_type) {
+
+        case CBF0_TAG:
+            cbf0_chunk = index;
+            break;
+
+        case CBFZ_TAG:
+            cbfz_chunk = index;
+            break;
+
+        case VPTR_TAG:
+            vptr_chunk = index;
+            break;
+
+        case VPRZ_TAG:
+            vprz_chunk = index;
+            break;
+
+        default:
+            av_log(s->avctx, AV_LOG_ERROR, "Found unknown chunk type: %s (%08X)\n",
+                   av_fourcc2str(av_bswap32(chunk_type)), chunk_type);
+            break;
+        }
+
+        bytestream2_skip(&s->gb, chunk_size + (chunk_size & 0x01));
+    }
+
+    /* next, look for a full codebook */
+    if ((cbf0_chunk != -1) && (cbfz_chunk != -1)) {
+        /* a chunk should not have both chunk types */
+        av_log(s->avctx, AV_LOG_ERROR, "problem: found both CBF0 and CBFZ chunks\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* decompress the full codebook chunk */
+    if (cbfz_chunk != -1) {
+        bytestream2_seek(&s->gb, cbfz_chunk, SEEK_SET);
+        chunk_size = bytestream2_get_be32(&s->gb);
+        if ((res = decode_format80(s, chunk_size, s->codebook,
+                                   s->codebook_size, 0)) < 0)
+            return res;
+    }
+
+    /* copy a full codebook */
+    if (cbf0_chunk != -1) {
+        bytestream2_seek(&s->gb, cbf0_chunk, SEEK_SET);
+        chunk_size = bytestream2_get_be32(&s->gb);
+        /* sanity check the full codebook size */
+        if (chunk_size > MAX_CODEBOOK_SIZE) {
+            av_log(s->avctx, AV_LOG_ERROR, "problem: CBF0 chunk too large (0x%X bytes)\n",
+                chunk_size);
+            return AVERROR_INVALIDDATA;
+        }
+
+        bytestream2_get_buffer(&s->gb, s->codebook, chunk_size);
+    }
+
+    if (vprz_chunk == -1 && vptr_chunk == -1) {
+        av_log(s->avctx, AV_LOG_ERROR, "frame has no block data\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* decode the frame */
+
+    if (vptr_chunk != -1) {
+        /* copy uncompressed tile data */
+        bytestream2_seek(&s->gb, vptr_chunk, SEEK_SET);
+        chunk_size = bytestream2_get_be32(&s->gb);
+        if (chunk_size > s->decode_buffer_size) {
+            av_log(s->avctx, AV_LOG_ERROR, "VPTR chunk didn't fit in decode buffer");
+            return AVERROR_INVALIDDATA;
+        }
+        bytestream2_get_buffer(&s->gb, s->decode_buffer, chunk_size);
+    } else if (vprz_chunk != -1) {
+        /* decompress the tile data */
+        bytestream2_seek(&s->gb, vprz_chunk, SEEK_SET);
+
+        chunk_size = bytestream2_get_be32(&s->gb);
+        if ((res = decode_format80(s, chunk_size, s->decode_buffer, s->decode_buffer_size, 0)) < 0)
+            return res;
+    } else {
+        av_log(s->avctx, AV_LOG_ERROR, "expected either VPTR or VPRZ chunk\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* now uncompress the per-row RLE of the decode buffer and draw the blocks in framebuffer */
+
+    stream = (unsigned char*)s->decode_buffer;
+
+    for (int y_pos = 0; y_pos < s->height; y_pos += s->vector_height) {
+        int x_pos = 0;
+
+        while (x_pos < s->width) {
+            int vector_index = 0;
+            int count = 0;
+            uint16_t code = *(uint16_t*)stream;
+            int type;
+
+            stream += 2;
+            type = code >> 13;
+            code &= 0x1fff;
+
+            if (type == 0) {
+                x_pos += 4 * code;
+                continue;
+            } else if (type < 3) {
+                vector_index = code & 0xff;
+                count = ((code & 0x1f00) >> 7) + 1 + type;
+            } else if (type < 5) {
+                vector_index = code;
+                count = 1;
+            } else if (type < 7) {
+                vector_index = code;
+                count = *stream++;
+            } else {
+                av_log(s->avctx, AV_LOG_ERROR, " unknown type in VPTR chunk (%d)\n",type);
+                return AVERROR_INVALIDDATA;
+            }
+
+            if (count < 0 || count > (s->width - x_pos) / s->vector_width) {
+                av_log(s->avctx, AV_LOG_ERROR, "invalid count: %d\n", count);
+                return AVERROR_INVALIDDATA;
+            }
+
+            while (count-- && x_pos < s->width) {
+                const int bytes_per_vector = 4 * s->vector_height * sizeof(uint16_t);
+                uint16_t* vectordata = (uint16_t*)&s->codebook[vector_index * bytes_per_vector];
+
+                if (vector_index >= MAX_VECTORS)
+                    return AVERROR_INVALIDDATA;
+
+                for (int y = 0; y < s->vector_height; y++) {
+                    for (int x = 0; x < 4; x++) {
+                        s->framebuffer[(y_pos + y) * s->width + x_pos + x] = *vectordata++;
+                    }
+                }
+
+                /* we might want to read the next block index from stream */
+                if ((type == 2) && count > 0) {
+                    vector_index = *stream++;
+                }
+
+                x_pos += 4;
+            }
+
+            if (count > 0) {
+                av_log(s->avctx, AV_LOG_ERROR, "had %d leftover vectors\n", count);
+                return AVERROR_BUG;
+            }
+        }
+    }
+
+    return 0;
+}
+
 static int vqa_decode_frame(AVCodecContext *avctx,
                             void *data, int *got_frame,
                             AVPacket *avpkt)
@@ -603,19 +807,37 @@ static int vqa_decode_frame(AVCodecContext *avctx,
     VqaContext *s = avctx->priv_data;
     AVFrame *frame = data;
     int res;
+    unsigned char* pixels;
 
     if ((res = ff_get_buffer(avctx, frame, 0)) < 0)
         return res;
 
     bytestream2_init(&s->gb, avpkt->data, avpkt->size);
-    if ((res = vqa_decode_chunk(s, frame)) < 0)
-        return res;
 
-    /* make the palette available on the way out */
-    memcpy(frame->data[1], s->palette, PALETTE_COUNT * 4);
-    frame->palette_has_changed = 1;
+    if (avctx->pix_fmt == AV_PIX_FMT_PAL8) {
+        if ((res = vqa_decode_frame_pal8(s, frame)) < 0)
+            return res;
+
+        /* make the palette available on the way out */
+        memcpy(frame->data[1], s->palette, PALETTE_COUNT * 4);
+        frame->palette_has_changed = 1;
+    } else if (avctx->pix_fmt == AV_PIX_FMT_RGB555LE) {
+        if ((res = vqa_decode_frame_hicolor(s, frame)) < 0)
+            return res;
+
+        pixels = frame->data[0];
+
+        /* copy our internal framebuffer to the frame */
+        for (int y = 0; y < s->height; y++) {
+            memcpy(pixels, &s->framebuffer[y * s->width], s->width * sizeof(s->framebuffer[0]));
+            pixels += frame->linesize[0];
+        }
+    } else {
+        av_log(s->avctx, AV_LOG_ERROR, "unsupported pixel format\n");
+        return AVERROR_BUG;
+    }
 
-    *got_frame      = 1;
+    *got_frame = 1;
 
     /* report that the buffer was completely consumed */
     return avpkt->size;
@@ -625,6 +847,7 @@ static av_cold int vqa_decode_end(AVCodecContext *avctx)
 {
     VqaContext *s = avctx->priv_data;
 
+    av_freep(&s->framebuffer);
     av_freep(&s->codebook);
     av_freep(&s->next_codebook_buffer);
     av_freep(&s->decode_buffer);
@@ -633,7 +856,7 @@ static av_cold int vqa_decode_end(AVCodecContext *avctx)
 }
 
 static const AVCodecDefault vqa_defaults[] = {
-    { "max_pixels", "320*240" },
+    { "max_pixels", "640*480" },
     { NULL },
 };
 
diff --git a/libavformat/westwood_vqa.c b/libavformat/westwood_vqa.c
index 587626ea67..1a516bdba2 100644
--- a/libavformat/westwood_vqa.c
+++ b/libavformat/westwood_vqa.c
@@ -47,10 +47,14 @@
 #define CINF_TAG MKBETAG('C', 'I', 'N', 'F')
 #define CINH_TAG MKBETAG('C', 'I', 'N', 'H')
 #define CIND_TAG MKBETAG('C', 'I', 'N', 'D')
+#define LINF_TAG MKBETAG('L', 'I', 'N', 'F')
 #define PINF_TAG MKBETAG('P', 'I', 'N', 'F')
 #define PINH_TAG MKBETAG('P', 'I', 'N', 'H')
 #define PIND_TAG MKBETAG('P', 'I', 'N', 'D')
 #define CMDS_TAG MKBETAG('C', 'M', 'D', 'S')
+#define SN2J_TAG MKBETAG('S', 'N', '2', 'J')
+#define VIEW_TAG MKBETAG('V', 'I', 'E', 'W')
+#define ZBUF_TAG MKBETAG('Z', 'B', 'U', 'F')
 
 #define VQA_HEADER_SIZE 0x2A
 #define VQA_PREAMBLE_SIZE 8
@@ -142,11 +146,14 @@ static int wsvqa_read_header(AVFormatContext *s)
         case CINF_TAG:
         case CINH_TAG:
         case CIND_TAG:
+        case LINF_TAG:
         case PINF_TAG:
         case PINH_TAG:
         case PIND_TAG:
         case FINF_TAG:
         case CMDS_TAG:
+        case VIEW_TAG:
+        case ZBUF_TAG:
             break;
 
         default:
@@ -287,8 +294,11 @@ static int wsvqa_read_packet(AVFormatContext *s,
 
             return ret;
         } else {
-            switch(chunk_type){
+            switch(chunk_type) {
             case CMDS_TAG:
+            case SN2J_TAG:
+            case VIEW_TAG:
+            case ZBUF_TAG:
                 break;
             default:
                 av_log(s, AV_LOG_INFO, "Skipping unknown chunk %s\n",
-- 
2.25.1



More information about the ffmpeg-devel mailing list