From: Kostya Shishkov Date: Thu, 27 Jun 2024 15:47:11 +0000 (+0200) Subject: Sierra RBT and SEQ formats support X-Git-Url: https://git.nihav.org/?a=commitdiff_plain;h=f9fc73be53d5226f12dc8f566b082f0758e87a87;p=nihav.git Sierra RBT and SEQ formats support --- diff --git a/nihav-game/Cargo.toml b/nihav-game/Cargo.toml index 9111b9a..af489f3 100644 --- a/nihav-game/Cargo.toml +++ b/nihav-game/Cargo.toml @@ -18,7 +18,7 @@ nihav_commonfmt = { path = "../nihav-commonfmt", default-features=false, feature [features] default = ["all_decoders", "all_demuxers", "all_muxers"] demuxers = [] -all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_cnm", "demuxer_fcmp", "demuxer_fst", "demuxer_gdv", "demuxer_hl_fmv", "demuxer_imax", "demuxer_q", "demuxer_sga", "demuxer_siff", "demuxer_smush", "demuxer_vmd", "demuxer_vx"] +all_demuxers = ["demuxer_bmv", "demuxer_bmv3", "demuxer_cnm", "demuxer_fcmp", "demuxer_fst", "demuxer_gdv", "demuxer_hl_fmv", "demuxer_imax", "demuxer_q", "demuxer_rbt", "demuxer_seq", "demuxer_sga", "demuxer_siff", "demuxer_smush", "demuxer_vmd", "demuxer_vx"] demuxer_bmv = ["demuxers"] demuxer_bmv3 = ["demuxers"] demuxer_cnm = ["demuxers"] @@ -28,6 +28,8 @@ demuxer_gdv = ["demuxers"] demuxer_hl_fmv = ["demuxers"] demuxer_imax = ["demuxers"] demuxer_q = ["demuxers"] +demuxer_rbt = ["demuxers"] +demuxer_seq = ["demuxers"] demuxer_sga = ["demuxers"] demuxer_siff = ["demuxers"] demuxer_smush = ["demuxers"] @@ -37,7 +39,7 @@ demuxer_vx = ["demuxers"] all_decoders = ["all_video_decoders", "all_audio_decoders"] decoders = [] -all_video_decoders = ["decoder_arxel_vid", "decoder_beam_fcp", "decoder_beam_vbv", "decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_hl_fmv", "decoder_imax", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_sga", "decoder_smush_video", "decoder_vmd", "decoder_vx"] +all_video_decoders = ["decoder_arxel_vid", "decoder_beam_fcp", "decoder_beam_vbv", "decoder_bmv", "decoder_bmv3", "decoder_fstvid", "decoder_gdvvid", "decoder_hl_fmv", "decoder_imax", "decoder_ipma", "decoder_midivid", "decoder_midivid3", "decoder_q", "decoder_rbt", "decoder_seq", "decoder_sga", "decoder_smush_video", "decoder_vmd", "decoder_vx"] decoder_arxel_vid = ["decoders"] decoder_beam_fcp = ["decoders"] decoder_beam_vbv = ["decoders"] @@ -51,6 +53,8 @@ decoder_ipma = ["decoders"] decoder_midivid = ["decoders"] decoder_midivid3 = ["decoders"] decoder_q = ["decoders"] +decoder_rbt = ["decoders"] +decoder_seq = ["decoders"] decoder_sga = ["decoders"] decoder_smush_video = ["decoders"] decoder_vmd = ["decoders"] diff --git a/nihav-game/src/codecs/mod.rs b/nihav-game/src/codecs/mod.rs index c8d1a38..af23b68 100644 --- a/nihav-game/src/codecs/mod.rs +++ b/nihav-game/src/codecs/mod.rs @@ -36,6 +36,10 @@ pub mod midivid; pub mod midivid3; #[cfg(feature="decoder_q")] pub mod q; +#[cfg(feature="decoder_rbt")] +pub mod rbt; +#[cfg(feature="decoder_seq")] +pub mod seq; #[cfg(feature="decoder_sga")] pub mod sga; #[cfg(any(feature="decoder_smush_video", feature="decoder_smush_audio"))] @@ -80,6 +84,12 @@ const GAME_CODECS: &[DecoderInfo] = &[ DecoderInfo { name: "ipma2", get_decoder: ipma::get_decoder_v2 }, #[cfg(feature="decoder_q")] DecoderInfo { name: "legend-q-video", get_decoder: q::get_decoder }, +#[cfg(feature="decoder_rbt")] + DecoderInfo { name: "rbt-video", get_decoder: rbt::get_decoder }, +#[cfg(feature="decoder_rbt")] + DecoderInfo { name: "rbt-audio", get_decoder: rbt::get_decoder_audio }, +#[cfg(feature="decoder_seq")] + DecoderInfo { name: "seq-video", get_decoder: seq::get_decoder }, #[cfg(feature="decoder_sga")] DecoderInfo { name: "dp-sga", get_decoder: sga::get_decoder }, #[cfg(feature="decoder_smush_video")] diff --git a/nihav-game/src/codecs/rbt.rs b/nihav-game/src/codecs/rbt.rs new file mode 100644 index 0000000..cc64bfb --- /dev/null +++ b/nihav-game/src/codecs/rbt.rs @@ -0,0 +1,453 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; +use nihav_core::compr::lz_copy; + +const FRAME_HEADER: usize = 24; + +struct RobotVideoDecoder { + info: NACodecInfoRef, + pal: [u8; 768], + frame: Vec, + cell_buf: Vec, + width: usize, + height: usize, + version: u8, +} + +impl RobotVideoDecoder { + fn new() -> Self { + Self { + info: NACodecInfoRef::default(), + pal: [0; 768], + frame: Vec::new(), + cell_buf: Vec::new(), + width: 0, + height: 0, + version: 0, + } + } +} + +struct BitReader<'a, 'b> { + br: &'a mut ByteReader<'b>, + end: u64, + bbuf: u32, + bits: u8, +} + +impl<'a, 'b> BitReader<'a, 'b> { + fn new(br: &'a mut ByteReader<'b>, size: usize) -> Self { + let end = br.tell() + (size as u64); + Self { + br, end, + bbuf: 0, + bits: 0, + } + } + fn refill(&mut self) -> DecoderResult<()> { + while self.bits <= 24 && self.br.tell() < self.end { + self.bbuf |= u32::from(self.br.read_byte()?) << (24 - self.bits); + self.bits += 8; + } + Ok(()) + } + fn read_bit(&mut self) -> DecoderResult { + self.refill()?; + if self.bits == 0 { return Err(DecoderError::ShortData); } + let bit = (self.bbuf >> 31) != 0; + self.bbuf <<= 1; + self.bits -= 1; + Ok(bit) + } + fn read(&mut self, nbits: u8) -> DecoderResult { + self.refill()?; + if self.bits < nbits { return Err(DecoderError::ShortData); } + let ret = self.bbuf >> (32 - nbits); + self.bbuf <<= nbits; + self.bits -= nbits; + Ok(ret) + } +} + +fn lzs_unpack(br: &mut ByteReader, csize: usize, dst: &mut [u8]) -> DecoderResult<()> { + let mut br = BitReader::new(br, csize); + + let mut dpos = 0; + loop { + if br.read_bit()? { + let offset = (if br.read_bit()? { + let off = br.read(7)?; + if off == 0 { + validate!(dpos == dst.len()); + return Ok(()); + } + off + } else { + br.read(11)? + }) as usize; + + let mut len = br.read(2)?; + if len < 3 { + len += 2; + } else { + len = br.read(2)?; + if len < 3 { + len += 5; + } else { + len = 8; + loop { + let t = br.read(4)?; + len += t; + if t != 0xF { + break; + } + } + } + } + let len = len as usize; + + validate!(offset <= dpos); + validate!(dpos + len <= dst.len()); + lz_copy(dst, dpos, offset, len); + dpos += len; + } else { + dst[dpos] = br.read(8)? as u8; + dpos += 1; + } + } +} + +fn unpack_cell(br: &mut ByteReader, cell_size: usize, nchunks: usize, dst: &mut Vec, limit: usize) -> DecoderResult<()> { + let mut data_left = cell_size; + dst.clear(); + dst.reserve(limit); + for _ in 0..nchunks { + validate!(data_left >= 10); + let csize = br.read_u32le()? as usize; + validate!(csize <= data_left); + let rsize = br.read_u32le()? as usize; + validate!(rsize + dst.len() <= limit); + let method = br.read_u16le()?; + + data_left -= 10; + + let cur_size = dst.len(); + dst.resize(cur_size + rsize, 0); + match method { + 0 => { lzs_unpack(br, csize, &mut dst[cur_size..])?; }, + 2 => { + validate!(rsize == csize); + br.read_buf(&mut dst[cur_size..])?; + }, + _ => return Err(DecoderError::NotImplemented), + } + data_left -= csize; + } + Ok(()) +} + +fn blit(dst: &mut [u8], dstride: usize, src: &[u8], sstride: usize) { + for (dline, sline) in dst.chunks_mut(dstride).zip(src.chunks(sstride)) { + dline[..sstride].copy_from_slice(sline); + } +} + +fn blit_scaled(dst: &mut [u8], dstride: usize, src: &[u8], sstride: usize, scale: u8) { + let mut slines = src.chunks(sstride); + let mut acc = 0; + + let mut cur_line = slines.next().unwrap(); + + for dline in dst.chunks_mut(dstride) { + dline[..sstride].copy_from_slice(cur_line); + acc += scale; + if acc >= 100 { + acc -= 100; + if let Some(line) = slines.next() { + cur_line = line; + } else { + break; + } + } + } +} + +impl NADecoder for RobotVideoDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(vinfo) = info.get_properties() { + self.width = vinfo.get_width(); + self.height = vinfo.get_height(); + self.frame.resize(self.width * self.height, 0); + if let Some(ref edata) = info.get_extradata() { + validate!(edata.len() > 2); + self.version = edata[0]; + validate!(self.version >= 4 && self.version <= 6); + if edata[1] != 0 { + validate!(edata.len() > 39); + let pal_start = read_u16le(&edata[25+2..])? as usize; + let pal_len = read_u16le(&edata[29+2..])? as usize; + validate!(pal_len > 0 && pal_start + pal_len <= 256); + match edata[32+2] { + 0 => { + let dpal = self.pal[pal_start * 3..].chunks_exact_mut(3); + for (dst, quad) in dpal.zip(edata[37+2..].chunks_exact(4)) { + dst.copy_from_slice(&quad[1..]); + } + }, + 1 => self.pal[pal_start * 3..][..pal_len * 3].copy_from_slice(&edata[37+2..][..pal_len * 3]), + _ => return Err(DecoderError::NotImplemented), + } + } else { + for (i, entry) in self.pal.chunks_exact_mut(3).enumerate() { + entry[0] = i as u8; + entry[1] = i as u8; + entry[2] = i as u8; + } + } + } else { + return Err(DecoderError::InvalidData); + } + 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(); + + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult { + let src = pkt.get_buffer(); + validate!(src.len() > FRAME_HEADER); + + let mut mr = MemoryReader::new_read(&src); + let mut br = ByteReader::new(&mut mr); + + let ncells = br.read_u16le()? as usize; + validate!(ncells > 0 && ncells <= 10); + for el in self.frame.iter_mut() { *el = 0xFF; } + for _ in 0..ncells { + br.read_byte()?; + let scale = br.read_byte()?; + let width = br.read_u16le()? as usize; + let height = br.read_u16le()? as usize; + br.read_skip(4)?; + let xoff = br.read_u16le()? as usize; + let yoff = br.read_u16le()? as usize; + validate!(xoff + width <= self.width && yoff + height <= self.height); + let mut cell_size = br.read_u16le()? as usize; + // hack + if self.version == 6 && ncells == 1 && (src.len() - 18 >= 0x10000) { + cell_size += (src.len() - 18) & !0xFFFF; + } + if self.version > 4 { + let nchunks = br.read_u16le()? as usize; + validate!(nchunks > 0); + br.read_skip(4)?; + + unpack_cell(&mut br, cell_size, nchunks, &mut self.cell_buf, width * height)?; + } else { + br.read_skip(6)?; + self.cell_buf.resize(width * height, 0); + lzs_unpack(&mut br, cell_size, &mut self.cell_buf)?; + } + if scale == 100 { + blit(&mut self.frame[xoff + yoff * self.width..], self.width, &self.cell_buf, width); + } else { + blit_scaled(&mut self.frame[xoff + yoff * self.width..], self.width, &self.cell_buf, width, scale); + } + } + + 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(); + + blit(data, stride, &self.frame, self.width); + data[paloff..][..768].copy_from_slice(&self.pal); + + let mut frm = NAFrame::new_from_pkt(pkt, self.info.clone(), buf); + let ftype = if pkt.keyframe { FrameType::I } else { FrameType::P }; + frm.set_frame_type(ftype); + Ok(frm.into_ref()) + } + fn flush(&mut self) { + } +} + +impl NAOptionHandler for RobotVideoDecoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +pub fn get_decoder() -> Box { + Box::new(RobotVideoDecoder::new()) +} + +struct RobotAudioDecoder { + info: NACodecInfoRef, + ainfo: NAAudioInfo, + chmap: NAChannelMap, +} + +impl RobotAudioDecoder { + fn new() -> Self { + Self { + ainfo: NAAudioInfo::new(11025, 1, SND_S16_FORMAT, 1), + info: NACodecInfo::new_dummy(), + chmap: NAChannelMap::from_str("C").unwrap(), + } + } + fn pred16(pred: i32, val: u8) -> i32 { + if (val & 0x80) != 0 { + pred - i32::from(SOL_AUD_STEPS16[(val & 0x7F) as usize]) + } else { + pred + i32::from(SOL_AUD_STEPS16[(val & 0x7F) as usize]) + } + } +} + +impl NADecoder for RobotAudioDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Audio(_ainfo) = info.get_properties() { + self.info = info.replace_info(NACodecTypeInfo::Audio(self.ainfo)); + + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult { + let src = pkt.get_buffer(); + validate!(src.len() > 1); + + let samples = match src[0] { + 0 => src.len() - 1, + 1 => src.len() - 1 - 8, + _ => return Err(DecoderError::InvalidData), + }; + + let abuf = alloc_audio_buffer(self.ainfo, samples / 2, self.chmap.clone())?; + let mut adata = abuf.get_abuf_i16().unwrap(); + let dst = adata.get_data_mut().unwrap(); + + match src[0] { + 0 => { + let mut pred = 0; + for (dst, &b) in dst.iter_mut().zip(src[1..][..src.len()/2].iter()) { + pred = Self::pred16(pred, b); + *dst = pred as i16; + } + }, + 1 => { + validate!(src.len() > 8); + let mut pred = 0; + for &b in src[1..9].iter() { + pred = Self::pred16(pred, b); + } + for (dst, &b) in dst.iter_mut().zip(src[9..].iter()) { + pred = Self::pred16(pred, b); + *dst = pred as i16; + } + }, + _ => unreachable!(), + } + + let frm = NAFrame::new_from_pkt(pkt, self.info.clone(), abuf); + Ok(frm.into_ref()) + } + fn flush(&mut self) { + } +} + +impl NAOptionHandler for RobotAudioDecoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +pub fn get_decoder_audio() -> Box { + Box::new(RobotAudioDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::*; + #[test] + fn test_rbt_v4() { + 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); + + // sample from SWAT demo + test_decoding("sierra-rbt", "rbt-video", "assets/Game/sierra/12.rbt", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x2a00775d, 0xef8da06a, 0x015b6f06, 0xa22d0158], + [0xf2acb558, 0x0d9c5c54, 0x32c43af4, 0xd9776b68], + [0x386e02e9, 0x76dbd5a6, 0x4e9da3d7, 0xa47fdca3]])); + } + #[test] + fn test_rbt_v5() { + 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); + + // sample from Phantasmagora (with scaling) + test_decoding("sierra-rbt", "rbt-video", "assets/Game/sierra/162.RBT", + None, &dmx_reg, &dec_reg, + ExpectedTestResult::MD5([0x4912fa8f, 0xae201d9e, 0x59707ea0, 0xc50bf0e2])); + } + #[test] + fn test_rbt_v6() { + 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); + + // sample from Rama + test_decoding("sierra-rbt", "rbt-video", "assets/Game/sierra/7531.RBT", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x49db87f3, 0x57881095, 0x676d1600, 0x5ddaa50b], + [0xa75ff558, 0xb6815b27, 0x5f9d872f, 0xd7f56470], + [0x60bca745, 0xc47d6882, 0xc193fe70, 0x7b8738c9]])); + } + #[test] + fn test_rbt_audio() { + 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); + + // sample from Space Quest 6 + test_decoding("sierra-rbt", "rbt-audio", "assets/Game/sierra/410.rbt", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5([0xdd44e3ca, 0x6cfc1bc1, 0xdcd4214a, 0x443cf5ed])); + } +} + +const SOL_AUD_STEPS16: [i16; 128] = [ + 0x00, 0x08, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, + 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, + 0xF0, 0x100, 0x110, 0x120, 0x130, 0x140, 0x150, 0x160, + 0x170, 0x180, 0x190, 0x1A0, 0x1B0, 0x1C0, 0x1D0, 0x1E0, + 0x1F0, 0x200, 0x208, 0x210, 0x218, 0x220, 0x228, 0x230, + 0x238, 0x240, 0x248, 0x250, 0x258, 0x260, 0x268, 0x270, + 0x278, 0x280, 0x288, 0x290, 0x298, 0x2A0, 0x2A8, 0x2B0, + 0x2B8, 0x2C0, 0x2C8, 0x2D0, 0x2D8, 0x2E0, 0x2E8, 0x2F0, + 0x2F8, 0x300, 0x308, 0x310, 0x318, 0x320, 0x328, 0x330, + 0x338, 0x340, 0x348, 0x350, 0x358, 0x360, 0x368, 0x370, + 0x378, 0x380, 0x388, 0x390, 0x398, 0x3A0, 0x3A8, 0x3B0, + 0x3B8, 0x3C0, 0x3C8, 0x3D0, 0x3D8, 0x3E0, 0x3E8, 0x3F0, + 0x3F8, 0x400, 0x440, 0x480, 0x4C0, 0x500, 0x540, 0x580, + 0x5C0, 0x600, 0x640, 0x680, 0x6C0, 0x700, 0x740, 0x780, + 0x7C0, 0x800, 0x900, 0xA00, 0xB00, 0xC00, 0xD00, 0xE00, + 0xF00, 0x1000, 0x1400, 0x1800, 0x1C00, 0x2000, 0x3000, 0x4000 +]; diff --git a/nihav-game/src/codecs/seq.rs b/nihav-game/src/codecs/seq.rs new file mode 100644 index 0000000..5968423 --- /dev/null +++ b/nihav-game/src/codecs/seq.rs @@ -0,0 +1,219 @@ +use nihav_core::codecs::*; +use nihav_core::io::byteio::*; + +const WIDTH: usize = 320; +const HEIGHT: usize = 200; +const FRAME_HEADER: usize = 24; + +struct SequenceDecoder { + info: NACodecInfoRef, + pal: [u8; 768], + frame: [u8; WIDTH * HEIGHT], +} + +impl SequenceDecoder { + fn new() -> Self { + Self { + info: NACodecInfoRef::default(), + pal: [0; 768], + frame: [0; WIDTH * HEIGHT], + } + } +} + +impl NADecoder for SequenceDecoder { + fn init(&mut self, _supp: &mut NADecoderSupport, info: NACodecInfoRef) -> DecoderResult<()> { + if let NACodecTypeInfo::Video(_vinfo) = info.get_properties() { + if let Some(ref edata) = info.get_extradata() { + validate!(edata.len() > 32); + let pal_start = read_u16le(&edata[25..])? as usize; + let pal_len = read_u16le(&edata[29..])? as usize; + validate!(pal_len > 0 && pal_start + pal_len <= 256); + match edata[32] { + 0 => { + let dpal = self.pal[pal_start * 3..].chunks_exact_mut(3); + for (dst, quad) in dpal.zip(edata[37..].chunks_exact(4)) { + dst.copy_from_slice(&quad[1..]); + } + }, + 1 => self.pal[pal_start * 3..][..pal_len * 3].copy_from_slice(&edata[37..][..pal_len * 3]), + _ => return Err(DecoderError::NotImplemented), + } + } else { + return Err(DecoderError::InvalidData); + } + let myinfo = NACodecTypeInfo::Video(NAVideoInfo::new(WIDTH, HEIGHT, false, PAL8_FORMAT)); + self.info = NACodecInfo::new_ref(info.get_name(), myinfo, info.get_extradata()).into_ref(); + + Ok(()) + } else { + Err(DecoderError::InvalidData) + } + } + fn decode(&mut self, _supp: &mut NADecoderSupport, pkt: &NAPacket) -> DecoderResult { + let src = pkt.get_buffer(); + validate!(src.len() > FRAME_HEADER); + + let mut mr = MemoryReader::new_read(&src); + let mut br = ByteReader::new(&mut mr); + + let width = br.read_u16le()? as usize; + let height = br.read_u16le()? as usize; + let xoff = br.read_u16le()? as usize; + let yoff = br.read_u16le()? as usize; + validate!(width + xoff <= WIDTH && height + yoff <= HEIGHT); + + let _ckey = br.read_byte()?; + let frm_type = br.read_byte()?; + br.read_skip(6)?; + let opcode_size = br.read_u16le()? as usize; + validate!(opcode_size <= src.len() - FRAME_HEADER); + + let opcodes = &src[FRAME_HEADER..][..opcode_size]; + let clr_data = &src[FRAME_HEADER + opcode_size..]; + match frm_type { + 0 => { + validate!(opcodes.is_empty()); + validate!(clr_data.len() == width * height); + + let dst = &mut self.frame[xoff + yoff * WIDTH..]; + for (dline, sline) in dst.chunks_mut(WIDTH).zip(clr_data.chunks(width)) { + dline[..width].copy_from_slice(sline); + } + }, + 1 | 11 => { + validate!(!opcodes.is_empty()); + let mut mr = MemoryReader::new_read(opcodes); + let mut ops = ByteReader::new(&mut mr); + let mut mr = MemoryReader::new_read(clr_data); + let mut clr = ByteReader::new(&mut mr); + + let mut x = xoff; + let mut y = yoff; + while y < yoff + height { + let op = ops.read_byte()?; + let mut len = (op & 0x3F) as usize; + if len == 0 { + len = width + xoff - x; + } + match op >> 6 { + 3 => x += len, + 2 => { + clr.read_buf(&mut self.frame[x + y * WIDTH..][..len])?; + x += len; + }, + _ => { + let len = ((u16::from(op & 0x7) << 8) | u16::from(ops.read_byte()?)) as usize; + match op >> 3 { + 2 => x += len, + 3 => { + for _ in 0..len { + validate!(y < height + yoff); + self.frame[x + y * WIDTH] = clr.read_byte()?; + x += 1; + if x == width + xoff { + y += 1; + x = xoff; + } + } + }, + 6 => { + let len = if len != 0 { len } else { height + yoff - y }; + for _ in 0..(len * width) { + validate!(y < height + yoff); + self.frame[x + y * WIDTH] = clr.read_byte()?; + x += 1; + if x == width + xoff { + y += 1; + x = xoff; + } + } + }, + 7 => { + if len > 0 { + y += len; + } else { + y = height + yoff; + } + }, + _ => return Err(DecoderError::NotImplemented), + } + }, + } + if x == width + xoff { + x = xoff; + y += 1; + } + } + }, + _ => return Err(DecoderError::InvalidData), + } + + + 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_exact(WIDTH)) { + drow[..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 pkt.keyframe { FrameType::I } else { FrameType::P }; + frm.set_frame_type(ftype); + Ok(frm.into_ref()) + } + fn flush(&mut self) { + } +} + +impl NAOptionHandler for SequenceDecoder { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} + +pub fn get_decoder() -> Box { + Box::new(SequenceDecoder::new()) +} + +#[cfg(test)] +mod test { + use nihav_core::codecs::RegisteredDecoders; + use nihav_core::demuxers::RegisteredDemuxers; + use nihav_codec_support::test::dec_video::*; + use crate::*; + #[test] + fn test_seq1() { + 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); + + // sample from King's Quest VI + test_decoding("sierra-seq", "seq-video", "assets/Game/sierra/FS1.SEQ", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x5a7472da, 0xa9e242fd, 0x867efa52, 0x9625f05c], + [0x720ab982, 0x704970a0, 0xf854af8b, 0x3b86bed9], + [0xaa1426e1, 0x79750652, 0x87b7a727, 0xc582a561]])); + } + #[test] + fn test_seq2() { + 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); + + // sample from Gabriel Knight + test_decoding("sierra-seq", "seq-video", "assets/Game/sierra/blood.seq", + Some(2), &dmx_reg, &dec_reg, + ExpectedTestResult::MD5Frames(vec![ + [0x989422ee, 0x5892beae, 0x0ca9db17, 0xe25ab710], + [0x0d5f395e, 0x2eeac229, 0x1504ece0, 0xa7d3401e], + [0x988d3fa6, 0x68be4639, 0x7ab7137c, 0x72e69e26]])); + } +} diff --git a/nihav-game/src/demuxers/mod.rs b/nihav-game/src/demuxers/mod.rs index 7895afd..3e3f842 100644 --- a/nihav-game/src/demuxers/mod.rs +++ b/nihav-game/src/demuxers/mod.rs @@ -24,6 +24,10 @@ mod hl_fmv; mod imax; #[cfg(feature="demuxer_q")] mod q; +#[cfg(feature="demuxer_rbt")] +mod rbt; +#[cfg(feature="demuxer_seq")] +mod seq; #[cfg(feature="demuxer_sga")] mod sga; #[cfg(feature="demuxer_siff")] @@ -54,6 +58,10 @@ const GAME_DEMUXERS: &[&dyn DemuxerCreator] = &[ &imax::IMAXDemuxerCreator {}, #[cfg(feature="demuxer_q")] &q::QDemuxerCreator {}, +#[cfg(feature="demuxer_rbt")] + &rbt::RobotDemuxerCreator {}, +#[cfg(feature="demuxer_seq")] + &seq::SequenceDemuxerCreator {}, #[cfg(feature="demuxer_sga")] &sga::SGADemuxerCreator {}, #[cfg(feature="demuxer_siff")] diff --git a/nihav-game/src/demuxers/rbt.rs b/nihav-game/src/demuxers/rbt.rs new file mode 100644 index 0000000..9f6da60 --- /dev/null +++ b/nihav-game/src/demuxers/rbt.rs @@ -0,0 +1,283 @@ +use nihav_core::frame::*; +use nihav_core::demuxers::*; + +const AFRAME_HDR_SIZE: usize = 16; + +#[allow(dead_code)] +struct RobotDemuxer<'a> { + src: &'a mut ByteReader<'a>, + version: u16, + vpts: u64, + apts: u64, + pkt_no: usize, + audio: bool, + has_audio: bool, + initial: Vec, + a_id: Option, + v_id: Option, + nframes: usize, + vframe_len: Vec, + pkt_len: Vec, +} + +impl<'a> DemuxCore<'a> for RobotDemuxer<'a> { + #[allow(unused_variables)] + fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let mut hdr = [0; 60]; + self.src.read_buf(&mut hdr)?; + validate!(hdr[0] == 0x16 || hdr[0] == 0x3D); + validate!(&hdr[2..6] == b"SOL\0"); + self.version = read_u16le(&hdr[6..])?; + let aframe_size = read_u16le(&hdr[8..])? as usize; + validate!(self.version >= 4 && self.version <= 6); + + self.nframes = read_u16le(&hdr[14..])? as usize; + validate!(self.nframes > 0); + let pal_size = read_u16le(&hdr[16..])? as usize; + let audio_pre_size = read_u16le(&hdr[18..])? as usize; + + let mut width = read_u16le(&hdr[20..])? as usize; + if width == 0 { width = 640; } + let mut height = read_u16le(&hdr[22..])? as usize; + if height == 0 { height = 480; } + let has_pal = hdr[24] != 0; + self.has_audio = hdr[25] != 0; + let fps = read_u16le(&hdr[28..])?; + if !self.has_audio || audio_pre_size == 0 { + self.src.read_skip(audio_pre_size)?; + } else { + let end_pos = self.src.tell() + (audio_pre_size as u64); + validate!(audio_pre_size >= 12); + validate!(aframe_size > AFRAME_HDR_SIZE); + let pre_size = self.src.read_u32le()? as usize; + validate!(pre_size <= audio_pre_size - 14); + let method = self.src.read_u16le()?; + validate!(method == 0); + let size1 = self.src.read_u32le()? as usize; + let size2 = self.src.read_u32le()? as usize; + validate!(size1 + size2 <= pre_size); + let to_skip = (aframe_size - AFRAME_HDR_SIZE) / 2; + if size1 + size2 > to_skip { + self.initial.resize(size1 + size2 + 1 - to_skip, 0); + self.initial[0] = 0; + self.src.read_buf(&mut self.initial[1..])?; + } + self.src.seek(SeekFrom::Start(end_pos))?; + } + let mut pal = vec![0; pal_size + 2]; + pal[0] = self.version as u8; + pal[1] = has_pal as u8; + self.src.read_buf(&mut pal[2..])?; + self.vframe_len.clear(); + self.vframe_len.reserve(self.nframes); + if self.version < 6 { + for _ in 0..self.nframes { + let size = self.src.read_u16le()?; + self.vframe_len.push(u32::from(size)); + } + } else { + for _ in 0..self.nframes { + let size = self.src.read_u32le()?; + self.vframe_len.push(size); + } + } + + self.pkt_len.clear(); + self.pkt_len.reserve(self.nframes); + if self.version < 6 { + for _ in 0..self.nframes { + let size = self.src.read_u16le()?; + self.pkt_len.push(u32::from(size)); + } + } else { + for _ in 0..self.nframes { + let size = self.src.read_u32le()?; + self.pkt_len.push(size); + } + } + self.src.read_skip(256 * 4)?; // cues + self.src.read_skip(256 * 2)?; // smth + let pos = (self.src.tell() & 0x7FF) as usize; + if pos != 0 { + self.src.read_skip(0x800 - pos)?; + } + + let vhdr = NAVideoInfo::new(width, height, false, PAL8_FORMAT); + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("rbt-video", vci, Some(pal)); + self.v_id = strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 1, fps.into(), self.nframes as u64)); + if self.has_audio { + let ahdr = NAAudioInfo::new(11025, 2, SND_S16_FORMAT, 1); + let ainfo = NACodecInfo::new("rbt-audio", NACodecTypeInfo::Audio(ahdr), None); + self.a_id = strmgr.add_stream(NAStream::new(StreamType::Audio, 1, ainfo, 1, 22050, 2)); + } + self.apts = 0; + self.vpts = 0; + self.pkt_no = 0; + self.audio = false; + + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + if self.has_audio && !self.initial.is_empty() { + let mut buf = Vec::new(); + std::mem::swap(&mut self.initial, &mut buf); + if let Some(a_id) = self.a_id { + let stream = strmgr.get_stream(a_id).unwrap(); + let ts = stream.make_ts(Some(0), None, None); + self.apts += (buf.len() - 1) as u64; + return Ok(NAPacket::new(stream, ts, true, buf)); + } + } + loop { + if self.pkt_no >= self.nframes { + return Err(DemuxerError::EOF); + } + + if !self.audio { + let stream = strmgr.get_stream(self.v_id.unwrap_or(0)).unwrap(); + let ts = stream.make_ts(Some(self.vpts), None, None); + self.vpts += 1; + + let mut buf = vec![0; self.vframe_len[self.pkt_no] as usize]; + self.src.read_buf(&mut buf)?; + + if self.has_audio { + self.audio = true; + } else { + self.src.read_skip((self.pkt_len[self.pkt_no] - self.vframe_len[self.pkt_no]) as usize)?; + self.pkt_no += 1; + } + + return Ok(NAPacket::new(stream, ts, self.vpts == 1, buf)); + } else { + let asize = (self.pkt_len[self.pkt_no] - self.vframe_len[self.pkt_no]) as usize; + validate!(asize >= AFRAME_HDR_SIZE); + self.audio = false; + self.pkt_no += 1; + + let _ref_apts = u64::from(self.src.read_u32le()?); + let ref_asize = self.src.read_u32le()? as usize; + validate!(asize == ref_asize + 8); + + if let Some(a_id) = self.a_id { + let stream = strmgr.get_stream(a_id).unwrap(); + let ts = stream.make_ts(Some(self.apts), None, None); + self.apts += (ref_asize - 8) as u64; + let mut buf = vec![0; ref_asize + 1]; + buf[0] = 1; + self.src.read_buf(&mut buf[1..])?; + return Ok(NAPacket::new(stream, ts, true, buf)); + } else { + self.src.read_skip(ref_asize)?; + } + } + } + } + + fn seek(&mut self, _time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> { + Err(DemuxerError::NotPossible) + } + fn get_duration(&self) -> u64 { 0 } +} +impl<'a> NAOptionHandler for RobotDemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} +impl<'a> RobotDemuxer<'a> { + fn new(io: &'a mut ByteReader<'a>) -> Self { + RobotDemuxer { + src: io, + vpts: 0, + apts: 0, + a_id: None, + v_id: None, + nframes: 0, + pkt_no: 0, + audio: false, + has_audio: false, + initial: Vec::new(), + version: 0, + vframe_len: Vec::new(), + pkt_len: Vec::new(), + } + } +} + +pub struct RobotDemuxerCreator { } + +impl DemuxerCreator for RobotDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box + 'a> { + Box::new(RobotDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "sierra-rbt" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + #[test] + fn test_rbt_v4() { + // sample from SWAT demo + let mut file = File::open("assets/Game/sierra/12.rbt").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = RobotDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } + #[test] + fn test_rbt_v5() { + // sample from Space Quest 6 + let mut file = File::open("assets/Game/sierra/410.rbt").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = RobotDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } + #[test] + fn test_rbt_v6() { + // sample from Rama + let mut file = File::open("assets/Game/sierra/7531.RBT").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = RobotDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } +} diff --git a/nihav-game/src/demuxers/seq.rs b/nihav-game/src/demuxers/seq.rs new file mode 100644 index 0000000..ae5b099 --- /dev/null +++ b/nihav-game/src/demuxers/seq.rs @@ -0,0 +1,111 @@ +use nihav_core::frame::*; +use nihav_core::demuxers::*; + +struct SequenceDemuxer<'a> { + src: &'a mut ByteReader<'a>, + nframes: usize, + frame_no: usize, +} + +impl<'a> DemuxCore<'a> for SequenceDemuxer<'a> { + #[allow(unused_variables)] + fn open(&mut self, strmgr: &mut StreamManager, _seek_index: &mut SeekIndex) -> DemuxerResult<()> { + let src = &mut self.src; + + self.nframes = src.read_u16le()? as usize; + validate!(self.nframes > 0); + let pal_size = src.read_u32le()? as usize; + validate!(pal_size > 37 && pal_size <= 1024 + 37); + let mut pal_chunk = vec![0; pal_size]; + src.read_buf(&mut pal_chunk)?; + + let vhdr = NAVideoInfo::new(320, 200, false, PAL8_FORMAT); + let vci = NACodecTypeInfo::Video(vhdr); + let vinfo = NACodecInfo::new("seq-video", vci, Some(pal_chunk)); + if strmgr.add_stream(NAStream::new(StreamType::Video, 0, vinfo, 1, 10, self.nframes as u64)).is_none() { + return Err(DemuxerError::MemoryError); + } + + self.frame_no = 0; + Ok(()) + } + + fn get_frame(&mut self, strmgr: &mut StreamManager) -> DemuxerResult { + const FRAME_HEADER: usize = 24; + + if self.frame_no == self.nframes { + return Err(DemuxerError::EOF); + } + + let stream = strmgr.get_stream(0).unwrap(); + let ts = stream.make_ts(Some(self.frame_no as u64), None, None); + + let mut buf = vec![0; FRAME_HEADER]; + self.src.read_buf(&mut buf)?; + let frm_size = read_u16le(&buf[12..])? as usize; + let offset = u64::from(self.src.read_u32le()?); + validate!(offset >= self.src.tell()); + self.src.seek(SeekFrom::Start(offset))?; + buf.resize(FRAME_HEADER + frm_size, 0); + self.src.read_buf(&mut buf[FRAME_HEADER..])?; + + self.frame_no += 1; + + Ok(NAPacket::new(stream, ts, self.frame_no == 1, buf)) + } + + fn seek(&mut self, _time: NATimePoint, _seek_index: &SeekIndex) -> DemuxerResult<()> { + Err(DemuxerError::NotPossible) + } + fn get_duration(&self) -> u64 { 0 } +} +impl<'a> NAOptionHandler for SequenceDemuxer<'a> { + fn get_supported_options(&self) -> &[NAOptionDefinition] { &[] } + fn set_options(&mut self, _options: &[NAOption]) { } + fn query_option_value(&self, _name: &str) -> Option { None } +} +impl<'a> SequenceDemuxer<'a> { + fn new(io: &'a mut ByteReader<'a>) -> Self { + SequenceDemuxer { + src: io, + frame_no: 0, + nframes: 0, + } + } +} + +pub struct SequenceDemuxerCreator { } + +impl DemuxerCreator for SequenceDemuxerCreator { + fn new_demuxer<'a>(&self, br: &'a mut ByteReader<'a>) -> Box + 'a> { + Box::new(SequenceDemuxer::new(br)) + } + fn get_name(&self) -> &'static str { "sierra-seq" } +} + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + // sample from King's Quest VI + #[test] + fn test_seq() { + let mut file = File::open("assets/Game/sierra/FS1.SEQ").unwrap(); + let mut fr = FileReader::new_read(&mut file); + let mut br = ByteReader::new(&mut fr); + let mut dmx = SequenceDemuxer::new(&mut br); + let mut sm = StreamManager::new(); + let mut si = SeekIndex::new(); + dmx.open(&mut sm, &mut si).unwrap(); + loop { + let pktres = dmx.get_frame(&mut sm); + if let Err(e) = pktres { + if (e as i32) == (DemuxerError::EOF as i32) { break; } + panic!("error"); + } + let pkt = pktres.unwrap(); + println!("Got {}", pkt); + } + } +} diff --git a/nihav-registry/src/detect.rs b/nihav-registry/src/detect.rs index 87226a7..a7f521d 100644 --- a/nihav-registry/src/detect.rs +++ b/nihav-registry/src/detect.rs @@ -419,6 +419,18 @@ const DETECTORS: &[DetectConditions] = &[ extensions: ".dtv,.avc", conditions: &[], }, + DetectConditions { + demux_name: "sierra-rbt", + extensions: ".rbt", + conditions: &[CheckItem{offs: 0, cond: &CC::Eq(Arg::Byte(0x16)) }, + CheckItem{offs: 2, cond: &CC::Str(b"SOL\0")}, + CheckItem{offs: 6, cond: &CC::In(Arg::U16LE(4), Arg::U16LE(6))}], + }, + DetectConditions { + demux_name: "sierra-seq", + extensions: ".seq", + conditions: &[], + }, DetectConditions { demux_name: "vmd", extensions: ".vmd", diff --git a/nihav-registry/src/register.rs b/nihav-registry/src/register.rs index 15a3f65..b2a7ca6 100644 --- a/nihav-registry/src/register.rs +++ b/nihav-registry/src/register.rs @@ -270,6 +270,9 @@ static CODEC_REGISTER: &[CodecDescription] = &[ desc!(video; "midivid", "MidiVid"), desc!(video; "midivid3", "MidiVid 3"), desc!(video-ll; "midivid-ll", "MidiVid Lossless"), + desc!(video-ll; "rbt-video", "Sierra Robot video"), + desc!(audio; "rbt-audio", "Sierra Robot audio"), + desc!(video; "seq-video", "Sierra Sequence video"), desc!(video; "smushv1", "SMUSH Video paletted"), desc!(video; "smushv2", "SMUSH Video 16-bit"), desc!(video; "smush-iact", "SMUSH IACT Audio"),