use cbvault_format::cbh::GameAnnotations;
use cbvault_format::cbh::moves::GameMoves;
use cbvault_format::error::{Error, Result};
use cbvault_format::file::DbFile;
use cbvault_format::game::RecordKind;
use rayon::prelude::*;
use super::walk::{self, GameBuf, game_ref};
use super::{Database, GameSink};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ConvertStats {
pub records: u64,
pub games: u64,
pub skipped: u64,
pub plies: u64,
pub keys: u64,
pub failures: u64,
pub complete: bool,
}
pub(crate) fn move_record<'a>(moves: &'a DbFile, at: u64, scratch: &'a mut Vec<u8>) -> Result<&'a [u8]> {
let head = match moves.slice_at(at, 4) {
Some(head) => [head[0], head[1], head[2], head[3]],
None => {
scratch.clear();
scratch.resize(4, 0);
moves.read_into(at, scratch)?;
[scratch[0], scratch[1], scratch[2], scratch[3]]
}
};
let size = u32::from_be_bytes([0, head[1], head[2], head[3]]) as usize;
if size < 4 {
return Err(Error::corrupt(moves.path(), at, format!("move record size {size} is smaller than its head")));
}
if let Some(slice) = moves.slice_at(at, size) {
return Ok(slice);
}
scratch.clear();
scratch.resize(size, 0);
moves.read_into(at, scratch)?;
Ok(scratch)
}
struct Walker<'db> {
db: &'db Database,
buf: GameBuf,
record: Vec<u8>,
want_annotations: bool,
}
impl<'db> Walker<'db> {
fn new(db: &'db Database, sink: &impl GameSink) -> Walker<'db> {
let mut buf = GameBuf::with_capacity(walk::DEFAULT_PLY_ROOM);
buf.set_wants(sink.wants_keys(), sink.wants_annotations());
Walker { db, buf, record: Vec::new(), want_annotations: sink.wants_annotations() }
}
fn one(&mut self, id: u32, at: u64, sink: &mut impl GameSink) -> Result<bool> {
let record = match move_record(self.db.moves()?, at, &mut self.record) {
Ok(record) => record,
Err(e) => return self.failed(id, e, sink),
};
let game = match GameMoves::parse(&self.db.members().moves, record) {
Ok(game) => game,
Err(e) => return self.failed(id, e, sink),
};
if let Err(e) = self.buf.walk(id, at, &game) {
return self.failed(id, e, sink);
}
let header = match self.db.header_ref(id) {
Ok(header) => header,
Err(e) => return self.failed(id, e, sink),
};
walk::resolve_names(&header, self.db.entities(), &mut self.buf)?;
let view = match self.buf.view(id, header, self.db, self.want_annotations) {
Ok(view) => view,
Err(_) => self.buf.view(id, header, self.db, false)?,
};
sink.game(view);
Ok(true)
}
fn failed(&mut self, id: u32, error: Error, sink: &mut impl GameSink) -> Result<bool> {
sink.failed(id, &error);
Ok(false)
}
}
pub(crate) fn annotations_of<'a>(
db: &'a Database,
header: &cbvault_format::cbh::GameHeaderRef<'_>,
scratch: &'a mut Vec<u8>,
) -> Result<Option<GameAnnotations<'a>>> {
let file = db.annotations()?;
Ok(Some(file.of_ref(header, db.wide(), scratch)?))
}
pub fn for_each_game(db: &Database, sink: &mut impl GameSink) -> Result<ConvertStats> {
for_each_range(db, 1, db.records(), sink)
}
pub fn for_each_range(db: &Database, first: u32, last: u32, sink: &mut impl GameSink) -> Result<ConvertStats> {
let mut stats = ConvertStats { complete: true, ..ConvertStats::default() };
let mut walker = Walker::new(db, sink);
for_each_record(db, first, last.min(db.records()), sink, &mut walker, &mut stats)?;
Ok(stats)
}
fn for_each_record(
db: &Database,
first: u32,
last: u32,
sink: &mut impl GameSink,
walker: &mut Walker<'_>,
stats: &mut ConvertStats,
) -> Result<()> {
for id in first..=last {
if sink.cancelled() {
stats.complete = false;
return Ok(());
}
stats.records += 1;
let header = match db.header_ref(id) {
Ok(header) => header,
Err(e) => {
sink.failed(id, &e);
stats.failures += 1;
continue;
}
};
if !matches!(header.kind(), RecordKind::Game) || header.is_deleted() {
stats.skipped += 1;
continue;
}
let at = match db.move_offset(&header) {
Ok(at) => at,
Err(e) => {
sink.failed(id, &e);
stats.failures += 1;
continue;
}
};
if walker.one(id, at, sink)? {
stats.games += 1;
stats.plies += walker.buf.moves().len() as u64;
stats.keys += walker.buf.keys().len() as u64;
} else {
stats.failures += 1;
}
}
Ok(())
}
pub const DEFAULT_BATCH: u32 = 8192;
const NO_ERROR: u32 = u32::MAX;
impl Default for Row {
fn default() -> Row {
Row { game: false, err: NO_ERROR, moves: (0, 0), keys: (0, 0), fen: (0, 0), annotated: false }
}
}
#[derive(Clone, Copy, Debug)]
struct Row {
moves: (u32, u32),
keys: (u32, u32),
fen: (u32, u32),
game: bool,
err: u32,
annotated: bool,
}
#[derive(Debug)]
struct Chunk {
first: u32,
headers: Vec<u8>,
moves: Vec<u16>,
keys: Vec<u64>,
fens: String,
rows: Vec<Row>,
errors: Vec<Error>,
stats: ConvertStats,
buf: GameBuf,
record: Vec<u8>,
}
impl Default for Chunk {
fn default() -> Chunk {
Chunk {
first: 0,
headers: Vec::new(),
moves: Vec::new(),
keys: Vec::new(),
fens: String::new(),
rows: Vec::new(),
errors: Vec::new(),
stats: ConvertStats::default(),
buf: GameBuf::new(),
record: Vec::new(),
}
}
}
impl Chunk {
fn new(batch: u32, keys: bool) -> Chunk {
let mut chunk = Chunk {
first: 0,
headers: Vec::with_capacity(batch as usize * 46),
moves: Vec::new(),
keys: Vec::new(),
fens: String::new(),
rows: Vec::with_capacity(batch as usize),
errors: Vec::new(),
stats: ConvertStats::default(),
buf: GameBuf::with_capacity(walk::DEFAULT_PLY_ROOM),
record: Vec::new(),
};
chunk.buf.set_wants(keys, false);
if keys {
chunk.keys.reserve(batch as usize * 8);
chunk.moves.reserve(batch as usize * 32);
}
chunk
}
fn reset(&mut self) {
self.headers.clear();
self.moves.clear();
self.keys.clear();
self.fens.clear();
self.rows.clear();
self.errors.clear();
self.stats = ConvertStats::default();
self.buf.clear();
}
fn decode(&mut self, db: &Database, first: u32, last: u32, want_annotations: bool) {
self.reset();
self.first = first;
let moves = match db.moves() {
Ok(moves) => moves,
Err(e) => return self.fail_all(first, last, e),
};
for id in first..=last {
self.stats.records += 1;
self.headers.resize(self.headers.len() + 46, 0);
match self.one(db, moves, id, want_annotations) {
Ok(row) => {
if row.err == NO_ERROR && row.game {
self.stats.games += 1;
self.stats.plies += row.moves.1 as u64;
self.stats.keys += row.keys.1 as u64;
}
self.rows.push(row);
}
Err(e) => {
let err = self.push_error(e);
self.rows.push(Row { err, ..Row::default() });
self.stats.failures += 1;
}
}
}
}
fn one(&mut self, db: &Database, moves: &DbFile, id: u32, want_annotations: bool) -> Result<Row> {
let header = db.header_ref(id)?;
if !matches!(header.kind(), RecordKind::Game) || header.is_deleted() {
return Ok(Row::default());
}
let at = db.move_offset(&header)?;
let record = move_record(moves, at, &mut self.record)?;
let game = GameMoves::parse(&db.members().moves, record)?;
self.buf.walk(id, at, &game)?;
let mut row = Row { game: true, ..Row::default() };
row.moves.0 = self.moves.len() as u32;
self.moves.extend_from_slice(self.buf.moves());
row.moves.1 = self.moves.len() as u32 - row.moves.0;
row.keys.0 = self.keys.len() as u32;
self.keys.extend_from_slice(self.buf.keys());
row.keys.1 = self.keys.len() as u32 - row.keys.0;
if let Some(fen) = self.buf.start_fen() {
row.fen.0 = self.fens.len() as u32;
self.fens.push_str(fen);
row.fen.1 = self.fens.len() as u32 - row.fen.0;
}
row.annotated = want_annotations && header.annotations_offset() != 0;
let slot = self.headers.len() - 46;
if let Some(bytes) = self.headers.get_mut(slot..slot + 46) {
bytes.copy_from_slice(header.bytes());
}
Ok(row)
}
fn fail_all(&mut self, first: u32, last: u32, error: Error) {
self.first = first;
for _ in first..=last {
self.stats.records += 1;
self.stats.failures += 1;
let err = self.push_error(error_clone(&error));
self.rows.push(Row { err, ..Row::default() });
}
}
fn push_error(&mut self, error: Error) -> u32 {
self.errors.push(error);
(self.errors.len() - 1) as u32
}
}
fn error_clone(error: &Error) -> Error {
Error::corrupt(error.path().unwrap_or(std::path::Path::new("<database>")), 0, error.to_string())
}
pub fn convert_parallel(db: &Database, sink: &mut impl GameSink, threads: usize, batch: u32) -> Result<ConvertStats> {
if threads <= 1 {
return for_each_game(db, sink);
}
let batch = if batch == 0 { DEFAULT_BATCH } else { batch };
let total = db.records();
let ranges = chunks(total, batch);
let keys = sink.wants_keys();
let want_annotations = sink.wants_annotations();
let mut pool: Vec<Chunk> = (0..threads.min(ranges.len().max(1))).map(|_| Chunk::new(batch, keys)).collect();
let mut writer = Writer {
db,
buf: GameBuf::with_capacity(walk::DEFAULT_PLY_ROOM),
scratch: Vec::new(),
stats: ConvertStats { complete: true, ..ConvertStats::default() },
};
let pool_threads = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.map_err(|e| Error::corrupt(db.base(), 0, format!("thread pool creation failed: {e}")))?;
for wave in ranges.chunks(pool.len().max(1)) {
if sink.cancelled() {
writer.stats.complete = false;
return Ok(writer.stats);
}
let jobs: Vec<(&mut Chunk, (u32, u32))> = pool.iter_mut().zip(wave.iter().copied()).collect();
pool_threads.install(|| {
jobs.into_par_iter().for_each(|(chunk, (first, last))| {
chunk.decode(db, first, last, want_annotations);
})
});
for chunk in &pool[..wave.len()] {
writer.deliver(sink, chunk, want_annotations)?;
}
}
Ok(writer.stats)
}
fn chunks(total: u32, batch: u32) -> Vec<(u32, u32)> {
let mut out = Vec::new();
let mut first = 1u32;
while first <= total {
let last = first.saturating_add(batch - 1).min(total);
out.push((first, last));
first = last + 1;
}
out
}
struct Writer<'db> {
db: &'db Database,
buf: GameBuf,
scratch: Vec<u8>,
stats: ConvertStats,
}
impl Writer<'_> {
fn deliver(&mut self, sink: &mut impl GameSink, chunk: &Chunk, want_annotations: bool) -> Result<()> {
for (index, row) in chunk.rows.iter().enumerate() {
let id = chunk.first + index as u32;
self.stats.records += 1;
if row.err != NO_ERROR {
let error = &chunk.errors[row.err as usize];
sink.failed(id, error);
self.stats.failures += 1;
continue;
}
if !row.game {
self.stats.skipped += 1;
continue;
}
let Some(header) = header_of(chunk, index) else {
sink.failed(id, &Error::corrupt(self.db.base(), 0, "the chunk has no header for the record"));
self.stats.failures += 1;
continue;
};
walk::resolve_names(&header, self.db.entities(), &mut self.buf)?;
let annotations = if row.annotated && want_annotations {
annotations_of(self.db, &header, &mut self.scratch).ok().flatten()
} else {
None
};
let moves = span(&chunk.moves, row.moves);
let keys = span(&chunk.keys, row.keys);
let fen = span_str(&chunk.fens, row.fen);
let game = game_ref(id, header, self.buf.names(), fen, moves, keys, annotations);
sink.game(game);
self.stats.games += 1;
self.stats.plies += moves.len() as u64;
self.stats.keys += keys.len() as u64;
}
Ok(())
}
}
fn header_of(chunk: &Chunk, index: usize) -> Option<cbvault_format::cbh::GameHeaderRef<'_>> {
let at = index * 46;
let bytes: &[u8; 46] = chunk.headers.get(at..at + 46)?.try_into().ok()?;
Some(cbvault_format::cbh::GameHeaderRef::from_bytes(chunk.first + index as u32, bytes))
}
fn span<T>(haystack: &[T], (at, len): (u32, u32)) -> &[T] {
let at = at as usize;
let end = at.saturating_add(len as usize);
haystack.get(at..end).unwrap_or(&[])
}
fn span_str(text: &str, span: (u32, u32)) -> Option<&str> {
let at = span.0 as usize;
let end = at.saturating_add(span.1 as usize);
(span.1 != 0).then(|| &text[at..end])
}