Proposal: the object layer under keyframe-delta
Status: proposed, not normative, not implemented. This document resolves #79. It states where object-track composition sits relative to delta application, closes the two smaller holes that answer depends on, and names the conformance variant that would pin it. Nothing here is in force until it is folded into the specification.
The layer and the temporal model were designed a revision apart, each correctly, and neither says what happens when a file carries both. Five implementations would each guess, and the guesses would not agree — because, as §2.3 shows, the reference's current guess is already wrong in a way that produces plausible output rather than an error.
1. What the format says today
1.1 The pieces are individually clear
Composition is defined on the base state. §3 reconstructs base_center and
orientation from the stored fields and then says:
When a non-zero
object_idhas an Object Track (§5.15.7), the decoder then applies that track's pose(R(t), T(t))exactly once, after reconstructing the base state:center = R(t) * base_center + T(t)orientation = R(t) ⊗ rotation
§6.6 restates it as an ordering: "first reconstruct the complete base state, including per-gaussian motion, then apply the one matching rigid track to center and orientation."
object_id is an ordinary optional attribute stream. §6.1: "the required set for version 1 is
position, scale, rotation index, rotation, colour, opacity, motion, mu_t, sigma_t, flags and
window index. source_group, source_index and object_id are optional." §6.6 gives it attribute
id 14 and the exact two's-complement bridge.
The provenance family is orthogonal to the temporal model. §5.15: "Every record in the family is
optional." Nothing in §5.15.6 or §5.15.7 mentions temporal_model, and nothing in §11 excludes
the family. The object-layer changelog note in §13 says the layer "composes with either of the two
[temporal models]".
keyframe-delta reconstruction ends in §3. §11.3 is unusually explicit:
The composed state
Sis a set of gaussians in exactly the state §3 describes — the same fields, the same types — and §3's arithmetic then applies verbatim. This model changes where the state comes from and nothing about what the state means.
1.2 What that leaves open
Read together, §11.3 and §3 arguably already answer the composition question: the chain produces a §3 state, §3 says a track is applied to a §3 state, therefore the track is applied after the chain. That reading is right and this proposal adopts it — but "arguably already answers" is exactly the condition that produces five different implementations, and §11.3's "§3's four lines" predates the object layer, so it names four lines that no longer describe all of §3.
Two smaller questions have no reading at all, and the composition rule is unusable without them:
-
Is
object_idin a delta's update group a bin difference or an absolute value? §5.18 says of the update group: "Every other stream present carries bin differences against the reference state." By the letter of that sentence a restatedobject_idis a difference of labels — object12minus object7is5, which is not an object. §11.5 makesrotation_indexandrotationabsolute by exception, and forbidssigma_t,flagsandwindow_indexin an update group outright.object_idis in neither list. -
May
object_idchange within a GOP at all? §11.5's invariant set issigma_t,flags,window_index,rotation_index, each for a stated reason about derived grids. None of those reasons applies to a label. The specification neither permits nor forbids the change.
The object-layer proposal answered both, in its §3.4:
object_idis an ordinary attribute stream, so it rides keyframe chunks (absolute) and MAY be restated in a delta's update group. When it is restated it is an absolute value, not a bin difference [...] It is therefore not in keyframe-delta §3.5's GOP-invariant set — an object may be relabelled mid-GOP — but when it changes it is restated whole. The track that applies to a gaussian at timetis the one matching itsobject_idatt, composed after the delta chain has produced that id.
That paragraph never reached the specification. object_id appears in the normative text at
lines describing §3, §5.3, §5.15.6, §5.15.7, §6.1 and §6.6, and nowhere in §5.18 or §11. The design
decision exists; the normative statement does not. That gap is the real content of #79.
2. What breaks
2.1 Nothing composes the layer on this path
Verified in the reference. python/fourdgs/fourdgs/keyframe_delta_file.py contains no occurrence of
the string object at all. Its _dequantize (:731-777) reads attributes 0–10 and emits
positions, scales, rotations, colours, motions, mu_t, sigma_t and window_index; object_id is
not among them. Its reconstruct_at (:780-828) applies §3's closed form and the window test, and
never looks for a track. decode_streamed (:563-626) and decode_indexed (:639-699) dispatch
on HEADER, QUANTIZATION, WINDOW_TABLE, CHUNK, DELTA_CHUNK and FOOTER, and on nothing in
0x20–0x2F.
The same holds in rust/fourdgs/src/keyframe_delta_file.rs, typescript/core/src/keyframeDelta.ts
and dart/fourdgs/lib/src/keyframe_delta.dart, as #79 states.
2.2 The reference writer cannot produce such a file
This is stronger than #79 claims and it changes what the fix costs. write_sequence
(keyframe_delta_file.py:252-265) takes samples, duration, cadence options, profile, cutoff,
library, codec, level and three write flags. There is no objects parameter and no provenance
parameter, and _keyframe_streams (:243-251) emits exactly gaussian_id followed by
_REQUIRED, which is opcode.REQUIRED_ATTRIBUTES (opcode.py:137-149) — attributes 0–10, with
no id 14.
So the combination is legal by the specification and has never existed as bytes anywhere in this repository. There is no file to decode wrongly today, which is why the corpus is green; and there is no encoder to produce a variant with, which is why step 2 of #134's sequence costs a writer change before it costs a decoder change.
2.3 The one place the reference would get it actively wrong
The chain machinery is attribute-generic, so object_id would survive it — State.bins is a plain
dict[int, ndarray] and apply_delta carries every attribute the reference state has forward
(keyframe_delta.py:134-198). That is the good news. The bad news is what happens when a delta
restates one:
GOP_INVARIANT = frozenset({op.A_SIGMA_T, op.A_FLAGS, op.A_WINDOW_INDEX}) # keyframe_delta.py:57
ABSOLUTE_IN_UPDATE = frozenset({op.A_ROTATION_INDEX, op.A_ROTATION}) # keyframe_delta.py:60
op.A_OBJECT_ID is 14 (opcode.py:135) and is in neither set, so the update branch at
keyframe_delta.py:163-166 falls through to _add_checked and adds the restated value to the
previous label. A gaussian that moves from object 7 to object 12 and whose producer follows
§5.18's letter by writing the difference 5 arrives at 12; a producer that follows the
object-layer proposal and writes 12 absolutely arrives at 19. Both files are accepted, neither
raises, and the gaussian ends up in an object that may well have a track — so the failure mode is a
gaussian transported by the wrong rigid body.
That is the shape this format refuses everywhere else: plausible, wrong, and silent. It is also exactly what happens if the composition rule is written without the absoluteness rule beside it.
2.4 The corpus cannot see any of it
Four keyframe/ variants, none carrying the object layer; four gaussian-birth files carrying it
(object/SingleObject, object/MultiObject, object/ObjectTrackComposed and the top-level
LongLived-UseChunkIndex-UseCrc-WithObjects). Note that the fourth is easy to miss — #79 and #134
both say "the three object-layer conformance variants", which undercounts by one; the top-level
WithObjects variant carries a table, a track and an object_id stream too.
The two canonical forms do not even overlap. A keyframe-delta file is summarized by
keyframe_delta_file.states_json rather than by canonical.summarize
(python/conformance/decode_streamed.py:53-62), and its JSON has six top-level keys — chunks,
cutoff, durationSec, gaussianCount, states, temporalModel — with no objects member and
no objectIds in its sample. So even if a file carried the layer, today's keyframe canonical would
not report it, and a decoder that dropped it entirely would still pass.
3. The options
The composition question has three answers, and only one of them is consistent with §11.3.
(a) Compose after the chain, on the reconstructed state at t — track ∘ (§3 ∘ compose)
The chain produces a §3 state; §3 reconstructs base_center from position + motion * (t - mu_t);
the track then transforms center and orientation once. Identical to the gaussian-birth path,
because the state is identical by §11.3's own claim.
- Cost: none beyond wiring. It adds no step to the model, and the arithmetic is code that
already exists for
gaussian-birth. - Property it keeps: "a consumer's decode path still ends in §3" (§11.3). One decode path, two sources of state.
(b) Compose per chunk, before delta application — store gaussians already transported
Apply each chunk's track pose while composing, so the state a delta references is already in world position.
- Fatal for the same reason §11.7 exists. A delta is a difference of bins; the track's pose is
an
f64rotation and translation evaluated at an arbitraryt. Transporting a state before differencing means the reference state is no longer a set of bins, so the telescoping argument that makes error bounds depth-independent collapses at the first delta. - It also breaks §11.5's rotation rule, since a composed orientation is not a smallest-three encoding of anything the file stored.
(c) Forbid the combination — a keyframe-delta file MUST NOT carry the object layer
- Cost: it deletes a capability the format currently has, for the one temporal model where rigid objects are most useful. A vehicle that moves rigidly through a captured sequence is the motivating case for both features at once.
- It also contradicts §5.15's "every record in the family is optional" and the object-layer changelog's "composes with either of the two", so it would need those retracted.
- The only argument for it is that it is the cheapest thing to implement, and the format does not buy cheapness with capability.
The object_id-in-a-delta question has two answers, and the difference between them is §2.3.
(i) Absolute in the update group
What the object-layer proposal specified. A label has no metric, so "difference of labels" is not a
smaller representation of the same thing — it is a different value that happens to be an integer.
Precedent: §11.5 already makes rotation_index and rotation absolute in an update group when a
difference would be meaningless.
(ii) GOP-invariant — a delta MUST NOT carry object_id at all
- Cheaper to implement (one entry added to
GOP_INVARIANT, one refusal) and strictly safer, since a file that cannot express the change cannot express it wrongly. - But it is a tightening that costs a real shape. An object that a producer wants to split or merge mid-GOP — a person setting down a bag, two segmented instances resolving into one — must then emit a death and a birth for every affected gaussian, or a keyframe. §11.5 charges that price for three attributes because a bin-difference across them has no meaning; a label restated absolutely has a perfectly good meaning, so the same price buys nothing here.
- Relaxing a rule is an append and tightening one is not (§11.6's reasoning), which argues for choosing (ii) if the two were otherwise equal. They are not: (i) is what the accepted object-layer design already decided, and reversing it needs a reason stronger than "cheaper".
4. Recommendation
Adopt (a) and (i): the track composes onto the state the chain reconstructs at t, exactly once,
after §3's arithmetic; and object_id in a delta's update group is an absolute restatement, not a
bin difference.
Three reasons.
-
(a) is not a new rule, it is §11.3 held to its word. §11.3 promises that a composed state is a §3 state and that §3's arithmetic then applies verbatim. §3 includes track composition. If (a) were not the answer, §11.3's promise would already be false, and the model's central selling point — that no consumer forks for it — would go with it. The only defect is that §11.3 says "§3's four lines" and §3 now has six.
-
(i) is the decision the object layer already made, in the proposal this repository accepted and folded into §5.15.6/§5.15.7/§6.6. What was lost in the folding was the paragraph about deltas, because §11 did not exist yet in the same document. Restoring it is a correction, not a new design.
-
The two rules are only safe together. (a) says "the track matching the gaussian's
object_id"; the chain is what determines that id att; and §2.3 shows the reference currently computes that id by addition. Writing (a) without (i) would make the composition rule normative on top of an id that six implementations compute differently — which is the failure #79 exists to prevent, moved one level down.
No existing file becomes illegal. No conformance variant changes. The combination has never been written, so this is a rule about files that do not exist yet — which is the cheapest moment to write one.
5. The normative text
5.1 Into §11.3, replacing the composed-state paragraph immediately before its final paragraph
The existing final paragraph — the separate rule that a keyframe sets every gaussian's mu_t to the
keyframe's t0 — remains in place, unchanged. Replace only the preceding paragraph that begins "The
composed state S is a set of gaussians" with the text below.
The composed state
Sis a set of gaussians in exactly the state §3 describes — the same fields, the same types — and §3's arithmetic then applies verbatim. This model changes where the state comes from and nothing about what the state means: decoding still ends in the same reconstructed gaussian state at timetthat §3 defines.This includes the object layer. When
Scarriesobject_id(§6.6) and the file carries an Object Track (§5.15.7) for a gaussian's id, the track composes ontoSat timetexactly as §3 defines it and in exactly one place: after the whole chain has been composed and after §3 has reconstructed the base center and orientation att, never during composition and never once per delta. Written as an order:1. chain -- compose(K, D1, ..., Dd) for t, per this section2. base -- §3's closed form on the composed state:base_center = position + motion * (t - mu_t)orientation = rotation3. track -- §3's rigid transform, applied once, for each gaussian whose object_id at tis non-zero and names a track:center = R(t) * base_center + T(t)orientation = R(t) ⊗ rotation4. shade -- §3's window test, marginal and opacity, on the untransformed temporal fieldsThe
object_ida track is selected by is the gaussian'sobject_idatt— the value the chain produced, not the value its keyframe stated. A track is applied to a gaussian at most once at any instant, because a gaussian has oneobject_idat any instant and at most one track may name an id (§5.15.7).Composing during the chain instead is not an optimization of this order, it is a different model: a delta is a difference of bins (§11.7), and a state transported by a track's
f64pose is no longer a set of bins, so the telescoping argument that keeps the declared bounds independent of depth would not survive the first delta. The transform is therefore applied to the reconstructed state and nowhere else.
5.2 Into §11.5, correcting the opening rule and then extending the rotation paragraph
First replace §11.5's opening sentence and invariant table, which incorrectly include
rotation_index, with:
Three attributes are GOP-invariant per gaussian:
sigma_t,flagsandwindow_index. A delta MUST NOT carry them in its update group, and a reader MUST refuse a file where one appears there.rotation_indexis not GOP-invariant: likerotation, it is an absolute restatement when present in an update, as the rotation paragraph below defines.
This is a normative correction, not only an implementation note: it makes §11.5 agree with §5.18
before the new object_id rule is inserted. Then add the following after the rotation paragraph:
object_idin a delta's update group is an absolute restatement, not a bin difference. The update carries theobject_idvalues as written and they replace the previous ones outright. This isrotation_index's treatment for a kindred reason: an id is a label (§6.6), a difference of labels is not a label, and a delta of5between object7and object12decodes silently into a gaussian that belongs to the wrong object and is therefore moved by the wrong track.
object_idis not GOP-invariant: a gaussian MAY be relabelled within a group of pictures, and a producer that relabels one restates the whole value. A birth carries it absolutely like every other attribute the birth group states.The optional-stream rule in §6.6 applies to every composed state: when the reference state omits
object_id, it logically carries an all-zero column, one zero for every live gaussian. An update MAY introduce the stream for any subset of the live population by including those gaussians in its singlegaussian_idstream. Once the update group physically carriesobject_id, however, that stream has exactlyupdate_countrows, aligned one-for-one with the id stream like every other present update attribute (§5.18). It restates a value for every gaussian in the group: rows whose id is unchanged repeat their reference value, including zero. There is no shorterobject_idsub-lane and no sentinel for "unmentioned" inside a present stream. Gaussians omitted from the update group keep their reference values. The composer materializes the implicit zero column before applying the aligned absolute replacements. When the reference already carriesobject_id, omitting it from an update means no restatement: every updated gaussian keeps its reference id, just as it keeps any other attribute absent from the update group. Omission does not replace an existing id with zero. When a birth group first introducesobject_id, the composer likewise materializes zeros for every surviving reference row before appending the births' absolute values; the resulting column has one value for every row in the composed population, not only for the births. Likewise, a birth group that omitsobject_idgives each birth id zero even when the surviving reference population already carries the column. Physical omission is therefore not an update-attribute mismatch and MUST NOT be refused as one.A producer deriving a delta from two complete states MUST normalize omission on both sides before differencing: an absent
object_idcolumn is a full zero column in the reference and in the current state. Thus a current state that omits the column after a non-zero reference requires absolute zero restatements for those rows; it is not an omitted update lane. If both complete states omit the column, their zero values compare equal and cost no update bytes. This authoring rule is distinct from physical omission inside an already-classified update group, which carries the reference value forward as stated above.
5.3 Into §5.18, in the updates bullet
Replace:
Every other stream present carries bin differences against the reference state, aligned element-for-element with the id stream.
with:
Every other stream present carries bin differences against the reference state, aligned element-for-element with the id stream, except where §11.5 makes a stream absolute.
and extend the bullet's final sentence:
sigma_t,flagsandwindow_indexMUST be absent (§11.5);rotation_index,rotationandobject_id, when present, are absolute (§11.5).
5.4 Into §6.6, after the "independently optional" paragraph
Under
keyframe-delta(§11)object_idrides a keyframe chunk absolutely, like every other attribute, and MAY be restated absolutely in a delta's update group (§11.5). The track that applies to a gaussian at timetis the one naming theobject_idthe chain produced for it att, and composition happens once, after reconstruction, per §11.3.Omitting the stream from a complete keyframe or from a birth means the logical value zero for the gaussians that record introduces. Omitting it from an update means the reference value is carried forward, not reset. Thus a keyframe whose later update or birth group introduces non-zero ids and a birth in a state where other gaussians already carry ids both begin with zero, while a position-only update to object
7remains on object7. §11.5 defines how the implicit zero column and carry-forward rule participate in composition.
5.5 In registry.md, replacing the objects profile row
| value | promises |
|---|---|
objects | One Object Table is present, and every gaussian introduction states membership explicitly: every non-empty gaussian-birth Chunk, every non-empty keyframe-delta keyframe Chunk, and every non-empty Delta Chunk birth group carries object_id. Updates that do not relabel a gaussian MAY omit object_id and carry the reference value forward; death groups never carry it. Thus a position-only update or a non-empty death-only Delta Chunk satisfies the profile without an object_id stream. Tracks and embeddings remain optional. |
The promise is about rows entering a reconstructed population, not about every physical record that
happens to have a nonzero count. That is the only interpretation compatible with §5.18: a death
group may carry only gaussian_id, while an update lane is intentionally sparse by attribute and
omission means carry-forward. It also preserves what the existing gaussian-birth wording meant —
every row a Chunk introduces has explicit membership — and extends that meaning to the two places a
keyframe-delta chain introduces rows.
Profiles constrain writers, not readers, as the registry already states. A decoder still accepts a
conforming file whose objects profile makes a promise it does not meet; a profile validator or
producer test is what reports the broken promise. This proposal therefore adds structural generator
assertions rather than inventing a decode refusal.
5.6 Into §13's changelog table
| Change | Kind |
|---|---|
§11.3/§11.5/§5.18/§6.6 and registry added: object tracks compose after the delta chain, object_id in an update group is absolute, and the objects profile applies to introduced rows | clarification, rule added |
with the accompanying note:
The object-layer-under-
keyframe-deltarow changes no existing file. No file has ever carried both: the referencekeyframe-deltawriter emitsgaussian_idplus the required attribute set and no object streams or records, and the fourkeyframe/variants carry none. What the row adds is the answer five implementations would otherwise each have guessed — and one of the two rules is a correction rather than a decision, since the reference's delta composer treats every non-exempt attribute as a difference, which for a label produces a plausible wrong object rather than an error.
6. The conformance variant that pins it
KeyframeDeltaObjects-UseChunkIndex, in data/keyframe/. The UseChunkIndex flag is part of
the name so the current harness selects both streamed and indexed runners; this proposal is not
proved if only the front-to-back path sees the object records and membership lanes. One variant
carries every corner the rules above decide.
- Shape:
Header.profileis"", because this variant deliberately exercises legal implicit membership rather than the strongerobjectsproducer promise. It has at least eight state chunks in two groups of pictures, with a chained delta in each, an Object Table naming two objects, and distinguishable Object Tracks for ids7and0x8000000C(2147483660), both with samples that put a non-identity pose at every probe. The first keyframe physically carries attribute14, with a mix of background and id7, so the keyframe decoder, canonical membership and id7track are exercised before any delta. The second keyframe deliberately omitsobject_id, so that group begins with implicit zero ids. Its first delta's birth group alone introduces id7on a new gaussian; no survivor update in that delta carriesobject_id. A later delta assigns one implicit-zero survivor to id7, a position-only delta updates it without anobject_idlane, and the next delta relabels it to the separately tracked high-bit id0x8000000C. A final complete source sample omits itsobject_idcolumn after that non-zero state, requiring the writer to emit an absolute zero restatement; that delta also births a gaussian with the lane omitted, which must receive zero. At least one gaussian carries per-gaussianmotionso thatR * (position + motion * dt) + Tis distinguishable fromR * position + T. - The second group's first birth introduces
object_id. Its absolute birth row is appended to a materialized zero column for all survivors. This is the inverse transition to an omitted birth after the column exists, and catches a generic composer that appends an attribute array containing only the birth rows. - The deltas exercise introduction and true absolute replacement independently. The second
group's birth-only introduction catches a composer that creates an attribute array containing only
birth rows. Moving a keyframe survivor from implicit zero to
7then proves that an omitted reference column is materialized instead of refused. Relabelling that survivor from non-zero id7to non-zero id0x8000000Cproves absolute replacement and the same-bits signed stream bridge together: the wire symbol is negative, but canonical membership is the unsigned decimal2147483660. The erroneous additive path produces a different high-bit id, while a decoder that forgets thei32-to-u32bridge produces a negative label that matches no track. The survivor is therefore moved first by id7in an intermediate reconstructed state and by id2147483660in the later one. The shared expectation MUST contain both ids instates[*].sample.objectIdsat their respective probes and distinguish the two track transforms. A decoder that composes tracks after each delta carries id7's transform into the next delta and then applies the high-bit track as well, while the required post-chain composition applies only the high-bit track to the final reconstructed base state. - Omitted update lanes carry forward. Add a position-only update for a gaussian already on id
7; its canonicalobjectIdsvalue remains7and its track still applies. This distinguishes update omission from the implicit zero assigned by an omitted complete-keyframe or birth lane. - Present update lanes cover the whole group. In one mixed two-gaussian update, change only the
position of gaussian A and relabel only gaussian B. The shared
gaussian_idstream names A and B, so the presentobject_idstream has two rows: A repeats its absolute reference id (zero in the introduction case) and B states its new id. The expectation catches both a writer that emits a one-row object lane and a reader that applies B's value to A by treating the lane as sparse. - Omitted complete-state lanes normalize to zero. After the same gaussian reaches id
0x8000000C, the next complete source sample omitsobject_id. The writer must detect the nonzero-to-zero change and physically emit an absolute zero update; the canonical state becomes untracked. This is the opposite direction from materializing an omitted reference and catches a writer that only normalizes the reference side before classifying changes. - What the canonical must report. The
keyframe-deltacanonical (keyframe_delta_file.states_json) has noobjectsmember and noobjectIdstoday, so it must gain both, in the shapecanonical.summarizealready uses forgaussian-birth: anobjectsblock with the table and the tracks' interpolated poses, and per-probestateswhose sample carries post-trackpositions,orientationsandobjectIds. Without that the variant would prove only that the file parses. - Ordering: the keyframe canonical orders by
gaussian_id, which is unique within a state (§11.2), so it does not inherit the tie hazard #95 describes forcanonical.py'sstates. That is worth keeping — it means this variant can be added without waiting on #95. - The
objectsprofile has its own case. AddKeyframeDeltaObjectsProfile-UseChunkIndexwithHeader.profile = "objects", one Object Table, two non-empty keyframes, a non-empty birth group, a position-only update and a death-only Delta Chunk. Every keyframe and the birth group physically carriesobject_id, including explicit zero rows; the position-only update omits the lane and carries membership forward; the death group carries onlygaussian_id. Before writing the expectation, the generator frames the file and asserts those exact presences and absences. The case therefore fails a writer that applies the old “every non-empty chunk” wording literally to death-only deltas, one that incorrectly requires the lane on every update, or one that forgets to put it on introduced rows. All runners still decode it normally, because profiles constrain producers rather than changing decode semantics. - Refusal variants, in
data/invalid/: a delta whose update group carriesobject_idfor a gaussian that is not live (already covered by the existing unknown-id refusal, so possibly redundant), and — if (ii) is chosen instead of (i) — a delta carryingobject_idat all.
Cost of the variant is a writer change first. write_sequence must grow objects and
scene_profile parameters. Its existing profile parameter selects quantization bounds
(fine/default/coarse) and currently also leaks that selector into Header.profile; the new
parameter separates the scene-profile registry value exactly as the gaussian-birth writer's
WriteOptions.scene_profile already does. Compatibility requires an omitted value to be distinct
from an explicit empty profile: define scene_profile: str | None = None, and when it is None
write the existing profile value into Header.profile exactly as the writer does today. Thus all
existing keyframe-delta generator calls remain byte-identical and their committed files and
checksums do not change. New callers pass scene_profile="" for an explicitly empty scene profile;
the profile-specific case passes scene_profile="objects". Removing the legacy fallback later would
be a separate compatibility change with an explicit corpus regeneration, not part of this rollout.
Sample quantization must carry an optional exact object_id column (attribute 14) rather than
stopping at attributes 0–10. _keyframe_streams then emits that column only when the keyframe
physically carries it. Delta generation must do more than index reference_bins[14]: when either
the reference or current complete state omits the column it materializes zeros for every row on that
side before classifying updates. When a birth first introduces the column it extends the survivor
zeros with the births' absolute values. Conversely, births that omit the column append zeros when
the reference already carries it, and a current complete state that omits the column after a nonzero
reference emits explicit zero updates. Only after those rules exist can the writer deliberately
produce the omitted-keyframe transition this variant requires. That is the honest sequencing note
for #134: for #79, step 2 is not "add a variant", it is "teach the reference encoder to write the
combination, then add a variant".
7. Claims in #79 this proposal could not confirm as written
- "A scene may legally carry both." Confirmed as a matter of the specification. Not confirmed as a matter of practice: no encoder in this repository can produce such a scene (§2.2), so the sentence "such a scene decodes to its uncomposed state" describes a file that does not exist rather than one that has been observed decoding wrongly. The conclusion is unchanged and the urgency is lower.
- "The keyframe-delta reconstruction path rebuilds base centres and scales from bins and never
reads the object layer." Confirmed, with one addition the issue does not make: the chain
composer would carry an
object_idcolumn forward if one existed (keyframe_delta.py:134-198is attribute-generic), and would compose it by addition (:163-166), which is a wrong answer rather than a missing one. That distinction is why §5.2's text is part of this proposal rather than a follow-up. - "The three object-layer conformance variants are all
gaussian-birth." There are four:object/SingleObject,object/MultiObject,object/ObjectTrackComposed, and the top-levelLongLived-UseChunkIndex-UseCrc-WithObjects. All four aregaussian-birth, so the point stands. - "§5.15.7 composes a track onto the reference state (
center = R*c0 + T)." §5.15.7 states the composition in prose and refers to §3; the formula is written out in §3 and in §6.6, not in §5.15.7. A reader looking for the arithmetic at the cited section will not find it there.
One thing found while writing §5.2 that #79 does not mention, and that whoever edits §11.5 should
fix in the same pass. §11.5 opens "Four attributes are GOP-invariant per gaussian: a delta
MUST NOT carry them in its update group, and a reader MUST refuse a file where one appears there",
and its table's fourth row is rotation_index — but the paragraph four lines below says "The update
carries rotation_index and the three rotation bins as written". The section contradicts itself
about rotation_index. §5.18 resolves it correctly ("sigma_t, flags and window_index MUST be
absent; rotation_index and rotation, when present, are absolute") and so does the reference
(GOP_INVARIANT holds three ids, not four), so the wire is unambiguous and only §11.5's opening
sentence is wrong. It matters here because this proposal adds a sentence to that section, and a
reader arriving at it to learn object_id's treatment reads the contradiction first.
8. Deliberately not decided here
- Non-rigid per-object deformation. A track carries the rigid part; whatever is left is the temporal model's business. That was the object layer's scope decision and this proposal does not reopen it.
- Whether a track should be able to reference GOP boundaries. A track is sampled on the scene clock and is entirely independent of where keyframes fall. Making the two interact — a per-GOP track, a track sample forced at each keyframe — would buy an encoder something and cost every reader a coupling between two independent records. Not proposed.
- Whether
object_idshould be GOP-invariant after all. Option (ii) in §3 above is a defensible answer and this proposal argues against it rather than pretending it is unavailable. If a maintainer prefers it, only §5.2 changes:object_idjoins §11.5's forbidden list and the variant's relabel row becomes a refusal. Everything else in §5 stands unaltered.