Files
OneCAD/docs/render-texture-normalization.md
seluneand0xlillith 1e984c8e46 render: normalize high precision texture inputs (#1267)
Adds a bounded normalization path for PNG16 and ICC-bearing JPEG texture inputs, producing float RGBA with recorded color and orientation decisions.

Co-authored-by: 0xlillith <0xlillith@users.noreply.github.com>
2026-10-15 13:55:35 -04:00

133 lines
8.9 KiB
Markdown

# Explicit texture normalization (#1218)
`texture_input::normalization` is a pure-Rust, in-memory input utility, separate
from the strict RGBA8 [texture admission API](render-texture-input.md). It does not
install a renderer, command, resource format, document property or GPU texture.
The old API still refuses 16-bit samples, ICC and nonidentity EXIF orientation.
## Input, policy and completed output
`NormalizationSource::new(bytes, role, options)` retains immutable encoded storage
and admits format/storage/budget only. `decode(cancelled)` admits metadata and
completes normalization before returning an immutable `NormalizedTexture`.
Header admission is not successful decoding. No partial result is returned.
- PNG channels expand to RGB/gray with alpha at their original 8/16-bit sample
depth; palette/sub-byte expansion uses the existing PNG codec. Output is RGBA
f32, without an implicit RGBA8 preview. This preserves distinguishable adjacent
16-bit raw samples; ICC conversion and floating-point arithmetic are not lossless.
- JPEG is initially 8-bit grayscale or three-component sequential/progressive DCT;
four-component/CMYK, arithmetic and lossless JPEG are outside this policy.
JPEG decoding is lossy irrespective of the output precision.
- Base color returns **linear sRGB** RGB and unassociated normalized alpha. A
renderer must not apply sRGB decoding again. Alpha never receives a color transform.
- Roughness, metallic, AO and OpenGL normal channels remain normalized **data**.
They receive no gamma correction, ICC conversion, normal renormalization, channel
convention conversion or filename inference. ICC on a data map refuses.
- Untagged color refuses by default. `DeclareSrgbPrimaries` is an explicit caller
declaration, not source certification: it uses PNG gAMA when present (encoding
gamma in [0.1, 10]), otherwise the sRGB transfer function. Non-sRGB cHRM refuses.
An explicit sRGB chunk selects sRGB transfer; ICC plus sRGB refuses.
- PNG HDR/CICP metadata and animation refuse for base color; HDR environments use
the separate [Radiance input utility](render-hdr-input.md). Data roles record
ignored PNG color metadata, rather than applying it to data channels.
- EXIF uses classic TIFF IFD0 only: either byte order, at most 128 entries,
one SHORT Orientation in 1..8. Missing Orientation uses TIFF's top-left default,
with distinct receipt flags for absent EXIF and absent Orientation. Other EXIF
fields/thumbnail IFDs are not interpreted or certified. Malformed admission refuses.
Nonidentity orientation requires `Apply`; all eight operations are exact pixel
permutations, without interpolation. Oriented normal maps refuse until a vector
and tangent-basis conversion contract exists.
## ICC scope and measured limitations
The already locked moxcms 0.8.1 is now a direct dependency (BSD-3-Clause OR
Apache-2.0). PNG 0.18.1 and SHA-256 were also already in the lockfile. No dependency
version, copied source, external image/profile, commercial asset or material dataset
is added. Fixtures are generated from original synthetic values in test code.
Initial ICC admission supports version 2/4 RGB input/display matrix/TRC profiles
with XYZ PCS and **identical RGB tone curves**. Required colorant/white-point/curve
tags are checked; CLUT and unknown conversion tags refuse. Limits: 256 KiB profile,
64 tags, 64 KiB per tag and 4096 sampled curve values. Offsets are four-byte aligned;
exact shared ranges are allowed, partial overlaps and duplicate signatures refuse.
The optional informational `chrm` tag initially admits three explicit device xy
channels (type 0, bounded xy values); PCS colorants still define the conversion.
This is bounded supported-profile admission, not a complete ICC conformance checker.
Conversion uses relative colorimetric intent, no source CICP override or fixed-point
preference, and **extended range**. Testing found the default destination LUT clips
before loss can be counted. Extended range preserves excursions until our policy.
f64 rows retain the CMS's 16-bit source LUT where analytic evaluation is unavailable;
the library still uses f32 matrix/curve arithmetic internally. No universal accuracy
or full-f64 colorimetry is claimed. Unequal RGB curves refuse because the pinned
same-curve optimization can treat two matching curves as three matching curves.
`GamutPolicy::Refuse` rejects encoded destination RGB outside [0, 1], allowing an
explicit edge tolerance of 3/65535 for finite-precision roundoff. `Clip` is an
explicit lossy preview policy. Every clamped RGB channel, including edge-tolerance
clamps, is counted. Nonfinite values always refuse. Output RGB is then linearized.
## Resource and provenance boundaries
- Encoded length **and capacity**: at most 16 MiB. Dimensions: at most 2048 per edge.
Raw decoded storage: at most 32 MiB. Completed float RGBA: at most 64 MiB.
- Maximum working admission: 256 MiB, charging retained encoded capacity, 24 bytes
per pixel for raw/output allowances, a 64 MiB codec reservation, 16 MiB conservative
CMS workspace, two 256 KiB profile allowances and two f64 RGB rows. The constructor
requires encoded capacity plus codec reservation before header work. Codec defaults
outside our control, allocator overhead and other jobs mean this is **not an RSS,
CPU-time or combined GPU/CPU ceiling**. Owned large buffers use fallible allocation.
- Framing/presence checks cap PNG chunks and JPEG marker segments at 4096, refuse
duplicate/late semantic metadata and require the codec to retain declared ICC/EXIF
information. JPEG scan stuffing/restarts are skipped without entropy decoding.
Existing codecs remain responsible for pixels and format validation; no second
compression decoder is introduced. PNG is fully finished before publication.
- Cancellation is checked before/after whole-image decode/profile setup, per
normalization row and during hashing. It cannot interrupt codec/profile construction.
The host must schedule this work away from the interactive UI thread. Worker ownership,
document generations, stale-result rejection, pooling and browser scheduling remain
mandatory consumer work; an immutable completed object does not implement those jobs.
- A read-only serializable receipt records recipe version 1, source and normalized
SHA-256 (little-endian f32 sample bytes), role, precision, dimensions, color conversion,
ICC identity, orientation operations/presence, chosen policies, clamp count, storage
and working estimate. It includes no paths or license approval. Hashes identify bytes,
not publishers. The receipt is diagnostic, not a persistent resource-pack schema or
permission to redistribute images/profiles. Per-file provenance and licensing remain
the host/library's responsibility.
Tests use independent analytic sRGB/linear expectations, adjacent 16-bit samples,
alpha/palette data, original ICC stress profiles, all eight orientations, malformed
and ignored metadata, budgets and cancellation. This utility alone cannot demonstrate
wood shading, material assignment, PBR/HDRI, GPU behavior or solver properties.
## Individually licensed file probe (2026-10-15)
The same [Black Oak Veneer](https://polyhaven.com/a/black_oak_veneer) 1K diffuse
files that the legacy utility refused now complete normalization with their actual
embedded ICC profile. Author: Jenelle van Heerden; asset license:
[Poly Haven CC0](https://polyhaven.com/license), verified at retrieval. Provider API
calls identified SolidCraft; this is an offline validation probe, not live API integration.
Files, generated output and probe harness are not bundled or committed.
| Input | Source SHA-256 | Completed result |
|---|---|---|
| PNG, 5,262,864 bytes | `3303ebadba1277488e43b0936b27743525711985db0da62ac9f989f695f6065c` | 16-bit source, 1024x1024 linear-sRGB RGBA f32, 16,777,216 output bytes, zero clamped channels |
| JPEG, 580,236 bytes | `7893f9bfeda626364f0a803dea6327c9f3a7b2f2fe330f6b4f326f6b06c0c265` | 8-bit source, same dimensions/encoding/storage, zero clamped channels |
Both receipts identify ICC SHA-256
`500fa2f01de5a0e051ce3b4111b0c94aa223cda0f4ccfbc1c631adc30a050616`.
The first probe refused its optional `chrm` tag. Support was added from ICC.1
sections 9.2.16/10.2, with an original valid/malformed fixture; no profile was stripped
or source primaries guessed. EXIF is present without Orientation, so TIFF's identity
default applies. PNG/JPEG output hashes differ as expected for different input bytes
and JPEG loss; these are completed input results, not wood rendering evidence.
Local receipts retain download URL/license/retrieval metadata and provider MD5 checks.
Any future shipped file needs its own ATTRIBUTION.md entry and reviewed provenance.
Public format references: [PNG third edition](https://www.w3.org/TR/png-3/),
[TIFF 6](https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf),
[ICC.1](https://www.color.org/specification/ICC1v43_2010-12.pdf),
[ICC embedding note](https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf)
and [JPEG T.81](https://www.w3.org/Graphics/JPEG/itu-t81.pdf).