fourdgs 0.1.0 copy "fourdgs: ^0.1.0" to clipboard
fourdgs: ^0.1.0 copied to clipboard

A decoder for the 4dgs container format: 4D gaussian splat scenes with native audio in a single seekable file. Streamed and indexed read paths, pure Dart.

example/main.dart

// Copyright 2026 Avala AI
// SPDX-License-Identifier: Apache-2.0

/// Reads a `.4dgs` file two ways and prints what each path answers.
///
/// ```
/// dart run example/main.dart scene.4dgs
/// dart run example/main.dart scene.4dgs 1.5
/// ```
///
/// The two paths exist because they cost different things. An indexed open
/// reads the head and the summary at the tail, learns where everything is, and
/// then transfers only the byte ranges an instant needs; on a 500 MiB scene
/// that is the difference between reading 500 MiB and reading a few hundred
/// kilobytes. A front-to-back read decodes the file from the first byte to the
/// last and hands back every gaussian, and it is the path that still answers
/// when the file was cut short.
///
/// Front to back does not mean incremental here. Every entry point on that path
/// — `readFourdgsBytes`, `decodeKeyframeDeltaStreamed`,
/// `decodeKeyframeDeltaIndexed` — takes a `Uint8List`, so the second half of
/// this example holds the whole file in memory. Nothing in the format requires
/// that: the walk never seeks backwards. But this package ships no `Stream`
/// decoder, and an example that mimed one would be describing a library that
/// does not exist. The way to not pay for the file is the first half.
library;

import 'dart:io';
import 'dart:typed_data';

import 'package:fourdgs/fourdgs.dart';
import 'package:fourdgs/io.dart';

Future<void> main(List<String> args) async {
  if (args.isEmpty) {
    stderr.writeln('usage: dart run example/main.dart <file.4dgs> [seconds]');
    exitCode = 64; // EX_USAGE
    return;
  }
  final path = args.first;
  final t = args.length > 1 ? double.tryParse(args[1]) : 0.0;
  if (t == null || !t.isFinite) {
    stderr.writeln('4dgs: not a finite number of seconds: ${args[1]}');
    exitCode = 64; // EX_USAGE
    return;
  }

  // The two passes are attempted independently, and that is the point rather
  // than tidiness. An indexed open needs the Footer and the summary block at
  // the tail, so a truncated file fails it — and a truncated file is precisely
  // the one the front-to-back reader can still recover a prefix of. Giving up
  // on the first refusal would leave the recovery below unreachable in the only
  // case it exists for.
  final indexed = await _indexed(path, t);
  // A transport failure is shared by both paths. Do not print it twice; both
  // helpers still catch their own I/O because the file can disappear between
  // these independent passes.
  final wholeFile =
      indexed == _ReadResult.inputError
          ? _ReadResult.inputError
          : await _wholeFile(path, t);
  if (indexed == _ReadResult.inputError ||
      wholeFile == _ReadResult.inputError) {
    exitCode = 66; // EX_NOINPUT
  } else if (indexed != _ReadResult.ok || wholeFile != _ReadResult.ok) {
    exitCode = 65; // EX_DATAERR
  }
}

enum _ReadResult { ok, dataError, inputError }

