From 2826a23f3a214e2f5d47d6dee6fb06754730dc0e Mon Sep 17 00:00:00 2001 From: Kostya Shishkov Date: Wed, 23 Mar 2022 18:29:01 +0100 Subject: [PATCH] support Legend Entertainment Q format version 7 --- nihav-game/src/codecs/q.rs | 261 ++++++++++++++++++++++++++++++++++- nihav-game/src/demuxers/q.rs | 22 +-- nihav-registry/src/detect.rs | 2 +- 3 files changed, 271 insertions(+), 14 deletions(-) diff --git a/nihav-game/src/codecs/q.rs b/nihav-game/src/codecs/q.rs index 9f30ae5..a884950 100644 --- a/nihav-game/src/codecs/q.rs +++ b/nihav-game/src/codecs/q.rs @@ -29,6 +29,7 @@ struct QVideoDecoder { mode: u8, patterns: [u16; 128], version: u8, + f8_cache: [[u8; 16]; 240], } macro_rules! copy_tile { @@ -59,6 +60,7 @@ impl QVideoDecoder { tile_h: 0, patterns: [0; 128], version: 0, + f8_cache: [[0; 16]; 240], } } @@ -186,7 +188,7 @@ impl QVideoDecoder { Ok(()) } - fn decode_frame(&mut self, br: &mut ByteReader, _ctype: u16) -> DecoderResult<()> { + fn decode_frame_5(&mut self, br: &mut ByteReader, _ctype: u16) -> DecoderResult<()> { let mut titer = self.tile_off.iter().enumerate(); let mut last_mode = TileMode::Start; @@ -386,6 +388,245 @@ impl QVideoDecoder { Ok(()) } + fn decode_frame_7(&mut self, br: &mut ByteReader, _ctype: u16) -> DecoderResult<()> { + let mut titer = self.tile_off.iter().enumerate(); + let mut last_mode = TileMode::Start; + + let mut f8_mode = false; + let row_size = self.w / self.tile_w; + let mut next_row = 0; + let mut f8_data = [0; 16]; + let mut f8_pos = 0; + + while let Some((tile_no, &tile_off)) = titer.next() { + if tile_no == next_row { + f8_mode = false; + next_row += row_size; + } + while br.peek_byte()? == 0xF8 { + br.read_byte()?; + if f8_mode { + f8_mode = false; + } else { + let idx = br.read_byte()? as usize; + if idx < 0x10 { + validate!(f8_pos < self.f8_cache.len()); + br.peek_buf(&mut self.f8_cache[f8_pos])?; + if idx > 0 { + br.read_skip(idx)?; + } + f8_data = self.f8_cache[f8_pos]; + } else { + f8_data = self.f8_cache[idx - 0x10]; + self.f8_cache[f8_pos] = f8_data; + } + f8_pos += 1; + f8_mode = true; + } + } + + let op = br.read_byte()?; + if op < 0xF8 { + let (clr0, clr1) = if !f8_mode { + if br.peek_byte()? < 0xF8 { + (op, br.read_byte()?) + } else { + (op, op) + } + } else { + (f8_data[(op & 0xF) as usize], f8_data[(op >> 4) as usize]) + }; + if clr0 == clr1 && (!f8_mode || ((op & 0xF) == (op >> 4))) { + for dline in self.frame[tile_off..].chunks_mut(self.w).take(self.tile_h) { + for el in dline[..self.tile_w].iter_mut() { + *el = clr0; + } + } + last_mode = TileMode::Fill; + } else { + let pat = br.read_byte()?; + let mut pattern = if pat < 128 { + last_mode = TileMode::ShortPattern(clr0, clr1); + self.patterns[pat as usize] + } else { + last_mode = TileMode::LongPattern(clr0, clr1); + u16::from(pat) | (u16::from(br.read_byte()?) << 8) + }; + for dline in self.frame[tile_off..].chunks_mut(self.w).take(self.tile_h) { + for el in dline[..self.tile_w].iter_mut() { + *el = if (pattern & 0x8000) == 0 { clr0 } else { clr1 }; + pattern <<= 1; + } + } + } + } else { + match op { + 0xF8 => { + unreachable!(); + }, + 0xF9 => { + let run = br.read_byte()? as usize; + validate!(run > 0); + + validate!(tile_no > 0); + validate!(last_mode != TileMode::Start); + let mut tile_no = tile_no; + let mut tile_off = tile_off; + for i in 0..run { + let copy_off = match last_mode { + TileMode::Forward(off) => { + tile_no + (off as usize) + }, + TileMode::Backward(off) => { + validate!(tile_no >= (off as usize)); + tile_no - (off as usize) + }, + TileMode::Skip => self.tile_off.len(), + _ => tile_no - 1, + }; + if copy_off < self.tile_off.len() { + copy_tile!(self, tile_off, self.tile_off[copy_off]); + } + if i + 1 < run { + let (tno, &toff) = titer.next().unwrap(); + tile_no = tno; + tile_off = toff; + } + } + last_mode = TileMode::Run; + }, + 0xFA => { + let rtile = br.read_u16le()? as usize; + validate!(rtile < self.tile_off.len()); + copy_tile!(self, tile_off, self.tile_off[rtile]); + last_mode = TileMode::Reuse; + }, + 0xFB => { + match self.mode { + 6 => { + let run = br.read_byte()? as usize; + validate!(run >= 2); + let mut tile_no = tile_no; + let mut tile_off = tile_off; + for i in 0..run { + match last_mode { + TileMode::Start => return Err(DecoderError::InvalidData), + TileMode::Fill => { + let clr = br.read_byte()?; + for dline in self.frame[tile_off..].chunks_mut(self.w).take(self.tile_h) { + for el in dline[..self.tile_w].iter_mut() { + *el = clr; + } + } + }, + TileMode::ShortPattern(clr0, clr1) => { + let pat = br.read_byte()?; + let mut pattern = if pat < 128 { + self.patterns[pat as usize] + } else { + u16::from(pat) | (u16::from(br.read_byte()?) << 8) + }; + for dline in self.frame[tile_off..].chunks_mut(self.w).take(self.tile_h) { + for el in dline[..self.tile_w].iter_mut() { + *el = if (pattern & 0x8000) == 0 { clr0 } else { clr1 }; + pattern <<= 1; + } + } + }, + TileMode::LongPattern(clr0, clr1) => { + let mut pattern = br.read_u16le()?; + for dline in self.frame[tile_off..].chunks_mut(self.w).take(self.tile_h) { + for el in dline[..self.tile_w].iter_mut() { + *el = if (pattern & 0x8000) == 0 { clr0 } else { clr1 }; + pattern <<= 1; + } + } + }, + TileMode::Reuse => { + let rtile = br.read_u16le()? as usize; + validate!(rtile < self.tile_off.len()); + copy_tile!(self, tile_off, self.tile_off[rtile]); + }, + TileMode::MV => { + let idx = br.read_byte()? as usize; + let (x, y) = DEF_MVS[idx]; + let src_off = (tile_off as isize) + (x as isize) * 4 + (y as isize) * 4 * (self.w as isize); + validate!(src_off >= 0); + validate!((src_off as usize) + self.tile_w + (self.tile_h - 1) * self.w <= self.w * self.h); + copy_tile!(self, tile_off, src_off as usize); + }, + TileMode::Forward(_) => { + let off = (br.read_byte()? as usize) + 1; + validate!(tile_no + off < self.tile_off.len()); + copy_tile!(self, tile_off, self.tile_off[tile_no + off]); + }, + TileMode::Backward(_) => { + let off = (br.read_byte()? as usize) + 1; + validate!(off <= tile_no); + copy_tile!(self, tile_off, self.tile_off[tile_no - off]); + }, + _ => unimplemented!(), + }; + + if i + 1 < run { + let (tno, &toff) = titer.next().unwrap(); + tile_no = tno; + tile_off = toff; + } + } + }, + 7 => { + validate!(self.tile_w == 4 && self.tile_h == 4); + let run = br.read_byte()? as usize; + validate!(run > 0); + + let mut tile_off = tile_off; + for i in 0..run { + Self::decode_mode7_tile(&mut self.frame[tile_off..], self.w, br)?; + + if i + 1 < run { + let (_tno, &toff) = titer.next().unwrap(); + tile_off = toff; + } + } + }, + _ => { + unimplemented!(); + }, + }; + last_mode = TileMode::FB; + }, + 0xFC => { + let idx = br.read_byte()? as usize; + let (x, y) = DEF_MVS[idx]; + let src_off = (tile_off as isize) + (x as isize) * 4 + (y as isize) * 4 * (self.w as isize); + validate!(src_off >= 0); + validate!((src_off as usize) + self.tile_w + (self.tile_h - 1) * self.w <= self.w * self.h); + + copy_tile!(self, tile_off, src_off as usize); + last_mode = TileMode::MV; + }, + 0xFD => { + let off = (br.read_byte()? as usize) + 1; + validate!(tile_no + off < self.tile_off.len()); + copy_tile!(self, tile_off, self.tile_off[tile_no + off]); + last_mode = TileMode::Forward(off as u16); + }, + 0xFE => { + let off = (br.read_byte()? as usize) + 1; + validate!(off <= tile_no); + copy_tile!(self, tile_off, self.tile_off[tile_no - off]); + last_mode = TileMode::Backward(off as u16); + }, + _ => { + last_mode = TileMode::Skip; + }, + }; + } + } + + Ok(()) + } fn output_frame(&mut self, bufinfo: &mut NABufferType, w: usize, h: usize) { let bufo = bufinfo.get_vbuf(); @@ -436,6 +677,7 @@ impl NADecoder for QVideoDecoder { self.mode = match self.version { 4 => 6, 5 => 7, + 7 => 7, _ => 0, }; @@ -479,8 +721,11 @@ impl NADecoder for QVideoDecoder { } if self.version == 3 { self.decode_frame_v3(&mut br, ctype)?; + } else if self.version < 7 { + self.decode_frame_5(&mut br, ctype)?; } else { - self.decode_frame(&mut br, ctype)?; + self.mode = if ctype == 11 { 7 } else { 6 }; + self.decode_frame_7(&mut br, ctype)?; } }, 5 => { @@ -526,7 +771,7 @@ mod test { use crate::game_register_all_decoders; use crate::game_register_all_demuxers; - // samples from Deathgate, Mission Critical and Shannara games + // samples from Callahan's Crosstime Saloon, Deathgate, Mission Critical and Shannara games #[test] fn test_q_video3() { let mut dmx_reg = RegisteredDemuxers::new(); @@ -557,6 +802,16 @@ mod test { test_decoding("legend-q", "legend-q-video", "assets/Game/mc703.q", Some(16), &dmx_reg, &dec_reg, ExpectedTestResult::MD5([0xf65ea3ce, 0x3052b2bb, 0xb10f8f69, 0x530d60f9])); } + #[test] + fn test_q_video7() { + let mut dmx_reg = RegisteredDemuxers::new(); + game_register_all_demuxers(&mut dmx_reg); + let mut dec_reg = RegisteredDecoders::new(); + game_register_all_decoders(&mut dec_reg); + + test_decoding("legend-q", "legend-q-video", "assets/Game/CCS003.Q", Some(16), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5([0x4c0f0712, 0xc6c39f5b, 0x5bb6902f, 0x9119940e])); + } } const DEF_MVS: [(i8, i8); 256] = [ diff --git a/nihav-game/src/demuxers/q.rs b/nihav-game/src/demuxers/q.rs index 473507a..6c52ab8 100644 --- a/nihav-game/src/demuxers/q.rs +++ b/nihav-game/src/demuxers/q.rs @@ -24,7 +24,7 @@ impl<'a> DemuxCore<'a> for QDemuxer<'a> { validate!(hdr[0] == 0x39); validate!(hdr[1] == 0x68); let version = hdr[2]; - validate!(version >= 3 && version <= 5); + validate!(version >= 3 && version <= 7); let mut width = read_u16le(&hdr[4..])? as usize; let mut height = read_u16le(&hdr[6..])? as usize; if version > 3 { @@ -55,14 +55,16 @@ impl<'a> DemuxCore<'a> for QDemuxer<'a> { let mut buf = vec![0; size]; self.src.read_buf(&mut buf)?; let arate = read_u32le(&buf[24..])?; - let channels = buf[22]; - let abits = buf[34] as usize; - validate!(abits == 8 || abits == 16); - self.bps = (channels as usize) * abits / 8; - let bsize = read_u16le(&buf[32..])? as usize; - let ahdr = NAAudioInfo::new(arate, channels as u8, if abits == 16 { SND_S16_FORMAT } else { SND_U8_FORMAT }, bsize); - let ainfo = NACodecInfo::new("pcm", NACodecTypeInfo::Audio(ahdr), None); - self.a_id = strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, arate, 2)); + if arate > 0 { + let channels = buf[22]; + let abits = buf[34] as usize; + validate!(abits == 8 || abits == 16); + self.bps = (channels as usize) * abits / 8; + let bsize = read_u16le(&buf[32..])? as usize; + let ahdr = NAAudioInfo::new(arate, channels as u8, if abits == 16 { SND_S16_FORMAT } else { SND_U8_FORMAT }, bsize); + let ainfo = NACodecInfo::new("pcm", NACodecTypeInfo::Audio(ahdr), None); + self.a_id = strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, arate, 2)); + } } } self.apts = 0; @@ -85,7 +87,7 @@ impl<'a> DemuxCore<'a> for QDemuxer<'a> { self.apts += (size / self.bps) as u64; return self.src.read_packet(str, ts, true, size); } else { - return Err(DemuxerError::InvalidData); + self.src.read_skip(size)?; } }, 1 => { diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index d32fac3..f79d063 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -272,7 +272,7 @@ const DETECTORS: &[DetectConditions] = &[ demux_name: "legend-q", extensions: ".q", conditions: &[CheckItem{offs: 0, cond: &CC::Eq(Arg::U16LE(0x6839))}, - CheckItem{offs: 2, cond: &CC::In(Arg::Byte(3), Arg::Byte(5))}], + CheckItem{offs: 2, cond: &CC::In(Arg::Byte(3), Arg::Byte(7))}], }, DetectConditions { demux_name: "smush", -- 2.39.5