Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
 help / color / mirror / Atom feed
* [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