36 KiB
WAI Extension: Exact Splats
Status: Draft. Registers the record
wai.splat.exact: a Gaussian splat set that travels in a glTF 2.0 asset as an integer-coded WAI object, and a renderer whose frames are the same bytes on every machine, so a frame can be signed. Awai.splat.int_codecobject rides in a mesh primitive of the asset through the draft compression extensionEXT_gaussian_splatting_compression_wai, nested in the primitive'sKHR_gaussian_splattingobject: a buffer view holds the object'sWAI1envelope, and the extension states its content hash and its parameter set (§3). The decode is integer throughout, so the attribute planes are bit-exact on every machine, and §4 maps them onto the base extension's attributes. The rendererwai-splat-render:i1(§5) is integer too: a fixed-point camera and projection, an order by squared camera distance with the Gaussian's index breaking every tie, alpha compositing with each rounding stated, and 16 × 16 tiles in raster order. The origin signs a commitment (§6): the object, its parameter set, the digest of its decoded attributes, and the SHA-256 of the frame at each of a set of canonical poses, so a viewer recomputes what the scene looks like from each of them, bit for bit. A sink signs a view statement for a frame it presented at a pose (§7). A view is a conversion of the splat set under the class lattice of synthesis §3.2, so it is never labelled lower than the splat set it shows (§8). No new transport. The reference implementation is thesplat_exactmodule ofwai-rs(featuresplat_exact); the corpus islive-conformance/splat/, and its second implementation islive-conformance/splat_exact_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision, each pinned to the version named: glTF 2.0 as ISO/IEC 12113:2022, and the glTF extension registry's repository as it stood on 22 October 2026: the mesh primitive extension
KHR_gaussian_splatting, whose text states its status as complete and ratified, added by pull request #2490 (merged 27 January 2026) and last changed by #2677 (merged 13 October 2026), with its schemamesh.primitive.KHR_gaussian_splatting.schema.json; the compression extensions proposed for it and still under review on that date,KHR_gaussian_splatting_compression_spz_2(pull request #2531),KHR_gaussian_splatting_lgsc(#2551) andKHR_gaussian_splatting_compression_EGSC(#2605); the earlier single proposal those replaced,KHR_spz_gaussian_splats_compression(draft #2486, closed); and the general compression extensionKHR_meshopt_compression(#2517, merged). A section number of one of those texts in this extension is that version's.
1. Scope
In scope: the records and their encodings (§2); how a glTF asset carries a wai.splat.int_codec
object and the draft extension's schema (§3); the attribute layout, its dequantization and its
mapping onto KHR_gaussian_splatting (§4); the renderer wai-splat-render:i1 (§5); canonical
poses and the commitment (§6); the view statement (§7); provenance (§8); verification (§9).
Out of scope: a transport; how a splat set is trained or encoded; the glTF node hierarchy (a pose is in the primitive's own frame, §2); view-dependent colour (spherical harmonics of degree above 0); lighting. WAI does not submit this extension to the glTF extension registry; its name is WAI's draft, and §3.5 says what would change if the registry took up a WAI form.
Why a compression extension. KHR_gaussian_splatting defines a splat primitive and admits
compression extensions nested in its own extensions object: such an extension says how its data
decodes back to the base attributes. The base text fixes the order of a render (back to front by
camera distance) and its 3σ kernel, and leaves the arithmetic to the renderer. This extension
carries an integer-coded object whose decode is fixed to the bit, and pins a renderer, so the step
from bytes to pixels is the same on every machine.
Why a pinned renderer. A splat renderer that sorts, projects and blends in floating point
does so in orders and widths that differ between devices and drivers: two viewers of one asset
see different pixels, and a frame cannot be signed. §12 measures it: the same floating-point
renderer in 32-bit and in 64-bit arithmetic gives different bytes for views of the corpus, and
for more of them the larger the frame. wai-splat-render:i1 is integer from the attribute bytes
to the frame, so its frame is a function of the asset and the pose, and its digest can be
committed to and signed.
Relationship to the core spec: wai.splat.exact is a record (SPEC §5): it names
no payload format and is not dispatchable (SPEC §4). The payload it carries is
wai.splat.int_codec's (WIS1, integer-payloads §6).
2. Records
Every record of this extension is a JSON object (RFC 8259) under the encodings of
jwp-receipts §2.4.3: byte strings in lowercase hexadecimal; integers unsigned,
at most 2^53 − 1, in plain decimal; no other number; no boolean; a term and a date-time as
defined there. A record is read as jwp-receipts §3.1 step 1 reads a claim: RFC 8259's grammar, no
member named twice in one object, at most 32 levels of nesting. Each record has exactly the
members its section lists, no member missing and none added, at any level, and a kind naming it;
one that does not is record_malformed.
Every record carries signer_pubkey (32 bytes, the Ed25519 key of RFC 8032 that signs it),
issued_at (a date-time, when its signer signed it, self-asserted) and sig (64 bytes):
sig = Ed25519(signer_key, "wai:splat-exact\x01" || JCS(record without "sig"))
link_hash = BLAKE3("wai:splat-exact-link\x01" || JCS(record))
JCS is RFC 8785. The signature is verified strictly (SPEC §7 Signatures): a key
that fails step 1 or 2 is bad_key, and a signature that fails a later step bad_signature. A
record is named by its link hash wherever another names it. An object's content hash is
jwp-receipts §2.1's: BLAKE3 over moq-jwp:object\x01 and the envelope's bytes.
A signed integer is a string: 0, or an optional - and a digit 1–9 followed by digits (so
-0, +1 and 01 are not one), of magnitude at most 2^28.
A pose is an object of exactly these members:
| member | type | meaning |
|---|---|---|
eye |
3 signed integers | the camera's position, at 2^16, in the primitive's own frame (the node transform is not applied) |
target |
3 signed integers | the point it looks at, at 2^16 |
up |
3 signed integers | its up direction, at 2^16, of any length |
focal |
integer | the focal length in pixels, 1 to 4096 |
width, height |
integer | the frame's size in pixels, each 1 to 1024, their product at most 2^20 |
A pose that breaks a rule of this table is record_malformed. Two poses are the same pose when
every member is equal.
3. Carriage in glTF
3.1 The primitive
A splat set travels as one mesh primitive of a glTF 2.0 asset (ISO/IEC 12113:2022; its
asset.version is "2.0") that carries
the extension object KHR_gaussian_splatting, and in it, under that object's own extensions,
the object EXT_gaussian_splatting_compression_wai (§3.2). The primitive:
- has
mode0 (POINTS), written; - holds
KHR_gaussian_splattingwithkernel"ellipse"andcolorSpace"srgb_rec709_display", andprojectionandsortingMethodabsent or"perspective"and"cameraDistance"; - names exactly five attributes, each an accessor of component type
FLOAT(5126) with nobufferViewand acount:POSITION(VEC3),KHR_gaussian_splatting:ROTATION(VEC4),KHR_gaussian_splatting:SCALE(VEC3),KHR_gaussian_splatting:OPACITY(SCALAR) andKHR_gaussian_splatting:SH_DEGREE_0_COEF_0(VEC3). glTF 2.0 initialises an accessor with no buffer view with zeros and lets an extension override them; a sink that implements this extension fills them from the decode (§4.3).
An asset carries one such primitive in this revision. The asset's extensionsUsed and
extensionsRequired both list KHR_gaussian_splatting and
EXT_gaussian_splatting_compression_wai: the accessors carry no data, so a reader without this
extension has nothing to fall back to, and the base extension asks that such an extension be
required.
3.2 The extension object
EXT_gaussian_splatting_compression_wai has these members:
| member | type | presence | meaning |
|---|---|---|---|
bufferView |
integer | required | the buffer view that holds the object's WAI1 envelope (SPEC §2), and nothing else |
capability |
string | required | the envelope's capability: "wai.splat.int_codec" in this revision |
object |
32 B hex | required | the envelope's content hash (§2) |
set |
object | required | the parameter set: {"sha256", "bufferView"}, its SHA-256 (the envelope's pin, SPEC §3.1) and, where the asset carries the set, the buffer view that holds its one file (g_s.whm, integer-payloads §9); bufferView absent where it does not |
commitment |
integer | optional | the buffer view that holds the commitment's text (§6.1) |
extensions, extras |
object | optional | glTF's own |
An object that lacks a required member, whose buffer views do not read (a view that names no
buffer, a buffer whose byteLength is not its bytes' length, or a range past its buffer), or
whose object or set.sha256 is not 32 bytes of hex, is asset_malformed. A capability that is
a string other than "wai.splat.int_codec" is capability_mismatch. The members are read in
this order: the buffer view of bufferView, capability, object, set, commitment.
The commitment is carried as text in a buffer, not as JSON inside the asset, so it is read as §2 reads a record, byte for byte, whatever the reader of the asset's JSON does with its numbers and members.
3.3 The envelope, its set and the decode
The buffer view holds exactly one WAI1 envelope: its magic, its manifest's length and manifest,
its payload's length and payload, and no byte after them (otherwise asset_malformed). Then:
- Its capability is
wai.splat.int_codec(capability_mismatch). - Its content hash is the extension's
object(object_mismatch). - Its manifest's pin (
conditioning.prior, SPEC §3.1) names the extension'sset.sha256(set_mismatch). The set is the buffer viewset.bufferViewnames, or else the file a verifier holds whose SHA-256 isset.sha256(set_not_heldwhere it holds none). Its SHA-256 isset.sha256, and the pin is verified as SPEC §3.1 has it (set_mismatch). - The payload is decoded as integer-payloads §6 has it, against that set;
a payload the decode refuses, with any code of integer-payloads §8, is
decode_refused.
The decode gives attr planes of s × s bytes and the payload's n_gaussians and ranges. Every
step of it is integer, so these bytes are the same on every machine: the buffer of the
capability's splat-equivalence digest. Their SHA-256 is the splat set's attribute digest.
3.4 The accessors
After the decode, every accessor's count is n_gaussians, the decode has at least three planes,
and POSITION has min and max, three numbers each, equal to the least and the greatest
dequantized coordinate on each axis (§4.2) divided by 2^16. Every such value is exact in binary64,
so a reader of the asset's JSON reads the same number. Otherwise asset_malformed. (glTF 2.0
requires min and max on POSITION; this makes them the decode's own.)
3.5 The draft schema, and the registry
The extension object's JSON Schema, draft 2020-12, is
gltf/EXT_gaussian_splatting_compression_wai.schema.json.
It states what §3.2 states of each member's type and presence; the rules that need the envelope,
the set or the decode are this section's, not the schema's.
The name has the prefix glTF's registry gives an extension that more than one implementation is meant to read. WAI does not propose it to the registry. Were the registry to take up a WAI form, the extension object would keep its members, and the commitment and the renderer would stay WAI's: they are records and a profile, not part of a decode back to the base attributes.
4. The attributes
4.1 The layout
This revision renders splat sets of 14 planes (attr = 14), the layout wai.splat.int_codec
codes (SPEC §5): Gaussian k, for k < n_gaussians, is cell k of every plane, in raster
order, and its planes are:
| planes | attribute |
|---|---|
| 0–2 | position x, y, z |
| 3–5 | scale: the standard deviation along the Gaussian's own x, y, z axes |
| 6–9 | rotation, a quaternion w, x, y, z (of any length) |
| 10–12 | colour r, g, b, display-referred, the base extension's srgb_rec709_display |
| 13 | opacity |
A splat set of another plane count is layout_mismatch at the renderer (§5.2).
4.2 Dequantization
Each plane p has the range (lo_p, hi_p) the payload states as two binary32 values. Each is
taken at 2^16, rounded half up and computed exactly from the binary32 value:
LO_p = ⌊lo_p · 2^16 + 1/2⌋, HI_p = ⌊hi_p · 2^16 + 1/2⌋. A byte u of plane p is then
q = LO_p + ⌊((HI_p − LO_p) · u + 127) / 255⌋
the value at 2^16 (the nearest to lo + (hi − lo)·u/255, a tie upward). The payload makes
lo_p ≤ hi_p, so HI_p − LO_p ≥ 0.
4.3 The base extension's attributes
A sink that fills the accessors (§3.1) writes, for Gaussian k, each value rounded to the nearest
binary32: POSITION = (x, y, z); KHR_gaussian_splatting:ROTATION = (x, y, z, w) of
the quaternion divided by its length, or (0, 0, 0, 1) where it is zero; KHR_gaussian_splatting:SCALE
= the three scales; KHR_gaussian_splatting:OPACITY = the opacity clamped to [0, 1]; and
KHR_gaussian_splatting:SH_DEGREE_0_COEF_0 = (c − 1/2) / C₀ per channel, with c the colour
clamped to [0, 1] and C₀ = 1/(2√π), the base extension's degree-0 term; each value being q /
2^16. These float attributes are presentation for a renderer of its own: the attribute digest and
the renderer of §5 read the bytes and §4.2's integers, never them.
5. The renderer wai-splat-render:i1
The renderer maps a splat set's attribute planes and a pose to a frame: width × height pixels of
RGB8, rows top to bottom, pixels left to right, three bytes a pixel, no padding — the canonical
buffer of reconstruction-binding §2. Every value is an integer and no
formula wraps or saturates unless it says so.
5.1 Integer functions
⌊a / b⌋, R_half_up(p, s), isqrt, isqrt_ceil, clamp and exp_neg_q16 are
integer-operators §2's. In addition, for b > 0:
rdiv(a, b) = ⌊(2a + b) / 2b⌋ a / b to the nearest integer, a tie toward +∞
a >> s is ⌊a / 2^s⌋; × and · are exact products.
5.2 A renderable splat set
A splat set is renderable when it has 14 planes (layout_mismatch otherwise) and, for every plane,
|LO_p| and |HI_p| are at most 2^28, and LO_p ≥ 0 for the scale planes 3–5
(range_unrenderable otherwise). Each Gaussian k then has, at 2^16: its position P, its
scale (sₓ, sᵧ, s_z) ≥ 0, its quaternion (w, x, y, z), its colour and its opacity
(§4.2).
5.3 The camera
From the pose's eye E, target and up U:
F = target − E n_f = isqrt(F·F); n_f = 0 is pose_degenerate
f = rdiv(F_i · 2^14, n_f) the forward row at 2^14
R = F × U n_r = isqrt(R·R); n_r = 0 is pose_degenerate
r = rdiv(R_i · 2^14, n_r) the right row at 2^14
u = R_half_up((r × f)_i, 14) the up row at 2^14
· and × are the dot and cross products of the integer vectors. With F along −z and U
along +y, r is +x and u is +y: the frame's columns go right and its rows go down.
5.4 Projecting a Gaussian
With W, H and f_l the pose's width, height and focal length, for Gaussian k:
D = P − E
x_c = R_half_up(r·D, 14) y_c = R_half_up(u·D, 14) z_c = R_half_up(f·D, 14) at 2^16
key = D·D at 2^32
m_x = 128·W + rdiv(256·f_l·x_c, z_c) m_y = 128·H − rdiv(256·f_l·y_c, z_c) at 2^8 pixels
The pixel in column i and row j has its centre at (256·i + 128, 256·j + 128) at 2^8.
The screen covariance follows the elliptical weighted average splatting model, with the camera coordinates held within 1.3 times the frame's half-extent as the Jacobian is formed:
L_x = rdiv(13·W·z_c, 20·f_l) x_l = clamp(x_c, −L_x, L_x)
L_y = rdiv(13·H·z_c, 20·f_l) y_l = clamp(y_c, −L_y, L_y)
j00 = rdiv(f_l · 2^32, z_c) j02 = rdiv((−f_l · x_l) · 2^32, z_c²)
j11 = −j00 j12 = rdiv((f_l · y_l) · 2^32, z_c²)
T0_c = R_half_up(j00·r_c + j02·f_c, 14) T1_c = R_half_up(j11·u_c + j12·f_c, 14) c = 0, 1, 2
The rotation, at 2^16, from the quaternion with n = w² + x² + y² + z²: the identity
(2^16 on the diagonal) where n = 0; otherwise, each entry rdiv(e · 2^16, n) of
| w²+x²−y²−z² 2(xy − wz) 2(xz + wy) |
| 2(xy + wz) w²−x²+y²−z² 2(yz − wx) |
| 2(xz − wy) 2(yz + wx) w²−x²−y²+z² |
Then M_ab = R_half_up(Rot_ab · s_b, 16), S_ab = Σ_c M_ac · M_bc (at 2^32), and
V_ab = Σ_c Σ_d T_a,c · S_cd · T_b,d a, b ∈ {0, 1}
A = R_half_up(V_00, 48) + 19661 B = R_half_up(V_01, 48) C = R_half_up(V_11, 48) + 19661
the screen covariance at 2^16 pixels², with 19661 (0.3 at 2^16) added to each variance as the low pass every splat renderer of this model adds.
5.5 Culling, the rectangle and the order
A Gaussian is culled, and adds to no pixel, when any of these holds, in this order:
-
z_c < 13107(0.2 at 2^16: behind the camera or inside its near plane); -
A ≤ 0orC ≤ 0; -
with
r_x = ⌈isqrt_ceil(9·A) / 256⌉andr_y = ⌈isqrt_ceil(9·C) / 256⌉(three standard deviations, in whole pixels),r_x > 1023orr_y > 1023; -
its rectangle is empty: columns
x0 = max(0, ⌈(m_x − 256·r_x − 128) / 256⌉)tox1 = min(W − 1, ⌊(m_x + 256·r_x − 128) / 256⌋), rowsy0,y1the same withm_y,r_yandH; empty whenx0 > x1ory0 > y1; -
Δ = A·C − B² ≤ 0; -
with the whitening coefficients at 2^16
k1 = rdiv(2^40, isqrt(A · 2^32)) σ = isqrt(A · Δ · 2^16) k2 = rdiv(A · 2^32, σ) k3 = rdiv(−B · 2^32, σ)one of
|k1|,|k2|,|k3|exceeds 2^31 − 1.
For each Gaussian kept, compositing reads its m_x, m_y, its rectangle, k1, k2, k3, its
opacity o = clamp(opacity, 0, 2^16) and its colour c_ch = clamp(colour_ch, 0, 2^16).
The kept Gaussians are put in order: by key, the squared distance from the eye, nearest
first; a tie of key by the Gaussian's index k, lower first. This is the base extension's
cameraDistance order, made total.
5.6 Compositing a pixel
Each pixel starts with transmittance T = 2^16 and accumulators acc_r = acc_g = acc_b = 0, and
takes the kept Gaussians in order. It skips a Gaussian whose rectangle does not hold it. For one
that does, at pixel (i, j):
dx = 256·i + 128 − m_x dy = 256·j + 128 − m_y at 2^8
e1 = k1·dx e2 = k3·dx + k2·dy at 2^24
- If
|e1| > 3·2^24or|e2| > 3·2^24, the Gaussian adds nothing here. a1 = R_half_up(e1, 12),a2 = R_half_up(e2, 12),d2 = R_half_up(a1² + a2², 8): the squared distance in standard deviations, at 2^16. Ifd2 > 9·2^16, it adds nothing (the base extension's 3σ cut-off).w = exp_neg_q16(−⌊d2 / 2⌋)andα = min(64880, ⌊o · w / 2^16⌋)(64880 is 0.99 at 2^16). Ifα < 257(1/255 at 2^16), it adds nothing.c = R_half_up(α · T, 16)andT' = T − c. IfT' < 7(0.0001 at 2^16), the pixel is done: this Gaussian adds nothing and no later one is taken.- Otherwise each
acc_ch += R_half_up(c_ch · c, 16), andT = T'.
The pixel's bytes are min(255, R_half_up(acc_ch · 255, 16)). The background is black.
Taking Gaussians nearest first and keeping the transmittance is the same blend as the base extension's back-to-front alpha blending, in exact arithmetic; here every rounding is stated, the transmittance falls by exactly what each Gaussian adds, and a pixel can stop once it is opaque.
5.7 Tiles
The frame is cut into tiles of 16 × 16 pixels, in raster order, the last column and row of tiles cut short by the frame's edge. A tile's list is the kept Gaussians whose rectangle meets the tile, in order. A pixel composes its tile's list (§5.6). A Gaussian whose rectangle holds a pixel is in that pixel's tile's list, so the frame is the one §5.6 gives over the whole order, whatever order the tiles are computed in and however the work is divided among them: tiles and pixels are independent, and only the order within one pixel's list matters.
5.8 The frame's digest
A frame's digest is the SHA-256 (FIPS 180-4) of its bytes.
5.9 Widths
Within the caps of §2 and §5.2, every value §5.3–§5.5 forms fits in 127 bits plus a sign, and
§5.5's rule 3 bounds A and C before Δ and σ are formed. Every value compositing reads
fits in 32 bits, and §5.6 forms every value in 32 bits except the three products and the sum that
make e1 and e2, which fit in 64: |k| ≤ 2^31 − 1 and |dx|, |dy| ≤ 256·1024 + 256, and
each e is narrowed only after it is checked against 3·2^24. α · T and c_ch · c are below
2^32 because α ≤ 64880 and T ≤ 2^16.
The renderer is not a graph of integer-operators: its order and its
rectangles are not graph operators. It uses that extension's integer functions (§2 there), so
exp_neg_q16 is the one the operator set's softmax, sigmoid and elu use.
6. Canonical poses and the commitment
6.1 The commitment
The origin of a splat set signs a commitment, kind "wai-splat-commitment":
| member | type | meaning |
|---|---|---|
kind |
string | "wai-splat-commitment" |
object |
32 B hex | the content hash of the object the asset carries (§3.2) |
set |
32 B hex | the SHA-256 of its parameter set |
attributes |
32 B hex | its attribute digest (§3.3) |
renderer |
term | the renderer: wai-splat-render:i1 in this revision |
class |
string | the splat set's class under synthesis §3.2: captured, reconstructed, assistive or synthesized |
origin |
32 B hex or null |
the link hash of the splat set's origin claim (synthesis §3.1), where it has one |
views |
array | 1 to 64 entries, each {"pose", "frame"}: a canonical pose (§2) and the digest of the frame §5 gives at it; no pose twice |
issued_at, signer_pubkey, sig |
§2 |
A commitment that breaks a rule of this table is record_malformed.
6.2 What it binds
A commitment names one object by content hash, so its decoded attributes, by digest, and through them every frame of every pose: the canonical poses are the ones whose frames the origin states. A viewer that recomputes a canonical frame and finds its digest knows its own decode and renderer reproduce what the origin saw from there, bit for bit; a viewer at any other pose renders a frame that every other conforming viewer at that pose renders too. The origin chooses the poses: views it wants recognised (a product shot, a catalogue's angles, the poses a scene was captured from).
7. The view statement
A sink signs a view statement, kind "wai-splat-view", for a frame it presented, under a key
of its viewing session (no identity of the viewer or the device):
| member | type | meaning |
|---|---|---|
kind |
string | "wai-splat-view" |
session |
32 B hex | fresh random bytes of the session |
commitment |
32 B hex | the link hash of the asset's commitment |
object |
32 B hex | the content hash of the object it rendered |
pose |
object | the pose (§2) |
canonical |
integer or null |
the index in the commitment's views of this pose, where the sink presents it as canonical; null otherwise |
frame |
32 B hex | the digest of the frame it presented |
renderer |
term | the renderer: wai-splat-render:i1 |
class |
string | the frame's class (§8) |
at |
integer | when it was presented, in microseconds of the session's clock |
issued_at, signer_pubkey, sig |
§2 |
A statement that breaks a rule of this table is record_malformed. A statement whose canonical
is an index states that its pose is that canonical pose, and its frame the one the commitment
binds there; with null it states only that its frame is §5's at its pose, which anyone holding
the asset recomputes.
8. Provenance
A rendered view is a conversion of the splat set under synthesis §3.2: it
rearranges and composes what the splat set holds, generates nothing and enhances nothing, and
the renderer names no model. Its class is therefore the greatest of reconstructed and the splat
set's class: the view of a captured or reconstructed scene is reconstructed, of an assistive one
assistive, of a synthesized one synthesized. A view statement's class is that class
(class_mismatch otherwise).
A splat set's own origin claim (synthesis §3.1) names media volumetric, and its subject is the
object the asset carries, by content hash. A sink that publishes a rendered view as an object
signs its origin claim as synthesis §3.4 has a conversion's: step convert, media image,
operation wai-splat-render:i1, record the link hash of its view statement, and one input,
the splat set's object with its claim's link hash, class and source.
A view statement is a presentation statement for one frame: what a sink presented, from which pose, of which committed object, with what class. Its frame is recomputable from the asset, so a verifier holding the asset proves it and does not take it on trust.
9. Verification
9.1 Inputs
A verifier is given an asset (its JSON and the bytes of the buffers it names), the parameter sets it holds, the splat set's origin claim where it holds one, and view statements.
9.2 Steps
In this order, a case stopping at its first refusal; the place of each refusal is named after it:
- The asset (§3.1, §3.2):
asset_malformed,capability_mismatch— asset. - The envelope, its set and the decode (§3.3):
asset_malformed,capability_mismatch,object_mismatch,set_mismatch,set_not_held,decode_refused— asset. - The accessors (§3.4):
asset_malformed— asset. An asset with no commitment is accepted here. - The commitment (§6.1), read and its signature verified; its
objectis the extension's (object_mismatch), itssetthe extension'sset.sha256(set_mismatch), itsattributesthe attribute digest (attributes_mismatch), itsrendererwai-splat-render:i1(renderer_unknown); the splat set is renderable (§5.2:layout_mismatch,range_unrenderable) — commitment. - Each view in order: the frame at its pose (
pose_degenerate), whose digest is itsframe(frame_mismatch) — view n, from 0. - Where the commitment's
originis notnulland the verifier holds the claim: the claim reads and verifies as synthesis §2 has it, its link hash isorigin, its subject is the object, its class is the commitment'sclassand the greatest of its step's class and its inputs' classes (synthesis §3.2); otherwiseorigin_mismatch— origin. - Each view statement in order, read and its signature verified; its
commitmentis the commitment's link hash (commitment_mismatch), itsobjectthe object (object_mismatch), itsrendererwai-splat-render:i1(renderer_unknown); with an index incanonical, that index names a view whose pose is the statement's (pose_not_canonical) and whose frame is the statement's (frame_mismatch); withnull, the frame at its pose (pose_degenerate) has the statement's digest (frame_mismatch); itsclassis the view class of §8 (class_mismatch) — statement n, from 0.
9.3 What an accepted case states
The object's content hash, the attribute digest and the Gaussian count; the digest of each
canonical frame, in the commitment's order; and, for each statement, its canonical and its
class.
10. Reasons
| code | meaning |
|---|---|
record_malformed |
a record or a pose breaks a rule of its table (§2, §6.1, §7) |
bad_key |
a record's key fails step 1 or 2 of SPEC §7's strict verification |
bad_signature |
a record's signature does not verify |
asset_malformed |
the asset breaks a rule of §3.1, §3.2 or §3.4, or its buffer view does not hold one envelope |
capability_mismatch |
the extension or the envelope names a capability other than wai.splat.int_codec |
object_mismatch |
a content hash the extension, the commitment or a statement states is not the envelope's |
set_mismatch |
the pin, the extension and the set disagree, or the pin does not verify |
set_not_held |
the set is named by digest only, and the verifier holds no file of that digest |
decode_refused |
the payload's decode refuses it |
attributes_mismatch |
the commitment's attribute digest is not the decode's: a commitment over other splats |
renderer_unknown |
a record names a renderer this revision does not pin |
layout_mismatch |
the splat set does not have the 14 planes of §4.1 |
range_unrenderable |
a range endpoint is past 2^28 at 2^16, or a scale range goes below zero |
pose_degenerate |
a pose whose eye is its target, or whose up direction is along its line of sight |
frame_mismatch |
a frame's digest is not the digest of the frame §5 gives at its pose |
origin_mismatch |
the origin claim the commitment names does not match it, or is not that claim |
commitment_mismatch |
a statement names another commitment than the asset's |
pose_not_canonical |
a statement claims a pose canonical that is not the commitment's pose at that index |
class_mismatch |
a statement's class is not the class of a view of the splat set (§8) |
11. Conformance
The corpus live-conformance/splat/ holds glTF assets, their buffers
and commitments, origin claims and view statements, each case with the outcome a verifier
reaches. Its scenes: the learned cloud of wai-rs/tests/vectors/int_splat/ (its payload and its
WHM1 synthesis, held by digest), and two scenes authored as attribute bytes and carried through
the identity set, a one-layer WHM1 network that decodes each latent value v at 2^8 to the
byte (255·v + 128) >> 8, so any planes can be coded. The identity set is a conformance device: it
codes each plane at the entropy of its bytes and learns nothing.
A verifier conforms when it reaches every case's stated outcome, place and statements. The corpus refuses a case with every code of §10, and holds these negative controls among them: a reordered sort (frames committed with a tie of distance broken by the greater index), a frame that does not recompute (rendered without §5.4's low pass), a commitment over the wrong splats (another scene's attribute digest), and a pose outside the pinned set claimed canonical.
Two implementations reach the same outcomes, sharing no code: the splat_exact module of wai-rs,
which also writes the corpus (wai_splat_exact_vectors gen, and verify to regenerate it byte for
byte), and live-conformance/splat_exact_verify.py, standard library only, written from this
text with its own integer renderer. The second also drops each of its own checks in turn and
changes each of its renderer's rules in turn (the tie order, sorting by depth instead of distance,
the low pass, the rounding of rdiv and of a contribution, exp_neg_q16, the stop, the alpha
ceiling, the cut-off, the rectangle's rounding, the near plane), and requires the corpus to catch
every one.
On a GPU. Compositing reads 32-bit words and forms 32-bit values but for e1 and e2
(§5.9), so it runs as a compute kernel in which every pixel is an invocation and every tile a
workgroup, with e1 and e2 formed by the 64-bit products of the integer operator set's GPU
executor (the wai-ops-gpu crate), whose arithmetic every device passes a probe on before it runs
anything. The host projects, culls and orders (§5.3–§5.5). splat_exact::gpu in wai-rs (with the
features splat_exact and ops_gpu) composes that way, and its test holds every canonical and
own-pose view of every scene of the corpus to the CPU's frame, byte for byte, on every adapter it
finds, and requires the corpus's views to catch each of three edited kernels (a contribution
rounded down, no alpha ceiling, the cut-off moved). Measured on 22 October 2026 with Metal, on the
integrated GPU of an arm64 desktop (macOS 27.0.1): every view equal, every edited kernel caught.
The macos job of wai-ops-gpu.yml runs it, and an absent adapter fails there.
12. The quality cost
wai_splat_exact_measure renders each scene of the corpus at its canonical poses and at the pose
of its own-pose view, with wai-splat-render:i1 and with a floating-point renderer of the same
model (the same camera, low pass, cut-off, alpha limits, stop and order, the platform's exp and
sqrt) in f64 and in f32. Measured on one machine (the corpus's frames, and the same poses at
four times the side and focal length):
| scene | frames | PSNR of the integer frame against f64 |
largest byte difference | bytes in which f32 and f64 differ |
|---|---|---|---|---|
| learned cloud | 96 × 96, 80 × 64 | 62.3–64.5 dB | 2 | 0–2 per frame |
| learned cloud | 384 × 384, 320 × 256 | 62.3–64.8 dB | 4 | 7–24 per frame |
| authored cluster | 80 × 60 | 59.4–61.5 dB | 2 | 0–2 per frame |
| authored cluster | 320 × 240 | 59.4–61.7 dB | 3 | 3–17 per frame |
| authored three Gaussians | 40 × 30 | 63.2–67.2 dB | 1 | 0 |
| authored three Gaussians | 160 × 120 | 64.5–67.0 dB | 3 | 0–1 per frame |
So the cost of the integer arithmetic, against the same model in f64, is a byte that differs by
at most 4 levels, at about 60 dB and above; it includes §4.2's fixed-point dequantization, which a
float renderer does in float. A float renderer is not one renderer: in f32 it gives other bytes
than in f64 in some of the corpus's frames and in nearly all of them at four times the side, and so do two devices that order or fuse its operations differently,
so no digest of a float frame is stable. These figures are this corpus's scenes; a scene of many
more Gaussians composes more terms per pixel and adds more rounding.
13. Security and privacy considerations
A commitment says what the origin's splat set looks like from each canonical pose, under this renderer; it says nothing about the scene in the world it was made from. Its class and origin claim say where the splat set came from (§8).
A view statement reveals a pose and a time: where a viewer looked, and when. It is signed under a
session key and names no viewer or device. A sink SHOULD use a fresh session key and fresh
session bytes for each viewing session, and SHOULD NOT sign statements for poses a viewer
did not choose to share.
The decode's caps are integer-payloads §7's; the renderer's work is bounded by the frame's size (at most 2^20 pixels) times the Gaussians whose rectangles hold a pixel, and §5.5 culls a Gaussian whose rectangle is wider than 2047 pixels before its pixels are visited. A verifier MAY refuse to render a commitment whose frames, added up, exceed its own budget, and says so; the corpus's are small.