2025-10-28 15:38:00 -07:00
use std ::io ::Write ;
2025-10-30 11:17:20 -07:00
use crate ::{
InvalidDeterminant ,
clock ::{ self , Timer } ,
statestream ,
} ;
2025-10-24 16:02:56 -07:00
use thiserror ::Error ;
2025-10-27 09:13:07 -07:00
// #[repr(usize)]
// pub enum HeaderV0V1Part {
// Magic = 0,
// Version = 4,
// CRC = 8,
// StateSize = 12,
// Identifier = 16,
// HeaderLen = 24,
// }
// #[repr(usize)]
// pub enum HeaderV2Part {
// FrameCount = 24,
// BlockSize = 28,
// SuperblockSize = 32,
// CheckpointConfig = 36,
// HeaderLen = 40,
// }
// const HEADER_V0V1_LEN_BYTES: usize = HeaderV0V1Part::HeaderLen as usize;
2025-10-28 15:38:00 -07:00
const HEADERV2_LEN_BYTES : usize = 40 ;
2025-10-24 13:53:37 -07:00
2025-10-27 09:13:07 -07:00
// const VERSION: u32 = 2;
const MAGIC : u32 = 0x4253_5632 ;
2025-10-24 13:53:37 -07:00
#[ repr(u8) ]
#[ non_exhaustive ]
#[ derive(Debug) ]
pub enum FrameToken {
Invalid = 0 ,
Regular = b 'f' ,
Checkpoint = b 'c' ,
Checkpoint2 = b 'C' ,
}
2025-10-24 16:02:56 -07:00
impl From < u8 > for FrameToken {
fn from ( value : u8 ) -> Self {
match value {
b 'f' = > FrameToken ::Regular ,
b 'c' = > FrameToken ::Checkpoint ,
b 'C' = > FrameToken ::Checkpoint2 ,
_ = > FrameToken ::Invalid ,
}
}
}
2025-10-28 15:38:00 -07:00
impl From < FrameToken > for u8 {
fn from ( value : FrameToken ) -> Self {
match value {
FrameToken ::Invalid = > 0 ,
FrameToken ::Regular = > b 'f' ,
FrameToken ::Checkpoint = > b 'c' ,
FrameToken ::Checkpoint2 = > b 'C' ,
}
}
}
2025-10-24 13:53:37 -07:00
#[ repr(u8) ]
#[ non_exhaustive ]
2025-10-24 16:02:56 -07:00
#[ derive(Debug, PartialEq, Eq, Clone, Copy) ]
2025-10-24 13:53:37 -07:00
pub enum Compression {
None = 0 ,
Zlib = 1 ,
Zstd = 2 ,
}
2025-10-24 16:02:56 -07:00
impl TryFrom < u8 > for Compression {
type Error = InvalidDeterminant ;
fn try_from ( value : u8 ) -> std ::result ::Result < Self , Self ::Error > {
match value {
0 = > Ok ( Compression ::None ) ,
1 = > Ok ( Compression ::Zlib ) ,
2 = > Ok ( Compression ::Zstd ) ,
_ = > Err ( InvalidDeterminant ( value ) ) ,
}
}
}
2025-10-24 13:53:37 -07:00
2025-10-28 15:38:00 -07:00
impl From < Compression > for u8 {
fn from ( value : Compression ) -> Self {
match value {
Compression ::None = > 0 ,
Compression ::Zlib = > 1 ,
Compression ::Zstd = > 2 ,
}
}
}
2025-10-24 13:53:37 -07:00
#[ repr(u8) ]
#[ non_exhaustive ]
2025-10-24 16:02:56 -07:00
#[ derive(Debug, PartialEq, Eq, Clone, Copy) ]
2025-10-24 13:53:37 -07:00
pub enum Encoding {
Raw = 0 ,
Statestream = 1 ,
}
2025-10-24 16:02:56 -07:00
impl TryFrom < u8 > for Encoding {
type Error = InvalidDeterminant ;
fn try_from ( value : u8 ) -> std ::result ::Result < Self , Self ::Error > {
match value {
0 = > Ok ( Encoding ::Raw ) ,
1 = > Ok ( Encoding ::Statestream ) ,
_ = > Err ( InvalidDeterminant ( value ) ) ,
}
}
}
2025-10-24 13:53:37 -07:00
2025-10-28 15:38:00 -07:00
impl From < Encoding > for u8 {
fn from ( value : Encoding ) -> Self {
match value {
Encoding ::Raw = > 0 ,
Encoding ::Statestream = > 1 ,
}
}
}
#[ derive(Debug, Clone) ]
2025-10-24 13:53:37 -07:00
pub struct HeaderBase {
pub version : u32 ,
pub content_crc : u32 ,
pub initial_state_size : u32 ,
pub identifier : u64 ,
}
2025-10-28 15:38:00 -07:00
#[ derive(Debug, Clone) ]
2025-10-24 13:53:37 -07:00
pub struct HeaderV2 {
pub base : HeaderBase ,
pub frame_count : u32 ,
pub block_size : u32 ,
pub superblock_size : u32 ,
pub checkpoint_commit_interval : u8 ,
pub checkpoint_commit_threshold : u8 ,
pub checkpoint_compression : Compression ,
}
2025-10-28 15:38:00 -07:00
#[ derive(Debug, Clone) ]
2025-10-24 13:53:37 -07:00
pub enum Header {
V0V1 ( HeaderBase ) ,
V2 ( HeaderV2 ) ,
}
#[ derive(Error, Debug) ]
pub enum ReplayError {
#[ error( " Invalid replay magic {0} " ) ]
Magic ( u32 ) ,
#[ error( " Unsupported version {0} " ) ]
Version ( u32 ) ,
#[ error( " Unsupported compression scheme {0} " ) ]
2025-10-27 09:13:07 -07:00
Compression ( InvalidDeterminant ) ,
#[ error( " Unsupported encoding scheme {0} " ) ]
Encoding ( InvalidDeterminant ) ,
2025-10-24 13:53:37 -07:00
#[ error( " I/O Error " ) ]
IO ( #[ from ] std ::io ::Error ) ,
2025-10-29 12:37:33 -07:00
#[ error( " Too many frames to {0} fit framecount header " ) ]
TooManyFrames ( std ::num ::TryFromIntError ) ,
2025-10-24 16:02:56 -07:00
#[ error( " Coreless frame read for version 0 not possible " ) ]
NoCoreRead ( ) ,
2025-10-27 09:13:07 -07:00
#[ error( " Checkpoint too big {0} " ) ]
CheckpointTooBig ( std ::num ::TryFromIntError ) ,
2025-10-28 15:38:00 -07:00
#[ error( " Frame too long {0} " ) ]
FrameTooLong ( std ::num ::TryFromIntError ) ,
#[ error( " Frame has too many key events {0} " ) ]
TooManyKeyEvents ( std ::num ::TryFromIntError ) ,
#[ error( " Frame has too many input events {0} " ) ]
TooManyInputEvents ( std ::num ::TryFromIntError ) ,
2025-10-24 16:02:56 -07:00
#[ error( " Invalid frame token {0} " ) ]
BadFrameToken ( u8 ) ,
2025-10-24 13:53:37 -07:00
}
type Result < T > = std ::result ::Result < T , ReplayError > ;
2025-10-31 13:23:51 -07:00
pub struct ReplayDecoder < R : std ::io ::BufRead > {
rply : R ,
2025-10-27 09:13:07 -07:00
pub header : Header ,
pub initial_state : Vec < u8 > ,
2025-10-28 10:59:47 -07:00
pub frame_number : u64 ,
2025-10-27 14:38:10 -07:00
ss_state : statestream ::Ctx ,
2025-10-27 09:13:07 -07:00
}
2025-10-31 13:23:51 -07:00
impl < R : std ::io ::BufRead > ReplayDecoder < R > {
2025-10-27 14:38:10 -07:00
/// Creates a [`ReplayDecoder`] for the given buffered readable stream.
///
/// # Errors
/// [`ReplayError::IO`]: Some issue with the read stream, e.g. insufficient length or unexpected end
/// [`ReplayError::Magic`]: Invalid magic number at beginning of file
/// [`ReplayError::Version`]: Version identifier not recognized by parser
/// [`ReplayError::Compression`]: Unsupported compression scheme for checkpoints
2025-10-31 13:23:51 -07:00
pub fn new ( mut rply : R ) -> Result < ReplayDecoder < R > > {
2025-10-27 14:38:10 -07:00
use byteorder ::{ LittleEndian , ReadBytesExt } ;
let magic = rply . read_u32 ::< LittleEndian > ( ) ? ;
if magic ! = MAGIC {
return Err ( ReplayError ::Magic ( magic ) ) ;
}
let version = rply . read_u32 ::< LittleEndian > ( ) ? ;
if version > 2 {
return Err ( ReplayError ::Version ( version ) ) ;
}
let content_crc = rply . read_u32 ::< LittleEndian > ( ) ? ;
let initial_state_size = rply . read_u32 ::< LittleEndian > ( ) ? ;
let identifier = rply . read_u64 ::< LittleEndian > ( ) ? ;
let base = HeaderBase {
version ,
content_crc ,
initial_state_size ,
identifier ,
} ;
let mut initial_state = vec! [ 0 ; initial_state_size as usize ] ;
if version < 2 {
rply . read_exact ( initial_state . as_mut_slice ( ) ) ? ;
return Ok ( ReplayDecoder {
header : Header ::V0V1 ( base ) ,
rply ,
initial_state ,
frame_number : 0 ,
ss_state : statestream ::Ctx ::new ( 1 , 1 ) ,
} ) ;
}
let frame_count = rply . read_u32 ::< LittleEndian > ( ) ? ;
let block_size = rply . read_u32 ::< LittleEndian > ( ) ? ;
let superblock_size = rply . read_u32 ::< LittleEndian > ( ) ? ;
let cp_config = rply . read_u32 ::< LittleEndian > ( ) ? ;
let checkpoint_commit_interval = ( cp_config > > 24 ) as u8 ;
let checkpoint_commit_threshold = ( ( cp_config > > 16 ) & 0xFF ) as u8 ;
let checkpoint_compression = Compression ::try_from ( ( ( cp_config > > 8 ) & 0xFF ) as u8 )
. map_err ( ReplayError ::Compression ) ? ;
2025-10-28 09:47:02 -07:00
let mut replay = ReplayDecoder {
2025-10-27 14:38:10 -07:00
rply ,
initial_state ,
header : Header ::V2 ( HeaderV2 {
base ,
frame_count ,
block_size ,
superblock_size ,
checkpoint_commit_interval ,
checkpoint_commit_threshold ,
checkpoint_compression ,
} ) ,
frame_number : 0 ,
ss_state : statestream ::Ctx ::new ( block_size , superblock_size ) ,
2025-10-28 09:47:02 -07:00
} ;
2025-10-31 13:23:51 -07:00
replay . decode_initial_checkpoint ( ) ? ;
2025-10-28 09:47:02 -07:00
Ok ( replay )
2025-10-27 14:38:10 -07:00
}
2025-10-31 13:23:51 -07:00
pub fn inner ( & mut self ) -> & mut R {
& mut self . rply
}
/// Reads keyboard event records at the current input position. Only really appropriate to explicitly call for v0 replays.
2025-10-27 09:13:07 -07:00
/// # Errors
/// [`ReplayError::IO`]: Unexpected end of stream or other I/O error
2025-10-31 13:23:51 -07:00
pub fn read_key_events ( & mut self , frame : & mut Frame ) -> Result < ( ) > {
2025-10-27 09:13:07 -07:00
use byteorder ::{ LittleEndian , ReadBytesExt } ;
2025-10-31 13:23:51 -07:00
let rply = & mut self . rply ;
2025-10-27 09:13:07 -07:00
let key_count = rply . read_u8 ( ) ? as usize ;
frame . key_events . resize_with ( key_count , Default ::default ) ;
for ki in 0 .. key_count {
/*
down, padding, mod_x2, code_x4, char_x4
*/
let down = rply . read_u8 ( ) ? ;
let _ = rply . read_u8 ( ) ? ; // padding
let modf = rply . read_u16 ::< LittleEndian > ( ) ? ;
let code = rply . read_u32 ::< LittleEndian > ( ) ? ;
let chr = rply . read_u32 ::< LittleEndian > ( ) ? ;
let key_data = KeyData {
down ,
/* buf[1] is padding */
modf ,
code ,
chr ,
} ;
frame . key_events [ ki ] = key_data ;
}
2025-10-31 13:23:51 -07:00
Ok ( ( ) )
}
/// Reads an end of frame marker at the current input position. Only really appropriate to explicitly call for v0 replays.
/// # Errors
/// [`ReplayError::IO`]: Unexpected end of stream or other I/O error
/// [`ReplayError::Compression`]: Unsupported compression scheme
/// [`ReplayError::Encoding`]: Unsupported encoding scheme
/// [`ReplayError::BadFrameToken`]: Frame token not recognized or misaligned
/// [`ReplayError::CheckpointTooBig`]: Tried to read a checkpoint bigger than the address space
pub fn read_end_of_frame ( & mut self , frame : & mut Frame ) -> Result < ( ) > {
use byteorder ::{ LittleEndian , ReadBytesExt } ;
let rply = & mut self . rply ;
let tok = rply . read_u8 ( ) ? ;
match FrameToken ::from ( tok ) {
FrameToken ::Regular = > {
frame . checkpoint_compression = Compression ::None ;
frame . checkpoint_encoding = Encoding ::Raw ;
frame . checkpoint_bytes . clear ( ) ;
}
FrameToken ::Checkpoint = > {
frame . checkpoint_compression = Compression ::None ;
frame . checkpoint_encoding = Encoding ::Raw ;
let cp_size = usize ::try_from ( rply . read_u64 ::< LittleEndian > ( ) ? )
. map_err ( ReplayError ::CheckpointTooBig ) ? ;
frame . checkpoint_bytes . resize ( cp_size , 0 ) ;
rply . read_exact ( frame . checkpoint_bytes . as_mut_slice ( ) ) ? ;
}
FrameToken ::Checkpoint2 = > {
self . decode_checkpoint ( & mut frame . checkpoint_bytes ) ? ;
}
_ = > return Err ( ReplayError ::BadFrameToken ( tok ) ) ,
}
Ok ( ( ) )
}
/// Reads a single button value at the current input position. Only appropriate for v0 replays and only if you are implementing an input callback for a core.
/// # Errors
/// [`ReplayError::IO`]: Unexpected end of stream or other I/O error
pub fn read_v0_button ( & mut self ) -> Result < i16 > {
use byteorder ::{ LittleEndian , ReadBytesExt } ;
self . rply
. read_i16 ::< LittleEndian > ( )
. map_err ( ReplayError ::IO )
}
/// Reads a single frame at the current decoder position.
/// # Errors
/// [`ReplayError::IO`]: Unexpected end of stream or other I/O error
/// [`ReplayError::Compression`]: Unsupported compression scheme
/// [`ReplayError::Encoding`]: Unsupported encoding scheme
/// [`ReplayError::BadFrameToken`]: Frame token not recognized or misaligned
/// [`ReplayError::NoCoreRead`]: Tried to read a frame on a version 0 replay without a loaded core
/// [`ReplayError::CheckpointTooBig`]: Tried to read a checkpoint bigger than the address space
#[ allow(clippy::too_many_lines) ]
pub fn read_frame ( & mut self , frame : & mut Frame ) -> Result < ( ) > {
use byteorder ::{ LittleEndian , ReadBytesExt } ;
let stopwatch = clock ::time ( Timer ::DecodeFrame ) ;
let vsn = self . header . version ( ) ;
if vsn = = 0 {
return Err ( ReplayError ::NoCoreRead ( ) ) ;
}
if vsn > 1 {
/* skip over the backref */
let _ = self . rply . read_u32 ::< LittleEndian > ( ) ? ;
}
self . read_key_events ( frame ) ? ;
let rply = & mut self . rply ;
2025-10-27 09:13:07 -07:00
let input_count = rply . read_u16 ::< LittleEndian > ( ) ? as usize ;
frame
. input_events
. resize_with ( input_count , Default ::default ) ;
for ii in 0 .. input_count {
/* port, device, idx, padding, id_x2, value_x2 */
let port = rply . read_u8 ( ) ? ;
let device = rply . read_u8 ( ) ? ;
let idx = rply . read_u8 ( ) ? ;
let _ = rply . read_u8 ( ) ? ;
let id = rply . read_u16 ::< LittleEndian > ( ) ? ;
let val = rply . read_i16 ::< LittleEndian > ( ) ? ;
let inp_data = InputData {
port ,
device ,
idx ,
id ,
val ,
} ;
frame . input_events [ ii ] = inp_data ;
}
2025-10-31 13:23:51 -07:00
self . read_end_of_frame ( frame ) ? ;
2025-10-28 10:59:47 -07:00
self . frame_number + = 1 ;
2025-10-30 11:17:20 -07:00
drop ( stopwatch ) ;
2025-10-27 14:38:10 -07:00
Ok ( ( ) )
}
2025-10-28 09:47:02 -07:00
fn decode_initial_checkpoint ( & mut self ) -> Result < ( ) > {
let mut initial_state = std ::mem ::take ( & mut self . initial_state ) ;
self . decode_checkpoint ( & mut initial_state ) ? ;
self . initial_state = initial_state ;
Ok ( ( ) )
}
fn decode_checkpoint ( & mut self , checkpoint_bytes : & mut Vec < u8 > ) -> Result < ( ) > {
2025-10-27 14:38:10 -07:00
use byteorder ::{ LittleEndian , ReadBytesExt } ;
2025-10-30 11:17:20 -07:00
let stopwatch = clock ::time ( Timer ::DecodeCheckpoint ) ;
2025-10-31 13:23:51 -07:00
let rply = & mut self . rply ;
2025-10-27 14:38:10 -07:00
// read a 1 byte compression code
let compression =
Compression ::try_from ( rply . read_u8 ( ) ? ) . map_err ( ReplayError ::Compression ) ? ;
// read a 1 byte encoding code
let encoding = Encoding ::try_from ( rply . read_u8 ( ) ? ) . map_err ( ReplayError ::Encoding ) ? ;
// read a 4 byte uncompressed unencoded size
let uc_ue_size = rply . read_u32 ::< LittleEndian > ( ) ? as usize ;
// read a 4 byte uncompressed encoded size
2025-10-28 09:40:49 -07:00
#[ expect(unused) ]
2025-10-27 14:38:10 -07:00
let uc_enc_size = rply . read_u32 ::< LittleEndian > ( ) ? as usize ;
// read a 4 byte compressed encoded size
2025-10-28 09:40:49 -07:00
#[ expect(unused) ]
2025-10-27 14:38:10 -07:00
let comp_enc_size = rply . read_u32 ::< LittleEndian > ( ) ? as usize ;
2025-10-28 09:47:02 -07:00
checkpoint_bytes . resize ( uc_ue_size , 0 ) ;
2025-10-27 14:38:10 -07:00
// maybe decompress
match ( compression , encoding ) {
( Compression ::None , Encoding ::Raw ) = > {
2025-10-28 09:47:02 -07:00
rply . read_exact ( checkpoint_bytes . as_mut_slice ( ) ) ? ;
2025-10-27 14:38:10 -07:00
}
( Compression ::None , Encoding ::Statestream ) = > {
2025-10-28 09:40:49 -07:00
let mut ss_decoder =
statestream ::Decoder ::new ( rply , & mut self . ss_state , uc_ue_size ) ;
2025-10-27 14:38:10 -07:00
std ::io ::copy (
& mut ss_decoder ,
2025-10-28 09:47:02 -07:00
& mut std ::io ::Cursor ::new ( checkpoint_bytes . as_mut_slice ( ) ) ,
2025-10-27 14:38:10 -07:00
) ? ;
}
( Compression ::Zlib , Encoding ::Raw ) = > {
use flate2 ::bufread ::ZlibDecoder ;
let mut decoder = ZlibDecoder ::new ( rply ) ;
std ::io ::copy (
& mut decoder ,
2025-10-28 09:47:02 -07:00
& mut std ::io ::Cursor ::new ( checkpoint_bytes . as_mut_slice ( ) ) ,
2025-10-27 14:38:10 -07:00
) ? ;
}
( Compression ::Zlib , Encoding ::Statestream ) = > {
use flate2 ::bufread ::ZlibDecoder ;
let mut decoder = ZlibDecoder ::new ( rply ) ;
2025-10-28 09:40:49 -07:00
let mut ss_decoder =
statestream ::Decoder ::new ( & mut decoder , & mut self . ss_state , uc_ue_size ) ;
2025-10-27 14:38:10 -07:00
std ::io ::copy (
& mut ss_decoder ,
2025-10-28 09:47:02 -07:00
& mut std ::io ::Cursor ::new ( checkpoint_bytes . as_mut_slice ( ) ) ,
2025-10-27 14:38:10 -07:00
) ? ;
}
( Compression ::Zstd , Encoding ::Raw ) = > {
use zstd ::Decoder ;
let mut decoder = Decoder ::with_buffer ( rply ) ? . single_frame ( ) ;
std ::io ::copy (
& mut decoder ,
2025-10-28 09:47:02 -07:00
& mut std ::io ::Cursor ::new ( checkpoint_bytes . as_mut_slice ( ) ) ,
2025-10-27 14:38:10 -07:00
) ? ;
}
( Compression ::Zstd , Encoding ::Statestream ) = > {
use zstd ::Decoder ;
let mut decoder = Decoder ::with_buffer ( rply ) ? . single_frame ( ) ;
2025-10-28 09:40:49 -07:00
let mut ss_decoder =
statestream ::Decoder ::new ( & mut decoder , & mut self . ss_state , uc_ue_size ) ;
2025-10-27 14:38:10 -07:00
std ::io ::copy (
& mut ss_decoder ,
2025-10-28 09:47:02 -07:00
& mut std ::io ::Cursor ::new ( checkpoint_bytes . as_mut_slice ( ) ) ,
2025-10-27 14:38:10 -07:00
) ? ;
2025-10-27 09:13:07 -07:00
}
}
2025-10-30 11:17:20 -07:00
drop ( stopwatch ) ;
2025-10-27 09:13:07 -07:00
Ok ( ( ) )
}
}
/// Creates a [`ReplayDecoder`] for the given buffered readable stream.
///
/// # Errors
2025-10-27 14:38:10 -07:00
/// See [`ReplayDecoder::new`].
2025-10-31 13:23:51 -07:00
pub fn decode < R : std ::io ::BufRead > ( rply : R ) -> Result < ReplayDecoder < R > > {
2025-10-27 14:38:10 -07:00
ReplayDecoder ::new ( rply )
2025-10-24 13:53:37 -07:00
}
2025-10-27 14:38:10 -07:00
2025-10-28 15:38:00 -07:00
pub struct ReplayEncoder < ' a , W : std ::io ::Write + std ::io ::Seek > {
rply : & ' a mut W ,
pub header : Header ,
pub frame_number : u64 ,
last_pos : u64 ,
ss_state : statestream ::Ctx ,
finished : bool ,
}
impl < ' w , W : std ::io ::Write + std ::io ::Seek > ReplayEncoder < ' w , W > {
/// Creates a [`ReplayEncoder`] for the given writable and seekable stream.
///
/// # Errors
/// [`ReplayError::IO`]: Some issue with the write stream, e.g. unexpected end
/// [`ReplayError::Version`]: Version identifier not supported by writer
/// [`ReplayError::Compression`]: Unsupported compression scheme for checkpoints
pub fn new < ' s > (
header : Header ,
initial_state : & ' s [ u8 ] ,
rply : & ' w mut W ,
) -> Result < ReplayEncoder < ' w , W > > {
if header . version ( ) ! = 2 {
return Err ( ReplayError ::Version ( header . version ( ) ) ) ;
}
let ss_state = statestream ::Ctx ::new ( header . block_size ( ) , header . superblock_size ( ) ) ;
let mut replay = ReplayEncoder {
rply ,
header ,
frame_number : 0 ,
last_pos : 0 ,
ss_state ,
finished : false ,
} ;
replay . write_header ( ) ? ;
if ! initial_state . is_empty ( ) {
replay . encode_initial_checkpoint ( initial_state ) ? ;
}
replay . last_pos = replay . rply . stream_position ( ) ? ;
Ok ( replay )
}
fn write_header ( & mut self ) -> Result < ( ) > {
use byteorder ::{ LittleEndian , WriteBytesExt } ;
self . header
. set_frame_count ( u32 ::try_from ( self . frame_number ) . unwrap_or_default ( ) ) ;
let old_pos = self . rply . stream_position ( ) ? ;
self . rply . seek ( std ::io ::SeekFrom ::Start ( 0 ) ) ? ;
self . rply . write_u32 ::< LittleEndian > ( MAGIC ) ? ;
self . rply . write_u32 ::< LittleEndian > ( 2 ) ? ;
self . rply
. write_u32 ::< LittleEndian > ( self . header . content_crc ( ) ) ? ;
// state size
self . rply
. write_u32 ::< LittleEndian > ( self . header . initial_state_size ( ) ) ? ;
self . rply
. write_u64 ::< LittleEndian > ( self . header . identifier ( ) ) ? ;
2025-10-29 12:37:33 -07:00
self . rply . write_u32 ::< LittleEndian > (
u32 ::try_from ( self . header . frame_count ( ) . unwrap ( ) )
. map_err ( ReplayError ::TooManyFrames ) ? ,
) ? ;
2025-10-28 15:38:00 -07:00
self . rply
. write_u32 ::< LittleEndian > ( self . header . block_size ( ) ) ? ;
self . rply
. write_u32 ::< LittleEndian > ( self . header . superblock_size ( ) ) ? ;
let cp_interval = u32 ::from ( self . header . checkpoint_commit_interval ( ) ) ;
let cp_threshold = u32 ::from ( self . header . checkpoint_commit_threshold ( ) ) ;
let cp_compression = u32 ::from ( u8 ::from ( self . header . checkpoint_compression ( ) ) ) ;
self . rply . write_u32 ::< LittleEndian > (
( cp_interval < < 24 ) | ( cp_threshold < < 16 ) | ( cp_compression < < 8 ) ,
) ? ;
self . rply . seek ( std ::io ::SeekFrom ::Start ( old_pos ) ) ? ;
Ok ( ( ) )
}
fn encode_checkpoint ( & mut self , checkpoint : & [ u8 ] , frame : u64 ) -> Result < ( ) > {
use byteorder ::{ LittleEndian , WriteBytesExt } ;
2025-10-30 11:17:20 -07:00
let stopwatch = clock ::time ( Timer ::EncodeCheckpoint ) ;
2025-10-28 15:38:00 -07:00
let compression = self . header . checkpoint_compression ( ) ;
let encoding = Encoding ::Statestream ;
self . rply . write_u8 ( u8 ::from ( compression ) ) ? ;
self . rply . write_u8 ( u8 ::from ( encoding ) ) ? ;
// write unencoded uncompressed size
let full_size = u32 ::try_from ( checkpoint . len ( ) ) . map_err ( ReplayError ::CheckpointTooBig ) ? ;
self . rply . write_u32 ::< LittleEndian > ( full_size ) ? ;
let size_pos = self . rply . stream_position ( ) ? ;
// can't yet write encoded uncompressed size, just write zeros for now
// write encoded compressed size
self . rply . write_u32 ::< LittleEndian > ( 0 ) ? ;
// write encoded compressed bytes
self . rply . write_u32 ::< LittleEndian > ( 0 ) ? ;
let ( encoded_size , compressed_size ) = match ( compression , encoding ) {
( Compression ::None , Encoding ::Raw ) = > {
self . rply . write_all ( checkpoint ) ? ;
( full_size , full_size )
}
( Compression ::None , Encoding ::Statestream ) = > {
let encoder = statestream ::Encoder ::new ( & mut self . rply , & mut self . ss_state ) ;
let encoded_size = encoder . encode_checkpoint ( checkpoint , frame ) ? ;
( encoded_size , encoded_size )
}
( Compression ::Zlib , Encoding ::Raw ) = > {
use flate2 ::write ::ZlibEncoder ;
let here_pos = self . rply . stream_position ( ) ? ;
let mut encoder = ZlibEncoder ::new ( & mut self . rply , flate2 ::Compression ::default ( ) ) ;
let encoded_size = full_size ;
encoder . write_all ( checkpoint ) ? ;
encoder . finish ( ) ? ;
let compressed_size = u32 ::try_from ( self . rply . stream_position ( ) ? - here_pos )
. map_err ( ReplayError ::CheckpointTooBig ) ? ;
( encoded_size , compressed_size )
}
( Compression ::Zlib , Encoding ::Statestream ) = > {
use flate2 ::write ::ZlibEncoder ;
let here_pos = self . rply . stream_position ( ) ? ;
let mut compressor =
ZlibEncoder ::new ( & mut self . rply , flate2 ::Compression ::default ( ) ) ;
let encoder = statestream ::Encoder ::new ( & mut compressor , & mut self . ss_state ) ;
let encoded_size = encoder . encode_checkpoint ( checkpoint , frame ) ? ;
compressor . finish ( ) ? ;
let compressed_size = u32 ::try_from ( self . rply . stream_position ( ) ? - here_pos )
. map_err ( ReplayError ::CheckpointTooBig ) ? ;
( encoded_size , compressed_size )
}
( Compression ::Zstd , Encoding ::Raw ) = > {
let here_pos = self . rply . stream_position ( ) ? ;
let mut encoder = zstd ::Encoder ::new ( & mut self . rply , 16 ) ? ;
encoder . write_all ( checkpoint ) ? ;
encoder . finish ( ) ? ;
let encoded_size = full_size ;
let compressed_size = u32 ::try_from ( self . rply . stream_position ( ) ? - here_pos )
. map_err ( ReplayError ::CheckpointTooBig ) ? ;
( encoded_size , compressed_size )
}
( Compression ::Zstd , Encoding ::Statestream ) = > {
let here_pos = self . rply . stream_position ( ) ? ;
let mut compressor = zstd ::Encoder ::new ( & mut self . rply , 16 ) ? ;
let encoder = statestream ::Encoder ::new ( & mut compressor , & mut self . ss_state ) ;
let encoded_size = encoder . encode_checkpoint ( checkpoint , frame ) ? ;
compressor . finish ( ) ? ;
let compressed_size = u32 ::try_from ( self . rply . stream_position ( ) ? - here_pos )
. map_err ( ReplayError ::CheckpointTooBig ) ? ;
( encoded_size , compressed_size )
}
} ;
let end_pos = self . rply . stream_position ( ) ? ;
self . rply . seek ( std ::io ::SeekFrom ::Start ( size_pos ) ) ? ;
// write encoded compressed size
self . rply . write_u32 ::< LittleEndian > ( encoded_size ) ? ;
// write encoded compressed bytes
self . rply . write_u32 ::< LittleEndian > ( compressed_size ) ? ;
self . rply . seek ( std ::io ::SeekFrom ::Start ( end_pos ) ) ? ;
2025-10-30 11:17:20 -07:00
drop ( stopwatch ) ;
2025-10-28 15:38:00 -07:00
Ok ( ( ) )
}
fn encode_initial_checkpoint ( & mut self , checkpoint : & [ u8 ] ) -> Result < ( ) > {
self . rply
. seek ( std ::io ::SeekFrom ::Start ( HEADERV2_LEN_BYTES as u64 ) ) ? ;
self . encode_checkpoint ( checkpoint , 0 ) ? ;
2025-10-29 12:37:33 -07:00
let encoded_size = self . rply . stream_position ( ) ? - HEADERV2_LEN_BYTES as u64 ;
2025-10-29 11:31:28 -07:00
self . header . set_initial_state_size (
2025-10-29 12:37:33 -07:00
u32 ::try_from ( encoded_size ) . map_err ( ReplayError ::CheckpointTooBig ) ? ,
2025-10-29 11:31:28 -07:00
) ;
2025-10-28 15:38:00 -07:00
// Have to rewrite header to account for initial state size
self . write_header ( ) ? ;
self . last_pos = self . rply . stream_position ( ) ? ;
Ok ( ( ) )
}
2025-10-29 11:31:28 -07:00
2025-10-28 15:38:00 -07:00
/// Writes a single frame at the current encoder position.
2025-10-29 11:31:28 -07:00
/// # Errors
/// [`ReplayError::FrameTooLong`]: Frame encoded to more than 2^32 bytes, backrefs invalid
/// [`ReplayError::TooManyKeyEvents`]: More key events than allowed by spec
/// [`ReplayError::TooManyInputEvents`]: More input events than allowed by spec
/// [`ReplayError::CheckpointTooBig`]: Checkpoint data takes up more than 2^32 bytes
2025-10-28 15:38:00 -07:00
pub fn write_frame ( & mut self , frame : & Frame ) -> Result < ( ) > {
use byteorder ::{ LittleEndian , WriteBytesExt } ;
2025-10-30 11:17:20 -07:00
let stopwatch = clock ::time ( Timer ::EncodeFrame ) ;
2025-10-28 15:38:00 -07:00
let start_pos = self . rply . stream_position ( ) ? ;
self . rply . write_u32 ::< LittleEndian > (
u32 ::try_from ( start_pos - self . last_pos ) . map_err ( ReplayError ::FrameTooLong ) ? ,
) ? ;
self . rply . write_u8 (
u8 ::try_from ( frame . key_events . len ( ) ) . map_err ( ReplayError ::TooManyKeyEvents ) ? ,
) ? ;
for evt in & frame . key_events {
self . rply . write_u8 ( evt . down ) ? ;
self . rply . write_u8 ( 0 ) ? ; // padding
self . rply . write_u16 ::< LittleEndian > ( evt . modf ) ? ;
self . rply . write_u32 ::< LittleEndian > ( evt . code ) ? ;
self . rply . write_u32 ::< LittleEndian > ( evt . chr ) ? ;
}
self . rply . write_u16 ::< LittleEndian > (
u16 ::try_from ( frame . input_events . len ( ) ) . map_err ( ReplayError ::TooManyInputEvents ) ? ,
) ? ;
for evt in & frame . input_events {
self . rply . write_u8 ( evt . port ) ? ;
self . rply . write_u8 ( evt . device ) ? ;
self . rply . write_u8 ( evt . idx ) ? ;
self . rply . write_u8 ( 0 ) ? ; // padding
self . rply . write_u16 ::< LittleEndian > ( evt . id ) ? ;
self . rply . write_i16 ::< LittleEndian > ( evt . val ) ? ;
}
if frame . checkpoint_bytes . is_empty ( ) {
self . rply . write_u8 ( u8 ::from ( FrameToken ::Regular ) ) ? ;
} else {
self . rply . write_u8 ( u8 ::from ( FrameToken ::Checkpoint2 ) ) ? ;
self . encode_checkpoint ( & frame . checkpoint_bytes , self . frame_number ) ? ;
}
self . frame_number + = 1 ;
self . last_pos = start_pos ;
2025-10-30 11:17:20 -07:00
drop ( stopwatch ) ;
2025-10-28 15:38:00 -07:00
Ok ( ( ) )
}
/// Finishes the encoding, writing the header in the process
2025-10-29 11:31:28 -07:00
/// # Errors
/// [`ReplayError::IO`]: Underlying writer fails to write header
2025-10-28 15:38:00 -07:00
pub fn finish ( & mut self ) -> Result < ( ) > {
if self . finished {
return Ok ( ( ) ) ;
}
self . write_header ( ) ? ;
self . finished = true ;
Ok ( ( ) )
}
}
impl < W : std ::io ::Write + std ::io ::Seek > Drop for ReplayEncoder < '_ , W > {
fn drop ( & mut self ) {
self . finish ( ) . unwrap ( ) ;
}
}
/// Creates a [`ReplayEncoder`] for the given writable & seekable stream.
///
/// # Errors
/// See [`ReplayEncoder::new`].
2025-10-29 11:31:28 -07:00
pub fn encode < ' w , W : std ::io ::Write + std ::io ::Seek > (
2025-10-28 15:38:00 -07:00
header : Header ,
2025-10-29 11:31:28 -07:00
initial_state : & [ u8 ] ,
2025-10-28 15:38:00 -07:00
rply : & ' w mut W ,
) -> Result < ReplayEncoder < ' w , W > > {
ReplayEncoder ::new ( header , initial_state , rply )
}
2025-10-24 16:02:56 -07:00
impl Header {
2025-10-28 15:38:00 -07:00
fn base ( & self ) -> & HeaderBase {
match self {
Header ::V0V1 ( header_base ) = > header_base ,
Header ::V2 ( header_v2 ) = > & header_v2 . base ,
}
}
fn base_mut ( & mut self ) -> & mut HeaderBase {
match self {
Header ::V0V1 ( header_base ) = > header_base ,
Header ::V2 ( header_v2 ) = > & mut header_v2 . base ,
}
}
2025-10-27 09:13:07 -07:00
#[ must_use ]
2025-10-24 16:02:56 -07:00
pub fn version ( & self ) -> u32 {
2025-10-28 15:38:00 -07:00
self . base ( ) . version
}
#[ must_use ]
pub fn content_crc ( & self ) -> u32 {
self . base ( ) . content_crc
}
pub fn set_content_crc ( & mut self , crc : u32 ) {
self . base_mut ( ) . content_crc = crc ;
}
#[ must_use ]
pub fn identifier ( & self ) -> u64 {
self . base ( ) . identifier
}
pub fn set_identifier ( & mut self , id : u64 ) {
self . base_mut ( ) . identifier = id ;
}
#[ must_use ]
pub fn initial_state_size ( & self ) -> u32 {
self . base ( ) . initial_state_size
}
pub fn set_initial_state_size ( & mut self , sz : u32 ) {
self . base_mut ( ) . initial_state_size = sz ;
2025-10-24 16:02:56 -07:00
}
2025-10-28 10:59:47 -07:00
#[ must_use ]
pub fn frame_count ( & self ) -> Option < u64 > {
match self {
Header ::V0V1 ( _ ) = > None ,
Header ::V2 ( header_v2 ) = > Some ( u64 ::from ( header_v2 . frame_count ) ) ,
}
}
2025-10-28 15:38:00 -07:00
pub fn set_frame_count ( & mut self , frames : u32 ) {
self . upgrade ( ) . frame_count = frames ;
}
pub fn upgrade ( & mut self ) -> & mut HeaderV2 {
if let Header ::V0V1 ( base ) = self {
* self = Header ::V2 ( HeaderV2 {
base : base . clone ( ) ,
frame_count : 0 ,
2025-10-31 13:23:51 -07:00
block_size : 256 ,
superblock_size : 256 ,
checkpoint_commit_interval : 8 ,
checkpoint_commit_threshold : 4 ,
2025-10-28 15:38:00 -07:00
checkpoint_compression : Compression ::None ,
} ) ;
}
let Header ::V2 ( v2 ) = self else { unreachable! ( ) } ;
2025-10-31 13:23:51 -07:00
v2 . base . version = 2 ;
2025-10-28 15:38:00 -07:00
v2
}
#[ must_use ]
pub fn block_size ( & self ) -> u32 {
match self {
Header ::V0V1 ( _ ) = > 0 ,
Header ::V2 ( header_v2 ) = > header_v2 . block_size ,
}
}
pub fn set_block_size ( & mut self , sz : u32 ) {
let v2 = self . upgrade ( ) ;
v2 . block_size = sz ;
}
#[ must_use ]
pub fn superblock_size ( & self ) -> u32 {
match self {
Header ::V0V1 ( _ ) = > 0 ,
Header ::V2 ( header_v2 ) = > header_v2 . superblock_size ,
}
}
pub fn set_superblock_size ( & mut self , sz : u32 ) {
let v2 = self . upgrade ( ) ;
v2 . superblock_size = sz ;
}
#[ must_use ]
pub fn checkpoint_commit_interval ( & self ) -> u8 {
match self {
Header ::V0V1 ( _ ) = > 0 ,
Header ::V2 ( header_v2 ) = > header_v2 . checkpoint_commit_interval ,
}
}
#[ must_use ]
pub fn checkpoint_commit_threshold ( & self ) -> u8 {
match self {
Header ::V0V1 ( _ ) = > 0 ,
Header ::V2 ( header_v2 ) = > header_v2 . checkpoint_commit_threshold ,
}
}
pub fn set_checkpoint_commit_settings ( & mut self , interval : u8 , threshold : u8 ) {
let v2 = self . upgrade ( ) ;
v2 . checkpoint_commit_interval = interval ;
v2 . checkpoint_commit_threshold = threshold ;
}
#[ must_use ]
pub fn checkpoint_compression ( & self ) -> Compression {
match self {
Header ::V0V1 ( _ ) = > Compression ::None ,
Header ::V2 ( header_v2 ) = > header_v2 . checkpoint_compression ,
}
}
pub fn set_checkpoint_compression ( & mut self , compression : Compression ) {
let v2 = self . upgrade ( ) ;
v2 . checkpoint_compression = compression ;
}
2025-10-24 16:02:56 -07:00
}
#[ derive(Debug, Default) ]
pub struct KeyData {
pub down : u8 ,
pub modf : u16 ,
pub code : u32 ,
pub chr : u32 ,
}
#[ derive(Debug, Default) ]
pub struct InputData {
pub port : u8 ,
pub device : u8 ,
pub idx : u8 ,
pub id : u16 ,
pub val : i16 ,
}
#[ derive(Debug) ]
pub struct Frame {
pub key_events : Vec < KeyData > ,
pub input_events : Vec < InputData > ,
2025-10-28 10:59:47 -07:00
pub checkpoint_bytes : Vec < u8 > ,
2025-10-24 16:02:56 -07:00
pub checkpoint_compression : Compression ,
pub checkpoint_encoding : Encoding ,
}
2025-10-28 10:59:47 -07:00
impl Frame {
#[ must_use ]
pub fn inputs ( & self ) -> String {
use std ::fmt ::Write ;
let mut output = String ::new ( ) ;
for i in 0 .. self . input_events . len ( ) {
let evt = & self . input_events [ i ] ;
write! ( output , " {:03}:{:016b} " , evt . id , evt . val ) . unwrap ( ) ;
if i + 1 < self . input_events . len ( ) {
write! ( output , " -- " ) . unwrap ( ) ;
}
}
output
}
2025-10-30 13:44:50 -07:00
pub fn drop_checkpoint ( & mut self ) {
self . checkpoint_bytes . clear ( ) ;
self . checkpoint_compression = Compression ::None ;
self . checkpoint_encoding = Encoding ::Raw ;
}
2025-10-31 13:23:51 -07:00
pub fn clear ( & mut self ) {
self . key_events . clear ( ) ;
self . input_events . clear ( ) ;
self . drop_checkpoint ( ) ;
}
2025-10-28 10:59:47 -07:00
}
2025-10-24 16:02:56 -07:00
impl Default for Frame {
fn default ( ) -> Self {
Self {
2025-10-27 09:13:07 -07:00
key_events : Vec ::default ( ) ,
input_events : Vec ::default ( ) ,
2025-10-27 14:38:10 -07:00
checkpoint_bytes : Vec ::default ( ) ,
2025-10-24 16:02:56 -07:00
checkpoint_compression : Compression ::None ,
checkpoint_encoding : Encoding ::Raw ,
}
}
}