/// Opens the file by byte range and answers `t` out of the index.
///
/// Returns whether the pass succeeded, refused the bytes, or could not open
/// the input.
Future<_ReadResult> _indexed(String path, double t) async {
  // `openFourdgsIndexed` reads the head and the summary at the tail. It does
  // not touch a chunk, so this is the same cost for a 5 MiB file and a 5 GiB
  // one, and it is the right way to answer "what is in here".
  FourdgsFileReadable? source;
  bool printed = false;
  try {
    source = await FourdgsFileReadable.open(path);
    final scene = await openFourdgsIndexed(source);
    final header = scene.header;
    printed = true;
    stdout
      ..writeln('profile        ${header.profile}')
      ..writeln('written by     ${header.library}')
      ..writeln('duration       ${header.durationSec} s')
      ..writeln('gaussians      ${header.gaussianCount}')
      ..writeln('temporal model ${header.temporalModel}')
      ..writeln('SH degree      ${header.shDegree}')
      ..writeln('has audio      ${header.hasAudio}')
      ..writeln('chunks         ${scene.index.length}')
      ..writeln('size           ${scene.resourceBytes} bytes');

    if (header.temporalModel == 'keyframe-delta') {
      _priceKeyframeDeltaSeek(scene, t);
    } else {
      await _readCoveringChunks(source, scene, t);
    }
    return _ReadResult.ok;
  } on FourdgsException catch (e) {
    // Every refusal says which byte, which record, which value, and what was
    // expected — print it as it comes rather than replacing it with "could not
    // read file", which throws away the only useful part.
    stderr.writeln('4dgs: indexed: $e');
    return _ReadResult.dataError;
  } on FileSystemException catch (e) {
    // Existence checks are a time-of-check/time-of-use race and do not cover
    // permissions. The operation that actually failed is the diagnosis.
    stderr.writeln('4dgs: indexed input: $e');
    return _ReadResult.inputError;
  } finally {
    // Separating the two passes, and only when the first one had something to
    // separate: a file that fails to open has printed nothing, and a stray
    // blank first line is a thing a reader has to stop and explain to itself.
    if (printed) stdout.writeln('');
    await source?.close();
  }
}

/// The `gaussian-birth` seek: fetch and decode the chunks covering `t`.
Future<void> _readCoveringChunks(
  FourdgsReadable source,
  FourdgsIndexedScene scene,
  double t,
) async {
  // The seek rule is `t0 <= t < t1`, half-open, so an instant selects exactly
  // the chunks that cover it. `bytesForTime` prices the seek before it happens,
  // which is what lets a caller decide whether to make it.
  final covering = scene.chunksForTime(t);
  stdout
    ..writeln('')
    ..writeln(
      'at t = $t s: ${covering.length} chunk(s), '
      '${scene.bytesForTime(t)} bytes to transfer',
    );

  for (final entry in covering) {
    final chunk = await readFourdgsChunk(source, scene, entry);
    stdout.writeln(
      '  [${entry.t0}, ${entry.t1})  ${chunk.count} gaussians decoded',
    );
  }
}

/// The `keyframe-delta` seek, priced from the index and not yet paid.
void _priceKeyframeDeltaSeek(FourdgsIndexedScene scene, double t) {
  // Under this model a chunk is not self-contained. The entry covering `t` may
  // be a Delta Chunk — a patch on the chunk it references — so `readFourdgsChunk`
  // would refuse it: that function reads a Chunk record and a delta is a
  // different opcode. Reaching `t` means composing the entry's whole reference
  // chain back to a keyframe, and `chainFrom` reads that chain out of the index
  // alone, before a byte of chunk data is fetched.
  //
  // Which also makes `bytesForTime` the wrong number here: it prices the
  // covering chunk, and the seek pays for the chain.
  final covering = scene.chunksForTime(t);
  if (covering.isEmpty) {
    stdout
      ..writeln('')
      ..writeln('at t = $t s: no chunk covers this instant');
    return;
  }
  final entry = covering.first;
  final chain = chainFrom(scene.index, entry);
  final chainBytes = chain.fold<int>(
    0,
    (int sum, FourdgsChunkIndexEntry e) => sum + e.chunkLength,
  );
  stdout
    ..writeln('')
    ..writeln(
      'at t = $t s: [${entry.t0}, ${entry.t1}), '
      '${entry.kind == 0 ? 'a keyframe' : 'a delta'} at depth ${entry.depth}',
    )
    ..writeln(
      '  chain: 1 keyframe + ${chain.length - 1} delta(s), '
      '$chainBytes bytes to transfer',
    )
    ..writeln('  the index claims ${entry.liveCount} gaussians live here');
}

