]> git.nihav.org Git - nihav.git/commitdiff
add KMVC decoder
authorKostya Shishkov <kostya.shishkov@gmail.com>
Wed, 16 Oct 2024 16:35:25 +0000 (18:35 +0200)
committerKostya Shishkov <kostya.shishkov@gmail.com>
Wed, 16 Oct 2024 16:35:25 +0000 (18:35 +0200)
nihav-game/Cargo.toml
nihav-game/src/codecs/kmvc.rs [new file with mode: 0644]
nihav-game/src/codecs/mod.rs
nihav-registry/src/register.rs

index 5b8151aa41b7e4693239c7b5712e559ca6f327bc..a32c2117bb1f4bb9b41a002bbd5f0ad114475fa8 100644 (file)
@@ -37,7 +37,7 @@ demuxer_vx = ["demuxers"]
 all_decoders = ["all_video_decoders", "all_audio_decoders"]
 decoders = []
 
-all_video_decoders = ["decoder_beam_fcp", "decoder_beam_vbv", "decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_rbt", "decoder_seq", "decoder_sga", "decoder_smush_video", "decoder_vmd", "decoder_vx"]
+all_video_decoders = ["decoder_beam_fcp", "decoder_beam_vbv", "decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_ipma", "decoder_kmvc", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_rbt", "decoder_seq", "decoder_sga", "decoder_smush_video", "decoder_vmd", "decoder_vx"]
 decoder_beam_fcp = ["decoders"]
 decoder_beam_vbv = ["decoders"]
 decoder_bmv = ["decoders"]
@@ -45,6 +45,7 @@ decoder_bmv3 = ["decoders"]
 decoder_fstvid = ["decoders"]
 decoder_gdvvid = ["decoders"]
 decoder_ipma = ["decoders"]
+decoder_kmvc = ["decoders"]
 decoder_midivid = ["decoders"]
 decoder_midivid3 = ["decoders"]
 decoder_q = ["decoders"]
diff --git a/nihav-game/src/codecs/kmvc.rs b/nihav-game/src/codecs/kmvc.rs
new file mode 100644 (file)
index 0000000..88cb7f4
--- /dev/null
@@ -0,0 +1,445 @@
+use nihav_core::codecs::*;
+use nihav_core::io::byteio::*;
+
+struct HybridReader<'a> {
+    src:    &'a [u8],
+    pos:    usize,
+    bitbuf: u8,
+    bits:   u8,
+}
+
+impl<'a> HybridReader<'a> {
+    fn new(src: &'a [u8]) -> Self {
+        Self {
+            src,
+            pos: 1,
+            bitbuf: if !src.is_empty() { src[0] } else { 0 },
+            bits: 8,
+        }
+    }
+    fn read_byte(&mut self) -> DecoderResult<u8> {
+        if self.pos < self.src.len() {
+            let ret = self.src[self.pos];
+            self.pos += 1;
+            Ok(ret)
+        } else {
+            Err(DecoderError::ShortData)
+        }
+    }
+    fn read_bit(&mut self) -> DecoderResult<bool> {
+        let ret = (self.bitbuf & 0x80) != 0;
+        self.bitbuf <<= 1;
+        self.bits    -= 1;
+        if self.bits == 0 {
+            self.bitbuf = self.read_byte()?;
+            self.bits = 8;
+        }
+        Ok(ret)
+    }
+}
+
+struct FrameDecoder<'a, 'b> {
+    width:      usize,
+    height:     usize,
+    stride:     usize,
+    dst:        &'a mut [u8],
+    prev_frm:   &'a [u8],
+    hr:         HybridReader<'b>,
+}
+
+impl<'a, 'b> FrameDecoder<'a, 'b> {
+    fn new(dst: &'a mut [u8], prev_frm: &'a [u8], width: usize, height: usize, hr: HybridReader<'b>) -> Self {
+        Self {
+            width, height,
+            stride: width,
+            dst, prev_frm,
+            hr,
+        }
+    }
+    fn decode_intra1(&mut self, blk_size: usize) -> DecoderResult<()> {
+        for y in (0..self.height).step_by(blk_size) {
+            for x in (0..self.width).step_by(blk_size) {
+                self.decode_block1(x, y, blk_size)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_block1(&mut self, x: usize, y: usize, blk_size: usize) -> DecoderResult<()> {
+        let cur_addr = x + y * self.stride;
+        if blk_size == 1 {
+            self.dst[cur_addr] = self.hr.read_byte()?;
+            return Ok(());
+        }
+        if !self.hr.read_bit()? {
+            let fill = self.hr.read_byte()?;
+            for line in self.dst[cur_addr..].chunks_mut(self.stride).take(blk_size) {
+                for el in line[..blk_size].iter_mut() {
+                    *el = fill;
+                }
+            }
+        } else {
+            let hsize = blk_size >> 1;
+            for n in 0..4 {
+                self.decode_block1(x + (n & 1) * hsize, y + (n >> 1) * hsize, hsize)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_inter2(&mut self, blk_size: usize) -> DecoderResult<()> {
+        for y in (0..self.height).step_by(blk_size) {
+            for x in (0..self.width).step_by(blk_size) {
+                self.decode_block2(x, y, blk_size)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_block2(&mut self, x: usize, y: usize, blk_size: usize) -> DecoderResult<()> {
+        let cur_addr = x + y * self.stride;
+        if blk_size == 1 {
+            self.dst[cur_addr] = self.hr.read_byte()?;
+            return Ok(());
+        }
+        if !self.hr.read_bit()? {
+            if self.hr.read_bit()? {
+                let fill = self.hr.read_byte()?;
+                for line in self.dst[cur_addr..].chunks_mut(self.stride).take(blk_size) {
+                    for el in line[..blk_size].iter_mut() {
+                        *el = fill;
+                    }
+                }
+            } else {
+                for (dline, sline) in self.dst[cur_addr..].chunks_mut(self.stride)
+                        .zip(self.prev_frm[cur_addr..].chunks(self.stride)).take(blk_size) {
+                    dline[..blk_size].copy_from_slice(&sline[..blk_size]);
+                }
+            }
+        } else {
+            let hsize = blk_size >> 1;
+            for n in 0..4 {
+                self.decode_block2(x + (n & 1) * hsize, y + (n >> 1) * hsize, hsize)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_intra3(&mut self, blk_size: usize) -> DecoderResult<()> {
+        for y in (0..self.height).step_by(blk_size) {
+            for x in (0..self.width).step_by(blk_size) {
+                self.decode_block3(x, y, blk_size, true)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_block3(&mut self, x: usize, y: usize, blk_size: usize, top: bool) -> DecoderResult<()> {
+        let mut cur_addr = x + y * self.stride;
+        if blk_size == 1 {
+            self.dst[cur_addr] = self.hr.read_byte()?;
+            return Ok(());
+        }
+        if !self.hr.read_bit()? {
+            if top || !self.hr.read_bit()? {
+                let fill = self.hr.read_byte()?;
+                for line in self.dst[cur_addr..].chunks_mut(self.stride).take(blk_size) {
+                    for el in line[..blk_size].iter_mut() {
+                        *el = fill;
+                    }
+                }
+            } else {
+                let mv = self.hr.read_byte()? as usize;
+                let offset = (mv & 0xF) + (mv >> 4) * self.stride;
+                validate!(offset <= cur_addr);
+                for _ in 0..blk_size {
+                    for x in 0..blk_size {
+                        self.dst[cur_addr + x] = self.dst[cur_addr + x - offset];
+                    }
+                    cur_addr += self.stride;
+                }
+            }
+        } else {
+            let hsize = blk_size >> 1;
+            for n in 0..4 {
+                self.decode_block3(x + (n & 1) * hsize, y + (n >> 1) * hsize, hsize, false)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_inter4(&mut self, blk_size: usize) -> DecoderResult<()> {
+        for y in (0..self.height).step_by(blk_size) {
+            for x in (0..self.width).step_by(blk_size) {
+                self.decode_block4(x, y, blk_size, true)?;
+            }
+        }
+        Ok(())
+    }
+    fn decode_block4(&mut self, x: usize, y: usize, blk_size: usize, top: bool) -> DecoderResult<()> {
+        let cur_addr = x + y * self.stride;
+        if blk_size == 1 {
+            self.dst[cur_addr] = self.hr.read_byte()?;
+            return Ok(());
+        }
+        if !self.hr.read_bit()? {
+            if !self.hr.read_bit()? {
+                let fill = self.hr.read_byte()?;
+                for line in self.dst[cur_addr..].chunks_mut(self.stride).take(blk_size) {
+                    for el in line[..blk_size].iter_mut() {
+                        *el = fill;
+                    }
+                }
+            } else if top {
+                for (dline, sline) in self.dst[cur_addr..].chunks_mut(self.stride)
+                        .zip(self.prev_frm[cur_addr..].chunks(self.stride)).take(blk_size) {
+                    dline[..blk_size].copy_from_slice(&sline[..blk_size]);
+                }
+            } else {
+                let mv = self.hr.read_byte()? as usize;
+                let mv_x = mv & 0xF;
+                let mv_y = mv >> 4;
+                validate!(x + mv_x >= 8 && x + mv_x - 8 + blk_size <= self.width);
+                validate!(y + mv_y >= 8 && y + mv_y - 8 + blk_size <= self.height);
+                let ref_addr = cur_addr + mv_x - 8 + mv_y * self.stride - 8 * self.stride;
+                for (dline, sline) in self.dst[cur_addr..].chunks_mut(self.stride)
+                        .zip(self.prev_frm[ref_addr..].chunks(self.stride)).take(blk_size) {
+                    dline[..blk_size].copy_from_slice(&sline[..blk_size]);
+                }
+            }
+        } else {
+            let hsize = blk_size >> 1;
+            for n in 0..4 {
+                self.decode_block4(x + (n & 1) * hsize, y + (n >> 1) * hsize, hsize, false)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+struct KMVCDecoder {
+    info:       NACodecInfoRef,
+    pal:        [u8; 768],
+    palsize:    usize,
+    frame:      Vec<u8>,
+    pframe:     Vec<u8>,
+    width:      usize,
+    height:     usize,
+    firstpal:   bool,
+}
+
+impl KMVCDecoder {
+    fn new() -> Self {
+        Self {
+            info:       NACodecInfoRef::default(),
+            pal:        [0; 768],
+            palsize:    127,
+            frame:      Vec::new(),
+            pframe:     Vec::new(),
+            width:      0,
+            height:     0,
+            firstpal:   false,
+        }
+    }
+}
+
+impl NADecoder for KMVCDecoder {
+    fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> {
+        if let NACodecTypeInfo::Video(vinfo) = info.get_properties() {
+            self.width  = vinfo.width;
+            self.height = vinfo.height;
+            self.frame  = vec![0; self.width * self.height];
+            self.pframe = vec![0; self.width * self.height];
+            self.pal    = [0; 768];
+            let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(self.width, self.height, false, PAL8_FORMAT));
+            self.info = NACodecInfo::new_ref(info.get_name(), myinfo, info.get_extradata()).into_ref();
+
+            if let Some(edata) = info.get_extradata() {
+                if edata.len() >= 12 {
+                    let palsize = read_u16le(&edata[10..12])? as usize;
+                    validate!(palsize > 0 && palsize < 256);
+                    self.palsize = palsize;
+                }
+                if edata.len() == 1036 {
+                    for (dst, src) in self.pal.chunks_exact_mut(3).zip(edata[12..].chunks_exact(4)) {
+                        dst[0] = src[2];
+                        dst[1] = src[1];
+                        dst[2] = src[0];
+                    }
+                    self.firstpal = true;
+                }
+            }
+
+            Ok(())
+        } else {
+            Err(DecoderError::InvalidData)
+        }
+    }
+    fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult<NAFrameRef> {
+        let src = pkt.get_buffer();
+        validate!(src.len() >= 2);
+
+        for sd in pkt.side_data.iter() {
+            if let NASideData::Palette(true, ref pal) = sd {
+                if !self.firstpal {
+                    for (dst, src) in self.pal.chunks_exact_mut(3).zip(pal.chunks_exact(4)) {
+                        dst[0] = src[0];
+                        dst[1] = src[1];
+                        dst[2] = src[2];
+                    }
+                } else {
+                    self.firstpal = false;
+                }
+                break;
+            }
+        }
+
+        let mut mr = MemoryReader::new_read(&src);
+        let mut br = ByteReader::new(&mut mr);
+
+        let header = br.read_byte()?;
+        let method = header & 0xF;
+        if (header & 0x80) != 0 && method < 3 {
+            let start_clr = br.read_byte()? as usize;
+            let nclrs     = br.read_byte()? as usize;
+            validate!(start_clr + nclrs <= 256);
+            br.read_buf(&mut self.pal[start_clr * 3..][..nclrs * 3])?;
+        }
+        if (header & 0x40) != 0 {
+            br.read_buf(&mut self.pal[3..][..self.palsize * 3])?;
+        }
+        let blk_size = br.read_byte()? as usize;
+        validate!(blk_size > 0 && (blk_size & (blk_size - 1)) == 0);
+        validate!(self.width % blk_size == 0);
+
+        let hr = HybridReader::new(&src[br.tell() as usize..]);
+        std::mem::swap(&mut self.frame, &mut self.pframe);
+        let mut dec = FrameDecoder::new(&mut self.frame, &self.pframe, self.width, self.height, hr);
+        match method {
+            1 => dec.decode_intra1(blk_size)?,
+            2 => dec.decode_inter2(blk_size)?,
+            3 => dec.decode_intra3(blk_size)?,
+            4 => dec.decode_inter4(blk_size)?,
+            _ => return Err(DecoderError::NotImplemented),
+        }
+
+        let buf = alloc_video_buffer(self.info.get_properties().get_video_info().unwrap(), 0)?;
+        let mut vbuf = buf.get_vbuf().unwrap();
+        let paloff = vbuf.get_offset(1);
+        let stride = vbuf.get_stride(0);
+        let data = vbuf.get_data_mut().unwrap();
+
+        for (drow, srow) in data.chunks_mut(stride).zip(self.frame.chunks(self.width)) {
+            drow[..self.width].copy_from_slice(srow);
+        }
+        data[paloff..][..768].copy_from_slice(&self.pal);
+
+        let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), buf);
+        let ftype = if (header & 0x80) != 0 { FrameType::I } else { FrameType::P };
+        frm.set_frame_type(ftype);
+        frm.set_keyframe(ftype == FrameType::I);
+        Ok(frm.into_ref())
+    }
+    fn flush(&mut self) {
+    }
+}
+
+impl NAOptionHandler for KMVCDecoder {
+    fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] }
+    fn set_options(&mut self, _options: &[NAOption]) { }
+    fn query_option_value(&self, _name: &str) -> Option<NAValue> { None }
+}
+
+pub fn get_decoder() -> Box<dyn NADecoder + Send> {
+    Box::new(KMVCDecoder::new())
+}
+
+#[cfg(test)]
+mod test {
+    use nihav_core::codecs::RegisteredDecoders;
+    use nihav_core::demuxers::RegisteredDemuxers;
+    use nihav_codec_support::test::dec_video::*;
+    use crate::game_register_all_decoders;
+    use nihav_commonfmt::generic_register_all_demuxers;
+    #[test]
+    fn test_kmvc_0() {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        generic_register_all_demuxers(&mut dmx_reg);
+        let mut dec_reg = RegisteredDecoders::new();
+        game_register_all_decoders(&mut dec_reg);
+
+        // sample from Alien Breed (PC version)
+        test_decoding("avi", "kmvc", "assets/Game/OUTRO.ANM",
+                      Some(12), &dmx_reg, &dec_reg,
+                      ExpectedTestResult::MD5Frames(vec![
+                            [0x2b2cd2eb, 0x02c7d77e, 0xbf70ce2e, 0x8bad43b0],
+                            [0x1a12d41f, 0x3309992e, 0xba018966, 0xa6349e19],
+                            [0xe8793445, 0x0f3eb58a, 0x5d218de3, 0xe45f0a23],
+                            [0xf42c5018, 0x98cd93c7, 0x01f2c546, 0xb9a825ce],
+                            [0x7c01159b, 0x4688930a, 0x00d82ebb, 0x29db5b96],
+                            [0xb3b659ac, 0xe16138a4, 0xc45369d4, 0xc139db89],
+                            [0xdd024a89, 0x58063f17, 0xc50b8975, 0x3e0465d7],
+                            [0x842f6bcf, 0xf0f6b4ac, 0xda0d2653, 0xa858d7ec],
+                            [0xa6aec7ba, 0x2c234945, 0x8a04525f, 0x5f6a4ab7],
+                            [0x7b6345fe, 0x0ab6f159, 0x438307f6, 0xf442ad08],
+                            [0x89a7ed16, 0x4e2cfcea, 0xb75a002d, 0xbac95a16],
+                            [0x6b18e35a, 0xe96bd87c, 0x071e5bd9, 0x77c19d1c],
+                            [0xc7e3e93f, 0xaffeb4f8, 0x2908e8ed, 0x9f3e4ce9]]));
+    }
+    #[test]
+    fn test_kmvc_1() {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        generic_register_all_demuxers(&mut dmx_reg);
+        let mut dec_reg = RegisteredDecoders::new();
+        game_register_all_decoders(&mut dec_reg);
+
+        // sample from https://samples.mplayerhq.hu/V-codecs/KMVC/FLAME.AVI
+        test_decoding("avi", "kmvc", "assets/Game/FLAME.AVI",
+                      Some(12), &dmx_reg, &dec_reg,
+                      ExpectedTestResult::MD5Frames(vec![
+                            [0xf0ad790f, 0x7e3a57d0, 0xee9282c1, 0x15547d92],
+                            [0x3a6b2a58, 0x6eaf7960, 0x9d4ff92e, 0x1833677a],
+                            [0xfe29ef44, 0x53ad9d6b, 0x0f90644e, 0x2e063cc0],
+                            [0x2888fc3e, 0x3931e3c5, 0xf152a8ac, 0xde4bfc6f],
+                            [0xc1fb2980, 0x7921d476, 0xd9946a83, 0xed437fdb],
+                            [0x8fcca880, 0x884260e9, 0x2f147ae1, 0x1ac3a77d],
+                            [0xcb93573f, 0xb9f00b90, 0xac09cbd6, 0xb65e852b],
+                            [0x293d955c, 0x15a0ec08, 0x2decbeeb, 0xb00a6845],
+                            [0x7b75471a, 0x0e720f1a, 0x679b3b76, 0x6f0acfa2],
+                            [0xcd4cef26, 0x9315612f, 0x6f7066a5, 0x9e931046],
+                            [0x8eaced5f, 0xa1e04658, 0xbb4784eb, 0xe5e64c93],
+                            [0x54a1e90d, 0xe9db5d38, 0x247ff005, 0x3f9c8e2b],
+                            [0xa65668a6, 0x581883d1, 0xaca638cc, 0x8e459a9f]]));
+    }
+    #[test]
+    fn test_kmvc_2() {
+        let mut dmx_reg = RegisteredDemuxers::new();
+        generic_register_all_demuxers(&mut dmx_reg);
+        let mut dec_reg = RegisteredDecoders::new();
+        game_register_all_decoders(&mut dec_reg);
+
+        // sample from https://samples.mplayerhq.hu/V-codecs/KMVC/baseball1.avi
+        test_decoding("avi", "kmvc", "assets/Game/baseball1.avi",
+                      Some(24), &dmx_reg, &dec_reg,
+                      ExpectedTestResult::MD5Frames(vec![
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0xf8778594, 0xd044befd, 0x054f3dcd, 0x72178a7b],
+                            [0x5aefe81d, 0xa193c130, 0x960ffb88, 0x576405b2],
+                            [0x95db7747, 0xdf4d34d7, 0x959c5c67, 0x8cf55708],
+                            [0xcd000e61, 0xd5c852f0, 0xe97ea320, 0x7465ddc7],
+                            [0xddcc347d, 0xb87eff08, 0xe6b5e9ab, 0xbc2e2f72],
+                            [0x98f0f17f, 0x30b5ea8c, 0xb6a2eddc, 0x2f15fff6],
+                            [0xe2fa257a, 0x2a24eda0, 0x0fe85644, 0x7b796130],
+                            [0x96521427, 0x5a9f9e12, 0x7b1ffeff, 0x4b94f0c3],
+                            [0x5d72afa7, 0x954e8404, 0x5598f536, 0x63e60d08],
+                            [0xce3fc2df, 0x98b067a2, 0xf85c597f, 0x45797ff5],
+                            [0x9f86d7bd, 0x3cac2a08, 0xdd00788e, 0x5d8a4731],
+                            [0x6f0b9bda, 0x0613670c, 0xe9f3f75d, 0x0ef112c9],
+                            [0x549ef3ee, 0xb7c582b7, 0xf995b7fe, 0x62140120],
+                            [0xcd29a18d, 0x0c51e581, 0x3dc8e1eb, 0x3dea5759],
+                            [0xb8bd405c, 0xd674557f, 0x9d783ccc, 0x75893613],
+                            [0x922084d6, 0xc36fdcfc, 0x46bfe658, 0x7bb5922a],
+                            [0xc237a20b, 0xbaacd94f, 0xc09a1665, 0xd988132d]]));
+    }
+}
index 7708f45a9dbd21c72334b11d6f0af5b5ccde62df..ba1ce1599b26024a9dc0adb1dc7d8af14f352ef6 100644 (file)
@@ -22,6 +22,8 @@ pub mod futurevision;
 pub mod gremlinvideo;
 #[cfg(feature="decoder_ipma")]
 pub mod ipma;
+#[cfg(feature="decoder_kmvc")]
+pub mod kmvc;
 #[cfg(feature="decoder_lhst500f22")]
 pub mod lhst500f22;
 #[cfg(feature="decoder_midivid")]
@@ -70,6 +72,8 @@ const GAME_CODECS: &[DecoderInfo] = &[
     DecoderInfo { name: "ipma", get_decoder: ipma::get_decoder },
 #[cfg(feature="decoder_ipma")]
     DecoderInfo { name: "ipma2", get_decoder: ipma::get_decoder_v2 },
+#[cfg(feature="decoder_kmvc")]
+    DecoderInfo { name: "kmvc", get_decoder: kmvc::get_decoder },
 #[cfg(feature="decoder_q")]
     DecoderInfo { name: "legend-q-video", get_decoder: q::get_decoder },
 #[cfg(feature="decoder_rbt")]
index 3df96a091404499e17c86eb8bbe8920244dbb2a4..d39339a3d59061dd4680eca83cbc1d528541b6bf 100644 (file)
@@ -266,6 +266,7 @@ static CODEC_REGISTER: &[CodecDescription] = &[
     desc!(video;    "hl-fmv-video",  "Highlander FMV video"),
     desc!(video-llp; "ipma",         "Imagination Pilots Matte Animation"),
     desc!(video-llp; "ipma2",        "Imagination Pilots Matte Animation v2"),
+    desc!(video;    "kmvc",          "Karl Morton's Video Codec"),
     desc!(video;    "legend-q-video", "Legend Entertainment Q video"),
     desc!(video;    "midivid",       "MidiVid"),
     desc!(video;    "midivid3",      "MidiVid 3"),
@@ -338,6 +339,7 @@ static AVI_VIDEO_CODEC_REGISTER: &[(&[u8;4], &str)] = &[
 
     (b"Ipma", "ipma"),
     (b"Ip20", "ipma2"),
+    (b"KMVC", "kmvc"),
 
     (b"MVDV", "midivid"),
     (b"MV30", "midivid3"),