Cross-platform visual parity¶
WhatsCanvas treats visual parity as a tested contract rather than a manual screenshot review. Android, iOS, Desktop and Web must render the same scene content, orientation, animation time and logical content viewport before their pixels are compared.
Canonical content window¶
Physical device screens cannot have one size. The primary comparable unit is a neutral, rotation-symmetric 2:1 design-space window:
- Landscape: 800 x 400.
- Portrait: 400 x 800.
These numbers are not copied from a device or its system bars. They are round
logical units with an exact reciprocal orientation, so a new device cannot
silently redefine the contract. The catalog also registers 16:9 phone, 4:3
tablet and 16:10 desktop layout standards. The former 393 x 759 / 786 x 377
Android-derived pair is retained as legacy_android for historical regression
only; it is not a primary pixel reference.
The four-platform pixel matrix runs the primary phone_2_1 standard. The
additional standards are layout-conformance cases rendered by the deterministic
Desktop Software host in CI. This separation keeps pixel comparisons stable
while still detecting assumptions that only work at one phone aspect ratio.
Run an auxiliary case with, for example,
WhatsCanvasDesktopHost --viewport-standard=tablet_4_3 --w=768 --h=1024.
Every host removes platform safe areas, aspect-fits this canonical canvas, centers it horizontally and anchors it to the usable top. The complete scene is scaled once. It never independently reflows cards, text, strokes or animation geometry. Extra host area is letterboxed with the scene background.
The shared implementation is
platforms/shared/scenes/CanonicalViewport.h.
Any new host must use it rather than defining another viewport policy.
Scene contract¶
tests/visual_parity/scenes.json is the
validation registry. Every scene declares:
- a stable scene id and contract version;
- Android, iOS, Desktop and Web as required platforms;
- portrait and landscape canonical sizes;
- deterministic animation samples in seconds;
- named comparison regions and their tolerance profile.
The registry currently contains four scenes. feature_showcase samples 0.0,
0.5, 1.25 and 2.0 seconds; each focused stress scene samples 1.25 seconds:
text_stress: fallback, emoji clusters, wrapping, baselines, glyph effects and text on a path;geometry_stress: even-odd holes, concave fills, nested clips, stroke joins, dashes, subpixel geometry, arcs and negative scale;compositing_stress: Porter-Duff cutouts, blend modes, backdrop glass, inner shadow and layer alpha.
New animation logic must be a pure function of the supplied elapsed time. Random, wall-clock and locale-dependent input must be seeded or supplied by the scene contract.
Profiles are intentionally separated:
graphicscatches path, clip, gradient, blend and transform drift;image_samplingkeeps a tight mean-error limit while allowing the antialiased edge colors produced when a nearest-neighbor texture is captured at different physical pixel densities;textallows limited native rasterization differences while still catching missing glyphs, wrapping and geometry changes;filtersallows small kernel/sampling differences while still rejecting a missing filter, incorrect layer bounds or broad compositing change;layoutcatches whole-card displacement and scaling regressions.
The comparator searches a small neighboring-pixel radius before measuring a
delta. This tolerates subpixel edge placement, but not a larger geometry shift.
Threshold changes require a reason and evidence; they must not be raised merely
to make a failing capture pass.
The latest measured evidence and threshold rationale are recorded in
The dated execution record is retained under internal/validation/; this page
defines the current contract.
Reference captures must use the canonical logical size with DPR=3, then be
normalized by the comparator. Merely rendering a 1200 x 2400 DPR=1 canvas is
not equivalent: text, image sampling, shadows and raster caches still take the
1x path. For example, the portrait Software reference is generated with
--w=400 --h=800 --dpr=3.
Capture identity and metadata¶
The capture tree consumed by the matrix command is:
captures/<platform>/<scene>/<viewport>/<sample>.png
captures/<platform>/<scene>/<viewport>/<sample>.json # when cropping is needed
PNG, binary PPM and PAM are supported without external Python dependencies. Device screenshots that include system UI or letterboxing must provide a JSON sidecar:
{
"schema_version": 1,
"scene_id": "feature_showcase",
"viewport_id": "portrait",
"sample_id": "t1250",
"platform": "ios",
"backend": "metal",
"content_rect_pixels": [0, 186, 1206, 2329]
}
content_rect_pixels is the exact canonical scene rectangle after safe-area
layout and before normalization. The comparator rejects an aspect mismatch
instead of guessing a crop.
Desktop accepts --time=<seconds> for deterministic dumps and --dpr=3 to
match high-density mobile rasterization. Android accepts
capture_time_seconds and capture_scene_id Activity extras. iOS accepts
--capture-time=<seconds> and --capture-scene=<id> together with its opt-in
--capture-frames switch. Web accepts scene, time and dpr query values.
Commands¶
Validate the registry and comparator:
python3 tools/visual_parity/visual_parity.py validate \
--contract tests/visual_parity/scenes.json
python3 tests/VisualParityToolTests.py
Compare one pair and write machine-readable results plus a heat map:
python3 tools/visual_parity/visual_parity.py compare \
--contract tests/visual_parity/scenes.json \
--scene feature_showcase --viewport landscape --sample t1250 \
--reference captures/desktop/feature_showcase/landscape/t1250.ppm \
--actual captures/android/feature_showcase/landscape/t1250.png \
--actual-metadata captures/android/feature_showcase/landscape/t1250.json \
--report out/visual-parity/android-landscape.json \
--diff out/visual-parity/android-landscape.pam
Run the complete required-platform matrix:
python3 tools/visual_parity/visual_parity.py matrix \
--contract tests/visual_parity/scenes.json \
--captures captures --reference-platform desktop \
--output out/visual-parity
Validation layers¶
- Contract and comparator unit tests run on every change.
- Deterministic Software golden tests guard the reference rasterizer.
- OpenGL, OpenGL ES and Metal tests compare their backend against Software.
- The device matrix compares normalized Android, iOS, Desktop and Web captures.
- Lifecycle, rotation, cold start and background/foreground tests run outside the pixel comparator, then capture the same deterministic scene again.
- Software layout-conformance tests render the registered phone, tablet and desktop standards independently of the primary pixel matrix.
Layers 1-3 are fast pull-request gates. The full device matrix should run on rendering pull requests, nightly, and before release. Missing required captures are failures, not skips, in the release job.
Adding a scene¶
- Put platform-independent drawing code under
platforms/shared/scenes/and use only the publicwsc::CanvasAPI. - Register the same stable id in every platform host.
- Add both canonical orientations and meaningful animation samples to
scenes.json. - Define focused regions for every feature the scene is intended to cover.
- Generate a Software golden and backend parity test.
- Capture all required platforms and run the matrix.
- Document any intentional platform-specific region. Do not silently mask it.
Platform branding can remain outside the strict graphics regions. Rendering content, geometry, timing and resource sampling cannot be platform-specific.