writeFourdgsToSink function
void
writeFourdgsToSink(
- Sink<
List< sink,int> > - FourdgsGaussianSet gaussians,
- double durationSec, {
- FourdgsWriteOptions options = const FourdgsWriteOptions(),
Encode gaussians to sink, one complete framed record at a time.
The sink is borrowed and is not closed. The writer retains the quantized
gaussian lanes and the small Chunk Index, but releases each Chunk and SH Band
record after Sink.add returns; memory does not grow with the output file.
A sink that performs I/O belongs in a transport package at the application
edge, while tests and browsers can supply any other Sink<List<int>>.
Implementation
void writeFourdgsToSink(
Sink<List<int>> sink,
FourdgsGaussianSet gaussians,
double durationSec, {
FourdgsWriteOptions options = const FourdgsWriteOptions(),
}) {
if (durationSec.isNaN || durationSec < 0.0) {
throw FourdgsInvalidInput(
'duration_sec is $durationSec; expected a value >= 0, or +Infinity for '
'an open-ended scene (a static asset says so with 0)',
);
}
if (!_profiles.containsKey(options.profile)) {
throw FourdgsInvalidInput(
'unknown quantization profile "${options.profile}"; '
'the profiles are ${_profiles.keys.toList()..sort()}',
);
}
if (options.codec != codecDeflate) {
throw FourdgsUnsupportedCodec(
'stream codec ${options.codec} is not available to a pure-Dart build; '
'write deflate, which every reader implements',
);
}
_checkOptions(options);
// A profile is a promise about what the file contains, made so a consumer can
// reject an unsuitable file up front rather than discovering the absence
// mid-decode. `objects` promises an `object_id` stream in every non-empty
// chunk and exactly one Object Table (registry, Profiles).
if (options.sceneProfile == 'objects') {
if (gaussians.count > 0 && gaussians.objectId == null) {
throw FourdgsInvalidInput(
'the objects profile requires an object_id stream in every non-empty '
'chunk, but the GaussianSet carries none',
);
}
if (options.objects?.table == null) {
throw FourdgsInvalidInput(
'the objects profile requires one ObjectTable record, but none was '
'supplied',
);
}
}
if (options.sceneProfile == 'relightable') {
throw FourdgsInvalidInput(
'the scene profile "relightable" is reserved for a future relighting '
'extension, and a version-1 writer MUST NOT emit it',
);
}
if (options.sceneProfile == 'keyframed') {
throw const FourdgsInvalidInput(
'the scene profile "keyframed" promises a keyframe-delta temporal model '
'with indexed state chunks and Statistics, while this writer emits the '
'gaussian-birth model; use the sequence writer or leave the profile empty',
);
}
if (options.sceneProfile == 'capture') {
throw const FourdgsInvalidInput(
'the scene profile "capture" promises finite windows, multiple indexed '
'chunks, and Statistics; this reference writer does not yet enforce all '
'three promises, so leave the scene profile empty',
);
}
if (options.sceneProfile != '' &&
options.sceneProfile != 'baked' &&
options.sceneProfile != 'objects') {
throw FourdgsInvalidInput(
'unknown scene profile "${options.sceneProfile}"; the registered '
'profiles are "", baked, capture, keyframed, objects, relightable, and '
'this writer can emit "", baked, or objects',
);
}
_checkInput(gaussians, options.cutoff);
final objects = options.objects;
int? objectTableRecordBytes;
if (objects != null) {
try {
_checkHeaderRecordSize(options);
objects.check();
final table = objects.table;
if (table != null) {
objectTableRecordBytes = _checkObjectTableRecordSize(table);
table.check();
}
for (final track in objects.tracks) {
// The readers enforce this before allocating their sample arrays. Do
// the same before `check()` walks the caller's arrays and before the
// record writer reserves 64 bytes per declared sample, so this writer
// cannot emit a file its own SDK refuses to reopen.
if (track.sampleCount > maxTrajectorySamples) {
throw FourdgsInvalidInput(
'ObjectTrack for object ${track.objectId} declares '
'${track.sampleCount} samples, past the '
'$maxTrajectorySamples ceiling',
);
}
}
} on FourdgsMalformedFile catch (error) {
// These validators are shared with the parser, where malformed bytes are
// the right diagnosis. Here the same values are authoring arguments.
throw FourdgsInvalidInput(error.message);
}
}
final n = gaussians.count;
// These are small option-derived tables, and option errors should win before
// grid derivation or chunk planning touches a large scene.
final bands = _bandColumns(gaussians, options.shBands);
final depths = _resolveShDepths(options.shBitDepths, bands);
final grid = _Grid.forScene(gaussians, options.profile);
final windows = _WindowTable.of(gaussians);
if (objects != null) {
_checkWindowTableRecordSize(windows.windows.length);
}
final encodedAabb = _encodedAabb(gaussians, grid);
final headerRecord = _header(
gaussians,
durationSec,
options,
bands.isEmpty ? 0 : bands.last.band,
encodedAabb,
);
final quantizationRecord = _quantizationRecord(grid, depths);
final windowTableRecord = _windowTableRecord(windows);
if (objects != null) {
_checkObjectFrontMatterReads(
objects,
prefixRecordBytes: <String, int>{
'Header': headerRecord.length,
'Quantization': quantizationRecord.length,
'WindowTable': windowTableRecord.length,
},
tableRecordBytes: objectTableRecordBytes,
terminalBytes:
gaussians.count == 0
? _footerRecord(0, 0, 0).length
: recordHeaderBytes,
);
try {
for (final track in objects.tracks) {
if (track.sampleCount == 0) {
// Section 5.15.7 reads a zero-sample track as absent, so its pose
// fields cannot make this write fail. Its id is not pose data: the
// record parser still requires a wire-representable non-background
// object id before treating the track as absent.
if (track.objectId < 0 || track.objectId > 0xFFFFFFFF) {
throw FourdgsInvalidInput(
'ObjectTrack has object_id ${track.objectId}; expected an integer '
'in [0, 4294967295]',
);
}
if (track.objectId == backgroundObject) {
throw const FourdgsInvalidInput(
'an ObjectTrack names object 0, which means background / '
'unassigned; a track must move an object that exists '
'(section 5.15.7)',
);
}
continue;
}
track.check();
}
} on FourdgsMalformedFile catch (error) {
throw FourdgsInvalidInput(error.message);
}
}
// Window boundaries are the top level of the temporal partition. Anything
// strictly inside the clip is a split point; the ends are always present.
final tops = _tops(windows.windows, durationSec);
final planningSupport = _planningSupport(
gaussians,
grid,
windows,
options.cutoff,
);
final plans = _planChunks(
gaussians,
planningSupport,
tops,
options,
staticScene: durationSec == 0.0,
);
// The index this partition would produce has to be one this package's own
// indexed reader will open. `openFourdgsIndexed` stops at
// `maxChunkIndexEntries` — the index has no declared count, so a runaway one
// is caught by a ceiling rather than by arithmetic — and the top level of the
// tree is the window table, so a scene giving every gaussian its own validity
// window produces one entry per window whatever `minChunkGaussians` says. Past
// the ceiling the file is still a legal stream, and it is one only the
// front-to-back path can read: the seeking path, which is the entire reason to
// write an index, refuses it. Saying so here names the count and the ceiling;
// `writeIndex: false` writes the same chunks without the claim.
if (options.writeIndex && plans.length > maxChunkIndexEntries) {
throw FourdgsInvalidInput(
'this scene partitions into ${plans.length} chunks, past the '
'$maxChunkIndexEntries entries an indexed reader will open — the top '
'level of the tree is the window table, so distinct validity windows set '
'the floor on the entry count; write fewer windows, or write this scene '
'with writeIndex: false',
);
}
final out = _SinkWriter(sink);
out.bytes(fourdgsMagic);
// The degree the file actually carries, which is the highest band written and
// not the degree the input happened to hold. `shBands` caps what is emitted,
// so a degree-3 scene written with `shBands: 1` carries band 1 alone — three
// coefficients per component — and declaring 3 there would promise fifteen.
// Bands are whole and a reader takes them whole (spec §6.5): bands 1..D give
// exactly a degree-D scene, so D is a count of what is present.
out.bytes(headerRecord);
out.bytes(quantizationRecord);
out.bytes(windowTableRecord);
if (objects?.table != null) {
out.bytes(_objectTableRecord(objects!.table!));
}
if (objects != null) {
for (final track in objects.tracks) {
if (track.sampleCount == 0) continue;
out.bytes(_objectTrackRecord(track));
}
}
final index = <_IndexEntry>[];
for (final plan in plans) {
// Spatial order is materialized for this one bounded chunk and released
// after it is written. The plan retains only a view into one packed i32
// assignment table shared by the whole partition.
final members = plan.members(gaussians);
// Quantized columns are bounded by one Chunk. Keeping the whole scene's
// eleven lanes here would make the sink API retain memory in proportion to
// output size even though every framed record is released immediately.
final quantized = _quantize(
gaussians,
members,
grid,
windows,
options.cutoff,
);
final streams = _ByteWriter(4096);
for (final lane in quantized.lanes) {
streams.bytes(
_encodeStream(
lane.attributeId,
lane.values,
lane.channels,
options.codec,
options.level,
),
);
}
final chunkOffset = out.length;
final chunk = _chunkRecord(
plan.t0,
plan.t1,
plan.level,
members.length,
streams.finish(),
);
out.bytes(chunk);
final List<_IndexBand>? entryBands =
options.writeIndex ? <_IndexBand>[] : null;
for (final band in bands) {
final blob = _bandRecord(gaussians, band, members, depths, options);
final at = out.length;
out.bytes(blob);
entryBands?.add(_IndexBand(band.band, at, blob.length));
}
if (options.writeIndex) {
index.add(
_IndexEntry(
t0: plan.t0,
t1: plan.t1,
chunkOffset: chunkOffset,
chunkLength: chunk.length,
gaussianCount: members.length,
bands: entryBands!,
),
);
}
}
// The summary (spec §4.5): the Chunk Index, then Statistics, then the Summary
// Offset, contiguous and immediately before the Footer. Nothing else may sit
// inside that run, because the Footer's `summary_start` names its first byte
// and the CRC covers precisely that range — which is what lets a streamed
// reader verify the checksum by retaining the trailing records rather than
// the file.
int summaryStart = 0;
int summaryOffsetStart = 0;
int summaryLength = 0;
int summaryCrc = 0;
void emitSummary(Uint8List record) {
out.bytes(record);
if (options.writeCrc) {
summaryCrc = fourdgsCrc32(record, summaryCrc);
}
}
if (options.writeIndex && index.isNotEmpty) {
summaryStart = out.length;
final groupStart = summaryStart;
for (final entry in index) {
emitSummary(entry.encode());
}
// Taken here, before anything else is appended. A Summary Offset frames one
// *class* of summary record, so that a consumer can range-read the index
// without the rest of the summary; measuring the group after Statistics has
// been written declares a range whose tail is a different record class,
// which is the one thing the record exists to prevent.
final groupEnd = out.length;
if (options.writeStatistics) {
emitSummary(_statisticsRecord(n, index.length, durationSec, encodedAabb));
}
if (options.writeSummaryOffsets) {
summaryOffsetStart = out.length;
emitSummary(
_summaryOffsetRecord(opChunkIndex, groupStart, groupEnd - groupStart),
);
}
summaryLength = out.length - summaryStart;
}
final crc = options.writeCrc && summaryLength > 0 ? summaryCrc : 0;
out.bytes(_footerRecord(summaryStart, summaryOffsetStart, crc));
out.bytes(fourdgsMagic);
}