Skip to content

Releases: mas-bandwidth/serialize.cs

serialize.cs 1.8.0: the clamp is cross-language-proven, with witnesses on the wire

Choose a tag to compare

@rowan-claude rowan-claude released this 26 Aug 02:49
1d2dbf0

The compressed-float range clamp now carries cross-language witnesses: the wire-compatibility gate pins the C++ reference at v1.14.0 and proves the clamped values byte-identical across the boundary. STANDARD.md is re-synced with upstream, including the corrected contraction-suppression guidance and the writer-fusion vector band.

serialize.cs 1.7.0: a passing test prints its name and nothing else

Choose a tag to compare

@rowan-claude rowan-claude released this 24 Aug 16:39
569f292

A passing test prints its name and nothing else.

The suite's informational narration is now opt-in: the differential check count, the negative-control divergence counts, and which arithmetic discipline the runtime exercised. Set SERIALIZE_TEST_VERBOSE=1 to bring it back. Failures print everything relevant regardless, and no check runs or does not run because of this switch. The suite still performs its 5.1 million differential checks and still asserts the coverage floor and both negative controls.

This one is worth more than its three lines. The runner is a plain console executable with no test framework capturing output, so every line went straight to the terminal, and tests-ns21 compiles the same sources again, so CI printed all of it twice.

The helper is tests/Verbose.cs, and it takes the same environment variable as the C++ and C suites so one switch covers the whole family.

v1.6.0 — top-of-range compressed floats round-trip in every declaration

Choose a tag to compare

@rowan-claude rowan-claude released this 23 Aug 14:22

The compressed float writer now clamps the quantized integer to max_integer_value after the floor, so a top-of-range value round-trips in every declaration rather than only outside [2^23, 2^24).

