* [FFmpeg-devel] [PATCH] avcodec/sanm: BL16 codec updates (PR #20233)
@ 2025-08-13 10:42 Manuel Lauss
0 siblings, 0 replies; only message in thread
From: Manuel Lauss @ 2025-08-13 10:42 UTC (permalink / raw)
To: ffmpeg-devel
PR #20233 opened by Manuel Lauss (mlauss2)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/20233
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/20233.patch
Blocky-16 is the internal name of the 16bit-SANM codec.
* add support for BL16 subcompression types 1 and 7:
Like codec47_comp1(), they decode a quarter-sized keyframe and
interpolate the missing pixels with a mixing function.
Type 1 delivers pixels as 16-bit values, while type 7 delivers
them as 8-bit indices into the large codebook.
They are implemented in the DLL used by the game "X-Wing Alliance",
in "Grim Fandango Remaster" and "Indiana Jones and the Infernal Machine",
but I haven't yet found any title which actually uses them.
* prefix the functions related to it with bl16_
* structure the subcompression handler like the other (old) codecs.
This lets at least gcc inline all the subcodecs into the main
function.
All the test videos I have still play without issues.
From 9ab275d17551c11562d1a86700534191155e1e60 Mon Sep 17 00:00:00 2001
From: Manuel Lauss <manuel.lauss@gmail.com>
Date: Wed, 13 Aug 2025 12:26:49 +0200
Subject: [PATCH] avcodec/sanm: BL16 codec updates
Blocky-16 is the internal name of the 16bit-SANM codec.
- add support for BL16 subcompression types 1 and 7:
Like codec47_comp1(), they decode a quarter-sized keyframe and
interpolate the missing pixels with a mixing function.
Type 1 delivers pixels as 16-bit values, while type 7 delivers
them as 8-bit indices into the large codebook.
They are implemented in the DLL used by the game "X-Wing Alliance",
but I haven't yet found any title which actually uses them.
- prefix the functions related to it with bl16_
- structure the subcompression handler like the other (old) codecs.
This lets at least gcc inline all the subcodecs into the main
function.
Signed-off-by: Manuel Lauss <manuel.lauss@gmail.com>
---
libavcodec/sanm.c | 281 +++++++++++++++++++++++++++++++---------------
1 file changed, 193 insertions(+), 88 deletions(-)
diff --git a/libavcodec/sanm.c b/libavcodec/sanm.c
index d345f58846..c24a358a9a 100644
--- a/libavcodec/sanm.c
+++ b/libavcodec/sanm.c
@@ -1880,7 +1880,7 @@ static int process_xpal(SANMVideoContext *ctx, int size)
return 0;
}
-static int decode_0(SANMVideoContext *ctx)
+static int bl16_decode_0(SANMVideoContext *ctx)
{
uint16_t *frm = ctx->frm0;
int x, y;
@@ -1897,13 +1897,66 @@ static int decode_0(SANMVideoContext *ctx)
return 0;
}
-static int decode_nop(SANMVideoContext *ctx)
+
+/* BL16 pixel interpolation function, see tgsmush.dll c690 */
+static inline uint16_t bl16_c1_avg_col(uint16_t c1, uint16_t c2)
{
- avpriv_request_sample(ctx->avctx, "Unknown/unsupported compression type");
- return AVERROR_PATCHWELCOME;
+ return (((c2 & 0x07e0) + (c1 & 0x07e0)) & 0x00fc0) |
+ (((c2 & 0xf800) + (c1 & 0xf800)) & 0x1f000) |
+ (((c2 & 0x001f) + (c1 & 0x001f))) >> 1;
}
-static void copy_block(uint16_t *pdest, uint16_t *psrc, int block_size, ptrdiff_t pitch)
+/* Quarter-sized keyframe encoded as stream of 16bit pixel values. Interpolate
+ * missing pixels by averaging the colors of immediate neighbours.
+ * Identical to codec47_comp1() but with 16bit-pixels. tgsmush.dll c6f0
+ */
+static int bl16_decode_1(SANMVideoContext *ctx)
+{
+ uint16_t hh, hw, c1, c2, c3, *dst1, *dst2;
+
+ if (bytestream2_get_bytes_left(&ctx->gb) < ((ctx->width * ctx->height) / 2))
+ return AVERROR_INVALIDDATA;
+
+ hh = (ctx->height + 1) >> 1;
+ dst1 = (uint16_t *)ctx->frm0 + ctx->pitch; /* start with line 1 */
+ while (hh--) {
+ hw = (ctx->width - 1) >> 1;
+ c1 = bytestream2_get_le16u(&ctx->gb);
+ dst1[0] = c1;
+ dst1[1] = c1;
+ dst2 = dst1 + 2;
+ while (hw--) {
+ c2 = bytestream2_get_le16u(&ctx->gb);
+ c3 = bl16_c1_avg_col(c1, c2);
+ *dst2++ = c3;
+ *dst2++ = c2;
+ c1 = c2;
+ }
+ dst1 += ctx->pitch * 2; /* skip to overnext line */
+ }
+ /* line 0 is a copy of line 1 */
+ memcpy(ctx->frm0, ctx->frm0 + ctx->pitch, ctx->pitch);
+
+ /* complete the skipped lines by averaging from the pixels in the lines
+ * above and below
+ */
+ dst1 = ctx->frm0 + (ctx->pitch * 2);
+ hh = (ctx->height - 1) >> 1;
+ while (hh--) {
+ hw = ctx->width;
+ dst2 = dst1;
+ while (hw--) {
+ c1 = *(dst2 - ctx->pitch); /* pixel from line above */
+ c2 = *(dst2 + ctx->pitch); /* pixel from line below */
+ c3 = bl16_c1_avg_col(c1, c2);
+ *dst2++ = c3;
+ }
+ dst1 += ctx->pitch * 2;
+ }
+ return 0;
+}
+
+static void bl16_copy_block(uint16_t *pdest, uint16_t *psrc, int block_size, ptrdiff_t pitch)
{
uint8_t *dst = (uint8_t *)pdest;
uint8_t *src = (uint8_t *)psrc;
@@ -1922,7 +1975,7 @@ static void copy_block(uint16_t *pdest, uint16_t *psrc, int block_size, ptrdiff_
}
}
-static void fill_block(uint16_t *pdest, uint16_t color, int block_size, ptrdiff_t pitch)
+static void bl16_fill_block(uint16_t *pdest, uint16_t color, int block_size, ptrdiff_t pitch)
{
int x, y;
@@ -1932,7 +1985,7 @@ static void fill_block(uint16_t *pdest, uint16_t color, int block_size, ptrdiff_
*pdest++ = color;
}
-static int draw_glyph(SANMVideoContext *ctx, uint16_t *dst, int index,
+static int bl16_draw_glyph(SANMVideoContext *ctx, uint16_t *dst, int index,
uint16_t fg_color, uint16_t bg_color, int block_size,
ptrdiff_t pitch)
{
@@ -1954,7 +2007,7 @@ static int draw_glyph(SANMVideoContext *ctx, uint16_t *dst, int index,
return 0;
}
-static int opcode_0xf7(SANMVideoContext *ctx, int cx, int cy, int block_size, ptrdiff_t pitch)
+static int bl16_opcode_0xf7(SANMVideoContext *ctx, int cx, int cy, int block_size, ptrdiff_t pitch)
{
uint16_t *dst = ctx->frm0 + cx + cy * ctx->pitch;
@@ -1983,12 +2036,12 @@ static int opcode_0xf7(SANMVideoContext *ctx, int cx, int cy, int block_size, pt
bgcolor = ctx->codebook[bytestream2_get_byteu(&ctx->gb)];
fgcolor = ctx->codebook[bytestream2_get_byteu(&ctx->gb)];
- draw_glyph(ctx, dst, glyph, fgcolor, bgcolor, block_size, pitch);
+ bl16_draw_glyph(ctx, dst, glyph, fgcolor, bgcolor, block_size, pitch);
}
return 0;
}
-static int opcode_0xf8(SANMVideoContext *ctx, int cx, int cy, int block_size, ptrdiff_t pitch)
+static int bl16_opcode_0xf8(SANMVideoContext *ctx, int cx, int cy, int block_size, ptrdiff_t pitch)
{
uint16_t *dst = ctx->frm0 + cx + cy * ctx->pitch;
@@ -2011,12 +2064,12 @@ static int opcode_0xf8(SANMVideoContext *ctx, int cx, int cy, int block_size, pt
bgcolor = bytestream2_get_le16u(&ctx->gb);
fgcolor = bytestream2_get_le16u(&ctx->gb);
- draw_glyph(ctx, dst, glyph, fgcolor, bgcolor, block_size, pitch);
+ bl16_draw_glyph(ctx, dst, glyph, fgcolor, bgcolor, block_size, pitch);
}
return 0;
}
-static int good_mvec(SANMVideoContext *ctx, int cx, int cy, int mx, int my,
+static int bl16_good_mvec(SANMVideoContext *ctx, int cx, int cy, int mx, int my,
int block_size)
{
int start_pos = cx + mx + (cy + my) * ctx->pitch;
@@ -2032,7 +2085,7 @@ static int good_mvec(SANMVideoContext *ctx, int cx, int cy, int mx, int my,
return good;
}
-static int codec2subblock(SANMVideoContext *ctx, int cx, int cy, int blk_size)
+static int bl16_codec2subblock(SANMVideoContext *ctx, int cx, int cy, int blk_size)
{
int16_t mx, my, index;
int opcode;
@@ -2047,8 +2100,8 @@ static int codec2subblock(SANMVideoContext *ctx, int cx, int cy, int blk_size)
mx = motion_vectors[opcode][0];
my = motion_vectors[opcode][1];
- if (good_mvec(ctx, cx, cy, mx, my, blk_size)) {
- copy_block(ctx->frm0 + cx + ctx->pitch * cy,
+ if (bl16_good_mvec(ctx, cx, cy, mx, my, blk_size)) {
+ bl16_copy_block(ctx->frm0 + cx + ctx->pitch * cy,
ctx->frm2 + cx + mx + ctx->pitch * (cy + my),
blk_size, ctx->pitch);
}
@@ -2061,55 +2114,55 @@ static int codec2subblock(SANMVideoContext *ctx, int cx, int cy, int blk_size)
mx = index % ctx->width;
my = index / ctx->width;
- if (good_mvec(ctx, cx, cy, mx, my, blk_size)) {
- copy_block(ctx->frm0 + cx + ctx->pitch * cy,
- ctx->frm2 + cx + mx + ctx->pitch * (cy + my),
- blk_size, ctx->pitch);
+ if (bl16_good_mvec(ctx, cx, cy, mx, my, blk_size)) {
+ bl16_copy_block(ctx->frm0 + cx + ctx->pitch * cy,
+ ctx->frm2 + cx + mx + ctx->pitch * (cy + my),
+ blk_size, ctx->pitch);
}
break;
case 0xF6:
- copy_block(ctx->frm0 + cx + ctx->pitch * cy,
- ctx->frm1 + cx + ctx->pitch * cy,
- blk_size, ctx->pitch);
+ bl16_copy_block(ctx->frm0 + cx + ctx->pitch * cy,
+ ctx->frm1 + cx + ctx->pitch * cy,
+ blk_size, ctx->pitch);
break;
case 0xF7:
- opcode_0xf7(ctx, cx, cy, blk_size, ctx->pitch);
+ bl16_opcode_0xf7(ctx, cx, cy, blk_size, ctx->pitch);
break;
case 0xF8:
- opcode_0xf8(ctx, cx, cy, blk_size, ctx->pitch);
+ bl16_opcode_0xf8(ctx, cx, cy, blk_size, ctx->pitch);
break;
case 0xF9:
case 0xFA:
case 0xFB:
case 0xFC:
- fill_block(ctx->frm0 + cx + cy * ctx->pitch,
+ bl16_fill_block(ctx->frm0 + cx + cy * ctx->pitch,
ctx->small_codebook[opcode - 0xf9], blk_size, ctx->pitch);
break;
case 0xFD:
if (bytestream2_get_bytes_left(&ctx->gb) < 1)
return AVERROR_INVALIDDATA;
- fill_block(ctx->frm0 + cx + cy * ctx->pitch,
+ bl16_fill_block(ctx->frm0 + cx + cy * ctx->pitch,
ctx->codebook[bytestream2_get_byteu(&ctx->gb)], blk_size, ctx->pitch);
break;
case 0xFE:
if (bytestream2_get_bytes_left(&ctx->gb) < 2)
return AVERROR_INVALIDDATA;
- fill_block(ctx->frm0 + cx + cy * ctx->pitch,
+ bl16_fill_block(ctx->frm0 + cx + cy * ctx->pitch,
bytestream2_get_le16u(&ctx->gb), blk_size, ctx->pitch);
break;
case 0xFF:
if (blk_size == 2) {
- opcode_0xf8(ctx, cx, cy, blk_size, ctx->pitch);
+ bl16_opcode_0xf8(ctx, cx, cy, blk_size, ctx->pitch);
} else {
blk_size >>= 1;
- if (codec2subblock(ctx, cx, cy, blk_size))
+ if (bl16_codec2subblock(ctx, cx, cy, blk_size))
return AVERROR_INVALIDDATA;
- if (codec2subblock(ctx, cx + blk_size, cy, blk_size))
+ if (bl16_codec2subblock(ctx, cx + blk_size, cy, blk_size))
return AVERROR_INVALIDDATA;
- if (codec2subblock(ctx, cx, cy + blk_size, blk_size))
+ if (bl16_codec2subblock(ctx, cx, cy + blk_size, blk_size))
return AVERROR_INVALIDDATA;
- if (codec2subblock(ctx, cx + blk_size, cy + blk_size, blk_size))
+ if (bl16_codec2subblock(ctx, cx + blk_size, cy + blk_size, blk_size))
return AVERROR_INVALIDDATA;
}
break;
@@ -2117,31 +2170,19 @@ static int codec2subblock(SANMVideoContext *ctx, int cx, int cy, int blk_size)
return 0;
}
-static int decode_2(SANMVideoContext *ctx)
+static int bl16_decode_2(SANMVideoContext *ctx)
{
int cx, cy, ret;
for (cy = 0; cy < ctx->aligned_height; cy += 8)
for (cx = 0; cx < ctx->aligned_width; cx += 8)
- if (ret = codec2subblock(ctx, cx, cy, 8))
+ if (ret = bl16_codec2subblock(ctx, cx, cy, 8))
return ret;
return 0;
}
-static int decode_3(SANMVideoContext *ctx)
-{
- memcpy(ctx->frm0, ctx->frm2, ctx->frm2_size);
- return 0;
-}
-
-static int decode_4(SANMVideoContext *ctx)
-{
- memcpy(ctx->frm0, ctx->frm1, ctx->frm1_size);
- return 0;
-}
-
-static int decode_5(SANMVideoContext *ctx)
+static int bl16_decode_5(SANMVideoContext *ctx)
{
#if HAVE_BIGENDIAN
uint16_t *frm;
@@ -2164,7 +2205,7 @@ static int decode_5(SANMVideoContext *ctx)
return 0;
}
-static int decode_6(SANMVideoContext *ctx)
+static int bl16_decode_6(SANMVideoContext *ctx)
{
int npixels = ctx->npixels;
uint16_t *frm = ctx->frm0;
@@ -2179,7 +2220,58 @@ static int decode_6(SANMVideoContext *ctx)
return 0;
}
-static int decode_8(SANMVideoContext *ctx)
+/* Quarter-sized keyframe encoded as stream of codebook indices. Interpolate
+ * missing pixels by averaging the colors of immediate neighbours.
+ * Identical to codec47_comp1(), but without the interpolation table.
+ * tgsmush.dll c6f0
+ */
+static int bl16_decode_7(SANMVideoContext *ctx)
+{
+ uint16_t hh, hw, c1, c2, c3, *dst1, *dst2;
+
+ if (bytestream2_get_bytes_left(&ctx->gb) < ((ctx->width * ctx->height) / 4))
+ return AVERROR_INVALIDDATA;
+
+ hh = (ctx->height + 1) >> 1;
+ dst1 = (uint16_t *)ctx->frm0 + ctx->pitch; /* start with line 1 */
+ while (hh--) {
+ hw = (ctx->width - 1) >> 1;
+ c1 = ctx->codebook[bytestream2_get_byteu(&ctx->gb)];
+ dst1[0] = c1; /* leftmost 2 pixels of a row are identical */
+ dst1[1] = c1;
+ dst2 = dst1 + 2;
+ while (hw--) {
+ c2 = ctx->codebook[bytestream2_get_byteu(&ctx->gb)];
+ c3 = bl16_c1_avg_col(c1, c2);
+ *dst2++ = c3;
+ *dst2++ = c2;
+ c1 = c2;
+ }
+ dst1 += ctx->pitch * 2; /* skip to overnext line */
+ }
+ /* line 0 is a copy of line 1 */
+ memcpy(ctx->frm0, ctx->frm0 + ctx->pitch, ctx->pitch);
+
+ /* complete the skipped lines by averaging from the pixels in the lines
+ * above and below.
+ */
+ dst1 = ctx->frm0 + (ctx->pitch * 2);
+ hh = (ctx->height - 1) >> 1;
+ while (hh--) {
+ hw = ctx->width;
+ dst2 = dst1;
+ while (hw--) {
+ c1 = *(dst2 - ctx->pitch); /* pixel from line above */
+ c2 = *(dst2 + ctx->pitch); /* pixel from line below */
+ c3 = bl16_c1_avg_col(c1, c2);
+ *dst2++ = c3;
+ }
+ dst1 += ctx->pitch * 2;
+ }
+ return 0;
+}
+
+static int bl16_decode_8(SANMVideoContext *ctx)
{
uint16_t *pdest = ctx->frm0;
uint8_t *rsrc;
@@ -2201,14 +2293,7 @@ static int decode_8(SANMVideoContext *ctx)
return 0;
}
-typedef int (*frm_decoder)(SANMVideoContext *ctx);
-
-static const frm_decoder v1_decoders[] = {
- decode_0, decode_nop, decode_2, decode_3, decode_4, decode_5,
- decode_6, decode_nop, decode_8
-};
-
-static int read_frame_header(SANMVideoContext *ctx, SANMFrameHeader *hdr)
+static int bl16_read_frame_header(SANMVideoContext *ctx, SANMFrameHeader *hdr)
{
int i, ret;
@@ -2248,7 +2333,7 @@ static int read_frame_header(SANMVideoContext *ctx, SANMFrameHeader *hdr)
return 0;
}
-static void fill_frame(uint16_t *pbuf, int buf_size, uint16_t color)
+static void bl16_fill_frame(uint16_t *pbuf, int buf_size, uint16_t color)
{
if (buf_size--) {
*pbuf++ = color;
@@ -2278,6 +2363,54 @@ static int copy_output(SANMVideoContext *ctx, SANMFrameHeader *hdr)
return 0;
}
+static int decode_bl16(AVCodecContext *avctx)
+{
+ SANMVideoContext *ctx = avctx->priv_data;
+ SANMFrameHeader header;
+ int ret;
+
+ if ((ret = bl16_read_frame_header(ctx, &header)))
+ return ret;
+
+ ctx->rotate_code = header.rotate_code;
+ if (!header.seq_num) {
+ ctx->frame->flags |= AV_FRAME_FLAG_KEY;
+ ctx->frame->pict_type = AV_PICTURE_TYPE_I;
+ bl16_fill_frame(ctx->frm1, ctx->npixels, header.bg_color);
+ bl16_fill_frame(ctx->frm2, ctx->npixels, header.bg_color);
+ } else {
+ ctx->frame->flags &= ~AV_FRAME_FLAG_KEY;
+ ctx->frame->pict_type = AV_PICTURE_TYPE_P;
+ }
+
+ switch (header.codec) {
+ case 0: ret = bl16_decode_0(ctx); break;
+ case 1: ret = bl16_decode_1(ctx); break;
+ case 2: ret = bl16_decode_2(ctx); break;
+ case 3: memcpy(ctx->frm0, ctx->frm2, ctx->frm2_size); ret = 0; break;
+ case 4: memcpy(ctx->frm0, ctx->frm1, ctx->frm1_size); ret = 0; break;
+ case 5: ret = bl16_decode_5(ctx); break;
+ case 6: ret = bl16_decode_6(ctx); break;
+ case 7: ret = bl16_decode_7(ctx); break;
+ case 8: ret = bl16_decode_8(ctx); break;
+ default:
+ avpriv_request_sample(avctx, "BL16 Subcodec %d", header.codec);
+ return AVERROR_PATCHWELCOME;
+ break;
+ }
+
+ if (ret) {
+ av_log(avctx, AV_LOG_ERROR,
+ "BL16 Subcodec %d: error decoding frame.\n", header.codec);
+ return ret;
+ }
+
+ if ((ret = copy_output(ctx, &header)))
+ return ret;
+
+ return 0;
+}
+
static int decode_frame(AVCodecContext *avctx, AVFrame *frame,
int *got_frame_ptr, AVPacket *pkt)
{
@@ -2388,38 +2521,10 @@ static int decode_frame(AVCodecContext *avctx, AVFrame *frame,
*got_frame_ptr = 1;
}
} else {
- SANMFrameHeader header;
-
- if ((ret = read_frame_header(ctx, &header)))
+ /* SANM Blocky-16 (BL16) video codec chunk */
+ if (ret = decode_bl16(avctx))
return ret;
-
- ctx->rotate_code = header.rotate_code;
- if (!header.seq_num) {
- ctx->frame->flags |= AV_FRAME_FLAG_KEY;
- ctx->frame->pict_type = AV_PICTURE_TYPE_I;
- fill_frame(ctx->frm1, ctx->npixels, header.bg_color);
- fill_frame(ctx->frm2, ctx->npixels, header.bg_color);
- } else {
- ctx->frame->flags &= ~AV_FRAME_FLAG_KEY;
- ctx->frame->pict_type = AV_PICTURE_TYPE_P;
- }
-
- if (header.codec < FF_ARRAY_ELEMS(v1_decoders)) {
- if ((ret = v1_decoders[header.codec](ctx))) {
- av_log(avctx, AV_LOG_ERROR,
- "Subcodec %d: error decoding frame.\n", header.codec);
- return ret;
- }
- } else {
- avpriv_request_sample(avctx, "Subcodec %d", header.codec);
- return AVERROR_PATCHWELCOME;
- }
-
- if ((ret = copy_output(ctx, &header)))
- return ret;
-
*got_frame_ptr = 1;
-
}
if (ctx->rotate_code)
rotate_bufs(ctx, ctx->rotate_code);
--
2.49.1
_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2025-08-13 10:42 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-08-13 10:42 [FFmpeg-devel] [PATCH] avcodec/sanm: BL16 codec updates (PR #20233) Manuel Lauss
Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
This inbox may be cloned and mirrored by anyone:
git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git
# If you have public-inbox 1.1+ installed, you may
# initialize and index your mirror using the following commands:
public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \
ffmpegdev@gitmailbox.com
public-inbox-index ffmpegdev
Example config snippet for mirrors.
AGPL code for this site: git clone https://public-inbox.org/public-inbox.git