/// Decodes the file front to back, with the whole of it in memory.
///
/// Returns whether the pass succeeded, refused the bytes, or could not open
/// the input.
Future<_ReadResult> _wholeFile(String path, double t) async {
  try {
    final bytes = await File(path).readAsBytes();
    // `recoverTruncated` returns what arrived before a cut instead of throwing,
    // with `truncated` set. That is the recovery the indexed open above cannot
    // offer: it needs a tail, and a cut file has none.
    final scene = readFourdgsBytes(bytes, recoverTruncated: true);
    final cut = scene.truncated ? ', file was truncated' : '';

    if (scene.header.temporalModel != 'keyframe-delta') {
      stdout.writeln('streamed: ${scene.gaussians.count} gaussians$cut');
      return _ReadResult.ok;
    }

    // A keyframe-delta file has its own readers, and this is why. Its chunks
    // are keyframes and patches rather than populations, so a total taken over
    // them counts operations — births, deaths and updates — and not gaussians;
    // the generic reader above skips the Delta Chunks it does not model, and
    // its `gaussians` are the keyframes' alone. `decodeKeyframeDeltaStreamed`
    // composes each chunk onto the state it references, which is the number a
    // caller actually wants.
    // The generic walk above established whether the tail is present. The
    // keyframe-delta decoder has no recovery flag, but it is deliberately
    // front-to-back and needs no Footer: give it exactly the complete records
    // the generic walk recovered, never the half-record after them.
    final sequence = decodeKeyframeDeltaStreamed(
      scene.truncated ? _completeRecordPrefix(bytes) : bytes,
    );
    stdout.writeln('streamed: ${sequence.chunks.length} state chunk(s)$cut');

    // A complete file can prove that the seeking decoder reaches the same
    // state. A prefix cannot be reopened through the index: doing so would
    // demand the very Footer it lacks and turn successful recovery back into a
    // refusal, so answer from the front-to-back composition already in hand.
    final composed =
        scene.truncated ? sequence : decodeKeyframeDeltaIndexed(bytes).sequence;
    for (final chunk in composed.chunks) {
      if (chunk.t0 <= t && t < chunk.t1) {
        stdout.writeln(
          'composed at t = $t s: ${chunk.state.count} gaussians, '
          '${scene.truncated ? 'from the streamed prefix' : 'by walking that chain'}',
        );
        break;
      }
    }
    return _ReadResult.ok;
  } on FourdgsException catch (e) {
    // The same treatment the indexed pass gives a refusal. Without it a file
    // that opens cleanly and then names an unsupported codec in some chunk the
    // instant never touched leaves the CLI with an unhandled stack trace and an
    // exit status that means "crashed", not "bad data".
    stderr.writeln('4dgs: streamed: $e');
    return _ReadResult.dataError;
  } on FileSystemException catch (e) {
    stderr.writeln('4dgs: streamed input: $e');
    return _ReadResult.inputError;
  }
}

/// The bytes through the last complete framed record in [bytes].
///
/// A cut can land in content, in the nine-byte record header, or exactly on a
/// record boundary. [scanRecordSpans] handles all three without allocating a
/// record-sized buffer. The returned view retains the input (which this
/// example already owns) and adds no second file-sized allocation.
Uint8List _completeRecordPrefix(Uint8List bytes) {
  int end = fourdgsMagic.length;
  for (final span in scanRecordSpans(bytes, fourdgsMagic.length)) {
    if (span.end > bytes.length) break;
    end = span.end;
  }
  return Uint8List.sublistView(bytes, 0, end);
}
1
likes
160
points
--
downloads

Documentation

Documentation
API reference

Publisher

verified publisheravala.ai

A decoder for the 4dgs container format: 4D gaussian splat scenes with native audio in a single seekable file. Streamed and indexed read paths, pure Dart.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#gaussian-splatting #graphics #codec #binary-format

License

Apache-2.0 (license)

Dependencies

archive

More

Packages that depend on fourdgs