This is the normative amendment ruled 2026-08-23 (schema#109) and carried by the whole family. Both witnesses the standard mandates are pinned here: [0, 8388609] at resolution 1, where the unclamped code was rejected by the reader, and [0, 16777215] at resolution 1, where the unclamped code overflowed the field and was masked to zero. The clamp sits in the single quantizer that every write entry point flows through — stream and batch, derive-per-call and precomputed — so no writer path is left uncovered.

Also in this release:

  • The decode's two roundings are pinned in the source rather than left to RyuJIT.
  • The differential gains negative controls and a run that can actually discriminate.
  • Two corpus shapes where ceil and round disagree.
  • The interop pin moves to v1.11.0, and the string buffer floor drops to 1.
  • The repo's first bench harness, with string and wstring rows.

No byte changes for any declaration outside [2^23, 2^24).

serialize.cs 1.5.0 — precomputed compressed float entry points

Choose a tag to compare

@gafferongames gafferongames released this 21 Aug 03:47
96a9240

The precomputed compressed float entry points — the C# port's leg of mas-bandwidth/schema#82, mirroring the C++ reference's 1.11.0 — reach a released version. Additive throughout: SerializeCompressedFloat is untouched on every surface.

New public API

SerializeUtil.CompressedFloatParams(min, max, resolution,
    out uint maxIntegerValue, out int bits, out float delta);

stream.SerializeCompressedFloatPrecomputed(ref value, maxIntegerValue, bits, delta, min);

The derivation — a float32 divide, a clamp, a ceiling and BitsRequired — depends only on the declaration, never on the value, so a schema compiler can run it at code generation time and hand the results to every call site. The entry point joins IBitStream, so it exists on WriteStream, ReadStream, MeasureStream and both batch surfaces.

Wire bytes are identical to SerializeCompressedFloat by construction: the derive-per-call entry point runs exactly CompressedFloatParams and then exactly the precomputed path's arithmetic. test_compressed_float_precomputed_differential holds four implementations — including a frozen verbatim copy of the pre-split v1.4.0 arithmetic — to identical measured bits, wire bytes, read acceptance and decoded bit patterns (never tolerance), over 18 declarations and 4,441,842 checks.

What is new since the API merged

The cross-language gate now covers it. The interop harness's FMA-boundary field (0.005f in [0,10] res 0.01) writes through SerializeCompressedFloatPrecomputed, with the constants its declaration derives. No byte moves, and the C++ half still writes it plainly — but the new entry point is now held under byte identity against the real serialize.h, in both directions, at the value that discriminates a strictly evaluated quantization from a contracted one.

The JIT verdict is measured, and it says do not adopt for speed. At a call site with literal min/max/resolution — what a schema compiler emits — RyuJIT already constant-folds the whole derivation: both entry points compile to the same 2360 bytes of arm64 machine code, with no ceiling and no BitsRequired in either, and measure the same (1.99 ns per field written, 1.90 read). Reach for the precomputed path for the single audited home. Where the declaration is not a compile-time constant the derivation does run per field, and the entry point removes it: 2.82 → 2.19 ns written, 2.71 → 2.28 read.

Compatibility

No wire change, no behavior change, no API removed or deprecated. Interop pin unchanged at C++ serialize v1.7.0. netstandard2.1 (Unity-class), net8.0 and net10.0 as before.

API misuse checks compile out of release builds: the C# check model matches the C++ reference

Choose a tag to compare

@rowan-claude rowan-claude released this 19 Aug 07:23

No wire change. Wire bytes are identical to 1.3.1: the golden pins and the C++ interop gate (byte identity plus cross reads) pass unchanged.

Read this before upgrading if you catch exceptions. The library now throws no exceptions of its own. In a Release build, API misuse that previously raised ArgumentException or ArgumentOutOfRangeException completes and produces garbage instead. That is the C++ library's model, and it is why this is a MINOR rather than a patch.

A Release build now performs the same amount of API-misuse checking the C++ library does: none. Six invented release-check classes become Debug.Assert / Conditional("DEBUG") on every stream, read and write alike (#17), per the family standard: "the caller is responsible for well formed writes... We want MINIMAL runtime checking in release."

The six classes, now debug-only

  1. Bits-range parameter throws ([1,32] / [1,64]) on all five serialize surfaces: WriteStream, ReadStream, MeasureStream, WriteBatch, ReadBatch.
  2. min/max ordering parameter throws on the same five surfaces.
  3. Buffer-size and declaration parameter throws: string bufferSize >= 2, compressed float min < max and resolution > 0, and fixed point Q format validation. ValidateBufferSize and ValidateFixedPointFormat now compile out whole via Conditional("DEBUG"), the C# analog of static_assert.
  4. Bitpacker construction throws: BitWriter buffer %8, BitReader bytes bound.
  5. The checked bitpacker API: WriteBits, WriteBytes, ReadBits, ReadBytes width, alignment and overflow throws on the raw BitWriter / BitReader.
  6. The write-side sticky branch: the per-field _error early-out on the write and measure spines, which the C++ write path does not have. The latch itself survives where it is the model, so SerializeObject still latches user aborts and refuses to descend after one, and the read side keeps its sticky no-op model in full.

Kept, in every build

All read-side packet data validation: buffer-end Overflow, Align, range, and the string and wstring content refusals. The red team harness re-proves the read path holds. The runtime's own array bounds checks stay too, the memory-safety floor named in the source header.

If you compute a range from previously decoded packet data, validate it before passing it in. In Release a violated parameter contract yields garbage-in-garbage-out bytes, never memory unsafety, and a checked reader rejects the malformed stream.

Proof

Both legs prove the model: test_api_misuse_asserts fires on every moved class in Debug, and test_api_misuse_checks_absent_in_release shows the same calls completing in Release. CI's Debug leg is where the assert-execution tests run, at 54 tests in Debug against 53 in Release.

src/Serialize.csproj carries 1.4.0, matching this tag.

Full changelog: v1.3.1...v1.4.0

The Debug contract asserts run in CI, and the standard carries the Implementation Law

Choose a tag to compare

@rowan-claude rowan-claude released this 17 Aug 09:47
08caf8a

The Debug contract asserts run in CI on all three platforms — six test legs where there were three — and the vendored standard carries the Implementation Law.

  • CI runs a Debug test leg on ubuntu, macos, and windows: the library's Debug-only contract asserts execute on every push, where before only Release configurations ran (#14).
  • STANDARD.md mirrors upstream verbatim at the Implementation Law revision — the law governs implementation practice, not the wire; no code change is implied (#16).

wstring rides as UTF-16 code units, and readers refuse malformed content

Choose a tag to compare

@rowan-claude rowan-claude released this 15 Aug 17:03

One intentional wire change, stated first: the old format's astral wstring groups (code points above 0xFFFF) flip from accepted to refused. Astral text now rides as surrogate pairs, matching the family's 2-byte shape; astral cross-language interop certifies at the family's v1.7.0 interop pin bump. BMP text is unchanged on the wire.

  • The writer is trusted in C# too: write-side checks are Debug.Assert (serialize#52).
  • Readers refuse malformed string content — invalid UTF-8, interior NULs, unpaired surrogates — with a pending-high surrogate pair machine.
  • The conformance pin was derived two independent ways: a reference bit-packer validated against STANDARD's worked example, then re-derived by hand bit by bit.
  • Verified beyond CI: Release suite 52/52, Debug suite 53/53 (the assert contracts execute), red team 29.4M hostile cases with no findings.

serialize.cs 1.2.0 — two roundings by language guarantee, and zero-bit fixed point at every width

Choose a tag to compare

@rowan-claude rowan-claude released this 15 Aug 03:47
ba51f30

Fixed

The compressed_float write path performs its two roundings by language guarantee, not JIT accident — a wire-correctness fix on FMA-boundary values. The quantization computed product and add in one expression, and ECMA-334 permits floating point to run at higher precision than the result type: the two roundings STANDARD.md requires were guaranteed only by RyuJIT's current behavior. Both sites now flow through one internal helper that stores the product through a float local with explicit (float) casts — the C# language-level guarantee that excess precision is discarded. On any host whose JIT fused the expression, written bytes change exactly on the boundary values where one rounding and two disagree; that is the fix, not a side effect, and test_compressed_float_quantization_boundaries pins the written integers. (This supersedes the earlier hold on shipping the pin: an intermediate that moves bytes only where the fused form was nonconformant is a correction, not a regression.) (680ac75)

A degenerate fixed range costs zero bits on every storage width. The narrow path always knew it; the 128-bit path computed fractionBits of zeros — the same declared field, two wire encodings, selected by an internal storage-width detail (serialize#54's C# half). The wide params now cost min == max nothing, in the one helper write, read and measure share, pinned by CheckFixedDegenerateWide across Q112.16 signed and unsigned, a negative bound, and Q64.64 — proven able to fail against the pre-fix params. The vendored STANDARD.md is resynced to the upstream revision that adopted the ruling. (#9)

Conformance

The interop gate pins C++ at v1.7.0 and the compat sequence carries the six fixed/128 fields — the cross-language byte-identity proof now covers exactly the operations the fixed-point family added. (#8)

CI

  • Windows joins the test matrix — this library exists for Unity, Unity authors on Windows, and it had never once compiled there. (#6)
  • The red team harness runs and its exit code is the verdict: no throw, no hang, no over-allocation, no out-of-bounds, errors as values. It was compiling in CI while nobody read its answers. One counted class stays open by design — non-finite decodes through compressed_float params whose delta overflows to infinity, kept for C++ parity pending an open family decision the harness must not preempt. (#6)
  • Analyzers and style gate, verify-only, across all five projects. (#6)
  • The CAA gate: the org's cla.yml, verbatim. (#7)

v1.1.0

Choose a tag to compare

@gafferongames gafferongames released this 13 Aug 22:12

Changed

A degenerate range (min == max) is now accepted, as STANDARD.md always defined it: zero bits, the value recovered from the range alone. Every port had independently grown a guard stricter than the format, so the same sequence worked against one runtime and failed against another — a cross-language divergence, which is the class that matters most in a family meant to be interchangeable.

min > max still fails. Relaxing the guard was meant to admit the degenerate case, not to stop validating.

Documentation

STANDARD.md was corrected: the int_relative ladder was missing its sixth tier (4378–69914, 17 bits), and its final tier transmits current rather than the difference. Both were documentation bugs — every implementation already agreed with the others.

Compatibility

No wire change. Still bit-identical with the C, C++, Go and Rust ports.

v1.0.0 — first release

Choose a tag to compare

@gafferongames gafferongames released this 13 Aug 02:38

First tagged release. The library has been complete and tested for a while; this marks it as no longer a draft.

What it is

A C# port of the C++ serialize bitpacking library, producing bit-identical output to the C++, Go and Rust ports — a stream written in one language reads in any other.

Wire compatibility is proven rather than asserted: a golden wire test pins bytes copied verbatim from the C++ test suite, and a live interop harness cross-checks against the real serialize.h compiled with clang++.

Family values

  • Zero third-party dependencies, including test frameworks.
  • Malicious packet data never throws. Reads fail cleanly through a sticky latched error; exceptions are reserved for API misuse by the caller. You can write your packet loop without a catch block.
  • No unsafe code.
  • Zero allocation on the serialization paths (strings on the read path are the documented exception).

Unity

netstandard2.1 is a first-class target, pinned to C# 9, so anything Unity's compiler would refuse fails in CI rather than in the editor. The full API surface — including the 128-bit paths — is available on every target framework.

Licence

BSD 3-Clause, matching the other three ports.