diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..22ee7a1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.zag text eol=lf +*.md text eol=lf +*.json text eol=lf +*.sh text eol=lf diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml new file mode 100644 index 0000000..4cba928 --- /dev/null +++ b/.github/workflows/contracts.yml @@ -0,0 +1,49 @@ +name: Repository contracts + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Check out source + uses: actions/checkout@v6 + - name: Resolve pinned Zag revision + id: toolchain + run: echo "zag_sha=$(jq -r '.zag.commit' contracts/toolchain.json)" >> "$GITHUB_OUTPUT" + - name: Check out pinned Zag toolchain + uses: actions/checkout@v6 + with: + repository: Sylorlabs/zag + ref: ${{ steps.toolchain.outputs.zag_sha }} + path: .toolchain/zag + - name: Restore fixed-point Zag compiler + id: zag-cache + uses: actions/cache@v5 + with: + path: | + .toolchain/zag/zag-poc/znc + .toolchain/zag/zag-poc/zagd + key: zag-fixed-${{ runner.os }}-${{ steps.toolchain.outputs.zag_sha }} + - name: Bootstrap pinned Zag source + if: steps.zag-cache.outputs.cache-hit != 'true' + run: | + chmod +x .toolchain/zag/zag-poc/znc .toolchain/zag/zag-poc/bootstrap.sh + ZAG_BOOTSTRAP_MEMORY_GUARD=off .toolchain/zag/zag-poc/bootstrap.sh + - name: Run strict headless contracts + env: + ZNC: ${{ github.workspace }}/.toolchain/zag/zag-poc/znc + run: | + chmod +x "$ZNC" + ./tools/test-headless.sh + - name: Validate product contracts + run: ./tools/check-contracts.sh + - name: Reject whitespace errors + run: git show --check --format= HEAD diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87acac0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Zag build products +zag-out/ +.zag-cache/ +.zagd.lock +.zagd.semantic-ready + +# Contract and benchmark output +artifacts/ +benchmark-results/ +capability-reports/ + +# Platform build products +.DS_Store +Thumbs.db +*.app/ +*.apk +*.aab +*.ipa +*.msix +*.dmg diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..006fd01 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,80 @@ +# Changelog + +Zagkit follows Semantic Versioning and keeps unreleased work under the heading +below. Public preview labels do not imply platform support. Every release must +include a generated capability record and exact Zag compiler revision. + +## Unreleased + +### Added + +- Milestone 0 product, architecture, quality, and dependency contracts. +- Machine readable platform, component, benchmark, compiler, and upstream + prerequisite records. +- An executable repository contract gate. +- Experimental typed state and binding primitives with exact dependency-read + invalidation evidence. +- Experimental keyed reconciliation with stable reorder identity and + fail-visible duplicate-key handling. +- Deterministic fixed-point geometry, constraints, and single-line Flex. +- Experimental retained intrinsic measurement for leaf, row, column, and + overlay trees with explicit constraint rules, priorities, baselines, + overflow, revision stability, ownership failures, deterministic identity, + and exact state-read layout invalidation causes. +- Experimental adaptive Flex primitives with tokenized density-aware spacing, + physical safe areas, text-scale-aware breakpoints, deterministic row and + column wrapping, RTL line bands, baseline-safe extents, per-line growth, + typed overflow, malformed-input rejection, and layout identity. +- An experimental owned semantics tree with roles, actions, focus order, + live regions, ranges, selection, text navigation, descriptions, bounds, + relationships, collection coordinates and counts, hierarchy, expansion, and + stable generated IDs. +- Bounded virtual Table and Tree semantic projection with full logical counts, + exact live geometry, deterministic collection IDs, and collision-atomic + preflight. +- The first in-process Zagkit Talkback dispatcher and machine-readable protocol + with protocol 0.2 query evidence for semantic geometry, collection metadata, + tree state, owned-text lengths, state flags, deterministic hashes, ID-first + targeting, and explicit scale-aware pixel fallback. +- A bounded owned render-resource store with typed stable IDs, canonical order, + exact replacement revisions, copied payloads, byte accounting, sealing, + mutation detection, typed display-list reference validation, and explicit + decoder boundaries. +- A canonical bounded `ZKPATH01` vector-path builder and codec with non-zero or + even-odd fill rules, line and curve commands, immutable identity, typed + display-resource validation, malformed-input offsets, and byte-identical + round trips. +- Canonical decoded RGBA8 image resources with overflow-safe dimensional + validation, exact payload sizing, explicit color-space truth, deterministic + premultiplied bilinear CPU sampling, fractional-edge coverage, and opacity. +- Deterministic PNG serialization for CPU surfaces with explicit sRGB intent, + exact filter-zero RGBA8 rows, CRC-checked chunks, a pure-Zag stored-zlib IDAT + stream, malformed-surface rejection, and byte-identical output evidence. +- A pure-Zag persisted headless reference scene whose PNG signature, substantial + output, and two-run byte identity execute in the normal repository gate. +- An experimental immutable display list for explicit paints, resources, clips, + transforms, layers, and effects with deterministic content identity. +- A versioned canonical display-list codec with bounded validated decoding and + byte-identical replay round trips, upgraded to ZKDL version 2 with owned + resource metadata, payload, allocation-policy, and revision serialization. +- The first deterministic CPU-oracle subset for fixed-point fills, clipping, + axis-aligned transforms, fractional coverage, and source-over alpha, extended + with centered analytic rectangle strokes, bounded canonical path fills, + quadratic and cubic flattening, non-zero and even-odd winding, 8 by 8 + coverage, and fail-before-pixel work ceilings. +- Strict executable headless contracts in CI using the exact pinned Zag + revision rather than an ambient compiler. +- Transformed hit testing and deterministic pointer routing with clipping, + z-order, capture, focus, stale-target recovery, and ordered evidence. +- A sealed deterministic replay tape and conformance executor covering exact + state revisions, pointer input, monotonic time, backend activation, loss, + recovery, and regenerated motion, semantics, measurement, Flex, display-list, + and CPU identities. +- A refresh-aware deterministic motion scheduler with fixed-step integer + springs, owned keyframes, explicit frame reasons, interruption, reversal, + resize continuity, gesture velocity handoff, and semantic reduced-motion + substitutions that perform no continuous work after settling. + +## 0.1.0-experimental.0 (unreleased) + +The repository contract seed. No runtime or platform backend is available. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..20a10f5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to Zagkit + +Zagkit accepts narrowly scoped, evidence backed changes that preserve the +architecture and support claims in this repository. + +## Before writing code + +1. Read [GOVERNANCE.md](GOVERNANCE.md), [DEPENDENCIES.md](DEPENDENCIES.md), and + the [accepted RFCs](docs/rfcs/README.md). +2. Check [the upstream ledger](contracts/upstream-zag.json). A reusable compiler, + ABI, concurrency, package, or platform defect belongs in Zag first. +3. Read the [active master goal checklist](GOAL.md) and name the stable IDs the + change advances. Never remove or check unrelated items. +4. Check the visual direction gate. Visual component production cannot begin + until [the design review](docs/design/visual-direction.md) is accepted. +5. Add or update the smallest contract, test, benchmark, or conformance scene + that can prove the change. + +## Change requirements + +- Production implementation is Zag. Do not add a C, C++, Zig, Rust, browser, + WebView, or foreign toolkit fallback to make a missing Zag feature disappear. +- Do not preserve a workaround merely because it would be conventional in + another language ecosystem. Improve Zag until the direct design is possible. +- Public OS APIs are permitted only at the platform seams listed in + [DEPENDENCIES.md](DEPENDENCIES.md). +- A backend or feature may be `supported`, `experimental`, or `unavailable`. + Supported and experimental states require named executable evidence. +- GPU work on hardware must be explicitly selected, bounded, and recoverable. + Discovery, compilation, screenshots, or CPU output do not certify GPU use. +- Tests must include cleanup and negative behavior where resources, malformed + input, callbacks, device loss, or platform boundaries are involved. +- Keep unrelated worktree changes out of the commit. +- Give every actionable component and application node a stable Zagkit Talkback + ID; pixel fallback must be explicit, observable, and justified. +- Use Flex semantic spacing and placement rules. Unexplained screen-local + offsets are defects, not polish. + +An upstream fix is complete only when it is committed separately in +`/home/micah/Desktop/Sylorlabs/zag`, carries source and native executable +conformance there, and the affected Zagkit gate passes against that exact Zag +commit. A local Zag working tree, patched compiler binary, or downstream-only +test is not a dependency revision. + +## Local gate + +```sh +./tools/check-contracts.sh +git diff --check +``` + +Later milestones add compiler, unit, fuzz, golden, live platform, performance, +accessibility, and packaging gates. A green contract check does not substitute +for any of them. + +## RFCs + +Use [RFC 0000](docs/rfcs/0000-rfc-process.md) for changes to public API, +architecture, dependency boundaries, backend truth, or release gates. An RFC +records a decision. It is not implementation evidence. diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000..5648390 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,59 @@ +# Dependency and ownership boundary + +Zagkit is built in Zag and owns the product layers above public operating system +and driver seams. This policy is normative. + +## Zagkit owned layers + +- state, bindings, actions, environment, view context, and stable node identity; +- reconciliation, layout, invalidation reasons, hit testing, and focus; +- semantics and automation trees; +- input routing, gestures, commands, animation, and frame scheduling; +- Unicode processing, OpenType shaping, font fallback, editing, and glyph + rasterization; +- render IR, immutable display lists, deterministic CPU rendering, GPU resource + ownership, and backend recovery; +- components, design tokens, adaptive behavior, inspectors, preview, reload, + snapshots, and command line tools. + +## Allowed operating system seams + +Public OS and GPU APIs may provide lifecycle, windows, surfaces, vsync, GPU +submission, IME, accessibility, clipboard, drag and drop, system menus, haptics, +notifications, and packaging. System fonts plus published Unicode and OpenType +data are allowed inputs. + +These seams must be isolated behind Zag interfaces, report capability truth, +clean up resources, and have native conformance tests. A platform seam does not +own Zagkit's view tree, layout, semantics, text model, or renderer architecture. + +## Prohibited runtime architecture + +The core and platform product may not depend on: + +- native widget proxy frameworks; +- browser or WebView runtimes; +- Skia, Flutter, Qt, FreeType, HarfBuzz, or another UI, text, or render engine; +- private Apple APIs; +- an LLVM based renderer; +- a C, C++, Zig, Rust, or other foreign implementation shim used to hide a Zag + compiler or runtime gap. + +Host tools used only to package, sign, drive a platform SDK, or validate +repository metadata are not runtime dependencies. They must be declared, +versioned where practical, and cannot implement product behavior. + +## Source first rule + +A reusable Zag language, compiler, ABI, concurrency, package, platform, or +runtime defect is fixed in the Zag repository with native conformance before +Zagkit consumes it. Zagkit may keep a failing reproducer and dependency record, +but not a permanent workaround. The upstream change is checked in separately, +and Zagkit pins the exact proven commit before relying on it. Patterns that +would normally be hidden behind a shim, reduced feature, unsafe escape, or +ecosystem workaround in another language are still Zag defects to solve at the +source. The authoritative queue is +[contracts/upstream-zag.json](contracts/upstream-zag.json). + +Vendor driver replacement can remain experimental and does not block 1.0. +Supported public GPU transports and the CPU visual oracle remain authoritative. diff --git a/GOAL.md b/GOAL.md new file mode 100644 index 0000000..1d92286 --- /dev/null +++ b/GOAL.md @@ -0,0 +1,139 @@ +# Zagkit master goal checklist + +- Goal status: **Active** +- Completion meaning: every item below is checked and carries reviewable evidence +- Scope: Zagkit, upstream Zag prerequisites, and the Zagkit based PrismStudio overhaul +- Rule: an implementation, screenshot, benchmark, or platform claim never checks a box by itself; the named exit condition must pass + +This is the durable execution checklist for humans and agents. Stable IDs may be +referenced by issues, commits, result bundles, and release notes. Do not delete, +weaken, or check an item to make a milestone appear complete. Split an item when +more precision is needed and preserve its original ID as the parent. + +## 0. Repository and product contract + +- [x] `G0-REPOSITORY` Establish the separate Apache-2.0 Zagkit repository. — Evidence: [LICENSE](LICENSE), [governance](GOVERNANCE.md) +- [x] `G0-VERSIONING` Establish semantic versioning and honest preview labels. — Evidence: [VERSIONING.md](VERSIONING.md), [zag.mod](zag.mod) +- [x] `G0-PLATFORMS` Record all five required platform families and fail-closed capability truth. — Evidence: [platform contract](contracts/platforms.json) +- [x] `G0-ARCHITECTURE` Accept the initial declarative, rendering, text, semantics, platform, and quality RFCs. — Evidence: [RFC index](docs/rfcs/README.md) +- [x] `G0-DEPENDENCIES` Forbid foreign UI and rendering engines from Zagkit core. — Evidence: [dependency boundary](DEPENDENCIES.md) +- [x] `G0-TOOLCHAIN` Pin each Zagkit release to an exact clean Zag revision. — Evidence: [toolchain contract](contracts/toolchain.json) +- [x] `G0-UPSTREAM` Record missing Zag prerequisites and native exit gates. — Evidence: [upstream ledger](contracts/upstream-zag.json) +- [x] `G0-BENCHMARKS` Define canonical benchmark scenes and variant axes without claiming results. — Evidence: [benchmark contract](contracts/benchmark-scenes.json) +- [x] `G0-COMPONENTS` Inventory the initial shared component surface. — Evidence: [component contract](contracts/components.json) +- [x] `G0-CI` Validate the product contracts in CI. — Evidence: [contract workflow](.github/workflows/contracts.yml) +- [x] `G0-EXPANDED-SCOPE` Bind Flex, Zagkit Talkback, visual fidelity, and PrismStudio redesign to release gates. — Evidence: [RFC 0006](docs/rfcs/0006-flex-talkback-visual-fidelity-and-prismstudio.md) +- [ ] `G0-VISUAL-DIRECTION` Select one of three accessibility-reviewed visual directions. — Exit: accepted visual-direction RFC with comparison images, risks, and conformance scenes + +## 1. Advance Zag at the source + +- [ ] `G1-LINUX-ARM64` Complete and natively conform the Linux ARM64 compiler target. — Exit: pure Zag ABI, executable, cleanup, and physical-device suites pass +- [ ] `G1-DARWIN` Implement Mach-O x86-64 and ARM64 targets. — Exit: native macOS executables pass ABI and lifecycle conformance +- [ ] `G1-WINDOWS` Implement PE/COFF x86-64 and ARM64 targets. — Exit: native Windows executables pass ABI, unwind, resource, and lifecycle conformance +- [ ] `G1-IOS` Implement the iOS ARM64 target. — Exit: signed Zag code runs on physical supported iOS devices +- [ ] `G1-ANDROID` Implement the Android ARM64 target. — Exit: Zag output runs on physical supported Android devices +- [ ] `G1-OBJC` Implement typed Objective-C runtime ABI seams. — Exit: message, callback, aggregate, ownership, and failure suites pass on macOS and iOS +- [ ] `G1-COM` Implement typed COM ABI seams. — Exit: lifetime, interface, callback, HRESULT, threading, and aggregate suites pass on Windows +- [ ] `G1-JNI` Implement typed JNI ABI seams. — Exit: calls, references, exceptions, threads, callbacks, and cleanup pass on Android +- [ ] `G1-CALLBACKS` Complete general foreign callback ABI support. — Exit: register, stack, lifetime, reentrancy, and negative suites pass per target +- [ ] `G1-AGGREGATES` Complete foreign aggregate argument and return support. — Exit: structs, unions, vectors, floats, alignment, and register-class suites pass per target +- [ ] `G1-RESOURCES` Implement deterministic compiler-owned resource embedding. — Exit: binary, empty, relative, malformed, cache-identity, reproducibility, and target suites pass +- [ ] `G1-DYNAMIC-LOAD` Complete typed cross-platform dynamic loading. — Exit: lookup, version failure, ownership, unload, callback, and aggregate suites pass +- [ ] `G1-CONCURRENCY` Complete main-loop and worker primitives with a documented memory model. — Exit: wakeup, cancellation, affinity, race, shutdown, and platform suites pass +- [ ] `G1-PACKAGES` Complete deterministic package resolution. — Exit: locks, checksums, offline, path, conflict, cache, and reproducibility suites pass +- [ ] `G1-RELOAD` Complete incremental compilation and safe reload hooks. — Exit: invalidation, state preservation, rollback, file-race, and crash-recovery suites pass +- [ ] `G1-SOURCE-FIRST` Keep reusable failures out of downstream workarounds and fallback languages. — Exit: every downstream Zag defect links to an upstream Zag regression and exact fixed revision + +## 2. Declarative and headless core + +- [ ] `G2-STATE` Implement `State`, `Binding`, actions, environment, and exact dependency reads. — Exit: unit, property, and deterministic replay suites pass +- [ ] `G2-RECONCILE` Implement keyed reconciliation and retained `RenderNode` ownership. — Exit: identity, reorder, replacement, cancellation, and cleanup suites pass +- [ ] `G2-CONSTRAINTS` Implement constraints, intrinsic measurement, size, rect, and invalidation reasons. — Exit: property tests prove bounded geometry and exact recomputation causes +- [ ] `G2-FLEX` Implement the Flex placement and spacing system. — Exit: tokenized gap, padding, alignment, distribution, baseline, wrap, grid, overlay, and breakpoint suites pass +- [ ] `G2-FLEX-RTL` Make Flex correct for RTL, safe areas, text scale, density, and platform adaptation. — Exit: full layout matrix passes without per-screen spacing exceptions +- [ ] `G2-VIRTUALIZATION` Implement scroll, virtual list, table, tree, and grid. — Exit: million-row identity, semantic range, bounded-node, and 120 Hz reference suites pass +- [ ] `G2-DISPLAY-LIST` Implement immutable paths, paints, images, glyphs, clips, transforms, layers, and effects. — Exit: fuzz, serialization, ownership, and replay suites pass +- [ ] `G2-CPU-RASTER` Implement the deterministic CPU visual oracle. — Exit: cross-machine goldens and malformed-input suites pass at every declared scale +- [ ] `G2-HIT-TEST` Implement transformed hit testing, capture, focus, and event routing. — Exit: property, overlap, clipping, cancellation, and replay suites pass +- [ ] `G2-SEMANTICS` Implement the parallel semantics tree. — Exit: roles, names, values, actions, focus, live regions, ranges, selection, and text navigation pass +- [ ] `G2-REPLAY` Implement deterministic state, input, time, backend, loss, and recovery replay. — Exit: identical inputs produce identical semantic, layout, display-list, and CPU outputs + +## 3. Text, assets, materials, and motion + +- [ ] `G3-UNICODE` Implement Zag-owned decoding, normalization, bidi, segmentation, and line breaking. — Exit: published-data differential, fuzz, malformed, and locale suites pass +- [ ] `G3-OPENTYPE` Implement Zag-owned OpenType shaping and rasterization. — Exit: fallback, variable, color, emoji, malformed-font, and reference suites pass +- [ ] `G3-EDITING` Implement selection, caret, undo, composition, and text navigation. — Exit: multilingual IME and editing replay suites pass +- [ ] `G3-FONTS` Implement precise typography tokens, fallback policy, hinting, and density-independent rasterization. — Exit: baseline, legibility, large-text, and international type-ramp goldens pass +- [ ] `G3-COLOR` Implement semantic color, wide-gamut conversion, alpha, contrast, and color-management contracts. — Exit: light, dark, high-contrast, and color-space oracle suites pass +- [ ] `G3-SVG` Implement secure scalable SVG decoding and rendering without a foreign UI engine. — Exit: path, gradient, transform, clip, text-policy, malformed, and scale goldens pass +- [ ] `G3-PNG` Implement color-managed PNG decoding and rendering. — Exit: alpha, palette, grayscale, color-profile, malformed, bomb-limit, and scale suites pass +- [ ] `G3-SHADOWS` Implement soft multi-lobe shadows and elevation tokens without pixelated edges. — Exit: CPU goldens and GPU tolerance suites pass across scales and contrast modes +- [ ] `G3-LIGHTING` Implement coherent light, surface, highlight, and depth composition. — Exit: deterministic material scenes preserve hierarchy in light, dark, and high contrast +- [ ] `G3-GLASS` Implement adaptive liquid-glass materials with blur, tint, refraction, highlights, and depth. — Exit: motion, overlap, text-legibility, reduced-transparency, CPU-oracle, and GPU suites pass +- [ ] `G3-MOTION` Implement springs, keyframes, layout, shared, and gesture-driven transitions. — Exit: interruption, reversal, velocity continuity, resize, and deterministic replay pass +- [ ] `G3-REDUCED-MOTION` Implement semantic reduced-motion substitutions. — Exit: every canonical animation has a tested non-motion or reduced-motion equivalent +- [ ] `G3-ASSET-PIPELINE` Implement resource identity, decoding limits, caching, invalidation, and cleanup. — Exit: malformed, replacement, missing, memory-pressure, and repeat-start suites pass + +## 4. Input, accessibility, Talkback, and tooling + +- [ ] `G4-INPUT` Implement pointer, keyboard, touch, pen, wheel, gamepad, and command routing. — Exit: arbitration, capture-loss, coalescing, focus, and replay suites pass +- [ ] `G4-GESTURES` Implement gesture arbitration, velocity, drag and drop, and handoff. — Exit: nested recognizer continuity and cleanup suites pass +- [ ] `G4-TALKBACK-PROTOCOL` Specify the versioned Zagkit Talkback native automation protocol. — Exit: request, response, event, timeout, capability, and error schemas are accepted +- [ ] `G4-TALKBACK-IDS` Expose stable developer-assigned and deterministic generated node IDs. — Exit: duplicate, stale, virtualized, window, reload, and semantic-tree identity suites pass +- [ ] `G4-TALKBACK-ACTIONS` Support semantic query, click, type, scroll, drag, focus, wait, assert, snapshot, and replay actions. — Exit: agent scripts exercise every action without pixel coordinates +- [ ] `G4-TALKBACK-PIXELS` Provide an explicit pixel-coordinate fallback. — Exit: fallback is capability-gated, scale-aware, logged, replayable, and never misreported as ID targeting +- [ ] `G4-TALKBACK-INSPECT` Ship tree, layout-reason, screenshot, timeline, and capability inspection. — Exit: an agent can diagnose a failed action from one retained evidence bundle +- [ ] `G4-TALKBACK-NAME` Keep Zagkit Talkback distinct from Android TalkBack accessibility. — Exit: public APIs, CLI help, docs, and reports use unambiguous qualified names +- [ ] `G4-ACCESSIBILITY` Implement Linux AT-SPI, Apple accessibility, Windows UIA, and Android accessibility adapters. — Exit: native assistive-technology suites pass on every promoted platform +- [ ] `G4-CLI` Implement `zagkit init`, `build`, `run`, and `test`. — Exit: reproducible clean-project workflows pass on every promoted target +- [ ] `G4-PREVIEW` Implement live preview and state-safe reload. — Exit: compatible state survives, incompatible changes roll back clearly, and crashes recover +- [ ] `G4-INSPECTORS` Implement semantic, accessibility, layout-reason, frame, theme, and capability inspectors. — Exit: inspectors agree with recorded runtime truth and add no idle work when closed +- [ ] `G4-GALLERY` Implement the component gallery as a conformance surface. — Exit: every conformant component exposes states, variants, semantics, motion, and goldens + +## 5. Polished Linux reference platform + +- [ ] `G5-WAYLAND` Implement the Wayland shell first. — Exit: lifecycle, windows, surfaces, scaling, input, clipboard, IME, and recovery pass on target distributions +- [ ] `G5-X11` Implement the X11 fallback without claiming Wayland equivalence. — Exit: declared X11 capability and limitation suites pass +- [ ] `G5-ATSPI` Implement and verify native AT-SPI exposure. — Exit: keyboard-only and supported Linux screen-reader scripts pass +- [ ] `G5-LINUX-CPU` Implement CPU presentation as the always-available visual oracle path. — Exit: live resize, multi-monitor, surface-loss, and ten-minute cleanup suites pass +- [ ] `G5-LINUX-GPU` Implement one explicit public Linux GPU transport. — Exit: opt-in physical-device execution, loss recovery, CPU comparison, and cleanup pass +- [ ] `G5-LINUX-POLISH` Make Linux the first no-rough-edges reference experience. — Exit: no known severity-one or severity-two visual, input, text, accessibility, recovery, or packaging defects remain +- [ ] `G5-LINUX-FIDELITY` Eliminate density-dependent and pixelated UI output. — Exit: screenshot comparisons pass at 1.0, 1.25, 1.5, 2.0, and 3.0 scales with crisp type, SVG, PNG, curves, glass, and shadows +- [ ] `G5-LINUX-PACKAGE` Package, install, launch, update, and uninstall Linux artifacts. — Exit: Ubuntu LTS and Fedora x86-64 and ARM64 release matrices pass where target support is declared + +## 6. Complete PrismStudio overhaul + +- [ ] `G6-INVENTORY` Inventory every existing PrismStudio workflow, command, screen, and safety boundary. — Exit: migration map has no unowned visible or keyboard-accessible behavior +- [ ] `G6-DESIGN` Select and approve one of three PrismStudio visual directions using representative CAD states. — Exit: maintainers choose a reviewed target with light, dark, contrast, large-text, and reduced-effects variants +- [ ] `G6-SHELL` Replace the entire visible PrismStudio shell with Zagkit. — Exit: no legacy app-owned widget, layout, or styling path remains in the supported Linux product +- [ ] `G6-WORKFLOWS` Preserve and modernize all CAD workflows and direct manipulation. — Exit: canonical task scripts pass by keyboard, pointer, and Zagkit Talkback IDs +- [ ] `G6-VIEWPORT` Migrate viewport chrome, overlays, tools, and input while retaining the explicit GPU safety boundary. — Exit: CPU-safe suite and separately authorized GPU evidence pass without weakening safety +- [ ] `G6-DENSE-UI` Migrate tables, trees, properties, menus, toolbars, dialogs, and command palette. — Exit: density, focus, virtualization, editing, semantics, and responsive-layout suites pass +- [ ] `G6-MATERIALS` Apply production typography, color, lighting, shadows, and liquid-glass materials coherently. — Exit: approved comparison screenshots pass at representative scales and states +- [ ] `G6-ASSETS` Migrate icons and imagery to crisp SVG, PNG, and font-backed assets. — Exit: no placeholder, ASCII, emoji, pixel-stretched, or missing production asset remains +- [ ] `G6-AUTOMATION` Give every actionable PrismStudio node a stable Talkback ID. — Exit: the canonical regression suite uses IDs for all normal actions and records every pixel fallback +- [ ] `G6-ACCESSIBILITY` Make the complete redesigned UI semantic and keyboard operable. — Exit: AT-SPI, focus, large-text, high-contrast, and keyboard-only suites pass +- [ ] `G6-SCREENSHOTS` Verify the actual native app with repeatable reference screenshots. — Exit: matched-state comparisons pass for startup, editing, menus, dialogs, viewport, long sessions, and recovery +- [ ] `G6-PERFORMANCE` Meet frame, idle, input-latency, memory, and stall budgets in real CAD scenes. — Exit: ten-minute reference traces pass with no unexplained two-frame stalls +- [ ] `G6-POLISH` Resolve all known rough edges in the supported Linux PrismStudio experience. — Exit: reviewed bug inventory has no open severity-one or severity-two product-quality issue + +## 7. Desktop, mobile, and shared 1.0 + +- [ ] `G7-MACOS` Ship the adaptive macOS shell, Metal transport, IME, VoiceOver, menus, drag and drop, and signed packaging. — Exit: macOS beta and stable gates pass on supported versions +- [ ] `G7-WINDOWS` Ship the adaptive Windows shell, D3D12 transport, Core Text input, Narrator, menus, drag and drop, and signed packaging. — Exit: Windows beta and stable gates pass on supported versions +- [ ] `G7-IOS` Ship the touch-first iOS shell, Metal transport, lifecycle restoration, IME, VoiceOver, haptics, and signed packaging. — Exit: iOS beta and stable gates pass on physical supported devices +- [ ] `G7-ANDROID` Ship the touch-first Android shell, GPU transport, lifecycle restoration, IME, Android TalkBack, haptics, and signed packaging. — Exit: Android beta and stable gates pass on physical supported devices +- [ ] `G7-MOBILE-REFERENCE` Ship and verify a focused mobile reference application. — Exit: shared components, navigation, text, semantics, recovery, performance, and packaging pass on iOS and Android +- [ ] `G7-COMPONENT-PARITY` Complete the shared adaptive component suite on all five families. — Exit: every inventory row is conformant with platform adaptation evidence +- [ ] `G7-TEXT-PARITY` Pass international text, IME, editing, fonts, and asset fidelity on all five families. — Exit: common text and visual matrices pass without hidden system-engine substitution +- [ ] `G7-RECOVERY` Pass device, surface, process, suspend, resume, loss, and cleanup gates on all five families. — Exit: live platform result bundles contain no unexplained leak or unrecovered loss +- [ ] `G7-PERFORMANCE` Pass common p99, 120 Hz reference, idle, memory, and stall gates. — Exit: reviewed result bundles pass on declared reference hardware +- [ ] `G7-PACKAGING` Install, launch, update, and uninstall signed artifacts on every supported OS. — Exit: reproducible packaging matrices and artifact hashes pass +- [ ] `G7-ONE-POINT-ZERO` Release Zagkit 1.0 only as one shared five-platform product. — Exit: every checklist item is checked, capability truth is supported, and no required waiver is expired + +## Checklist maintenance + +`./tools/check-contracts.sh` verifies that this file remains active, keeps stable +unique IDs, retains the non-negotiable requirements, and gives every checked +item evidence and every unchecked item an exit condition. That check validates +the plan's integrity; it does not certify the unfinished work. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..bd0a10f --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,49 @@ +# Governance + +## Project ownership + +Zagkit is a first party Sylor Labs project. Maintainers are responsible for the +public API, repository releases, accepted RFCs, support claims, security +response, and the relationship with Zag and first party consumers. + +The project uses reviewable pull requests for normative and implementation +changes. Direct emergency fixes must receive follow up review and evidence. + +## Decision classes + +| Change | Required record | +|---|---| +| Typo, test repair, internal refactor | pull request and relevant gates | +| Public API or behavior | RFC, compatibility note, tests | +| New runtime dependency or OS seam | RFC and dependency boundary review | +| Platform status promotion | native evidence bundle and capability record | +| Performance waiver over 5 percent | explicit reviewed waiver with expiry | +| Stable release or 1.0 gate change | maintainer approval and release audit | + +Accepted RFCs are normative until superseded. Code that conflicts with an +accepted RFC is a defect unless a newer RFC explicitly changes the decision. + +## Support claims + +Only evidence can promote a capability. Marketing copy, a code path, a compile, +a screenshot, emulation, or a provider report is not native runtime proof. +Capability changes must identify the device, operating system, compiler +revision, backend selected, executable gate, and cleanup result. + +No individual platform is named Zagkit 1.0. The product reaches 1.0 only when +Linux, macOS, Windows, iOS, and Android all satisfy the common release contract. + +## Upstream ownership + +Reusable language, compiler, ABI, package, concurrency, platform, or runtime +work is implemented and tested in Zag. Zagkit may carry a temporary failing +conformance test that demonstrates the need, but it may not hide the defect +behind a foreign language shim or permanent local workaround. The upstream fix +lands as its own reviewable Zag commit with native conformance; the downstream +change then pins that exact commit and proves the consumer path. + +## Conduct and security + +Contributors must be direct, respectful, and evidence led. Security reports +follow [SECURITY.md](SECURITY.md); do not publish exploit details before a fix +and coordinated disclosure are ready. diff --git a/README.md b/README.md index dca9fb6..458bbba 100644 --- a/README.md +++ b/README.md @@ -1 +1,264 @@ -# zagkit \ No newline at end of file +# Zagkit + +Zagkit is a first party application platform and UI toolkit built in Zag. It is +being designed for one adaptive product codebase across Linux, macOS, Windows, +iOS, and Android, with precise text, fluid interaction, built in semantics, and +inspectable runtime truth. + +The durable, CI-checked execution plan is the [master goal checklist](GOAL.md). +It includes the Flex placement system, Zagkit Talkback native automation, +modern materials and asset fidelity, and a complete PrismStudio UI replacement. + +This repository is at **0.1.0-experimental.0**. It currently contains the +accepted product contract, executable Milestone 0 checks, and the first +deterministic state, keyed reconciliation, geometry, intrinsic measurement, +Flex, Grid, Overlay, scroll, virtual-list, semantics, Talkback, display-list, +CPU-oracle, input, replay, and motion +slices. It does not yet +contain a usable renderer, window shell, component library, or supported platform +backend. Nothing in this repository is a Zagkit 1.0 release. + +## What Zagkit owns + +Zagkit will own the declarative view model, state tracking, reconciliation, +layout, semantics, input, animation, text, display lists, CPU rendering, +components, and developer tools. Public operating system APIs are narrow seams +for lifecycle, surfaces, input methods, accessibility, GPU submission, and +packaging. They do not define Zagkit's architecture. + +The deterministic CPU renderer will be the visual oracle. Metal, D3D12, and +public Linux and Android GPU APIs will carry a Zag owned render IR. Backend +selection, fallback, and device loss will always be observable through a +capability record. + +Zagkit is not a wrapper around native widgets and will not ship Skia, Flutter, +Qt, FreeType, HarfBuzz, a browser, or a WebView as its UI engine. System fonts +and published Unicode and OpenType data are allowed inputs. The exact boundary +is normative in [DEPENDENCIES.md](DEPENDENCIES.md). + +## Status + +| Area | Current state | Proof | +|---|---|---| +| Product and architecture contract | accepted | [RFC index](docs/rfcs/README.md) | +| Compiler dependency | pinned, prerequisites incomplete | [toolchain lock](contracts/toolchain.json) | +| Platform shells | unavailable | [support matrix](SUPPORT.md) | +| Headless core | experimental state, reconciliation, intrinsic measurement, geometry, Flex, Grid, Overlay, scroll, virtual collections, collection semantics, Talkback, canonical paths and images, display lists, CPU raster, input, replay, and motion | [headless test](tools/test-headless.sh) | +| Components and visual language | inventory only, visual review pending | [component inventory](contracts/components.json) | +| Flex and Zagkit Talkback | Flex foundation and in-process ID-first Talkback dispatch executing; native transport remains unavailable | [Talkback contract](docs/automation/talkback.md) | +| Benchmarks | scene specifications only, no results | [benchmark contract](benchmarks/README.md) | + +Run the repository contract gate with: + +```sh +./tools/check-contracts.sh +``` + +The gate validates the release identity, exact Zag revision, required platform +families, backend truth states, upstream prerequisite ledger, component +inventory, benchmark scene coverage, and the 1.0 block. + +The compiled headless contract currently provides revisioned `State`, +action-producing `Binding`, inherited integer environment values, exact +per-node state-read records, fail-visible keyed reconciliation, deterministic +fixed-point geometry, and single-line or wrapped Flex. These APIs are +experimental. A retained intrinsic tree now aggregates leaf, row, column, and +overlay size ranges, rejects invalid ownership and unstable same-revision +measurement, resolves constraints with explicit rules and overflow, and records +exact state-read layout causes. Typed environment values, reconciliation +cancellation, general retained child lifecycle, replay serialization, editable +collection behavior, and full breakpoint policy remain open; the corresponding Milestone +2 checklist items are not complete. See the +[measurement contract](docs/architecture/measurement.md). + +Flex now also provides an experimental primitive spacing scale, compact, +standard, and touch density resolution, physical safe-area composition, +text-scale-aware compact/medium/expanded breakpoints, and deterministic row or +column wrapping. Wrapped lines preserve logical stable-ID order while resolving +physical RTL placement, per-line growth and shrinkage, baseline-safe extents, +line alignment, overflow truth, and layout identity. These are headless +placement primitives rather than the still-unselected visual design language. +Grid now resolves fixed, intrinsic, and weighted fraction tracks, spans, +row-major auto placement, explicit collision policy, baseline placement, +content distribution, quantified overflow, and logical RTL columns. Overlay +resolves intrinsic containers, padding, child insets, alignment, z-order, +baseline output, overflow, and RTL start/end. Both contribute deterministic +identity to replay. End-to-end retained invalidation coverage and the full +adaptive matrix remain open, so `G2-FLEX` and `G2-FLEX-RTL` are not complete. +See the [Flex contract](docs/architecture/flex.md) and +[Grid and Overlay contract](docs/architecture/grid-and-overlay.md). + +Scroll now retains exact logical offsets, consumed and unconsumed gesture +deltas, reveal alignment, anchor-preserving content reconciliation, RTL visible +coordinates, revisions, and deterministic identity. Virtual List performs +binary visible-range selection across up to one billion stable IDs, supports +sparse variable extents and anchor correction, and hard-limits live placement +allocation. The focused proof queries the middle of one million rows with nine +live placements and fewer than 64 examined records. This is correctness and +bounded-work evidence, not a 120 Hz hardware result. See the +[scroll and virtualization contract](docs/architecture/scroll-and-virtualization.md). + +Table now composes bounded row and column virtualization with pinned logical +columns, exact resize consumption, composite stable cell IDs, RTL placement, +and hard cell residency limits. Tree validates preorder ownership and expansion +before projecting collapsed descendants into the virtual range. A retained +recycling store preserves offscreen focus and restores the exact instance and +generation when its logical ID returns. These identities participate in replay. +Sorting, selection, editing, drag reordering, component semantics, and native +interaction remain open. + +The semantics slice owns copied names, descriptions, and values; stable keys; +logical bounds; label, description, and control relationships; explicit action +capabilities; deterministic focus order; live-region state; ranges; selection; +text navigation; collection coordinates and counts; tree levels; set position; +and expansion truth. Invalid geometry, dangling relationships, collection +metadata, parents, IDs, focus order, ranges, and selections fail visibly before +tree mutation. Virtual Table and Tree projection retain full logical counts +while materializing only live semantic rows and cells. Composite collection +identities are deterministically mapped to `NodeKey` and collision-preflighted +before any tree mutation. Native accessibility adapters remain open. + +The in-process Zagkit Talkback dispatcher resolves queries and emits validated +actions against those semantic IDs. Query responses expose role, actions, +fixed-point bounds, collection coordinates and counts, tree state, owned-text +lengths, state flags, and a deterministic semantic evidence hash. Dispatch +rejects stale revisions and unavailable actions, keeps pixel fallback disabled +unless explicitly advertised, applies recorded display scale to pixel bounds, +and logs accepted and rejected requests in one ordered stream. +It is not yet a native automation transport; the exact available and unavailable +surface is documented in [the protocol contract](docs/automation/talkback.md). + +The immutable display-list slice records retained ownership, clips, transforms, +fixed-point geometry, RGBA16 paints, paths, images, glyph runs, layers, and +effects as explicit operations. Invalid geometry, resources, parameters, and +stack balance fail before mutation; the builder rejects writes after sealing, +and verification detects out-of-contract raw mutation against deterministic +content identity. An integrated experimental resource store owns opaque typed +payload bytes with stable IDs, canonical order, bounded allocation, exact +replacement revisions, sealing, and mutation verification. Display-list seal +requires every reference to resolve the exact resource kind. Path resources use +a bounded immutable command builder and canonical `ZKPATH01` version 1 payload; +fill rules, moves, lines, quadratic and cubic curves, contour closure, sequence, +geometry, identity, truncation, and trailing bytes are validated before use. +The version 2 +binary codec round-trips resource metadata, payloads, revisions, allocation +policy, and operations byte-identically and rejects malformed, truncated, +noncanonical, unknown-version, and hash-mismatched input. Scale-adaptive path +coverage, remaining encoded-image formats, glyph interpretation, damage, +complete CPU rasterization, GPU transport, schema evolution, and fuzz coverage +remain open. +See the [path contract](docs/architecture/paths.md) and +[image contract](docs/architecture/images.md). + +The first CPU-oracle subset rasterizes fixed-point rectangle fills, centered +rectangle strokes, and canonical path fills with exact clip and axis-aligned +transform state, analytic rectangle coverage, deterministic 8 by 8 path +coverage, non-zero and even-odd winding, bounded curve flattening, and +source-over alpha into owned RGBA8 surfaces. Path edge and work budgets fail +before pixel mutation. Canonical decoded sRGB RGBA8 images render with exact +one-to-one texels, premultiplied bilinear scaling, fractional-edge coverage, +clip and transform state, and operation opacity. Unsupported color conversion, +glyphs, path strokes, skew, layers, and effects fail at the exact operation; +this subset is not yet the complete CPU renderer required by Milestone 2. + +Verified CPU surfaces now serialize to deterministic PNG snapshot bytes with +an explicit sRGB chunk, exact RGBA8 rows, valid CRC32 chunks, and a pure-Zag +stored-zlib IDAT stream. Identical pixels produce byte-identical files without +timestamps or host metadata. The pure-Zag headless reference runner persists a +320 by 200 conformance scene twice and the standard gate verifies signature, +nonempty size, and byte identity. Manifests, golden comparison, Talkback +evidence bundles, and native screenshot capture remain open, so this does not +yet claim the complete snapshot runner or screenshot release gate. See the +[PNG snapshot contract](docs/quality/png-snapshots.md). + +Encoded PNG assets now enter the same canonical image boundary through a +bounded pure-Zag decoder. It validates chunk spelling, order, CRCs, complete +termination, and exact decompressed size; supports stored, fixed, and dynamic +DEFLATE; reverses all five PNG filters; and expands legal grayscale, RGB, +indexed, grayscale-alpha, and RGBA bit depths with PLTE and tRNS into owned +sRGB RGBA8. Both noninterlaced rows and all seven Adam7 passes are supported. +Focused contracts pass identically on x86-64 and ARM64/qemu, an independent +240-file corpus covers every accepted color and bit-depth family across five +zlib strategies, and an independently encoded Adam7 image matches exact source +pixels. End-to-end decoded-resource CPU goldens pass at 1x, 1.25x, 1.5x, 2x, +and 3x on both compiled architectures. Non-sRGB gamma transfer curves and +arbitrary valid `cHRM` primaries now convert deterministically to sRGB, including +Bradford white-point adaptation; eight full transfer ramps and Display P3/D50 +fixtures match independent references. Bounded `iCCP` ingestion executes ICC +v2/v4 RGB matrix profiles in XYZ PCS with shared identity, gamma, or ICC +parametric curve types 0 through 4 or sampled curves of up to 4,096 entries per +channel. It also executes bounded RGB-to-XYZ `A2B0` `lut16Type` profiles with +owned input tables, a trilinearly interpolated 3D CLUT, and output tables. +The first `lutAToBType` path executes the permitted A-curves, CLUT, B-curves +combination with independent embedded curves, per-axis grid sizes, and 8-bit or +16-bit CLUT precision. +Malformed wrappers and profile or table bounds fail explicitly. A deterministic +60,000-input PNG/ICC validation harness plus 4,096 general, sampled-profile, +LUT16, and `lutAToBType` cleanup cycles passes on both architectures. The +`lutAToBType` matrix combinations, Lab PCS, and coverage-guided sanitizer +evidence remain open, so the `G3-PNG` +completion box remains deliberately unchecked. See the +[image and PNG ingestion contract](docs/architecture/images.md). + +The first input slice resolves full affine transforms back to local coordinates, +honors local clips and z-order, rejects singular or malformed hit nodes, and +routes pointer phases through explicit capture and focus truth. Capture loss, +pointer-up release, stale targets, misses, and invalid pointers remain visible +in one ordered event stream. Keyboard, touch arbitration, wheel payloads, +gestures, and platform input adaptation remain open. + +The first replay slice owns a sealed ordered tape for exact state revisions, +pointer phases, monotonic time, backend activation, device loss, and recovery. +Executing the same tape regenerates identical motion, semantic, measurement, +Flex, display-list, and CPU identities; raw mutation, stale revisions, clock +regression, and invalid backend transitions fail before they can become +evidence. The current scene is an executable conformance reference, not yet a +general application callback boundary or a versioned replay-file format, so +`G2-REPLAY` remains open. + +The experimental motion kernel uses supplied monotonic microseconds and the +same 26.6 logical units as layout. Its refresh-aware scheduler records why +frames exist, stops requesting frames when every track settles, and refuses +clock advances that would skip live motion. Fixed-step integer springs preserve +position and velocity through interruption, reversal, resize, and gesture +handoff. Owned keyframe timelines expose exact segment velocity. Reduced motion +uses explicit snap or bounded opacity-fade substitutions rather than a global +duration multiplier. Layout/shared-transition orchestration, easing curves, +platform vsync adapters, retained event-log bounds, and performance evidence +remain open, so `G3-MOTION` and `G3-REDUCED-MOTION` are not complete. See the +[motion contract](docs/architecture/motion.md). + +Run the deterministic headless foundation test with: + +```sh +./tools/test-headless.sh +``` + +The script compiles every executable contract with strict Zag semantic +analysis. CI checks out the exact compiler revision from the toolchain contract, +self-hosts that pinned source to a byte-identical compiler fixed point, and runs +the same suite before validating repository metadata. The committed upstream +seed is bootstrap authority, not evidence that it already contains later +compiler-source fixes. + +CI allows 30 minutes for a cold fixed-point rebuild and caches only `znc` and +`zagd` under the exact operating system and Zag source SHA. A source-revision +change cannot reuse an older compiler cache; warm runs still execute every +strict Zagkit contract. + +## Build order + +1. Advance reusable compiler, ABI, concurrency, package, and platform features + in [Zag](https://github.com/Sylorlabs/zag), each with native executable proof. +2. Build Zagkit's deterministic headless core. +3. Ship a polished Linux reference, then completely rebuild PrismStudio's UI on Zagkit. +4. Reach desktop parity on macOS and Windows. +5. Reach mobile parity on iOS and Android. +6. Call the shared product 1.0 only after all five families pass the same gate. + +See [ROADMAP.md](ROADMAP.md) for milestone exit conditions and +[CONTRIBUTING.md](CONTRIBUTING.md) before proposing implementation work. + +## License + +Apache License 2.0. See [LICENSE](LICENSE). diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..984ff0f --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,114 @@ +# Delivery roadmap + +The milestones are ordered by evidence dependency. Public previews may ship +incrementally. A milestone is complete only when every exit condition is linked +to executable evidence. + +## 0. Product contract + +**Status: implemented in this branch, pending review.** + +- Separate Apache 2.0 repository and governance. +- Semantic version and honest experimental label. +- Accepted architecture, dependency, backend truth, semantics, and release + RFCs. +- Machine readable platform matrix, component inventory, benchmark scene + inventory, compiler pin, and upstream Zag prerequisite ledger. +- Executable contract validation in CI. +- Explicit visual direction and accessibility review gate. +- CI-checked master goal checklist covering Flex, Zagkit Talkback, visual and + asset fidelity, and the complete PrismStudio overhaul. + +This milestone creates no runtime support. Its completion cannot promote any +platform capability. + +## 1. Advance Zag + +**Status: blocked by the entries in +[contracts/upstream-zag.json](contracts/upstream-zag.json).** + +Upstream native object targets and ABI seams for Darwin, Windows, iOS, Android, +Objective C, COM, JNI, callbacks, and aggregates. Add resource embedding, +dynamic platform loading, event loop and worker primitives, package resolution, +incremental compilation, and reload hooks. Every slice requires a Zag source +regression, native executable conformance on its target, cleanup evidence, and +affected Zagkit conformance. + +## 2. Headless core + +**Status: experimental foundation in progress.** + +Strict native Zag contracts currently execute state and dependency tracking, +keyed reconciliation, bounded geometry, retained intrinsic measurement, +adaptive and wrapped Flex, semantics, ID-first in-process Talkback dispatch, +immutable display lists and their codec, a shape and image CPU-oracle subset, +transformed pointer routing, deterministic replay, and motion kernels. This is +real headless execution but does not satisfy the milestone exit gate yet. + +Implement state dependencies, reconciliation, layout, semantics, immutable +display lists, deterministic CPU rasterization, Unicode and OpenType text, +animation clocks, input routing, hit testing, snapshots, and deterministic +replay without a window system. Exit requires unit, property, fuzz, +differential, golden, and replay gates. + +Flex is the common placement and spacing system. Zagkit Talkback is built over +retained node and semantic identity with ID-first interaction and observable, +scale-aware pixel fallback. Fonts, semantic colors, SVG, PNG, lighting, soft +shadows, and adaptive liquid-glass materials are renderer and asset-pipeline +requirements, including designed reduced-effects variants. + +## 3. Linux preview + +**Status: not started.** + +Implement Wayland first with X11 fallback, multi window and monitor handling, +fractional scaling, clipboard, IME, AT-SPI, CPU presentation, and one explicit +public GPU transport. Exit requires live native execution and fail closed +capability reporting on Ubuntu LTS and current Fedora baselines for x86-64 and +ARM64 where the compiler target exists. + +Linux is the first polish reference: modern density-independent output, exact +spacing, crisp curves and assets, coherent motion and glass, native +accessibility, Talkback automation, recovery, screenshots at all declared +scales, and packaging must have no known severity-one or severity-two defects. + +## 4. PrismStudio dogfood + +**Status: not started.** + +Replace the complete visible PrismStudio UI with Zagkit as the first complex +consumer. Preserve and modernize every CAD workflow, direct manipulation path, +table, tree, menu, keyboard command, dialog, viewport behavior, performance +contract, and explicit physical GPU safety boundary. Apply the selected Zagkit +direction, Flex spacing, production fonts and colors, SVG and PNG assets, +lighting, soft shadows, motion, and liquid-glass materials. Every actionable +node receives a stable Talkback ID. Native screenshot comparisons are required +alongside semantics, AT-SPI, interaction, recovery, and performance evidence. + +## 5. Desktop parity + +**Status: not started.** + +Add macOS and Windows shells, desktop adaptation, native IME and accessibility, +menus, clipboard, drag and drop, packaging, signing, Metal, and D3D12. Public +betas require device execution with VoiceOver and Narrator plus recovery and +packaging gates. + +## 6. Mobile parity + +**Status: not started.** + +Add iOS and Android shells, lifecycle restoration, rotation, safe areas, +touch-first navigation, gestures, soft keyboard, haptics, mobile accessibility, +Metal and Android GPU transports, packaging, and a focused reference app. +Public betas require device execution with VoiceOver and TalkBack. + +## 7. All platform 1.0 + +**Status: blocked.** + +Freeze the initial API only after the shared component suite, PrismStudio, +mobile reference app, international text, accessibility, recovery, performance, +native packaging, and reproducible release gates pass across Linux, macOS, +Windows, iOS, and Android. Unsupported and untested hardware stays visibly +unavailable. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..68c8623 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security policy + +Zagkit is pre release software and currently has no supported runtime. Security +issues in repository automation, specifications, or future implementation +should be reported privately through the Sylor Labs GitHub organization contact +or GitHub private vulnerability reporting when it is enabled for this +repository. + +Please include the affected revision, platform, backend, reproduction, impact, +and whether the issue crosses a process, GPU, accessibility, input, font, or +package boundary. Do not include secrets or destructive proof when a bounded +reproduction is sufficient. + +No version is currently eligible for security support. A beta or stable release +will replace this paragraph with its exact support window and response policy. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..8ea6de4 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,49 @@ +# Platform support + +Zagkit currently supports no runtime platform. The repository is an +experimental product contract and all shell, renderer, accessibility, text +input, GPU, and packaging capabilities are `unavailable`. + +The machine readable authority is +[contracts/platforms.json](contracts/platforms.json). Future release artifacts +will contain a generated instance of the same capability model with one of +three states: + +- `supported`: the feature passed its complete native gate on a named target. +- `experimental`: executable evidence exists, but the support gate is + incomplete and the missing condition is named. +- `unavailable`: the backend did not activate or has no qualifying evidence; + the reason is mandatory. + +`.auto` is a selection request, not a capability. A runtime using `.auto` must +record the backend actually activated. Device loss and fallback append +observable events and update the capability record. + +## Intended beta policy + +When a backend first enters beta, its matrix freezes exact OS versions and +reference devices: + +| Family | Intended policy | Current Zagkit state | +|---|---|---| +| Linux | current Ubuntu LTS and current Fedora releases on x86-64 and ARM64 | unavailable | +| macOS | current and previous two stable major releases | unavailable | +| Windows | current and previous two supported stable releases | unavailable | +| iOS | current and previous two stable major releases | unavailable | +| Android | current and previous two stable API generations | unavailable | + +Version names are deliberately not frozen before beta. Recording a date based +guess here would look precise while providing no tested support. Every beta and +stable release instead records the exact versions used by its native evidence. + +Web, browser rendering, and WebView hosting are outside the 1.0 scope. + +## Promotion gate + +A capability cannot move out of `unavailable` based on a code path, +cross-compile, screenshot, emulator, or API discovery. Promotion requires the +target device, exact OS and compiler revisions, executable test, selected +backend, assistive technology where relevant, cleanup, and recovery result. + +The whole product remains pre-1.0 until all five required platform families +pass [the release gates](docs/quality/release-gates.md). diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..8c97348 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,46 @@ +# Versioning and release channels + +Zagkit uses Semantic Versioning 2.0.0. + +## Version meaning before 1.0 + +- `0.MINOR.PATCH-experimental.N` may change any API and may have no usable + platform backend. +- `0.MINOR.PATCH-alpha.N` provides coherent end to end slices for named + platforms, but important APIs and recovery behavior remain incomplete. +- `0.MINOR.PATCH-beta.N` freezes the intended API for named platforms and has + native accessibility, text input, packaging, recovery, and performance + evidence. Defects can still block production use. +- `1.0.0` requires the common gate on all five platform families. There is no + platform specific early 1.0. + +A release channel describes maturity, not capability. The generated capability +record remains authoritative. + +## Compatibility + +Before 1.0, breaking API changes require a changelog entry and migration note. +After 1.0, public source API compatibility follows SemVer. Serialization, +replay, snapshot, render IR, plugin, and binary compatibility each remain +separately versioned contracts and must not be inferred from the package +version. + +The Zag ABI is currently unstable. Every Zagkit release pins an exact Zag +commit and edition in [contracts/toolchain.json](contracts/toolchain.json). +Consumers rebuild Zagkit and its Zag dependencies with that toolchain unless a +release explicitly proves a compatible range. + +## Release contents + +Every release must include: + +- exact Zagkit and Zag revisions; +- backend capability record, including `.auto` selections and fallbacks; +- reference hardware and operating system versions; +- benchmark and quality gate results; +- known unsupported features and untested hardware; +- reproducible build inputs and artifact hashes; +- packaging install, launch, update, and uninstall evidence where applicable. + +Until the release automation can produce this evidence, releases remain +experimental and source only. diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..9d2564b --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,19 @@ +# Benchmark scene specifications + +[benchmark-scenes.json](../contracts/benchmark-scenes.json) defines the first +canonical workloads and global performance contract. These are specifications, +not benchmark implementations or results. + +Every implemented scene will have: + +- deterministic seed and replay stream; +- declared variant coverage; +- reference output or semantic assertions; +- warmup, duration, and sample policy; +- frame reason, CPU, GPU, memory, allocation, resource, and input latency traces; +- exact Zagkit, Zag, OS, device, driver, display, and power metadata; +- cleanup and idle observation periods. + +Raw run artifacts live outside source control until reviewed. Published summary +data links to immutable raw evidence. A missing metric stays missing rather than +being encoded as zero. No single screenshot or one-off timing is a result. diff --git a/contracts/benchmark-scenes.json b/contracts/benchmark-scenes.json new file mode 100644 index 0000000..0988ddd --- /dev/null +++ b/contracts/benchmark-scenes.json @@ -0,0 +1,152 @@ +{ + "schema_version": 1, + "generated_for": "0.1.0-experimental.0", + "result_state": "specifications-only", + "warning": "No performance result is recorded or implied by this manifest.", + "variant_matrix": { + "scale_factors": [1.0, 1.25, 1.5, 2.0, 3.0], + "themes": ["light", "dark"], + "contrast": ["standard", "high"], + "directions": ["ltr", "rtl"], + "text_scales": [1.0, 1.3, 2.0], + "motion": ["full", "reduced"], + "locales": ["en-US", "ar-EG", "he-IL", "hi-IN", "ja-JP", "th-TH", "zh-Hant-TW"] + }, + "global_performance_contract": { + "frame_deadline_p99_fraction_max": 1.0, + "scroll_refresh_hz_on_supported_reference_hardware": 120, + "idle_layouts_per_second_max": 0, + "idle_paints_per_second_max": 0, + "unexplained_two_frame_stalls_per_ten_minutes_max": 0, + "regression_without_reviewed_waiver_percent_max": 5.0, + "canonical_run_minutes": 10 + }, + "scenes": [ + { + "id": "type-ramp", + "name": "International type ramp", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Resize through breakpoints while changing text scale and locale.", + "stresses": ["font-fallback", "variable-fonts", "color-fonts", "line-breaking", "intrinsic-layout"], + "assertions": ["deterministic-golden", "baseline-stability", "no-missing-glyph-with-declared-fonts", "semantic-reading-order"] + }, + { + "id": "bidi-editor", + "name": "Bidirectional text editor", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Compose, select, replace, undo, and navigate mixed Arabic, Hebrew, Latin, emoji, and combining text.", + "stresses": ["bidi", "segmentation", "ime", "selection", "editing", "malformed-input"], + "assertions": ["caret-roundtrip", "selection-stability", "ime-commit", "text-navigation-semantics", "deterministic-replay"] + }, + { + "id": "million-row-list", + "name": "Million row virtual list", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Fling, reverse, resize, select, and keyboard navigate a variable height million row list.", + "stresses": ["virtualization", "scrolling", "reconciliation", "measurement", "focus"], + "assertions": ["120hz-reference-scroll", "stable-node-identity", "bounded-live-nodes", "semantic-range", "no-idle-work"] + }, + { + "id": "gesture-handoff", + "name": "Nested gesture handoff", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Transfer touch and pen motion among nested scroll, drag, zoom, and edge navigation recognizers.", + "stresses": ["gesture-arbitration", "velocity", "touch", "pen", "nested-scroll"], + "assertions": ["continuous-position", "single-winner", "cancellation-cleanup", "deterministic-replay"] + }, + { + "id": "interruptible-transitions", + "name": "Interruptible transition lab", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Interrupt, reverse, resize, and hand off spring, keyframe, layout, and shared transitions.", + "stresses": ["animation", "layout-transition", "shared-transition", "gesture-handoff", "resize"], + "assertions": ["position-continuity", "velocity-continuity", "reduced-motion-substitution", "deterministic-replay", "no-two-frame-stall"] + }, + { + "id": "adaptive-dashboard", + "name": "Adaptive dashboard", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Resize continuously across compact, medium, and expanded layouts in LTR and RTL.", + "stresses": ["grid", "flex", "breakpoints", "safe-areas", "rtl", "large-text"], + "assertions": ["no-overflow", "focus-order", "exact-invalidation-reason", "semantic-order", "no-idle-work"] + }, + { + "id": "semantic-form", + "name": "Semantic settings form", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Complete validation, errors, help, ranges, live updates, and grouped controls with keyboard and automation only.", + "stresses": ["semantics", "focus", "live-regions", "forms", "large-text"], + "assertions": ["role-name-value", "action-parity", "focus-order", "live-region-order", "screen-reader-script"] + }, + { + "id": "cad-viewport", + "name": "PrismStudio viewport and chrome", + "milestone": 4, + "status": "specification", + "canonical_interaction": "Orbit, pan, zoom, select, edit properties, use menus, and scrub a dense CAD scene.", + "stresses": ["viewport", "canvas", "tables", "menus", "direct-manipulation", "gpu-transport"], + "assertions": ["input-latency", "cpu-oracle-tolerance", "backend-event-log", "keyboard-equivalence", "resource-cleanup"] + }, + { + "id": "multi-window-scale", + "name": "Multi window scale migration", + "milestone": 3, + "status": "specification", + "canonical_interaction": "Create, resize, move, suspend, restore, and close windows across monitors with different scale and refresh rates.", + "stresses": ["multi-window", "multi-monitor", "fractional-scale", "surface-loss", "refresh-rate"], + "assertions": ["physical-size-continuity", "surface-recovery", "backend-event-log", "resource-cleanup", "no-idle-work"] + }, + { + "id": "layer-effects", + "name": "Layer and effects stress", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Animate clipped transformed layers with paths, glyphs, images, opacity, blur, and shadows.", + "stresses": ["display-list", "clipping", "transforms", "layers", "effects", "rasterization"], + "assertions": ["deterministic-golden", "cpu-oracle", "gpu-tolerance", "bounded-resources", "damage-correctness"] + }, + { + "id": "material-fidelity", + "name": "Lighting, shadow, and liquid-glass fidelity", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Move, overlap, resize, interrupt, and scroll translucent materials across changing light and content.", + "stresses": ["backdrop", "blur", "refraction", "lighting", "shadows", "color-management", "animation"], + "assertions": ["crisp-all-scales", "text-legibility", "reduced-transparency-substitution", "cpu-oracle", "gpu-tolerance", "no-two-frame-stall"] + }, + { + "id": "asset-fidelity", + "name": "Font, SVG, and PNG fidelity", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Zoom and rescale international text, SVG icons, and color-managed PNG images through every declared density.", + "stresses": ["font-rasterization", "svg", "png", "color-space", "alpha", "scaling", "malformed-assets"], + "assertions": ["no-pixelation", "color-profile-correctness", "bounded-decoding", "deterministic-golden", "semantic-labels"] + }, + { + "id": "talkback-native-automation", + "name": "Zagkit Talkback native automation", + "milestone": 2, + "status": "specification", + "canonical_interaction": "Discover, query, click, type, scroll, drag, wait, assert, capture, and replay a multi-window app by stable node ID.", + "stresses": ["stable-ids", "semantics", "virtualization", "reload", "multi-window", "timeouts", "pixel-fallback"], + "assertions": ["id-first-actions", "structured-failures", "fallback-observability", "deterministic-replay", "capability-truth"] + }, + { + "id": "prismstudio-complete-ui", + "name": "Complete PrismStudio Zagkit experience", + "milestone": 4, + "status": "specification", + "canonical_interaction": "Complete representative CAD editing, inspection, menu, dialog, navigation, and recovery workflows in the fully redesigned native application.", + "stresses": ["flex", "talkback", "materials", "assets", "viewport", "tables", "accessibility", "recovery"], + "assertions": ["no-legacy-ui-path", "id-driven-workflows", "matched-screenshot-set", "at-spi", "keyboard-equivalence", "no-rough-edges"] + } + ] +} diff --git a/contracts/components.json b/contracts/components.json new file mode 100644 index 0000000..0b33c97 --- /dev/null +++ b/contracts/components.json @@ -0,0 +1,74 @@ +{ + "schema_version": 1, + "generated_for": "0.1.0-experimental.0", + "status_vocabulary": ["planned", "implementing", "experimental", "conformant"], + "conformance_dimensions": [ + "semantics", + "keyboard", + "pointer", + "touch", + "focus", + "rtl", + "large_text", + "high_contrast", + "reduced_motion", + "snapshot" + ], + "visual_direction_gate": { + "state": "blocked", + "decision": "docs/design/visual-direction.md", + "rule": "Visual component production cannot begin until visual direction and accessibility review are accepted." + }, + "components": [ + {"id":"text","family":"content","name":"Text","milestone":2,"status":"planned","semantic_roles":["text"],"inputs":[],"adaptive":["locale","direction","text_scale"]}, + {"id":"rich-text","family":"content","name":"RichText","milestone":2,"status":"planned","semantic_roles":["text","link"],"inputs":["pointer","keyboard","touch"],"adaptive":["locale","direction","text_scale"]}, + {"id":"image","family":"content","name":"Image","milestone":2,"status":"planned","semantic_roles":["image"],"inputs":[],"adaptive":["density","contrast"]}, + {"id":"icon","family":"content","name":"Icon","milestone":2,"status":"planned","semantic_roles":["image","none"],"inputs":[],"adaptive":["density","contrast","direction"]}, + {"id":"svg","family":"content","name":"Svg","milestone":2,"status":"planned","semantic_roles":["image","none"],"inputs":[],"adaptive":["density","contrast","direction","color_space"]}, + {"id":"png-image","family":"content","name":"PngImage","milestone":2,"status":"planned","semantic_roles":["image"],"inputs":[],"adaptive":["density","contrast","color_space"]}, + {"id":"material","family":"effects","name":"Material","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["contrast","color_space","reduced_transparency"]}, + {"id":"glass","family":"effects","name":"Glass","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["contrast","reduced_motion","reduced_transparency"]}, + {"id":"shadow","family":"effects","name":"Shadow","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["density","contrast","color_space"]}, + {"id":"light","family":"effects","name":"Light","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["contrast","color_space","reduced_transparency"]}, + {"id":"divider","family":"content","name":"Divider","milestone":2,"status":"planned","semantic_roles":["separator"],"inputs":[],"adaptive":["density","contrast","direction"]}, + {"id":"button","family":"actions","name":"Button","milestone":2,"status":"planned","semantic_roles":["button"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","contrast","reduced_motion"]}, + {"id":"toggle","family":"actions","name":"Toggle","milestone":2,"status":"planned","semantic_roles":["switch"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","contrast","reduced_motion"]}, + {"id":"checkbox","family":"actions","name":"Checkbox","milestone":2,"status":"planned","semantic_roles":["checkbox"],"inputs":["pointer","keyboard","touch"],"adaptive":["density","platform","contrast"]}, + {"id":"radio","family":"actions","name":"RadioGroup","milestone":2,"status":"planned","semantic_roles":["radio","radiogroup"],"inputs":["pointer","keyboard","touch"],"adaptive":["density","platform","direction"]}, + {"id":"slider","family":"actions","name":"Slider","milestone":2,"status":"planned","semantic_roles":["slider"],"inputs":["pointer","keyboard","touch","pen","gamepad"],"adaptive":["density","platform","direction","reduced_motion"]}, + {"id":"stepper","family":"actions","name":"Stepper","milestone":2,"status":"planned","semantic_roles":["spinbutton"],"inputs":["pointer","keyboard","touch"],"adaptive":["density","platform","direction"]}, + {"id":"progress","family":"feedback","name":"Progress","milestone":2,"status":"planned","semantic_roles":["progressbar"],"inputs":[],"adaptive":["contrast","direction","reduced_motion"]}, + {"id":"activity","family":"feedback","name":"ActivityIndicator","milestone":2,"status":"planned","semantic_roles":["status"],"inputs":[],"adaptive":["contrast","reduced_motion"]}, + {"id":"badge","family":"feedback","name":"Badge","milestone":2,"status":"planned","semantic_roles":["status"],"inputs":[],"adaptive":["density","locale","direction","text_scale"]}, + {"id":"text-field","family":"input","name":"TextField","milestone":2,"status":"planned","semantic_roles":["textbox"],"inputs":["pointer","keyboard","touch","pen","ime"],"adaptive":["density","platform","locale","direction","text_scale"]}, + {"id":"secure-field","family":"input","name":"SecureField","milestone":2,"status":"planned","semantic_roles":["textbox"],"inputs":["pointer","keyboard","touch","ime"],"adaptive":["density","platform","locale","direction","text_scale"]}, + {"id":"text-editor","family":"input","name":"TextEditor","milestone":2,"status":"planned","semantic_roles":["textbox"],"inputs":["pointer","keyboard","touch","pen","ime"],"adaptive":["density","platform","locale","direction","text_scale"]}, + {"id":"search-field","family":"input","name":"SearchField","milestone":2,"status":"planned","semantic_roles":["searchbox"],"inputs":["pointer","keyboard","touch","ime"],"adaptive":["density","platform","locale","direction","text_scale"]}, + {"id":"picker","family":"input","name":"Picker","milestone":2,"status":"planned","semantic_roles":["combobox","listbox"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"color-picker","family":"input","name":"ColorPicker","milestone":4,"status":"planned","semantic_roles":["group","slider"],"inputs":["pointer","keyboard","touch","pen"],"adaptive":["density","platform","contrast"]}, + {"id":"link","family":"navigation","name":"Link","milestone":2,"status":"planned","semantic_roles":["link"],"inputs":["pointer","keyboard","touch"],"adaptive":["platform","contrast","direction"]}, + {"id":"tabs","family":"navigation","name":"Tabs","milestone":2,"status":"planned","semantic_roles":["tab","tablist","tabpanel"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"navigation-stack","family":"navigation","name":"NavigationStack","milestone":2,"status":"planned","semantic_roles":["navigation"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["breakpoint","platform","direction","reduced_motion"]}, + {"id":"split-view","family":"navigation","name":"SplitView","milestone":2,"status":"planned","semantic_roles":["navigation","separator"],"inputs":["pointer","keyboard","touch"],"adaptive":["breakpoint","platform","direction","text_scale"]}, + {"id":"toolbar","family":"navigation","name":"Toolbar","milestone":2,"status":"planned","semantic_roles":["toolbar"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"list","family":"collections","name":"List","milestone":2,"status":"planned","semantic_roles":["list","listitem"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"virtual-list","family":"collections","name":"VirtualList","milestone":2,"status":"planned","semantic_roles":["list","listitem"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"table","family":"collections","name":"Table","milestone":2,"status":"planned","semantic_roles":["table","row","columnheader","cell"],"inputs":["pointer","keyboard","touch"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"tree","family":"collections","name":"Tree","milestone":2,"status":"planned","semantic_roles":["tree","treeitem"],"inputs":["pointer","keyboard","touch"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"grid","family":"collections","name":"Grid","milestone":2,"status":"planned","semantic_roles":["grid","row","gridcell"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","breakpoint","direction","text_scale"]}, + {"id":"dialog","family":"overlays","name":"Dialog","milestone":2,"status":"planned","semantic_roles":["dialog"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["breakpoint","platform","direction","text_scale","reduced_motion"]}, + {"id":"sheet","family":"overlays","name":"Sheet","milestone":2,"status":"planned","semantic_roles":["dialog"],"inputs":["pointer","keyboard","touch","gesture"],"adaptive":["breakpoint","platform","direction","reduced_motion"]}, + {"id":"popover","family":"overlays","name":"Popover","milestone":2,"status":"planned","semantic_roles":["dialog"],"inputs":["pointer","keyboard","touch"],"adaptive":["breakpoint","platform","direction","text_scale"]}, + {"id":"tooltip","family":"overlays","name":"Tooltip","milestone":2,"status":"planned","semantic_roles":["tooltip"],"inputs":["pointer","keyboard"],"adaptive":["platform","direction","text_scale","reduced_motion"]}, + {"id":"menu","family":"overlays","name":"Menu","milestone":2,"status":"planned","semantic_roles":["menu","menuitem"],"inputs":["pointer","keyboard","touch","gamepad"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"context-menu","family":"overlays","name":"ContextMenu","milestone":2,"status":"planned","semantic_roles":["menu","menuitem"],"inputs":["pointer","keyboard","touch","pen"],"adaptive":["density","platform","direction","text_scale"]}, + {"id":"stack","family":"layout","name":"Stack","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["density","direction","text_scale"]}, + {"id":"flex","family":"layout","name":"Flex","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["density","breakpoint","direction","text_scale"]}, + {"id":"overlay","family":"layout","name":"Overlay","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["direction","text_scale"]}, + {"id":"scroll","family":"layout","name":"Scroll","milestone":2,"status":"planned","semantic_roles":["group"],"inputs":["pointer","keyboard","touch","pen","wheel","gamepad"],"adaptive":["density","platform","direction","reduced_motion"]}, + {"id":"safe-area","family":"layout","name":"SafeArea","milestone":2,"status":"planned","semantic_roles":["none"],"inputs":[],"adaptive":["platform","orientation"]}, + {"id":"canvas","family":"advanced","name":"Canvas","milestone":2,"status":"planned","semantic_roles":["group"],"inputs":["pointer","keyboard","touch","pen"],"adaptive":["density","contrast","direction"]}, + {"id":"viewport","family":"advanced","name":"Viewport","milestone":4,"status":"planned","semantic_roles":["application","group"],"inputs":["pointer","keyboard","touch","pen","wheel","gamepad","gesture"],"adaptive":["density","platform","contrast","reduced_motion"]}, + {"id":"command-palette","family":"advanced","name":"CommandPalette","milestone":4,"status":"planned","semantic_roles":["dialog","searchbox","listbox"],"inputs":["pointer","keyboard","touch"],"adaptive":["density","platform","locale","direction","text_scale"]} + ] +} diff --git a/contracts/platforms.json b/contracts/platforms.json new file mode 100644 index 0000000..8b42c9e --- /dev/null +++ b/contracts/platforms.json @@ -0,0 +1,119 @@ +{ + "schema_version": 1, + "generated_for": "0.1.0-experimental.0", + "generated_on": "2026-08-06", + "status_vocabulary": ["supported", "experimental", "unavailable"], + "required_capabilities": [ + "platform_shell", + "cpu_renderer", + "gpu_transport", + "text_input", + "accessibility", + "clipboard_drag_drop", + "multi_window", + "packaging", + "auto_backend_selection" + ], + "platforms": [ + { + "id": "linux", + "family": "Linux", + "architectures": ["x86_64", "arm64"], + "beta_os_policy": "Current Ubuntu LTS and current Fedora releases when the backend enters beta.", + "toolchain_state": "Pinned Zag supports x86-64 Linux and marks ARM64 Linux experimental with qemu-user execution evidence; physical ARM64 target proof is not recorded.", + "public_channel": "experimental", + "capabilities": [ + {"id":"platform_shell","state":"unavailable","reason":"Wayland and X11 shells are not implemented.","evidence":[]}, + {"id":"cpu_renderer","state":"unavailable","reason":"A headless rectangle CPU-oracle subset exists, but full display-list coverage and Linux surface presentation are not implemented.","evidence":["tests/cpu_raster_contract.zag","tests/replay_contract.zag"]}, + {"id":"gpu_transport","state":"unavailable","reason":"No Zagkit Linux GPU transport exists or has device evidence.","evidence":[]}, + {"id":"text_input","state":"unavailable","reason":"No Linux IME bridge or editing engine exists.","evidence":[]}, + {"id":"accessibility","state":"unavailable","reason":"No AT-SPI adapter exists or has assistive technology evidence.","evidence":[]}, + {"id":"clipboard_drag_drop","state":"unavailable","reason":"Clipboard and drag and drop seams are not implemented.","evidence":[]}, + {"id":"multi_window","state":"unavailable","reason":"Window, monitor, and scale lifecycle support is not implemented.","evidence":[]}, + {"id":"packaging","state":"unavailable","reason":"No installable Linux artifact or packaging gate exists.","evidence":[]}, + {"id":"auto_backend_selection","state":"unavailable","reason":"There are no eligible Zagkit backends for .auto to select.","evidence":[]} + ] + }, + { + "id": "macos", + "family": "macOS", + "architectures": ["arm64", "x86_64"], + "beta_os_policy": "Current and previous two stable major releases when the backend enters beta.", + "toolchain_state": "The pinned Zag revision has no Mach-O or Darwin target.", + "public_channel": "experimental", + "capabilities": [ + {"id":"platform_shell","state":"unavailable","reason":"The AppKit shell and Darwin target are not implemented.","evidence":[]}, + {"id":"cpu_renderer","state":"unavailable","reason":"A platform-independent headless rectangle CPU-oracle subset exists, but full display-list coverage and macOS presentation are not implemented.","evidence":["tests/cpu_raster_contract.zag","tests/replay_contract.zag"]}, + {"id":"gpu_transport","state":"unavailable","reason":"No Zagkit Metal transport exists or has device evidence.","evidence":[]}, + {"id":"text_input","state":"unavailable","reason":"No NSTextInputClient bridge or editing engine exists.","evidence":[]}, + {"id":"accessibility","state":"unavailable","reason":"No NSAccessibility adapter exists or has VoiceOver evidence.","evidence":[]}, + {"id":"clipboard_drag_drop","state":"unavailable","reason":"Clipboard and drag and drop seams are not implemented.","evidence":[]}, + {"id":"multi_window","state":"unavailable","reason":"Window, screen, and scale lifecycle support is not implemented.","evidence":[]}, + {"id":"packaging","state":"unavailable","reason":"No signed, notarized, installable artifact or packaging gate exists.","evidence":[]}, + {"id":"auto_backend_selection","state":"unavailable","reason":"There are no eligible Zagkit backends for .auto to select.","evidence":[]} + ] + }, + { + "id": "windows", + "family": "Windows", + "architectures": ["x86_64", "arm64"], + "beta_os_policy": "Current and previous two supported stable releases when the backend enters beta.", + "toolchain_state": "The pinned Zag revision has no PE/COFF or Windows target.", + "public_channel": "experimental", + "capabilities": [ + {"id":"platform_shell","state":"unavailable","reason":"The Win32 shell and Windows target are not implemented.","evidence":[]}, + {"id":"cpu_renderer","state":"unavailable","reason":"A platform-independent headless rectangle CPU-oracle subset exists, but full display-list coverage and Windows presentation are not implemented.","evidence":["tests/cpu_raster_contract.zag","tests/replay_contract.zag"]}, + {"id":"gpu_transport","state":"unavailable","reason":"No Zagkit D3D12 transport exists or has device evidence.","evidence":[]}, + {"id":"text_input","state":"unavailable","reason":"No Core Text input bridge or editing engine exists.","evidence":[]}, + {"id":"accessibility","state":"unavailable","reason":"No UI Automation adapter exists or has Narrator evidence.","evidence":[]}, + {"id":"clipboard_drag_drop","state":"unavailable","reason":"Clipboard and drag and drop seams are not implemented.","evidence":[]}, + {"id":"multi_window","state":"unavailable","reason":"Window, display, and scale lifecycle support is not implemented.","evidence":[]}, + {"id":"packaging","state":"unavailable","reason":"No signed installable artifact or packaging gate exists.","evidence":[]}, + {"id":"auto_backend_selection","state":"unavailable","reason":"There are no eligible Zagkit backends for .auto to select.","evidence":[]} + ] + }, + { + "id": "ios", + "family": "iOS", + "architectures": ["arm64"], + "beta_os_policy": "Current and previous two stable major releases when the backend enters beta.", + "toolchain_state": "The pinned Zag revision has no Mach-O, iOS ARM64, or Objective C ABI target.", + "public_channel": "experimental", + "capabilities": [ + {"id":"platform_shell","state":"unavailable","reason":"The UIKit shell and iOS target are not implemented.","evidence":[]}, + {"id":"cpu_renderer","state":"unavailable","reason":"A platform-independent headless rectangle CPU-oracle subset exists, but full display-list coverage and iOS presentation are not implemented.","evidence":["tests/cpu_raster_contract.zag","tests/replay_contract.zag"]}, + {"id":"gpu_transport","state":"unavailable","reason":"No Zagkit mobile Metal transport exists or has device evidence.","evidence":[]}, + {"id":"text_input","state":"unavailable","reason":"No UIKit text input bridge or editing engine exists.","evidence":[]}, + {"id":"accessibility","state":"unavailable","reason":"No UIKit accessibility adapter exists or has mobile VoiceOver evidence.","evidence":[]}, + {"id":"clipboard_drag_drop","state":"unavailable","reason":"Clipboard and drag and drop seams are not implemented.","evidence":[]}, + {"id":"multi_window","state":"unavailable","reason":"Scene, display, rotation, and safe area lifecycle support is not implemented.","evidence":[]}, + {"id":"packaging","state":"unavailable","reason":"No signed installable device artifact or packaging gate exists.","evidence":[]}, + {"id":"auto_backend_selection","state":"unavailable","reason":"There are no eligible Zagkit backends for .auto to select.","evidence":[]} + ] + }, + { + "id": "android", + "family": "Android", + "architectures": ["arm64"], + "beta_os_policy": "Current and previous two stable API generations when the backend enters beta.", + "toolchain_state": "The pinned Zag revision has no Android ARM64 or JNI ABI target.", + "public_channel": "experimental", + "capabilities": [ + {"id":"platform_shell","state":"unavailable","reason":"The Android shell and target are not implemented.","evidence":[]}, + {"id":"cpu_renderer","state":"unavailable","reason":"A platform-independent headless rectangle CPU-oracle subset exists, but full display-list coverage and Android presentation are not implemented.","evidence":["tests/cpu_raster_contract.zag","tests/replay_contract.zag"]}, + {"id":"gpu_transport","state":"unavailable","reason":"No Zagkit Android GPU transport exists or has device evidence.","evidence":[]}, + {"id":"text_input","state":"unavailable","reason":"No Android IME bridge or editing engine exists.","evidence":[]}, + {"id":"accessibility","state":"unavailable","reason":"No Android accessibility adapter exists or has TalkBack evidence.","evidence":[]}, + {"id":"clipboard_drag_drop","state":"unavailable","reason":"Clipboard and drag and drop seams are not implemented.","evidence":[]}, + {"id":"multi_window","state":"unavailable","reason":"Activity, display, rotation, and safe area lifecycle support is not implemented.","evidence":[]}, + {"id":"packaging","state":"unavailable","reason":"No signed installable device artifact or packaging gate exists.","evidence":[]}, + {"id":"auto_backend_selection","state":"unavailable","reason":"There are no eligible Zagkit backends for .auto to select.","evidence":[]} + ] + } + ], + "one_point_zero": { + "blocked": true, + "required_platforms": ["linux", "macos", "windows", "ios", "android"], + "reason": "No required platform currently has a supported capability set." + } +} diff --git a/contracts/talkback-protocol.json b/contracts/talkback-protocol.json new file mode 100644 index 0000000..c705715 --- /dev/null +++ b/contracts/talkback-protocol.json @@ -0,0 +1,98 @@ +{ + "name": "Zagkit Talkback", + "version": "0.2.0-experimental", + "android_assistive_technology": "Android TalkBack", + "transport": "not-yet-available", + "targeting": { + "primary": "semantic-id", + "id_scope": ["application", "window", "retained-node", "generation"], + "pixel_fallback": { + "state": "experimental", + "capability_gated": true, + "record_coordinates": true, + "record_scale": true, + "may_report_as_id_target": false + } + }, + "commands": [ + "discover", + "query", + "click", + "type", + "key", + "focus", + "scroll", + "drag", + "gesture", + "wait", + "assert", + "screenshot", + "timeline", + "capability-report", + "snapshot", + "replay" + ], + "statuses": [ + "accepted", + "invalid-request", + "invalid-timeout", + "stale-semantics", + "target-not-found", + "action-unavailable", + "pixel-fallback-disabled", + "pixel-out-of-bounds", + "unsupported-command" + ], + "request_fields": [ + "request-id", + "command", + "target-kind", + "target-id", + "semantics-revision", + "timeout-ms", + "pixel-x", + "pixel-y", + "scale-numerator", + "scale-denominator" + ], + "response_fields": [ + "request-id", + "status", + "target-kind", + "target-id", + "semantics-revision", + "event-sequence", + "resolved-node-index", + "result-count", + "emitted-action", + "resolved-role", + "action-mask", + "has-bounds", + "bounds", + "row-count", + "column-count", + "row-index", + "column-index", + "level", + "expanded", + "name-length", + "description-length", + "value-length", + "state-flags", + "evidence-hash" + ], + "event_contract": { + "ordered": true, + "records_accepted_and_rejected": true, + "retains_target_kind": true, + "retains_pixel_scale": true + }, + "current_runtime_truth": { + "semantic-query": "experimental", + "id-actions": "experimental-event-emission", + "pixel-fallback": "experimental-disabled-by-default", + "screenshots": "unavailable", + "replay": "unavailable", + "native-transport": "unavailable" + } +} diff --git a/contracts/toolchain.json b/contracts/toolchain.json new file mode 100644 index 0000000..32f292b --- /dev/null +++ b/contracts/toolchain.json @@ -0,0 +1,21 @@ +{ + "schema_version": 1, + "zagkit_version": "0.1.0-experimental.0", + "zag": { + "repository": "https://github.com/Sylorlabs/zag", + "resolved_ref": "refs/heads/agent/zagkit-prerequisites", + "commit": "43870455a07bf8e7d4adf38fad807fe3baee4e26", + "compiler_version": "2026.07.0-dev", + "edition": "2026" + }, + "audited_on": "2026-08-06", + "pin_policy": "Every Zagkit release replaces this record with the exact Zag source commit used to build and test it. Branch names and local dirty state are never release identities.", + "evidence": [ + "Zag commit 43870455a07bf8e7d4adf38fad807fe3baee4e26 was exported from the exact clean Git tree and audited on 2026-08-06.", + "zag-poc/zag.mod at the pinned commit declares version 2026.07.0-dev and edition 2026.", + "The clean pinned tree self-hosted to a byte-identical stage-2/stage-3 fixpoint with no external tools and rebuilt zagd.", + "The clean pinned tree self-hosts the generic numeric raw-pointer slice repair to a byte-identical compiler fixpoint; its aliasing, mutation, nested-slice, explicit-bound, and negative contracts pass on x86-64 and qemu-user ARM64.", + "The clean pinned tree passes compiler-owned resource embedding conformance on x86-64 and qemu-user ARM64, including parser, formatter, cache, and deterministic identity cases.", + "The pinned tree provides pure-Zag CRC32, Adler32, deterministic stored-zlib output, and general stored/fixed/dynamic DEFLATE inflate; the same 14-check suite passes on x86-64 and qemu-user ARM64." + ] +} diff --git a/contracts/upstream-zag.json b/contracts/upstream-zag.json new file mode 100644 index 0000000..3faf29a --- /dev/null +++ b/contracts/upstream-zag.json @@ -0,0 +1,146 @@ +{ + "schema_version": 1, + "audited_repository": "https://github.com/Sylorlabs/zag", + "audited_commit": "43870455a07bf8e7d4adf38fad807fe3baee4e26", + "audited_on": "2026-08-06", + "state_vocabulary": ["available", "partial", "missing"], + "evidence_rule": "Available requires native executable conformance at the audited commit. Source presence, a cross compile, or a dirty working tree does not qualify.", + "entries": [ + { + "id": "target-linux-arm64", + "category": "object-target", + "state": "partial", + "needed_by": [3, 7], + "evidence": "zag-poc/VERSIONING.md marks ARM64 Linux experimental and cites qemu-user execution, while unsupported numeric, debug, and call cases remain and no physical ARM64 evidence is recorded.", + "exit_gate": "Pure Zag ARM64 ELF generation, ABI corpus, native device execution, self-hosting or documented bootstrap boundary, and cleanup pass." + }, + { + "id": "target-darwin-macho", + "category": "object-target", + "state": "missing", + "needed_by": [5, 6, 7], + "evidence": "zag-poc/VERSIONING.md calls macOS not planned yet and requires a Mach-O backend.", + "exit_gate": "Mach-O x86-64 and ARM64 executables run natively on declared macOS versions with ABI and lifecycle conformance." + }, + { + "id": "target-windows-pe-coff", + "category": "object-target", + "state": "missing", + "needed_by": [5, 7], + "evidence": "zag-poc/VERSIONING.md calls Windows not planned and requires a PE/COFF backend.", + "exit_gate": "PE/COFF x86-64 and ARM64 executables run natively with Windows ABI, unwind, resource, and lifecycle conformance." + }, + { + "id": "target-ios-arm64", + "category": "object-target", + "state": "missing", + "needed_by": [6, 7], + "evidence": "No iOS target or native conformance suite exists at the pinned commit.", + "exit_gate": "Signed Zag executable code runs on physical iOS devices with lifecycle, ABI, and packaging conformance." + }, + { + "id": "target-android-arm64", + "category": "object-target", + "state": "missing", + "needed_by": [6, 7], + "evidence": "No Android target or native conformance suite exists at the pinned commit.", + "exit_gate": "Zag ARM64 output runs on physical Android devices with ELF, JNI, lifecycle, and packaging conformance." + }, + { + "id": "abi-objective-c", + "category": "platform-abi", + "state": "missing", + "needed_by": [5, 6, 7], + "evidence": "No Objective C runtime ABI implementation or native conformance exists at the pinned commit.", + "exit_gate": "Typed Objective C message sends, callbacks, ownership, exceptions boundary, and aggregate cases pass on macOS and iOS devices." + }, + { + "id": "abi-com", + "category": "platform-abi", + "state": "missing", + "needed_by": [5, 7], + "evidence": "No COM ABI implementation or native conformance exists at the pinned commit.", + "exit_gate": "IUnknown lifetime, interface calls, callbacks, HRESULTs, threading model, and aggregate cases pass on Windows." + }, + { + "id": "abi-jni", + "category": "platform-abi", + "state": "missing", + "needed_by": [6, 7], + "evidence": "No JNI ABI implementation or native conformance exists at the pinned commit.", + "exit_gate": "Java to Zag and Zag to Java calls, references, exceptions, threads, callbacks, and cleanup pass on Android devices." + }, + { + "id": "abi-callbacks", + "category": "foreign-abi", + "state": "partial", + "needed_by": [3, 5, 6, 7], + "evidence": "The pinned v2 ABI documents executable qsort evidence for one direct captureless scalar and pointer callback, while captures, returned callbacks, floats, aggregates, ownership, and unload contracts remain unsupported.", + "exit_gate": "Captureless and context callbacks across each supported platform ABI pass register, stack, lifetime, reentrancy, unwind boundary, and negative tests." + }, + { + "id": "abi-aggregates", + "category": "foreign-abi", + "state": "missing", + "needed_by": [3, 5, 6, 7], + "evidence": "No general foreign aggregate parameter and return ABI conformance exists at the pinned commit.", + "exit_gate": "Platform ABI suites pass small and large structs, unions, vectors, floats, alignment, return by value, and mixed register classes." + }, + { + "id": "resource-embedding", + "category": "build-system", + "state": "partial", + "needed_by": [3, 5, 6, 7], + "evidence": "The exact clean pinned commit defines compiler-owned #embed, source-relative identity, binary and empty resources, structured E0017 failures, foreground-cache byte identity, deterministic x86-64 execution, and qemu-user ARM64 execution through zag-poc/tests/run_resource_embed.sh. The clean tree also reached a byte-identical self-host fixpoint.", + "exit_gate": "Deterministic byte resources embed into every supported object format with identity, alignment, lookup, reproducibility, and malformed input tests." + }, + { + "id": "dynamic-platform-loading", + "category": "runtime", + "state": "partial", + "needed_by": [3, 5, 6, 7], + "evidence": "zag-poc/docs/DYNAMIC_SYSTEM_ABI.md proves a narrow x86-64 Linux dynamic ELF import path with scalar outbound calls and one captureless callback; other targets, aggregates, unload, TLS, and general callbacks are unsupported.", + "exit_gate": "Typed library loading, symbol lookup, version failure, ownership, unload, callback, and aggregate tests pass per supported platform." + }, + { + "id": "main-loop-and-workers", + "category": "concurrency", + "state": "partial", + "needed_by": [2, 3, 5, 6, 7], + "evidence": "The pinned v2 concurrency guide documents bounded x86-64 atomics, futex wait and wake, and a direct join-only Linux worker slice; general arguments, detach, TLS, cross-platform workers, and a complete memory model remain unsupported.", + "exit_gate": "Public main-loop scheduling, wakeup, cancellation, worker pool, atomics, memory model, thread affinity, shutdown, and race tests pass on every target." + }, + { + "id": "package-resolution", + "category": "package-system", + "state": "partial", + "needed_by": [2, 3, 5, 6, 7], + "evidence": "zag.mod parsing and local dependency validation exist, but the language specification excludes package registry and dependency resolution semantics.", + "exit_gate": "Deterministic resolution, lockfile, checksums, offline mode, path dependencies, conflicts, cache ownership, and reproducible fetch tests pass." + }, + { + "id": "incremental-and-reload-hooks", + "category": "developer-tooling", + "state": "partial", + "needed_by": [2, 3, 5, 6, 7], + "evidence": "The pinned zagd architecture documents a checksummed incremental declaration index and background semantic rechecking, but not an in-process incremental parser or stable state-preserving library reload contract.", + "exit_gate": "Dependency invalidation, state preserving reload, ABI rejection, rollback, file race, crash recovery, and deterministic event hooks pass end to end." + }, + { + "id": "generic-numeric-pointer-slices", + "category": "language-runtime", + "state": "available", + "needed_by": [2, 3, 4, 5, 6, 7], + "evidence": "The exact clean pinned commit self-hosts bounded *E[lo..hi] formation for every supported numeric slice family. zag-poc/tests/run_generic_pointer_slice.sh passes typed []i64 formation, parameter flow, backing-store mutation aliasing, nested slices, x86-64 execution, qemu-user ARM64 execution, and rejection of open-ended raw-pointer slices.", + "exit_gate": "Keep typed bounded raw-pointer slice formation, element stride, aliasing, nested slicing, negative diagnostics, and cross-target execution green at every pinned compiler revision." + }, + { + "id": "checksum-and-zlib-inflate", + "category": "standard-library", + "state": "available", + "needed_by": [2, 3, 6, 7], + "evidence": "The exact clean pinned commit contains pure-Zag IEEE CRC32, Adler32, canonical stored-block zlib encode/decode, and bounded general stored/fixed/dynamic DEFLATE inflate. The 12-check primitive suite and 14-check inflater suite pass on x86-64; the same 14 checks pass in a Zag-generated ARM64 ELF under qemu-user, including independent Python-zlib fixtures, mixed blocks, every strict prefix, and single-bit mutations.", + "exit_gate": "Keep the focused checksum, stored-zlib, and cross-target general-inflate suites green at the exact Zag revision pinned by each Zagkit release." + } + ] +} diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..3a1d667 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,126 @@ +# Architecture map + +Zagkit separates product semantics from rendering transport and platform shell: + +```text +Application views, state, actions, environment + | + reconciliation and layout + / \ + SemanticsNode tree DisplayList + / \ / \ +accessibility automation CPU oracle GPU transport + \ / \ / + public platform input, lifecycle, and surface seams +``` + +The semantics and display trees are parallel outputs of the same retained view +state. Accessibility is not reconstructed from pixels. GPU transports do not +receive view nodes. Platform shells do not own component state. + +Normative decisions: + +- [product and platform](../rfcs/0001-product-and-platform-contract.md) +- [declarative core and rendering](../rfcs/0002-declarative-core-and-rendering.md) +- [text, semantics, and input](../rfcs/0003-text-semantics-and-input.md) +- [platform seams and backend truth](../rfcs/0004-platform-seams-and-backend-truth.md) +- [quality and release](../rfcs/0005-quality-and-release-contract.md) + +Detailed experimental contracts: + +- [intrinsic measurement and invalidation reasons](measurement.md) +- [Flex placement and adaptive spacing](flex.md) +- [motion scheduler and tracks](motion.md) +- [canonical vector paths](paths.md) +- [canonical images and bounded PNG ingestion](images.md) +- [deterministic PNG snapshot bytes](../quality/png-snapshots.md) + +The first experimental compiled slice now fixes the initial Zag shapes for +`NodeKey`, `State`, `Binding`, `Action`, `Environment`, `ViewContext`, +`ViewSpec`, and `RenderNode`. State reads record their reader and revision; +invalidation reports the exact read and revision edge; keyed reconciliation +preserves retained identity through reorder and fails visibly on duplicate +keys. Ownership, threading, serialization, typed environment values, +cancellation and replay serialization are still open contracts rather than +stable API. + +The retained intrinsic measurement tree aggregates leaf, row, column, and +overlay size ranges with exact gaps, padding, baselines, ownership, constraint +rules, overflow, and deterministic input/output identity. Revision witnesses +reject unstable intrinsic results, while `LayoutTrace` maps real state reads to +the exact node, ancestor, phase, and rule that must recompute. The full contract +and incomplete lifecycle boundaries are documented in +[measurement.md](measurement.md). + +Flex extends fixed-point constraints with deterministic single-line and wrapped +row or column placement. Its primitive spacing scale adapts to density, safe +areas, text scale, breakpoints, and physical RTL without changing logical ID +order. Wrapped output retains line ranges, exact placement identity, and +fail-visible overflow or malformed input. See [flex.md](flex.md). + +The parallel experimental `SemanticsTree` retains owned names, descriptions, +and values plus roles, actions, focus order, live regions, ranges, selection, +text navigation, fixed-point bounds, relationships, collection coordinates, +full collection counts, tree level, set position, and expansion truth. Virtual +Table and Tree output projects only live semantic descendants while preserving +the complete logical range on the root. Stable `NodeKey` identity is shared +with rendering, so accessibility adapters, semantic tests, and Zagkit Talkback +automation query product meaning without reconstructing it from pixels. + +Rendering begins with an experimental immutable `DisplayList`. Every operation +retains its owning `NodeKey`; paths, images, glyph runs, clips, transforms, +RGBA16 paints, layers, and effects remain explicit rather than backend calls. +Balanced lists seal with deterministic content identity, and verification +detects mutation outside the builder contract before rendering. Owned resource +payloads participate in sealing and identity; damage and GPU transports remain +open. + +The [render resource ownership layer](render-resources.md) copies typed payloads +behind positive stable IDs, canonicalizes insertion order, +enforces configurable byte and count ceilings, requires exact replacement +revisions, and verifies sealed byte-level identity. Display lists own the store. +Path resources additionally require the bounded canonical +[ZKPATH01 contract](paths.md) and are validated once before operation references +seal. Canonical decoded [RGBA8 images](images.md) also have exact dimensional, +schema, color-space, and payload-size validation. SVG, PNG, font, and glyph +decoding remain unavailable. + +Display-list replay uses the versioned little-endian `ZKDL` version 2 codec. +Decoding is bounded to one million operations, reconstructs operations through the +same validation path as live building, requires balanced seal state, verifies +the stored content identity and revision, and rejects trailing bytes so one +scene has one canonical encoding. A bounded variable-length resource section +preserves allocation policy, typed metadata, revisions, and exact owned bytes. + +The CPU oracle begins with deterministic RGBA8 rectangle fills, centered +rectangle strokes, canonical path fills, and canonical decoded image draws from +26.6 fixed-point geometry. Clip and positive axis-aligned transform state, +analytic rectangle coverage, 8 by 8 path coverage, non-zero and even-odd +winding, fixed curve flattening, premultiplied bilinear image sampling, and +source-over alpha are integer-only. +Explicit edge and work ceilings reject pathological path scenes before pixel +mutation. Every unsupported display operation fails at its exact index instead +of silently degrading or claiming a visual result. + +The experimental input router consumes a parallel retained `HitTree`. It uses +integer affine inversion for local coordinates, resolves local clips and +z-order deterministically, and keeps pointer capture and focus explicit. +Accepted, missed, captured, cancelled, and invalid events share one ordered +evidence stream; platform input adaptation remains open. + +The first deterministic replay executor consumes an immutable ordered tape of +state revisions, pointer events, monotonic clock samples, backend activation, +loss, and recovery. It rejects stale or impossible transitions at their exact +event index, then rebuilds a conformance scene through the real semantics, +motion, intrinsic measurement, Flex, display-list, and CPU-oracle paths. A +repeated tape must produce identical subsystem and aggregate hashes. A general +application callback contract, versioned tape codec, resource capture, and +platform lifecycle integration are still open. + +The motion scheduler consumes an authoritative monotonic clock and advertises +the current refresh interval without deriving motion from callback count. +Integer fixed-step springs and owned keyframe timelines retain stable track +identity, exact position and velocity, explicit frame reasons, interruption, +reversal, resize retargeting, gesture handoff, and reduced-motion substitution. +Settled tracks request no further frame. The full contract and current limits +are documented in [motion.md](motion.md). diff --git a/docs/architecture/flex.md b/docs/architecture/flex.md new file mode 100644 index 0000000..1bf4a32 --- /dev/null +++ b/docs/architecture/flex.md @@ -0,0 +1,107 @@ +# Experimental Flex placement contract + +Flex is Zagkit's public placement and spacing foundation. It works entirely in +signed 26.6 logical units and preserves stable item IDs in logical order. The +current implementation covers single-line rows and columns plus deterministic +multi-line wrapping; it is not yet the complete `G2-FLEX` contract. + +## Primitive spacing scale + +`FlexSpacingToken` resolves to this standard-density scale: + +| Token | Logical pixels | +|---|---:| +| `none` | 0 | +| `micro` | 2 | +| `tiny` | 4 | +| `small` | 8 | +| `medium` | 12 | +| `large` | 16 | +| `xlarge` | 24 | +| `xxlarge` | 32 | + +Compact density resolves each primitive to three quarters of its standard +value. Touch density resolves to five quarters. All results remain exact 26.6 +values. Text scale does not silently multiply gaps; it affects adaptive +composition through breakpoint selection. + +These are experimental layout primitives, not accepted component styling. The +three-direction visual review will decide which semantic component tokens map +to the scale before visual component production begins. + +## Safe areas and breakpoints + +`FlexAdaptiveContext` carries viewport size, physical safe-area insets, +density, text scale, and layout direction. Safe areas are normalized before +arithmetic and can be added to tokenized padding. They remain physical: left +and right do not swap under RTL. + +Breakpoints use safe content width divided by effective text scale: + +- compact below 600 logical pixels; +- medium from 600 through 1,023 logical pixels; +- expanded from 1,024 logical pixels. + +Text scale is bounded from 1x through 4x for breakpoint calculation. Larger +text therefore selects a roomier composition without distorting the primitive +spacing scale. + +## Intrinsic items and priorities + +`flex_item_intrinsic` retains a measured node's main-axis minimum, preferred, +and maximum bounds plus preferred cross extent and baseline instead of +flattening it to an unexplained size. +`FlexPriority` provides lowest, low, normal, high, and required policies with +explicit grow and shrink weights. Lower priorities yield more under pressure; +required items neither grow nor shrink. Remaining pressure after intrinsic +bounds is reported as overflow. See the +[measurement contract](measurement.md) for the exact mapping and retained +intrinsic tree. + +## Wrapping + +`FlexWrapStyle` adds line gap, line alignment, and a wrapping switch to a +normal `FlexStyle`. Row and column wrapping use the constrained inner main +extent after padding. Items remain in logical insertion order and retain stable +IDs across line changes. + +Each line independently runs the same grow, shrink, main alignment, cross +alignment, and baseline rules as single-line Flex. Baseline lines reserve the +largest ascent plus largest descent, avoiding clipping when equal-height items +have different baselines. + +Line alignment supports start, center, end, space-between, space-around, +space-evenly, and stretch. RTL rows place logical starts at the physical right. +RTL columns also advance line bands from right to left while preserving item +order and physical safe-area edges. + +`FlexWrapResult` owns placements and line ranges. It reports main and cross +overflow separately, duplicate IDs, whether wrapping occurred, and a +deterministic layout hash over line and placement truth. + +## Failure and ownership + +Single-line and wrapped layout reject malformed IDs or geometry, unsafe style +extents, and arithmetic that would exceed Zagkit's bounded logical extent. +Single-line failures return `FlexError`; wrapping additionally rejects more than +one million input items and returns `FlexWrapError`. Both identify the exact +item when applicable and return empty placement ownership. Wrapped failures +also retain a deterministic failure identity. No partial placement is reported +as success. + +Grow and shrink weights are nonnegative and bounded to 4,096 so weighted +distribution cannot overflow the fixed-point extent contract. + +Call `flex_wrap_free` for every result, including failures. It releases both +the placement and line arrays. + +## Current boundary + +The following remain required before either Flex checklist item can complete: + +- safe-area and density matrices driven by real platform shells; +- end-to-end state, environment, and platform-input invalidation reasons across + retained components; +- layout-transition orchestration; +- full RTL, large-text, and adaptive component conformance; +- visual-direction acceptance and screenshot goldens. diff --git a/docs/architecture/grid-and-overlay.md b/docs/architecture/grid-and-overlay.md new file mode 100644 index 0000000..1d85921 --- /dev/null +++ b/docs/architecture/grid-and-overlay.md @@ -0,0 +1,58 @@ +# Grid and Overlay placement contract + +Status: experimental headless primitive + +Grid and Overlay are deterministic retained-placement primitives in Zagkit's +Flex layout system. They operate in signed 26.6 fixed-point logical units and +own their output arrays. They do not select a visual direction, create native +widgets, or imply that the higher-level Grid component is conformant. + +## Grid + +`GridTrack` has three explicit sizing modes: + +- fixed tracks never grow or shrink; +- intrinsic tracks start at their preferred extent and remain between their + declared minimum and maximum; +- fraction tracks start at their minimum and share remaining space by integer + weight, assigning division remainder deterministically. + +Spanning item minimum and preferred sizes pressure every eligible track in the +span before the container is resolved. Under a finite constraint, fraction +tracks grow first, `stretch` may grow remaining non-fixed tracks, and +non-fixed tracks shrink toward declared minima. Anything still outside the +container is reported by axis; fixed sizes are never silently falsified. + +Items use stable positive IDs. A pair of `-1` coordinates requests row-major +auto placement; partial automatic coordinates are invalid. Explicit overlap is +fail-closed unless `allow_overlap` is set, and the result then reports the +number of collided cells. Row and column spans, placement alignment, one-row +baseline alignment, z-order, content distribution, padding, gaps, intrinsic +container sizing, and logical RTL columns are part of the layout hash. + +## Overlay + +Overlay derives an unconstrained container from the largest child preferred +extent plus child insets and outer padding. Under finite constraints, each +child resolves independently with logical start, center, end, or stretch +alignment. Logical horizontal start and end mirror in RTL. Stable ID, z-order, +baseline output, per-axis minimum-size overflow, and input/output layout hashes +remain observable. + +Overlay rejects duplicate IDs, malformed intrinsic ranges, unsafe extents, +unsupported baseline placement requests, invalid constraints, and arithmetic +overflow before returning partial geometry. + +## Ownership, replay, and proof + +Call `grid_free` or `overlay_free` exactly once for every result, including +typed error results. Deterministic replay mixes Flex wrap, Grid, and Overlay +layout identities into the scene identity, so a placement change cannot hide +behind identical state or pixels. + +The focused executable contracts cover fixed, intrinsic, and fraction tracks; +spans; auto placement; collisions; opt-in overlap; baseline; z-order; RTL; +unbounded intrinsic sizing; quantified overflow; malformed input; deterministic +identity; 122 grid widths; and 253 overlay sizes. These tests are headless +correctness evidence. They are not screenshot, native accessibility, native +window, performance, or platform certification. diff --git a/docs/architecture/images.md b/docs/architecture/images.md new file mode 100644 index 0000000..5059308 --- /dev/null +++ b/docs/architecture/images.md @@ -0,0 +1,136 @@ +# Canonical decoded images + +Status: experimental headless primitive + +Zagkit separates encoded assets from renderer-ready pixels. Canonical decoded +image resources use format tag 1, explicit positive width and height, explicit +color-space metadata, and exactly `width * height * 4` owned RGBA8 bytes in +row-major order. Channels use straight alpha at the ownership boundary. + +Dimension and byte-count arithmetic is overflow checked before allocation or +indexing. Unknown schemas, absent color-space truth, mismatched payload size, +zero dimensions, and dimensions above the shared hard limits fail during +display-list resource validation, before draw operations seal. + +## CPU sampling + +The current CPU oracle renders canonical sRGB images into positive +axis-aligned destination rectangles. It maps destination and source pixel +centers, performs deterministic 8-bit bilinear interpolation in premultiplied +form to avoid transparent-edge color fringes, then converts back to straight +color for source-over composition. A 4 by 4 coverage grid handles fractional +destination edges and clips separately from texture filtering, so one-to-one +integer placement preserves exact texels. Operation alpha is the only paint +modulation; unused paint channels must remain canonical. + +Transformed destination geometry and pixel-sample work have explicit ceilings. +Unsupported color-space conversion fails at the exact draw operation before +pixels are touched. The deterministic replay scene exercises the owned image, +display-list, and CPU route. + +## PNG ingestion + +`png_decode(bytes)` is the owned, fail-closed path from encoded PNG assets to +the canonical image form. It validates the signature, chunk type spelling and +reserved bit, chunk ordering, duplicate single-instance metadata, every CRC32, IEND, +and absence of trailing data before trusting image content. IDAT payloads are +joined under an encoded-input ceiling and decoded by the pinned pure-Zag +stored/fixed/dynamic DEFLATE inflater with the exact expected scanline byte +count as its output ceiling. + +The decoder reverses None, Sub, Up, Average, and Paeth filters. It accepts the +legal grayscale, RGB, indexed, grayscale-alpha, and RGBA bit-depth combinations, +including packed 1/2/4-bit samples, PLTE, and tRNS. Noninterlaced rows and all +seven bounded Adam7 passes reconstruct into the same canonical pixels. +Renderer-owned output is always straight-alpha sRGB RGBA8. Sixteen-bit samples +use their high byte as the current deterministic conversion policy; +transparency comparisons use the original full sample before conversion. Files +without explicit color metadata use Zagkit's documented sRGB fallback policy; +this is an assumption +recorded by the asset pipeline, not a claim that the source declared sRGB. An +explicit `sRGB` chunk records sRGB directly. Valid `cHRM` primaries are converted +through a bounded linear RGB-to-XYZ matrix, Bradford white-point adaptation, +and the canonical linear-sRGB matrix. Singular, non-finite, or out-of-domain +chromaticities fail before decompression. A gamma chunk without declared +primaries still records the fallback as assumed. All nonzero +[`gAMA`](https://www.w3.org/TR/png-3/#11gAMA) transfer curves are +converted into canonical sRGB by deterministic fixed-point log, power, and +sRGB-transfer operations. When `cHRM` is absent, that conversion explicitly +assumes sRGB primaries; an `sRGB` chunk takes precedence over accompanying +gamma and chromaticity metadata. A meaningless zero gamma is ignored without +becoming declared profile truth. Eight complete 256-value ramps plus Display P3 +and non-D65 fixtures match independently calculated references exactly on both +compiled architectures. + +An `iCCP` wrapper must provide a valid 1-to-79-byte PNG profile name, compression +method zero, and one independently bounded zlib stream. The decompressed ICC +profile is limited to 4 MiB and must be an ICC v2 or v4 scanner/display RGB +profile using XYZ PCS and the D50 PCS illuminant. Zagkit validates the +declared profile size, header signature, reserved header bytes, rendering intent, +bounded aligned tag table, unique required tags, XYZ tag types, and transfer +curve types before executing it. The current executable subset accepts the six +required RGB matrix/TRC tags with independent per-channel identity curves, +single gamma curves, parametric curve types 0 through 4, or sampled curves with +at most 4,096 entries. Sampled curves use ICC linear interpolation to build an +owned 256-entry fixed-point table for each RGBA8 input channel. Each piecewise +function executes from bounded fixed-point coefficients and clips to its declared range; +undefined or unsafe coefficient domains fail before pixels are touched. Matrix +columns are converted +from D50 PCS into linear sRGB, then encoded through the canonical sRGB transfer. + +The first LUT subset accepts an `A2B0` `lut16Type` with exactly three RGB input +and three PCSXYZ output channels when the profile header selects perceptual +rendering. It does not substitute `A2B0` for another intent. Its matrix must be +the ICC-mandated identity matrix for non-PCS input. Each input and output table +has 2 to 4,096 entries; +the three-dimensional CLUT has 2 to 33 grid points per axis. The tag must end at +the exact computed table boundary before Zagkit allocates owned tables. Pixels +execute through input-table interpolation, fixed-point trilinear CLUT +interpolation, output-table interpolation, PCSXYZ decoding, and the D50-to-sRGB +matrix in that declared order. This supports high-precision LUT16 profiles +without assigning an implementation-specific meaning to ambiguous 8-bit +PCSXYZ LUTs. + +The first `lutAToBType` subset accepts the specification's A-curves, CLUT, +B-curves combination. Each of the three A and B curves is independently parsed +as an embedded identity, gamma, sampled, or parametric curve and normalized to +an owned 256-entry table. The CLUT permits independent 2-to-33-point red, green, +and blue axes and either 8-bit or 16-bit data. Offsets must be aligned, bounded +by the exact element boundary, and padded with canonical zeroes; only the +specification-permitted sharing of complete curve sets may overlap. The B-only +and M-curves, matrix, B-curves combinations remain explicit +unsupported paths until their distinct execution stages have contracts. + +An understood `iCCP` profile takes precedence over compatibility `sRGB`, `gAMA`, +and `cHRM` chunks. The result records declared rather than assumed profile truth. + +`png_decode_resource_spec` +adapts a successful owned result to the normal copying resource-store API, so +the decoder can be freed immediately after insertion. + +The end-to-end CPU contract decodes an encoded alpha image, transfers it through +the canonical resource store, and compares full-surface hashes at 1x, 1.25x, +1.5x, 2x, and 3x. The same golden hashes pass on x86-64 and ARM64; this is +headless scale evidence, not native compositor or monitor evidence. + +The strict deterministic fuzz gate decodes 20,000 arbitrary byte streams, +20,000 structured mutations of a valid PNG, 20,000 arbitrary decompressed ICC +profiles, every strict seed prefix, 4,096 general decode/free repetitions, and +4,096 sampled-profile, 4,096 LUT16, and 4,096 `lutAToBType` +parse/decode/free repetitions on both +x86-64 and ARM64. Every result must preserve success or failure ownership +invariants. Coverage-guided sanitizer campaigns +and a larger published malformed corpus remain additional required evidence. + +Dimension, encoded-data, decompressed-scanline, output-pixel, palette-index, +filter, color-matrix, ICC inflate, ICC tag-table, and arithmetic limits fail +before out-of-bounds access. Monochrome profiles, device-link profiles, Lab PCS, +the remaining `lutAToBType` combinations, and ambiguous `lut8Type` with PCSXYZ, +plus unsupported `cICP` metadata, fail as unsupported color profiles. Unknown +interlace methods fail before decompression. These paths keep `G3-PNG` open until +the remaining ICC transform families and coverage-guided malformed-input +evidence land. + +Linear-light filtering, wide-gamut output surfaces, mipmapping, +high-quality downsampling, image tiling, and GPU upload caches also remain +unavailable. diff --git a/docs/architecture/measurement.md b/docs/architecture/measurement.md new file mode 100644 index 0000000..f75e936 --- /dev/null +++ b/docs/architecture/measurement.md @@ -0,0 +1,117 @@ +# Experimental intrinsic measurement contract + +Zagkit measurement is retained, deterministic, and independent of a window +system or rendering backend. The current contract operates in signed 26.6 +logical units and gives layout caches an inspectable identity for both inputs +and outputs. It advances `G2-CONSTRAINTS`; it does not complete that checklist +item by itself. + +## Retained tree + +`IntrinsicTree` owns stable `IntrinsicNode` records and parent-child edges. +Leaf nodes declare minimum, preferred, and maximum width and height, a baseline, +and a content revision. Container nodes aggregate those declarations as a row, +column, or overlay with explicit gap and physical padding. + +Rows sum widths, take the largest height, and reserve the largest ascent plus +largest descent. Columns take the largest width and sum heights. Overlays take +the largest extent on both axes. Rows and columns include gaps only between +children. Every form composes padding after child aggregation. + +A valid retained measurement tree has: + +- one positive stable ID per node; +- exactly one owner for every non-root node; +- no orphan, missing, duplicate, or leaf-owned edges; +- monotonic minimum, preferred, and maximum ranges; +- a baseline inside preferred height; +- bounded gap, padding, revision, and fixed-point geometry; +- no ownership cycle. + +The current executable bound is 4,096 nodes, 16,384 ownership edges, and 512 +retained levels per measured tree. Inputs beyond those limits fail before +unbounded allocation or native call-stack growth. + +Malformed trees fail before returning usable size. `MeasureResult` retains the +exact error node and edge where applicable, the count of nodes reached before +failure, and no partial success claim. + +## Constraint resolution + +The intrinsic preferred size is resolved independently on each axis against +normalized `Constraints`. `MeasureRule` records whether intrinsic preference, +the minimum constraint, or the maximum constraint selected the final extent. +If the authoritative constraint is smaller than intrinsic minimum, width and +height overflow remain separate and explicit. Baselines clamp to the resolved +height instead of escaping the result bounds. + +Measurement identity includes the full retained tree, node revisions, +ownership edges, intrinsic result, normalized constraints, resolved size, +baseline, overflow, rules, and visited-node count. A constraint or hidden +overlay-child change therefore cannot masquerade as a cache hit merely because +the final rectangle happens to stay the same. + +## Stability and revisions + +`IntrinsicStability` retains the input hash observed for each stable node and +content revision. A node that returns different intrinsic inputs without +advancing its revision fails as `unstable_measurement`. A changed input is +accepted only after an explicit newer revision. Regressing a revision also +fails. This turns intrinsic instability into inspectable evidence rather than +layout jitter. + +Call `intrinsic_stability_free` and `intrinsic_tree_free` for every owned +instance. + +## Flex priorities + +`flex_item_intrinsic` converts intrinsic metrics into a Flex item while +retaining main-axis minimum, preferred, and maximum bounds plus preferred cross +extent and baseline truth. +`FlexPriority` maps to explicit grow and shrink resistance: + +| Priority | Grow weight | Shrink weight | +|---|---:|---:| +| `lowest` | 0 | 8 | +| `low` | 1 | 4 | +| `normal` | 2 | 2 | +| `high` | 4 | 1 | +| `required` | 0 | 0 | + +Required content neither grows nor yields under pressure. Lower-priority +content yields more of a deficit than higher-priority content. Intrinsic bounds +remain authoritative during distribution, and unresolved pressure is reported +as Flex overflow. + +## Exact invalidation reasons + +The separate `invalidation.zag` module keeps authoring-state dependencies out +of low-level intrinsic and Flex code. Its `LayoutTrace` copies actual +`ViewContext` state reads into retained layout dependencies. Every record names +the reader `NodeKey`, retained ancestor, +state ID and read revision, affected phase, and rule. Current phases are +measure, layout, paint, and semantics. Current rules cover intrinsic content, +constraints, child measurement, Flex priority, gap, padding, direction, safe +area, density, text scale, and breakpoint selection. + +One `StateChange` produces every matching `LayoutInvalidationCause`, including +the exact old and new revision edge. Unread state produces no cause. Repeated +identical reads collapse without losing phase or rule truth. + +## Current boundary + +The following remain before `G2-CONSTRAINTS` can complete: + +- retained `RenderNode` lifecycle and cancellation integration; +- incremental subtree cache replacement and cleanup under reconciliation; +- component and text measurement producers; +- cross-axis intrinsic bounds in Flex item distribution; +- exact non-state environment and platform-input revision dependencies; +- serialized measurement and invalidation evidence; +- property and fuzz coverage across the complete component layout matrix. + +Editable Table and Tree behavior, general retained-node lifecycle, and +platform-driven adaptive conformance remain separate Flex and virtualization +work. Grid, Overlay, Scroll, Virtual List, Table projection, Tree projection, +and bounded recycling now have experimental contracts; their existence does +not complete the full component layout matrix. diff --git a/docs/architecture/motion.md b/docs/architecture/motion.md new file mode 100644 index 0000000..3833538 --- /dev/null +++ b/docs/architecture/motion.md @@ -0,0 +1,79 @@ +# Experimental motion contract + +Zagkit motion is driven by an explicit monotonic clock. Platform callbacks do +not define animation progress, so the same clock and event sequence must +produce the same track state and evidence on every backend. + +## Units and ownership + +- Values use signed 26.6 logical units; 64 units equal one density-independent + logical pixel. Velocity uses those fixed-point units per second. +- Time uses monotonic microseconds supplied by the host or deterministic replay. +- A `MotionScheduler` owns every `MotionTrack`, copied keyframe, and retained + `MotionEvent`. `motion_scheduler_free` releases that complete ownership tree. +- Stable positive track IDs reject duplicates before allocation. + +## Scheduling truth + +The scheduler records its active refresh rate and integer frame interval for +24 through 480 Hz. Refresh changes emit `refresh_rate_change`. A running track +causes `motion_needs_frame` and `MotionTickResult.request_next_frame` to return +true. When every track settles, later ticks perform no work, append no event, +and request no frame. + +Clock regression and individual clock jumps over one second fail before time or +track mutation. Once motion is running, callers must advance time through +`motion_tick`; `motion_set_clock` cannot silently skip evaluation. + +Every evaluated active frame emits an ordered `tick` reason. Starts, +interruptions, reversals, resize retargeting, gesture handoff, reduced-motion +substitution, and refresh changes retain their own reason and exact track state. + +## Springs + +Springs use deterministic integer semi-implicit integration in fixed one +millisecond steps. Sub-unit velocity and position remainders are retained so a +spring cannot stall because of integer truncation. Grouping identical elapsed +time into 60 Hz, 120 Hz, or irregular callbacks produces identical position +and velocity. + +Retargeting preserves visible position and incoming velocity. Interruption, +reversal, and resize are semantic reasons for the same continuity operation. +Gesture handoff replaces position and velocity with the recognizer's exact +sample and continues toward the supplied target. When configured position and +velocity tolerances are both reached, the track snaps exactly to its target, +zeros velocity, and becomes idle. + +## Keyframes + +A keyframe timeline starts at zero microseconds, contains strictly increasing +times, and is copied into scheduler ownership. Linear interpolation reports +exact constant velocity within each segment and settles exactly on the final +value. Schedulers and timelines are each bounded to 4,096 tracks or samples, +and a timeline is bounded to 60 seconds; malformed order, +unsafe values, and unsafe segment velocity fail before ownership transfer. + +Additional easing curves, keyframe interruption into a replacement timeline, +and spline velocity matching remain open. + +## Reduced motion + +Reduced motion is an explicit semantic substitution: + +- spatial springs use `snap_to_end`; +- components may supply a separate `opacity_fade` keyframe timeline; +- an opacity substitute is limited to values from zero through one logical + unit and a maximum duration of 250 milliseconds. + +Zagkit does not multiply every duration by a global factor. A spatial spring +cannot be mislabeled as an opacity fade. Component-level substitutions and +their semantic equivalence still require conformance coverage before +`G3-REDUCED-MOTION` can be completed. + +## Current boundary + +This slice proves the deterministic headless kernel. It does not yet provide +layout or shared-transition orchestration, gesture recognition, platform vsync +adapters, bounded long-running event retention, easing families, visual +goldens, p99 frame evidence, 120 Hz device certification, or ten-minute stall +traces. Those remain required by the master goal and release gates. diff --git a/docs/architecture/paths.md b/docs/architecture/paths.md new file mode 100644 index 0000000..524d3bc --- /dev/null +++ b/docs/architecture/paths.md @@ -0,0 +1,52 @@ +# Canonical vector paths + +Status: experimental headless primitive + +Zagkit paths use a Zag-owned immutable command stream rather than backend path +objects. `PathData` records one explicit fill rule and ordered `move_to`, +`line_to`, `quad_to`, `cubic_to`, and `close` commands in the same signed 26.6 +fixed-point coordinate domain as layout and display lists. + +## Builder and sequence rules + +Every contour starts with `move_to`. Line and curve commands require an open +contour; `close` closes exactly one open contour, and more geometry then +requires another move. A new move may begin a new contour without an explicit +close because open contours are legal path input. Empty paths, commands before +a move, duplicate closes, noncanonical unused point fields, out-of-domain +coordinates, and more than 65,536 commands fail before builder mutation. + +Sealing computes deterministic identity over the fill rule and every command +field. Sealed builders reject writes, while verification detects direct command +or identity mutation. Callers own each `PathData` and release it with +`path_free`. + +## ZKPATH01 bytes + +`ZKPATH01` version 1 is the canonical little-endian resource payload. Its +40-byte header contains magic, version, fill rule, command count, and content +hash. Each command occupies 56 bytes: verb plus three fixed-point points. +Unused points must be zero, so one path has one encoding. Decoding is bounded, +reuses the live builder validation path, produces a sealed path, and rejects +bad magic, versions, fill rules, verbs, sequences, geometry, hashes, +truncation, and trailing bytes with an exact offset. + +Path render resources use format tag 1. Display-list sealing validates every +owned path payload once, then validates operation references by stable ID, +resource kind, and format. This avoids both opaque-path acceptance and repeated +decode work when one path is drawn many times. + +The CPU oracle closes open contours for filling, flattens quadratic and cubic +commands into 16 deterministic line segments, applies the retained positive +axis-aligned transform and clip, evaluates non-zero or even-odd winding, and +uses an 8 by 8 subpixel grid for symmetric coverage. It rejects transformed +coordinates, edge counts, or pixel-edge-sample work beyond explicit bounds +before touching pixels. The replay conformance scene exercises this real path +route. + +Fixed-subdivision flattening is an experimental correctness step, not the final +curve-quality contract. Scale-adaptive or analytic curve coverage, exact curve +extrema, path strokes, joins, caps, dashes, boolean path operations, and SVG +path syntax remain unavailable until their deterministic, malformed-input, +golden, fidelity, and cleanup suites pass. Axis-aligned rectangle strokes are a +separate implemented CPU primitive. diff --git a/docs/architecture/render-resources.md b/docs/architecture/render-resources.md new file mode 100644 index 0000000..3efb52d --- /dev/null +++ b/docs/architecture/render-resources.md @@ -0,0 +1,63 @@ +# Render resource ownership + +Status: experimental headless primitive + +`RenderResourceStore` is the ownership and identity boundary for bytes that +feed Zagkit paths, decoded images, glyph runs, effects, fonts, SVG, and PNG. It +is not itself a decoder and does not make an opaque payload renderable. +SVG parsing, PNG parsing and color management, font validation, glyph shaping, +and decoded rendering remain separate open contracts. Canonical path payloads +and decoded RGBA8 images are the first typed schemas layered over this store; +see [paths](paths.md) and [images](images.md). + +## Identity and metadata + +Every resource has a positive stable ID, one explicit kind, a positive format +tag, revision, owned byte payload, optional dimensions, and color-space truth. +Decoded image resources require positive dimensions and a nonempty color space. +The format tag identifies a caller-defined canonical payload schema; it is not +interpreted by this layer. + +New IDs begin at revision zero. Replacement retains the same ID and kind and +must advance exactly one revision. Missing replacement targets, stale or +skipped revisions, and kind changes fail before the old payload is released. +Payload bytes are copied at successful add or replacement, so callers retain no +borrowed lifetime obligation. + +## Bounds and canonical order + +Each store declares resource-count, total-byte, and per-payload ceilings within +hard library maxima. Configuration, empty payload, metadata, per-payload, +aggregate, duplicate-ID, and count errors are checked before ownership changes. +Tests use intentionally small limits to prove each failure without expensive +allocations. + +Resources are retained in stable-ID order regardless of insertion order. +Content identity covers configuration, byte accounting, store revision, +resource metadata, per-resource revision, and every payload byte. Two stores +with the same resources therefore have the same identity even when populated +in different orders. + +## Immutability and cleanup + +Sealing first verifies the complete store. A sealed store rejects add and +replacement. Verification detects order changes, byte mutations, metadata or +revision changes, forged accounting, invalid limits, and aggregate identity +changes. `render_resource_store_free` releases every owned payload and then the +resource array; callers invoke it exactly once for every store, sealed or not. + +`DisplayList` now owns this store. Sealing verifies that every resource-bearing +operation resolves an existing resource of the exact required kind. Resource +bytes, metadata, allocation policy, and replacement revisions contribute to +the display-list revision and content identity. `ZKDL` version 2 serializes the +canonical resource section before operations and reconstructs it through the +same bounded validation path; decoding and re-encoding are byte identical. + +The current executable contract decodes bounded PNG assets into canonical owned +RGBA8 resources, including arbitrary valid `cHRM` conversion and bounded ICC +matrix/TRC profiles. It does not yet decode SVG, execute general ICC curve/LUT +profiles, validate font tables, interpret glyph payload schemas, rasterize path +strokes, target wide-gamut output surfaces, cache +platform uploads, or implement memory-pressure eviction. Those +capabilities remain unavailable until their own malformed-input, rendering, +replacement, and cleanup suites pass. diff --git a/docs/architecture/scroll-and-virtualization.md b/docs/architecture/scroll-and-virtualization.md new file mode 100644 index 0000000..907c451 --- /dev/null +++ b/docs/architecture/scroll-and-virtualization.md @@ -0,0 +1,131 @@ +# Scroll and virtual-list contract + +Status: experimental headless primitive + +The current scroll and virtual-list slice supplies deterministic geometry and +bounded retained work without a window system. It is intended to support later +List, Table, Tree, Talkback, and PrismStudio collection work. It does not claim +native wheel, touch, accessibility, or 120 Hz performance certification. + +## Scroll truth + +`ScrollState` owns viewport and content sizes, logical offsets, and a revision. +Mutations report old and new offsets, consumed and unconsumed deltas, whether a +revision changed, and deterministic identity. Unconsumed deltas are explicit so +nested scrolling and gesture arbitration can hand pressure to an ancestor +without guessing. + +Offsets clamp to the exact content boundary. Repeated pressure at a boundary +does not create a phantom revision. Reveal supports nearest, start, center, and +end alignment. Reconciliation can preserve an explicit content point at its +viewport position while content or viewport sizes change. Logical horizontal +start maps to the opposite physical content edge in RTL. + +## Million-row model + +`VirtualListModel` represents as many as one billion stable IDs with a uniform +default extent and up to 4,096 sorted sparse extent overrides. It does not +allocate one record per item. Binary range selection finds the visible range; +only visible plus requested overscan placements are owned by the result. + +Each layout reports: + +- the full semantic item count; +- first and last visible and live indices; +- viewport-local frames and stable IDs; +- content extent, clamped scroll offset, and maximum scroll; +- the number of examined records; +- deterministic input/output identity. + +`max_live_items` is a hard allocation ceiling. A viewport that would exceed it +fails with `live_limit_exceeded` before placement allocation. Invalid sparse +ordering, malformed extents, unsafe scrolling, and content extent overflow also +fail with typed reasons. A viewport containing only collection padding reports +no phantom visible item. + +## Variable extents and anchors + +Sparse overrides carry their own revision. `VirtualAnchor` couples stable ID, +index, and viewport offset. When an extent above the viewport changes, +`virtual_resolve_anchor` computes the new scroll offset that keeps the anchored +item visually stationary. Missing or replaced IDs fail instead of silently +anchoring a different row. + +Horizontal lists retain logical order and mirror placement in RTL. Padding +remains physical. Vertical collection direction does not reorder semantic IDs. + +## Table projection + +Virtual Table composes the million-row model with up to 256 stable columns. +Logical-leading pinned columns remain resident while the remaining columns +scroll and overscan independently. Header and body cells carry a collision-free +composite identity of table, row, column, and header state; a single lossy hash +is never presented as the authoritative cell ID. + +Table results expose semantic row and column counts, visible and live row +ranges, visible scrolling columns, pinned count, both scroll maxima, and every +live cell frame. `max_live_cells` fails before allocation. Column resize retains +minimum and maximum bounds, reports consumed and unconsumed delta, and advances +both column and model revisions only when width changes. Pinned columns must be +one contiguous logical-leading region and mirror physically under RTL. + +`semantics_project_table` creates a bounded parallel tree from those live +cells. The Table root retains the full logical row and column counts. Live row, +header, and cell nodes retain exact fixed-point frames and zero-based logical +indices. Their `NodeKey` values derive deterministically from the authoritative +composite identity; all generated keys and the caller-owned root ID are checked +against the existing semantic tree before the first node is added. A collision +therefore fails without a partial semantic projection. + +## Tree projection + +Tree input is canonical preorder with stable ID, explicit parent ID, depth, +child capability, expansion state, extent, and revision. Projection validates +duplicate IDs, depth jumps, parent ownership, and leaf ownership before hiding +collapsed descendants. The visible projection retains source index and parent +identity, then uses the same bounded virtual range engine as List. + +Indentation applies to the content frame at logical start while the complete +row frame remains available for selection and hit testing. RTL moves indentation +to the right without changing semantic order. An open-addressed stable-ID index +keeps duplicate validation linear; the current retained projection remains +capped at 16,384 source nodes as an explicit allocation bound. + +`semantics_project_tree` retains the visible projection count on the Tree root +while adding only live placements. Each live item exposes its stable source ID, +full row frame, zero-based visible row, one-based hierarchy level and set +position, and an expand or collapse action that matches current state. Virtual +items are direct semantic children of the collection root so an offscreen +ancestor is never fabricated as a live node; level metadata preserves the +hierarchy required by native accessibility adapters. + +## Focus and recycling lifecycle + +`VirtualRetainedStore` separates logical IDs from recyclable retained instances. +Evicted nonfocused entries may be rebound with an incremented generation; +logical identity never inherits the previous item's generation. A focused +offscreen entry is pinned against recycling. When it returns, the exact instance +and generation are restored. Clearing focus makes it recyclable again. + +Residency has an explicit capacity. Live IDs plus an offscreen focus pin must +fit or reconciliation fails before mutation. Duplicate IDs and unknown focus +requests also fail. Identical live membership performs no revision, timestamp, +or identity churn. + +## Ownership and evidence boundary + +Call `virtual_free` once for every layout result, including failures. The model +borrows its sparse override list; its creator owns that list. Replay mixes +scroll and virtual-list identity with Flex, Grid, and Overlay identity. + +The executable suites prove a one-million-row mid-list query with nine live +placements and fewer than 64 examined records, a million-row two-axis Table, +collapsed and expanded Tree projections, sparse variable extents, anchor and +offscreen-focus stability, RTL placement, exact empty and padding-only ranges, +bounded allocation failures, malformed input, overflow rejection, scroll, +expansion, semantic collection projection, collision-atomic identity, and +100-window recycling sweeps. Replay incorporates List, Table, Tree, scroll, +semantics, and lifecycle identities. Stopwatch timing in CI is not a 120 Hz +claim. Native input, semantic adapters, editable cells, sorting, selection, +drag reordering, general Tree indexing, and reference-hardware performance +remain open. diff --git a/docs/automation/talkback.md b/docs/automation/talkback.md new file mode 100644 index 0000000..d7f38d0 --- /dev/null +++ b/docs/automation/talkback.md @@ -0,0 +1,51 @@ +# Zagkit Talkback protocol + +Zagkit Talkback is Zagkit's native automation and inspection control plane. It +is distinct from Android TalkBack, which remains a required assistive-technology +test target. The protocol is experimental and has no native transport yet. + +## Target truth + +Normal automation uses the exact `NodeKey` published by the retained semantics +tree. A request may pin the semantics revision; a mismatch returns +`stale-semantics` instead of risking an action on a replacement node. Missing, +hidden, disabled, and semantically unsupported targets fail explicitly. + +Pixel coordinates are a separate target kind, disabled by default. A backend +must advertise the fallback before dispatch. Pixel requests record physical +coordinates and a positive rational display scale. Zagkit converts them to its +26.6 fixed-point logical viewport for bounds checks. Every accepted or rejected +pixel attempt remains labelled `pixel`; it can never be reported as an ID +action. + +## Requests and evidence + +The machine-readable field, command, status, and capability vocabulary lives in +[the protocol contract](../../contracts/talkback-protocol.json). Request IDs +must be positive. Timeouts must be between 1 and 300000 milliseconds. Every +dispatch receives one monotonic event sequence and records the command, target +kind, target ID, semantic revision, status, coordinates, and scale. + +The in-process slice resolves semantic discovery and queries and validates +click, type, focus, and scroll before emitting them into the ordered event +stream. Protocol minor version 2 query responses distinguish read-only results +from emitted actions and expose the resolved node index, role, action mask, +fixed-point bounds, collection counts and coordinates, tree level and expansion +state, owned-text lengths, state flags, and a deterministic evidence hash over +the complete semantic node. This makes geometry and semantic-state changes +observable even before the native transport gains structured text payloads. +Capability reports and timeline counts are available. Key, drag, gesture, +wait, assertion payloads, screenshots, snapshots, replay, native action +consumption, and native transport remain unavailable and fail closed. +Advertising a command in the protocol vocabulary does not claim its runtime +capability. + +## Planned agent contract + +The complete control plane will add app and window discovery, structured query +results, text payloads, keyboard input, scrolling parameters, paths and +velocity for drag and gesture, waits and assertions, screenshots, frame +timelines, capability records, snapshots, and deterministic replay. Failed +actions will retain candidates, geometry, semantics, backend truth, and timeout +evidence in one inspectable bundle. These remain unchecked in the master goal +until their executable exit suites pass. diff --git a/docs/design/visual-direction.md b/docs/design/visual-direction.md new file mode 100644 index 0000000..8c9f12a --- /dev/null +++ b/docs/design/visual-direction.md @@ -0,0 +1,53 @@ +# Visual direction selection gate + +- Status: Required, not started +- Blocks: visual component production +- Does not block: architecture, headless primitives, semantics, text engines, + test infrastructure, and benchmark harnesses + +Zagkit needs its own adaptive design language before polished components are +implemented. It should feel precise, calm, fluid, and capable, with +SwiftUI-class motion and current Apple-class material coherence, without +copying a private implementation or becoming an imitation of Apple, Material, +Fluent, or a desktop widget theme. + +## Required decision package + +At least three materially different directions must be evaluated using the same +content and interactions. Each direction includes: + +- semantic color, typography, spacing, shape, elevation, material, motion, and + icon token proposals; +- physically coherent lighting, soft-shadow, and liquid-glass proposals with + reduced-transparency and CPU-oracle equivalents; +- crisp font, curve, SVG, and color-managed PNG evidence at every declared + scale factor; +- compact, medium, and expanded density behavior; +- desktop pointer and keyboard plus mobile touch examples; +- light, dark, high contrast, RTL, large text, reduced transparency, and reduced + motion variants; +- text field, button, menu, dialog, navigation, list, table, and viewport chrome; +- interrupt, reversal, gesture handoff, focus, error, disabled, selection, and + loading states; +- typography evidence across Latin, Arabic, Hebrew, Indic, CJK, Thai, and emoji; +- representative PrismStudio viewport, properties, table, menu, dialog, and + command-palette states using identical content in all three directions. + +## Selection criteria + +The chosen direction must preserve information hierarchy at large text, meet +contrast and target size requirements, expose unmistakable focus, avoid motion +as the only carrier of meaning, remain coherent from CAD density to touch +density, and stay implementable through semantic tokens rather than per-screen +exceptions. + +## Acceptance + +Maintainer and accessibility review accept one direction through an RFC. The +review names known risks and required conformance scenes. Until then, +[the component inventory](../../contracts/components.json) remains `planned` and +no screenshot can promote a component. + +Selection is exactly three directions. After selection, visual production must +match the accepted target; style is not improvised independently per component +or PrismStudio screen. diff --git a/docs/milestones/0000-product-contract.md b/docs/milestones/0000-product-contract.md new file mode 100644 index 0000000..4c56a99 --- /dev/null +++ b/docs/milestones/0000-product-contract.md @@ -0,0 +1,49 @@ +# Milestone 0 audit + +- Status: Implemented, pending review and CI +- Date: 2026-08-06 +- Runtime capability promoted: none + +## Deliverables + +| Requirement | Record | Gate | +|---|---|---| +| Separate Apache 2.0 repository | repository root and LICENSE | required file check | +| Governance | GOVERNANCE.md | required file check | +| Semantic versioning and labels | VERSIONING.md, zag.mod | version identity check | +| Support matrix | contracts/platforms.json | platform and capability validation | +| Architecture RFCs | docs/rfcs | accepted RFC check | +| Benchmark scenes | contracts/benchmark-scenes.json | scene and variant validation | +| Component inventory | contracts/components.json | component and visual gate validation | +| Compiler revision | contracts/toolchain.json | exact SHA, version, and edition validation | +| Upstream prerequisites | contracts/upstream-zag.json | required entry and evidence validation | +| Visual direction pause | docs/design/visual-direction.md | blocked gate validation | +| CI contract gate | tools/check-contracts.sh | local and GitHub Actions execution | +| Durable goal checklist | GOAL.md | stable ID, evidence, and exit-condition validation | +| Expanded toolkit contract | RFC 0006 | Flex, Talkback, visual fidelity, and PrismStudio validation | + +## Zag audit boundary + +The release pin resolves the clean Zag `agent/zagkit-prerequisites` commit +`43870455a07bf8e7d4adf38fad807fe3baee4e26`. It was exported from the exact Git +tree, self-hosted to a byte-identical stage-2/stage-3 fixpoint without external +tools, and passed resource embedding plus the 14-check general DEFLATE suite on +x86-64 and qemu-user ARM64. It also passed generic numeric raw-pointer slice +formation, aliasing, nested-slice, explicit-bound, and negative contracts on both +compiled architectures. The neighboring +local Zag checkout still contains extensive unrelated +changes; those changes were not used to claim capabilities or construct this +release identity. + +At the pinned commit, Zag documents x86-64 Linux as supported and ARM64 Linux as +experimental through qemu-user execution. Mach-O, PE/COFF, iOS, Android, the +platform ABIs, general foreign callbacks and aggregates, cross-platform resource +object formats, cross-platform dynamic loading, complete concurrency, package +resolution, and stable reload hooks remain missing or partial as recorded in the +upstream ledger. + +## Completion meaning + +Passing this milestone means the program can start against a coherent and +machine checked contract. It does not mean the headless core, Linux preview, +text engine, accessibility, components, renderer, or tooling exists. diff --git a/docs/quality/png-snapshots.md b/docs/quality/png-snapshots.md new file mode 100644 index 0000000..92565a2 --- /dev/null +++ b/docs/quality/png-snapshots.md @@ -0,0 +1,34 @@ +# Deterministic PNG snapshot bytes + +Status: experimental headless evidence primitive + +`png_encode_surface` converts a verified owned `CpuSurface` into one canonical +PNG byte stream. It writes the PNG signature, RGBA8 noninterlaced `IHDR`, an +explicit perceptual `sRGB` chunk, one `IDAT`, and `IEND`. Rows use PNG filter +zero. Chunk CRC32, the zlib Adler32 trailer, and stored DEFLATE blocks come from +the pinned pure-Zag standard-library primitives rather than foreign codecs. + +The encoder writes no timestamps, text, host metadata, or adaptive compression +choices. Identical CPU pixels and dimensions therefore produce byte-identical +PNG evidence. Width, height, pixel byte count, scanline size, zlib input, every +chunk CRC, decompressed scanline bytes, ordering, and absence of trailing data +have executable contracts. + +Stored DEFLATE intentionally favors a small deterministic trusted surface over +file size. The separate [`png_decode`](../architecture/images.md) asset path +accepts bounded stored, fixed, and dynamic DEFLATE PNGs; this snapshot encoder +does not share its adaptive input policy. A valid PNG file does not by itself +certify layout, accessibility, interaction, or native presentation. + +`tools/render-headless-reference.sh ` compiles and runs a pure-Zag +320 by 200 conformance scene and persists its PNG. The normal headless gate +renders it twice, requires byte identity, checks the PNG signature, and rejects +unexpectedly empty output. The scene exercises fixed-point fills, centered +strokes, alpha composition, a curved path, decoded image scaling, CPU raster, +PNG encoding, and file output. It deliberately contains no product typography +or selected design direction. + +Manifest naming, golden comparison, tolerances, retained Talkback evidence +bundles, and actual native screenshot capture remain open. Until those land, +this is a deterministic headless reference runner rather than the complete +snapshot runner or screenshot gate. diff --git a/docs/quality/release-gates.md b/docs/quality/release-gates.md new file mode 100644 index 0000000..5efdbc3 --- /dev/null +++ b/docs/quality/release-gates.md @@ -0,0 +1,121 @@ +# Quality and release gates + +This is the common evidence contract. Milestones select the applicable rows but +cannot weaken their meaning. + +## Headless correctness + +- Unit and property tests for state dependency tracking, bindings, keys, + reconciliation, constraints, intrinsic measurement, geometry, hit testing, + focus, gesture arbitration, and animation clocks. +- Fuzz tests for state event streams, malformed fonts, Unicode, bidi, line + breaking, display lists, semantics, resources, and replay files. +- Differential tests against published Unicode and OpenType data plus internal + slow reference implementations where possible. +- Deterministic replay with state, input, time, platform, backend, loss, and + recovery events. +- Resource ownership tests for creation failure, partial initialization, + cancellation, replacement, device loss, shutdown, and repeated startup. + +## Visual oracle + +CPU goldens cover every axis in +[the benchmark manifest](../../contracts/benchmark-scenes.json): scale factors, +light and dark themes, standard and high contrast, LTR and RTL, normal and large +text, full and reduced motion, and representative locales. + +GPU output is compared to CPU output with versioned tolerance rules by display +list operation, pixel format, color space, and device. An unexplained mismatch +fails. Updating a golden requires a reviewed intent record. + +Native screenshot comparisons cover crisp typography, curves, SVG, PNG, +lighting, shadows, and transparent materials at every declared scale. Each +comparison records viewport, state, backend, color space, scale, fonts, theme, +contrast, direction, text scale, motion, and transparency preferences. +Screenshots remain necessary visual evidence, not complete product evidence. + +## Flex and Zagkit Talkback + +Flex conformance covers gap, padding, alignment, distribution, baseline, wrap, +intrinsic size, constraints, grid tracks and spans, overlay placement, +breakpoints, safe areas, density, RTL, and large text. Grid collisions fail +closed unless overlap is explicit, and every primitive reports quantified +overflow plus deterministic identity. Every layout change reports its exact +state read and rule. + +Virtualization conformance requires stable semantic identity independent of +live-node residency, variable-extent anchor stability, bounded overscan, +fail-closed allocation ceilings, focus restoration, and no linear work per +scroll frame. A million-row range proof is necessary but does not replace the +120 Hz reference-hardware and ten-minute stall gates. + +Table gates additionally cover two-axis residency, pinned-column occlusion, +resize bounds, composite cell identity, keyboard grids, and editable-cell +focus. Tree gates cover expansion, collapse, parent relationships, logical +indentation, selection, and focus restoration. Recycling cannot transfer a +logical generation between IDs, and offscreen focused nodes remain protected +until focus explicitly moves or clears. + +Zagkit Talkback conformance drives canonical applications by stable ID and +checks discovery, query, action, wait, assertion, screenshot, timeline, +capability, snapshot, and replay behavior. Pixel fallback is separately +reported, scale-aware, and forbidden where a required actionable semantic node +should exist. This automation never replaces native assistive-technology runs. + +## Live platform suite + +Each promoted target runs resize, suspend and resume, background and foreground, +surface and device loss, multi window, multi monitor or display movement, +scaling, clipboard, drag and drop, IME composition, missing fonts, keyboard only +operation, focus restoration, shutdown, and crash recovery. + +Native assistive technology runs are mandatory: + +- AT-SPI on Linux; +- VoiceOver on macOS and iOS; +- Narrator on Windows; +- TalkBack on Android. + +Automation through the semantics tree does not replace these runs. + +## Motion and input + +Canonical tests cover interruption, reversal, gesture handoff, capture loss, +reduced motion substitution, resize during transition, refresh rate changes, +coalesced input, and deterministic replay. Position and velocity continuity are +measured where applicable. + +## Performance + +- Active interactions meet the display deadline at p99. +- Scrolling sustains 120 Hz on supported reference hardware. +- Idle scenes perform zero continuous layout and paint work. +- Ten minute canonical runs contain zero unexplained two frame stalls. +- CPU time, GPU time, memory, allocations, cache size, resource counts, input + latency, and frame reasons are captured. +- A regression over 5 percent fails unless a reviewed, expiring waiver exists. + +Reference hardware, thermal state, power mode, OS build, display configuration, +compiler revision, backend, warmup, sample count, and raw results are retained. + +## Packaging and release + +Signed artifacts install, launch, update, and uninstall on every supported OS. +Reproducible builds record exact source and tool inputs plus artifact hashes. +Release notes include capability records, reference hardware, benchmark results, +known unsupported features, and untested hardware. + +## Promotion levels + +| Level | Minimum evidence | +|---|---| +| Experimental | bounded executable slice or specification with explicit unavailable boundaries | +| Alpha | coherent end to end slice with repeatable native tests and known recovery gaps | +| Beta | intended API plus native accessibility, text input, recovery, performance, and packaging evidence | +| Stable | complete common gates, supported version policy, reproducible signed artifacts, and support process | +| 1.0 | Stable gate on all five required platform families plus PrismStudio and mobile reference proof | + +The PrismStudio proof is a complete UI replacement on Zagkit, not a sample +screen or partial shell migration. Linux polish additionally requires zero open +severity-one or severity-two visual, spacing, text, input, accessibility, +automation, recovery, performance, or packaging defects. diff --git a/docs/rfcs/0000-rfc-process.md b/docs/rfcs/0000-rfc-process.md new file mode 100644 index 0000000..4a61bff --- /dev/null +++ b/docs/rfcs/0000-rfc-process.md @@ -0,0 +1,36 @@ +# RFC 0000: RFC process + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## Purpose + +An RFC records a reviewable decision that changes Zagkit's public API, +architecture, runtime dependency boundary, support truth model, or release +gate. It prevents consequential decisions from being hidden in implementation. + +## Lifecycle + +1. A proposal begins as `Proposed` with context, decision, alternatives, + consequences, compatibility, safety, and verification sections. +2. Maintainers can request evidence or a prototype before accepting it. +3. `Accepted` makes the decision normative. It does not imply implementation. +4. `Implemented` requires links to the executable gates named by the RFC. +5. A later RFC may mark it `Superseded`; rejected proposals remain in history. + +Status changes use dated pull requests. Material scope changes return an RFC to +`Proposed` rather than silently rewriting an accepted decision. + +## Required review + +Dependency and platform seam RFCs require security and ownership review. +Visual system RFCs require accessibility review. Platform promotion and release +gate RFCs require native evidence on the affected targets. + +## Evidence rule + +Prose, code presence, compilation, screenshots, cross execution, and emulation +can support review but do not prove native capability. An implementation claim +names the executable command, exact revision, target device, result artifact, +cleanup behavior, and any unavailable boundary. diff --git a/docs/rfcs/0001-product-and-platform-contract.md b/docs/rfcs/0001-product-and-platform-contract.md new file mode 100644 index 0000000..f92f3cb --- /dev/null +++ b/docs/rfcs/0001-product-and-platform-contract.md @@ -0,0 +1,63 @@ +# RFC 0001: Product and platform contract + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## Context + +A widget library cannot provide coherent text, gesture, animation, +accessibility, recovery, and tooling guarantees across desktop and mobile. +Proxying platform widgets also fragments behavior and makes renderer truth +uninspectable. + +## Decision + +Zagkit is a separately versioned first party application platform built in Zag. +It owns the view, layout, semantics, text, input, motion, rendering, component, +and tooling layers. It targets Linux, macOS, Windows, iOS, and Android with one +adaptive design system. + +Web, browser rendering, and WebView hosting are outside 1.0. Zagkit is not an +exact imitation of any platform, but adaptive components respect platform input, +navigation, density, menu, typography, and accessibility conventions. + +The parity baseline is the capability developers expect from mature declarative +systems: [SwiftUI accessibility](https://developer.apple.com/documentation/swiftui/accessibility-fundamentals), +[Compose state and semantics](https://developer.android.com/develop/ui/compose/architecture), +[Flutter's layered engine and embedders](https://docs.flutter.dev/resources/architectural-overview), +and the [Qt Quick scene graph](https://doc.qt.io/QT-6/qtquick-visualcanvas-scenegraph.html). +These are architecture references, not runtime dependencies. + +PrismStudio is the first complex consumer after the Linux preview. A focused +mobile reference app proves the mobile architecture. The component gallery is a +conformance tool, not the only product proof. + +## Tooling contract + +The first party command surface is `zagkit init`, `zagkit build`, `zagkit run`, +and `zagkit test`. The development environment includes live preview and reload, +a component gallery, semantic inspector, layout reason inspector, frame +timeline, accessibility overlay, theme editor, snapshot runner, and capability +report. These tools consume the same state, semantics, display list, scheduler, +and capability records as applications. Debug tooling cannot become a second +renderer or hidden source of behavior. + +## Compatibility + +Public API begins unstable under `0.x` SemVer. No platform receives the product +name 1.0 early. The initial stable API freezes only after all five families pass +the shared gate. + +## Consequences + +This is a larger program than wrapping existing native widgets. It creates one +coherent interaction and inspection model, keeps platform seams replaceable, +and forces missing compiler and runtime primitives to improve Zag for every +consumer. + +## Verification + +The repository contract gate must find exactly five required platform families, +an active 1.0 block, an exact compiler pin, and no unsupported capability claim. +Runtime verification is defined in RFC 0005. diff --git a/docs/rfcs/0002-declarative-core-and-rendering.md b/docs/rfcs/0002-declarative-core-and-rendering.md new file mode 100644 index 0000000..7bb6f70 --- /dev/null +++ b/docs/rfcs/0002-declarative-core-and-rendering.md @@ -0,0 +1,69 @@ +# RFC 0002: Declarative core and rendering architecture + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## State and authoring contract + +State flows down and actions flow up. The initial public concepts are: + +- `State` owns observable value and revision identity; +- `Binding` provides a scoped read and action based write path; +- `NodeKey` supplies stable identity across reconciliation; +- `Action` is an application intent, not an arbitrary mutation callback; +- `Environment` carries typed inherited values; +- `ViewContext` records state reads, environment reads, actions, and child + identity during view evaluation. + +The exact Zag syntax remains implementation work. These semantic names cannot +be repurposed without a superseding RFC. + +Each state or environment read is tracked. When work is invalidated, the +inspector can name the read identity, old revision, new revision, affected node, +and whether view, measure, layout, paint, or semantics work followed. + +`RenderNode` is the retained escape hatch for specialized layout and painting. +`Canvas` is the immediate drawing escape hatch. Neither bypasses resource +ownership, semantics, damage, capability, or replay contracts. + +## Layout contract + +The core types are `Constraints`, `Size`, and `Rect`. Layout supports intrinsic +measurement, stack, flex, grid, overlay, scroll, virtual list, table, tree, safe +areas, breakpoints, direction, text scale, and platform density. + +Constraints are finite or explicitly unbounded by axis. Measurement and layout +must be deterministic for the same inputs. Cycles, non-finite geometry, +overflow, unstable intrinsic measurement, and duplicate keys fail with +inspectable reasons. + +The first executable Grid and Overlay interpretation of this decision is +documented in [the placement contract](../architecture/grid-and-overlay.md). +That experimental surface does not freeze the eventual declarative View API. + +## Rendering contract + +Rendering produces an immutable `DisplayList` of paths, paints, images, glyph +runs, clips, transforms, layers, and effects. Resources are content addressed +and carry explicit lifetime. Damage and cache reuse cannot change output. + +The CPU renderer is deterministic and is the visual oracle. GPU transports +consume a versioned Zag owned render IR and are compared to the oracle within +documented operation and device tolerances. Metal, D3D12, Vulkan or other +public Linux and Android submission APIs are transports, not the architecture. + +## Scheduling and motion + +The scheduler is refresh rate aware, does no continuous layout or paint while +idle, and records why every frame exists. Springs, keyframes, layout transitions, +and shared transitions are interruptible and deterministic under a supplied +clock. Reduced motion changes behavior through explicit substitution, not a +global duration multiplier. + +## Verification + +Property tests cover constraints and identity. Deterministic replay covers state, +input, time, and backend events. CPU goldens cover the full variant matrix. GPU +comparison, interruption, reversal, resize, idle, and resource cleanup gates +are mandatory before promotion. diff --git a/docs/rfcs/0003-text-semantics-and-input.md b/docs/rfcs/0003-text-semantics-and-input.md new file mode 100644 index 0000000..e60d3da --- /dev/null +++ b/docs/rfcs/0003-text-semantics-and-input.md @@ -0,0 +1,54 @@ +# RFC 0003: Text, semantics, and input ownership + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## Text decision + +Zagkit owns Unicode decoding and normalization, bidi, segmentation, line +breaking, OpenType shaping, font fallback, variable and color fonts, emoji, +selection, editing, and rasterization. System fonts and published Unicode and +OpenType data are inputs. FreeType, HarfBuzz, Skia, native text widgets, and +browser text engines are not runtime dependencies. + +Malformed input, fonts, tables, clusters, and variation data must fail safely +under fuzzing. Font absence is an observable fallback event, never silent +replacement presented as exact typography. + +## Semantics decision + +Every component produces a parallel `SemanticsNode` tree. Nodes carry stable +identity, role, name, description, value, state, actions, focus order, live +region behavior, selection, ranges, text navigation, bounds, and relationships. + +The semantics tree drives native accessibility adapters, automation, semantic +tests, and the inspector. Accessibility does not scrape pixels or infer meaning +from implementation type names. Visual and semantic trees may differ in shape, +but their relationship is inspectable. + +## Input decision + +The normalized input model covers pointer, keyboard, touch, pen, wheel, +gamepad, focus, commands, drag and drop, and IME composition. Gesture +recognizers participate in explicit arbitration and cancellation. Velocity, +coalesced events, history, capture, and gesture handoff are retained so motion +does not jump when ownership changes. + +Platform text clients remain required seams. AppKit and UIKit adapters expose +their public [accessibility](https://developer.apple.com/documentation/appkit/accessibility-for-appkit) +and [text input](https://developer.apple.com/documentation/appkit/nstextinputclient) +contracts; Windows uses its public +[custom text input](https://learn.microsoft.com/en-us/windows/apps/develop/input/custom-text-input) +and UI Automation contracts; Linux and Android provide equivalent public +adapters. System APIs transport editing and accessibility state but do not own +the document model or semantics tree. + +## Verification + +Unicode and font parsers receive unit, property, fuzz, and differential tests +against published data. Editing suites cover composition in several script +families, selection, replacement, undo, clipboard, missing fonts, and malformed +input. Semantics suites cover role, name, value, actions, focus, live regions, +selection, ranges, and text navigation in automation and native assistive +technologies. diff --git a/docs/rfcs/0004-platform-seams-and-backend-truth.md b/docs/rfcs/0004-platform-seams-and-backend-truth.md new file mode 100644 index 0000000..5077ec8 --- /dev/null +++ b/docs/rfcs/0004-platform-seams-and-backend-truth.md @@ -0,0 +1,54 @@ +# RFC 0004: Platform seams and backend truth + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## Platform boundary + +Public platform APIs are permitted for lifecycle, windows, surfaces, vsync, GPU +submission, IME, accessibility, clipboard, drag and drop, menus, haptics, +notifications, signing, and packaging. Each adapter is narrow, owned through Zag +types, and independently conformable. Private APIs are prohibited. + +Reusable target, ABI, resource, loading, concurrency, and package features live +in Zag. The prerequisite ledger blocks Zagkit milestones until native Zag +conformance exists. + +## Capability model + +`PlatformCapabilities` is the public immutable snapshot. `BackendEvent` is the +ordered lifecycle record used by diagnostics, replay, tests, and inspectors. + +Every runtime feature reports one of: + +- `supported`, with complete native evidence for the active target; +- `experimental`, with executable evidence plus a named incomplete gate; +- `unavailable`, with a mandatory reason. + +The initial feature set includes shell, CPU renderer, GPU transport, text input, +accessibility, clipboard and drag and drop, multi window, packaging, and backend +selection. Future features use the same vocabulary. + +`.auto` records the actual backend chosen and why higher priority candidates +were rejected. Startup fallback, surface loss, device loss, recovery, and +runtime fallback append timestamped events containing source backend, target +backend, cause, resource impact, and whether visual continuity was preserved. + +No request setting can override unavailable truth. An informed developer may +explicitly select an experimental backend, but the resulting record remains +experimental. + +## GPU boundary + +The CPU renderer remains authoritative. GPU discovery is not submission, +submission is not correct readback, and correct readback is not general renderer +conformance. Physical GPU dispatch is explicit and externally bounded when the +device is display bound or fault isolation is unavailable. + +## Verification + +Capability tests cover priority, explicit selection, unavailable reasons, +startup fallback, mid-frame loss, surface recreation, resource cleanup, event +ordering, and crash recovery. Native platform promotion requires the evidence +bundle defined in RFC 0005. diff --git a/docs/rfcs/0005-quality-and-release-contract.md b/docs/rfcs/0005-quality-and-release-contract.md new file mode 100644 index 0000000..5d70b1e --- /dev/null +++ b/docs/rfcs/0005-quality-and-release-contract.md @@ -0,0 +1,37 @@ +# RFC 0005: Quality and release contract + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## Decision + +Quality, performance, accessibility, tooling, recovery, and packaging are +release requirements. They are not post-1.0 polish. + +The normative gates are listed in +[release-gates.md](../quality/release-gates.md). A result bundle identifies the +Zagkit and Zag commits, target, device, OS, backend capability record, commands, +durations, artifacts, and cleanup outcome. + +## Performance + +Canonical interactions meet the active display deadline at p99 and preserve +120 Hz scrolling on supported reference hardware. Idle scenes perform no +continuous layout or paint work. Ten minute scripted runs contain no unexplained +two frame stalls. A regression over 5 percent requires a reviewed waiver with +scope, reason, owner, expiry, and recovery plan. + +## Visual correctness + +CPU goldens run across scale, theme, contrast, direction, text scale, reduced +motion, and representative locale axes. GPU output is compared to the CPU oracle +using documented per-operation tolerances. Screenshots alone do not certify +input, semantics, text editing, accessibility, recovery, or performance. + +## Release truth + +Experimental, alpha, and beta releases state their missing gates. 1.0 is +blocked until Linux, macOS, Windows, iOS, and Android pass the common component, +consumer, mobile reference, text, accessibility, recovery, performance, +packaging, and reproducibility requirements. diff --git a/docs/rfcs/0006-flex-talkback-visual-fidelity-and-prismstudio.md b/docs/rfcs/0006-flex-talkback-visual-fidelity-and-prismstudio.md new file mode 100644 index 0000000..1cce99b --- /dev/null +++ b/docs/rfcs/0006-flex-talkback-visual-fidelity-and-prismstudio.md @@ -0,0 +1,103 @@ +# RFC 0006: Flex, Talkback, visual fidelity, and PrismStudio + +- Status: Accepted +- Decision date: 2026-08-06 +- Owners: Zagkit maintainers + +## Decision + +Zagkit includes a first-party placement system named **Flex**, a first-party +native automation and inspection system named **Zagkit Talkback**, and a modern +adaptive visual-material system. The complete PrismStudio interface will be +rebuilt on these public Zagkit contracts and is required product proof. + +These are core architecture and release requirements. They are not examples, +optional packages, post-1.0 polish, or PrismStudio-only helpers. + +## Flex + +Flex is the public placement and spacing contract for stacks, wrapping rows and +columns, grids, overlays, scroll content, safe areas, and adaptive composition. +It owns typed length and spacing tokens, gap, padding, alignment, distribution, +baseline alignment, wrapping, minimum and maximum constraints, priorities, +intrinsic sizes, breakpoints, and layout transitions. + +All values are density-independent and resolve through explicit platform scale, +text scale, direction, safe-area, and environment inputs. RTL changes logical +start and end, not physical truth. Stable layout inspection reports the node, +resolved inputs, state read, rule, and ancestor that caused every measurement or +placement. Product screens must compose public Flex rules and semantic tokens; +unexplained per-screen offsets are conformance failures. + +## Zagkit Talkback + +Zagkit Talkback is the Playwright-equivalent control plane for native Zagkit +applications. It uses the same retained node and semantics truth as rendering +and accessibility. Developer-assigned IDs are preferred; deterministic +generated IDs are available for framework-owned nodes. IDs are scoped by app, +window, and retained node identity and have defined behavior across +virtualization, navigation, reload, replacement, and stale references. + +The versioned protocol supports discovery, semantic and property queries, +click, type, key, focus, scroll, drag, gesture, wait, assertion, screenshot, +timeline, capability report, snapshot, and deterministic replay. Normal agent +automation targets IDs. Pixel coordinates are an explicit, scale-aware fallback +for canvases or unavailable semantics; every fallback is recorded and never +reported as an ID action. Failed actions return structured candidates, geometry, +semantics, capability truth, and timeout evidence. + +The product name is always qualified as **Zagkit Talkback** in contexts where it +could be confused with **Android TalkBack**, the assistive technology. Zagkit +Talkback does not replace AT-SPI, VoiceOver, Narrator, or Android TalkBack tests. + +## Visual and asset fidelity + +Zagkit's renderer owns scalable curves, anti-aliasing, subpixel-aware placement, +color management, font rasterization, SVG, PNG, gradients, masks, filters, +lighting, soft shadows, and adaptive materials. SVG and PNG decoders are +bounded, fuzzed, color-managed, and never delegated to a foreign UI engine. + +The visual language may use liquid-glass materials: backdrop sampling, blur, +tint, refraction or distortion, edge highlights, specular response, depth, and +motion must form one coherent material model. Text and controls remain legible +over changing content. Reduced transparency, high contrast, reduced motion, and +CPU fallback have designed equivalents. Effects must remain crisp across every +declared scale and may not collapse into hard, pixelated approximations. + +The target is competitive capability and finish, including SwiftUI-class +fluidity and current Apple-class material coherence, without copying Apple's +private implementation or making Zagkit an imitation of a platform skin. + +## PrismStudio proof + +Milestone 4 is a complete visible UI replacement, not a partial migration. +Zagkit must own the supported Linux shell, placement, styling, assets, +components, semantics, focus, input, and automation. Existing CAD workflows, +keyboard control, direct manipulation, tables, trees, menus, dialogs, viewport, +performance evidence, and GPU safety boundaries remain functional while the +experience is redesigned to the selected Zagkit direction. + +Every actionable node receives a stable Talkback ID. Canonical tasks run by ID, +with pixel fallback permitted only for declared canvas interactions and always +reported. Native screenshots at matched state, viewport, scale, theme, and +effect settings are compared to approved direction images and CPU goldens. +Screenshots are necessary visual proof but do not replace interaction, +semantics, accessibility, recovery, or performance evidence. + +Linux is the first polish reference. Its preview cannot be called polished +while known severity-one or severity-two defects remain in visual fidelity, +spacing, text, input, accessibility, recovery, automation, or packaging. + +## Visual selection gate + +Before visual component or PrismStudio production, maintainers compare exactly +three materially different directions using the same representative CAD and +component states. Review covers light, dark, high contrast, large text, RTL, +reduced motion, and reduced transparency. One direction is accepted by RFC; +implementation then follows that target rather than inventing style per screen. + +## Evidence + +The durable itemized exit conditions live in [the master goal checklist](../../GOAL.md). +Removing or weakening those items requires an RFC amendment. Capability records +remain fail-closed until executable evidence exists. diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md new file mode 100644 index 0000000..7efec5a --- /dev/null +++ b/docs/rfcs/README.md @@ -0,0 +1,14 @@ +# RFC index + +| RFC | Title | Status | +|---|---|---| +| [0000](0000-rfc-process.md) | RFC process | Accepted | +| [0001](0001-product-and-platform-contract.md) | Product and platform contract | Accepted | +| [0002](0002-declarative-core-and-rendering.md) | Declarative core and rendering architecture | Accepted | +| [0003](0003-text-semantics-and-input.md) | Text, semantics, and input ownership | Accepted | +| [0004](0004-platform-seams-and-backend-truth.md) | Platform seams and backend truth | Accepted | +| [0005](0005-quality-and-release-contract.md) | Quality and release contract | Accepted | +| [0006](0006-flex-talkback-visual-fidelity-and-prismstudio.md) | Flex, Talkback, visual fidelity, and PrismStudio | Accepted | + +Accepted RFCs are decisions, not proof that implementation exists. Current +runtime truth remains in [the platform capability record](../../contracts/platforms.json). diff --git a/src/automation/talkback.zag b/src/automation/talkback.zag new file mode 100644 index 0000000..0151330 --- /dev/null +++ b/src/automation/talkback.zag @@ -0,0 +1,349 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("../semantics/semantics.zag") + +enum TalkbackTargetKind { semantic_id, pixel } + +enum TalkbackCommand { + discover, + query, + click, + type_text, + key, + focus, + scroll, + drag, + gesture, + wait_for, + assert_value, + screenshot, + timeline, + capability_report, + snapshot, + replay, +} + +enum TalkbackStatus { + accepted, + invalid_request, + invalid_timeout, + stale_semantics, + target_not_found, + action_unavailable, + pixel_fallback_disabled, + pixel_out_of_bounds, + unsupported_command, +} + +struct TalkbackCapabilities { + protocol_major: i64, + protocol_minor: i64, + semantic_query: i32, + id_actions: i32, + pixel_fallback: i32, + screenshots: i32, + replay: i32, +} + +struct TalkbackRequest { + request_id: i64, + command: TalkbackCommand, + target_kind: TalkbackTargetKind, + target_id: NodeKey, + semantics_revision: i64, + timeout_ms: i64, + pixel_x: i64, + pixel_y: i64, + scale_numerator: i64, + scale_denominator: i64, +} + +struct TalkbackResponse { + request_id: i64, + status: TalkbackStatus, + target_kind: TalkbackTargetKind, + target_id: NodeKey, + semantics_revision: i64, + event_sequence: i64, + resolved_node_index: i32, + result_count: i32, + emitted_action: i32, + resolved_role: SemanticRole, + action_mask: i64, + has_bounds: i32, + bounds: Rect, + row_count: i64, + column_count: i64, + row_index: i64, + column_index: i64, + level: i64, + expanded: i32, + name_length: i32, + description_length: i32, + value_length: i32, + state_flags: i32, + evidence_hash: i64, +} + +struct TalkbackEvent { + sequence: i64, + request_id: i64, + command: TalkbackCommand, + target_kind: TalkbackTargetKind, + target_id: NodeKey, + status: TalkbackStatus, + semantics_revision: i64, + pixel_x: i64, + pixel_y: i64, + scale_numerator: i64, + scale_denominator: i64, +} + +struct TalkbackSession { + capabilities: TalkbackCapabilities, + events: ArrayList[TalkbackEvent], + next_sequence: i64, +} + +fn talkback_capabilities() TalkbackCapabilities { + return TalkbackCapabilities{ + .protocol_major = 0, + .protocol_minor = 2, + .semantic_query = 1, + .id_actions = 1, + .pixel_fallback = 0, + .screenshots = 0, + .replay = 0, + }; +} + +fn talkback_session_make(capabilities: TalkbackCapabilities) TalkbackSession { + return TalkbackSession{ + .capabilities = capabilities, + .events = make[TalkbackEvent](16), + .next_sequence = 1, + }; +} + +fn talkback_session_free(session: *TalkbackSession) void { + free[TalkbackEvent](&session.*.events); +} + +fn talkback_request(request_id: i64, command: TalkbackCommand, target_id: NodeKey, semantics_revision: i64) TalkbackRequest { + return TalkbackRequest{ + .request_id = request_id, + .command = command, + .target_kind = TalkbackTargetKind.semantic_id, + .target_id = target_id, + .semantics_revision = semantics_revision, + .timeout_ms = 1000, + .pixel_x = 0, + .pixel_y = 0, + .scale_numerator = 1, + .scale_denominator = 1, + }; +} + +fn talkback_pixel_request(request_id: i64, command: TalkbackCommand, pixel_x: i64, pixel_y: i64, scale_numerator: i64, scale_denominator: i64) TalkbackRequest { + let request: TalkbackRequest = talkback_request(request_id, command, semantic_root_key(), 0 - 1); + request.target_kind = TalkbackTargetKind.pixel; + request.pixel_x = pixel_x; + request.pixel_y = pixel_y; + request.scale_numerator = scale_numerator; + request.scale_denominator = scale_denominator; + return request; +} + +fn talkback_command_has_target(command: TalkbackCommand) i32 { + return switch (command) { + .discover => 0, + .query => 1, + .click => 1, + .type_text => 1, + .key => 1, + .focus => 1, + .scroll => 1, + .drag => 1, + .gesture => 1, + .wait_for => 1, + .assert_value => 1, + .screenshot => 0, + .timeline => 0, + .capability_report => 0, + .snapshot => 0, + .replay => 0, + }; +} + +fn talkback_action_supported(capabilities: TalkbackCapabilities, target_kind: TalkbackTargetKind, pixel_action: i32) i32 { + if (target_kind == TalkbackTargetKind.pixel) { + if (pixel_action == 0) { return 0; } + return capabilities.pixel_fallback; + } + return capabilities.id_actions; +} + +fn talkback_command_supported(capabilities: TalkbackCapabilities, command: TalkbackCommand, target_kind: TalkbackTargetKind) i32 { + return switch (command) { + .discover => capabilities.semantic_query, + .query => capabilities.semantic_query, + .click => talkback_action_supported(capabilities, target_kind, 1), + .type_text => talkback_action_supported(capabilities, target_kind, 0), + .key => 0, + .focus => talkback_action_supported(capabilities, target_kind, 0), + .scroll => talkback_action_supported(capabilities, target_kind, 1), + .drag => 0, + .gesture => 0, + .wait_for => 0, + .assert_value => 0, + .screenshot => capabilities.screenshots, + .timeline => 1, + .capability_report => 1, + .snapshot => 0, + .replay => capabilities.replay, + }; +} + +fn talkback_required_action(command: TalkbackCommand) i64 { + return switch (command) { + .click => semantic_action_bit(SemanticAction.activate), + .type_text => semantic_action_bit(SemanticAction.set_value), + .focus => semantic_action_bit(SemanticAction.focus), + .scroll => semantic_action_bit(SemanticAction.scroll), + .discover => 0, + .query => 0, + .key => 0, + .drag => 0, + .gesture => 0, + .wait_for => 0, + .assert_value => 0, + .screenshot => 0, + .timeline => 0, + .capability_report => 0, + .snapshot => 0, + .replay => 0, + }; +} + +fn talkback_record(session: *TalkbackSession, tree_revision: i64, request: TalkbackRequest, status: TalkbackStatus) TalkbackResponse { + let sequence: i64 = session.*.next_sequence; + session.*.next_sequence = session.*.next_sequence + 1; + push[TalkbackEvent](&session.*.events, TalkbackEvent{ + .sequence = sequence, + .request_id = request.request_id, + .command = request.command, + .target_kind = request.target_kind, + .target_id = request.target_id, + .status = status, + .semantics_revision = tree_revision, + .pixel_x = request.pixel_x, + .pixel_y = request.pixel_y, + .scale_numerator = request.scale_numerator, + .scale_denominator = request.scale_denominator, + }); + return TalkbackResponse{ + .request_id = request.request_id, + .status = status, + .target_kind = request.target_kind, + .target_id = request.target_id, + .semantics_revision = tree_revision, + .event_sequence = sequence, + .resolved_node_index = 0 - 1, + .result_count = 0, + .emitted_action = 0, + .resolved_role = SemanticRole.group, + .action_mask = 0, + .has_bounds = 0, + .bounds = rect(0, 0, 0, 0), + .row_count = 0 - 1, + .column_count = 0 - 1, + .row_index = 0 - 1, + .column_index = 0 - 1, + .level = 0, + .expanded = 0 - 1, + .name_length = 0, + .description_length = 0, + .value_length = 0, + .state_flags = 0, + .evidence_hash = 0, + }; +} + +fn talkback_dispatch(session: *TalkbackSession, tree: SemanticsTree, viewport: Rect, request: TalkbackRequest) TalkbackResponse { + if (request.request_id <= 0) { + return talkback_record(session, tree.revision, request, TalkbackStatus.invalid_request); + } + if (request.timeout_ms <= 0 || request.timeout_ms > 300000) { + return talkback_record(session, tree.revision, request, TalkbackStatus.invalid_timeout); + } + if (talkback_command_has_target(request.command) != 0 && + request.target_kind == TalkbackTargetKind.pixel && + session.*.capabilities.pixel_fallback == 0) { + return talkback_record(session, tree.revision, request, TalkbackStatus.pixel_fallback_disabled); + } + if (talkback_command_supported(session.*.capabilities, request.command, request.target_kind) == 0) { + return talkback_record(session, tree.revision, request, TalkbackStatus.unsupported_command); + } + if (talkback_command_has_target(request.command) == 0) { + let response: TalkbackResponse = talkback_record(session, tree.revision, request, TalkbackStatus.accepted); + if (request.command == TalkbackCommand.discover) { response.result_count = tree.nodes.len; } + if (request.command == TalkbackCommand.timeline) { response.result_count = session.*.events.len; } + if (request.command == TalkbackCommand.capability_report) { response.result_count = 1; } + return response; + } + + if (request.target_kind == TalkbackTargetKind.pixel) { + if (request.scale_numerator <= 0 || request.scale_numerator > 16 || + request.scale_denominator <= 0 || request.scale_denominator > 16 || + request.pixel_x < 0 || request.pixel_y < 0) { + return talkback_record(session, tree.revision, request, TalkbackStatus.invalid_request); + } + let logical_x: i64 = (request.pixel_x * unit_scale() * request.scale_denominator) / request.scale_numerator; + let logical_y: i64 = (request.pixel_y * unit_scale() * request.scale_denominator) / request.scale_numerator; + if (rect_contains(viewport, logical_x, logical_y) == 0) { + return talkback_record(session, tree.revision, request, TalkbackStatus.pixel_out_of_bounds); + } + let response: TalkbackResponse = talkback_record(session, tree.revision, request, TalkbackStatus.accepted); + response.emitted_action = 1; + return response; + } + + if (request.semantics_revision >= 0 && request.semantics_revision != tree.revision) { + return talkback_record(session, tree.revision, request, TalkbackStatus.stale_semantics); + } + let node_index: i32 = semantics_find_index(tree, request.target_id); + if (node_index < 0 || tree.nodes.data[node_index].hidden != 0) { + return talkback_record(session, tree.revision, request, TalkbackStatus.target_not_found); + } + let required_action: i64 = talkback_required_action(request.command); + if (tree.nodes.data[node_index].disabled != 0 || + (required_action != 0 && (tree.nodes.data[node_index].action_mask & required_action) == 0)) { + return talkback_record(session, tree.revision, request, TalkbackStatus.action_unavailable); + } + let response: TalkbackResponse = talkback_record(session, tree.revision, request, TalkbackStatus.accepted); + response.resolved_node_index = node_index; + response.result_count = 1; + response.resolved_role = tree.nodes.data[node_index].role; + response.action_mask = tree.nodes.data[node_index].action_mask; + response.has_bounds = tree.nodes.data[node_index].has_bounds; + response.bounds = tree.nodes.data[node_index].bounds; + response.row_count = tree.nodes.data[node_index].row_count; + response.column_count = tree.nodes.data[node_index].column_count; + response.row_index = tree.nodes.data[node_index].row_index; + response.column_index = tree.nodes.data[node_index].column_index; + response.level = tree.nodes.data[node_index].level; + response.expanded = tree.nodes.data[node_index].expanded; + response.name_length = tree.nodes.data[node_index].name.len; + response.description_length = tree.nodes.data[node_index].description.len; + response.value_length = tree.nodes.data[node_index].value.len; + response.state_flags = tree.nodes.data[node_index].disabled; + if (tree.nodes.data[node_index].selected != 0) { + response.state_flags = response.state_flags | 2; + } + if (tree.nodes.data[node_index].hidden != 0) { + response.state_flags = response.state_flags | 4; + } + response.evidence_hash = semantic_node_evidence_hash(tree.nodes.data[node_index]); + if (required_action != 0) { response.emitted_action = 1; } + return response; +} diff --git a/src/core/geometry.zag b/src/core/geometry.zag new file mode 100644 index 0000000..e00c6f4 --- /dev/null +++ b/src/core/geometry.zag @@ -0,0 +1,104 @@ +// Deterministic layout geometry. All values are signed 26.6 fixed-point units: +// 64 units equal one density-independent logical pixel. + +fn unit_scale() i64 { return 64; } +fn unbounded_extent() i64 { return 1125899906842624; } + +struct Size { + width: i64, + height: i64, +} + +struct Rect { + x: i64, + y: i64, + width: i64, + height: i64, +} + +struct Insets { + top: i64, + right: i64, + bottom: i64, + left: i64, +} + +struct Constraints { + min_width: i64, + max_width: i64, + min_height: i64, + max_height: i64, +} + +fn size(width: i64, height: i64) Size { + return Size{ .width = width, .height = height }; +} + +fn rect(x: i64, y: i64, width: i64, height: i64) Rect { + return Rect{ .x = x, .y = y, .width = width, .height = height }; +} + +fn insets_all(value: i64) Insets { + let v: i64 = value; + if (v < 0) { v = 0; } + return Insets{ .top = v, .right = v, .bottom = v, .left = v }; +} + +fn clamp_i64(value: i64, minimum: i64, maximum: i64) i64 { + let lo: i64 = minimum; + let hi: i64 = maximum; + if (lo < 0) { lo = 0; } + if (hi < lo) { hi = lo; } + if (value < lo) { return lo; } + if (value > hi) { return hi; } + return value; +} + +fn normalize_constraints(value: Constraints) Constraints { + let min_width: i64 = value.min_width; + let min_height: i64 = value.min_height; + if (min_width < 0) { min_width = 0; } + if (min_height < 0) { min_height = 0; } + + let max_width: i64 = value.max_width; + let max_height: i64 = value.max_height; + if (max_width < min_width) { max_width = min_width; } + if (max_height < min_height) { max_height = min_height; } + + return Constraints{ + .min_width = min_width, + .max_width = max_width, + .min_height = min_height, + .max_height = max_height, + }; +} + +fn tight_constraints(width: i64, height: i64) Constraints { + let w: i64 = width; + let h: i64 = height; + if (w < 0) { w = 0; } + if (h < 0) { h = 0; } + return Constraints{ .min_width = w, .max_width = w, .min_height = h, .max_height = h }; +} + +fn constrain_size(value: Size, raw: Constraints) Size { + let c: Constraints = normalize_constraints(raw); + return Size{ + .width = clamp_i64(value.width, c.min_width, c.max_width), + .height = clamp_i64(value.height, c.min_height, c.max_height), + }; +} + +fn rect_contains(value: Rect, x: i64, y: i64) i32 { + if (value.width <= 0 || value.height <= 0) { return 0; } + if (x < value.x || y < value.y) { return 0; } + if (x >= value.x + value.width || y >= value.y + value.height) { return 0; } + return 1; +} + +fn rect_intersects(a: Rect, b: Rect) i32 { + if (a.width <= 0 || a.height <= 0 || b.width <= 0 || b.height <= 0) { return 0; } + if (a.x + a.width <= b.x || b.x + b.width <= a.x) { return 0; } + if (a.y + a.height <= b.y || b.y + b.height <= a.y) { return 0; } + return 1; +} diff --git a/src/core/reconcile.zag b/src/core/reconcile.zag new file mode 100644 index 0000000..f664eca --- /dev/null +++ b/src/core/reconcile.zag @@ -0,0 +1,122 @@ +@import("std:list") +@import("view_contract.zag") + +struct ViewSpec { + key: NodeKey, + kind: i64, + fingerprint: i64, +} + +struct RenderNode { + key: NodeKey, + kind: i64, + fingerprint: i64, + instance_id: i64, + generation: i64, +} + +struct ReconcileResult { + nodes: ArrayList[RenderNode], + inserted: i32, + reused: i32, + updated: i32, + removed: i32, + duplicate_keys: i32, +} + +fn view_spec(key: NodeKey, kind: i64, fingerprint: i64) ViewSpec { + return ViewSpec{ .key = key, .kind = kind, .fingerprint = fingerprint }; +} + +fn render_node(key: NodeKey, kind: i64, fingerprint: i64, instance_id: i64) RenderNode { + return RenderNode{ + .key = key, + .kind = kind, + .fingerprint = fingerprint, + .instance_id = instance_id, + .generation = 0, + }; +} + +fn reconcile_free(result: *ReconcileResult) void { + free[RenderNode](&result.*.nodes); +} + +fn reconcile_has_earlier_key(next: ArrayList[ViewSpec], index: i32) i32 { + let i: i32 = 0; + while (i < index) { + if (node_key_equal(next.data[i].key, next.data[index].key) == 1) { return 1; } + i = i + 1; + } + return 0; +} + +fn reconcile(previous: ArrayList[RenderNode], next: ArrayList[ViewSpec], next_instance_id: *i64) ReconcileResult { + let output: ArrayList[RenderNode] = make[RenderNode](next.len); + let matched: ArrayList[i32] = make[i32](previous.len); + let i: i32 = 0; + while (i < previous.len) { push[i32](&matched, 0); i = i + 1; } + + let inserted: i32 = 0; + let reused: i32 = 0; + let updated: i32 = 0; + let removed: i32 = 0; + let duplicate_keys: i32 = 0; + + i = 0; + while (i < next.len) { + let spec: ViewSpec = next.data[i]; + if (reconcile_has_earlier_key(next, i) == 1) { + duplicate_keys = duplicate_keys + 1; + i = i + 1; + continue; + } + + let found: i32 = 0 - 1; + let j: i32 = 0; + while (j < previous.len) { + if (matched.data[j] == 0 && node_key_equal(previous.data[j].key, spec.key) == 1) { + found = j; + break; + } + j = j + 1; + } + + if (found >= 0 && previous.data[found].kind == spec.kind) { + let node: RenderNode = previous.data[found]; + matched.data[found] = 1; + reused = reused + 1; + if (node.fingerprint != spec.fingerprint) { + node.fingerprint = spec.fingerprint; + node.generation = node.generation + 1; + updated = updated + 1; + } + push[RenderNode](&output, node); + } else { + if (found >= 0) { + matched.data[found] = 1; + removed = removed + 1; + } + let instance_id: i64 = next_instance_id.*; + next_instance_id.* = next_instance_id.* + 1; + push[RenderNode](&output, render_node(spec.key, spec.kind, spec.fingerprint, instance_id)); + inserted = inserted + 1; + } + i = i + 1; + } + + i = 0; + while (i < previous.len) { + if (matched.data[i] == 0) { removed = removed + 1; } + i = i + 1; + } + free[i32](&matched); + return ReconcileResult{ + .nodes = output, + .inserted = inserted, + .reused = reused, + .updated = updated, + .removed = removed, + .duplicate_keys = duplicate_keys, + }; +} diff --git a/src/core/state.zag b/src/core/state.zag new file mode 100644 index 0000000..3e69af7 --- /dev/null +++ b/src/core/state.zag @@ -0,0 +1,51 @@ +@import("view_contract.zag") + +struct State[T] { + id: i64, + revision: i64, + value: T, +} + +struct Binding[T] { + target: *State[T], + action_id: i64, +} + +fn state_make[T](id: i64, value: T) State[T] { + return State[T]{ .id = id, .revision = 0, .value = value }; +} + +fn state_read[T](state: *State[T], ctx: *ViewContext) T { + view_context_record_read(ctx, state.*.id, state.*.revision); + return state.*.value; +} + +fn state_set[T](state: *State[T], value: T) StateChange { + let from_revision: i64 = state.*.revision; + state.*.value = value; + state.*.revision = state.*.revision + 1; + return StateChange{ + .state_id = state.*.id, + .from_revision = from_revision, + .to_revision = state.*.revision, + }; +} + +fn binding_make[T](state: *State[T], action_id: i64) Binding[T] { + return Binding[T]{ .target = state, .action_id = action_id }; +} + +fn binding_read[T](binding: Binding[T], ctx: *ViewContext) T { + return state_read[T](binding.target, ctx); +} + +fn binding_set[T](binding: Binding[T], value: T, source: NodeKey) Action { + let change: StateChange = state_set[T](binding.target, value); + return Action{ + .id = binding.action_id, + .source = source, + .target_state_id = change.state_id, + .from_revision = change.from_revision, + .to_revision = change.to_revision, + }; +} diff --git a/src/core/view_contract.zag b/src/core/view_contract.zag new file mode 100644 index 0000000..1fd54df --- /dev/null +++ b/src/core/view_contract.zag @@ -0,0 +1,157 @@ +@import("std:list") + +struct NodeKey { + value: i64, + generation: i64, +} + +struct StateRead { + reader: NodeKey, + state_id: i64, + revision: i64, +} + +struct StateChange { + state_id: i64, + from_revision: i64, + to_revision: i64, +} + +struct InvalidationCause { + found: i32, + reader: NodeKey, + state_id: i64, + read_revision: i64, + from_revision: i64, + to_revision: i64, +} + +struct Action { + id: i64, + source: NodeKey, + target_state_id: i64, + from_revision: i64, + to_revision: i64, +} + +struct EnvironmentEntry { + key: i64, + value: i64, + revision: i64, +} + +struct Environment { + entries: ArrayList[EnvironmentEntry], + revision: i64, +} + +struct ViewContext { + reader: NodeKey, + reads: ArrayList[StateRead], +} + +fn node_key(value: i64) NodeKey { + return NodeKey{ .value = value, .generation = 0 }; +} + +fn node_key_scoped(value: i64, generation: i64) NodeKey { + return NodeKey{ .value = value, .generation = generation }; +} + +fn node_key_equal(a: NodeKey, b: NodeKey) i32 { + return (a.value == b.value && a.generation == b.generation) as i32; +} + +fn environment_make() Environment { + return Environment{ .entries = make[EnvironmentEntry](4), .revision = 0 }; +} + +fn environment_free(env: *Environment) void { + free[EnvironmentEntry](&env.*.entries); +} + +fn environment_set(env: *Environment, key: i64, value: i64) i64 { + let i: i32 = 0; + while (i < env.*.entries.len) { + if (env.*.entries.data[i].key == key) { + if (env.*.entries.data[i].value != value) { + env.*.revision = env.*.revision + 1; + env.*.entries.data[i].value = value; + env.*.entries.data[i].revision = env.*.revision; + } + return env.*.entries.data[i].revision; + } + i = i + 1; + } + env.*.revision = env.*.revision + 1; + push[EnvironmentEntry](&env.*.entries, EnvironmentEntry{ + .key = key, + .value = value, + .revision = env.*.revision, + }); + return env.*.revision; +} + +fn environment_get(env: Environment, key: i64, fallback: i64) i64 { + let i: i32 = env.entries.len - 1; + while (i >= 0) { + if (env.entries.data[i].key == key) { return env.entries.data[i].value; } + i = i - 1; + } + return fallback; +} + +fn view_context_make(reader: NodeKey) ViewContext { + return ViewContext{ .reader = reader, .reads = make[StateRead](4) }; +} + +fn view_context_free(ctx: *ViewContext) void { + free[StateRead](&ctx.*.reads); +} + +fn view_context_record_read(ctx: *ViewContext, state_id: i64, revision: i64) void { + let i: i32 = 0; + while (i < ctx.*.reads.len) { + if (ctx.*.reads.data[i].state_id == state_id && + node_key_equal(ctx.*.reads.data[i].reader, ctx.*.reader) == 1) { + ctx.*.reads.data[i].revision = revision; + return; + } + i = i + 1; + } + push[StateRead](&ctx.*.reads, StateRead{ + .reader = ctx.*.reader, + .state_id = state_id, + .revision = revision, + }); +} + +fn view_context_invalidation(ctx: ViewContext, change: StateChange) InvalidationCause { + let i: i32 = 0; + while (i < ctx.reads.len) { + let read: StateRead = ctx.reads.data[i]; + if (read.state_id == change.state_id && read.revision < change.to_revision) { + return InvalidationCause{ + .found = 1, + .reader = read.reader, + .state_id = read.state_id, + .read_revision = read.revision, + .from_revision = change.from_revision, + .to_revision = change.to_revision, + }; + } + i = i + 1; + } + return InvalidationCause{ + .found = 0, + .reader = ctx.reader, + .state_id = change.state_id, + .read_revision = 0, + .from_revision = change.from_revision, + .to_revision = change.to_revision, + }; +} + +fn view_context_invalidated(ctx: ViewContext, change: StateChange) i32 { + return view_context_invalidation(ctx, change).found; +} diff --git a/src/core/virtual_lifecycle.zag b/src/core/virtual_lifecycle.zag new file mode 100644 index 0000000..c7ee5ee --- /dev/null +++ b/src/core/virtual_lifecycle.zag @@ -0,0 +1,264 @@ +@import("std:list") + +enum VirtualLifecycleError { + none, + invalid_capacity, + too_many_live_nodes, + invalid_id, + duplicate_id, + unknown_focus, + no_recyclable_node, +} + +struct VirtualRetainedEntry { + logical_id: i64, + instance_id: i64, + generation: i64, + live: i32, + focused: i32, + last_seen_revision: i64, +} + +struct VirtualRetainedStore { + entries: ArrayList[VirtualRetainedEntry], + capacity: i32, + next_instance_id: i64, + revision: i64, + focused_id: i64, +} + +struct VirtualLifecycleResult { + inserted: i32, + reused: i32, + rebound: i32, + evicted: i32, + live: i32, + resident: i32, + focus_restored: i32, + lifecycle_hash: i64, + error: VirtualLifecycleError, + error_index: i32, +} + +fn virtual_store_make(capacity: i32) VirtualRetainedStore { + let safe_capacity: i32 = capacity; + if (safe_capacity < 1 || safe_capacity > 4096) { safe_capacity = 1; } + return VirtualRetainedStore{ + .entries = make[VirtualRetainedEntry](safe_capacity), + .capacity = capacity, + .next_instance_id = 1, + .revision = 0, + .focused_id = 0, + }; +} + +fn virtual_store_free(store: *VirtualRetainedStore) void { + free[VirtualRetainedEntry](&store.*.entries); +} + +fn virtual_lifecycle_mix(hash: i64, value: i64) i64 { + return (hash * 1099511628211) ^ value; +} + +fn virtual_store_find(store: VirtualRetainedStore, logical_id: i64) i32 { + let i: i32 = 0; + while (i < store.entries.len) { + if (store.entries.data[i].logical_id == logical_id) { return i; } + i = i + 1; + } + return 0 - 1; +} + +fn virtual_ids_contains(ids: ArrayList[i64], logical_id: i64) i32 { + let i: i32 = 0; + while (i < ids.len) { + if (ids.data[i] == logical_id) { return 1; } + i = i + 1; + } + return 0; +} + +fn virtual_store_hash(store: VirtualRetainedStore) i64 { + let hash: i64 = virtual_lifecycle_mix(virtual_lifecycle_mix(17, store.capacity as i64), store.revision); + hash = virtual_lifecycle_mix(hash, store.next_instance_id); + hash = virtual_lifecycle_mix(hash, store.focused_id); + hash = virtual_lifecycle_mix(hash, store.entries.len as i64); + let i: i32 = 0; + while (i < store.entries.len) { + let entry: VirtualRetainedEntry = store.entries.data[i]; + hash = virtual_lifecycle_mix(hash, entry.logical_id); + hash = virtual_lifecycle_mix(hash, entry.instance_id); + hash = virtual_lifecycle_mix(hash, entry.generation); + hash = virtual_lifecycle_mix(hash, entry.live as i64); + hash = virtual_lifecycle_mix(hash, entry.focused as i64); + hash = virtual_lifecycle_mix(hash, entry.last_seen_revision); + i = i + 1; + } + return hash; +} + +fn virtual_lifecycle_error(store: VirtualRetainedStore, error: VirtualLifecycleError, + index: i32) VirtualLifecycleResult { + return VirtualLifecycleResult{ + .inserted = 0, .reused = 0, .rebound = 0, .evicted = 0, + .live = 0, .resident = store.entries.len, .focus_restored = 0, + .lifecycle_hash = virtual_lifecycle_mix(virtual_store_hash(store), error as i64), + .error = error, .error_index = index, + }; +} + +fn virtual_store_focus(store: *VirtualRetainedStore, logical_id: i64) VirtualLifecycleError { + if (logical_id == 0) { + if (store.*.focused_id == 0) { return VirtualLifecycleError.none; } + let old_index: i32 = virtual_store_find(store.*, store.*.focused_id); + if (old_index >= 0) { store.*.entries.data[old_index].focused = 0; } + store.*.focused_id = 0; + store.*.revision = store.*.revision + 1; + return VirtualLifecycleError.none; + } + let index: i32 = virtual_store_find(store.*, logical_id); + if (index < 0) { return VirtualLifecycleError.unknown_focus; } + if (store.*.focused_id == logical_id) { return VirtualLifecycleError.none; } + let old_index: i32 = virtual_store_find(store.*, store.*.focused_id); + if (old_index >= 0) { store.*.entries.data[old_index].focused = 0; } + store.*.focused_id = logical_id; + store.*.entries.data[index].focused = 1; + store.*.revision = store.*.revision + 1; + return VirtualLifecycleError.none; +} + +fn virtual_store_reconcile(store: *VirtualRetainedStore, + live_ids: ArrayList[i64]) VirtualLifecycleResult { + if (store.*.capacity < 1 || store.*.capacity > 4096) { + return virtual_lifecycle_error(store.*, VirtualLifecycleError.invalid_capacity, 0 - 1); + } + if (live_ids.len > store.*.capacity) { + return virtual_lifecycle_error(store.*, VirtualLifecycleError.too_many_live_nodes, 0 - 1); + } + if (store.*.focused_id != 0 && virtual_ids_contains(live_ids, store.*.focused_id) == 0 && + live_ids.len >= store.*.capacity) { + return virtual_lifecycle_error(store.*, VirtualLifecycleError.too_many_live_nodes, 0 - 1); + } + let i: i32 = 0; + while (i < live_ids.len) { + if (live_ids.data[i] <= 0) { + return virtual_lifecycle_error(store.*, VirtualLifecycleError.invalid_id, i); + } + let j: i32 = 0; + while (j < i) { + if (live_ids.data[j] == live_ids.data[i]) { + return virtual_lifecycle_error(store.*, VirtualLifecycleError.duplicate_id, i); + } + j = j + 1; + } + i = i + 1; + } + + let current_live: i32 = 0; + let identical_membership: i32 = 1; + i = 0; + while (i < store.*.entries.len) { + if (store.*.entries.data[i].live != 0) { + current_live = current_live + 1; + if (virtual_ids_contains(live_ids, store.*.entries.data[i].logical_id) == 0) { + identical_membership = 0; + } + } + i = i + 1; + } + if (current_live != live_ids.len) { identical_membership = 0; } + if (identical_membership != 0) { + return VirtualLifecycleResult{ + .inserted = 0, .reused = live_ids.len, .rebound = 0, .evicted = 0, + .live = live_ids.len, .resident = store.*.entries.len, + .focus_restored = 0, .lifecycle_hash = virtual_store_hash(store.*), + .error = VirtualLifecycleError.none, .error_index = 0 - 1, + }; + } + + let was_live: ArrayList[i32] = make[i32](store.*.entries.len); + let evicted: i32 = 0; + i = 0; + while (i < store.*.entries.len) { + push[i32](&was_live, store.*.entries.data[i].live); + if (store.*.entries.data[i].live != 0 && + virtual_ids_contains(live_ids, store.*.entries.data[i].logical_id) == 0) { + evicted = evicted + 1; + } + store.*.entries.data[i].live = 0; + i = i + 1; + } + + let inserted: i32 = 0; + let reused: i32 = 0; + let rebound: i32 = 0; + let focus_restored: i32 = 0; + let next_revision: i64 = store.*.revision + 1; + i = 0; + while (i < live_ids.len) { + let logical_id: i64 = live_ids.data[i]; + let found: i32 = virtual_store_find(store.*, logical_id); + if (found >= 0) { + if (store.*.entries.data[found].focused != 0 && was_live.data[found] == 0) { + focus_restored = focus_restored + 1; + } + store.*.entries.data[found].live = 1; + store.*.entries.data[found].last_seen_revision = next_revision; + reused = reused + 1; + } else if (store.*.entries.len < store.*.capacity) { + let instance_id: i64 = store.*.next_instance_id; + store.*.next_instance_id = store.*.next_instance_id + 1; + push[VirtualRetainedEntry](&store.*.entries, VirtualRetainedEntry{ + .logical_id = logical_id, .instance_id = instance_id, .generation = 0, + .live = 1, .focused = 0, .last_seen_revision = next_revision, + }); + inserted = inserted + 1; + } else { + let candidate: i32 = 0 - 1; + let oldest: i64 = 0; + let j: i32 = 0; + while (j < store.*.entries.len) { + let entry: VirtualRetainedEntry = store.*.entries.data[j]; + if (entry.live == 0 && entry.focused == 0 && + virtual_ids_contains(live_ids, entry.logical_id) == 0 && + (candidate < 0 || entry.last_seen_revision < oldest)) { + candidate = j; + oldest = entry.last_seen_revision; + } + j = j + 1; + } + if (candidate < 0) { + i = 0; + while (i < store.*.entries.len) { + store.*.entries.data[i].live = was_live.data[i]; + i = i + 1; + } + free[i32](&was_live); + return virtual_lifecycle_error(store.*, VirtualLifecycleError.no_recyclable_node, i); + } + store.*.entries.data[candidate].logical_id = logical_id; + store.*.entries.data[candidate].generation = store.*.entries.data[candidate].generation + 1; + store.*.entries.data[candidate].live = 1; + store.*.entries.data[candidate].last_seen_revision = next_revision; + rebound = rebound + 1; + } + i = i + 1; + } + + let membership_changed: i32 = (inserted > 0 || rebound > 0 || evicted > 0) as i32; + i = 0; + while (i < was_live.len) { + if (was_live.data[i] == 0 && store.*.entries.data[i].live != 0) { + membership_changed = 1; + } + i = i + 1; + } + if (membership_changed != 0) { store.*.revision = next_revision; } + free[i32](&was_live); + return VirtualLifecycleResult{ + .inserted = inserted, .reused = reused, .rebound = rebound, .evicted = evicted, + .live = live_ids.len, .resident = store.*.entries.len, + .focus_restored = focus_restored, .lifecycle_hash = virtual_store_hash(store.*), + .error = VirtualLifecycleError.none, .error_index = 0 - 1, + }; +} diff --git a/src/input/hit_test.zag b/src/input/hit_test.zag new file mode 100644 index 0000000..ca8a346 --- /dev/null +++ b/src/input/hit_test.zag @@ -0,0 +1,183 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("../core/view_contract.zag") +@import("../render/display_list.zag") + +enum HitTreeError { + none, + duplicate_id, + missing_parent, + invalid_geometry, + invalid_transform, + singular_transform, +} + +struct HitNode { + id: NodeKey, + parent: NodeKey, + bounds: Rect, + clip: Rect, + has_clip: i32, + transform: Transform, + z_order: i64, + enabled: i32, + focusable: i32, +} + +struct HitTree { + nodes: ArrayList[HitNode], + revision: i64, + last_error: HitTreeError, + error_node: NodeKey, +} + +struct HitResult { + found: i32, + id: NodeKey, + node_index: i32, + local_x: i64, + local_y: i64, + z_order: i64, +} + +fn hit_root_key() NodeKey { return node_key(0 - 1); } + +fn hit_node(id: NodeKey, parent: NodeKey, bounds: Rect) HitNode { + return HitNode{ + .id = id, + .parent = parent, + .bounds = bounds, + .clip = bounds, + .has_clip = 0, + .transform = transform_identity(), + .z_order = 0, + .enabled = 1, + .focusable = 0, + }; +} + +fn hit_tree_make() HitTree { + return HitTree{ + .nodes = make[HitNode](16), + .revision = 0, + .last_error = HitTreeError.none, + .error_node = hit_root_key(), + }; +} + +fn hit_tree_free(tree: *HitTree) void { + free[HitNode](&tree.*.nodes); +} + +fn hit_tree_find_index(tree: HitTree, id: NodeKey) i32 { + let i: i32 = tree.nodes.len - 1; + while (i >= 0) { + if (node_key_equal(tree.nodes.data[i].id, id) == 1) { return i; } + i = i - 1; + } + return 0 - 1; +} + +fn hit_transform_values_valid(transform: Transform) i32 { + let limit: i64 = 4096 * unit_scale(); + if (transform.scale_x < 0 - limit || transform.scale_x > limit || + transform.scale_y < 0 - limit || transform.scale_y > limit || + transform.skew_x < 0 - limit || transform.skew_x > limit || + transform.skew_y < 0 - limit || transform.skew_y > limit || + transform.translate_x < -1000000000 || transform.translate_x > 1000000000 || + transform.translate_y < -1000000000 || transform.translate_y > 1000000000) { return 0; } + return 1; +} + +fn hit_geometry_valid(value: Rect) i32 { + let limit: i64 = 1000000000; + return (value.x >= 0 - limit && value.x <= limit && + value.y >= 0 - limit && value.y <= limit && + value.width > 0 && value.width <= limit && + value.height > 0 && value.height <= limit) as i32; +} + +fn hit_tree_fail(tree: *HitTree, error: HitTreeError, id: NodeKey) HitTreeError { + tree.*.last_error = error; + tree.*.error_node = id; + return error; +} + +fn hit_tree_add(tree: *HitTree, node: HitNode) HitTreeError { + if (hit_tree_find_index(tree.*, node.id) >= 0) { + return hit_tree_fail(tree, HitTreeError.duplicate_id, node.id); + } + if (node_key_equal(node.parent, hit_root_key()) == 0 && + hit_tree_find_index(tree.*, node.parent) < 0) { + return hit_tree_fail(tree, HitTreeError.missing_parent, node.id); + } + if (hit_geometry_valid(node.bounds) == 0 || + (node.has_clip != 0 && hit_geometry_valid(node.clip) == 0)) { + return hit_tree_fail(tree, HitTreeError.invalid_geometry, node.id); + } + if (hit_transform_values_valid(node.transform) == 0) { + return hit_tree_fail(tree, HitTreeError.invalid_transform, node.id); + } + let determinant: i64 = node.transform.scale_x * node.transform.scale_y - + node.transform.skew_x * node.transform.skew_y; + if (determinant == 0) { + return hit_tree_fail(tree, HitTreeError.singular_transform, node.id); + } + push[HitNode](&tree.*.nodes, node); + tree.*.revision = tree.*.revision + 1; + tree.*.last_error = HitTreeError.none; + tree.*.error_node = hit_root_key(); + return HitTreeError.none; +} + +fn hit_empty_result() HitResult { + return HitResult{ + .found = 0, + .id = hit_root_key(), + .node_index = 0 - 1, + .local_x = 0, + .local_y = 0, + .z_order = 0, + }; +} + +fn hit_node_local(node: HitNode, world_x: i64, world_y: i64) HitResult { + if (world_x < -1000000000 || world_x > 1000000000 || + world_y < -1000000000 || world_y > 1000000000) { return hit_empty_result(); } + let dx: i64 = world_x - node.transform.translate_x; + let dy: i64 = world_y - node.transform.translate_y; + let determinant: i64 = node.transform.scale_x * node.transform.scale_y - + node.transform.skew_x * node.transform.skew_y; + if (determinant == 0) { return hit_empty_result(); } + let local_x: i64 = ((dx * node.transform.scale_y - dy * node.transform.skew_x) * unit_scale()) / determinant; + let local_y: i64 = ((dy * node.transform.scale_x - dx * node.transform.skew_y) * unit_scale()) / determinant; + return HitResult{ + .found = 1, + .id = node.id, + .node_index = 0 - 1, + .local_x = local_x, + .local_y = local_y, + .z_order = node.z_order, + }; +} + +fn hit_test(tree: HitTree, world_x: i64, world_y: i64) HitResult { + if (world_x < -1000000000 || world_x > 1000000000 || + world_y < -1000000000 || world_y > 1000000000) { return hit_empty_result(); } + let best: HitResult = hit_empty_result(); + let i: i32 = tree.nodes.len - 1; + while (i >= 0) { + let node: HitNode = tree.nodes.data[i]; + if (node.enabled != 0) { + let candidate: HitResult = hit_node_local(node, world_x, world_y); + if (candidate.found != 0 && rect_contains(node.bounds, candidate.local_x, candidate.local_y) != 0 && + (node.has_clip == 0 || rect_contains(node.clip, candidate.local_x, candidate.local_y) != 0) && + (best.found == 0 || node.z_order > best.z_order)) { + best = candidate; + best.node_index = i; + } + } + i = i - 1; + } + return best; +} diff --git a/src/input/router.zag b/src/input/router.zag new file mode 100644 index 0000000..9384882 --- /dev/null +++ b/src/input/router.zag @@ -0,0 +1,182 @@ +@import("std:list") +@import("hit_test.zag") + +enum PointerPhase { down, move, up, cancel, wheel } +enum RouteStatus { accepted, invalid_pointer } + +struct PointerEvent { + pointer_id: i64, + phase: PointerPhase, + x: i64, + y: i64, + buttons: i64, +} + +struct PointerCapture { + pointer_id: i64, + target: NodeKey, +} + +struct RoutedEvent { + sequence: i64, + pointer_id: i64, + phase: PointerPhase, + target: NodeKey, + local_x: i64, + local_y: i64, + captured: i32, + status: RouteStatus, +} + +struct RouteResult { + status: RouteStatus, + hit: HitResult, + sequence: i64, + captured: i32, +} + +struct InputRouter { + captures: ArrayList[PointerCapture], + active_pointers: ArrayList[i64], + events: ArrayList[RoutedEvent], + focus: NodeKey, + next_sequence: i64, +} + +fn pointer_event(pointer_id: i64, phase: PointerPhase, x: i64, y: i64) PointerEvent { + return PointerEvent{ .pointer_id = pointer_id, .phase = phase, .x = x, .y = y, .buttons = 0 }; +} + +fn input_router_make() InputRouter { + return InputRouter{ + .captures = make[PointerCapture](4), + .active_pointers = make[i64](4), + .events = make[RoutedEvent](32), + .focus = hit_root_key(), + .next_sequence = 1, + }; +} + +fn input_router_free(router: *InputRouter) void { + free[PointerCapture](&router.*.captures); + free[i64](&router.*.active_pointers); + free[RoutedEvent](&router.*.events); +} + +fn input_active_index(router: InputRouter, pointer_id: i64) i32 { + let i: i32 = router.active_pointers.len - 1; + while (i >= 0) { + if (router.active_pointers.data[i] == pointer_id) { return i; } + i = i - 1; + } + return 0 - 1; +} + +fn input_set_active(router: *InputRouter, pointer_id: i64) void { + if (input_active_index(router.*, pointer_id) < 0) { push[i64](&router.*.active_pointers, pointer_id); } +} + +fn input_clear_active(router: *InputRouter, pointer_id: i64) void { + let index: i32 = input_active_index(router.*, pointer_id); + if (index < 0) { return; } + let i: i32 = index; + while (i + 1 < router.*.active_pointers.len) { + router.*.active_pointers.data[i] = router.*.active_pointers.data[i + 1]; + i = i + 1; + } + router.*.active_pointers.len = router.*.active_pointers.len - 1; +} + +fn input_capture_index(router: InputRouter, pointer_id: i64) i32 { + let i: i32 = router.captures.len - 1; + while (i >= 0) { + if (router.captures.data[i].pointer_id == pointer_id) { return i; } + i = i - 1; + } + return 0 - 1; +} + +fn input_release_capture(router: *InputRouter, pointer_id: i64) i32 { + let index: i32 = input_capture_index(router.*, pointer_id); + if (index < 0) { return 0; } + let i: i32 = index; + while (i + 1 < router.*.captures.len) { + router.*.captures.data[i] = router.*.captures.data[i + 1]; + i = i + 1; + } + router.*.captures.len = router.*.captures.len - 1; + return 1; +} + +fn input_request_capture(router: *InputRouter, tree: HitTree, pointer_id: i64, target: NodeKey) i32 { + if (pointer_id < 0 || input_active_index(router.*, pointer_id) < 0) { return 0; } + let node_index: i32 = hit_tree_find_index(tree, target); + if (node_index < 0 || tree.nodes.data[node_index].enabled == 0) { return 0; } + let existing: i32 = input_capture_index(router.*, pointer_id); + if (existing >= 0) { + router.*.captures.data[existing].target = target; + return 1; + } + push[PointerCapture](&router.*.captures, PointerCapture{ .pointer_id = pointer_id, .target = target }); + return 1; +} + +fn input_request_focus(router: *InputRouter, tree: HitTree, target: NodeKey) i32 { + let node_index: i32 = hit_tree_find_index(tree, target); + if (node_index < 0 || tree.nodes.data[node_index].enabled == 0 || + tree.nodes.data[node_index].focusable == 0) { return 0; } + router.*.focus = target; + return 1; +} + +fn input_route_record(router: *InputRouter, event: PointerEvent, status: RouteStatus, hit: HitResult, captured: i32) RouteResult { + let sequence: i64 = router.*.next_sequence; + router.*.next_sequence = router.*.next_sequence + 1; + push[RoutedEvent](&router.*.events, RoutedEvent{ + .sequence = sequence, + .pointer_id = event.pointer_id, + .phase = event.phase, + .target = hit.id, + .local_x = hit.local_x, + .local_y = hit.local_y, + .captured = captured, + .status = status, + }); + return RouteResult{ .status = status, .hit = hit, .sequence = sequence, .captured = captured }; +} + +fn input_route_pointer(router: *InputRouter, tree: HitTree, event: PointerEvent) RouteResult { + if (event.pointer_id < 0 || event.x < -1000000000 || event.x > 1000000000 || + event.y < -1000000000 || event.y > 1000000000) { + return input_route_record(router, event, RouteStatus.invalid_pointer, hit_empty_result(), 0); + } + + if (event.phase == PointerPhase.down) { input_set_active(router, event.pointer_id); } + + let hit: HitResult = hit_empty_result(); + let captured: i32 = 0; + let capture_index: i32 = input_capture_index(router.*, event.pointer_id); + if (capture_index >= 0) { + let target_index: i32 = hit_tree_find_index(tree, router.*.captures.data[capture_index].target); + if (target_index >= 0 && tree.nodes.data[target_index].enabled != 0) { + hit = hit_node_local(tree.nodes.data[target_index], event.x, event.y); + hit.node_index = target_index; + captured = 1; + } else { + _ = input_release_capture(router, event.pointer_id); + } + } + if (captured == 0 && event.phase != PointerPhase.cancel) { + hit = hit_test(tree, event.x, event.y); + } + if (event.phase == PointerPhase.down && hit.found != 0 && + tree.nodes.data[hit.node_index].focusable != 0) { + router.*.focus = hit.id; + } + let result: RouteResult = input_route_record(router, event, RouteStatus.accepted, hit, captured); + if (event.phase == PointerPhase.up || event.phase == PointerPhase.cancel) { + _ = input_release_capture(router, event.pointer_id); + input_clear_active(router, event.pointer_id); + } + return result; +} diff --git a/src/layout/adaptive.zag b/src/layout/adaptive.zag new file mode 100644 index 0000000..b728906 --- /dev/null +++ b/src/layout/adaptive.zag @@ -0,0 +1,116 @@ +@import("flex.zag") + +enum FlexSpacingToken { none, micro, tiny, small, medium, large, xlarge, xxlarge } +enum FlexDensity { compact, standard, touch } +enum FlexBreakpoint { compact, medium, expanded } + +struct FlexAdaptiveContext { + viewport: Size, + safe_area: Insets, + density: FlexDensity, + text_scale: i64, + direction: LayoutDirection, +} + +fn flex_adaptive_context(viewport: Size) FlexAdaptiveContext { + return FlexAdaptiveContext{ + .viewport = viewport, + .safe_area = insets_all(0), + .density = FlexDensity.standard, + .text_scale = unit_scale(), + .direction = LayoutDirection.ltr, + }; +} + +fn flex_spacing_base(token: FlexSpacingToken) i64 { + return switch (token) { + .none => 0, + .micro => 2 * unit_scale(), + .tiny => 4 * unit_scale(), + .small => 8 * unit_scale(), + .medium => 12 * unit_scale(), + .large => 16 * unit_scale(), + .xlarge => 24 * unit_scale(), + .xxlarge => 32 * unit_scale(), + }; +} + +fn flex_spacing(token: FlexSpacingToken, density: FlexDensity) i64 { + let value: i64 = flex_spacing_base(token); + return switch (density) { + .compact => (value * 3) / 4, + .standard => value, + .touch => (value * 5) / 4, + }; +} + +fn flex_safe_extent(value: i64) i64 { + if (value < 0) { return 0; } + if (value > unbounded_extent() / 4) { return unbounded_extent() / 4; } + return value; +} + +fn flex_safe_insets(raw: Insets) Insets { + let value: Insets = raw; + value.top = flex_safe_extent(value.top); + value.right = flex_safe_extent(value.right); + value.bottom = flex_safe_extent(value.bottom); + value.left = flex_safe_extent(value.left); + return value; +} + +fn flex_insets_add(a: Insets, b: Insets) Insets { + return Insets{ + .top = flex_safe_extent(a.top + b.top), + .right = flex_safe_extent(a.right + b.right), + .bottom = flex_safe_extent(a.bottom + b.bottom), + .left = flex_safe_extent(a.left + b.left), + }; +} + +fn flex_content_size(context: FlexAdaptiveContext) Size { + let safe: Insets = flex_safe_insets(context.safe_area); + let viewport_width: i64 = context.viewport.width; + let viewport_height: i64 = context.viewport.height; + if (viewport_width < 0) { viewport_width = 0; } + if (viewport_height < 0) { viewport_height = 0; } + if (viewport_width > unbounded_extent()) { viewport_width = unbounded_extent(); } + if (viewport_height > unbounded_extent()) { viewport_height = unbounded_extent(); } + let width: i64 = viewport_width - safe.left - safe.right; + let height: i64 = viewport_height - safe.top - safe.bottom; + if (width < 0) { width = 0; } + if (height < 0) { height = 0; } + return size(width, height); +} + +fn flex_effective_text_scale(context: FlexAdaptiveContext) i64 { + let scale: i64 = context.text_scale; + if (scale < unit_scale()) { scale = unit_scale(); } + if (scale > unit_scale() * 4) { scale = unit_scale() * 4; } + return scale; +} + +fn flex_breakpoint(context: FlexAdaptiveContext) FlexBreakpoint { + let content: Size = flex_content_size(context); + let effective_width: i64 = (content.width * unit_scale()) / flex_effective_text_scale(context); + if (effective_width < 600 * unit_scale()) { return FlexBreakpoint.compact; } + if (effective_width < 1024 * unit_scale()) { return FlexBreakpoint.medium; } + return FlexBreakpoint.expanded; +} + +fn flex_adaptive_style( + axis: FlexAxis, + gap: FlexSpacingToken, + padding: FlexSpacingToken, + context: FlexAdaptiveContext, + include_safe_area: i32, +) FlexStyle { + let pad: i64 = flex_spacing(padding, context.density); + let insets: Insets = insets_all(pad); + if (include_safe_area != 0) { + insets = flex_insets_add(insets, flex_safe_insets(context.safe_area)); + } + let style: FlexStyle = flex_style(axis, flex_spacing(gap, context.density), insets); + style.direction = context.direction; + return style; +} diff --git a/src/layout/common.zag b/src/layout/common.zag new file mode 100644 index 0000000..1884d06 --- /dev/null +++ b/src/layout/common.zag @@ -0,0 +1,3 @@ +enum LayoutDirection { ltr, rtl } +enum PlacementAlignment { start, center, end, stretch, baseline } +enum ContentAlignment { start, center, end, space_between, space_around, space_evenly, stretch } diff --git a/src/layout/flex.zag b/src/layout/flex.zag new file mode 100644 index 0000000..75ce0c4 --- /dev/null +++ b/src/layout/flex.zag @@ -0,0 +1,369 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("common.zag") +@import("measure.zag") + +enum FlexAxis { row, column } +enum MainAlignment { start, center, end, space_between, space_around, space_evenly } +enum CrossAlignment { start, center, end, stretch, baseline } +enum FlexError { none, invalid_constraints, invalid_style, invalid_item, arithmetic_overflow } +enum FlexPriority { lowest, low, normal, high, required } + +struct FlexItem { + id: i64, + basis: i64, + grow: i64, + shrink: i64, + min_main: i64, + max_main: i64, + cross: i64, + baseline: i64, +} + +struct FlexStyle { + axis: FlexAxis, + direction: LayoutDirection, + main_alignment: MainAlignment, + cross_alignment: CrossAlignment, + gap: i64, + padding: Insets, +} + +struct FlexPlacement { + id: i64, + frame: Rect, + baseline: i64, +} + +struct FlexResult { + placements: ArrayList[FlexPlacement], + container: Size, + overflow: i64, + duplicate_ids: i32, + error: FlexError, + error_index: i32, +} + +fn flex_item(id: i64, basis: i64, cross: i64) FlexItem { + return FlexItem{ + .id = id, + .basis = basis, + .grow = 0, + .shrink = 1, + .min_main = 0, + .max_main = unbounded_extent(), + .cross = cross, + .baseline = cross, + }; +} + +fn flex_item_intrinsic( + id: i64, + metrics: IntrinsicMetrics, + axis: FlexAxis, + priority: FlexPriority, +) FlexItem { + let item: FlexItem = switch (axis) { + .row => flex_item(id, metrics.preferred.width, metrics.preferred.height), + .column => flex_item(id, metrics.preferred.height, metrics.preferred.width), + }; + if (axis == FlexAxis.row) { + item.min_main = metrics.minimum.width; + item.max_main = metrics.maximum.width; + item.baseline = metrics.baseline; + } else { + item.min_main = metrics.minimum.height; + item.max_main = metrics.maximum.height; + item.baseline = metrics.preferred.width; + } + switch (priority) { + .lowest => { item.grow = 0; item.shrink = 8; } + .low => { item.grow = 1; item.shrink = 4; } + .normal => { item.grow = 2; item.shrink = 2; } + .high => { item.grow = 4; item.shrink = 1; } + .required => { item.grow = 0; item.shrink = 0; } + } + return item; +} + +fn flex_style(axis: FlexAxis, gap: i64, padding: Insets) FlexStyle { + let actual_gap: i64 = gap; + if (actual_gap < 0) { actual_gap = 0; } + return FlexStyle{ + .axis = axis, + .direction = LayoutDirection.ltr, + .main_alignment = MainAlignment.start, + .cross_alignment = CrossAlignment.start, + .gap = actual_gap, + .padding = padding, + }; +} + +fn flex_free(result: *FlexResult) void { + free[FlexPlacement](&result.*.placements); +} + +fn flex_error_result(error: FlexError, error_index: i32) FlexResult { + return FlexResult{ + .placements = make[FlexPlacement](1), + .container = size(0, 0), + .overflow = 0, + .duplicate_ids = 0, + .error = error, + .error_index = error_index, + }; +} + +fn flex_checked_add(a: i64, b: i64) i64 { + if (a < 0 || b < 0 || a > unbounded_extent() - b) { return 0 - 1; } + return a + b; +} + +fn flex_item_valid(item: FlexItem) i32 { + return (item.id > 0 && item.basis >= 0 && item.basis <= unbounded_extent() && + item.grow >= 0 && item.grow <= 4096 && item.shrink >= 0 && item.shrink <= 4096 && + item.min_main >= 0 && item.max_main >= item.min_main && item.max_main <= unbounded_extent() && + item.cross >= 0 && item.cross <= unbounded_extent() / 4 && + item.baseline >= 0 && item.baseline <= item.cross) as i32; +} + +fn flex_main_min(c: Constraints, axis: FlexAxis) i64 { + return switch (axis) { .row => c.min_width, .column => c.min_height }; +} + +fn flex_main_max(c: Constraints, axis: FlexAxis) i64 { + return switch (axis) { .row => c.max_width, .column => c.max_height }; +} + +fn flex_cross_min(c: Constraints, axis: FlexAxis) i64 { + return switch (axis) { .row => c.min_height, .column => c.min_width }; +} + +fn flex_cross_max(c: Constraints, axis: FlexAxis) i64 { + return switch (axis) { .row => c.max_height, .column => c.max_width }; +} + +fn flex_main_padding(p: Insets, axis: FlexAxis) i64 { + return switch (axis) { .row => p.left + p.right, .column => p.top + p.bottom }; +} + +fn flex_cross_padding(p: Insets, axis: FlexAxis) i64 { + return switch (axis) { .row => p.top + p.bottom, .column => p.left + p.right }; +} + +fn flex_has_duplicate_id(items: ArrayList[FlexItem]) i32 { + let i: i32 = 0; + while (i < items.len) { + let j: i32 = i + 1; + while (j < items.len) { + if (items.data[i].id == items.data[j].id) { return 1; } + j = j + 1; + } + i = i + 1; + } + return 0; +} + +fn flex_item_main(item: FlexItem) i64 { + return clamp_i64(item.basis, item.min_main, item.max_main); +} + +fn flex_layout(items: ArrayList[FlexItem], raw_constraints: Constraints, raw_style: FlexStyle) FlexResult { + let constraints: Constraints = normalize_constraints(raw_constraints); + let style: FlexStyle = raw_style; + if (constraints.min_width > unbounded_extent() || constraints.max_width > unbounded_extent() || + constraints.min_height > unbounded_extent() || constraints.max_height > unbounded_extent()) { + return flex_error_result(FlexError.invalid_constraints, 0 - 1); + } + if (style.gap > unbounded_extent() / 4 || style.padding.top > unbounded_extent() / 4 || + style.padding.right > unbounded_extent() / 4 || style.padding.bottom > unbounded_extent() / 4 || + style.padding.left > unbounded_extent() / 4) { + return flex_error_result(FlexError.invalid_style, 0 - 1); + } + let invalid_index: i32 = 0; + while (invalid_index < items.len && flex_item_valid(items.data[invalid_index]) != 0) { + invalid_index = invalid_index + 1; + } + if (invalid_index < items.len) { return flex_error_result(FlexError.invalid_item, invalid_index); } + if (style.gap < 0) { style.gap = 0; } + if (style.padding.top < 0) { style.padding.top = 0; } + if (style.padding.right < 0) { style.padding.right = 0; } + if (style.padding.bottom < 0) { style.padding.bottom = 0; } + if (style.padding.left < 0) { style.padding.left = 0; } + + let main_padding: i64 = flex_main_padding(style.padding, style.axis); + let cross_padding: i64 = flex_cross_padding(style.padding, style.axis); + let gap_total: i64 = 0; + let gap_index: i32 = 1; + while (gap_index < items.len) { + gap_total = flex_checked_add(gap_total, style.gap); + if (gap_total < 0) { return flex_error_result(FlexError.arithmetic_overflow, gap_index); } + gap_index = gap_index + 1; + } + + let base_total: i64 = flex_checked_add(main_padding, gap_total); + if (base_total < 0) { return flex_error_result(FlexError.arithmetic_overflow, 0 - 1); } + let desired_cross: i64 = cross_padding; + let grow_total: i64 = 0; + let shrink_total: i64 = 0; + let i: i32 = 0; + while (i < items.len) { + base_total = flex_checked_add(base_total, flex_item_main(items.data[i])); + if (base_total < 0) { return flex_error_result(FlexError.arithmetic_overflow, i); } + if (items.data[i].grow > 0) { grow_total = grow_total + items.data[i].grow; } + if (items.data[i].shrink > 0) { shrink_total = shrink_total + items.data[i].shrink; } + let item_cross: i64 = items.data[i].cross; + if (item_cross > desired_cross - cross_padding) { desired_cross = cross_padding + item_cross; } + i = i + 1; + } + + let main_limit: i64 = flex_main_max(constraints, style.axis); + let container_main: i64 = base_total; + if (main_limit < unbounded_extent()) { container_main = main_limit; } + container_main = clamp_i64(container_main, flex_main_min(constraints, style.axis), main_limit); + let container_cross: i64 = clamp_i64(desired_cross, flex_cross_min(constraints, style.axis), flex_cross_max(constraints, style.axis)); + + let main_sizes: ArrayList[i64] = make[i64](items.len); + i = 0; + while (i < items.len) { push[i64](&main_sizes, flex_item_main(items.data[i])); i = i + 1; } + + let delta: i64 = container_main - base_total; + if (delta > 0 && grow_total > 0) { + let distributed: i64 = 0; + let last_grow: i32 = 0 - 1; + i = 0; + while (i < items.len) { if (items.data[i].grow > 0) { last_grow = i; } i = i + 1; } + i = 0; + while (i < items.len) { + if (items.data[i].grow > 0) { + let share: i64 = 0; + if (i == last_grow) { share = delta - distributed; } + else { share = (delta * items.data[i].grow) / grow_total; } + let old_size: i64 = main_sizes.data[i]; + main_sizes.data[i] = clamp_i64(old_size + share, items.data[i].min_main, items.data[i].max_main); + distributed = distributed + (main_sizes.data[i] - old_size); + } + i = i + 1; + } + } + if (delta < 0 && shrink_total > 0) { + let need: i64 = 0 - delta; + let removed: i64 = 0; + let last_shrink: i32 = 0 - 1; + i = 0; + while (i < items.len) { if (items.data[i].shrink > 0) { last_shrink = i; } i = i + 1; } + i = 0; + while (i < items.len) { + if (items.data[i].shrink > 0) { + let share: i64 = 0; + if (i == last_shrink) { share = need - removed; } + else { share = (need * items.data[i].shrink) / shrink_total; } + let old_size: i64 = main_sizes.data[i]; + main_sizes.data[i] = clamp_i64(old_size - share, items.data[i].min_main, items.data[i].max_main); + removed = removed + (old_size - main_sizes.data[i]); + } + i = i + 1; + } + } + + let used_main: i64 = flex_checked_add(main_padding, gap_total); + if (used_main < 0) { + free[i64](&main_sizes); + return flex_error_result(FlexError.arithmetic_overflow, 0 - 1); + } + i = 0; + while (i < main_sizes.len) { + used_main = flex_checked_add(used_main, main_sizes.data[i]); + if (used_main < 0) { + free[i64](&main_sizes); + return flex_error_result(FlexError.arithmetic_overflow, i); + } + i = i + 1; + } + let remaining: i64 = container_main - used_main; + let positive_remaining: i64 = remaining; + if (positive_remaining < 0) { positive_remaining = 0; } + let leading: i64 = 0; + let extra_gap: i64 = 0; + switch (style.main_alignment) { + .start => { } + .center => { leading = positive_remaining / 2; } + .end => { leading = positive_remaining; } + .space_between => { if (items.len > 1) { extra_gap = positive_remaining / ((items.len - 1) as i64); } } + .space_around => { + if (items.len > 0) { extra_gap = positive_remaining / (items.len as i64); leading = extra_gap / 2; } + } + .space_evenly => { + if (items.len > 0) { extra_gap = positive_remaining / ((items.len + 1) as i64); leading = extra_gap; } + } + } + + let max_baseline: i64 = 0; + if (style.axis == FlexAxis.row && style.cross_alignment == CrossAlignment.baseline) { + i = 0; + while (i < items.len) { + let baseline: i64 = clamp_i64(items.data[i].baseline, 0, items.data[i].cross); + if (baseline > max_baseline) { max_baseline = baseline; } + i = i + 1; + } + } + + let placements: ArrayList[FlexPlacement] = make[FlexPlacement](items.len); + let cursor: i64 = leading; + i = 0; + while (i < items.len) { + let item: FlexItem = items.data[i]; + let main_size: i64 = main_sizes.data[i]; + let inner_cross: i64 = container_cross - cross_padding; + let cross_size: i64 = clamp_i64(item.cross, 0, inner_cross); + if (style.cross_alignment == CrossAlignment.stretch) { cross_size = inner_cross; } + let cross_offset: i64 = 0; + switch (style.cross_alignment) { + .start => { } + .center => { cross_offset = (inner_cross - cross_size) / 2; } + .end => { cross_offset = inner_cross - cross_size; } + .stretch => { } + .baseline => { + if (style.axis == FlexAxis.row) { + cross_offset = max_baseline - clamp_i64(item.baseline, 0, cross_size); + } + } + } + + let frame: Rect = rect(0, 0, 0, 0); + if (style.axis == FlexAxis.row) { + frame.width = main_size; + frame.height = cross_size; + frame.y = style.padding.top + cross_offset; + if (style.direction == LayoutDirection.ltr) { frame.x = style.padding.left + cursor; } + else { frame.x = container_main - style.padding.right - cursor - main_size; } + } else { + frame.width = cross_size; + frame.height = main_size; + frame.y = style.padding.top + cursor; + let logical_cross: i64 = cross_offset; + if (style.direction == LayoutDirection.ltr) { frame.x = style.padding.left + logical_cross; } + else { frame.x = container_cross - style.padding.right - logical_cross - cross_size; } + } + push[FlexPlacement](&placements, FlexPlacement{ .id = item.id, .frame = frame, .baseline = frame.y + clamp_i64(item.baseline, 0, frame.height) }); + cursor = cursor + main_size; + if (i + 1 < items.len) { cursor = cursor + style.gap + extra_gap; } + i = i + 1; + } + + free[i64](&main_sizes); + let output_size: Size = switch (style.axis) { + .row => Size{ .width = container_main, .height = container_cross }, + .column => Size{ .width = container_cross, .height = container_main }, + }; + let overflow: i64 = used_main - container_main; + if (overflow < 0) { overflow = 0; } + return FlexResult{ + .placements = placements, + .container = output_size, + .overflow = overflow, + .duplicate_ids = flex_has_duplicate_id(items), + .error = FlexError.none, + .error_index = 0 - 1, + }; +} diff --git a/src/layout/flex_wrap.zag b/src/layout/flex_wrap.zag new file mode 100644 index 0000000..55fc9cb --- /dev/null +++ b/src/layout/flex_wrap.zag @@ -0,0 +1,387 @@ +@import("std:list") +@import("adaptive.zag") + +enum FlexLineAlignment { start, center, end, space_between, space_around, space_evenly, stretch } +enum FlexWrapError { none, too_many_items, invalid_item, invalid_style, arithmetic_overflow } + +struct FlexWrapStyle { + flex: FlexStyle, + line_gap: i64, + line_alignment: FlexLineAlignment, + wrap: i32, +} + +struct FlexLine { + start: i32, + count: i32, + cross_extent: i64, + cross_offset: i64, +} + +struct FlexWrapResult { + placements: ArrayList[FlexPlacement], + lines: ArrayList[FlexLine], + container: Size, + overflow_main: i64, + overflow_cross: i64, + duplicate_ids: i32, + wrapped: i32, + layout_hash: i64, + error: FlexWrapError, + error_index: i32, +} + +fn flex_wrap_style(style: FlexStyle, line_gap: i64) FlexWrapStyle { + let actual_gap: i64 = line_gap; + if (actual_gap < 0) { actual_gap = 0; } + return FlexWrapStyle{ + .flex = style, + .line_gap = actual_gap, + .line_alignment = FlexLineAlignment.start, + .wrap = 1, + }; +} + +fn flex_wrap_free(result: *FlexWrapResult) void { + free[FlexPlacement](&result.*.placements); + free[FlexLine](&result.*.lines); +} + +fn flex_wrap_empty_result(error: FlexWrapError, error_index: i32) FlexWrapResult { + let result: FlexWrapResult = FlexWrapResult{ + .placements = make[FlexPlacement](1), + .lines = make[FlexLine](1), + .container = size(0, 0), + .overflow_main = 0, + .overflow_cross = 0, + .duplicate_ids = 0, + .wrapped = 0, + .layout_hash = 0, + .error = error, + .error_index = error_index, + }; + result.layout_hash = flex_wrap_hash(result); + return result; +} + +fn flex_wrap_checked_add(a: i64, b: i64) i64 { + if (a < 0 || b < 0 || a > unbounded_extent() - b) { return 0 - 1; } + return a + b; +} + +fn flex_wrap_item_valid(item: FlexItem) i32 { + return (item.id > 0 && item.basis >= 0 && item.basis <= unbounded_extent() && + item.min_main >= 0 && item.max_main >= item.min_main && item.max_main <= unbounded_extent() && + item.grow >= 0 && item.grow <= 4096 && item.shrink >= 0 && item.shrink <= 4096 && + item.cross >= 0 && item.cross <= unbounded_extent() / 4 && + item.baseline >= 0 && item.baseline <= item.cross) as i32; +} + +fn flex_wrap_mix(hash: i64, value: i64) i64 { + return (hash * 1099511628211) ^ value; +} + +fn flex_wrap_validate(items: ArrayList[FlexItem], constraints: Constraints, style: FlexWrapStyle) FlexWrapError { + if (items.len > 1000000) { return FlexWrapError.too_many_items; } + if (constraints.min_width > unbounded_extent() || constraints.max_width > unbounded_extent() || + constraints.min_height > unbounded_extent() || constraints.max_height > unbounded_extent() || + style.flex.gap > unbounded_extent() / 4 || style.line_gap > unbounded_extent() / 4 || + style.flex.padding.top > unbounded_extent() / 4 || + style.flex.padding.right > unbounded_extent() / 4 || + style.flex.padding.bottom > unbounded_extent() / 4 || + style.flex.padding.left > unbounded_extent() / 4) { + return FlexWrapError.invalid_style; + } + let i: i32 = 0; + while (i < items.len) { + if (flex_wrap_item_valid(items.data[i]) == 0) { return FlexWrapError.invalid_item; } + i = i + 1; + } + return FlexWrapError.none; +} + +fn flex_line_cross_extent(items: ArrayList[FlexItem], start: i32, count: i32, style: FlexStyle) i64 { + let extent: i64 = 0; + let max_baseline: i64 = 0; + let max_descent: i64 = 0; + let i: i32 = start; + while (i < start + count) { + let cross: i64 = items.data[i].cross; + if (cross > extent) { extent = cross; } + if (style.axis == FlexAxis.row && style.cross_alignment == CrossAlignment.baseline) { + let baseline: i64 = clamp_i64(items.data[i].baseline, 0, cross); + let descent: i64 = cross - baseline; + if (baseline > max_baseline) { max_baseline = baseline; } + if (descent > max_descent) { max_descent = descent; } + } + i = i + 1; + } + if (max_baseline + max_descent > extent) { extent = max_baseline + max_descent; } + return extent; +} + +fn flex_wrap_hash(result: FlexWrapResult) i64 { + let hash: i64 = flex_wrap_mix(flex_wrap_mix(17, result.container.width), result.container.height); + hash = flex_wrap_mix(hash, result.overflow_main); + hash = flex_wrap_mix(hash, result.overflow_cross); + hash = flex_wrap_mix(hash, result.duplicate_ids as i64); + hash = flex_wrap_mix(hash, result.wrapped as i64); + hash = flex_wrap_mix(hash, result.error as i64); + hash = flex_wrap_mix(hash, result.error_index as i64); + hash = flex_wrap_mix(hash, result.lines.len as i64); + let i: i32 = 0; + while (i < result.lines.len) { + let line: FlexLine = result.lines.data[i]; + hash = flex_wrap_mix(hash, line.start as i64); + hash = flex_wrap_mix(hash, line.count as i64); + hash = flex_wrap_mix(hash, line.cross_extent); + hash = flex_wrap_mix(hash, line.cross_offset); + i = i + 1; + } + hash = flex_wrap_mix(hash, result.placements.len as i64); + i = 0; + while (i < result.placements.len) { + let placement: FlexPlacement = result.placements.data[i]; + hash = flex_wrap_mix(hash, placement.id); + hash = flex_wrap_mix(hash, placement.frame.x); + hash = flex_wrap_mix(hash, placement.frame.y); + hash = flex_wrap_mix(hash, placement.frame.width); + hash = flex_wrap_mix(hash, placement.frame.height); + hash = flex_wrap_mix(hash, placement.baseline); + i = i + 1; + } + return hash; +} + +fn flex_wrap_layout(items: ArrayList[FlexItem], raw_constraints: Constraints, raw_wrap_style: FlexWrapStyle) FlexWrapResult { + let constraints: Constraints = normalize_constraints(raw_constraints); + let wrap_style: FlexWrapStyle = raw_wrap_style; + let validation: FlexWrapError = flex_wrap_validate(items, constraints, wrap_style); + if (validation != FlexWrapError.none) { + let index: i32 = 0 - 1; + if (validation == FlexWrapError.invalid_item) { + index = 0; + while (index < items.len && flex_wrap_item_valid(items.data[index]) != 0) { index = index + 1; } + } + return flex_wrap_empty_result(validation, index); + } + if (wrap_style.line_gap < 0) { wrap_style.line_gap = 0; } + if (wrap_style.flex.gap < 0) { wrap_style.flex.gap = 0; } + wrap_style.flex.padding = flex_safe_insets(wrap_style.flex.padding); + + let main_padding: i64 = flex_main_padding(wrap_style.flex.padding, wrap_style.flex.axis); + let cross_padding: i64 = flex_cross_padding(wrap_style.flex.padding, wrap_style.flex.axis); + let main_limit: i64 = flex_main_max(constraints, wrap_style.flex.axis); + let desired_main: i64 = main_limit; + let i: i32 = 0; + if (main_limit >= unbounded_extent()) { + desired_main = main_padding; + while (i < items.len) { + desired_main = flex_wrap_checked_add(desired_main, flex_item_main(items.data[i])); + if (desired_main < 0) { return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, i); } + if (i > 0) { + desired_main = flex_wrap_checked_add(desired_main, wrap_style.flex.gap); + if (desired_main < 0) { return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, i); } + } + i = i + 1; + } + } + let container_main: i64 = desired_main; + if (main_limit < unbounded_extent()) { container_main = main_limit; } + container_main = clamp_i64(container_main, flex_main_min(constraints, wrap_style.flex.axis), main_limit); + let inner_main: i64 = container_main - main_padding; + if (inner_main < 0) { inner_main = 0; } + + let lines: ArrayList[FlexLine] = make[FlexLine](16); + let line_start: i32 = 0; + let line_count: i32 = 0; + let line_used: i64 = 0; + i = 0; + while (i < items.len) { + let item_main: i64 = flex_item_main(items.data[i]); + let next_used: i64 = line_used; + if (line_count > 0) { next_used = flex_wrap_checked_add(next_used, wrap_style.flex.gap); } + if (next_used >= 0) { next_used = flex_wrap_checked_add(next_used, item_main); } + if (next_used < 0) { + free[FlexLine](&lines); + return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, i); + } + if (wrap_style.wrap != 0 && line_count > 0 && next_used > inner_main) { + push[FlexLine](&lines, FlexLine{ + .start = line_start, + .count = line_count, + .cross_extent = flex_line_cross_extent(items, line_start, line_count, wrap_style.flex), + .cross_offset = 0, + }); + line_start = i; + line_count = 0; + line_used = 0; + } + if (line_count > 0) { line_used = flex_wrap_checked_add(line_used, wrap_style.flex.gap); } + if (line_used >= 0) { line_used = flex_wrap_checked_add(line_used, item_main); } + if (line_used < 0) { + free[FlexLine](&lines); + return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, i); + } + line_count = line_count + 1; + i = i + 1; + } + if (line_count > 0) { + push[FlexLine](&lines, FlexLine{ + .start = line_start, + .count = line_count, + .cross_extent = flex_line_cross_extent(items, line_start, line_count, wrap_style.flex), + .cross_offset = 0, + }); + } + + let lines_cross: i64 = 0; + i = 0; + while (i < lines.len) { + lines_cross = flex_wrap_checked_add(lines_cross, lines.data[i].cross_extent); + if (lines_cross < 0) { + free[FlexLine](&lines); + return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, i); + } + i = i + 1; + } + let line_gap_total: i64 = 0; + i = 1; + while (i < lines.len) { + line_gap_total = flex_wrap_checked_add(line_gap_total, wrap_style.line_gap); + if (line_gap_total < 0) { + free[FlexLine](&lines); + return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, i); + } + i = i + 1; + } + let desired_cross: i64 = flex_wrap_checked_add(cross_padding, lines_cross); + if (desired_cross >= 0) { desired_cross = flex_wrap_checked_add(desired_cross, line_gap_total); } + if (desired_cross < 0) { + free[FlexLine](&lines); + return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, 0 - 1); + } + let cross_limit: i64 = flex_cross_max(constraints, wrap_style.flex.axis); + let container_cross: i64 = clamp_i64(desired_cross, flex_cross_min(constraints, wrap_style.flex.axis), cross_limit); + let available_cross: i64 = container_cross - cross_padding; + let positive_extra: i64 = available_cross - lines_cross - line_gap_total; + if (positive_extra < 0) { positive_extra = 0; } + let leading: i64 = 0; + let extra_line_gap: i64 = 0; + let stretch_remainder: i64 = 0; + switch (wrap_style.line_alignment) { + .start => { } + .center => { leading = positive_extra / 2; } + .end => { leading = positive_extra; } + .space_between => { + if (lines.len > 1) { extra_line_gap = positive_extra / ((lines.len - 1) as i64); } + } + .space_around => { + if (lines.len > 0) { + extra_line_gap = positive_extra / (lines.len as i64); + leading = extra_line_gap / 2; + } + } + .space_evenly => { + if (lines.len > 0) { + extra_line_gap = positive_extra / ((lines.len + 1) as i64); + leading = extra_line_gap; + } + } + .stretch => { + if (lines.len > 0) { stretch_remainder = positive_extra; } + } + } + + let placements: ArrayList[FlexPlacement] = make[FlexPlacement](items.len); + let cross_cursor: i64 = leading; + let overflow_main: i64 = 0; + let stretched: i64 = 0; + i = 0; + while (i < lines.len) { + let line: FlexLine = lines.data[i]; + let line_cross: i64 = line.cross_extent; + if (wrap_style.line_alignment == FlexLineAlignment.stretch && lines.len > 0) { + let share: i64 = 0; + if (i + 1 == lines.len) { share = stretch_remainder - stretched; } + else { share = stretch_remainder / (lines.len as i64); } + line_cross = line_cross + share; + stretched = stretched + share; + } + let line_items: ArrayList[FlexItem] = make[FlexItem](line.count); + let j: i32 = 0; + while (j < line.count) { + push[FlexItem](&line_items, items.data[line.start + j]); + j = j + 1; + } + let line_style: FlexStyle = wrap_style.flex; + line_style.padding = insets_all(0); + let line_constraints: Constraints = tight_constraints(inner_main, line_cross); + if (wrap_style.flex.axis == FlexAxis.column) { + line_constraints = tight_constraints(line_cross, inner_main); + } + let laid_out: FlexResult = flex_layout(line_items, line_constraints, line_style); + if (laid_out.error != FlexError.none) { + let error_index: i32 = laid_out.error_index; + if (error_index >= 0) { error_index = error_index + line.start; } + flex_free(&laid_out); + free[FlexItem](&line_items); + free[FlexPlacement](&placements); + free[FlexLine](&lines); + return flex_wrap_empty_result(FlexWrapError.arithmetic_overflow, error_index); + } + if (laid_out.overflow > overflow_main) { overflow_main = laid_out.overflow; } + + let band_offset: i64 = cross_cursor; + lines.data[i].cross_extent = line_cross; + if (wrap_style.flex.axis == FlexAxis.row) { + lines.data[i].cross_offset = wrap_style.flex.padding.top + band_offset; + } else if (wrap_style.flex.direction == LayoutDirection.ltr) { + lines.data[i].cross_offset = wrap_style.flex.padding.left + band_offset; + } else { + lines.data[i].cross_offset = container_cross - wrap_style.flex.padding.right - band_offset - line_cross; + } + + j = 0; + while (j < laid_out.placements.len) { + let placement: FlexPlacement = laid_out.placements.data[j]; + if (wrap_style.flex.axis == FlexAxis.row) { + placement.frame.x = placement.frame.x + wrap_style.flex.padding.left; + placement.frame.y = placement.frame.y + wrap_style.flex.padding.top + band_offset; + placement.baseline = placement.baseline + wrap_style.flex.padding.top + band_offset; + } else { + placement.frame.y = placement.frame.y + wrap_style.flex.padding.top; + placement.frame.x = placement.frame.x + lines.data[i].cross_offset; + placement.baseline = placement.baseline + wrap_style.flex.padding.top; + } + push[FlexPlacement](&placements, placement); + j = j + 1; + } + flex_free(&laid_out); + free[FlexItem](&line_items); + cross_cursor = cross_cursor + line_cross; + if (i + 1 < lines.len) { cross_cursor = cross_cursor + wrap_style.line_gap + extra_line_gap; } + i = i + 1; + } + + let output_size: Size = switch (wrap_style.flex.axis) { + .row => size(container_main, container_cross), + .column => size(container_cross, container_main), + }; + let overflow_cross: i64 = desired_cross - container_cross; + if (overflow_cross < 0) { overflow_cross = 0; } + let result: FlexWrapResult = FlexWrapResult{ + .placements = placements, + .lines = lines, + .container = output_size, + .overflow_main = overflow_main, + .overflow_cross = overflow_cross, + .duplicate_ids = flex_has_duplicate_id(items), + .wrapped = (lines.len > 1) as i32, + .layout_hash = 0, + .error = FlexWrapError.none, + .error_index = 0 - 1, + }; + result.layout_hash = flex_wrap_hash(result); + return result; +} diff --git a/src/layout/grid.zag b/src/layout/grid.zag new file mode 100644 index 0000000..97611de --- /dev/null +++ b/src/layout/grid.zag @@ -0,0 +1,726 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("common.zag") +@import("measure.zag") + +enum GridTrackKind { fixed, intrinsic, fraction } +enum GridError { + none, + too_many_items, + too_many_tracks, + invalid_constraints, + invalid_style, + invalid_track, + invalid_item, + duplicate_id, + collision, + no_space, + arithmetic_overflow, +} + +struct GridTrack { + kind: GridTrackKind, + minimum: i64, + preferred: i64, + maximum: i64, + fraction: i64, +} + +struct GridItem { + id: i64, + row: i32, + column: i32, + row_span: i32, + column_span: i32, + metrics: IntrinsicMetrics, + horizontal: PlacementAlignment, + vertical: PlacementAlignment, + z_order: i64, +} + +struct GridStyle { + column_gap: i64, + row_gap: i64, + padding: Insets, + direction: LayoutDirection, + horizontal_content: ContentAlignment, + vertical_content: ContentAlignment, + allow_overlap: i32, +} + +struct GridCell { + row: i32, + column: i32, +} + +struct GridTrackPlacement { + offset: i64, + extent: i64, +} + +struct GridPlacement { + id: i64, + row: i32, + column: i32, + row_span: i32, + column_span: i32, + frame: Rect, + baseline: i64, + z_order: i64, +} + +struct GridResult { + placements: ArrayList[GridPlacement], + columns: ArrayList[GridTrackPlacement], + rows: ArrayList[GridTrackPlacement], + container: Size, + overflow_width: i64, + overflow_height: i64, + overlap_cells: i32, + layout_hash: i64, + error: GridError, + error_index: i32, +} + +struct GridDistribution { leading: i64, extra_gap: i64 } + +fn grid_fixed(value: i64) GridTrack { + return GridTrack{ .kind = GridTrackKind.fixed, .minimum = value, .preferred = value, + .maximum = value, .fraction = 0 }; +} + +fn grid_intrinsic(minimum: i64, preferred: i64, maximum: i64) GridTrack { + return GridTrack{ .kind = GridTrackKind.intrinsic, .minimum = minimum, + .preferred = preferred, .maximum = maximum, .fraction = 0 }; +} + +fn grid_fraction(fraction: i64, minimum: i64, maximum: i64) GridTrack { + return GridTrack{ .kind = GridTrackKind.fraction, .minimum = minimum, + .preferred = minimum, .maximum = maximum, .fraction = fraction }; +} + +fn grid_item(id: i64, metrics: IntrinsicMetrics) GridItem { + return GridItem{ + .id = id, .row = 0 - 1, .column = 0 - 1, .row_span = 1, .column_span = 1, + .metrics = metrics, .horizontal = PlacementAlignment.stretch, + .vertical = PlacementAlignment.stretch, .z_order = 0, + }; +} + +fn grid_item_at(id: i64, row: i32, column: i32, metrics: IntrinsicMetrics) GridItem { + let item: GridItem = grid_item(id, metrics); + item.row = row; + item.column = column; + return item; +} + +fn grid_style(column_gap: i64, row_gap: i64, padding: Insets) GridStyle { + return GridStyle{ + .column_gap = column_gap, .row_gap = row_gap, .padding = padding, + .direction = LayoutDirection.ltr, + .horizontal_content = ContentAlignment.start, + .vertical_content = ContentAlignment.start, + .allow_overlap = 0, + }; +} + +fn grid_free(result: *GridResult) void { + free[GridPlacement](&result.*.placements); + free[GridTrackPlacement](&result.*.columns); + free[GridTrackPlacement](&result.*.rows); +} + +fn grid_mix(hash: i64, value: i64) i64 { return (hash * 1099511628211) ^ value; } + +fn grid_error_result(error: GridError, index: i32) GridResult { + let result: GridResult = GridResult{ + .placements = make[GridPlacement](1), + .columns = make[GridTrackPlacement](1), + .rows = make[GridTrackPlacement](1), + .container = size(0, 0), .overflow_width = 0, .overflow_height = 0, + .overlap_cells = 0, .layout_hash = 17, .error = error, .error_index = index, + }; + result.layout_hash = grid_mix(grid_mix(result.layout_hash, error as i64), index as i64); + return result; +} + +fn grid_checked_add(a: i64, b: i64) i64 { + if (a < 0 || b < 0 || a > unbounded_extent() - b) { return 0 - 1; } + return a + b; +} + +fn grid_extent_valid(value: i64) i32 { + return (value >= 0 && value <= unbounded_extent() / 4) as i32; +} + +fn grid_insets_valid(value: Insets) i32 { + return (grid_extent_valid(value.top) == 1 && grid_extent_valid(value.right) == 1 && + grid_extent_valid(value.bottom) == 1 && grid_extent_valid(value.left) == 1) as i32; +} + +fn grid_track_valid(track: GridTrack) i32 { + if (track.minimum < 0 || track.preferred < track.minimum || + track.maximum < track.preferred || track.maximum > unbounded_extent()) { return 0; } + if (track.kind == GridTrackKind.fixed) { + return (track.minimum == track.preferred && track.preferred == track.maximum && + track.fraction == 0) as i32; + } + if (track.kind == GridTrackKind.intrinsic) { return (track.fraction == 0) as i32; } + return (track.fraction > 0 && track.fraction <= 4096 && + track.preferred == track.minimum) as i32; +} + +fn grid_item_valid(item: GridItem, row_count: i32, column_count: i32) i32 { + if (item.id <= 0 || intrinsic_metric_valid(item.metrics) == 0 || + item.row_span <= 0 || item.column_span <= 0 || + item.row_span > row_count || item.column_span > column_count || + item.horizontal == PlacementAlignment.baseline || + (item.vertical == PlacementAlignment.baseline && item.row_span != 1)) { return 0; } + let automatic: i32 = (item.row == 0 - 1 && item.column == 0 - 1) as i32; + let explicit: i32 = (item.row >= 0 && item.column >= 0 && + item.row + item.row_span <= row_count && + item.column + item.column_span <= column_count) as i32; + return (automatic == 1 || explicit == 1) as i32; +} + +fn grid_cells_free(occupancy: ArrayList[i32], columns: i32, row: i32, column: i32, + row_span: i32, column_span: i32) i32 { + let r: i32 = row; + while (r < row + row_span) { + let c: i32 = column; + while (c < column + column_span) { + if (occupancy.data[r * columns + c] != 0) { return 0; } + c = c + 1; + } + r = r + 1; + } + return 1; +} + +fn grid_mark_cells(occupancy: *ArrayList[i32], columns: i32, row: i32, column: i32, + row_span: i32, column_span: i32, marker: i32) i32 { + let overlaps: i32 = 0; + let r: i32 = row; + while (r < row + row_span) { + let c: i32 = column; + while (c < column + column_span) { + let index: i32 = r * columns + c; + if (occupancy.*.data[index] != 0) { overlaps = overlaps + 1; } + occupancy.*.data[index] = marker; + c = c + 1; + } + r = r + 1; + } + return overlaps; +} + +fn grid_track_initial(track: GridTrack) i64 { return track.preferred; } + +fn grid_span_extent(sizes: ArrayList[i64], start: i32, count: i32, gap: i64) i64 { + let extent: i64 = 0; + let i: i32 = 0; + while (i < count) { + extent = grid_checked_add(extent, sizes.data[start + i]); + if (extent < 0) { return 0 - 1; } + if (i > 0) { + extent = grid_checked_add(extent, gap); + if (extent < 0) { return 0 - 1; } + } + i = i + 1; + } + return extent; +} + +fn grid_grow_span(tracks: ArrayList[GridTrack], sizes: *ArrayList[i64], start: i32, + count: i32, gap: i64, target: i64) i32 { + let current: i64 = grid_span_extent(sizes.*, start, count, gap); + if (current < 0) { return 0; } + let remaining: i64 = target - current; + if (remaining <= 0) { return 1; } + let rounds: i32 = 0; + while (remaining > 0 && rounds < count + 1) { + let active: i32 = 0; + let i: i32 = 0; + while (i < count) { + let index: i32 = start + i; + if (tracks.data[index].kind != GridTrackKind.fixed && + sizes.*.data[index] < tracks.data[index].maximum) { active = active + 1; } + i = i + 1; + } + if (active == 0) { return 1; } + let before: i64 = remaining; + i = 0; + while (i < count && remaining > 0) { + let index: i32 = start + i; + if (tracks.data[index].kind != GridTrackKind.fixed && + sizes.*.data[index] < tracks.data[index].maximum) { + let share: i64 = remaining / (active as i64); + if (share < 1) { share = 1; } + let capacity: i64 = tracks.data[index].maximum - sizes.*.data[index]; + if (share > capacity) { share = capacity; } + if (share > remaining) { share = remaining; } + sizes.*.data[index] = sizes.*.data[index] + share; + remaining = remaining - share; + active = active - 1; + } + i = i + 1; + } + if (remaining == before) { return 1; } + rounds = rounds + 1; + } + return 1; +} + +fn grid_axis_prepare(items: ArrayList[GridItem], cells: ArrayList[GridCell], + tracks: ArrayList[GridTrack], sizes: *ArrayList[i64], gap: i64, horizontal: i32) i32 { + let i: i32 = 0; + while (i < tracks.len) { push[i64](sizes, grid_track_initial(tracks.data[i])); i = i + 1; } + i = 0; + while (i < items.len) { + let start: i32 = cells.data[i].row; + let count: i32 = items.data[i].row_span; + let minimum: i64 = items.data[i].metrics.minimum.height; + let preferred: i64 = items.data[i].metrics.preferred.height; + if (horizontal != 0) { + start = cells.data[i].column; + count = items.data[i].column_span; + minimum = items.data[i].metrics.minimum.width; + preferred = items.data[i].metrics.preferred.width; + } + if (grid_grow_span(tracks, sizes, start, count, gap, minimum) == 0 || + grid_grow_span(tracks, sizes, start, count, gap, preferred) == 0) { return i; } + i = i + 1; + } + return 0 - 1; +} + +fn grid_sizes_sum(sizes: ArrayList[i64]) i64 { + let total: i64 = 0; + let i: i32 = 0; + while (i < sizes.len) { + total = grid_checked_add(total, sizes.data[i]); + if (total < 0) { return 0 - 1; } + i = i + 1; + } + return total; +} + +fn grid_grow_available(tracks: ArrayList[GridTrack], sizes: *ArrayList[i64], extra: i64, + fraction_only: i32) i64 { + let remaining: i64 = extra; + let rounds: i32 = 0; + while (remaining > 0 && rounds < tracks.len + 1) { + let weight: i64 = 0; + let last_eligible: i32 = 0 - 1; + let i: i32 = 0; + while (i < tracks.len) { + let eligible: i32 = (sizes.*.data[i] < tracks.data[i].maximum && + tracks.data[i].kind != GridTrackKind.fixed) as i32; + if (eligible == 1 && (fraction_only == 0 || tracks.data[i].kind == GridTrackKind.fraction)) { + if (fraction_only != 0) { weight = weight + tracks.data[i].fraction; } + else { weight = weight + 1; } + last_eligible = i; + } + i = i + 1; + } + if (weight == 0) { return remaining; } + let before: i64 = remaining; + i = 0; + while (i < tracks.len && remaining > 0) { + let eligible: i32 = (sizes.*.data[i] < tracks.data[i].maximum && + tracks.data[i].kind != GridTrackKind.fixed) as i32; + if (eligible == 1 && (fraction_only == 0 || tracks.data[i].kind == GridTrackKind.fraction)) { + let item_weight: i64 = 1; + if (fraction_only != 0) { item_weight = tracks.data[i].fraction; } + let share: i64 = (before * item_weight) / weight; + if (i == last_eligible) { share = remaining; } + let capacity: i64 = tracks.data[i].maximum - sizes.*.data[i]; + if (share > capacity) { share = capacity; } + if (share > remaining) { share = remaining; } + sizes.*.data[i] = sizes.*.data[i] + share; + remaining = remaining - share; + } + i = i + 1; + } + if (remaining == before) { return remaining; } + rounds = rounds + 1; + } + return remaining; +} + +fn grid_shrink_available(tracks: ArrayList[GridTrack], sizes: *ArrayList[i64], need: i64) i64 { + let remaining: i64 = need; + let rounds: i32 = 0; + while (remaining > 0 && rounds < tracks.len + 1) { + let active: i32 = 0; + let i: i32 = 0; + while (i < tracks.len) { + if (tracks.data[i].kind != GridTrackKind.fixed && + sizes.*.data[i] > tracks.data[i].minimum) { active = active + 1; } + i = i + 1; + } + if (active == 0) { return remaining; } + let before: i64 = remaining; + i = 0; + while (i < tracks.len && remaining > 0) { + if (tracks.data[i].kind != GridTrackKind.fixed && + sizes.*.data[i] > tracks.data[i].minimum) { + let share: i64 = remaining / (active as i64); + if (share < 1) { share = 1; } + let capacity: i64 = sizes.*.data[i] - tracks.data[i].minimum; + if (share > capacity) { share = capacity; } + if (share > remaining) { share = remaining; } + sizes.*.data[i] = sizes.*.data[i] - share; + remaining = remaining - share; + active = active - 1; + } + i = i + 1; + } + if (remaining == before) { return remaining; } + rounds = rounds + 1; + } + return remaining; +} + +fn grid_adjust_axis(tracks: ArrayList[GridTrack], sizes: *ArrayList[i64], available: i64, + alignment: ContentAlignment) i64 { + let used: i64 = grid_sizes_sum(sizes.*); + if (used < 0) { return 0 - 1; } + if (used < available) { + let remaining: i64 = grid_grow_available(tracks, sizes, available - used, 1); + if (alignment == ContentAlignment.stretch && remaining > 0) { + _ = grid_grow_available(tracks, sizes, remaining, 0); + } + } else if (used > available) { + _ = grid_shrink_available(tracks, sizes, used - available); + } + return grid_sizes_sum(sizes.*); +} + +fn grid_distribution(free_space: i64, count: i32, alignment: ContentAlignment) GridDistribution { + let free: i64 = free_space; + if (free < 0) { free = 0; } + let result: GridDistribution = GridDistribution{ .leading = 0, .extra_gap = 0 }; + switch (alignment) { + .start => { } + .center => { result.leading = free / 2; } + .end => { result.leading = free; } + .space_between => { if (count > 1) { result.extra_gap = free / ((count - 1) as i64); } } + .space_around => { + if (count > 0) { result.extra_gap = free / (count as i64); result.leading = result.extra_gap / 2; } + } + .space_evenly => { + if (count > 0) { result.extra_gap = free / ((count + 1) as i64); result.leading = result.extra_gap; } + } + .stretch => { } + } + return result; +} + +fn grid_axis_item_size(minimum: i64, preferred: i64, maximum: i64, available: i64, + alignment: PlacementAlignment) i64 { + if (alignment == PlacementAlignment.stretch) { return clamp_i64(available, minimum, maximum); } + let upper: i64 = maximum; + if (available < upper) { upper = available; } + return clamp_i64(preferred, minimum, upper); +} + +fn grid_axis_item_offset(available: i64, extent: i64, alignment: PlacementAlignment) i64 { + if (alignment == PlacementAlignment.center) { return (available - extent) / 2; } + if (alignment == PlacementAlignment.end) { return available - extent; } + return 0; +} + +fn grid_track_hash(track: GridTrack) i64 { + let hash: i64 = grid_mix(17, track.kind as i64); + hash = grid_mix(hash, track.minimum); hash = grid_mix(hash, track.preferred); + hash = grid_mix(hash, track.maximum); hash = grid_mix(hash, track.fraction); + return hash; +} + +fn grid_input_hash(items: ArrayList[GridItem], columns: ArrayList[GridTrack], + rows: ArrayList[GridTrack], constraints: Constraints, style: GridStyle) i64 { + let hash: i64 = grid_mix(grid_mix(17, columns.len as i64), rows.len as i64); + hash = grid_mix(hash, constraints.min_width); hash = grid_mix(hash, constraints.max_width); + hash = grid_mix(hash, constraints.min_height); hash = grid_mix(hash, constraints.max_height); + hash = grid_mix(hash, style.column_gap); hash = grid_mix(hash, style.row_gap); + hash = grid_mix(hash, style.padding.top); hash = grid_mix(hash, style.padding.right); + hash = grid_mix(hash, style.padding.bottom); hash = grid_mix(hash, style.padding.left); + hash = grid_mix(hash, style.direction as i64); + hash = grid_mix(hash, style.horizontal_content as i64); + hash = grid_mix(hash, style.vertical_content as i64); + hash = grid_mix(hash, style.allow_overlap as i64); + let i: i32 = 0; + while (i < columns.len) { hash = grid_mix(hash, grid_track_hash(columns.data[i])); i = i + 1; } + i = 0; + while (i < rows.len) { hash = grid_mix(hash, grid_track_hash(rows.data[i])); i = i + 1; } + i = 0; + while (i < items.len) { + let item: GridItem = items.data[i]; + hash = grid_mix(hash, item.id); hash = grid_mix(hash, item.row as i64); + hash = grid_mix(hash, item.column as i64); hash = grid_mix(hash, item.row_span as i64); + hash = grid_mix(hash, item.column_span as i64); + hash = grid_mix(hash, intrinsic_metrics_hash(item.metrics)); + hash = grid_mix(hash, item.horizontal as i64); hash = grid_mix(hash, item.vertical as i64); + hash = grid_mix(hash, item.z_order); + i = i + 1; + } + return hash; +} + +fn grid_output_hash(result: GridResult) i64 { + let hash: i64 = grid_mix(grid_mix(17, result.container.width), result.container.height); + hash = grid_mix(hash, result.overflow_width); hash = grid_mix(hash, result.overflow_height); + hash = grid_mix(hash, result.overlap_cells as i64); hash = grid_mix(hash, result.error as i64); + let i: i32 = 0; + while (i < result.columns.len) { + hash = grid_mix(hash, result.columns.data[i].offset); + hash = grid_mix(hash, result.columns.data[i].extent); i = i + 1; + } + i = 0; + while (i < result.rows.len) { + hash = grid_mix(hash, result.rows.data[i].offset); + hash = grid_mix(hash, result.rows.data[i].extent); i = i + 1; + } + i = 0; + while (i < result.placements.len) { + let placement: GridPlacement = result.placements.data[i]; + hash = grid_mix(hash, placement.id); hash = grid_mix(hash, placement.row as i64); + hash = grid_mix(hash, placement.column as i64); + hash = grid_mix(hash, placement.frame.x); hash = grid_mix(hash, placement.frame.y); + hash = grid_mix(hash, placement.frame.width); hash = grid_mix(hash, placement.frame.height); + hash = grid_mix(hash, placement.baseline); hash = grid_mix(hash, placement.z_order); + i = i + 1; + } + return hash; +} + +fn grid_layout(items: ArrayList[GridItem], columns: ArrayList[GridTrack], rows: ArrayList[GridTrack], + raw_constraints: Constraints, style: GridStyle) GridResult { + if (items.len > 4096) { return grid_error_result(GridError.too_many_items, 0 - 1); } + if (columns.len <= 0 || rows.len <= 0 || columns.len > 128 || rows.len > 128) { + return grid_error_result(GridError.too_many_tracks, 0 - 1); + } + let constraints: Constraints = normalize_constraints(raw_constraints); + if (constraints.min_width > unbounded_extent() || constraints.max_width > unbounded_extent() || + constraints.min_height > unbounded_extent() || constraints.max_height > unbounded_extent()) { + return grid_error_result(GridError.invalid_constraints, 0 - 1); + } + if (grid_extent_valid(style.column_gap) == 0 || grid_extent_valid(style.row_gap) == 0 || + grid_insets_valid(style.padding) == 0 || + (style.allow_overlap != 0 && style.allow_overlap != 1)) { + return grid_error_result(GridError.invalid_style, 0 - 1); + } + let i: i32 = 0; + while (i < columns.len) { + if (grid_track_valid(columns.data[i]) == 0) { return grid_error_result(GridError.invalid_track, i); } + i = i + 1; + } + i = 0; + while (i < rows.len) { + if (grid_track_valid(rows.data[i]) == 0) { return grid_error_result(GridError.invalid_track, columns.len + i); } + i = i + 1; + } + i = 0; + while (i < items.len) { + if (grid_item_valid(items.data[i], rows.len, columns.len) == 0) { + return grid_error_result(GridError.invalid_item, i); + } + let j: i32 = 0; + while (j < i) { + if (items.data[j].id == items.data[i].id) { return grid_error_result(GridError.duplicate_id, i); } + j = j + 1; + } + i = i + 1; + } + + let occupancy: ArrayList[i32] = make[i32](rows.len * columns.len); + i = 0; + while (i < rows.len * columns.len) { push[i32](&occupancy, 0); i = i + 1; } + let cells: ArrayList[GridCell] = make[GridCell](items.len); + let overlap_cells: i32 = 0; + i = 0; + while (i < items.len) { + let item: GridItem = items.data[i]; + let row: i32 = item.row; + let column: i32 = item.column; + if (row < 0) { + row = 0 - 1; + let candidate_row: i32 = 0; + while (candidate_row + item.row_span <= rows.len && row < 0) { + let candidate_column: i32 = 0; + while (candidate_column + item.column_span <= columns.len && row < 0) { + if (grid_cells_free(occupancy, columns.len, candidate_row, candidate_column, + item.row_span, item.column_span) == 1) { + row = candidate_row; column = candidate_column; + } + candidate_column = candidate_column + 1; + } + candidate_row = candidate_row + 1; + } + if (row < 0) { + free[i32](&occupancy); free[GridCell](&cells); + return grid_error_result(GridError.no_space, i); + } + } else if (grid_cells_free(occupancy, columns.len, row, column, + item.row_span, item.column_span) == 0 && style.allow_overlap == 0) { + free[i32](&occupancy); free[GridCell](&cells); + return grid_error_result(GridError.collision, i); + } + overlap_cells = overlap_cells + grid_mark_cells(&occupancy, columns.len, row, column, + item.row_span, item.column_span, i + 1); + push[GridCell](&cells, GridCell{ .row = row, .column = column }); + i = i + 1; + } + + let column_sizes: ArrayList[i64] = make[i64](columns.len); + let row_sizes: ArrayList[i64] = make[i64](rows.len); + let error_index: i32 = grid_axis_prepare(items, cells, columns, &column_sizes, style.column_gap, 1); + if (error_index >= 0) { + free[i32](&occupancy); free[GridCell](&cells); free[i64](&column_sizes); free[i64](&row_sizes); + return grid_error_result(GridError.arithmetic_overflow, error_index); + } + error_index = grid_axis_prepare(items, cells, rows, &row_sizes, style.row_gap, 0); + if (error_index >= 0) { + free[i32](&occupancy); free[GridCell](&cells); free[i64](&column_sizes); free[i64](&row_sizes); + return grid_error_result(GridError.arithmetic_overflow, error_index); + } + + let horizontal_padding: i64 = grid_checked_add(style.padding.left, style.padding.right); + let vertical_padding: i64 = grid_checked_add(style.padding.top, style.padding.bottom); + let column_gaps: i64 = 0; + i = 1; + while (i < columns.len) { column_gaps = grid_checked_add(column_gaps, style.column_gap); i = i + 1; } + let row_gaps: i64 = 0; + i = 1; + while (i < rows.len) { row_gaps = grid_checked_add(row_gaps, style.row_gap); i = i + 1; } + let desired_width: i64 = grid_checked_add(grid_checked_add(horizontal_padding, column_gaps), grid_sizes_sum(column_sizes)); + let desired_height: i64 = grid_checked_add(grid_checked_add(vertical_padding, row_gaps), grid_sizes_sum(row_sizes)); + if (horizontal_padding < 0 || vertical_padding < 0 || column_gaps < 0 || row_gaps < 0 || + desired_width < 0 || desired_height < 0) { + free[i32](&occupancy); free[GridCell](&cells); free[i64](&column_sizes); free[i64](&row_sizes); + return grid_error_result(GridError.arithmetic_overflow, 0 - 1); + } + let container: Size = size( + clamp_i64(desired_width, constraints.min_width, constraints.max_width), + clamp_i64(desired_height, constraints.min_height, constraints.max_height) + ); + let available_columns: i64 = container.width - horizontal_padding - column_gaps; + if (available_columns < 0) { available_columns = 0; } + let available_rows: i64 = container.height - vertical_padding - row_gaps; + if (available_rows < 0) { available_rows = 0; } + let used_columns: i64 = grid_adjust_axis(columns, &column_sizes, available_columns, style.horizontal_content); + let used_rows: i64 = grid_adjust_axis(rows, &row_sizes, available_rows, style.vertical_content); + if (used_columns < 0 || used_rows < 0) { + free[i32](&occupancy); free[GridCell](&cells); free[i64](&column_sizes); free[i64](&row_sizes); + return grid_error_result(GridError.arithmetic_overflow, 0 - 1); + } + let column_distribution: GridDistribution = grid_distribution( + available_columns - used_columns, columns.len, style.horizontal_content); + let row_distribution: GridDistribution = grid_distribution( + available_rows - used_rows, rows.len, style.vertical_content); + + let column_offsets: ArrayList[i64] = make[i64](columns.len); + let row_offsets: ArrayList[i64] = make[i64](rows.len); + let cursor: i64 = column_distribution.leading; + i = 0; + while (i < columns.len) { + push[i64](&column_offsets, cursor); + cursor = cursor + column_sizes.data[i]; + if (i + 1 < columns.len) { cursor = cursor + style.column_gap + column_distribution.extra_gap; } + i = i + 1; + } + cursor = row_distribution.leading; + i = 0; + while (i < rows.len) { + push[i64](&row_offsets, cursor); + cursor = cursor + row_sizes.data[i]; + if (i + 1 < rows.len) { cursor = cursor + style.row_gap + row_distribution.extra_gap; } + i = i + 1; + } + + let column_output: ArrayList[GridTrackPlacement] = make[GridTrackPlacement](columns.len); + i = 0; + while (i < columns.len) { + let offset: i64 = style.padding.left + column_offsets.data[i]; + if (style.direction == LayoutDirection.rtl) { + offset = container.width - style.padding.right - column_offsets.data[i] - column_sizes.data[i]; + } + push[GridTrackPlacement](&column_output, GridTrackPlacement{ .offset = offset, .extent = column_sizes.data[i] }); + i = i + 1; + } + let row_output: ArrayList[GridTrackPlacement] = make[GridTrackPlacement](rows.len); + i = 0; + while (i < rows.len) { + push[GridTrackPlacement](&row_output, GridTrackPlacement{ + .offset = style.padding.top + row_offsets.data[i], .extent = row_sizes.data[i], + }); + i = i + 1; + } + + let row_baselines: ArrayList[i64] = make[i64](rows.len); + i = 0; + while (i < rows.len) { push[i64](&row_baselines, 0); i = i + 1; } + i = 0; + while (i < items.len) { + if (items.data[i].vertical == PlacementAlignment.baseline) { + let baseline: i64 = items.data[i].metrics.baseline; + let row_index: i32 = cells.data[i].row; + if (baseline > row_baselines.data[row_index]) { row_baselines.data[row_index] = baseline; } + } + i = i + 1; + } + + let placements: ArrayList[GridPlacement] = make[GridPlacement](items.len); + let overflow_width: i64 = desired_width - container.width; + if (overflow_width < 0) { overflow_width = 0; } + let overflow_height: i64 = desired_height - container.height; + if (overflow_height < 0) { overflow_height = 0; } + i = 0; + while (i < items.len) { + let item: GridItem = items.data[i]; + let cell: GridCell = cells.data[i]; + let cell_width: i64 = grid_span_extent(column_sizes, cell.column, item.column_span, + style.column_gap + column_distribution.extra_gap); + let cell_height: i64 = grid_span_extent(row_sizes, cell.row, item.row_span, + style.row_gap + row_distribution.extra_gap); + let width: i64 = grid_axis_item_size(item.metrics.minimum.width, item.metrics.preferred.width, + item.metrics.maximum.width, cell_width, item.horizontal); + let height: i64 = grid_axis_item_size(item.metrics.minimum.height, item.metrics.preferred.height, + item.metrics.maximum.height, cell_height, item.vertical); + let x_offset: i64 = grid_axis_item_offset(cell_width, width, item.horizontal); + let y_offset: i64 = grid_axis_item_offset(cell_height, height, item.vertical); + if (item.vertical == PlacementAlignment.baseline) { + y_offset = row_baselines.data[cell.row] - item.metrics.baseline; + } + let logical_x: i64 = column_offsets.data[cell.column] + x_offset; + let frame_x: i64 = style.padding.left + logical_x; + if (style.direction == LayoutDirection.rtl) { + frame_x = container.width - style.padding.right - logical_x - width; + } + let frame: Rect = rect(frame_x, style.padding.top + row_offsets.data[cell.row] + y_offset, + width, height); + let item_overflow_width: i64 = width - cell_width; + if (item_overflow_width > overflow_width) { overflow_width = item_overflow_width; } + let item_overflow_height: i64 = height - cell_height; + if (item_overflow_height > overflow_height) { overflow_height = item_overflow_height; } + push[GridPlacement](&placements, GridPlacement{ + .id = item.id, .row = cell.row, .column = cell.column, + .row_span = item.row_span, .column_span = item.column_span, + .frame = frame, .baseline = frame.y + clamp_i64(item.metrics.baseline, 0, frame.height), + .z_order = item.z_order, + }); + i = i + 1; + } + + let result: GridResult = GridResult{ + .placements = placements, .columns = column_output, .rows = row_output, + .container = container, .overflow_width = overflow_width, .overflow_height = overflow_height, + .overlap_cells = overlap_cells, .layout_hash = 0, + .error = GridError.none, .error_index = 0 - 1, + }; + result.layout_hash = grid_mix(grid_input_hash(items, columns, rows, constraints, style), grid_output_hash(result)); + free[i32](&occupancy); free[GridCell](&cells); free[i64](&column_sizes); free[i64](&row_sizes); + free[i64](&column_offsets); free[i64](&row_offsets); free[i64](&row_baselines); + return result; +} diff --git a/src/layout/invalidation.zag b/src/layout/invalidation.zag new file mode 100644 index 0000000..c484eb7 --- /dev/null +++ b/src/layout/invalidation.zag @@ -0,0 +1,115 @@ +@import("std:list") +@import("../core/view_contract.zag") + +enum LayoutPhase { measure, layout, paint, semantics } +enum LayoutReason { + intrinsic_content, + constraints, + child_measurement, + flex_priority, + gap, + padding, + direction, + safe_area, + density, + text_scale, + breakpoint, +} + +struct LayoutDependency { + node: NodeKey, + ancestor: NodeKey, + state_id: i64, + read_revision: i64, + phase: LayoutPhase, + reason: LayoutReason, +} + +struct LayoutInvalidationCause { + node: NodeKey, + ancestor: NodeKey, + state_id: i64, + read_revision: i64, + from_revision: i64, + to_revision: i64, + phase: LayoutPhase, + reason: LayoutReason, +} + +struct LayoutTrace { + dependencies: ArrayList[LayoutDependency], +} + +struct LayoutInvalidationResult { + causes: ArrayList[LayoutInvalidationCause], +} + +fn layout_trace_make() LayoutTrace { + return LayoutTrace{ .dependencies = make[LayoutDependency](16) }; +} + +fn layout_trace_free(trace: *LayoutTrace) void { + free[LayoutDependency](&trace.*.dependencies); +} + +fn layout_trace_record( + trace: *LayoutTrace, + context: ViewContext, + ancestor: NodeKey, + phase: LayoutPhase, + reason: LayoutReason, +) void { + let i: i32 = 0; + while (i < context.reads.len) { + let read: StateRead = context.reads.data[i]; + let existing: i32 = 0; + while (existing < trace.*.dependencies.len && !( + node_key_equal(trace.*.dependencies.data[existing].node, read.reader) == 1 && + node_key_equal(trace.*.dependencies.data[existing].ancestor, ancestor) == 1 && + trace.*.dependencies.data[existing].state_id == read.state_id && + trace.*.dependencies.data[existing].phase == phase && + trace.*.dependencies.data[existing].reason == reason + )) { existing = existing + 1; } + if (existing < trace.*.dependencies.len) { + trace.*.dependencies.data[existing].read_revision = read.revision; + } else { + push[LayoutDependency](&trace.*.dependencies, LayoutDependency{ + .node = read.reader, + .ancestor = ancestor, + .state_id = read.state_id, + .read_revision = read.revision, + .phase = phase, + .reason = reason, + }); + } + i = i + 1; + } +} + +fn layout_trace_invalidate(trace: LayoutTrace, change: StateChange) LayoutInvalidationResult { + let result: LayoutInvalidationResult = LayoutInvalidationResult{ + .causes = make[LayoutInvalidationCause](4), + }; + let i: i32 = 0; + while (i < trace.dependencies.len) { + let dependency: LayoutDependency = trace.dependencies.data[i]; + if (dependency.state_id == change.state_id && dependency.read_revision < change.to_revision) { + push[LayoutInvalidationCause](&result.causes, LayoutInvalidationCause{ + .node = dependency.node, + .ancestor = dependency.ancestor, + .state_id = dependency.state_id, + .read_revision = dependency.read_revision, + .from_revision = change.from_revision, + .to_revision = change.to_revision, + .phase = dependency.phase, + .reason = dependency.reason, + }); + } + i = i + 1; + } + return result; +} + +fn layout_invalidation_free(result: *LayoutInvalidationResult) void { + free[LayoutInvalidationCause](&result.*.causes); +} diff --git a/src/layout/measure.zag b/src/layout/measure.zag new file mode 100644 index 0000000..53a9aca --- /dev/null +++ b/src/layout/measure.zag @@ -0,0 +1,653 @@ +@import("std:list") +@import("../core/geometry.zag") + +enum IntrinsicKind { leaf, row, column, overlay } +enum IntrinsicError { + none, + too_many_nodes, + invalid_node, + duplicate_node, + missing_node, + missing_child, + invalid_edge, + multiple_parents, + orphan_node, + cycle, + depth_exceeded, + unstable_measurement, + invalid_constraints, + arithmetic_overflow, +} +enum MeasureRule { intrinsic_preferred, minimum_constraint, maximum_constraint } + +struct IntrinsicMetrics { + minimum: Size, + preferred: Size, + maximum: Size, + baseline: i64, +} + +struct IntrinsicNode { + id: i64, + kind: IntrinsicKind, + metrics: IntrinsicMetrics, + gap: i64, + padding: Insets, + revision: i64, +} + +struct IntrinsicEdge { + parent_id: i64, + child_id: i64, +} + +struct IntrinsicTree { + nodes: ArrayList[IntrinsicNode], + edges: ArrayList[IntrinsicEdge], +} + +struct MeasureResult { + intrinsic: IntrinsicMetrics, + resolved: Size, + baseline: i64, + overflow_width: i64, + overflow_height: i64, + width_rule: MeasureRule, + height_rule: MeasureRule, + visited_nodes: i32, + measure_hash: i64, + error: IntrinsicError, + error_node: i64, + error_edge: i32, +} + +struct IntrinsicSolveState { + states: ArrayList[i32], + values: ArrayList[IntrinsicMetrics], + visited_nodes: i32, + error: IntrinsicError, + error_node: i64, + error_edge: i32, + depth: i32, +} + +struct IntrinsicWitness { + node_id: i64, + revision: i64, + input_hash: i64, +} + +struct IntrinsicStability { + witnesses: ArrayList[IntrinsicWitness], +} + +struct IntrinsicStabilityResult { + error: IntrinsicError, + node_id: i64, + previous_hash: i64, + current_hash: i64, +} + +fn intrinsic_metrics( + min_width: i64, + min_height: i64, + preferred_width: i64, + preferred_height: i64, + max_width: i64, + max_height: i64, + baseline: i64, +) IntrinsicMetrics { + return IntrinsicMetrics{ + .minimum = size(min_width, min_height), + .preferred = size(preferred_width, preferred_height), + .maximum = size(max_width, max_height), + .baseline = baseline, + }; +} + +fn intrinsic_zero() IntrinsicMetrics { + return intrinsic_metrics(0, 0, 0, 0, 0, 0, 0); +} + +fn intrinsic_leaf(id: i64, metrics: IntrinsicMetrics, revision: i64) IntrinsicNode { + return IntrinsicNode{ + .id = id, + .kind = IntrinsicKind.leaf, + .metrics = metrics, + .gap = 0, + .padding = insets_all(0), + .revision = revision, + }; +} + +fn intrinsic_group(id: i64, kind: IntrinsicKind, gap: i64, padding: Insets) IntrinsicNode { + return IntrinsicNode{ + .id = id, + .kind = kind, + .metrics = intrinsic_zero(), + .gap = gap, + .padding = padding, + .revision = 0, + }; +} + +fn intrinsic_tree_make() IntrinsicTree { + return IntrinsicTree{ + .nodes = make[IntrinsicNode](16), + .edges = make[IntrinsicEdge](24), + }; +} + +fn intrinsic_node_limit() i32 { return 4096; } +fn intrinsic_edge_limit() i32 { return 16384; } +fn intrinsic_depth_limit() i32 { return 512; } + +fn intrinsic_tree_free(tree: *IntrinsicTree) void { + free[IntrinsicNode](&tree.*.nodes); + free[IntrinsicEdge](&tree.*.edges); +} + +fn intrinsic_tree_add_node(tree: *IntrinsicTree, node: IntrinsicNode) void { + push[IntrinsicNode](&tree.*.nodes, node); +} + +fn intrinsic_tree_add_child(tree: *IntrinsicTree, parent_id: i64, child_id: i64) void { + push[IntrinsicEdge](&tree.*.edges, IntrinsicEdge{ .parent_id = parent_id, .child_id = child_id }); +} + +fn intrinsic_metric_valid(value: IntrinsicMetrics) i32 { + return (value.minimum.width >= 0 && value.minimum.height >= 0 && + value.preferred.width >= value.minimum.width && + value.preferred.height >= value.minimum.height && + value.maximum.width >= value.preferred.width && + value.maximum.height >= value.preferred.height && + value.maximum.width <= unbounded_extent() && + value.maximum.height <= unbounded_extent() && + value.baseline >= 0 && value.baseline <= value.preferred.height) as i32; +} + +fn intrinsic_node_valid(node: IntrinsicNode) i32 { + if (node.id <= 0 || node.revision < 0 || node.gap < 0 || + node.gap > unbounded_extent() / 4 || + node.padding.top < 0 || node.padding.top > unbounded_extent() / 4 || + node.padding.right < 0 || node.padding.right > unbounded_extent() / 4 || + node.padding.bottom < 0 || node.padding.bottom > unbounded_extent() / 4 || + node.padding.left < 0 || node.padding.left > unbounded_extent() / 4) { return 0; } + if (node.kind == IntrinsicKind.leaf) { return intrinsic_metric_valid(node.metrics); } + return 1; +} + +fn intrinsic_find_node(tree: IntrinsicTree, id: i64) i32 { + let i: i32 = 0; + while (i < tree.nodes.len) { + if (tree.nodes.data[i].id == id) { return i; } + i = i + 1; + } + return 0 - 1; +} + +fn intrinsic_add(a: i64, b: i64) i64 { + if (a < 0 || b < 0) { return 0 - 1; } + if (a > unbounded_extent() - b) { return 0 - 1; } + return a + b; +} + +fn intrinsic_add_maximum(a: i64, b: i64) i64 { + if (a < 0 || b < 0) { return 0 - 1; } + if (a == unbounded_extent() || b == unbounded_extent()) { return unbounded_extent(); } + return intrinsic_add(a, b); +} + +fn intrinsic_max(a: i64, b: i64) i64 { + if (b > a) { return b; } + return a; +} + +fn intrinsic_mix(hash: i64, value: i64) i64 { + return (hash * 1099511628211) ^ value; +} + +fn intrinsic_metrics_hash(value: IntrinsicMetrics) i64 { + let hash: i64 = 17; + hash = intrinsic_mix(hash, value.minimum.width); + hash = intrinsic_mix(hash, value.minimum.height); + hash = intrinsic_mix(hash, value.preferred.width); + hash = intrinsic_mix(hash, value.preferred.height); + hash = intrinsic_mix(hash, value.maximum.width); + hash = intrinsic_mix(hash, value.maximum.height); + hash = intrinsic_mix(hash, value.baseline); + return hash; +} + +fn intrinsic_node_hash(node: IntrinsicNode) i64 { + let hash: i64 = intrinsic_mix(17, node.id); + hash = intrinsic_mix(hash, node.kind as i64); + hash = intrinsic_mix(hash, intrinsic_metrics_hash(node.metrics)); + hash = intrinsic_mix(hash, node.gap); + hash = intrinsic_mix(hash, node.padding.top); + hash = intrinsic_mix(hash, node.padding.right); + hash = intrinsic_mix(hash, node.padding.bottom); + hash = intrinsic_mix(hash, node.padding.left); + return hash; +} + +fn intrinsic_tree_hash(tree: IntrinsicTree) i64 { + let hash: i64 = intrinsic_mix(17, tree.nodes.len as i64); + let i: i32 = 0; + while (i < tree.nodes.len) { + let node: IntrinsicNode = tree.nodes.data[i]; + hash = intrinsic_mix(hash, intrinsic_node_hash(node)); + hash = intrinsic_mix(hash, node.revision); + i = i + 1; + } + hash = intrinsic_mix(hash, tree.edges.len as i64); + i = 0; + while (i < tree.edges.len) { + hash = intrinsic_mix(hash, tree.edges.data[i].parent_id); + hash = intrinsic_mix(hash, tree.edges.data[i].child_id); + i = i + 1; + } + return hash; +} + +fn intrinsic_stability_make() IntrinsicStability { + return IntrinsicStability{ .witnesses = make[IntrinsicWitness](16) }; +} + +fn intrinsic_stability_free(stability: *IntrinsicStability) void { + free[IntrinsicWitness](&stability.*.witnesses); +} + +fn intrinsic_stability_observe( + stability: *IntrinsicStability, + tree: IntrinsicTree, +) IntrinsicStabilityResult { + let i: i32 = 0; + while (i < tree.nodes.len) { + let node: IntrinsicNode = tree.nodes.data[i]; + let duplicate_index: i32 = 0; + while (duplicate_index < i) { + if (tree.nodes.data[duplicate_index].id == node.id) { + return IntrinsicStabilityResult{ + .error = IntrinsicError.duplicate_node, + .node_id = node.id, + .previous_hash = 0, + .current_hash = intrinsic_node_hash(node), + }; + } + duplicate_index = duplicate_index + 1; + } + let input_hash: i64 = intrinsic_node_hash(node); + let found: i32 = 0 - 1; + let j: i32 = 0; + while (j < stability.*.witnesses.len) { + if (stability.*.witnesses.data[j].node_id == node.id) { + found = j; + j = stability.*.witnesses.len; + } else { + j = j + 1; + } + } + if (found >= 0) { + let witness: IntrinsicWitness = stability.*.witnesses.data[found]; + if (node.revision == witness.revision && input_hash != witness.input_hash) { + return IntrinsicStabilityResult{ + .error = IntrinsicError.unstable_measurement, + .node_id = node.id, + .previous_hash = witness.input_hash, + .current_hash = input_hash, + }; + } + if (node.revision < witness.revision) { + return IntrinsicStabilityResult{ + .error = IntrinsicError.invalid_node, + .node_id = node.id, + .previous_hash = witness.input_hash, + .current_hash = input_hash, + }; + } + if (node.revision > witness.revision) { + stability.*.witnesses.data[found].revision = node.revision; + stability.*.witnesses.data[found].input_hash = input_hash; + } + } else { + push[IntrinsicWitness](&stability.*.witnesses, IntrinsicWitness{ + .node_id = node.id, + .revision = node.revision, + .input_hash = input_hash, + }); + } + i = i + 1; + } + return IntrinsicStabilityResult{ + .error = IntrinsicError.none, + .node_id = 0, + .previous_hash = 0, + .current_hash = 0, + }; +} + +fn intrinsic_set_error( + solve: *IntrinsicSolveState, + error: IntrinsicError, + node: i64, + edge: i32, +) void { + if (solve.*.error == IntrinsicError.none) { + solve.*.error = error; + solve.*.error_node = node; + solve.*.error_edge = edge; + } +} + +fn intrinsic_compute( + tree: IntrinsicTree, + node_index: i32, + solve: *IntrinsicSolveState, +) IntrinsicMetrics { + if (solve.*.states.data[node_index] == 2) { return solve.*.values.data[node_index]; } + let node: IntrinsicNode = tree.nodes.data[node_index]; + if (solve.*.states.data[node_index] == 1) { + intrinsic_set_error(solve, IntrinsicError.cycle, node.id, 0 - 1); + return intrinsic_zero(); + } + if (solve.*.depth >= intrinsic_depth_limit()) { + intrinsic_set_error(solve, IntrinsicError.depth_exceeded, node.id, 0 - 1); + return intrinsic_zero(); + } + solve.*.depth = solve.*.depth + 1; + solve.*.states.data[node_index] = 1; + solve.*.visited_nodes = solve.*.visited_nodes + 1; + if (node.kind == IntrinsicKind.leaf) { + solve.*.values.data[node_index] = node.metrics; + solve.*.states.data[node_index] = 2; + solve.*.depth = solve.*.depth - 1; + return node.metrics; + } + + let minimum_width: i64 = 0; + let minimum_height: i64 = 0; + let preferred_width: i64 = 0; + let preferred_height: i64 = 0; + let maximum_width: i64 = 0; + let maximum_height: i64 = 0; + let max_baseline: i64 = 0; + let max_descent: i64 = 0; + let first_baseline: i64 = 0; + let child_count: i32 = 0; + let edge_index: i32 = 0; + while (edge_index < tree.edges.len && solve.*.error == IntrinsicError.none) { + let edge: IntrinsicEdge = tree.edges.data[edge_index]; + if (edge.parent_id == node.id) { + let child_index: i32 = intrinsic_find_node(tree, edge.child_id); + if (child_index < 0) { + intrinsic_set_error(solve, IntrinsicError.missing_child, edge.child_id, edge_index); + return intrinsic_zero(); + } + let child: IntrinsicMetrics = intrinsic_compute(tree, child_index, solve); + if (solve.*.error != IntrinsicError.none) { return intrinsic_zero(); } + if (child_count == 0) { first_baseline = child.baseline; } + + if (node.kind == IntrinsicKind.row) { + if (child_count > 0) { + minimum_width = intrinsic_add(minimum_width, node.gap); + preferred_width = intrinsic_add(preferred_width, node.gap); + maximum_width = intrinsic_add_maximum(maximum_width, node.gap); + } + if (minimum_width >= 0) { minimum_width = intrinsic_add(minimum_width, child.minimum.width); } + if (preferred_width >= 0) { preferred_width = intrinsic_add(preferred_width, child.preferred.width); } + if (maximum_width >= 0) { + maximum_width = intrinsic_add_maximum(maximum_width, child.maximum.width); + } + minimum_height = intrinsic_max(minimum_height, child.minimum.height); + preferred_height = intrinsic_max(preferred_height, child.preferred.height); + maximum_height = intrinsic_max(maximum_height, child.maximum.height); + max_baseline = intrinsic_max(max_baseline, child.baseline); + max_descent = intrinsic_max(max_descent, child.preferred.height - child.baseline); + } else if (node.kind == IntrinsicKind.column) { + minimum_width = intrinsic_max(minimum_width, child.minimum.width); + preferred_width = intrinsic_max(preferred_width, child.preferred.width); + maximum_width = intrinsic_max(maximum_width, child.maximum.width); + if (child_count > 0) { + minimum_height = intrinsic_add(minimum_height, node.gap); + preferred_height = intrinsic_add(preferred_height, node.gap); + maximum_height = intrinsic_add_maximum(maximum_height, node.gap); + } + if (minimum_height >= 0) { minimum_height = intrinsic_add(minimum_height, child.minimum.height); } + if (preferred_height >= 0) { preferred_height = intrinsic_add(preferred_height, child.preferred.height); } + if (maximum_height >= 0) { + maximum_height = intrinsic_add_maximum(maximum_height, child.maximum.height); + } + } else { + minimum_width = intrinsic_max(minimum_width, child.minimum.width); + minimum_height = intrinsic_max(minimum_height, child.minimum.height); + preferred_width = intrinsic_max(preferred_width, child.preferred.width); + preferred_height = intrinsic_max(preferred_height, child.preferred.height); + maximum_width = intrinsic_max(maximum_width, child.maximum.width); + maximum_height = intrinsic_max(maximum_height, child.maximum.height); + max_baseline = intrinsic_max(max_baseline, child.baseline); + } + if (minimum_width < 0 || minimum_height < 0 || preferred_width < 0 || + preferred_height < 0 || maximum_width < 0 || maximum_height < 0) { + intrinsic_set_error(solve, IntrinsicError.arithmetic_overflow, node.id, edge_index); + return intrinsic_zero(); + } + child_count = child_count + 1; + } + edge_index = edge_index + 1; + } + + if (node.kind == IntrinsicKind.row) { + let baseline_extent: i64 = intrinsic_add(max_baseline, max_descent); + if (baseline_extent < 0) { + intrinsic_set_error(solve, IntrinsicError.arithmetic_overflow, node.id, 0 - 1); + return intrinsic_zero(); + } + preferred_height = intrinsic_max(preferred_height, baseline_extent); + } + + let horizontal: i64 = intrinsic_add(node.padding.left, node.padding.right); + let vertical: i64 = intrinsic_add(node.padding.top, node.padding.bottom); + if (horizontal < 0 || vertical < 0) { + intrinsic_set_error(solve, IntrinsicError.arithmetic_overflow, node.id, 0 - 1); + return intrinsic_zero(); + } + minimum_width = intrinsic_add(minimum_width, horizontal); + preferred_width = intrinsic_add(preferred_width, horizontal); + maximum_width = intrinsic_add_maximum(maximum_width, horizontal); + minimum_height = intrinsic_add(minimum_height, vertical); + preferred_height = intrinsic_add(preferred_height, vertical); + maximum_height = intrinsic_add_maximum(maximum_height, vertical); + if (minimum_width < 0 || minimum_height < 0 || preferred_width < 0 || + preferred_height < 0 || maximum_width < 0 || maximum_height < 0) { + intrinsic_set_error(solve, IntrinsicError.arithmetic_overflow, node.id, 0 - 1); + return intrinsic_zero(); + } + + let baseline: i64 = node.padding.top; + if (child_count > 0) { + if (node.kind == IntrinsicKind.row || node.kind == IntrinsicKind.overlay) { + baseline = intrinsic_add(node.padding.top, max_baseline); + } else { + baseline = intrinsic_add(node.padding.top, first_baseline); + } + } + if (baseline < 0) { + intrinsic_set_error(solve, IntrinsicError.arithmetic_overflow, node.id, 0 - 1); + return intrinsic_zero(); + } + let result: IntrinsicMetrics = intrinsic_metrics( + minimum_width, minimum_height, + preferred_width, preferred_height, + maximum_width, maximum_height, + baseline + ); + if (intrinsic_metric_valid(result) == 0) { + intrinsic_set_error(solve, IntrinsicError.invalid_node, node.id, 0 - 1); + return intrinsic_zero(); + } + solve.*.values.data[node_index] = result; + solve.*.states.data[node_index] = 2; + solve.*.depth = solve.*.depth - 1; + return result; +} + +fn measure_error_result(error: IntrinsicError, node: i64, edge: i32) MeasureResult { + let result: MeasureResult = MeasureResult{ + .intrinsic = intrinsic_zero(), + .resolved = size(0, 0), + .baseline = 0, + .overflow_width = 0, + .overflow_height = 0, + .width_rule = MeasureRule.intrinsic_preferred, + .height_rule = MeasureRule.intrinsic_preferred, + .visited_nodes = 0, + .measure_hash = 17, + .error = error, + .error_node = node, + .error_edge = edge, + }; + result.measure_hash = intrinsic_mix(result.measure_hash, error as i64); + result.measure_hash = intrinsic_mix(result.measure_hash, node); + result.measure_hash = intrinsic_mix(result.measure_hash, edge as i64); + return result; +} + +fn measure_rule(preferred: i64, minimum: i64, maximum: i64) MeasureRule { + if (preferred < minimum) { return MeasureRule.minimum_constraint; } + if (preferred > maximum) { return MeasureRule.maximum_constraint; } + return MeasureRule.intrinsic_preferred; +} + +fn intrinsic_tree_measure(tree: IntrinsicTree, root_id: i64, raw_constraints: Constraints) MeasureResult { + if (tree.nodes.len > intrinsic_node_limit() || tree.edges.len > intrinsic_edge_limit()) { + return measure_error_result(IntrinsicError.too_many_nodes, root_id, 0 - 1); + } + let constraints: Constraints = normalize_constraints(raw_constraints); + if (constraints.min_width > unbounded_extent() || constraints.max_width > unbounded_extent() || + constraints.min_height > unbounded_extent() || constraints.max_height > unbounded_extent()) { + return measure_error_result(IntrinsicError.invalid_constraints, root_id, 0 - 1); + } + let root_index: i32 = intrinsic_find_node(tree, root_id); + if (root_index < 0) { return measure_error_result(IntrinsicError.missing_node, root_id, 0 - 1); } + + let i: i32 = 0; + while (i < tree.nodes.len) { + if (intrinsic_node_valid(tree.nodes.data[i]) == 0) { + return measure_error_result(IntrinsicError.invalid_node, tree.nodes.data[i].id, 0 - 1); + } + let j: i32 = i + 1; + while (j < tree.nodes.len) { + if (tree.nodes.data[i].id == tree.nodes.data[j].id) { + return measure_error_result(IntrinsicError.duplicate_node, tree.nodes.data[j].id, j); + } + j = j + 1; + } + i = i + 1; + } + let edge_index: i32 = 0; + while (edge_index < tree.edges.len) { + let edge: IntrinsicEdge = tree.edges.data[edge_index]; + let parent_index: i32 = intrinsic_find_node(tree, edge.parent_id); + let child_index: i32 = intrinsic_find_node(tree, edge.child_id); + if (parent_index < 0) { + return measure_error_result(IntrinsicError.missing_node, edge.parent_id, edge_index); + } + if (child_index < 0) { + return measure_error_result(IntrinsicError.missing_child, edge.child_id, edge_index); + } + if (tree.nodes.data[parent_index].kind == IntrinsicKind.leaf) { + return measure_error_result(IntrinsicError.invalid_edge, edge.parent_id, edge_index); + } + if (edge.child_id == root_id) { + return measure_error_result(IntrinsicError.cycle, root_id, edge_index); + } + let prior: i32 = 0; + while (prior < edge_index) { + if (tree.edges.data[prior].parent_id == edge.parent_id && + tree.edges.data[prior].child_id == edge.child_id) { + return measure_error_result(IntrinsicError.invalid_edge, edge.child_id, edge_index); + } + if (tree.edges.data[prior].child_id == edge.child_id) { + return measure_error_result(IntrinsicError.multiple_parents, edge.child_id, edge_index); + } + prior = prior + 1; + } + edge_index = edge_index + 1; + } + + let solve: IntrinsicSolveState = IntrinsicSolveState{ + .states = make[i32](tree.nodes.len), + .values = make[IntrinsicMetrics](tree.nodes.len), + .visited_nodes = 0, + .error = IntrinsicError.none, + .error_node = 0, + .error_edge = 0 - 1, + .depth = 0, + }; + i = 0; + while (i < tree.nodes.len) { + push[i32](&solve.states, 0); + push[IntrinsicMetrics](&solve.values, intrinsic_zero()); + i = i + 1; + } + let metrics: IntrinsicMetrics = intrinsic_compute(tree, root_index, &solve); + if (solve.error != IntrinsicError.none) { + let failed: MeasureResult = measure_error_result(solve.error, solve.error_node, solve.error_edge); + failed.visited_nodes = solve.visited_nodes; + failed.measure_hash = intrinsic_mix(failed.measure_hash, failed.visited_nodes as i64); + free[i32](&solve.states); + free[IntrinsicMetrics](&solve.values); + return failed; + } + if (solve.visited_nodes != tree.nodes.len) { + i = 0; + while (i < solve.states.len && solve.states.data[i] != 0) { i = i + 1; } + let orphan_id: i64 = 0; + if (i < tree.nodes.len) { orphan_id = tree.nodes.data[i].id; } + let failed: MeasureResult = measure_error_result(IntrinsicError.orphan_node, orphan_id, 0 - 1); + failed.visited_nodes = solve.visited_nodes; + failed.measure_hash = intrinsic_mix(failed.measure_hash, failed.visited_nodes as i64); + free[i32](&solve.states); + free[IntrinsicMetrics](&solve.values); + return failed; + } + + let resolved: Size = size( + clamp_i64(metrics.preferred.width, constraints.min_width, constraints.max_width), + clamp_i64(metrics.preferred.height, constraints.min_height, constraints.max_height) + ); + let overflow_width: i64 = metrics.minimum.width - resolved.width; + if (overflow_width < 0) { overflow_width = 0; } + let overflow_height: i64 = metrics.minimum.height - resolved.height; + if (overflow_height < 0) { overflow_height = 0; } + let result: MeasureResult = MeasureResult{ + .intrinsic = metrics, + .resolved = resolved, + .baseline = clamp_i64(metrics.baseline, 0, resolved.height), + .overflow_width = overflow_width, + .overflow_height = overflow_height, + .width_rule = measure_rule(metrics.preferred.width, constraints.min_width, constraints.max_width), + .height_rule = measure_rule(metrics.preferred.height, constraints.min_height, constraints.max_height), + .visited_nodes = solve.visited_nodes, + .measure_hash = intrinsic_mix(intrinsic_tree_hash(tree), intrinsic_metrics_hash(metrics)), + .error = IntrinsicError.none, + .error_node = 0, + .error_edge = 0 - 1, + }; + result.measure_hash = intrinsic_mix(result.measure_hash, resolved.width); + result.measure_hash = intrinsic_mix(result.measure_hash, resolved.height); + result.measure_hash = intrinsic_mix(result.measure_hash, result.baseline); + result.measure_hash = intrinsic_mix(result.measure_hash, result.overflow_width); + result.measure_hash = intrinsic_mix(result.measure_hash, result.overflow_height); + result.measure_hash = intrinsic_mix(result.measure_hash, result.width_rule as i64); + result.measure_hash = intrinsic_mix(result.measure_hash, result.height_rule as i64); + result.measure_hash = intrinsic_mix(result.measure_hash, result.visited_nodes as i64); + result.measure_hash = intrinsic_mix(result.measure_hash, constraints.min_width); + result.measure_hash = intrinsic_mix(result.measure_hash, constraints.max_width); + result.measure_hash = intrinsic_mix(result.measure_hash, constraints.min_height); + result.measure_hash = intrinsic_mix(result.measure_hash, constraints.max_height); + free[i32](&solve.states); + free[IntrinsicMetrics](&solve.values); + return result; +} diff --git a/src/layout/overlay.zag b/src/layout/overlay.zag new file mode 100644 index 0000000..1c7f684 --- /dev/null +++ b/src/layout/overlay.zag @@ -0,0 +1,322 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("common.zag") +@import("measure.zag") + +enum OverlayError { + none, + too_many_items, + invalid_constraints, + invalid_style, + invalid_item, + duplicate_id, + arithmetic_overflow, +} + +struct OverlayItem { + id: i64, + metrics: IntrinsicMetrics, + horizontal: PlacementAlignment, + vertical: PlacementAlignment, + insets: Insets, + z_order: i64, +} + +struct OverlayStyle { + padding: Insets, + direction: LayoutDirection, +} + +struct OverlayPlacement { + id: i64, + frame: Rect, + baseline: i64, + z_order: i64, +} + +struct OverlayResult { + placements: ArrayList[OverlayPlacement], + container: Size, + overflow_width: i64, + overflow_height: i64, + layout_hash: i64, + error: OverlayError, + error_index: i32, +} + +fn overlay_item(id: i64, metrics: IntrinsicMetrics) OverlayItem { + return OverlayItem{ + .id = id, + .metrics = metrics, + .horizontal = PlacementAlignment.start, + .vertical = PlacementAlignment.start, + .insets = insets_all(0), + .z_order = 0, + }; +} + +fn overlay_style(padding: Insets, direction: LayoutDirection) OverlayStyle { + return OverlayStyle{ .padding = padding, .direction = direction }; +} + +fn overlay_item_limit() i32 { return 4096; } + +fn overlay_free(result: *OverlayResult) void { + free[OverlayPlacement](&result.*.placements); +} + +fn overlay_mix(hash: i64, value: i64) i64 { + return (hash * 1099511628211) ^ value; +} + +fn overlay_error_result(error: OverlayError, index: i32) OverlayResult { + let result: OverlayResult = OverlayResult{ + .placements = make[OverlayPlacement](1), + .container = size(0, 0), + .overflow_width = 0, + .overflow_height = 0, + .layout_hash = 17, + .error = error, + .error_index = index, + }; + result.layout_hash = overlay_mix(result.layout_hash, error as i64); + result.layout_hash = overlay_mix(result.layout_hash, index as i64); + return result; +} + +fn overlay_extent_valid(value: i64) i32 { + return (value >= 0 && value <= unbounded_extent() / 4) as i32; +} + +fn overlay_insets_valid(value: Insets) i32 { + return (overlay_extent_valid(value.top) == 1 && overlay_extent_valid(value.right) == 1 && + overlay_extent_valid(value.bottom) == 1 && overlay_extent_valid(value.left) == 1) as i32; +} + +fn overlay_item_valid(item: OverlayItem) i32 { + if (item.id <= 0 || intrinsic_metric_valid(item.metrics) == 0 || + overlay_insets_valid(item.insets) == 0) { return 0; } + if (item.horizontal == PlacementAlignment.baseline || + item.vertical == PlacementAlignment.baseline) { return 0; } + return 1; +} + +fn overlay_checked_add(a: i64, b: i64) i64 { + if (a < 0 || b < 0 || a > unbounded_extent() - b) { return 0 - 1; } + return a + b; +} + +fn overlay_axis_size( + minimum: i64, + preferred: i64, + maximum: i64, + available: i64, + alignment: PlacementAlignment, +) i64 { + if (alignment == PlacementAlignment.stretch) { + return clamp_i64(available, minimum, maximum); + } + let upper: i64 = maximum; + if (available < upper) { upper = available; } + return clamp_i64(preferred, minimum, upper); +} + +fn overlay_axis_offset( + available: i64, + extent: i64, + alignment: PlacementAlignment, + reverse: i32, +) i64 { + let remaining: i64 = available - extent; + if (alignment == PlacementAlignment.center) { return remaining / 2; } + if (alignment == PlacementAlignment.end) { + if (reverse != 0) { return 0; } + return remaining; + } + if (alignment == PlacementAlignment.start) { + if (reverse != 0) { return remaining; } + return 0; + } + return 0; +} + +fn overlay_hash(result: OverlayResult) i64 { + let hash: i64 = overlay_mix(overlay_mix(17, result.container.width), result.container.height); + hash = overlay_mix(hash, result.overflow_width); + hash = overlay_mix(hash, result.overflow_height); + hash = overlay_mix(hash, result.error as i64); + hash = overlay_mix(hash, result.error_index as i64); + hash = overlay_mix(hash, result.placements.len as i64); + let i: i32 = 0; + while (i < result.placements.len) { + let placement: OverlayPlacement = result.placements.data[i]; + hash = overlay_mix(hash, placement.id); + hash = overlay_mix(hash, placement.frame.x); + hash = overlay_mix(hash, placement.frame.y); + hash = overlay_mix(hash, placement.frame.width); + hash = overlay_mix(hash, placement.frame.height); + hash = overlay_mix(hash, placement.baseline); + hash = overlay_mix(hash, placement.z_order); + i = i + 1; + } + return hash; +} + +fn overlay_input_hash( + items: ArrayList[OverlayItem], + constraints: Constraints, + style: OverlayStyle, +) i64 { + let hash: i64 = overlay_mix(17, items.len as i64); + hash = overlay_mix(hash, constraints.min_width); + hash = overlay_mix(hash, constraints.max_width); + hash = overlay_mix(hash, constraints.min_height); + hash = overlay_mix(hash, constraints.max_height); + hash = overlay_mix(hash, style.padding.top); + hash = overlay_mix(hash, style.padding.right); + hash = overlay_mix(hash, style.padding.bottom); + hash = overlay_mix(hash, style.padding.left); + hash = overlay_mix(hash, style.direction as i64); + let i: i32 = 0; + while (i < items.len) { + let item: OverlayItem = items.data[i]; + hash = overlay_mix(hash, item.id); + hash = overlay_mix(hash, intrinsic_metrics_hash(item.metrics)); + hash = overlay_mix(hash, item.horizontal as i64); + hash = overlay_mix(hash, item.vertical as i64); + hash = overlay_mix(hash, item.insets.top); + hash = overlay_mix(hash, item.insets.right); + hash = overlay_mix(hash, item.insets.bottom); + hash = overlay_mix(hash, item.insets.left); + hash = overlay_mix(hash, item.z_order); + i = i + 1; + } + return hash; +} + +fn overlay_layout( + items: ArrayList[OverlayItem], + raw_constraints: Constraints, + style: OverlayStyle, +) OverlayResult { + if (items.len > overlay_item_limit()) { + return overlay_error_result(OverlayError.too_many_items, 0 - 1); + } + let constraints: Constraints = normalize_constraints(raw_constraints); + if (constraints.min_width > unbounded_extent() || constraints.max_width > unbounded_extent() || + constraints.min_height > unbounded_extent() || constraints.max_height > unbounded_extent()) { + return overlay_error_result(OverlayError.invalid_constraints, 0 - 1); + } + if (overlay_insets_valid(style.padding) == 0) { + return overlay_error_result(OverlayError.invalid_style, 0 - 1); + } + let i: i32 = 0; + while (i < items.len) { + if (overlay_item_valid(items.data[i]) == 0) { + return overlay_error_result(OverlayError.invalid_item, i); + } + let j: i32 = 0; + while (j < i) { + if (items.data[j].id == items.data[i].id) { + return overlay_error_result(OverlayError.duplicate_id, i); + } + j = j + 1; + } + i = i + 1; + } + + let horizontal_padding: i64 = overlay_checked_add(style.padding.left, style.padding.right); + let vertical_padding: i64 = overlay_checked_add(style.padding.top, style.padding.bottom); + if (horizontal_padding < 0 || vertical_padding < 0) { + return overlay_error_result(OverlayError.arithmetic_overflow, 0 - 1); + } + let desired_width: i64 = horizontal_padding; + let desired_height: i64 = vertical_padding; + i = 0; + while (i < items.len) { + let item: OverlayItem = items.data[i]; + let item_horizontal: i64 = overlay_checked_add(item.insets.left, item.insets.right); + let item_vertical: i64 = overlay_checked_add(item.insets.top, item.insets.bottom); + let width: i64 = overlay_checked_add(item.metrics.preferred.width, item_horizontal); + let height: i64 = overlay_checked_add(item.metrics.preferred.height, item_vertical); + if (width < 0 || height < 0) { + return overlay_error_result(OverlayError.arithmetic_overflow, i); + } + let outer_width: i64 = overlay_checked_add(horizontal_padding, width); + let outer_height: i64 = overlay_checked_add(vertical_padding, height); + if (outer_width < 0 || outer_height < 0) { + return overlay_error_result(OverlayError.arithmetic_overflow, i); + } + if (outer_width > desired_width) { desired_width = outer_width; } + if (outer_height > desired_height) { desired_height = outer_height; } + i = i + 1; + } + let container: Size = size( + clamp_i64(desired_width, constraints.min_width, constraints.max_width), + clamp_i64(desired_height, constraints.min_height, constraints.max_height) + ); + let content_width: i64 = container.width - horizontal_padding; + let content_height: i64 = container.height - vertical_padding; + if (content_width < 0) { content_width = 0; } + if (content_height < 0) { content_height = 0; } + + let placements: ArrayList[OverlayPlacement] = make[OverlayPlacement](items.len); + let overflow_width: i64 = horizontal_padding - container.width; + if (overflow_width < 0) { overflow_width = 0; } + let overflow_height: i64 = vertical_padding - container.height; + if (overflow_height < 0) { overflow_height = 0; } + i = 0; + while (i < items.len) { + let item: OverlayItem = items.data[i]; + let item_horizontal: i64 = item.insets.left + item.insets.right; + let item_vertical: i64 = item.insets.top + item.insets.bottom; + let available_width: i64 = content_width - item_horizontal; + let available_height: i64 = content_height - item_vertical; + if (available_width < 0) { available_width = 0; } + if (available_height < 0) { available_height = 0; } + let width: i64 = overlay_axis_size( + item.metrics.minimum.width, item.metrics.preferred.width, + item.metrics.maximum.width, available_width, item.horizontal + ); + let height: i64 = overlay_axis_size( + item.metrics.minimum.height, item.metrics.preferred.height, + item.metrics.maximum.height, available_height, item.vertical + ); + let x_offset: i64 = overlay_axis_offset( + available_width, width, item.horizontal, + (style.direction == LayoutDirection.rtl) as i32 + ); + let y_offset: i64 = 0; + if (item.vertical == PlacementAlignment.center) { y_offset = (available_height - height) / 2; } + else if (item.vertical == PlacementAlignment.end) { y_offset = available_height - height; } + let frame: Rect = rect( + style.padding.left + item.insets.left + x_offset, + style.padding.top + item.insets.top + y_offset, + width, + height + ); + let item_overflow_width: i64 = width - available_width; + if (item_overflow_width > overflow_width) { overflow_width = item_overflow_width; } + let item_overflow_height: i64 = height - available_height; + if (item_overflow_height > overflow_height) { overflow_height = item_overflow_height; } + push[OverlayPlacement](&placements, OverlayPlacement{ + .id = item.id, + .frame = frame, + .baseline = frame.y + clamp_i64(item.metrics.baseline, 0, frame.height), + .z_order = item.z_order, + }); + i = i + 1; + } + + let result: OverlayResult = OverlayResult{ + .placements = placements, + .container = container, + .overflow_width = overflow_width, + .overflow_height = overflow_height, + .layout_hash = 0, + .error = OverlayError.none, + .error_index = 0 - 1, + }; + result.layout_hash = overlay_mix(overlay_input_hash(items, constraints, style), overlay_hash(result)); + return result; +} diff --git a/src/layout/scroll.zag b/src/layout/scroll.zag new file mode 100644 index 0000000..ea9fa04 --- /dev/null +++ b/src/layout/scroll.zag @@ -0,0 +1,240 @@ +@import("../core/geometry.zag") +@import("common.zag") + +enum ScrollError { + none, + invalid_viewport, + invalid_content, + invalid_offset, + invalid_target, +} + +enum ScrollAlignment { nearest, start, center, end } + +struct ScrollState { + viewport: Size, + content: Size, + offset_x: i64, + offset_y: i64, + revision: i64, +} + +struct ScrollAnchor { + content_x: i64, + content_y: i64, + viewport_x: i64, + viewport_y: i64, + preserve_x: i32, + preserve_y: i32, +} + +struct ScrollMutation { + old_offset_x: i64, + old_offset_y: i64, + offset_x: i64, + offset_y: i64, + consumed_x: i64, + consumed_y: i64, + unconsumed_x: i64, + unconsumed_y: i64, + revision: i64, + changed: i32, + mutation_hash: i64, + error: ScrollError, +} + +fn scroll_mix(hash: i64, value: i64) i64 { return (hash * 1099511628211) ^ value; } + +fn scroll_size_valid(value: Size) i32 { + return (value.width >= 0 && value.height >= 0 && + value.width <= unbounded_extent() && value.height <= unbounded_extent()) as i32; +} + +fn scroll_state(viewport: Size, content: Size) ScrollState { + return ScrollState{ + .viewport = viewport, .content = content, + .offset_x = 0, .offset_y = 0, .revision = 0, + }; +} + +fn scroll_max_x(state: ScrollState) i64 { + let value: i64 = state.content.width - state.viewport.width; + if (value < 0) { return 0; } + return value; +} + +fn scroll_max_y(state: ScrollState) i64 { + let value: i64 = state.content.height - state.viewport.height; + if (value < 0) { return 0; } + return value; +} + +fn scroll_state_valid(state: ScrollState) ScrollError { + if (scroll_size_valid(state.viewport) == 0) { return ScrollError.invalid_viewport; } + if (scroll_size_valid(state.content) == 0) { return ScrollError.invalid_content; } + if (state.revision < 0 || state.offset_x < 0 || state.offset_y < 0 || + state.offset_x > scroll_max_x(state) || state.offset_y > scroll_max_y(state)) { + return ScrollError.invalid_offset; + } + return ScrollError.none; +} + +fn scroll_state_hash(state: ScrollState) i64 { + let hash: i64 = scroll_mix(scroll_mix(17, state.viewport.width), state.viewport.height); + hash = scroll_mix(hash, state.content.width); hash = scroll_mix(hash, state.content.height); + hash = scroll_mix(hash, state.offset_x); hash = scroll_mix(hash, state.offset_y); + return scroll_mix(hash, state.revision); +} + +fn scroll_mutation_hash(result: ScrollMutation) i64 { + let hash: i64 = scroll_mix(scroll_mix(17, result.old_offset_x), result.old_offset_y); + hash = scroll_mix(hash, result.offset_x); hash = scroll_mix(hash, result.offset_y); + hash = scroll_mix(hash, result.consumed_x); hash = scroll_mix(hash, result.consumed_y); + hash = scroll_mix(hash, result.unconsumed_x); hash = scroll_mix(hash, result.unconsumed_y); + hash = scroll_mix(hash, result.revision); hash = scroll_mix(hash, result.changed as i64); + return scroll_mix(hash, result.error as i64); +} + +fn scroll_error_mutation(state: ScrollState, error: ScrollError) ScrollMutation { + let result: ScrollMutation = ScrollMutation{ + .old_offset_x = state.offset_x, .old_offset_y = state.offset_y, + .offset_x = state.offset_x, .offset_y = state.offset_y, + .consumed_x = 0, .consumed_y = 0, .unconsumed_x = 0, .unconsumed_y = 0, + .revision = state.revision, .changed = 0, .mutation_hash = 0, .error = error, + }; + result.mutation_hash = scroll_mutation_hash(result); + return result; +} + +fn scroll_apply(state: *ScrollState, raw_x: i64, raw_y: i64, requested_x: i64, + requested_y: i64) ScrollMutation { + let validation: ScrollError = scroll_state_valid(state.*); + if (validation != ScrollError.none) { return scroll_error_mutation(state.*, validation); } + if (raw_x < 0 - unbounded_extent() || raw_x > unbounded_extent() * 2 || + raw_y < 0 - unbounded_extent() || raw_y > unbounded_extent() * 2 || + requested_x < 0 - unbounded_extent() || requested_x > unbounded_extent() || + requested_y < 0 - unbounded_extent() || requested_y > unbounded_extent()) { + return scroll_error_mutation(state.*, ScrollError.invalid_offset); + } + let old_x: i64 = state.*.offset_x; + let old_y: i64 = state.*.offset_y; + let next_x: i64 = clamp_i64(raw_x, 0, scroll_max_x(state.*)); + let next_y: i64 = clamp_i64(raw_y, 0, scroll_max_y(state.*)); + let changed: i32 = (next_x != old_x || next_y != old_y) as i32; + if (changed != 0) { state.*.revision = state.*.revision + 1; } + state.*.offset_x = next_x; state.*.offset_y = next_y; + let consumed_x: i64 = next_x - old_x; + let consumed_y: i64 = next_y - old_y; + let result: ScrollMutation = ScrollMutation{ + .old_offset_x = old_x, .old_offset_y = old_y, + .offset_x = next_x, .offset_y = next_y, + .consumed_x = consumed_x, .consumed_y = consumed_y, + .unconsumed_x = requested_x - consumed_x, + .unconsumed_y = requested_y - consumed_y, + .revision = state.*.revision, .changed = changed, + .mutation_hash = 0, .error = ScrollError.none, + }; + result.mutation_hash = scroll_mutation_hash(result); + return result; +} + +fn scroll_to(state: *ScrollState, x: i64, y: i64) ScrollMutation { + return scroll_apply(state, x, y, x - state.*.offset_x, y - state.*.offset_y); +} + +fn scroll_by(state: *ScrollState, delta_x: i64, delta_y: i64) ScrollMutation { + if (delta_x < 0 - unbounded_extent() || delta_x > unbounded_extent() || + delta_y < 0 - unbounded_extent() || delta_y > unbounded_extent()) { + return scroll_error_mutation(state.*, ScrollError.invalid_offset); + } + return scroll_apply(state, state.*.offset_x + delta_x, state.*.offset_y + delta_y, + delta_x, delta_y); +} + +fn scroll_anchor(content_x: i64, content_y: i64, viewport_x: i64, viewport_y: i64) ScrollAnchor { + return ScrollAnchor{ + .content_x = content_x, .content_y = content_y, + .viewport_x = viewport_x, .viewport_y = viewport_y, + .preserve_x = 1, .preserve_y = 1, + }; +} + +fn scroll_reconcile(state: *ScrollState, viewport: Size, content: Size, + anchor: ScrollAnchor) ScrollMutation { + let old: ScrollState = state.*; + let old_validation: ScrollError = scroll_state_valid(old); + if (old_validation != ScrollError.none) { return scroll_error_mutation(old, old_validation); } + if (scroll_size_valid(viewport) == 0) { return scroll_error_mutation(old, ScrollError.invalid_viewport); } + if (scroll_size_valid(content) == 0) { return scroll_error_mutation(old, ScrollError.invalid_content); } + if ((anchor.preserve_x != 0 && anchor.preserve_x != 1) || + (anchor.preserve_y != 0 && anchor.preserve_y != 1) || + anchor.content_x < 0 || anchor.content_y < 0 || + anchor.viewport_x < 0 - unbounded_extent() || anchor.viewport_x > unbounded_extent() || + anchor.viewport_y < 0 - unbounded_extent() || anchor.viewport_y > unbounded_extent()) { + return scroll_error_mutation(old, ScrollError.invalid_target); + } + state.*.viewport = viewport; state.*.content = content; + let raw_x: i64 = old.offset_x; + let raw_y: i64 = old.offset_y; + if (anchor.preserve_x != 0) { raw_x = anchor.content_x - anchor.viewport_x; } + if (anchor.preserve_y != 0) { raw_y = anchor.content_y - anchor.viewport_y; } + state.*.offset_x = clamp_i64(raw_x, 0, scroll_max_x(state.*)); + state.*.offset_y = clamp_i64(raw_y, 0, scroll_max_y(state.*)); + let changed: i32 = (state.*.viewport.width != old.viewport.width || + state.*.viewport.height != old.viewport.height || + state.*.content.width != old.content.width || + state.*.content.height != old.content.height || + state.*.offset_x != old.offset_x || state.*.offset_y != old.offset_y) as i32; + if (changed != 0) { state.*.revision = old.revision + 1; } + let result: ScrollMutation = ScrollMutation{ + .old_offset_x = old.offset_x, .old_offset_y = old.offset_y, + .offset_x = state.*.offset_x, .offset_y = state.*.offset_y, + .consumed_x = state.*.offset_x - old.offset_x, + .consumed_y = state.*.offset_y - old.offset_y, + .unconsumed_x = 0, .unconsumed_y = 0, + .revision = state.*.revision, .changed = changed, + .mutation_hash = 0, .error = ScrollError.none, + }; + result.mutation_hash = scroll_mutation_hash(result); + return result; +} + +fn scroll_axis_reveal(current: i64, viewport: i64, target_start: i64, + target_extent: i64, alignment: ScrollAlignment) i64 { + if (alignment == ScrollAlignment.start) { return target_start; } + if (alignment == ScrollAlignment.center) { + return target_start + target_extent / 2 - viewport / 2; + } + if (alignment == ScrollAlignment.end) { return target_start + target_extent - viewport; } + if (target_extent >= viewport) { return target_start; } + if (target_start < current) { return target_start; } + if (target_start + target_extent > current + viewport) { + return target_start + target_extent - viewport; + } + return current; +} + +fn scroll_reveal(state: *ScrollState, target: Rect, horizontal: ScrollAlignment, + vertical: ScrollAlignment) ScrollMutation { + if (target.x < 0 || target.y < 0 || target.width < 0 || target.height < 0 || + target.x > unbounded_extent() - target.width || + target.y > unbounded_extent() - target.height) { + return scroll_error_mutation(state.*, ScrollError.invalid_target); + } + let raw_x: i64 = scroll_axis_reveal(state.*.offset_x, state.*.viewport.width, + target.x, target.width, horizontal); + let raw_y: i64 = scroll_axis_reveal(state.*.offset_y, state.*.viewport.height, + target.y, target.height, vertical); + return scroll_apply(state, raw_x, raw_y, + raw_x - state.*.offset_x, raw_y - state.*.offset_y); +} + +fn scroll_visible_rect(state: ScrollState, direction: LayoutDirection) Rect { + let width: i64 = state.viewport.width; + if (state.content.width < width) { width = state.content.width; } + let height: i64 = state.viewport.height; + if (state.content.height < height) { height = state.content.height; } + let x: i64 = state.offset_x; + if (direction == LayoutDirection.rtl) { x = scroll_max_x(state) - state.offset_x; } + return rect(x, state.offset_y, width, height); +} diff --git a/src/layout/virtual_list.zag b/src/layout/virtual_list.zag new file mode 100644 index 0000000..41417e1 --- /dev/null +++ b/src/layout/virtual_list.zag @@ -0,0 +1,439 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("common.zag") + +enum VirtualAxis { vertical, horizontal } +enum VirtualListError { + none, + invalid_model, + invalid_override, + invalid_viewport, + invalid_scroll, + invalid_style, + invalid_anchor, + arithmetic_overflow, + live_limit_exceeded, +} + +struct VirtualExtentOverride { + index: i64, + extent: i64, + revision: i64, +} + +struct VirtualListModel { + count: i64, + id_base: i64, + default_extent: i64, + gap: i64, + revision: i64, + overrides: ArrayList[VirtualExtentOverride], +} + +struct VirtualListStyle { + axis: VirtualAxis, + direction: LayoutDirection, + padding: Insets, + overscan_before: i32, + overscan_after: i32, + max_live_items: i32, +} + +struct VirtualPlacement { + id: i64, + index: i64, + frame: Rect, + extent_revision: i64, +} + +struct VirtualAnchor { + id: i64, + index: i64, + viewport_offset: i64, +} + +struct VirtualAnchorResolution { + scroll_offset: i64, + changed: i32, + error: VirtualListError, +} + +struct VirtualListResult { + placements: ArrayList[VirtualPlacement], + viewport: Size, + content: Size, + scroll_offset: i64, + max_scroll: i64, + first_visible: i64, + last_visible: i64, + first_live: i64, + last_live: i64, + semantic_count: i64, + examined_items: i32, + layout_hash: i64, + error: VirtualListError, + error_index: i64, +} + +fn virtual_override(index: i64, extent: i64, revision: i64) VirtualExtentOverride { + return VirtualExtentOverride{ .index = index, .extent = extent, .revision = revision }; +} + +fn virtual_model(count: i64, id_base: i64, default_extent: i64, + gap: i64, revision: i64, overrides: ArrayList[VirtualExtentOverride]) VirtualListModel { + return VirtualListModel{ + .count = count, .id_base = id_base, .default_extent = default_extent, + .gap = gap, .revision = revision, .overrides = overrides, + }; +} + +fn virtual_style(axis: VirtualAxis, padding: Insets) VirtualListStyle { + return VirtualListStyle{ + .axis = axis, .direction = LayoutDirection.ltr, .padding = padding, + .overscan_before = 2, .overscan_after = 2, .max_live_items = 512, + }; +} + +fn virtual_free(result: *VirtualListResult) void { + free[VirtualPlacement](&result.*.placements); +} + +fn virtual_mix(hash: i64, value: i64) i64 { return (hash * 1099511628211) ^ value; } + +fn virtual_error_result(error: VirtualListError, index: i64) VirtualListResult { + let result: VirtualListResult = VirtualListResult{ + .placements = make[VirtualPlacement](1), + .viewport = size(0, 0), .content = size(0, 0), + .scroll_offset = 0, .max_scroll = 0, + .first_visible = 0 - 1, .last_visible = 0 - 1, + .first_live = 0 - 1, .last_live = 0 - 1, + .semantic_count = 0, .examined_items = 0, + .layout_hash = 17, .error = error, .error_index = index, + }; + result.layout_hash = virtual_mix(virtual_mix(17, error as i64), index); + return result; +} + +fn virtual_extent_valid(value: i64) i32 { + return (value >= 0 && value <= unbounded_extent() / 4) as i32; +} + +fn virtual_insets_valid(value: Insets) i32 { + return (virtual_extent_valid(value.top) == 1 && virtual_extent_valid(value.right) == 1 && + virtual_extent_valid(value.bottom) == 1 && virtual_extent_valid(value.left) == 1) as i32; +} + +fn virtual_model_error(model: VirtualListModel) VirtualListError { + if (model.count < 0 || model.count > 1000000000 || model.id_base <= 0 || + model.default_extent <= 0 || model.default_extent > unbounded_extent() / 4 || + model.gap < 0 || model.gap > unbounded_extent() / 4 || + model.revision < 0 || model.overrides.len > 4096 || + (model.count > 0 && model.id_base > unbounded_extent() - model.count)) { + return VirtualListError.invalid_model; + } + let i: i32 = 0; + while (i < model.overrides.len) { + let item: VirtualExtentOverride = model.overrides.data[i]; + if (item.index < 0 || item.index >= model.count || item.extent <= 0 || + item.extent > unbounded_extent() / 4 || item.revision < 0 || + (i > 0 && model.overrides.data[i - 1].index >= item.index)) { + return VirtualListError.invalid_override; + } + i = i + 1; + } + return VirtualListError.none; +} + +fn virtual_style_valid(style: VirtualListStyle) i32 { + return (virtual_insets_valid(style.padding) == 1 && style.overscan_before >= 0 && + style.overscan_after >= 0 && style.overscan_before <= 4096 && + style.overscan_after <= 4096 && style.max_live_items > 0 && + style.max_live_items <= 4096) as i32; +} + +fn virtual_content_extent(model: VirtualListModel, style: VirtualListStyle) i64 { + let leading: i64 = style.padding.top; + let trailing: i64 = style.padding.bottom; + if (style.axis == VirtualAxis.horizontal) { + leading = style.padding.left; trailing = style.padding.right; + } + let total: i64 = leading + trailing; + if (total < 0 || total > unbounded_extent()) { return 0 - 1; } + if (model.count == 0) { return total; } + let stride: i64 = model.default_extent + model.gap; + if (stride <= 0 || model.count > (unbounded_extent() - total) / stride) { return 0 - 1; } + total = total + model.count * stride - model.gap; + let i: i32 = 0; + while (i < model.overrides.len) { + total = total + model.overrides.data[i].extent - model.default_extent; + if (total < 0 || total > unbounded_extent()) { return 0 - 1; } + i = i + 1; + } + return total; +} + +fn virtual_item_extent(model: VirtualListModel, index: i64) i64 { + let i: i32 = 0; + while (i < model.overrides.len) { + if (model.overrides.data[i].index == index) { return model.overrides.data[i].extent; } + if (model.overrides.data[i].index > index) { return model.default_extent; } + i = i + 1; + } + return model.default_extent; +} + +fn virtual_item_revision(model: VirtualListModel, index: i64) i64 { + let i: i32 = 0; + while (i < model.overrides.len) { + if (model.overrides.data[i].index == index) { return model.overrides.data[i].revision; } + if (model.overrides.data[i].index > index) { return model.revision; } + i = i + 1; + } + return model.revision; +} + +fn virtual_item_offset(model: VirtualListModel, style: VirtualListStyle, index: i64) i64 { + let leading: i64 = style.padding.top; + if (style.axis == VirtualAxis.horizontal) { + if (style.direction == LayoutDirection.ltr) { leading = style.padding.left; } + else { leading = style.padding.right; } + } + let offset: i64 = leading + index * (model.default_extent + model.gap); + let i: i32 = 0; + while (i < model.overrides.len && model.overrides.data[i].index < index) { + offset = offset + model.overrides.data[i].extent - model.default_extent; + i = i + 1; + } + return offset; +} + +fn virtual_first_end_after(model: VirtualListModel, style: VirtualListStyle, + position: i64, steps: *i32) i64 { + let low: i64 = 0; + let high: i64 = model.count; + while (low < high) { + let mid: i64 = low + (high - low) / 2; + let finish: i64 = virtual_item_offset(model, style, mid) + virtual_item_extent(model, mid); + steps.* = steps.* + 1; + if (finish <= position) { low = mid + 1; } + else { high = mid; } + } + return low; +} + +fn virtual_first_start_at_or_after(model: VirtualListModel, style: VirtualListStyle, + position: i64, steps: *i32) i64 { + let low: i64 = 0; + let high: i64 = model.count; + while (low < high) { + let mid: i64 = low + (high - low) / 2; + let start: i64 = virtual_item_offset(model, style, mid); + steps.* = steps.* + 1; + if (start < position) { low = mid + 1; } + else { high = mid; } + } + return low; +} + +fn virtual_input_hash(model: VirtualListModel, viewport: Size, raw_scroll: i64, + style: VirtualListStyle) i64 { + let hash: i64 = virtual_mix(virtual_mix(17, model.count), model.id_base); + hash = virtual_mix(hash, model.default_extent); hash = virtual_mix(hash, model.gap); + hash = virtual_mix(hash, model.revision); hash = virtual_mix(hash, viewport.width); + hash = virtual_mix(hash, viewport.height); hash = virtual_mix(hash, raw_scroll); + hash = virtual_mix(hash, style.axis as i64); hash = virtual_mix(hash, style.direction as i64); + hash = virtual_mix(hash, style.padding.top); hash = virtual_mix(hash, style.padding.right); + hash = virtual_mix(hash, style.padding.bottom); hash = virtual_mix(hash, style.padding.left); + hash = virtual_mix(hash, style.overscan_before as i64); + hash = virtual_mix(hash, style.overscan_after as i64); + hash = virtual_mix(hash, style.max_live_items as i64); + let i: i32 = 0; + while (i < model.overrides.len) { + hash = virtual_mix(hash, model.overrides.data[i].index); + hash = virtual_mix(hash, model.overrides.data[i].extent); + hash = virtual_mix(hash, model.overrides.data[i].revision); + i = i + 1; + } + return hash; +} + +fn virtual_output_hash(result: VirtualListResult) i64 { + let hash: i64 = virtual_mix(virtual_mix(17, result.content.width), result.content.height); + hash = virtual_mix(hash, result.scroll_offset); hash = virtual_mix(hash, result.max_scroll); + hash = virtual_mix(hash, result.first_visible); hash = virtual_mix(hash, result.last_visible); + hash = virtual_mix(hash, result.first_live); hash = virtual_mix(hash, result.last_live); + hash = virtual_mix(hash, result.semantic_count); + hash = virtual_mix(hash, result.examined_items as i64); + let i: i32 = 0; + while (i < result.placements.len) { + let placement: VirtualPlacement = result.placements.data[i]; + hash = virtual_mix(hash, placement.id); hash = virtual_mix(hash, placement.index); + hash = virtual_mix(hash, placement.frame.x); hash = virtual_mix(hash, placement.frame.y); + hash = virtual_mix(hash, placement.frame.width); hash = virtual_mix(hash, placement.frame.height); + hash = virtual_mix(hash, placement.extent_revision); + i = i + 1; + } + return hash; +} + +fn virtual_list_layout(model: VirtualListModel, viewport: Size, raw_scroll: i64, + style: VirtualListStyle) VirtualListResult { + let model_error: VirtualListError = virtual_model_error(model); + if (model_error != VirtualListError.none) { + let index: i64 = 0 - 1; + if (model_error == VirtualListError.invalid_override) { + index = 0; + while (index < model.overrides.len && model.overrides.data[index as i32].index >= 0 && + model.overrides.data[index as i32].index < model.count && + model.overrides.data[index as i32].extent > 0 && + model.overrides.data[index as i32].extent <= unbounded_extent() / 4 && + model.overrides.data[index as i32].revision >= 0 && + (index == 0 || model.overrides.data[(index - 1) as i32].index < + model.overrides.data[index as i32].index)) { index = index + 1; } + } + return virtual_error_result(model_error, index); + } + if (viewport.width <= 0 || viewport.height <= 0 || + viewport.width > unbounded_extent() || viewport.height > unbounded_extent()) { + return virtual_error_result(VirtualListError.invalid_viewport, 0 - 1); + } + if (virtual_style_valid(style) == 0) { + return virtual_error_result(VirtualListError.invalid_style, 0 - 1); + } + if (raw_scroll < 0 - unbounded_extent() || raw_scroll > unbounded_extent() * 2) { + return virtual_error_result(VirtualListError.invalid_scroll, 0 - 1); + } + let axis_extent: i64 = viewport.height; + if (style.axis == VirtualAxis.horizontal) { axis_extent = viewport.width; } + let content_extent: i64 = virtual_content_extent(model, style); + if (content_extent < 0) { return virtual_error_result(VirtualListError.arithmetic_overflow, 0 - 1); } + let max_scroll: i64 = content_extent - axis_extent; + if (max_scroll < 0) { max_scroll = 0; } + let scroll_offset: i64 = clamp_i64(raw_scroll, 0, max_scroll); + let content: Size = size(viewport.width, content_extent); + if (style.axis == VirtualAxis.horizontal) { content = size(content_extent, viewport.height); } + + if (model.count == 0) { + let empty: VirtualListResult = VirtualListResult{ + .placements = make[VirtualPlacement](1), .viewport = viewport, .content = content, + .scroll_offset = scroll_offset, .max_scroll = max_scroll, + .first_visible = 0 - 1, .last_visible = 0 - 1, + .first_live = 0 - 1, .last_live = 0 - 1, + .semantic_count = 0, .examined_items = 0, .layout_hash = 0, + .error = VirtualListError.none, .error_index = 0 - 1, + }; + empty.layout_hash = virtual_mix(virtual_input_hash(model, viewport, raw_scroll, style), + virtual_output_hash(empty)); + return empty; + } + + let examined: i32 = 0; + let first_visible: i64 = virtual_first_end_after(model, style, scroll_offset, &examined); + let after_visible: i64 = virtual_first_start_at_or_after( + model, style, scroll_offset + axis_extent, &examined); + if (first_visible >= model.count || + virtual_item_offset(model, style, first_visible) >= scroll_offset + axis_extent) { + let padding_only: VirtualListResult = VirtualListResult{ + .placements = make[VirtualPlacement](1), .viewport = viewport, .content = content, + .scroll_offset = scroll_offset, .max_scroll = max_scroll, + .first_visible = 0 - 1, .last_visible = 0 - 1, + .first_live = 0 - 1, .last_live = 0 - 1, + .semantic_count = model.count, .examined_items = examined, .layout_hash = 0, + .error = VirtualListError.none, .error_index = 0 - 1, + }; + padding_only.layout_hash = virtual_mix( + virtual_input_hash(model, viewport, raw_scroll, style), + virtual_output_hash(padding_only) + ); + return padding_only; + } + let last_visible: i64 = after_visible - 1; + if (last_visible < first_visible) { last_visible = first_visible; } + if (last_visible >= model.count) { last_visible = model.count - 1; } + let first_live: i64 = first_visible - style.overscan_before as i64; + if (first_live < 0) { first_live = 0; } + let last_live: i64 = last_visible + style.overscan_after as i64; + if (last_live >= model.count) { last_live = model.count - 1; } + let live_count: i64 = last_live - first_live + 1; + if (live_count > style.max_live_items as i64) { + return virtual_error_result(VirtualListError.live_limit_exceeded, first_visible); + } + let placements: ArrayList[VirtualPlacement] = make[VirtualPlacement](live_count as i32); + let index: i64 = first_live; + while (index <= last_live) { + let offset: i64 = virtual_item_offset(model, style, index); + let extent: i64 = virtual_item_extent(model, index); + let frame: Rect = rect(0, 0, 0, 0); + if (style.axis == VirtualAxis.vertical) { + frame.x = style.padding.left; + frame.y = offset - scroll_offset; + frame.width = viewport.width - style.padding.left - style.padding.right; + if (frame.width < 0) { frame.width = 0; } + frame.height = extent; + } else { + frame.y = style.padding.top; + frame.height = viewport.height - style.padding.top - style.padding.bottom; + if (frame.height < 0) { frame.height = 0; } + frame.width = extent; + frame.x = offset - scroll_offset; + if (style.direction == LayoutDirection.rtl) { + frame.x = viewport.width - (offset - scroll_offset) - extent; + } + } + push[VirtualPlacement](&placements, VirtualPlacement{ + .id = model.id_base + index, .index = index, .frame = frame, + .extent_revision = virtual_item_revision(model, index), + }); + examined = examined + 1; + index = index + 1; + } + let result: VirtualListResult = VirtualListResult{ + .placements = placements, .viewport = viewport, .content = content, + .scroll_offset = scroll_offset, .max_scroll = max_scroll, + .first_visible = first_visible, .last_visible = last_visible, + .first_live = first_live, .last_live = last_live, + .semantic_count = model.count, .examined_items = examined, + .layout_hash = 0, .error = VirtualListError.none, .error_index = 0 - 1, + }; + result.layout_hash = virtual_mix(virtual_input_hash(model, viewport, raw_scroll, style), + virtual_output_hash(result)); + return result; +} + +fn virtual_anchor_at(model: VirtualListModel, style: VirtualListStyle, + index: i64, scroll_offset: i64) VirtualAnchor { + if (index < 0 || index >= model.count) { + return VirtualAnchor{ .id = 0, .index = 0 - 1, .viewport_offset = 0 }; + } + return VirtualAnchor{ + .id = model.id_base + index, .index = index, + .viewport_offset = virtual_item_offset(model, style, index) - scroll_offset, + }; +} + +fn virtual_resolve_anchor(model: VirtualListModel, style: VirtualListStyle, + anchor: VirtualAnchor, viewport_extent: i64, old_scroll: i64) VirtualAnchorResolution { + if (virtual_model_error(model) != VirtualListError.none || + virtual_style_valid(style) == 0 || + anchor.index < 0 || anchor.index >= model.count || + anchor.id != model.id_base + anchor.index || viewport_extent <= 0 || + viewport_extent > unbounded_extent()) { + return VirtualAnchorResolution{ + .scroll_offset = old_scroll, .changed = 0, .error = VirtualListError.invalid_anchor, + }; + } + let content_extent: i64 = virtual_content_extent(model, style); + if (content_extent < 0) { + return VirtualAnchorResolution{ + .scroll_offset = old_scroll, .changed = 0, .error = VirtualListError.arithmetic_overflow, + }; + } + let max_scroll: i64 = content_extent - viewport_extent; + if (max_scroll < 0) { max_scroll = 0; } + let raw: i64 = virtual_item_offset(model, style, anchor.index) - anchor.viewport_offset; + let resolved: i64 = clamp_i64(raw, 0, max_scroll); + return VirtualAnchorResolution{ + .scroll_offset = resolved, .changed = (resolved != old_scroll) as i32, + .error = VirtualListError.none, + }; +} diff --git a/src/layout/virtual_table.zag b/src/layout/virtual_table.zag new file mode 100644 index 0000000..d50f16c --- /dev/null +++ b/src/layout/virtual_table.zag @@ -0,0 +1,403 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("common.zag") +@import("virtual_list.zag") + +enum VirtualTableError { + none, + invalid_model, + invalid_column, + duplicate_column, + invalid_pinned_order, + invalid_style, + invalid_viewport, + row_layout_error, + cell_limit_exceeded, + unknown_column, + column_not_resizable, +} + +struct VirtualTableColumn { + id: i64, + width: i64, + minimum: i64, + maximum: i64, + pinned: i32, + resizable: i32, + revision: i64, +} + +struct VirtualTableModel { + id: i64, + rows: VirtualListModel, + columns: ArrayList[VirtualTableColumn], + revision: i64, +} + +struct VirtualTableStyle { + header_extent: i64, + column_gap: i64, + padding: Insets, + direction: LayoutDirection, + row_overscan_before: i32, + row_overscan_after: i32, + column_overscan: i32, + max_live_cells: i32, +} + +struct VirtualTableCellId { + table_id: i64, + row_id: i64, + column_id: i64, + header: i32, +} + +struct VirtualTableCell { + id: VirtualTableCellId, + row_id: i64, + column_id: i64, + row_index: i64, + column_index: i32, + frame: Rect, + header: i32, + pinned: i32, + row_revision: i64, + column_revision: i64, +} + +struct VirtualTableResult { + cells: ArrayList[VirtualTableCell], + viewport: Size, + content: Size, + vertical_scroll: i64, + horizontal_scroll: i64, + max_vertical_scroll: i64, + max_horizontal_scroll: i64, + first_visible_row: i64, + last_visible_row: i64, + first_live_row: i64, + last_live_row: i64, + first_visible_column: i32, + last_visible_column: i32, + semantic_rows: i64, + semantic_columns: i32, + pinned_columns: i32, + layout_hash: i64, + error: VirtualTableError, + error_index: i32, +} + +struct VirtualTableResizeResult { + column_id: i64, + old_width: i64, + width: i64, + consumed: i64, + unconsumed: i64, + revision: i64, + changed: i32, + error: VirtualTableError, +} + +fn virtual_table_column(id: i64, width: i64, minimum: i64, + maximum: i64) VirtualTableColumn { + return VirtualTableColumn{ + .id = id, .width = width, .minimum = minimum, .maximum = maximum, + .pinned = 0, .resizable = 1, .revision = 0, + }; +} + +fn virtual_table_model(id: i64, rows: VirtualListModel, + columns: ArrayList[VirtualTableColumn], revision: i64) VirtualTableModel { + return VirtualTableModel{ .id = id, .rows = rows, .columns = columns, .revision = revision }; +} + +fn virtual_table_style(header_extent: i64, column_gap: i64, + padding: Insets) VirtualTableStyle { + return VirtualTableStyle{ + .header_extent = header_extent, .column_gap = column_gap, .padding = padding, + .direction = LayoutDirection.ltr, + .row_overscan_before = 2, .row_overscan_after = 2, + .column_overscan = 1, .max_live_cells = 4096, + }; +} + +fn virtual_table_free(result: *VirtualTableResult) void { + free[VirtualTableCell](&result.*.cells); +} + +fn virtual_table_mix(hash: i64, value: i64) i64 { return (hash * 1099511628211) ^ value; } + +fn virtual_table_error_result(error: VirtualTableError, index: i32) VirtualTableResult { + let result: VirtualTableResult = VirtualTableResult{ + .cells = make[VirtualTableCell](1), .viewport = size(0, 0), .content = size(0, 0), + .vertical_scroll = 0, .horizontal_scroll = 0, + .max_vertical_scroll = 0, .max_horizontal_scroll = 0, + .first_visible_row = 0 - 1, .last_visible_row = 0 - 1, + .first_live_row = 0 - 1, .last_live_row = 0 - 1, + .first_visible_column = 0 - 1, .last_visible_column = 0 - 1, + .semantic_rows = 0, .semantic_columns = 0, .pinned_columns = 0, + .layout_hash = virtual_table_mix(virtual_table_mix(17, error as i64), index as i64), + .error = error, .error_index = index, + }; + return result; +} + +fn virtual_table_column_valid(column: VirtualTableColumn) i32 { + return (column.id > 0 && column.minimum > 0 && column.width >= column.minimum && + column.maximum >= column.width && column.maximum <= unbounded_extent() / 4 && + (column.pinned == 0 || column.pinned == 1) && + (column.resizable == 0 || column.resizable == 1) && column.revision >= 0) as i32; +} + +fn virtual_table_style_valid(style: VirtualTableStyle) i32 { + return (style.header_extent > 0 && style.header_extent <= unbounded_extent() / 4 && + style.column_gap >= 0 && style.column_gap <= unbounded_extent() / 4 && + virtual_insets_valid(style.padding) == 1 && + style.row_overscan_before >= 0 && style.row_overscan_before <= 4096 && + style.row_overscan_after >= 0 && style.row_overscan_after <= 4096 && + style.column_overscan >= 0 && style.column_overscan <= 256 && + style.max_live_cells > 0 && style.max_live_cells <= 65536) as i32; +} + +fn virtual_table_cell_id(table_id: i64, row_id: i64, column_id: i64, + header: i32) VirtualTableCellId { + return VirtualTableCellId{ + .table_id = table_id, .row_id = row_id, + .column_id = column_id, .header = header, + }; +} + +fn virtual_table_resize(model: *VirtualTableModel, column_id: i64, + delta: i64) VirtualTableResizeResult { + let index: i32 = 0 - 1; + let i: i32 = 0; + while (i < model.*.columns.len) { + if (model.*.columns.data[i].id == column_id) { index = i; break; } + i = i + 1; + } + if (index < 0) { + return VirtualTableResizeResult{ + .column_id = column_id, .old_width = 0, .width = 0, + .consumed = 0, .unconsumed = delta, .revision = model.*.revision, + .changed = 0, .error = VirtualTableError.unknown_column, + }; + } + let column: VirtualTableColumn = model.*.columns.data[index]; + if (column.resizable == 0) { + return VirtualTableResizeResult{ + .column_id = column_id, .old_width = column.width, .width = column.width, + .consumed = 0, .unconsumed = delta, .revision = model.*.revision, + .changed = 0, .error = VirtualTableError.column_not_resizable, + }; + } + if (delta < 0 - unbounded_extent() || delta > unbounded_extent()) { + return VirtualTableResizeResult{ + .column_id = column_id, .old_width = column.width, .width = column.width, + .consumed = 0, .unconsumed = delta, .revision = model.*.revision, + .changed = 0, .error = VirtualTableError.invalid_column, + }; + } + let width: i64 = clamp_i64(column.width + delta, column.minimum, column.maximum); + let consumed: i64 = width - column.width; + let changed: i32 = (consumed != 0) as i32; + if (changed != 0) { + model.*.columns.data[index].width = width; + model.*.columns.data[index].revision = model.*.columns.data[index].revision + 1; + model.*.revision = model.*.revision + 1; + } + return VirtualTableResizeResult{ + .column_id = column_id, .old_width = column.width, .width = width, + .consumed = consumed, .unconsumed = delta - consumed, + .revision = model.*.revision, .changed = changed, .error = VirtualTableError.none, + }; +} + +fn virtual_table_layout(model: VirtualTableModel, viewport: Size, + vertical_scroll: i64, horizontal_scroll: i64, + style: VirtualTableStyle) VirtualTableResult { + if (model.id <= 0 || model.revision < 0 || model.columns.len <= 0 || model.columns.len > 256 || + virtual_model_error(model.rows) != VirtualListError.none) { + return virtual_table_error_result(VirtualTableError.invalid_model, 0 - 1); + } + let pinned_count: i32 = 0; + let seen_unpinned: i32 = 0; + let i: i32 = 0; + while (i < model.columns.len) { + let column: VirtualTableColumn = model.columns.data[i]; + if (virtual_table_column_valid(column) == 0) { + return virtual_table_error_result(VirtualTableError.invalid_column, i); + } + let j: i32 = 0; + while (j < i) { + if (model.columns.data[j].id == column.id) { + return virtual_table_error_result(VirtualTableError.duplicate_column, i); + } + j = j + 1; + } + if (column.pinned != 0) { + if (seen_unpinned != 0) { + return virtual_table_error_result(VirtualTableError.invalid_pinned_order, i); + } + pinned_count = pinned_count + 1; + } else { seen_unpinned = 1; } + i = i + 1; + } + if (virtual_table_style_valid(style) == 0) { + return virtual_table_error_result(VirtualTableError.invalid_style, 0 - 1); + } + if (viewport.width <= 0 || viewport.height <= 0 || + viewport.width > unbounded_extent() || viewport.height > unbounded_extent() || + viewport.width <= style.padding.left + style.padding.right || + viewport.height <= style.padding.top + style.padding.bottom + style.header_extent) { + return virtual_table_error_result(VirtualTableError.invalid_viewport, 0 - 1); + } + let inner_width: i64 = viewport.width - style.padding.left - style.padding.right; + let body_height: i64 = viewport.height - style.padding.top - style.padding.bottom - style.header_extent; + let pinned_width: i64 = 0; + i = 0; + while (i < pinned_count) { + pinned_width = pinned_width + model.columns.data[i].width; + if (i + 1 < model.columns.len) { pinned_width = pinned_width + style.column_gap; } + i = i + 1; + } + if (pinned_width >= inner_width) { + return virtual_table_error_result(VirtualTableError.invalid_viewport, pinned_count - 1); + } + let scroll_viewport_width: i64 = inner_width - pinned_width; + let unpinned_content_width: i64 = 0; + i = pinned_count; + while (i < model.columns.len) { + unpinned_content_width = unpinned_content_width + model.columns.data[i].width; + if (i + 1 < model.columns.len) { unpinned_content_width = unpinned_content_width + style.column_gap; } + if (unpinned_content_width > unbounded_extent()) { + return virtual_table_error_result(VirtualTableError.invalid_column, i); + } + i = i + 1; + } + let max_horizontal: i64 = unpinned_content_width - scroll_viewport_width; + if (max_horizontal < 0) { max_horizontal = 0; } + let horizontal: i64 = clamp_i64(horizontal_scroll, 0, max_horizontal); + + let row_style: VirtualListStyle = virtual_style(VirtualAxis.vertical, insets_all(0)); + row_style.overscan_before = style.row_overscan_before; + row_style.overscan_after = style.row_overscan_after; + row_style.max_live_items = 4096; + let row_layout: VirtualListResult = virtual_list_layout( + model.rows, size(inner_width, body_height), vertical_scroll, row_style + ); + if (row_layout.error != VirtualListError.none) { + let error_index: i32 = row_layout.error_index as i32; + virtual_free(&row_layout); + return virtual_table_error_result(VirtualTableError.row_layout_error, error_index); + } + + let visible_columns: ArrayList[i32] = make[i32](model.columns.len); + i = 0; + while (i < pinned_count) { push[i32](&visible_columns, i); i = i + 1; } + let first_scrolling: i32 = 0 - 1; + let last_scrolling: i32 = 0 - 1; + let logical_offset: i64 = 0; + i = pinned_count; + while (i < model.columns.len) { + let width: i64 = model.columns.data[i].width; + if (logical_offset + width > horizontal && + logical_offset < horizontal + scroll_viewport_width) { + if (first_scrolling < 0) { first_scrolling = i; } + last_scrolling = i; + } + logical_offset = logical_offset + width; + if (i + 1 < model.columns.len) { logical_offset = logical_offset + style.column_gap; } + i = i + 1; + } + let live_column_start: i32 = first_scrolling - style.column_overscan; + if (live_column_start < pinned_count) { live_column_start = pinned_count; } + let live_column_end: i32 = last_scrolling + style.column_overscan; + if (live_column_end >= model.columns.len) { live_column_end = model.columns.len - 1; } + if (first_scrolling >= 0) { + i = live_column_start; + while (i <= live_column_end) { push[i32](&visible_columns, i); i = i + 1; } + } + let required_cells: i64 = visible_columns.len as i64; + required_cells = required_cells * (row_layout.placements.len as i64 + 1); + if (required_cells > style.max_live_cells as i64) { + free[i32](&visible_columns); virtual_free(&row_layout); + return virtual_table_error_result(VirtualTableError.cell_limit_exceeded, required_cells as i32); + } + + let column_offsets: ArrayList[i64] = make[i64](model.columns.len); + let pinned_cursor: i64 = 0; + let unpinned_cursor: i64 = 0; + i = 0; + while (i < model.columns.len) { + if (i < pinned_count) { + push[i64](&column_offsets, pinned_cursor); + pinned_cursor = pinned_cursor + model.columns.data[i].width + style.column_gap; + } else { + push[i64](&column_offsets, pinned_width + unpinned_cursor - horizontal); + unpinned_cursor = unpinned_cursor + model.columns.data[i].width + style.column_gap; + } + i = i + 1; + } + + let cells: ArrayList[VirtualTableCell] = make[VirtualTableCell](required_cells as i32); + i = 0; + while (i < visible_columns.len) { + let column_index: i32 = visible_columns.data[i]; + let column: VirtualTableColumn = model.columns.data[column_index]; + let x: i64 = style.padding.left + column_offsets.data[column_index]; + if (style.direction == LayoutDirection.rtl) { + x = viewport.width - style.padding.right - column_offsets.data[column_index] - column.width; + } + push[VirtualTableCell](&cells, VirtualTableCell{ + .id = virtual_table_cell_id(model.id, 0, column.id, 1), + .row_id = 0, .column_id = column.id, .row_index = 0 - 1, + .column_index = column_index, + .frame = rect(x, style.padding.top, column.width, style.header_extent), + .header = 1, .pinned = column.pinned, + .row_revision = model.revision, .column_revision = column.revision, + }); + let r: i32 = 0; + while (r < row_layout.placements.len) { + let row: VirtualPlacement = row_layout.placements.data[r]; + push[VirtualTableCell](&cells, VirtualTableCell{ + .id = virtual_table_cell_id(model.id, row.id, column.id, 0), + .row_id = row.id, .column_id = column.id, .row_index = row.index, + .column_index = column_index, + .frame = rect(x, style.padding.top + style.header_extent + row.frame.y, + column.width, row.frame.height), + .header = 0, .pinned = column.pinned, + .row_revision = row.extent_revision, .column_revision = column.revision, + }); + r = r + 1; + } + i = i + 1; + } + let content_width: i64 = pinned_width + unpinned_content_width + style.padding.left + style.padding.right; + let content_height: i64 = row_layout.content.height + style.header_extent + + style.padding.top + style.padding.bottom; + let result: VirtualTableResult = VirtualTableResult{ + .cells = cells, .viewport = viewport, .content = size(content_width, content_height), + .vertical_scroll = row_layout.scroll_offset, .horizontal_scroll = horizontal, + .max_vertical_scroll = row_layout.max_scroll, .max_horizontal_scroll = max_horizontal, + .first_visible_row = row_layout.first_visible, .last_visible_row = row_layout.last_visible, + .first_live_row = row_layout.first_live, .last_live_row = row_layout.last_live, + .first_visible_column = first_scrolling, .last_visible_column = last_scrolling, + .semantic_rows = model.rows.count, .semantic_columns = model.columns.len, + .pinned_columns = pinned_count, .layout_hash = virtual_table_mix(row_layout.layout_hash, model.revision), + .error = VirtualTableError.none, .error_index = 0 - 1, + }; + i = 0; + while (i < result.cells.len) { + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].id.table_id); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].id.row_id); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].id.column_id); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].id.header as i64); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].frame.x); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].frame.y); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].frame.width); + result.layout_hash = virtual_table_mix(result.layout_hash, result.cells.data[i].frame.height); + i = i + 1; + } + free[i64](&column_offsets); free[i32](&visible_columns); virtual_free(&row_layout); + return result; +} diff --git a/src/layout/virtual_tree.zag b/src/layout/virtual_tree.zag new file mode 100644 index 0000000..eecdc27 --- /dev/null +++ b/src/layout/virtual_tree.zag @@ -0,0 +1,324 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("common.zag") +@import("virtual_list.zag") + +enum VirtualTreeError { + none, + too_many_nodes, + invalid_node, + duplicate_id, + invalid_depth, + invalid_parent, + invalid_style, + layout_error, +} + +struct VirtualTreeNode { + id: i64, + parent_id: i64, + depth: i32, + has_children: i32, + expanded: i32, + extent: i64, + revision: i64, +} + +struct VirtualTreeVisibleNode { + id: i64, + parent_id: i64, + source_index: i32, + depth: i32, + has_children: i32, + expanded: i32, + extent: i64, + revision: i64, +} + +struct VirtualTreeProjection { + nodes: ArrayList[VirtualTreeVisibleNode], + source_count: i32, + revision: i64, + projection_hash: i64, + error: VirtualTreeError, + error_index: i32, +} + +struct VirtualTreeStyle { + row_extent: i64, + row_gap: i64, + indent: i64, + padding: Insets, + direction: LayoutDirection, + overscan_before: i32, + overscan_after: i32, + max_live_nodes: i32, +} + +struct VirtualTreePlacement { + id: i64, + parent_id: i64, + source_index: i32, + visible_index: i64, + depth: i32, + has_children: i32, + expanded: i32, + frame: Rect, + content_frame: Rect, + revision: i64, +} + +struct VirtualTreeResult { + placements: ArrayList[VirtualTreePlacement], + viewport: Size, + content: Size, + scroll_offset: i64, + max_scroll: i64, + first_visible: i64, + last_visible: i64, + first_live: i64, + last_live: i64, + semantic_count: i64, + source_count: i32, + layout_hash: i64, + error: VirtualTreeError, + error_index: i32, +} + +fn virtual_tree_node(id: i64, parent_id: i64, depth: i32, + has_children: i32, expanded: i32, extent: i64, revision: i64) VirtualTreeNode { + return VirtualTreeNode{ + .id = id, .parent_id = parent_id, .depth = depth, + .has_children = has_children, .expanded = expanded, + .extent = extent, .revision = revision, + }; +} + +fn virtual_tree_style(row_extent: i64, row_gap: i64, indent: i64, + padding: Insets) VirtualTreeStyle { + return VirtualTreeStyle{ + .row_extent = row_extent, .row_gap = row_gap, .indent = indent, + .padding = padding, .direction = LayoutDirection.ltr, + .overscan_before = 2, .overscan_after = 2, .max_live_nodes = 512, + }; +} + +fn virtual_tree_mix(hash: i64, value: i64) i64 { return (hash * 1099511628211) ^ value; } + +fn virtual_tree_projection_free(projection: *VirtualTreeProjection) void { + free[VirtualTreeVisibleNode](&projection.*.nodes); +} + +fn virtual_tree_free(result: *VirtualTreeResult) void { + free[VirtualTreePlacement](&result.*.placements); +} + +fn virtual_tree_projection_error(error: VirtualTreeError, index: i32, + source_count: i32) VirtualTreeProjection { + let result: VirtualTreeProjection = VirtualTreeProjection{ + .nodes = make[VirtualTreeVisibleNode](1), .source_count = source_count, + .revision = 0, .projection_hash = 17, + .error = error, .error_index = index, + }; + result.projection_hash = virtual_tree_mix(virtual_tree_mix(17, error as i64), index as i64); + return result; +} + +fn virtual_tree_result_error(error: VirtualTreeError, index: i32) VirtualTreeResult { + let result: VirtualTreeResult = VirtualTreeResult{ + .placements = make[VirtualTreePlacement](1), + .viewport = size(0, 0), .content = size(0, 0), + .scroll_offset = 0, .max_scroll = 0, + .first_visible = 0 - 1, .last_visible = 0 - 1, + .first_live = 0 - 1, .last_live = 0 - 1, + .semantic_count = 0, .source_count = 0, .layout_hash = 17, + .error = error, .error_index = index, + }; + result.layout_hash = virtual_tree_mix(virtual_tree_mix(17, error as i64), index as i64); + return result; +} + +fn virtual_tree_node_valid(node: VirtualTreeNode) i32 { + return (node.id > 0 && node.parent_id >= 0 && node.depth >= 0 && node.depth <= 512 && + (node.has_children == 0 || node.has_children == 1) && + (node.expanded == 0 || node.expanded == 1) && + (node.has_children == 1 || node.expanded == 0) && + node.extent > 0 && node.extent <= unbounded_extent() / 4 && node.revision >= 0) as i32; +} + +fn virtual_tree_seen_insert(seen: *ArrayList[i64], id: i64) i32 { + let index: i32 = (id % seen.*.len as i64) as i32; + let probes: i32 = 0; + while (probes < seen.*.len) { + if (seen.*.data[index] == id) { return 0; } + if (seen.*.data[index] == 0) { seen.*.data[index] = id; return 1; } + index = index + 1; + if (index == seen.*.len) { index = 0; } + probes = probes + 1; + } + return 0 - 1; +} + +fn virtual_tree_project(source: ArrayList[VirtualTreeNode], revision: i64) VirtualTreeProjection { + if (source.len > 16384) { + return virtual_tree_projection_error(VirtualTreeError.too_many_nodes, 0 - 1, source.len); + } + if (revision < 0) { + return virtual_tree_projection_error(VirtualTreeError.invalid_node, 0 - 1, source.len); + } + let ancestor_ids: ArrayList[i64] = make[i64](32); + let ancestor_has_children: ArrayList[i32] = make[i32](32); + let seen: ArrayList[i64] = make[i64](source.len * 2 + 1); + let seen_index: i32 = 0; + while (seen_index < source.len * 2 + 1) { push[i64](&seen, 0); seen_index = seen_index + 1; } + let visible: ArrayList[VirtualTreeVisibleNode] = make[VirtualTreeVisibleNode](source.len); + let hidden_depth: i32 = 0 - 1; + let hash: i64 = virtual_tree_mix(17, revision); + let i: i32 = 0; + while (i < source.len) { + let node: VirtualTreeNode = source.data[i]; + if (virtual_tree_node_valid(node) == 0) { + free[i64](&ancestor_ids); free[i32](&ancestor_has_children); free[i64](&seen); + free[VirtualTreeVisibleNode](&visible); + return virtual_tree_projection_error(VirtualTreeError.invalid_node, i, source.len); + } + if (virtual_tree_seen_insert(&seen, node.id) != 1) { + free[i64](&ancestor_ids); free[i32](&ancestor_has_children); free[i64](&seen); + free[VirtualTreeVisibleNode](&visible); + return virtual_tree_projection_error(VirtualTreeError.duplicate_id, i, source.len); + } + if ((i == 0 && node.depth != 0) || + (i > 0 && node.depth > source.data[i - 1].depth + 1)) { + free[i64](&ancestor_ids); free[i32](&ancestor_has_children); free[i64](&seen); + free[VirtualTreeVisibleNode](&visible); + return virtual_tree_projection_error(VirtualTreeError.invalid_depth, i, source.len); + } + while (ancestor_ids.len > node.depth) { + ancestor_ids.len = ancestor_ids.len - 1; + ancestor_has_children.len = ancestor_has_children.len - 1; + } + if (node.depth == 0) { + if (node.parent_id != 0) { + free[i64](&ancestor_ids); free[i32](&ancestor_has_children); free[i64](&seen); + free[VirtualTreeVisibleNode](&visible); + return virtual_tree_projection_error(VirtualTreeError.invalid_parent, i, source.len); + } + } else if (ancestor_ids.len != node.depth || + ancestor_ids.data[node.depth - 1] != node.parent_id || + ancestor_has_children.data[node.depth - 1] == 0) { + free[i64](&ancestor_ids); free[i32](&ancestor_has_children); free[i64](&seen); + free[VirtualTreeVisibleNode](&visible); + return virtual_tree_projection_error(VirtualTreeError.invalid_parent, i, source.len); + } + push[i64](&ancestor_ids, node.id); + push[i32](&ancestor_has_children, node.has_children); + + if (hidden_depth >= 0 && node.depth <= hidden_depth) { hidden_depth = 0 - 1; } + if (hidden_depth < 0) { + push[VirtualTreeVisibleNode](&visible, VirtualTreeVisibleNode{ + .id = node.id, .parent_id = node.parent_id, .source_index = i, + .depth = node.depth, .has_children = node.has_children, + .expanded = node.expanded, .extent = node.extent, .revision = node.revision, + }); + if (node.has_children != 0 && node.expanded == 0) { hidden_depth = node.depth; } + } + hash = virtual_tree_mix(hash, node.id); hash = virtual_tree_mix(hash, node.parent_id); + hash = virtual_tree_mix(hash, node.depth as i64); + hash = virtual_tree_mix(hash, node.has_children as i64); + hash = virtual_tree_mix(hash, node.expanded as i64); + hash = virtual_tree_mix(hash, node.extent); hash = virtual_tree_mix(hash, node.revision); + i = i + 1; + } + free[i64](&ancestor_ids); free[i32](&ancestor_has_children); free[i64](&seen); + hash = virtual_tree_mix(hash, visible.len as i64); + i = 0; + while (i < visible.len) { hash = virtual_tree_mix(hash, visible.data[i].id); i = i + 1; } + return VirtualTreeProjection{ + .nodes = visible, .source_count = source.len, .revision = revision, + .projection_hash = hash, .error = VirtualTreeError.none, .error_index = 0 - 1, + }; +} + +fn virtual_tree_style_valid(style: VirtualTreeStyle) i32 { + return (style.row_extent > 0 && style.row_extent <= unbounded_extent() / 4 && + style.row_gap >= 0 && style.row_gap <= unbounded_extent() / 4 && + style.indent >= 0 && style.indent <= unbounded_extent() / 4 && + virtual_insets_valid(style.padding) == 1 && + style.overscan_before >= 0 && style.overscan_before <= 4096 && + style.overscan_after >= 0 && style.overscan_after <= 4096 && + style.max_live_nodes > 0 && style.max_live_nodes <= 4096) as i32; +} + +fn virtual_tree_layout(projection: VirtualTreeProjection, viewport: Size, + scroll_offset: i64, style: VirtualTreeStyle) VirtualTreeResult { + if (projection.error != VirtualTreeError.none) { + return virtual_tree_result_error(projection.error, projection.error_index); + } + if (virtual_tree_style_valid(style) == 0) { + return virtual_tree_result_error(VirtualTreeError.invalid_style, 0 - 1); + } + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](projection.nodes.len); + let i: i32 = 0; + while (i < projection.nodes.len) { + if (projection.nodes.data[i].extent != style.row_extent || + projection.nodes.data[i].revision != projection.revision) { + push[VirtualExtentOverride](&overrides, virtual_override( + i as i64, projection.nodes.data[i].extent, projection.nodes.data[i].revision + )); + } + i = i + 1; + } + let model: VirtualListModel = virtual_model( + projection.nodes.len as i64, 1, style.row_extent, style.row_gap, + projection.revision, overrides + ); + let list_style: VirtualListStyle = virtual_style(VirtualAxis.vertical, style.padding); + list_style.direction = style.direction; + list_style.overscan_before = style.overscan_before; + list_style.overscan_after = style.overscan_after; + list_style.max_live_items = style.max_live_nodes; + let layout: VirtualListResult = virtual_list_layout(model, viewport, scroll_offset, list_style); + free[VirtualExtentOverride](&overrides); + if (layout.error != VirtualListError.none) { + let index: i32 = layout.error_index as i32; + virtual_free(&layout); + return virtual_tree_result_error(VirtualTreeError.layout_error, index); + } + let placements: ArrayList[VirtualTreePlacement] = make[VirtualTreePlacement](layout.placements.len); + i = 0; + while (i < layout.placements.len) { + let row: VirtualPlacement = layout.placements.data[i]; + let node: VirtualTreeVisibleNode = projection.nodes.data[row.index as i32]; + let indentation: i64 = style.indent * (node.depth as i64); + if (indentation < 0 || indentation > row.frame.width) { indentation = row.frame.width; } + let content_frame: Rect = row.frame; + content_frame.width = row.frame.width - indentation; + if (style.direction == LayoutDirection.ltr) { content_frame.x = row.frame.x + indentation; } + push[VirtualTreePlacement](&placements, VirtualTreePlacement{ + .id = node.id, .parent_id = node.parent_id, + .source_index = node.source_index, .visible_index = row.index, + .depth = node.depth, .has_children = node.has_children, + .expanded = node.expanded, .frame = row.frame, + .content_frame = content_frame, .revision = node.revision, + }); + i = i + 1; + } + let result: VirtualTreeResult = VirtualTreeResult{ + .placements = placements, .viewport = layout.viewport, .content = layout.content, + .scroll_offset = layout.scroll_offset, .max_scroll = layout.max_scroll, + .first_visible = layout.first_visible, .last_visible = layout.last_visible, + .first_live = layout.first_live, .last_live = layout.last_live, + .semantic_count = projection.nodes.len as i64, .source_count = projection.source_count, + .layout_hash = virtual_tree_mix(projection.projection_hash, layout.layout_hash), + .error = VirtualTreeError.none, .error_index = 0 - 1, + }; + i = 0; + while (i < result.placements.len) { + result.layout_hash = virtual_tree_mix(result.layout_hash, result.placements.data[i].id); + result.layout_hash = virtual_tree_mix(result.layout_hash, result.placements.data[i].content_frame.x); + result.layout_hash = virtual_tree_mix(result.layout_hash, result.placements.data[i].content_frame.width); + i = i + 1; + } + virtual_free(&layout); + return result; +} diff --git a/src/motion/motion.zag b/src/motion/motion.zag new file mode 100644 index 0000000..69947d0 --- /dev/null +++ b/src/motion/motion.zag @@ -0,0 +1,654 @@ +@import("std:list") +@import("../render/display_list.zag") + +// Motion values use the same signed 26.6 logical-unit representation as +// layout. Time is supplied explicitly in monotonic microseconds. + +enum MotionKind { spring, keyframes } +enum MotionStatus { running, settled } +enum MotionPreference { full, reduced } +enum MotionReducedSubstitution { snap_to_end, opacity_fade } + +enum MotionFrameReason { + start, + tick, + interrupt, + reverse, + gesture_handoff, + resize, + reduced_motion, + refresh_rate_change, +} + +enum MotionError { + none, + duplicate_id, + missing_track, + invalid_spec, + invalid_keyframes, + clock_regression, + clock_advance_requires_tick, + clock_step_too_large, +} + +struct SpringSpec { + stiffness: i64, + damping: i64, + position_epsilon: i64, + velocity_epsilon: i64, +} + +struct MotionKeyframe { + time_us: i64, + value: i64, +} + +struct MotionTrack { + id: i64, + kind: MotionKind, + status: MotionStatus, + value: i64, + velocity: i64, + target: i64, + spring: SpringSpec, + keyframes: ArrayList[MotionKeyframe], + elapsed_us: i64, + remainder_us: i64, + velocity_remainder: i64, + position_remainder: i64, +} + +struct MotionEvent { + sequence: i64, + reason: MotionFrameReason, + track_id: i64, + time_us: i64, + value: i64, + velocity: i64, + target: i64, +} + +struct MotionTickResult { + error: MotionError, + did_work: i32, + updated_tracks: i32, + running_tracks: i32, + request_next_frame: i32, + frame_interval_us: i64, +} + +struct MotionSample { + found: i32, + id: i64, + kind: MotionKind, + status: MotionStatus, + value: i64, + velocity: i64, + target: i64, + elapsed_us: i64, +} + +struct MotionScheduler { + tracks: ArrayList[MotionTrack], + events: ArrayList[MotionEvent], + now_us: i64, + has_clock: i32, + refresh_hz: i64, + frame_interval_us: i64, + next_sequence: i64, + last_error: MotionError, + error_track: i64, +} + +fn motion_abs(value: i64) i64 { + if (value < 0) { return 0 - value; } + return value; +} + +fn motion_value_valid(value: i64) i32 { + return (value >= -1000000000 && value <= 1000000000) as i32; +} + +fn spring_spec(stiffness: i64, damping: i64) SpringSpec { + return SpringSpec{ + .stiffness = stiffness, + .damping = damping, + .position_epsilon = 1, + .velocity_epsilon = unit_scale(), + }; +} + +fn motion_spring_valid(spec: SpringSpec) i32 { + return (spec.stiffness > 0 && spec.stiffness <= 4096 && + spec.damping >= 0 && spec.damping <= 1024 && + spec.position_epsilon >= 0 && spec.position_epsilon <= unit_scale() * 16 && + spec.velocity_epsilon >= 0 && spec.velocity_epsilon <= unit_scale() * 1024) as i32; +} + +fn motion_scheduler_make(refresh_hz: i64) MotionScheduler { + let actual_hz: i64 = refresh_hz; + if (actual_hz < 24 || actual_hz > 480) { actual_hz = 60; } + return MotionScheduler{ + .tracks = make[MotionTrack](16), + .events = make[MotionEvent](64), + .now_us = 0, + .has_clock = 0, + .refresh_hz = actual_hz, + .frame_interval_us = 1000000 / actual_hz, + .next_sequence = 1, + .last_error = MotionError.none, + .error_track = 0, + }; +} + +fn motion_scheduler_free(scheduler: *MotionScheduler) void { + let i: i32 = 0; + while (i < scheduler.*.tracks.len) { + free[MotionKeyframe](&scheduler.*.tracks.data[i].keyframes); + i = i + 1; + } + free[MotionTrack](&scheduler.*.tracks); + free[MotionEvent](&scheduler.*.events); +} + +fn motion_find_index(scheduler: MotionScheduler, id: i64) i32 { + let i: i32 = scheduler.tracks.len - 1; + while (i >= 0) { + if (scheduler.tracks.data[i].id == id) { return i; } + i = i - 1; + } + return 0 - 1; +} + +fn motion_fail(scheduler: *MotionScheduler, error: MotionError, track_id: i64) MotionError { + scheduler.*.last_error = error; + scheduler.*.error_track = track_id; + return error; +} + +fn motion_clear_error(scheduler: *MotionScheduler) void { + scheduler.*.last_error = MotionError.none; + scheduler.*.error_track = 0; +} + +fn motion_record(scheduler: *MotionScheduler, reason: MotionFrameReason, track_id: i64) void { + let value: i64 = 0; + let velocity: i64 = 0; + let target: i64 = 0; + let index: i32 = motion_find_index(scheduler.*, track_id); + if (index >= 0) { + value = scheduler.*.tracks.data[index].value; + velocity = scheduler.*.tracks.data[index].velocity; + target = scheduler.*.tracks.data[index].target; + } + push[MotionEvent](&scheduler.*.events, MotionEvent{ + .sequence = scheduler.*.next_sequence, + .reason = reason, + .track_id = track_id, + .time_us = scheduler.*.now_us, + .value = value, + .velocity = velocity, + .target = target, + }); + scheduler.*.next_sequence = scheduler.*.next_sequence + 1; +} + +fn motion_set_clock(scheduler: *MotionScheduler, time_us: i64) MotionError { + if (time_us < 0) { return motion_fail(scheduler, MotionError.invalid_spec, 0); } + if (scheduler.*.has_clock != 0 && time_us < scheduler.*.now_us) { + return motion_fail(scheduler, MotionError.clock_regression, 0); + } + if (scheduler.*.has_clock != 0 && time_us > scheduler.*.now_us && + motion_running_count(scheduler.*) > 0) { + return motion_fail(scheduler, MotionError.clock_advance_requires_tick, 0); + } + scheduler.*.now_us = time_us; + scheduler.*.has_clock = 1; + motion_clear_error(scheduler); + return MotionError.none; +} + +fn motion_running_count(scheduler: MotionScheduler) i32 { + let count: i32 = 0; + let i: i32 = 0; + while (i < scheduler.tracks.len) { + if (scheduler.tracks.data[i].status == MotionStatus.running) { count = count + 1; } + i = i + 1; + } + return count; +} + +fn motion_needs_frame(scheduler: MotionScheduler) i32 { + return (motion_running_count(scheduler) > 0) as i32; +} + +fn motion_empty_keyframes() ArrayList[MotionKeyframe] { + return make[MotionKeyframe](1); +} + +fn motion_start_spring( + scheduler: *MotionScheduler, + id: i64, + value: i64, + target: i64, + spec: SpringSpec, + preference: MotionPreference, + substitution: MotionReducedSubstitution, +) MotionError { + if (scheduler.*.has_clock == 0 || id <= 0 || scheduler.*.tracks.len >= 4096 || + motion_find_index(scheduler.*, id) >= 0) { + if (motion_find_index(scheduler.*, id) >= 0) { + return motion_fail(scheduler, MotionError.duplicate_id, id); + } + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + if (motion_value_valid(value) == 0 || motion_value_valid(target) == 0 || motion_spring_valid(spec) == 0) { + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + if (preference == MotionPreference.reduced && substitution != MotionReducedSubstitution.snap_to_end) { + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + let track: MotionTrack = MotionTrack{ + .id = id, + .kind = MotionKind.spring, + .status = MotionStatus.running, + .value = value, + .velocity = 0, + .target = target, + .spring = spec, + .keyframes = motion_empty_keyframes(), + .elapsed_us = 0, + .remainder_us = 0, + .velocity_remainder = 0, + .position_remainder = 0, + }; + if (value == target) { track.status = MotionStatus.settled; } + if (preference == MotionPreference.reduced && substitution == MotionReducedSubstitution.snap_to_end) { + track.value = target; + track.status = MotionStatus.settled; + } + push[MotionTrack](&scheduler.*.tracks, track); + if (preference == MotionPreference.reduced) { + motion_record(scheduler, MotionFrameReason.reduced_motion, id); + } else { + motion_record(scheduler, MotionFrameReason.start, id); + } + motion_clear_error(scheduler); + return MotionError.none; +} + +fn motion_keyframes_valid(keyframes: ArrayList[MotionKeyframe]) i32 { + if (keyframes.len < 2 || keyframes.len > 4096 || keyframes.data[0].time_us != 0) { return 0; } + let i: i32 = 0; + while (i < keyframes.len) { + if (motion_value_valid(keyframes.data[i].value) == 0 || keyframes.data[i].time_us < 0 || + keyframes.data[i].time_us > 60000000) { return 0; } + if (i > 0 && keyframes.data[i].time_us <= keyframes.data[i - 1].time_us) { return 0; } + if (i > 0) { + let duration: i64 = keyframes.data[i].time_us - keyframes.data[i - 1].time_us; + let velocity: i64 = ((keyframes.data[i].value - keyframes.data[i - 1].value) * 1000000) / duration; + if (motion_value_valid(velocity) == 0) { return 0; } + } + i = i + 1; + } + return 1; +} + +fn motion_opacity_keyframes_valid(keyframes: ArrayList[MotionKeyframe]) i32 { + if (motion_keyframes_valid(keyframes) == 0 || keyframes.data[keyframes.len - 1].time_us > 250000) { + return 0; + } + let i: i32 = 0; + while (i < keyframes.len) { + if (keyframes.data[i].value < 0 || keyframes.data[i].value > unit_scale()) { return 0; } + i = i + 1; + } + return 1; +} + +fn motion_copy_keyframes(keyframes: ArrayList[MotionKeyframe]) ArrayList[MotionKeyframe] { + let owned: ArrayList[MotionKeyframe] = make[MotionKeyframe](keyframes.len); + let i: i32 = 0; + while (i < keyframes.len) { + push[MotionKeyframe](&owned, keyframes.data[i]); + i = i + 1; + } + return owned; +} + +fn motion_start_keyframes( + scheduler: *MotionScheduler, + id: i64, + keyframes: ArrayList[MotionKeyframe], + preference: MotionPreference, + substitution: MotionReducedSubstitution, +) MotionError { + if (scheduler.*.has_clock == 0 || id <= 0 || scheduler.*.tracks.len >= 4096 || + motion_find_index(scheduler.*, id) >= 0) { + if (motion_find_index(scheduler.*, id) >= 0) { + return motion_fail(scheduler, MotionError.duplicate_id, id); + } + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + if (motion_keyframes_valid(keyframes) == 0) { + return motion_fail(scheduler, MotionError.invalid_keyframes, id); + } + if (preference == MotionPreference.reduced && substitution == MotionReducedSubstitution.opacity_fade && + motion_opacity_keyframes_valid(keyframes) == 0) { + return motion_fail(scheduler, MotionError.invalid_keyframes, id); + } + let owned: ArrayList[MotionKeyframe] = motion_copy_keyframes(keyframes); + let last: MotionKeyframe = owned.data[owned.len - 1]; + let track: MotionTrack = MotionTrack{ + .id = id, + .kind = MotionKind.keyframes, + .status = MotionStatus.running, + .value = owned.data[0].value, + .velocity = 0, + .target = last.value, + .spring = spring_spec(1, 0), + .keyframes = owned, + .elapsed_us = 0, + .remainder_us = 0, + .velocity_remainder = 0, + .position_remainder = 0, + }; + if (preference == MotionPreference.reduced && substitution == MotionReducedSubstitution.snap_to_end) { + track.value = track.target; + track.status = MotionStatus.settled; + } + if (preference == MotionPreference.reduced && substitution == MotionReducedSubstitution.opacity_fade) { + // The caller supplied an explicit opacity timeline as its semantic + // substitute. It is not derived by multiplying the original duration. + track.status = MotionStatus.running; + } + push[MotionTrack](&scheduler.*.tracks, track); + if (preference == MotionPreference.reduced) { + motion_record(scheduler, MotionFrameReason.reduced_motion, id); + } else { + motion_record(scheduler, MotionFrameReason.start, id); + } + motion_clear_error(scheduler); + return MotionError.none; +} + +fn motion_set_refresh_rate(scheduler: *MotionScheduler, refresh_hz: i64) MotionError { + if (refresh_hz < 24 || refresh_hz > 480) { + return motion_fail(scheduler, MotionError.invalid_spec, 0); + } + if (scheduler.*.refresh_hz != refresh_hz) { + scheduler.*.refresh_hz = refresh_hz; + scheduler.*.frame_interval_us = 1000000 / refresh_hz; + motion_record(scheduler, MotionFrameReason.refresh_rate_change, 0); + } + motion_clear_error(scheduler); + return MotionError.none; +} + +fn motion_spring_step(track: *MotionTrack) void { + let error: i64 = track.*.target - track.*.value; + let acceleration: i64 = error * track.*.spring.stiffness - track.*.velocity * track.*.spring.damping; + let velocity_numerator: i64 = acceleration * 1000 + track.*.velocity_remainder; + let velocity_delta: i64 = velocity_numerator / 1000000; + track.*.velocity_remainder = velocity_numerator - velocity_delta * 1000000; + track.*.velocity = track.*.velocity + velocity_delta; + if (track.*.velocity < -1000000000) { + track.*.velocity = -1000000000; + track.*.velocity_remainder = 0; + } + if (track.*.velocity > 1000000000) { + track.*.velocity = 1000000000; + track.*.velocity_remainder = 0; + } + let position_numerator: i64 = track.*.velocity * 1000 + track.*.position_remainder; + let position_delta: i64 = position_numerator / 1000000; + track.*.position_remainder = position_numerator - position_delta * 1000000; + track.*.value = track.*.value + position_delta; + if (track.*.value < -1000000000) { + track.*.value = -1000000000; + track.*.position_remainder = 0; + } + if (track.*.value > 1000000000) { + track.*.value = 1000000000; + track.*.position_remainder = 0; + } + if (motion_abs(track.*.target - track.*.value) <= track.*.spring.position_epsilon && + motion_abs(track.*.velocity) <= track.*.spring.velocity_epsilon) { + track.*.value = track.*.target; + track.*.velocity = 0; + track.*.status = MotionStatus.settled; + track.*.remainder_us = 0; + track.*.velocity_remainder = 0; + track.*.position_remainder = 0; + } +} + +fn motion_advance_spring(track: *MotionTrack, delta_us: i64) void { + let remaining: i64 = delta_us + track.*.remainder_us; + while (remaining >= 1000 && track.*.status == MotionStatus.running) { + motion_spring_step(track); + remaining = remaining - 1000; + } + if (track.*.status == MotionStatus.running) { track.*.remainder_us = remaining; } +} + +fn motion_advance_keyframes(track: *MotionTrack, delta_us: i64) void { + track.*.elapsed_us = track.*.elapsed_us + delta_us; + let last: MotionKeyframe = track.*.keyframes.data[track.*.keyframes.len - 1]; + if (track.*.elapsed_us >= last.time_us) { + track.*.elapsed_us = last.time_us; + track.*.value = last.value; + track.*.velocity = 0; + track.*.status = MotionStatus.settled; + return; + } + let i: i32 = 1; + while (i < track.*.keyframes.len && track.*.keyframes.data[i].time_us < track.*.elapsed_us) { + i = i + 1; + } + let before: MotionKeyframe = track.*.keyframes.data[i - 1]; + let after: MotionKeyframe = track.*.keyframes.data[i]; + let duration: i64 = after.time_us - before.time_us; + let elapsed: i64 = track.*.elapsed_us - before.time_us; + let difference: i64 = after.value - before.value; + track.*.value = before.value + (difference * elapsed) / duration; + track.*.velocity = (difference * 1000000) / duration; +} + +fn motion_tick(scheduler: *MotionScheduler, time_us: i64) MotionTickResult { + let failed: MotionTickResult = MotionTickResult{ + .error = MotionError.none, + .did_work = 0, + .updated_tracks = 0, + .running_tracks = motion_running_count(scheduler.*), + .request_next_frame = 0, + .frame_interval_us = scheduler.*.frame_interval_us, + }; + if (scheduler.*.has_clock == 0) { + let error: MotionError = motion_set_clock(scheduler, time_us); + failed.error = error; + failed.running_tracks = motion_running_count(scheduler.*); + failed.request_next_frame = (failed.running_tracks > 0) as i32; + return failed; + } + if (time_us < scheduler.*.now_us) { + failed.error = motion_fail(scheduler, MotionError.clock_regression, 0); + return failed; + } + let delta_us: i64 = time_us - scheduler.*.now_us; + if (delta_us > 1000000) { + failed.error = motion_fail(scheduler, MotionError.clock_step_too_large, 0); + return failed; + } + if (delta_us == 0 || failed.running_tracks == 0) { + scheduler.*.now_us = time_us; + failed.request_next_frame = (failed.running_tracks > 0) as i32; + motion_clear_error(scheduler); + return failed; + } + + scheduler.*.now_us = time_us; + let updated: i32 = 0; + let i: i32 = 0; + while (i < scheduler.*.tracks.len) { + if (scheduler.*.tracks.data[i].status == MotionStatus.running) { + if (scheduler.*.tracks.data[i].kind == MotionKind.spring) { + motion_advance_spring(&scheduler.*.tracks.data[i], delta_us); + } else { + motion_advance_keyframes(&scheduler.*.tracks.data[i], delta_us); + } + updated = updated + 1; + } + i = i + 1; + } + motion_record(scheduler, MotionFrameReason.tick, 0); + let running: i32 = motion_running_count(scheduler.*); + motion_clear_error(scheduler); + return MotionTickResult{ + .error = MotionError.none, + .did_work = 1, + .updated_tracks = updated, + .running_tracks = running, + .request_next_frame = (running > 0) as i32, + .frame_interval_us = scheduler.*.frame_interval_us, + }; +} + +fn motion_retarget( + scheduler: *MotionScheduler, + id: i64, + target: i64, + reason: MotionFrameReason, + preference: MotionPreference, + substitution: MotionReducedSubstitution, +) MotionError { + let index: i32 = motion_find_index(scheduler.*, id); + if (index < 0) { return motion_fail(scheduler, MotionError.missing_track, id); } + if (scheduler.*.tracks.data[index].kind != MotionKind.spring || motion_value_valid(target) == 0 || + (reason != MotionFrameReason.interrupt && reason != MotionFrameReason.reverse && reason != MotionFrameReason.resize)) { + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + if (preference == MotionPreference.reduced && substitution != MotionReducedSubstitution.snap_to_end) { + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + scheduler.*.tracks.data[index].target = target; + scheduler.*.tracks.data[index].status = MotionStatus.running; + scheduler.*.tracks.data[index].remainder_us = 0; + scheduler.*.tracks.data[index].velocity_remainder = 0; + if (preference == MotionPreference.reduced && substitution == MotionReducedSubstitution.snap_to_end) { + scheduler.*.tracks.data[index].value = target; + scheduler.*.tracks.data[index].velocity = 0; + scheduler.*.tracks.data[index].status = MotionStatus.settled; + scheduler.*.tracks.data[index].remainder_us = 0; + scheduler.*.tracks.data[index].velocity_remainder = 0; + scheduler.*.tracks.data[index].position_remainder = 0; + motion_record(scheduler, MotionFrameReason.reduced_motion, id); + } else { + motion_record(scheduler, reason, id); + } + motion_clear_error(scheduler); + return MotionError.none; +} + +fn motion_gesture_handoff( + scheduler: *MotionScheduler, + id: i64, + value: i64, + velocity: i64, + target: i64, +) MotionError { + let index: i32 = motion_find_index(scheduler.*, id); + if (index < 0) { return motion_fail(scheduler, MotionError.missing_track, id); } + if (scheduler.*.tracks.data[index].kind != MotionKind.spring || motion_value_valid(value) == 0 || + motion_value_valid(velocity) == 0 || motion_value_valid(target) == 0) { + return motion_fail(scheduler, MotionError.invalid_spec, id); + } + scheduler.*.tracks.data[index].value = value; + scheduler.*.tracks.data[index].velocity = velocity; + scheduler.*.tracks.data[index].target = target; + scheduler.*.tracks.data[index].status = MotionStatus.running; + scheduler.*.tracks.data[index].remainder_us = 0; + scheduler.*.tracks.data[index].velocity_remainder = 0; + scheduler.*.tracks.data[index].position_remainder = 0; + motion_record(scheduler, MotionFrameReason.gesture_handoff, id); + motion_clear_error(scheduler); + return MotionError.none; +} + +fn motion_sample(scheduler: MotionScheduler, id: i64) MotionSample { + let index: i32 = motion_find_index(scheduler, id); + if (index >= 0) { + let track: MotionTrack = scheduler.tracks.data[index]; + return MotionSample{ + .found = 1, + .id = track.id, + .kind = track.kind, + .status = track.status, + .value = track.value, + .velocity = track.velocity, + .target = track.target, + .elapsed_us = track.elapsed_us, + }; + } + return MotionSample{ + .found = 0, + .id = 0, + .kind = MotionKind.spring, + .status = MotionStatus.settled, + .value = 0, + .velocity = 0, + .target = 0, + .elapsed_us = 0, + }; +} + +fn motion_scheduler_hash(scheduler: MotionScheduler) i64 { + let hash: i64 = display_mix(17, scheduler.now_us); + hash = display_mix(hash, scheduler.has_clock as i64); + hash = display_mix(hash, scheduler.refresh_hz); + hash = display_mix(hash, scheduler.frame_interval_us); + hash = display_mix(hash, scheduler.next_sequence); + hash = display_mix(hash, scheduler.last_error as i64); + hash = display_mix(hash, scheduler.error_track); + hash = display_mix(hash, scheduler.tracks.len as i64); + let i: i32 = 0; + while (i < scheduler.tracks.len) { + let track: MotionTrack = scheduler.tracks.data[i]; + hash = display_mix(hash, track.id); + hash = display_mix(hash, track.kind as i64); + hash = display_mix(hash, track.status as i64); + hash = display_mix(hash, track.value); + hash = display_mix(hash, track.velocity); + hash = display_mix(hash, track.target); + hash = display_mix(hash, track.spring.stiffness); + hash = display_mix(hash, track.spring.damping); + hash = display_mix(hash, track.spring.position_epsilon); + hash = display_mix(hash, track.spring.velocity_epsilon); + hash = display_mix(hash, track.elapsed_us); + hash = display_mix(hash, track.remainder_us); + hash = display_mix(hash, track.velocity_remainder); + hash = display_mix(hash, track.position_remainder); + hash = display_mix(hash, track.keyframes.len as i64); + let j: i32 = 0; + while (j < track.keyframes.len) { + hash = display_mix(hash, track.keyframes.data[j].time_us); + hash = display_mix(hash, track.keyframes.data[j].value); + j = j + 1; + } + i = i + 1; + } + hash = display_mix(hash, scheduler.events.len as i64); + i = 0; + while (i < scheduler.events.len) { + let event: MotionEvent = scheduler.events.data[i]; + hash = display_mix(hash, event.sequence); + hash = display_mix(hash, event.reason as i64); + hash = display_mix(hash, event.track_id); + hash = display_mix(hash, event.time_us); + hash = display_mix(hash, event.value); + hash = display_mix(hash, event.velocity); + hash = display_mix(hash, event.target); + i = i + 1; + } + return hash; +} diff --git a/src/render/cpu_raster.zag b/src/render/cpu_raster.zag new file mode 100644 index 0000000..b6ab9cb --- /dev/null +++ b/src/render/cpu_raster.zag @@ -0,0 +1,832 @@ +@import("std:list") +@import("display_list.zag") + +enum CpuRasterError { + none, + invalid_surface, + unsealed_display_list, + mutated_display_list, + invalid_geometry, + work_limit, + unsupported_operation, +} + +struct CpuSurface { + width: i32, + height: i32, + pixels: ArrayList[u8], +} + +struct CpuRasterState { + clip: Rect, + transform: Transform, +} + +struct CpuRasterResult { + surface: CpuSurface, + error: CpuRasterError, + error_op: i32, + rendered_ops: i32, + pixels_touched: i64, +} + +struct CpuPathEdge { + start: PathPoint, + end: PathPoint, +} + +struct CpuPathBuildResult { + edges: ArrayList[CpuPathEdge], + bounds: Rect, + error: CpuRasterError, +} + +struct CpuImageSample { + red_premultiplied: i64, + green_premultiplied: i64, + blue_premultiplied: i64, + alpha: i64, +} + +fn cpu_surface_make(width: i32, height: i32) CpuSurface { + let pixels: ArrayList[u8] = make[u8](1); + if (width <= 0 || height <= 0 || width > 8192 || height > 8192) { + return CpuSurface{ .width = 0, .height = 0, .pixels = pixels }; + } + let byte_count: i64 = (width as i64) * (height as i64) * 4; + if (byte_count <= 0 || byte_count > 268435456) { + return CpuSurface{ .width = 0, .height = 0, .pixels = pixels }; + } + free[u8](&pixels); + pixels = make[u8](byte_count as i32); + let i: i64 = 0; + while (i < byte_count) { push[u8](&pixels, 0); i = i + 1; } + return CpuSurface{ .width = width, .height = height, .pixels = pixels }; +} + +fn cpu_surface_free(surface: *CpuSurface) void { + free[u8](&surface.*.pixels); +} + +fn cpu_raster_free(result: *CpuRasterResult) void { + cpu_surface_free(&result.*.surface); +} + +fn cpu_surface_hash(surface: CpuSurface) i64 { + let hash: i64 = display_mix(display_mix(17, surface.width as i64), surface.height as i64); + let i: i32 = 0; + while (i < surface.pixels.len) { + hash = display_mix(hash, surface.pixels.data[i] as i64); + i = i + 1; + } + return hash; +} + +fn cpu_result(surface: CpuSurface, error: CpuRasterError, error_op: i32, rendered_ops: i32, pixels_touched: i64) CpuRasterResult { + return CpuRasterResult{ + .surface = surface, + .error = error, + .error_op = error_op, + .rendered_ops = rendered_ops, + .pixels_touched = pixels_touched, + }; +} + +fn cpu_rect_intersection(a: Rect, b: Rect) Rect { + let left: i64 = a.x; + if (b.x > left) { left = b.x; } + let top: i64 = a.y; + if (b.y > top) { top = b.y; } + let right: i64 = a.x + a.width; + if (b.x + b.width < right) { right = b.x + b.width; } + let bottom: i64 = a.y + a.height; + if (b.y + b.height < bottom) { bottom = b.y + b.height; } + if (right <= left || bottom <= top) { return rect(0, 0, 0, 0); } + return rect(left, top, right - left, bottom - top); +} + +fn cpu_fixed_safe(value: i64) i32 { + return (value >= -1000000000000 && value <= 1000000000000) as i32; +} + +fn cpu_transform_safe(transform: Transform) i32 { + let max_scale: i64 = 4096 * unit_scale(); + return (transform.scale_x > 0 && transform.scale_x <= max_scale && + transform.scale_y > 0 && transform.scale_y <= max_scale && + transform.skew_x == 0 && transform.skew_y == 0 && + cpu_fixed_safe(transform.translate_x) != 0 && + cpu_fixed_safe(transform.translate_y) != 0) as i32; +} + +fn cpu_transform_rect(value: Rect, transform: Transform) Rect { + return rect( + (value.x * transform.scale_x) / unit_scale() + transform.translate_x, + (value.y * transform.scale_y) / unit_scale() + transform.translate_y, + (value.width * transform.scale_x) / unit_scale(), + (value.height * transform.scale_y) / unit_scale() + ); +} + +fn cpu_compose_transform(current: Transform, next: Transform) Transform { + return Transform{ + .scale_x = (current.scale_x * next.scale_x) / unit_scale(), + .skew_x = 0, + .skew_y = 0, + .scale_y = (current.scale_y * next.scale_y) / unit_scale(), + .translate_x = current.translate_x + (current.scale_x * next.translate_x) / unit_scale(), + .translate_y = current.translate_y + (current.scale_y * next.translate_y) / unit_scale(), + }; +} + +fn cpu_path_coordinate_limit() i64 { return 1000000000; } +fn cpu_path_edge_limit() i32 { return 262144; } +fn cpu_path_sample_axis() i32 { return 8; } +fn cpu_path_work_limit() i64 { return 50000000; } + +fn cpu_path_point_safe(point: PathPoint) i32 { + let limit: i64 = cpu_path_coordinate_limit(); + return (point.x >= 0 - limit && point.x <= limit && + point.y >= 0 - limit && point.y <= limit) as i32; +} + +fn cpu_transform_path_point(point: PathPoint, transform: Transform) PathPoint { + return path_point( + (point.x * transform.scale_x) / unit_scale() + transform.translate_x, + (point.y * transform.scale_y) / unit_scale() + transform.translate_y + ); +} + +fn cpu_path_empty_build() CpuPathBuildResult { + return CpuPathBuildResult{ + .edges = make[CpuPathEdge](32), + .bounds = rect(0, 0, 0, 0), + .error = CpuRasterError.none, + }; +} + +fn cpu_path_include_point(result: *CpuPathBuildResult, point: PathPoint) void { + if (result.*.edges.len == 0 && result.*.bounds.width == 0 && + result.*.bounds.height == 0) { + result.*.bounds = rect(point.x, point.y, 0, 0); + return; + } + let left: i64 = result.*.bounds.x; + let top: i64 = result.*.bounds.y; + let right: i64 = result.*.bounds.x + result.*.bounds.width; + let bottom: i64 = result.*.bounds.y + result.*.bounds.height; + if (point.x < left) { left = point.x; } + if (point.y < top) { top = point.y; } + if (point.x > right) { right = point.x; } + if (point.y > bottom) { bottom = point.y; } + result.*.bounds = rect(left, top, right - left, bottom - top); +} + +fn cpu_path_add_edge(result: *CpuPathBuildResult, local_start: PathPoint, + local_end: PathPoint, transform: Transform) i32 { + let start: PathPoint = cpu_transform_path_point(local_start, transform); + let end: PathPoint = cpu_transform_path_point(local_end, transform); + if (cpu_path_point_safe(start) == 0 || cpu_path_point_safe(end) == 0) { + result.*.error = CpuRasterError.invalid_geometry; + return 0; + } + if (start.x == end.x && start.y == end.y) { return 1; } + if (result.*.edges.len >= cpu_path_edge_limit()) { + result.*.error = CpuRasterError.work_limit; + return 0; + } + cpu_path_include_point(result, start); + push[CpuPathEdge](&result.*.edges, CpuPathEdge{ .start = start, .end = end }); + cpu_path_include_point(result, end); + return 1; +} + +fn cpu_quad_point(start: PathPoint, control: PathPoint, end: PathPoint, + step: i32, divisions: i32) PathPoint { + let inverse: i64 = (divisions - step) as i64; + let progress: i64 = step as i64; + let denominator: i64 = (divisions as i64) * (divisions as i64); + return path_point( + (inverse * inverse * start.x + 2 * inverse * progress * control.x + + progress * progress * end.x) / denominator, + (inverse * inverse * start.y + 2 * inverse * progress * control.y + + progress * progress * end.y) / denominator + ); +} + +fn cpu_cubic_point(start: PathPoint, control_1: PathPoint, + control_2: PathPoint, end: PathPoint, step: i32, divisions: i32) PathPoint { + let inverse: i64 = (divisions - step) as i64; + let progress: i64 = step as i64; + let denominator: i64 = (divisions as i64) * (divisions as i64) * (divisions as i64); + return path_point( + (inverse * inverse * inverse * start.x + + 3 * inverse * inverse * progress * control_1.x + + 3 * inverse * progress * progress * control_2.x + + progress * progress * progress * end.x) / denominator, + (inverse * inverse * inverse * start.y + + 3 * inverse * inverse * progress * control_1.y + + 3 * inverse * progress * progress * control_2.y + + progress * progress * progress * end.y) / denominator + ); +} + +fn cpu_path_build(path: PathData, transform: Transform) CpuPathBuildResult { + let result: CpuPathBuildResult = cpu_path_empty_build(); + let current: PathPoint = path_zero_point(); + let contour_start: PathPoint = path_zero_point(); + let contour_open: i32 = 0; + let i: i32 = 0; + while (i < path.commands.len) { + let command: PathCommand = path.commands.data[i]; + if (command.verb == PathVerb.move_to) { + if (contour_open != 0 && + cpu_path_add_edge(&result, current, contour_start, transform) == 0) { + return result; + } + current = command.point_1; + contour_start = current; + contour_open = 1; + } else if (command.verb == PathVerb.line_to) { + if (cpu_path_add_edge(&result, current, command.point_1, transform) == 0) { + return result; + } + current = command.point_1; + } else if (command.verb == PathVerb.quad_to) { + let start: PathPoint = current; + let previous: PathPoint = current; + let step: i32 = 1; + while (step <= 16) { + let next: PathPoint = cpu_quad_point(start, command.point_1, + command.point_2, step, 16); + if (cpu_path_add_edge(&result, previous, next, transform) == 0) { + return result; + } + previous = next; + step = step + 1; + } + current = command.point_2; + } else if (command.verb == PathVerb.cubic_to) { + let start: PathPoint = current; + let previous: PathPoint = current; + let step: i32 = 1; + while (step <= 16) { + let next: PathPoint = cpu_cubic_point(start, command.point_1, + command.point_2, command.point_3, step, 16); + if (cpu_path_add_edge(&result, previous, next, transform) == 0) { + return result; + } + previous = next; + step = step + 1; + } + current = command.point_3; + } else if (command.verb == PathVerb.close) { + if (cpu_path_add_edge(&result, current, contour_start, transform) == 0) { + return result; + } + current = contour_start; + contour_open = 0; + } + i = i + 1; + } + if (contour_open != 0 && + cpu_path_add_edge(&result, current, contour_start, transform) == 0) { + return result; + } + return result; +} + +fn cpu_path_contains(edges: ArrayList[CpuPathEdge], fill_rule: PathFillRule, + x: i64, y: i64) i32 { + let winding: i32 = 0; + let crossings: i32 = 0; + let i: i32 = 0; + while (i < edges.len) { + let edge: CpuPathEdge = edges.data[i]; + let delta_x: i64 = edge.end.x - edge.start.x; + let delta_y: i64 = edge.end.y - edge.start.y; + let cross: i64 = delta_x * (y - edge.start.y) - + (x - edge.start.x) * delta_y; + if (edge.start.y <= y && edge.end.y > y && cross > 0) { + winding = winding + 1; + crossings = crossings + 1; + } else if (edge.end.y <= y && edge.start.y > y && cross < 0) { + winding = winding - 1; + crossings = crossings + 1; + } + i = i + 1; + } + if (fill_rule == PathFillRule.even_odd) { return (crossings % 2 != 0) as i32; } + return (winding != 0) as i32; +} + +fn cpu_fill_path(surface: *CpuSurface, path: PathData, transform: Transform, + clip: Rect, paint: Paint) CpuRasterResult { + let built: CpuPathBuildResult = cpu_path_build(path, transform); + if (built.error != CpuRasterError.none) { + free[CpuPathEdge](&built.edges); + return cpu_result(surface.*, built.error, 0 - 1, 0, 0); + } + if (built.edges.len == 0 || built.bounds.width <= 0 || built.bounds.height <= 0) { + free[CpuPathEdge](&built.edges); + return cpu_result(surface.*, CpuRasterError.none, 0 - 1, 0, 0); + } + let surface_bounds: Rect = rect(0, 0, (surface.*.width as i64) * unit_scale(), + (surface.*.height as i64) * unit_scale()); + let bounds: Rect = cpu_rect_intersection( + cpu_rect_intersection(built.bounds, clip), surface_bounds + ); + if (bounds.width <= 0 || bounds.height <= 0) { + free[CpuPathEdge](&built.edges); + return cpu_result(surface.*, CpuRasterError.none, 0 - 1, 0, 0); + } + let first_x: i32 = (bounds.x / unit_scale()) as i32; + let first_y: i32 = (bounds.y / unit_scale()) as i32; + let last_x: i32 = ((bounds.x + bounds.width + unit_scale() - 1) / unit_scale()) as i32; + let last_y: i32 = ((bounds.y + bounds.height + unit_scale() - 1) / unit_scale()) as i32; + let pixel_count: i64 = ((last_x - first_x) as i64) * ((last_y - first_y) as i64); + let samples: i64 = (cpu_path_sample_axis() as i64) * (cpu_path_sample_axis() as i64); + if (built.edges.len > 0 && (pixel_count <= 0 || + pixel_count > cpu_path_work_limit() / samples / (built.edges.len as i64))) { + free[CpuPathEdge](&built.edges); + return cpu_result(surface.*, CpuRasterError.work_limit, 0 - 1, 0, 0); + } + + let touched: i64 = 0; + let y: i32 = first_y; + while (y < last_y) { + let x: i32 = first_x; + while (x < last_x) { + let inside: i64 = 0; + let sample_y: i32 = 0; + while (sample_y < cpu_path_sample_axis()) { + let point_y: i64 = (y as i64) * unit_scale() + + ((sample_y as i64) * unit_scale() + unit_scale() / 2) / + (cpu_path_sample_axis() as i64); + let sample_x: i32 = 0; + while (sample_x < cpu_path_sample_axis()) { + let point_x: i64 = (x as i64) * unit_scale() + + ((sample_x as i64) * unit_scale() + unit_scale() / 2) / + (cpu_path_sample_axis() as i64); + if (point_x >= clip.x && point_x < clip.x + clip.width && + point_y >= clip.y && point_y < clip.y + clip.height && + cpu_path_contains(built.edges, path.fill_rule, point_x, point_y) != 0) { + inside = inside + 1; + } + sample_x = sample_x + 1; + } + sample_y = sample_y + 1; + } + if (inside > 0) { + cpu_blend_pixel(surface, x, y, paint, (inside * 4096) / samples); + touched = touched + 1; + } + x = x + 1; + } + y = y + 1; + } + free[CpuPathEdge](&built.edges); + return cpu_result(surface.*, CpuRasterError.none, 0 - 1, 0, touched); +} + +fn cpu_blend_pixel(surface: *CpuSurface, x: i32, y: i32, paint: Paint, coverage: i64) void { + if (coverage <= 0) { return; } + let index: i32 = (y * surface.*.width + x) * 4; + let source_r: i64 = (paint.red * 255 + 32767) / 65535; + let source_g: i64 = (paint.green * 255 + 32767) / 65535; + let source_b: i64 = (paint.blue * 255 + 32767) / 65535; + let paint_alpha: i64 = (paint.alpha * 255 + 32767) / 65535; + let source_a: i64 = (paint_alpha * coverage + 2048) / 4096; + let dest_r: i64 = surface.*.pixels.data[index] as i64; + let dest_g: i64 = surface.*.pixels.data[index + 1] as i64; + let dest_b: i64 = surface.*.pixels.data[index + 2] as i64; + let dest_a: i64 = surface.*.pixels.data[index + 3] as i64; + let inverse_a: i64 = 255 - source_a; + let out_alpha_numerator: i64 = source_a * 255 + dest_a * inverse_a; + if (out_alpha_numerator <= 0) { + surface.*.pixels.data[index] = 0; + surface.*.pixels.data[index + 1] = 0; + surface.*.pixels.data[index + 2] = 0; + surface.*.pixels.data[index + 3] = 0; + return; + } + let out_r_numerator: i64 = source_r * source_a * 255 + dest_r * dest_a * inverse_a; + let out_g_numerator: i64 = source_g * source_a * 255 + dest_g * dest_a * inverse_a; + let out_b_numerator: i64 = source_b * source_a * 255 + dest_b * dest_a * inverse_a; + surface.*.pixels.data[index] = ((out_r_numerator + out_alpha_numerator / 2) / out_alpha_numerator) as u8; + surface.*.pixels.data[index + 1] = ((out_g_numerator + out_alpha_numerator / 2) / out_alpha_numerator) as u8; + surface.*.pixels.data[index + 2] = ((out_b_numerator + out_alpha_numerator / 2) / out_alpha_numerator) as u8; + surface.*.pixels.data[index + 3] = ((out_alpha_numerator + 127) / 255) as u8; +} + +fn cpu_fill_rect(surface: *CpuSurface, raw_bounds: Rect, clip: Rect, paint: Paint) i64 { + let surface_bounds: Rect = rect(0, 0, (surface.*.width as i64) * unit_scale(), (surface.*.height as i64) * unit_scale()); + let bounds: Rect = cpu_rect_intersection(cpu_rect_intersection(raw_bounds, clip), surface_bounds); + if (bounds.width <= 0 || bounds.height <= 0) { return 0; } + let first_x: i32 = (bounds.x / unit_scale()) as i32; + let first_y: i32 = (bounds.y / unit_scale()) as i32; + let last_x: i32 = ((bounds.x + bounds.width + unit_scale() - 1) / unit_scale()) as i32; + let last_y: i32 = ((bounds.y + bounds.height + unit_scale() - 1) / unit_scale()) as i32; + let touched: i64 = 0; + let y: i32 = first_y; + while (y < last_y) { + let pixel_top: i64 = (y as i64) * unit_scale(); + let overlap_top: i64 = bounds.y; + if (pixel_top > overlap_top) { overlap_top = pixel_top; } + let overlap_bottom: i64 = bounds.y + bounds.height; + if (pixel_top + unit_scale() < overlap_bottom) { overlap_bottom = pixel_top + unit_scale(); } + let x: i32 = first_x; + while (x < last_x) { + let pixel_left: i64 = (x as i64) * unit_scale(); + let overlap_left: i64 = bounds.x; + if (pixel_left > overlap_left) { overlap_left = pixel_left; } + let overlap_right: i64 = bounds.x + bounds.width; + if (pixel_left + unit_scale() < overlap_right) { overlap_right = pixel_left + unit_scale(); } + let coverage: i64 = (overlap_right - overlap_left) * (overlap_bottom - overlap_top); + if (coverage > 0) { + cpu_blend_pixel(surface, x, y, paint, coverage); + touched = touched + 1; + } + x = x + 1; + } + y = y + 1; + } + return touched; +} + +fn cpu_rect_pixel_coverage(bounds: Rect, x: i32, y: i32) i64 { + if (bounds.width <= 0 || bounds.height <= 0) { return 0; } + let pixel_left: i64 = (x as i64) * unit_scale(); + let pixel_top: i64 = (y as i64) * unit_scale(); + let overlap_left: i64 = bounds.x; + if (pixel_left > overlap_left) { overlap_left = pixel_left; } + let overlap_top: i64 = bounds.y; + if (pixel_top > overlap_top) { overlap_top = pixel_top; } + let overlap_right: i64 = bounds.x + bounds.width; + if (pixel_left + unit_scale() < overlap_right) { + overlap_right = pixel_left + unit_scale(); + } + let overlap_bottom: i64 = bounds.y + bounds.height; + if (pixel_top + unit_scale() < overlap_bottom) { + overlap_bottom = pixel_top + unit_scale(); + } + if (overlap_right <= overlap_left || overlap_bottom <= overlap_top) { return 0; } + return (overlap_right - overlap_left) * (overlap_bottom - overlap_top); +} + +fn cpu_stroke_rect(surface: *CpuSurface, raw_bounds: Rect, clip: Rect, + transform: Transform, paint: Paint) i64 { + let low: i64 = paint.stroke_width / 2; + let high: i64 = paint.stroke_width - low; + let outer_local: Rect = rect( + raw_bounds.x - low, + raw_bounds.y - low, + raw_bounds.width + paint.stroke_width, + raw_bounds.height + paint.stroke_width + ); + let inner_local: Rect = rect( + raw_bounds.x + high, + raw_bounds.y + high, + raw_bounds.width - paint.stroke_width, + raw_bounds.height - paint.stroke_width + ); + let outer: Rect = cpu_rect_intersection( + cpu_rect_intersection(cpu_transform_rect(outer_local, transform), clip), + rect(0, 0, (surface.*.width as i64) * unit_scale(), + (surface.*.height as i64) * unit_scale()) + ); + let inner: Rect = rect(0, 0, 0, 0); + if (inner_local.width > 0 && inner_local.height > 0) { + inner = cpu_rect_intersection( + cpu_rect_intersection(cpu_transform_rect(inner_local, transform), clip), + rect(0, 0, (surface.*.width as i64) * unit_scale(), + (surface.*.height as i64) * unit_scale()) + ); + } + if (outer.width <= 0 || outer.height <= 0) { return 0; } + let first_x: i32 = (outer.x / unit_scale()) as i32; + let first_y: i32 = (outer.y / unit_scale()) as i32; + let last_x: i32 = ((outer.x + outer.width + unit_scale() - 1) / unit_scale()) as i32; + let last_y: i32 = ((outer.y + outer.height + unit_scale() - 1) / unit_scale()) as i32; + let touched: i64 = 0; + let y: i32 = first_y; + while (y < last_y) { + let x: i32 = first_x; + while (x < last_x) { + let coverage: i64 = cpu_rect_pixel_coverage(outer, x, y) - + cpu_rect_pixel_coverage(inner, x, y); + if (coverage > 0) { + cpu_blend_pixel(surface, x, y, paint, coverage); + touched = touched + 1; + } + x = x + 1; + } + y = y + 1; + } + return touched; +} + +fn cpu_image_texel(resource: RenderResource, x: i32, y: i32, + channel: i32) i64 { + let index: i32 = ((y * resource.width as i32 + x) * 4) + channel; + return resource.payload.data[index] as i64; +} + +fn cpu_image_bilinear(value_00: i64, value_10: i64, + value_01: i64, value_11: i64, fraction_x: i64, fraction_y: i64) i64 { + let inverse_x: i64 = 256 - fraction_x; + let inverse_y: i64 = 256 - fraction_y; + let top: i64 = value_00 * inverse_x + value_10 * fraction_x; + let bottom: i64 = value_01 * inverse_x + value_11 * fraction_x; + return (top * inverse_y + bottom * fraction_y + 32768) / 65536; +} + +fn cpu_image_sample(resource: RenderResource, source_x: i64, + source_y: i64) CpuImageSample { + let max_x: i64 = (resource.width - 1) * 256; + let max_y: i64 = (resource.height - 1) * 256; + let clamped_x: i64 = source_x; + let clamped_y: i64 = source_y; + if (clamped_x < 0) { clamped_x = 0; } + if (clamped_y < 0) { clamped_y = 0; } + if (clamped_x > max_x) { clamped_x = max_x; } + if (clamped_y > max_y) { clamped_y = max_y; } + let x0: i32 = (clamped_x / 256) as i32; + let y0: i32 = (clamped_y / 256) as i32; + let x1: i32 = x0; + let y1: i32 = y0; + if (x0 + 1 < resource.width as i32) { x1 = x0 + 1; } + if (y0 + 1 < resource.height as i32) { y1 = y0 + 1; } + let fraction_x: i64 = clamped_x % 256; + let fraction_y: i64 = clamped_y % 256; + let alpha_00: i64 = cpu_image_texel(resource, x0, y0, 3); + let alpha_10: i64 = cpu_image_texel(resource, x1, y0, 3); + let alpha_01: i64 = cpu_image_texel(resource, x0, y1, 3); + let alpha_11: i64 = cpu_image_texel(resource, x1, y1, 3); + return CpuImageSample{ + .red_premultiplied = cpu_image_bilinear( + cpu_image_texel(resource, x0, y0, 0) * alpha_00, + cpu_image_texel(resource, x1, y0, 0) * alpha_10, + cpu_image_texel(resource, x0, y1, 0) * alpha_01, + cpu_image_texel(resource, x1, y1, 0) * alpha_11, + fraction_x, fraction_y + ), + .green_premultiplied = cpu_image_bilinear( + cpu_image_texel(resource, x0, y0, 1) * alpha_00, + cpu_image_texel(resource, x1, y0, 1) * alpha_10, + cpu_image_texel(resource, x0, y1, 1) * alpha_01, + cpu_image_texel(resource, x1, y1, 1) * alpha_11, + fraction_x, fraction_y + ), + .blue_premultiplied = cpu_image_bilinear( + cpu_image_texel(resource, x0, y0, 2) * alpha_00, + cpu_image_texel(resource, x1, y0, 2) * alpha_10, + cpu_image_texel(resource, x0, y1, 2) * alpha_01, + cpu_image_texel(resource, x1, y1, 2) * alpha_11, + fraction_x, fraction_y + ), + .alpha = cpu_image_bilinear(alpha_00, alpha_10, alpha_01, alpha_11, + fraction_x, fraction_y), + }; +} + +fn cpu_draw_image(surface: *CpuSurface, resource: RenderResource, + destination: Rect, clip: Rect, opacity: i64) CpuRasterResult { + if (resource.color_space != RenderColorSpace.srgb) { + return cpu_result(surface.*, CpuRasterError.unsupported_operation, + 0 - 1, 0, 0); + } + let coordinate_limit: i64 = cpu_path_coordinate_limit(); + if (destination.x < 0 - coordinate_limit || destination.x > coordinate_limit || + destination.y < 0 - coordinate_limit || destination.y > coordinate_limit || + destination.width <= 0 || destination.width > coordinate_limit || + destination.height <= 0 || destination.height > coordinate_limit) { + return cpu_result(surface.*, CpuRasterError.invalid_geometry, 0 - 1, 0, 0); + } + let surface_bounds: Rect = rect(0, 0, (surface.*.width as i64) * unit_scale(), + (surface.*.height as i64) * unit_scale()); + let bounds: Rect = cpu_rect_intersection( + cpu_rect_intersection(destination, clip), surface_bounds + ); + if (bounds.width <= 0 || bounds.height <= 0) { + return cpu_result(surface.*, CpuRasterError.none, 0 - 1, 0, 0); + } + let first_x: i32 = (bounds.x / unit_scale()) as i32; + let first_y: i32 = (bounds.y / unit_scale()) as i32; + let last_x: i32 = ((bounds.x + bounds.width + unit_scale() - 1) / unit_scale()) as i32; + let last_y: i32 = ((bounds.y + bounds.height + unit_scale() - 1) / unit_scale()) as i32; + let pixel_count: i64 = ((last_x - first_x) as i64) * ((last_y - first_y) as i64); + let sample_axis: i32 = 4; + let samples: i64 = (sample_axis as i64) * (sample_axis as i64); + if (pixel_count <= 0 || pixel_count > 50000000 / samples) { + return cpu_result(surface.*, CpuRasterError.work_limit, 0 - 1, 0, 0); + } + let touched: i64 = 0; + let y: i32 = first_y; + while (y < last_y) { + let x: i32 = first_x; + while (x < last_x) { + let inside: i64 = 0; + let sample_y: i32 = 0; + while (sample_y < sample_axis) { + let point_y: i64 = (y as i64) * unit_scale() + + ((sample_y as i64) * unit_scale() + unit_scale() / 2) / + (sample_axis as i64); + let sample_x: i32 = 0; + while (sample_x < sample_axis) { + let point_x: i64 = (x as i64) * unit_scale() + + ((sample_x as i64) * unit_scale() + unit_scale() / 2) / + (sample_axis as i64); + if (point_x >= destination.x && point_x < destination.x + destination.width && + point_y >= destination.y && point_y < destination.y + destination.height && + point_x >= clip.x && point_x < clip.x + clip.width && + point_y >= clip.y && point_y < clip.y + clip.height) { + inside = inside + 1; + } + sample_x = sample_x + 1; + } + sample_y = sample_y + 1; + } + if (inside > 0) { + let center_x: i64 = (x as i64) * unit_scale() + unit_scale() / 2; + let center_y: i64 = (y as i64) * unit_scale() + unit_scale() / 2; + if (center_x < destination.x) { center_x = destination.x; } + if (center_y < destination.y) { center_y = destination.y; } + if (center_x >= destination.x + destination.width) { + center_x = destination.x + destination.width - 1; + } + if (center_y >= destination.y + destination.height) { + center_y = destination.y + destination.height - 1; + } + let source_x: i64 = ((center_x - destination.x) * + (resource.width * 256)) / destination.width - 128; + let source_y: i64 = ((center_y - destination.y) * + (resource.height * 256)) / destination.height - 128; + let sample: CpuImageSample = cpu_image_sample(resource, source_x, source_y); + let alpha: i64 = (sample.alpha * inside + samples / 2) / samples; + if (alpha > 0 && sample.alpha > 0) { + let output_alpha: i64 = (alpha * opacity + 32767) / 65535; + let paint: Paint = paint_rgba16( + ((sample.red_premultiplied + sample.alpha / 2) / + sample.alpha) * 257, + ((sample.green_premultiplied + sample.alpha / 2) / + sample.alpha) * 257, + ((sample.blue_premultiplied + sample.alpha / 2) / + sample.alpha) * 257, + output_alpha * 257 + ); + if (output_alpha > 0) { + cpu_blend_pixel(surface, x, y, paint, 4096); + touched = touched + 1; + } + } + } + x = x + 1; + } + y = y + 1; + } + return cpu_result(surface.*, CpuRasterError.none, 0 - 1, 0, touched); +} + +fn cpu_rasterize(list: DisplayList, width: i32, height: i32) CpuRasterResult { + let surface: CpuSurface = cpu_surface_make(width, height); + if (surface.width == 0 || surface.height == 0) { + return cpu_result(surface, CpuRasterError.invalid_surface, 0 - 1, 0, 0); + } + if (list.sealed == 0) { + return cpu_result(surface, CpuRasterError.unsealed_display_list, 0 - 1, 0, 0); + } + if (display_list_verify(list) != DisplayError.none) { + return cpu_result(surface, CpuRasterError.mutated_display_list, 0 - 1, 0, 0); + } + + let stack: ArrayList[CpuRasterState] = make[CpuRasterState](16); + let current: CpuRasterState = CpuRasterState{ + .clip = rect(0, 0, (width as i64) * unit_scale(), (height as i64) * unit_scale()), + .transform = transform_identity(), + }; + let rendered_ops: i32 = 0; + let pixels_touched: i64 = 0; + let i: i32 = 0; + while (i < list.ops.len) { + let op: DisplayOp = list.ops.data[i]; + if (op.kind == DisplayOpKind.save) { + push[CpuRasterState](&stack, current); + } else if (op.kind == DisplayOpKind.restore) { + current = pop[CpuRasterState](&stack); + } else if (op.kind == DisplayOpKind.clip_rect) { + if (cpu_fixed_safe(op.bounds.x) == 0 || cpu_fixed_safe(op.bounds.y) == 0 || + cpu_fixed_safe(op.bounds.width) == 0 || cpu_fixed_safe(op.bounds.height) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, i, rendered_ops, pixels_touched); + } + let transformed: Rect = cpu_transform_rect(op.bounds, current.transform); + current.clip = cpu_rect_intersection(current.clip, transformed); + } else if (op.kind == DisplayOpKind.concat_transform) { + if (cpu_transform_safe(op.transform) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.unsupported_operation, i, rendered_ops, pixels_touched); + } + let composed: Transform = cpu_compose_transform(current.transform, op.transform); + if (cpu_transform_safe(composed) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, i, rendered_ops, pixels_touched); + } + current.transform = composed; + } else if (op.kind == DisplayOpKind.fill_rect) { + if (cpu_fixed_safe(op.bounds.x) == 0 || cpu_fixed_safe(op.bounds.y) == 0 || + cpu_fixed_safe(op.bounds.width) == 0 || cpu_fixed_safe(op.bounds.height) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, i, rendered_ops, pixels_touched); + } + let transformed: Rect = cpu_transform_rect(op.bounds, current.transform); + if (cpu_fixed_safe(transformed.x) == 0 || cpu_fixed_safe(transformed.y) == 0 || + cpu_fixed_safe(transformed.width) == 0 || cpu_fixed_safe(transformed.height) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, i, rendered_ops, pixels_touched); + } + pixels_touched = pixels_touched + cpu_fill_rect(&surface, transformed, current.clip, op.paint); + } else if (op.kind == DisplayOpKind.stroke_rect) { + let low: i64 = op.paint.stroke_width / 2; + let expanded: Rect = rect( + op.bounds.x - low, + op.bounds.y - low, + op.bounds.width + op.paint.stroke_width, + op.bounds.height + op.paint.stroke_width + ); + if (cpu_fixed_safe(expanded.x) == 0 || cpu_fixed_safe(expanded.y) == 0 || + cpu_fixed_safe(expanded.width) == 0 || cpu_fixed_safe(expanded.height) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, + i, rendered_ops, pixels_touched); + } + let transformed: Rect = cpu_transform_rect(expanded, current.transform); + if (cpu_fixed_safe(transformed.x) == 0 || cpu_fixed_safe(transformed.y) == 0 || + cpu_fixed_safe(transformed.width) == 0 || cpu_fixed_safe(transformed.height) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, + i, rendered_ops, pixels_touched); + } + pixels_touched = pixels_touched + cpu_stroke_rect( + &surface, op.bounds, current.clip, current.transform, op.paint + ); + } else if (op.kind == DisplayOpKind.draw_image) { + let resource_index: i32 = render_resource_find_index(list.resources, op.resource_id); + if (resource_index < 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.mutated_display_list, + i, rendered_ops, pixels_touched); + } + let transformed: Rect = cpu_transform_rect(op.bounds, current.transform); + if (cpu_fixed_safe(transformed.x) == 0 || cpu_fixed_safe(transformed.y) == 0 || + cpu_fixed_safe(transformed.width) == 0 || cpu_fixed_safe(transformed.height) == 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.invalid_geometry, + i, rendered_ops, pixels_touched); + } + let image_result: CpuRasterResult = cpu_draw_image( + &surface, list.resources.resources.data[resource_index], + transformed, current.clip, op.paint.alpha + ); + if (image_result.error != CpuRasterError.none) { + free[CpuRasterState](&stack); + return cpu_result(surface, image_result.error, i, + rendered_ops, pixels_touched); + } + pixels_touched = pixels_touched + image_result.pixels_touched; + } else if (op.kind == DisplayOpKind.draw_path) { + let resource_index: i32 = render_resource_find_index(list.resources, op.resource_id); + if (resource_index < 0) { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.mutated_display_list, + i, rendered_ops, pixels_touched); + } + let resource: RenderResource = list.resources.resources.data[resource_index]; + let decoded: PathDecodeResult = path_decode( + resource.payload.data[0..resource.payload.len] + ); + if (decoded.error != PathError.none) { + path_free(&decoded.path); + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.mutated_display_list, + i, rendered_ops, pixels_touched); + } + let path_result: CpuRasterResult = cpu_fill_path( + &surface, decoded.path, current.transform, current.clip, op.paint + ); + path_free(&decoded.path); + if (path_result.error != CpuRasterError.none) { + free[CpuRasterState](&stack); + return cpu_result(surface, path_result.error, i, + rendered_ops, pixels_touched); + } + pixels_touched = pixels_touched + path_result.pixels_touched; + } else { + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.unsupported_operation, i, rendered_ops, pixels_touched); + } + rendered_ops = rendered_ops + 1; + i = i + 1; + } + free[CpuRasterState](&stack); + return cpu_result(surface, CpuRasterError.none, 0 - 1, rendered_ops, pixels_touched); +} diff --git a/src/render/display_list.zag b/src/render/display_list.zag new file mode 100644 index 0000000..7bd480b --- /dev/null +++ b/src/render/display_list.zag @@ -0,0 +1,412 @@ +@import("std:list") +@import("../core/geometry.zag") +@import("../core/view_contract.zag") +@import("resources.zag") +@import("path.zag") +@import("image.zag") + +enum DisplayOpKind { + save, + restore, + clip_rect, + concat_transform, + fill_rect, + stroke_rect, + draw_path, + draw_image, + draw_glyph_run, + begin_layer, + end_layer, + apply_effect, +} + +enum DisplayError { + none, + not_sealed, + sealed, + content_mutated, + invalid_geometry, + invalid_resource, + invalid_parameter, + stack_underflow, + layer_underflow, + unbalanced_stack, + unbalanced_layers, +} + +struct Paint { + red: i64, + green: i64, + blue: i64, + alpha: i64, + stroke_width: i64, +} + +struct Transform { + scale_x: i64, + skew_x: i64, + skew_y: i64, + scale_y: i64, + translate_x: i64, + translate_y: i64, +} + +struct DisplayOp { + kind: DisplayOpKind, + owner: NodeKey, + bounds: Rect, + paint: Paint, + transform: Transform, + resource_id: i64, + parameter: i64, +} + +struct DisplayList { + ops: ArrayList[DisplayOp], + resources: RenderResourceStore, + sealed: i32, + stack_depth: i32, + layer_depth: i32, + revision: i64, + content_hash: i64, + last_error: DisplayError, + error_index: i32, +} + +fn display_color_channel(value: i64) i64 { + if (value < 0) { return 0; } + if (value > 65535) { return 65535; } + return value; +} + +fn display_fixed_value_valid(value: i64) i32 { + return (value >= 0 - unbounded_extent() && value <= unbounded_extent()) as i32; +} + +fn display_paint_valid(paint: Paint) i32 { + return (paint.red >= 0 && paint.red <= 65535 && + paint.green >= 0 && paint.green <= 65535 && + paint.blue >= 0 && paint.blue <= 65535 && + paint.alpha >= 0 && paint.alpha <= 65535 && + paint.stroke_width >= 0 && display_fixed_value_valid(paint.stroke_width) != 0) as i32; +} + +fn display_transform_valid(transform: Transform) i32 { + return (display_fixed_value_valid(transform.scale_x) != 0 && + display_fixed_value_valid(transform.skew_x) != 0 && + display_fixed_value_valid(transform.skew_y) != 0 && + display_fixed_value_valid(transform.scale_y) != 0 && + display_fixed_value_valid(transform.translate_x) != 0 && + display_fixed_value_valid(transform.translate_y) != 0) as i32; +} + +fn paint_rgba16(red: i64, green: i64, blue: i64, alpha: i64) Paint { + return Paint{ + .red = display_color_channel(red), + .green = display_color_channel(green), + .blue = display_color_channel(blue), + .alpha = display_color_channel(alpha), + .stroke_width = unit_scale(), + }; +} + +fn transform_identity() Transform { + return Transform{ + .scale_x = unit_scale(), + .skew_x = 0, + .skew_y = 0, + .scale_y = unit_scale(), + .translate_x = 0, + .translate_y = 0, + }; +} + +fn display_op(kind: DisplayOpKind, owner: NodeKey) DisplayOp { + return DisplayOp{ + .kind = kind, + .owner = owner, + .bounds = rect(0, 0, 0, 0), + .paint = paint_rgba16(0, 0, 0, 65535), + .transform = transform_identity(), + .resource_id = 0, + .parameter = 0, + }; +} + +fn display_list_make_with_resource_limits(max_resources: i32, + max_total_bytes: i64, max_payload_bytes: i64) DisplayList { + let resources: RenderResourceStore = render_resource_store_make( + max_resources, max_total_bytes, max_payload_bytes + ); + return DisplayList{ + .ops = make[DisplayOp](32), + .resources = resources, + .sealed = 0, + .stack_depth = 0, + .layer_depth = 0, + .revision = 0, + .content_hash = display_mix(17, resources.content_hash), + .last_error = DisplayError.none, + .error_index = 0 - 1, + }; +} + +fn display_list_make() DisplayList { + return display_list_make_with_resource_limits(4096, 268435456, 33554432); +} + +fn display_list_free(list: *DisplayList) void { + render_resource_store_free(&list.*.resources); + free[DisplayOp](&list.*.ops); +} + +fn display_mix(hash: i64, value: i64) i64 { + let modulus: i64 = 2147483647; + let component: i64 = value % modulus; + if (component < 0) { component = component + modulus; } + return (hash * 131 + component) % modulus; +} + +fn display_hash_op(hash: i64, op: DisplayOp) i64 { + let result: i64 = display_mix(hash, op.kind as i64); + result = display_mix(result, op.owner.value); + result = display_mix(result, op.owner.generation); + result = display_mix(result, op.bounds.x); + result = display_mix(result, op.bounds.y); + result = display_mix(result, op.bounds.width); + result = display_mix(result, op.bounds.height); + result = display_mix(result, op.paint.red); + result = display_mix(result, op.paint.green); + result = display_mix(result, op.paint.blue); + result = display_mix(result, op.paint.alpha); + result = display_mix(result, op.paint.stroke_width); + result = display_mix(result, op.transform.scale_x); + result = display_mix(result, op.transform.skew_x); + result = display_mix(result, op.transform.skew_y); + result = display_mix(result, op.transform.scale_y); + result = display_mix(result, op.transform.translate_x); + result = display_mix(result, op.transform.translate_y); + result = display_mix(result, op.resource_id); + return display_mix(result, op.parameter); +} + +fn display_list_calculate_hash(list: DisplayList) i64 { + let hash: i64 = display_mix(17, list.resources.content_hash); + let i: i32 = 0; + while (i < list.ops.len) { + hash = display_hash_op(hash, list.ops.data[i]); + i = i + 1; + } + return hash; +} + +fn display_list_add_resource(list: *DisplayList, + spec: RenderResourceSpec) RenderResourceError { + if (list.*.sealed != 0) { return RenderResourceError.sealed; } + let error: RenderResourceError = render_resource_add(&list.*.resources, spec); + if (error == RenderResourceError.none) { + list.*.revision = list.*.ops.len as i64 + list.*.resources.revision; + list.*.content_hash = display_list_calculate_hash(list.*); + list.*.last_error = DisplayError.none; + list.*.error_index = 0 - 1; + } + return error; +} + +fn display_list_replace_resource(list: *DisplayList, + spec: RenderResourceSpec) RenderResourceError { + if (list.*.sealed != 0) { return RenderResourceError.sealed; } + let error: RenderResourceError = render_resource_replace(&list.*.resources, spec); + if (error == RenderResourceError.none) { + list.*.revision = list.*.ops.len as i64 + list.*.resources.revision; + list.*.content_hash = display_list_calculate_hash(list.*); + list.*.last_error = DisplayError.none; + list.*.error_index = 0 - 1; + } + return error; +} + +fn display_fail(list: *DisplayList, error: DisplayError) DisplayError { + list.*.last_error = error; + list.*.error_index = list.*.ops.len; + return error; +} + +fn display_fail_at(list: *DisplayList, error: DisplayError, index: i32) DisplayError { + list.*.last_error = error; + list.*.error_index = index; + return error; +} + +fn display_expected_resource_kind(kind: DisplayOpKind) i64 { + return switch (kind) { + .draw_path => RenderResourceKind.path as i64, + .draw_image => RenderResourceKind.image as i64, + .draw_glyph_run => RenderResourceKind.glyph_run as i64, + .apply_effect => RenderResourceKind.effect as i64, + .save => 0 - 1, + .restore => 0 - 1, + .clip_rect => 0 - 1, + .concat_transform => 0 - 1, + .fill_rect => 0 - 1, + .stroke_rect => 0 - 1, + .begin_layer => 0 - 1, + .end_layer => 0 - 1, + }; +} + +fn display_resource_reference_valid(list: DisplayList, op: DisplayOp) i32 { + let expected: i64 = display_expected_resource_kind(op.kind); + if (expected < 0) { return 1; } + let index: i32 = render_resource_find_index(list.resources, op.resource_id); + if (index < 0) { return 0; } + let resource: RenderResource = list.resources.resources.data[index]; + if ((resource.kind as i64) != expected) { return 0; } + if (op.kind == DisplayOpKind.draw_path) { + return (resource.format_tag == path_resource_format_tag()) as i32; + } + return 1; +} + +fn display_resource_payload_valid(resource: RenderResource) i32 { + if (resource.kind == RenderResourceKind.path) { + return (resource.format_tag == path_resource_format_tag() && + path_payload_valid(resource.payload.data[0..resource.payload.len]) != 0) as i32; + } + if (resource.kind == RenderResourceKind.image) { + return (image_resource_validate(resource) == ImagePayloadError.none) as i32; + } + return 1; +} + +fn display_op_requires_bounds(kind: DisplayOpKind) i32 { + return (kind == DisplayOpKind.clip_rect || kind == DisplayOpKind.fill_rect || + kind == DisplayOpKind.stroke_rect || kind == DisplayOpKind.draw_image || + kind == DisplayOpKind.begin_layer) as i32; +} + +fn display_op_requires_resource(kind: DisplayOpKind) i32 { + return (kind == DisplayOpKind.draw_path || kind == DisplayOpKind.draw_image || + kind == DisplayOpKind.draw_glyph_run || kind == DisplayOpKind.apply_effect) as i32; +} + +fn display_list_push(list: *DisplayList, op: DisplayOp) DisplayError { + if (list.*.sealed != 0) { return display_fail(list, DisplayError.sealed); } + if (display_fixed_value_valid(op.bounds.x) == 0 || + display_fixed_value_valid(op.bounds.y) == 0 || + display_fixed_value_valid(op.bounds.width) == 0 || + display_fixed_value_valid(op.bounds.height) == 0) { + return display_fail(list, DisplayError.invalid_geometry); + } + if (display_op_requires_bounds(op.kind) != 0 && + (op.bounds.width <= 0 || op.bounds.height <= 0)) { + return display_fail(list, DisplayError.invalid_geometry); + } + if (display_op_requires_resource(op.kind) != 0 && op.resource_id <= 0) { + return display_fail(list, DisplayError.invalid_resource); + } + if (display_paint_valid(op.paint) == 0 || display_transform_valid(op.transform) == 0 || + op.parameter < 0 || display_fixed_value_valid(op.parameter) == 0) { + return display_fail(list, DisplayError.invalid_parameter); + } + if (op.kind == DisplayOpKind.stroke_rect && op.paint.stroke_width <= 0) { + return display_fail(list, DisplayError.invalid_parameter); + } + if (op.kind == DisplayOpKind.draw_image && + (op.paint.red != 0 || op.paint.green != 0 || op.paint.blue != 0 || + op.paint.stroke_width != unit_scale())) { + return display_fail(list, DisplayError.invalid_parameter); + } + if (op.kind == DisplayOpKind.begin_layer && (op.parameter < 0 || op.parameter > 65535)) { + return display_fail(list, DisplayError.invalid_parameter); + } + if (op.kind == DisplayOpKind.restore && list.*.stack_depth <= 0) { + return display_fail(list, DisplayError.stack_underflow); + } + if (op.kind == DisplayOpKind.end_layer && list.*.layer_depth <= 0) { + return display_fail(list, DisplayError.layer_underflow); + } + + push[DisplayOp](&list.*.ops, op); + if (op.kind == DisplayOpKind.save) { list.*.stack_depth = list.*.stack_depth + 1; } + if (op.kind == DisplayOpKind.restore) { list.*.stack_depth = list.*.stack_depth - 1; } + if (op.kind == DisplayOpKind.begin_layer) { list.*.layer_depth = list.*.layer_depth + 1; } + if (op.kind == DisplayOpKind.end_layer) { list.*.layer_depth = list.*.layer_depth - 1; } + list.*.revision = list.*.ops.len as i64 + list.*.resources.revision; + list.*.content_hash = display_hash_op(list.*.content_hash, op); + list.*.last_error = DisplayError.none; + list.*.error_index = 0 - 1; + return DisplayError.none; +} + +fn display_list_seal(list: *DisplayList) DisplayError { + if (list.*.sealed != 0) { return DisplayError.none; } + if (list.*.stack_depth != 0) { return display_fail(list, DisplayError.unbalanced_stack); } + if (list.*.layer_depth != 0) { return display_fail(list, DisplayError.unbalanced_layers); } + let resource_index: i32 = 0; + while (resource_index < list.*.resources.resources.len) { + if (display_resource_payload_valid( + list.*.resources.resources.data[resource_index]) == 0) { + return display_fail_at(list, DisplayError.invalid_resource, 0 - 1); + } + resource_index = resource_index + 1; + } + let i: i32 = 0; + while (i < list.*.ops.len) { + if (display_resource_reference_valid(list.*, list.*.ops.data[i]) == 0) { + return display_fail_at(list, DisplayError.invalid_resource, i); + } + i = i + 1; + } + if (render_resource_store_seal(&list.*.resources) != RenderResourceError.none) { + return display_fail(list, DisplayError.invalid_resource); + } + list.*.content_hash = display_list_calculate_hash(list.*); + list.*.revision = list.*.ops.len as i64 + list.*.resources.revision; + list.*.sealed = 1; + list.*.last_error = DisplayError.none; + list.*.error_index = 0 - 1; + return DisplayError.none; +} + +fn display_list_verify(list: DisplayList) DisplayError { + if (list.sealed == 0) { return DisplayError.not_sealed; } + if (render_resource_store_verify(list.resources) != RenderResourceError.none) { + return DisplayError.content_mutated; + } + let resource_index: i32 = 0; + while (resource_index < list.resources.resources.len) { + if (display_resource_payload_valid( + list.resources.resources.data[resource_index]) == 0) { + return DisplayError.content_mutated; + } + resource_index = resource_index + 1; + } + let hash: i64 = display_mix(17, list.resources.content_hash); + let stack_depth: i32 = 0; + let layer_depth: i32 = 0; + let i: i32 = 0; + while (i < list.ops.len) { + let op: DisplayOp = list.ops.data[i]; + if (display_resource_reference_valid(list, op) == 0) { + return DisplayError.content_mutated; + } + hash = display_hash_op(hash, op); + if (op.kind == DisplayOpKind.save) { stack_depth = stack_depth + 1; } + if (op.kind == DisplayOpKind.restore) { + if (stack_depth <= 0) { return DisplayError.content_mutated; } + stack_depth = stack_depth - 1; + } + if (op.kind == DisplayOpKind.begin_layer) { layer_depth = layer_depth + 1; } + if (op.kind == DisplayOpKind.end_layer) { + if (layer_depth <= 0) { return DisplayError.content_mutated; } + layer_depth = layer_depth - 1; + } + i = i + 1; + } + if (stack_depth != 0 || layer_depth != 0 || hash != list.content_hash || + list.revision != list.ops.len as i64 + list.resources.revision) { + return DisplayError.content_mutated; + } + return DisplayError.none; +} diff --git a/src/render/display_list_codec.zag b/src/render/display_list_codec.zag new file mode 100644 index 0000000..a69c907 --- /dev/null +++ b/src/render/display_list_codec.zag @@ -0,0 +1,306 @@ +@import("std:list") +@import("display_list.zag") + +enum DisplayCodecError { + none, + not_sealed, + content_mutated, + bad_magic, + unsupported_version, + invalid_count, + truncated, + trailing_data, + invalid_kind, + invalid_operation, + invalid_resource, + invalid_payload, + invalid_configuration, + hash_mismatch, +} + +struct DisplayEncodeResult { + bytes: ArrayList[u8], + error: DisplayCodecError, +} + +struct DisplayDecodeResult { + list: DisplayList, + error: DisplayCodecError, + error_offset: i64, +} + +fn display_codec_version() i64 { return 2; } +fn display_codec_header_size() i64 { return 76; } +fn display_codec_resource_header_size() i64 { return 64; } +fn display_codec_op_size() i64 { return 160; } + +fn display_write_i64(bytes: *ArrayList[u8], value: i64) void { + let i: i32 = 0; + while (i < 8) { + push[u8](bytes, ((value >> (i * 8)) & 255) as u8); + i = i + 1; + } +} + +fn display_read_i64(bytes: ArrayList[u8], offset: i64) i64 { + let value: i64 = 0; + let i: i32 = 0; + while (i < 8) { + value = value | ((bytes.data[(offset as i32) + i] as i64) << (i * 8)); + i = i + 1; + } + return value; +} + +fn display_encode_fail(error: DisplayCodecError) DisplayEncodeResult { + return DisplayEncodeResult{ .bytes = make[u8](1), .error = error }; +} + +fn display_list_encode(list: DisplayList) DisplayEncodeResult { + if (list.sealed == 0) { return display_encode_fail(DisplayCodecError.not_sealed); } + if (display_list_verify(list) != DisplayError.none) { + return display_encode_fail(DisplayCodecError.content_mutated); + } + if (list.ops.len < 0 || list.ops.len > 1000000 || + list.resources.resources.len < 0 || + list.resources.resources.len > render_resource_hard_count_limit()) { + return display_encode_fail(DisplayCodecError.invalid_count); + } + + let resource_bytes: i64 = 0; + let resource_index: i32 = 0; + while (resource_index < list.resources.resources.len) { + let resource: RenderResource = list.resources.resources.data[resource_index]; + if (resource.payload.len <= 0 || + resource.payload.len as i64 > list.resources.max_payload_bytes) { + return display_encode_fail(DisplayCodecError.invalid_payload); + } + resource_bytes = resource_bytes + display_codec_resource_header_size() + + resource.payload.len as i64; + resource_index = resource_index + 1; + } + let encoded_size: i64 = display_codec_header_size() + resource_bytes + + (list.ops.len as i64) * display_codec_op_size(); + if (encoded_size <= 0 || encoded_size > 1073741824) { + return display_encode_fail(DisplayCodecError.invalid_count); + } + let bytes: ArrayList[u8] = make[u8](encoded_size as i32); + push[u8](&bytes, 90); push[u8](&bytes, 75); push[u8](&bytes, 68); push[u8](&bytes, 76); + display_write_i64(&bytes, display_codec_version()); + display_write_i64(&bytes, list.resources.resources.len as i64); + display_write_i64(&bytes, list.ops.len as i64); + display_write_i64(&bytes, resource_bytes); + display_write_i64(&bytes, list.content_hash); + display_write_i64(&bytes, list.revision); + display_write_i64(&bytes, list.resources.max_resources as i64); + display_write_i64(&bytes, list.resources.max_total_bytes); + display_write_i64(&bytes, list.resources.max_payload_bytes); + + let r: i32 = 0; + while (r < list.resources.resources.len) { + let resource: RenderResource = list.resources.resources.data[r]; + display_write_i64(&bytes, resource.id); + display_write_i64(&bytes, resource.kind as i64); + display_write_i64(&bytes, resource.revision); + display_write_i64(&bytes, resource.format_tag); + display_write_i64(&bytes, resource.width); + display_write_i64(&bytes, resource.height); + display_write_i64(&bytes, resource.color_space as i64); + display_write_i64(&bytes, resource.payload.len as i64); + let p: i32 = 0; + while (p < resource.payload.len) { + push[u8](&bytes, resource.payload.data[p]); + p = p + 1; + } + r = r + 1; + } + + let i: i32 = 0; + while (i < list.ops.len) { + let op: DisplayOp = list.ops.data[i]; + display_write_i64(&bytes, op.kind as i64); + display_write_i64(&bytes, op.owner.value); + display_write_i64(&bytes, op.owner.generation); + display_write_i64(&bytes, op.bounds.x); + display_write_i64(&bytes, op.bounds.y); + display_write_i64(&bytes, op.bounds.width); + display_write_i64(&bytes, op.bounds.height); + display_write_i64(&bytes, op.paint.red); + display_write_i64(&bytes, op.paint.green); + display_write_i64(&bytes, op.paint.blue); + display_write_i64(&bytes, op.paint.alpha); + display_write_i64(&bytes, op.paint.stroke_width); + display_write_i64(&bytes, op.transform.scale_x); + display_write_i64(&bytes, op.transform.skew_x); + display_write_i64(&bytes, op.transform.skew_y); + display_write_i64(&bytes, op.transform.scale_y); + display_write_i64(&bytes, op.transform.translate_x); + display_write_i64(&bytes, op.transform.translate_y); + display_write_i64(&bytes, op.resource_id); + display_write_i64(&bytes, op.parameter); + i = i + 1; + } + return DisplayEncodeResult{ .bytes = bytes, .error = DisplayCodecError.none }; +} + +fn display_encode_free(result: *DisplayEncodeResult) void { + free[u8](&result.*.bytes); +} + +fn display_decode_fail(list: DisplayList, error: DisplayCodecError, offset: i64) DisplayDecodeResult { + return DisplayDecodeResult{ .list = list, .error = error, .error_offset = offset }; +} + +fn display_list_decode(bytes: ArrayList[u8]) DisplayDecodeResult { + let list: DisplayList = display_list_make(); + if ((bytes.len as i64) < display_codec_header_size()) { + return display_decode_fail(list, DisplayCodecError.truncated, bytes.len as i64); + } + if (bytes.data[0] != 90 || bytes.data[1] != 75 || bytes.data[2] != 68 || bytes.data[3] != 76) { + return display_decode_fail(list, DisplayCodecError.bad_magic, 0); + } + let version: i64 = display_read_i64(bytes, 4); + if (version != display_codec_version()) { + return display_decode_fail(list, DisplayCodecError.unsupported_version, 4); + } + let resource_count: i64 = display_read_i64(bytes, 12); + let count: i64 = display_read_i64(bytes, 20); + let resource_bytes: i64 = display_read_i64(bytes, 28); + if (resource_count < 0 || resource_count > render_resource_hard_count_limit() as i64) { + return display_decode_fail(list, DisplayCodecError.invalid_count, 12); + } + if (count < 0 || count > 1000000 || resource_bytes < 0 || + resource_bytes > render_resource_hard_total_limit() + + resource_count * display_codec_resource_header_size()) { + return display_decode_fail(list, DisplayCodecError.invalid_count, 20); + } + let expected_size: i64 = display_codec_header_size() + resource_bytes + + count * display_codec_op_size(); + if ((bytes.len as i64) < expected_size) { + return display_decode_fail(list, DisplayCodecError.truncated, bytes.len as i64); + } + if ((bytes.len as i64) > expected_size) { + return display_decode_fail(list, DisplayCodecError.trailing_data, expected_size); + } + let stored_hash: i64 = display_read_i64(bytes, 36); + let stored_revision: i64 = display_read_i64(bytes, 44); + let max_resources: i64 = display_read_i64(bytes, 52); + let max_total_bytes: i64 = display_read_i64(bytes, 60); + let max_payload_bytes: i64 = display_read_i64(bytes, 68); + if (max_resources <= 0 || max_resources > render_resource_hard_count_limit() as i64 || + max_total_bytes <= 0 || max_total_bytes > render_resource_hard_total_limit() || + max_payload_bytes <= 0 || max_payload_bytes > render_resource_hard_payload_limit() || + max_payload_bytes > max_total_bytes || resource_count > max_resources) { + return display_decode_fail(list, DisplayCodecError.invalid_configuration, 52); + } + display_list_free(&list); + list = display_list_make_with_resource_limits( + max_resources as i32, max_total_bytes, max_payload_bytes + ); + + let offset: i64 = display_codec_header_size(); + let resources_end: i64 = display_codec_header_size() + resource_bytes; + let r: i64 = 0; + while (r < resource_count) { + if (offset + display_codec_resource_header_size() > resources_end) { + return display_decode_fail(list, DisplayCodecError.truncated, offset); + } + let kind_code: i64 = display_read_i64(bytes, offset + 8); + let color_code: i64 = display_read_i64(bytes, offset + 48); + let payload_length: i64 = display_read_i64(bytes, offset + 56); + if (kind_code < 0 || kind_code > 6 || color_code < 0 || color_code > 3) { + return display_decode_fail(list, DisplayCodecError.invalid_resource, offset); + } + if (payload_length <= 0 || payload_length > max_payload_bytes || + offset + display_codec_resource_header_size() + payload_length > resources_end) { + return display_decode_fail(list, DisplayCodecError.invalid_payload, offset + 56); + } + let payload_start: i32 = (offset + display_codec_resource_header_size()) as i32; + let payload_end: i32 = (offset + display_codec_resource_header_size() + payload_length) as i32; + let spec: RenderResourceSpec = render_resource_spec( + display_read_i64(bytes, offset), kind_code as RenderResourceKind, + bytes.data[payload_start..payload_end], display_read_i64(bytes, offset + 24) + ); + let stored_resource_revision: i64 = display_read_i64(bytes, offset + 16); + spec.width = display_read_i64(bytes, offset + 32); + spec.height = display_read_i64(bytes, offset + 40); + spec.color_space = color_code as RenderColorSpace; + if (stored_resource_revision < 0 || stored_resource_revision > 2147483647 || + render_resource_add(&list.resources, spec) != RenderResourceError.none) { + return display_decode_fail(list, DisplayCodecError.invalid_resource, offset); + } + let added_index: i32 = render_resource_find_index(list.resources, spec.id); + if (display_resource_payload_valid( + list.resources.resources.data[added_index]) == 0) { + return display_decode_fail(list, DisplayCodecError.invalid_payload, + payload_start as i64); + } + list.resources.resources.data[added_index].revision = stored_resource_revision; + list.resources.resources.data[added_index].content_hash = + render_resource_hash(list.resources.resources.data[added_index]); + offset = offset + display_codec_resource_header_size() + payload_length; + r = r + 1; + } + if (offset != resources_end) { + return display_decode_fail(list, DisplayCodecError.invalid_payload, offset); + } + if (stored_revision < count) { + return display_decode_fail(list, DisplayCodecError.hash_mismatch, 44); + } + let resource_revision: i64 = stored_revision - count; + list.resources.revision = resource_revision; + render_resource_store_rehash(&list.resources); + list.revision = resource_revision; + list.content_hash = display_list_calculate_hash(list); + + let i: i64 = 0; + while (i < count) { + let kind_code: i64 = display_read_i64(bytes, offset); + if (kind_code < 0 || kind_code > 11) { + return display_decode_fail(list, DisplayCodecError.invalid_kind, offset); + } + let op: DisplayOp = display_op(kind_code as DisplayOpKind, node_key_scoped( + display_read_i64(bytes, offset + 8), + display_read_i64(bytes, offset + 16) + )); + op.bounds = rect( + display_read_i64(bytes, offset + 24), + display_read_i64(bytes, offset + 32), + display_read_i64(bytes, offset + 40), + display_read_i64(bytes, offset + 48) + ); + op.paint = Paint{ + .red = display_read_i64(bytes, offset + 56), + .green = display_read_i64(bytes, offset + 64), + .blue = display_read_i64(bytes, offset + 72), + .alpha = display_read_i64(bytes, offset + 80), + .stroke_width = display_read_i64(bytes, offset + 88), + }; + op.transform = Transform{ + .scale_x = display_read_i64(bytes, offset + 96), + .skew_x = display_read_i64(bytes, offset + 104), + .skew_y = display_read_i64(bytes, offset + 112), + .scale_y = display_read_i64(bytes, offset + 120), + .translate_x = display_read_i64(bytes, offset + 128), + .translate_y = display_read_i64(bytes, offset + 136), + }; + op.resource_id = display_read_i64(bytes, offset + 144); + op.parameter = display_read_i64(bytes, offset + 152); + if (display_list_push(&list, op) != DisplayError.none) { + return display_decode_fail(list, DisplayCodecError.invalid_operation, offset); + } + offset = offset + display_codec_op_size(); + i = i + 1; + } + if (display_list_seal(&list) != DisplayError.none) { + return display_decode_fail(list, DisplayCodecError.invalid_operation, offset); + } + if (list.content_hash != stored_hash || list.revision != stored_revision) { + return display_decode_fail(list, DisplayCodecError.hash_mismatch, 36); + } + return DisplayDecodeResult{ .list = list, .error = DisplayCodecError.none, .error_offset = 0 - 1 }; +} + +fn display_decode_free(result: *DisplayDecodeResult) void { + display_list_free(&result.*.list); +} diff --git a/src/render/image.zag b/src/render/image.zag new file mode 100644 index 0000000..e81d192 --- /dev/null +++ b/src/render/image.zag @@ -0,0 +1,40 @@ +@import("resources.zag") + +enum ImagePayloadError { + none, + wrong_kind, + unsupported_format, + invalid_dimensions, + invalid_color_space, + size_mismatch, +} + +fn image_resource_format_tag() i64 { return 1; } + +fn image_payload_expected_bytes(width: i64, height: i64) i64 { + if (width <= 0 || height <= 0 || width > 1048576 || height > 1048576) { + return 0; + } + if (width > 536870912 / height) { return 0; } + let pixels: i64 = width * height; + if (pixels > 536870912 / 4) { return 0; } + return pixels * 4; +} + +fn image_resource_validate(resource: RenderResource) ImagePayloadError { + if (resource.kind != RenderResourceKind.image) { + return ImagePayloadError.wrong_kind; + } + if (resource.format_tag != image_resource_format_tag()) { + return ImagePayloadError.unsupported_format; + } + let expected: i64 = image_payload_expected_bytes(resource.width, resource.height); + if (expected <= 0) { return ImagePayloadError.invalid_dimensions; } + if (resource.color_space == RenderColorSpace.none) { + return ImagePayloadError.invalid_color_space; + } + if (resource.payload.len as i64 != expected) { + return ImagePayloadError.size_mismatch; + } + return ImagePayloadError.none; +} diff --git a/src/render/path.zag b/src/render/path.zag new file mode 100644 index 0000000..3281719 --- /dev/null +++ b/src/render/path.zag @@ -0,0 +1,396 @@ +@import("std:list") +@import("../core/geometry.zag") + +enum PathFillRule { + non_zero, + even_odd, +} + +enum PathVerb { + move_to, + line_to, + quad_to, + cubic_to, + close, +} + +enum PathError { + none, + sealed, + not_sealed, + empty, + invalid_fill_rule, + invalid_verb, + invalid_geometry, + invalid_sequence, + too_many_commands, + content_mutated, + bad_magic, + unsupported_version, + truncated, + trailing_data, + hash_mismatch, +} + +struct PathPoint { + x: i64, + y: i64, +} + +struct PathCommand { + verb: PathVerb, + point_1: PathPoint, + point_2: PathPoint, + point_3: PathPoint, +} + +struct PathData { + commands: ArrayList[PathCommand], + fill_rule: PathFillRule, + sealed: i32, + content_hash: i64, + last_error: PathError, + error_index: i32, +} + +struct PathEncodeResult { + bytes: ArrayList[u8], + error: PathError, +} + +struct PathDecodeResult { + path: PathData, + error: PathError, + error_offset: i64, +} + +fn path_resource_format_tag() i64 { return 1; } +fn path_codec_version() i64 { return 1; } +fn path_codec_header_size() i64 { return 40; } +fn path_codec_command_size() i64 { return 56; } +fn path_command_limit() i32 { return 65536; } + +fn path_point(x: i64, y: i64) PathPoint { + return PathPoint{ .x = x, .y = y }; +} + +fn path_zero_point() PathPoint { return path_point(0, 0); } + +fn path_command(verb: PathVerb) PathCommand { + return PathCommand{ + .verb = verb, + .point_1 = path_zero_point(), + .point_2 = path_zero_point(), + .point_3 = path_zero_point(), + }; +} + +fn path_mix(hash: i64, value: i64) i64 { + let modulus: i64 = 2147483647; + let component: i64 = value % modulus; + if (component < 0) { component = component + modulus; } + return (hash * 131 + component) % modulus; +} + +fn path_hash_command(hash: i64, command: PathCommand) i64 { + let result: i64 = path_mix(hash, command.verb as i64); + result = path_mix(result, command.point_1.x); + result = path_mix(result, command.point_1.y); + result = path_mix(result, command.point_2.x); + result = path_mix(result, command.point_2.y); + result = path_mix(result, command.point_3.x); + return path_mix(result, command.point_3.y); +} + +fn path_calculate_hash(path: PathData) i64 { + let hash: i64 = path_mix(17, path.fill_rule as i64); + hash = path_mix(hash, path.commands.len as i64); + let i: i32 = 0; + while (i < path.commands.len) { + hash = path_hash_command(hash, path.commands.data[i]); + i = i + 1; + } + return hash; +} + +fn path_make(fill_rule: PathFillRule) PathData { + return PathData{ + .commands = make[PathCommand](16), + .fill_rule = fill_rule, + .sealed = 0, + .content_hash = 0, + .last_error = PathError.none, + .error_index = 0 - 1, + }; +} + +fn path_free(path: *PathData) void { + free[PathCommand](&path.*.commands); +} + +fn path_fail(path: *PathData, error: PathError) PathError { + path.*.last_error = error; + path.*.error_index = path.*.commands.len; + return error; +} + +fn path_fixed_valid(value: i64) i32 { + return (value >= 0 - unbounded_extent() && value <= unbounded_extent()) as i32; +} + +fn path_point_valid(point: PathPoint) i32 { + return (path_fixed_valid(point.x) != 0 && path_fixed_valid(point.y) != 0) as i32; +} + +fn path_point_zero(point: PathPoint) i32 { + return (point.x == 0 && point.y == 0) as i32; +} + +fn path_command_canonical(command: PathCommand) i32 { + if (path_point_valid(command.point_1) == 0 || + path_point_valid(command.point_2) == 0 || + path_point_valid(command.point_3) == 0) { return 0; } + if (command.verb == PathVerb.move_to || command.verb == PathVerb.line_to) { + return (path_point_zero(command.point_2) != 0 && + path_point_zero(command.point_3) != 0) as i32; + } + if (command.verb == PathVerb.quad_to) { + return path_point_zero(command.point_3); + } + if (command.verb == PathVerb.cubic_to) { return 1; } + if (command.verb == PathVerb.close) { + return (path_point_zero(command.point_1) != 0 && + path_point_zero(command.point_2) != 0 && + path_point_zero(command.point_3) != 0) as i32; + } + return 0; +} + +fn path_sequence_valid(path: PathData) i32 { + if ((path.fill_rule as i64) < 0 || (path.fill_rule as i64) > 1 || + path.commands.len <= 0 || path.commands.len > path_command_limit()) { return 0; } + let contour_open: i32 = 0; + let i: i32 = 0; + while (i < path.commands.len) { + let command: PathCommand = path.commands.data[i]; + if (path_command_canonical(command) == 0) { return 0; } + if (command.verb == PathVerb.move_to) { + contour_open = 1; + } else if (command.verb == PathVerb.close) { + if (contour_open == 0) { return 0; } + contour_open = 0; + } else if (contour_open == 0) { + return 0; + } + i = i + 1; + } + return 1; +} + +fn path_push(path: *PathData, command: PathCommand) PathError { + if (path.*.sealed != 0) { return path_fail(path, PathError.sealed); } + if ((path.*.fill_rule as i64) < 0 || (path.*.fill_rule as i64) > 1) { + return path_fail(path, PathError.invalid_fill_rule); + } + if (path.*.commands.len >= path_command_limit()) { + return path_fail(path, PathError.too_many_commands); + } + if (path_command_canonical(command) == 0) { + return path_fail(path, PathError.invalid_geometry); + } + let contour_open: i32 = 0; + if (path.*.commands.len > 0) { + let i: i32 = path.*.commands.len - 1; + while (i >= 0) { + let previous: PathVerb = path.*.commands.data[i].verb; + if (previous == PathVerb.close) { break; } + if (previous == PathVerb.move_to) { contour_open = 1; break; } + i = i - 1; + } + } + if (command.verb == PathVerb.close) { + if (contour_open == 0) { return path_fail(path, PathError.invalid_sequence); } + } else if (command.verb != PathVerb.move_to && contour_open == 0) { + return path_fail(path, PathError.invalid_sequence); + } + push[PathCommand](&path.*.commands, command); + path.*.last_error = PathError.none; + path.*.error_index = 0 - 1; + return PathError.none; +} + +fn path_move_to(path: *PathData, point: PathPoint) PathError { + let command: PathCommand = path_command(PathVerb.move_to); + command.point_1 = point; + return path_push(path, command); +} + +fn path_line_to(path: *PathData, point: PathPoint) PathError { + let command: PathCommand = path_command(PathVerb.line_to); + command.point_1 = point; + return path_push(path, command); +} + +fn path_quad_to(path: *PathData, control: PathPoint, end: PathPoint) PathError { + let command: PathCommand = path_command(PathVerb.quad_to); + command.point_1 = control; + command.point_2 = end; + return path_push(path, command); +} + +fn path_cubic_to(path: *PathData, control_1: PathPoint, + control_2: PathPoint, end: PathPoint) PathError { + let command: PathCommand = path_command(PathVerb.cubic_to); + command.point_1 = control_1; + command.point_2 = control_2; + command.point_3 = end; + return path_push(path, command); +} + +fn path_close(path: *PathData) PathError { + return path_push(path, path_command(PathVerb.close)); +} + +fn path_seal(path: *PathData) PathError { + if (path.*.sealed != 0) { return PathError.none; } + if (path.*.commands.len == 0) { return path_fail(path, PathError.empty); } + if (path_sequence_valid(path.*) == 0) { + return path_fail(path, PathError.invalid_sequence); + } + path.*.content_hash = path_calculate_hash(path.*); + path.*.sealed = 1; + path.*.last_error = PathError.none; + path.*.error_index = 0 - 1; + return PathError.none; +} + +fn path_verify(path: PathData) PathError { + if (path.sealed == 0) { return PathError.not_sealed; } + if (path_sequence_valid(path) == 0 || + path_calculate_hash(path) != path.content_hash) { + return PathError.content_mutated; + } + return PathError.none; +} + +fn path_write_i64(bytes: *ArrayList[u8], value: i64) void { + let i: i32 = 0; + while (i < 8) { + push[u8](bytes, ((value >> (i * 8)) & 255) as u8); + i = i + 1; + } +} + +fn path_read_i64_bytes(bytes: []u8, offset: i64) i64 { + let value: i64 = 0; + let i: i32 = 0; + while (i < 8) { + value = value | ((bytes[(offset as i32) + i] as i64) << (i * 8)); + i = i + 1; + } + return value; +} + +fn path_encode(path: PathData) PathEncodeResult { + if (path.sealed == 0) { + return PathEncodeResult{ .bytes = make[u8](1), .error = PathError.not_sealed }; + } + if (path_verify(path) != PathError.none) { + return PathEncodeResult{ .bytes = make[u8](1), .error = PathError.content_mutated }; + } + let size: i64 = path_codec_header_size() + + (path.commands.len as i64) * path_codec_command_size(); + let bytes: ArrayList[u8] = make[u8](size as i32); + push[u8](&bytes, 90); push[u8](&bytes, 75); push[u8](&bytes, 80); push[u8](&bytes, 65); + push[u8](&bytes, 84); push[u8](&bytes, 72); push[u8](&bytes, 48); push[u8](&bytes, 49); + path_write_i64(&bytes, path_codec_version()); + path_write_i64(&bytes, path.fill_rule as i64); + path_write_i64(&bytes, path.commands.len as i64); + path_write_i64(&bytes, path.content_hash); + let i: i32 = 0; + while (i < path.commands.len) { + let command: PathCommand = path.commands.data[i]; + path_write_i64(&bytes, command.verb as i64); + path_write_i64(&bytes, command.point_1.x); + path_write_i64(&bytes, command.point_1.y); + path_write_i64(&bytes, command.point_2.x); + path_write_i64(&bytes, command.point_2.y); + path_write_i64(&bytes, command.point_3.x); + path_write_i64(&bytes, command.point_3.y); + i = i + 1; + } + return PathEncodeResult{ .bytes = bytes, .error = PathError.none }; +} + +fn path_encode_free(result: *PathEncodeResult) void { + free[u8](&result.*.bytes); +} + +fn path_decode_fail(path: PathData, error: PathError, offset: i64) PathDecodeResult { + return PathDecodeResult{ .path = path, .error = error, .error_offset = offset }; +} + +fn path_decode(payload: []u8) PathDecodeResult { + let path: PathData = path_make(PathFillRule.non_zero); + if ((payload.len as i64) < path_codec_header_size()) { + return path_decode_fail(path, PathError.truncated, payload.len as i64); + } + if (payload[0] != 90 || payload[1] != 75 || payload[2] != 80 || payload[3] != 65 || + payload[4] != 84 || payload[5] != 72 || payload[6] != 48 || payload[7] != 49) { + return path_decode_fail(path, PathError.bad_magic, 0); + } + if (path_read_i64_bytes(payload, 8) != path_codec_version()) { + return path_decode_fail(path, PathError.unsupported_version, 8); + } + let fill_code: i64 = path_read_i64_bytes(payload, 16); + if (fill_code < 0 || fill_code > 1) { + return path_decode_fail(path, PathError.invalid_fill_rule, 16); + } + let count: i64 = path_read_i64_bytes(payload, 24); + if (count <= 0 || count > path_command_limit() as i64) { + return path_decode_fail(path, PathError.too_many_commands, 24); + } + let expected: i64 = path_codec_header_size() + count * path_codec_command_size(); + if ((payload.len as i64) < expected) { + return path_decode_fail(path, PathError.truncated, payload.len as i64); + } + if ((payload.len as i64) > expected) { + return path_decode_fail(path, PathError.trailing_data, expected); + } + path.fill_rule = fill_code as PathFillRule; + let offset: i64 = path_codec_header_size(); + let i: i64 = 0; + while (i < count) { + let verb_code: i64 = path_read_i64_bytes(payload, offset); + if (verb_code < 0 || verb_code > 4) { + return path_decode_fail(path, PathError.invalid_verb, offset); + } + let command: PathCommand = PathCommand{ + .verb = verb_code as PathVerb, + .point_1 = path_point(path_read_i64_bytes(payload, offset + 8), path_read_i64_bytes(payload, offset + 16)), + .point_2 = path_point(path_read_i64_bytes(payload, offset + 24), path_read_i64_bytes(payload, offset + 32)), + .point_3 = path_point(path_read_i64_bytes(payload, offset + 40), path_read_i64_bytes(payload, offset + 48)), + }; + let error: PathError = path_push(&path, command); + if (error != PathError.none) { + return path_decode_fail(path, error, offset); + } + offset = offset + path_codec_command_size(); + i = i + 1; + } + let stored_hash: i64 = path_read_i64_bytes(payload, 32); + if (path_seal(&path) != PathError.none) { + return path_decode_fail(path, PathError.invalid_sequence, offset); + } + if (path.content_hash != stored_hash) { + return path_decode_fail(path, PathError.hash_mismatch, 32); + } + return PathDecodeResult{ .path = path, .error = PathError.none, .error_offset = 0 - 1 }; +} + +fn path_payload_valid(payload: []u8) i32 { + let decoded: PathDecodeResult = path_decode(payload); + let ok: i32 = (decoded.error == PathError.none) as i32; + path_free(&decoded.path); + return ok; +} diff --git a/src/render/png_decode.zag b/src/render/png_decode.zag new file mode 100644 index 0000000..03474b1 --- /dev/null +++ b/src/render/png_decode.zag @@ -0,0 +1,2025 @@ +@import("std:list") +@import("std:checksum") +@import("std:zlib_inflate") +@import("image.zag") + +enum PngDecodeError { + none, + input_limit, + invalid_signature, + truncated, + invalid_chunk_length, + invalid_chunk_type, + invalid_chunk_crc, + invalid_chunk_order, + duplicate_chunk, + unknown_critical_chunk, + invalid_header, + unsupported_color_type, + unsupported_bit_depth, + unsupported_interlace, + unsupported_color_profile, + missing_palette, + invalid_palette, + invalid_transparency, + missing_image_data, + compression_error, + decompressed_size, + invalid_filter, + palette_index, + pixel_limit, + trailing_data, +} + +struct PngDecodeResult { + pixels: ArrayList[u8], + width: i32, + height: i32, + color_space: RenderColorSpace, + profile_assumed: i32, + error: PngDecodeError, + error_offset: i64, +} + +struct PngScanlineResult { + bytes: ArrayList[u8], + error: PngDecodeError, + error_offset: i64, +} + +struct PngMatrixF64 { + m00: f64, m01: f64, m02: f64, + m10: f64, m11: f64, m12: f64, + m20: f64, m21: f64, m22: f64, +} + +struct PngMatrixF64Result { matrix: PngMatrixF64, valid: i32 } + +struct PngVectorF64 { x: f64, y: f64, z: f64 } + +struct PngColorTransform { + m00: i64, m01: i64, m02: i64, + m10: i64, m11: i64, m12: i64, + m20: i64, m21: i64, m22: i64, + active: i32, + valid: i32, +} + +struct PngIccTag { offset: i32, size: i32, found: i32, valid: i32 } + +struct PngIccVectorResult { value: PngVectorF64, valid: i32 } + +struct PngTransferCurve { + kind: i32, + g: i64, + a: i64, b: i64, c: i64, d: i64, e: i64, f: i64, + samples: ArrayList[i64], + valid: i32, +} + +struct PngIccLut { + red_grid_points: i32, + green_grid_points: i32, + blue_grid_points: i32, + input_entries: i32, + output_entries: i32, + input_tables: ArrayList[i32], + clut: ArrayList[i32], + output_tables: ArrayList[i32], + valid: i32, +} + +struct PngIccLutSample { x: i64, y: i64, z: i64 } + +struct PngIccCurveTableResult { table: ArrayList[i32], valid: i32 } + +struct PngIccProfileResult { + transform: PngColorTransform, + red_curve: PngTransferCurve, + green_curve: PngTransferCurve, + blue_curve: PngTransferCurve, + lut: PngIccLut, + valid: i32, +} + +fn png_decode_input_limit() i64 { return 134217728; } +fn png_decode_pixel_byte_limit() i64 { return 67108864; } +fn png_decode_dimension_limit() i32 { return 8192; } + +fn png_decode_fail(error: PngDecodeError, offset: i64) PngDecodeResult { + return PngDecodeResult{ + .pixels = make[u8](1), .width = 0, .height = 0, + .color_space = RenderColorSpace.none, + .profile_assumed = 0, + .error = error, .error_offset = offset, + }; +} + +fn png_decode_free(result: *PngDecodeResult) void { + free[u8](&result.*.pixels); +} + +fn png_read_u16_be(bytes: []u8, offset: i32) i32 { + return ((bytes[offset] as i32) << 8) | (bytes[offset + 1] as i32); +} + +fn png_read_u32_be(bytes: []u8, offset: i32) i64 { + return ((bytes[offset] as i64) << 24) | + ((bytes[offset + 1] as i64) << 16) | + ((bytes[offset + 2] as i64) << 8) | + (bytes[offset + 3] as i64); +} + +fn png_chunk_is(bytes: []u8, offset: i32, name: []u8) i32 { + if (name.len != 4) { return 0; } + let i: i32 = 0; + while (i < 4) { + if (bytes[offset + 4 + i] != name[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn png_chunk_type_valid(bytes: []u8, offset: i32) i32 { + let i: i32 = 0; + while (i < 4) { + let c: u8 = bytes[offset + 4 + i]; + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { return 0; } + i = i + 1; + } + return ((bytes[offset + 6] & 32) == 0) as i32; +} + +fn png_signature_valid(bytes: []u8) i32 { + if (bytes.len < 8) { return 0; } + return (bytes[0] == 137 && bytes[1] == 80 && bytes[2] == 78 && + bytes[3] == 71 && bytes[4] == 13 && bytes[5] == 10 && + bytes[6] == 26 && bytes[7] == 10) as i32; +} + +fn png_chrm_is_srgb(bytes: []u8, offset: i32, length: i32) i32 { + if (length != 32) { return 0; } + return (png_read_u32_be(bytes, offset) == 31270 && + png_read_u32_be(bytes, offset + 4) == 32900 && + png_read_u32_be(bytes, offset + 8) == 64000 && + png_read_u32_be(bytes, offset + 12) == 33000 && + png_read_u32_be(bytes, offset + 16) == 30000 && + png_read_u32_be(bytes, offset + 20) == 60000 && + png_read_u32_be(bytes, offset + 24) == 15000 && + png_read_u32_be(bytes, offset + 28) == 6000) as i32; +} + +fn png_matrix_zero() PngMatrixF64 { + return PngMatrixF64{ + .m00 = 0.0, .m01 = 0.0, .m02 = 0.0, + .m10 = 0.0, .m11 = 0.0, .m12 = 0.0, + .m20 = 0.0, .m21 = 0.0, .m22 = 0.0, + }; +} + +fn png_matrix_identity() PngMatrixF64 { + return PngMatrixF64{ + .m00 = 1.0, .m01 = 0.0, .m02 = 0.0, + .m10 = 0.0, .m11 = 1.0, .m12 = 0.0, + .m20 = 0.0, .m21 = 0.0, .m22 = 1.0, + }; +} + +fn png_matrix_multiply(left: PngMatrixF64, + right: PngMatrixF64) PngMatrixF64 { + return PngMatrixF64{ + .m00 = left.m00 * right.m00 + left.m01 * right.m10 + left.m02 * right.m20, + .m01 = left.m00 * right.m01 + left.m01 * right.m11 + left.m02 * right.m21, + .m02 = left.m00 * right.m02 + left.m01 * right.m12 + left.m02 * right.m22, + .m10 = left.m10 * right.m00 + left.m11 * right.m10 + left.m12 * right.m20, + .m11 = left.m10 * right.m01 + left.m11 * right.m11 + left.m12 * right.m21, + .m12 = left.m10 * right.m02 + left.m11 * right.m12 + left.m12 * right.m22, + .m20 = left.m20 * right.m00 + left.m21 * right.m10 + left.m22 * right.m20, + .m21 = left.m20 * right.m01 + left.m21 * right.m11 + left.m22 * right.m21, + .m22 = left.m20 * right.m02 + left.m21 * right.m12 + left.m22 * right.m22, + }; +} + +fn png_matrix_vector(matrix: PngMatrixF64, + vector: PngVectorF64) PngVectorF64 { + return PngVectorF64{ + .x = matrix.m00 * vector.x + matrix.m01 * vector.y + matrix.m02 * vector.z, + .y = matrix.m10 * vector.x + matrix.m11 * vector.y + matrix.m12 * vector.z, + .z = matrix.m20 * vector.x + matrix.m21 * vector.y + matrix.m22 * vector.z, + }; +} + +fn png_f64_abs(value: f64) f64 { + if (value < 0.0) { return 0.0 - value; } + return value; +} + +fn png_matrix_finite_bounded(matrix: PngMatrixF64, bound: f64) i32 { + if (matrix.m00 != matrix.m00 || matrix.m01 != matrix.m01 || + matrix.m02 != matrix.m02 || matrix.m10 != matrix.m10 || + matrix.m11 != matrix.m11 || matrix.m12 != matrix.m12 || + matrix.m20 != matrix.m20 || matrix.m21 != matrix.m21 || + matrix.m22 != matrix.m22) { return 0; } + return (png_f64_abs(matrix.m00) <= bound && + png_f64_abs(matrix.m01) <= bound && + png_f64_abs(matrix.m02) <= bound && + png_f64_abs(matrix.m10) <= bound && + png_f64_abs(matrix.m11) <= bound && + png_f64_abs(matrix.m12) <= bound && + png_f64_abs(matrix.m20) <= bound && + png_f64_abs(matrix.m21) <= bound && + png_f64_abs(matrix.m22) <= bound) as i32; +} + +fn png_matrix_inverse(matrix: PngMatrixF64) PngMatrixF64Result { + let determinant: f64 = matrix.m00 * + (matrix.m11 * matrix.m22 - matrix.m12 * matrix.m21) - + matrix.m01 * (matrix.m10 * matrix.m22 - matrix.m12 * matrix.m20) + + matrix.m02 * (matrix.m10 * matrix.m21 - matrix.m11 * matrix.m20); + if (png_f64_abs(determinant) < 0.000000000001) { + return PngMatrixF64Result{ .matrix = png_matrix_zero(), .valid = 0 }; + } + let inverse: f64 = 1.0 / determinant; + return PngMatrixF64Result{ + .matrix = PngMatrixF64{ + .m00 = (matrix.m11 * matrix.m22 - matrix.m12 * matrix.m21) * inverse, + .m01 = (matrix.m02 * matrix.m21 - matrix.m01 * matrix.m22) * inverse, + .m02 = (matrix.m01 * matrix.m12 - matrix.m02 * matrix.m11) * inverse, + .m10 = (matrix.m12 * matrix.m20 - matrix.m10 * matrix.m22) * inverse, + .m11 = (matrix.m00 * matrix.m22 - matrix.m02 * matrix.m20) * inverse, + .m12 = (matrix.m02 * matrix.m10 - matrix.m00 * matrix.m12) * inverse, + .m20 = (matrix.m10 * matrix.m21 - matrix.m11 * matrix.m20) * inverse, + .m21 = (matrix.m01 * matrix.m20 - matrix.m00 * matrix.m21) * inverse, + .m22 = (matrix.m00 * matrix.m11 - matrix.m01 * matrix.m10) * inverse, + }, + .valid = 1, + }; +} + +fn png_chromaticity_vector(x: i64, y: i64) PngVectorF64 { + let xf: f64 = x as f64 / 100000.0; + let yf: f64 = y as f64 / 100000.0; + return PngVectorF64{ + .x = xf / yf, .y = 1.0, .z = (1.0 - xf - yf) / yf, + }; +} + +fn png_rgb_to_xyz(red_x: i64, red_y: i64, green_x: i64, green_y: i64, + blue_x: i64, blue_y: i64, white_x: i64, + white_y: i64) PngMatrixF64Result { + let red: PngVectorF64 = png_chromaticity_vector(red_x, red_y); + let green: PngVectorF64 = png_chromaticity_vector(green_x, green_y); + let blue: PngVectorF64 = png_chromaticity_vector(blue_x, blue_y); + let primaries: PngMatrixF64 = PngMatrixF64{ + .m00 = red.x, .m01 = green.x, .m02 = blue.x, + .m10 = red.y, .m11 = green.y, .m12 = blue.y, + .m20 = red.z, .m21 = green.z, .m22 = blue.z, + }; + let inverse: PngMatrixF64Result = png_matrix_inverse(primaries); + if (inverse.valid == 0) { return inverse; } + let scale: PngVectorF64 = png_matrix_vector( + inverse.matrix, png_chromaticity_vector(white_x, white_y) + ); + return PngMatrixF64Result{ + .matrix = PngMatrixF64{ + .m00 = primaries.m00 * scale.x, + .m01 = primaries.m01 * scale.y, + .m02 = primaries.m02 * scale.z, + .m10 = primaries.m10 * scale.x, + .m11 = primaries.m11 * scale.y, + .m12 = primaries.m12 * scale.z, + .m20 = primaries.m20 * scale.x, + .m21 = primaries.m21 * scale.y, + .m22 = primaries.m22 * scale.z, + }, + .valid = 1, + }; +} + +fn png_quantize_matrix_value(value: f64) i64 { + if (value >= 0.0) { return (value * 16777216.0 + 0.5) as i64; } + return (value * 16777216.0 - 0.5) as i64; +} + +fn png_color_transform_identity() PngColorTransform { + return PngColorTransform{ + .m00 = 16777216, .m01 = 0, .m02 = 0, + .m10 = 0, .m11 = 16777216, .m12 = 0, + .m20 = 0, .m21 = 0, .m22 = 16777216, + .active = 0, .valid = 1, + }; +} + +fn png_chrm_values_valid(values: []i64) i32 { + if (values.len != 8) { return 0; } + let pair: i32 = 0; + while (pair < 4) { + let x: i64 = values[pair * 2]; + let y: i64 = values[pair * 2 + 1]; + if (x <= 0 || y <= 0 || x >= 100000 || y >= 100000 || + x + y > 100000) { return 0; } + pair = pair + 1; + } + return 1; +} + +fn png_chrm_transform(bytes: []u8, offset: i32, + length: i32) PngColorTransform { + if (length != 32) { + let invalid: PngColorTransform = png_color_transform_identity(); + invalid.valid = 0; return invalid; + } + if (png_chrm_is_srgb(bytes, offset, length) != 0) { + return png_color_transform_identity(); + } + let values: ArrayList[i64] = make[i64](8); + let index: i32 = 0; + while (index < 8) { + push[i64](&values, png_read_u32_be(bytes, offset + index * 4)); + index = index + 1; + } + if (png_chrm_values_valid(values.data[0..values.len]) == 0) { + free[i64](&values); + let invalid: PngColorTransform = png_color_transform_identity(); + invalid.valid = 0; return invalid; + } + let source: PngMatrixF64Result = png_rgb_to_xyz( + values.data[2], values.data[3], values.data[4], values.data[5], + values.data[6], values.data[7], values.data[0], values.data[1] + ); + let target: PngMatrixF64Result = png_rgb_to_xyz( + 64000, 33000, 30000, 60000, 15000, 6000, 31270, 32900 + ); + let bradford: PngMatrixF64 = PngMatrixF64{ + .m00 = 0.8951, .m01 = 0.2664, .m02 = -0.1614, + .m10 = -0.7502, .m11 = 1.7135, .m12 = 0.0367, + .m20 = 0.0389, .m21 = -0.0685, .m22 = 1.0296, + }; + let bradford_inverse: PngMatrixF64Result = png_matrix_inverse(bradford); + let target_inverse: PngMatrixF64Result = png_matrix_inverse(target.matrix); + if (source.valid == 0 || target.valid == 0 || + bradford_inverse.valid == 0 || target_inverse.valid == 0) { + free[i64](&values); + let invalid: PngColorTransform = png_color_transform_identity(); + invalid.valid = 0; return invalid; + } + let source_white: PngVectorF64 = png_chromaticity_vector( + values.data[0], values.data[1] + ); + let target_white: PngVectorF64 = png_chromaticity_vector(31270, 32900); + free[i64](&values); + let source_cone: PngVectorF64 = png_matrix_vector(bradford, source_white); + let target_cone: PngVectorF64 = png_matrix_vector(bradford, target_white); + if (png_f64_abs(source_cone.x) < 0.000000000001 || + png_f64_abs(source_cone.y) < 0.000000000001 || + png_f64_abs(source_cone.z) < 0.000000000001) { + let invalid: PngColorTransform = png_color_transform_identity(); + invalid.valid = 0; return invalid; + } + let cone_scale: PngMatrixF64 = PngMatrixF64{ + .m00 = target_cone.x / source_cone.x, .m01 = 0.0, .m02 = 0.0, + .m10 = 0.0, .m11 = target_cone.y / source_cone.y, .m12 = 0.0, + .m20 = 0.0, .m21 = 0.0, .m22 = target_cone.z / source_cone.z, + }; + let adaptation: PngMatrixF64 = png_matrix_multiply( + bradford_inverse.matrix, png_matrix_multiply(cone_scale, bradford) + ); + let final_matrix: PngMatrixF64 = png_matrix_multiply( + target_inverse.matrix, png_matrix_multiply(adaptation, source.matrix) + ); + if (png_matrix_finite_bounded(final_matrix, 8.0) == 0) { + let invalid: PngColorTransform = png_color_transform_identity(); + invalid.valid = 0; return invalid; + } + return PngColorTransform{ + .m00 = png_quantize_matrix_value(final_matrix.m00), + .m01 = png_quantize_matrix_value(final_matrix.m01), + .m02 = png_quantize_matrix_value(final_matrix.m02), + .m10 = png_quantize_matrix_value(final_matrix.m10), + .m11 = png_quantize_matrix_value(final_matrix.m11), + .m12 = png_quantize_matrix_value(final_matrix.m12), + .m20 = png_quantize_matrix_value(final_matrix.m20), + .m21 = png_quantize_matrix_value(final_matrix.m21), + .m22 = png_quantize_matrix_value(final_matrix.m22), + .active = 1, .valid = 1, + }; +} + +fn png_data_is(bytes: []u8, offset: i32, name: []u8) i32 { + if (name.len != 4 || offset < 0 || offset + 4 > bytes.len) { return 0; } + let i: i32 = 0; + while (i < 4) { + if (bytes[offset + i] != name[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn png_read_i32_be(bytes: []u8, offset: i32) i64 { + let raw: i64 = png_read_u32_be(bytes, offset); + if (raw >= 2147483648) { raw = raw - 4294967296; } + return raw; +} + +fn png_read_s15_fixed16(bytes: []u8, offset: i32) f64 { + let raw: i64 = png_read_i32_be(bytes, offset); + return raw as f64 / 65536.0; +} + +fn png_icc_tag(bytes: []u8, signature: []u8) PngIccTag { + let missing: PngIccTag = PngIccTag{ + .offset = 0, .size = 0, .found = 0, .valid = 1, + }; + if (bytes.len < 132) { missing.valid = 0; return missing; } + let count64: i64 = png_read_u32_be(bytes, 128); + if (count64 > 128 || 132 + count64 * 12 > bytes.len as i64) { + missing.valid = 0; return missing; + } + let result: PngIccTag = missing; + let index: i32 = 0; + while (index < count64 as i32) { + let entry: i32 = 132 + index * 12; + let tag_offset64: i64 = png_read_u32_be(bytes, entry + 4); + let tag_size64: i64 = png_read_u32_be(bytes, entry + 8); + if (tag_offset64 < 132 + count64 * 12 || tag_offset64 % 4 != 0 || + tag_size64 < 12 || tag_offset64 + tag_size64 > bytes.len as i64) { + result.valid = 0; return result; + } + if (png_data_is(bytes, entry, signature) != 0) { + if (result.found != 0) { result.valid = 0; return result; } + result.offset = tag_offset64 as i32; + result.size = tag_size64 as i32; + result.found = 1; + } + index = index + 1; + } + return result; +} + +fn png_icc_xyz(bytes: []u8, tag: PngIccTag) PngIccVectorResult { + let result: PngIccVectorResult = PngIccVectorResult{ + .value = PngVectorF64{ .x = 0.0, .y = 0.0, .z = 0.0 }, .valid = 0, + }; + if (tag.valid == 0 || tag.found == 0 || tag.size < 20 || + png_data_is(bytes, tag.offset, "XYZ ") == 0 || + png_read_u32_be(bytes, tag.offset + 4) != 0) { return result; } + result.value = PngVectorF64{ + .x = png_read_s15_fixed16(bytes, tag.offset + 8), + .y = png_read_s15_fixed16(bytes, tag.offset + 12), + .z = png_read_s15_fixed16(bytes, tag.offset + 16), + }; + if (result.value.x != result.value.x || result.value.y != result.value.y || + result.value.z != result.value.z || png_f64_abs(result.value.x) > 4.0 || + png_f64_abs(result.value.y) > 4.0 || + png_f64_abs(result.value.z) > 4.0) { return result; } + result.valid = 1; + return result; +} + +fn png_transfer_curve_invalid() PngTransferCurve { + return PngTransferCurve{ + .kind = 0, .g = 0, .a = 0, .b = 0, .c = 0, .d = 0, + .e = 0, .f = 0, + .samples = ArrayList[i64]{ + .data = null as *i64, .len = 0, .cap = 0, + }, + .valid = 0, + }; +} + +fn png_transfer_curve_free(curve: *PngTransferCurve) void { + if (curve.*.samples.cap > 0) { free[i64](&curve.*.samples); } +} + +fn png_icc_lut_invalid() PngIccLut { + return PngIccLut{ + .red_grid_points = 0, .green_grid_points = 0, .blue_grid_points = 0, + .input_entries = 0, .output_entries = 0, + .input_tables = ArrayList[i32]{ + .data = null as *i32, .len = 0, .cap = 0, + }, + .clut = ArrayList[i32]{ + .data = null as *i32, .len = 0, .cap = 0, + }, + .output_tables = ArrayList[i32]{ + .data = null as *i32, .len = 0, .cap = 0, + }, + .valid = 0, + }; +} + +fn png_icc_lut_free(lut: *PngIccLut) void { + if (lut.*.input_tables.cap > 0) { free[i32](&lut.*.input_tables); } + if (lut.*.clut.cap > 0) { free[i32](&lut.*.clut); } + if (lut.*.output_tables.cap > 0) { free[i32](&lut.*.output_tables); } +} + +fn png_icc_profile_free(profile: *PngIccProfileResult) void { + png_transfer_curve_free(&profile.*.red_curve); + png_transfer_curve_free(&profile.*.green_curve); + png_transfer_curve_free(&profile.*.blue_curve); + png_icc_lut_free(&profile.*.lut); +} + +fn png_transfer_curve_external() PngTransferCurve { + let curve: PngTransferCurve = png_transfer_curve_invalid(); + curve.valid = 1; return curve; +} + +fn png_icc_curve(bytes: []u8, tag: PngIccTag) PngTransferCurve { + let curve: PngTransferCurve = png_transfer_curve_invalid(); + if (tag.valid == 0 || tag.found == 0 || tag.size < 12 || + png_read_u32_be(bytes, tag.offset + 4) != 0) { return curve; } + if (png_data_is(bytes, tag.offset, "curv") != 0) { + let count: i64 = png_read_u32_be(bytes, tag.offset + 8); + curve.kind = 1; + if (count == 0) { curve.g = 1048576; curve.valid = 1; return curve; } + if (count == 1) { + if (tag.size < 14) { return curve; } + curve.g = (png_read_u16_be(bytes, tag.offset + 12) as i64) * 4096; + } else { + if (count > 4096 || (tag.size as i64) < 12 + count * 2) { + return curve; + } + curve.kind = 6; + curve.samples = make[i64](256); + let sample: i32 = 0; + while (sample < 256) { + let scaled: i64 = (sample as i64) * (count - 1); + let lower: i32 = (scaled / 255) as i32; + let remainder: i64 = scaled % 255; + let upper: i32 = lower + + (lower + 1 < (count as i32)) as i32; + let lower_value: i64 = png_read_u16_be( + bytes, tag.offset + 12 + lower * 2 + ); + let upper_value: i64 = png_read_u16_be( + bytes, tag.offset + 12 + upper * 2 + ); + let interpolated: i64 = (lower_value * (255 - remainder) + + upper_value * remainder + 127) / 255; + push[i64](&curve.samples, + (interpolated * 1073741824 + 32767) / 65535); + sample = sample + 1; + } + curve.valid = 1; + return curve; + } + } else if (png_data_is(bytes, tag.offset, "para") != 0) { + let function_type: i32 = png_read_u16_be(bytes, tag.offset + 8); + let parameter_count: i32 = 0; + if (function_type == 0) { parameter_count = 1; } + else if (function_type == 1) { parameter_count = 3; } + else if (function_type == 2) { parameter_count = 4; } + else if (function_type == 3) { parameter_count = 5; } + else if (function_type == 4) { parameter_count = 7; } + else { return curve; } + if (png_read_u16_be(bytes, tag.offset + 10) != 0 || + tag.size < 12 + parameter_count * 4) { return curve; } + curve.kind = function_type + 1; + curve.g = png_read_i32_be(bytes, tag.offset + 12) * 16; + if (parameter_count > 1) { + curve.a = png_read_i32_be(bytes, tag.offset + 16) * 16384; + curve.b = png_read_i32_be(bytes, tag.offset + 20) * 16384; + } + if (parameter_count > 3) { + curve.c = png_read_i32_be(bytes, tag.offset + 24) * 16384; + } + if (function_type == 3) { + curve.d = png_read_i32_be(bytes, tag.offset + 28) * 16384; + } else if (function_type == 4) { + curve.d = png_read_i32_be(bytes, tag.offset + 28) * 16384; + curve.e = png_read_i32_be(bytes, tag.offset + 32) * 16384; + curve.f = png_read_i32_be(bytes, tag.offset + 36) * 16384; + } + } else { return curve; } + if (curve.g < 104858 || curve.g > 10485760 || + (curve.kind > 1 && (curve.a <= 0 || curve.a > 2147483648 || + curve.a + curve.b > 1073741824)) || + (curve.kind >= 4 && (curve.d < 0 || curve.d > 1073741824)) || + (curve.kind >= 4 && + (curve.a * curve.d) / 1073741824 + curve.b < 0) || + png_f64_abs(curve.b as f64) > 2147483648.0 || + png_f64_abs(curve.c as f64) > 2147483648.0 || + png_f64_abs(curve.e as f64) > 2147483648.0 || + png_f64_abs(curve.f as f64) > 2147483648.0) { return curve; } + curve.valid = 1; + return curve; +} + +fn png_icc_curve_size(bytes: []u8, offset: i32, limit: i32) i32 { + if (offset < 0 || limit - offset < 12 || + png_read_u32_be(bytes, offset + 4) != 0) { return 0; } + if (png_data_is(bytes, offset, "curv") != 0) { + let count: i64 = png_read_u32_be(bytes, offset + 8); + if (count > 4096 || 12 + count * 2 > (limit - offset) as i64) { + return 0; + } + return (12 + count * 2) as i32; + } + if (png_data_is(bytes, offset, "para") != 0) { + let function_type: i32 = png_read_u16_be(bytes, offset + 8); + let parameter_count: i32 = 0; + if (function_type == 0) { parameter_count = 1; } + else if (function_type == 1) { parameter_count = 3; } + else if (function_type == 2) { parameter_count = 4; } + else if (function_type == 3) { parameter_count = 5; } + else if (function_type == 4) { parameter_count = 7; } + else { return 0; } + if (png_read_u16_be(bytes, offset + 10) != 0 || + 12 + parameter_count * 4 > limit - offset) { return 0; } + return 12 + parameter_count * 4; + } + return 0; +} + +fn png_icc_curve_tables(bytes: []u8, base: i32, start: i32, + limit: i32) PngIccCurveTableResult { + let result: PngIccCurveTableResult = PngIccCurveTableResult{ + .table = make[i32](768), .valid = 0, + }; + let cursor: i32 = start; + let channel: i32 = 0; + while (channel < 3) { + let size: i32 = png_icc_curve_size(bytes, base + cursor, base + limit); + if (size == 0) { free[i32](&result.table); return result; } + let curve: PngTransferCurve = png_icc_curve(bytes, PngIccTag{ + .offset = base + cursor, .size = size, .found = 1, .valid = 1, + }); + if (curve.valid == 0) { + png_transfer_curve_free(&curve); + free[i32](&result.table); return result; + } + let sample: i32 = 0; + while (sample < 256) { + let q30: i64 = png_transfer_sample_to_linear_q30( + sample as u8, 0, curve + ); + push[i32](&result.table, + ((q30 * 65535 + 536870912) / 1073741824) as i32); + sample = sample + 1; + } + png_transfer_curve_free(&curve); + let aligned: i32 = (size + 3) & (0 - 4); + let padding: i32 = size; + while (padding < aligned) { + if (cursor + padding >= limit || + bytes[base + cursor + padding] != 0) { + free[i32](&result.table); return result; + } + padding = padding + 1; + } + cursor = cursor + aligned; + channel = channel + 1; + } + if (cursor != limit) { free[i32](&result.table); return result; } + result.valid = 1; return result; +} + +fn png_icc_element_limit(start: i32, first: i32, second: i32, + third: i32, tag_size: i32) i32 { + let limit: i32 = tag_size; + if (first > start && first < limit) { limit = first; } + if (second > start && second < limit) { limit = second; } + if (third > start && third < limit) { limit = third; } + return limit; +} + +fn png_icc_lut16(bytes: []u8, tag: PngIccTag) PngIccLut { + let lut: PngIccLut = png_icc_lut_invalid(); + if (tag.valid == 0 || tag.found == 0 || tag.size < 52 || + png_data_is(bytes, tag.offset, "mft2") == 0 || + png_read_u32_be(bytes, tag.offset + 4) != 0 || + bytes[tag.offset + 8] != 3 || bytes[tag.offset + 9] != 3 || + bytes[tag.offset + 11] != 0) { return lut; } + let grid: i32 = bytes[tag.offset + 10] as i32; + let input_entries: i32 = png_read_u16_be(bytes, tag.offset + 48); + let output_entries: i32 = png_read_u16_be(bytes, tag.offset + 50); + if (grid < 2 || grid > 33 || input_entries < 2 || input_entries > 4096 || + output_entries < 2 || output_entries > 4096) { return lut; } + let matrix_index: i32 = 0; + while (matrix_index < 9) { + let expected: i64 = 0; + if (matrix_index == 0 || matrix_index == 4 || matrix_index == 8) { + expected = 65536; + } + if (png_read_i32_be(bytes, tag.offset + 12 + matrix_index * 4) != expected) { + return lut; + } + matrix_index = matrix_index + 1; + } + let grid64: i64 = grid as i64; + let clut_entries64: i64 = grid64 * grid64 * grid64 * 3; + let expected_size: i64 = 52 + (input_entries as i64) * 3 * 2 + + clut_entries64 * 2 + (output_entries as i64) * 3 * 2; + if (expected_size != tag.size as i64 || clut_entries64 > 107811) { + return lut; + } + lut.input_tables = make[i32](input_entries * 3); + lut.clut = make[i32](clut_entries64 as i32); + lut.output_tables = make[i32](output_entries * 3); + let cursor: i32 = tag.offset + 52; + let index: i32 = 0; + while (index < input_entries * 3) { + push[i32](&lut.input_tables, png_read_u16_be(bytes, cursor)); + cursor = cursor + 2; index = index + 1; + } + index = 0; + while (index < clut_entries64 as i32) { + push[i32](&lut.clut, png_read_u16_be(bytes, cursor)); + cursor = cursor + 2; index = index + 1; + } + index = 0; + while (index < output_entries * 3) { + push[i32](&lut.output_tables, png_read_u16_be(bytes, cursor)); + cursor = cursor + 2; index = index + 1; + } + lut.red_grid_points = grid; + lut.green_grid_points = grid; + lut.blue_grid_points = grid; + lut.input_entries = input_entries; + lut.output_entries = output_entries; + lut.valid = 1; + return lut; +} + +fn png_icc_lut_atob(bytes: []u8, tag: PngIccTag) PngIccLut { + let lut: PngIccLut = png_icc_lut_invalid(); + if (tag.valid == 0 || tag.found == 0 || tag.size < 32 || + png_data_is(bytes, tag.offset, "mAB ") == 0 || + png_read_u32_be(bytes, tag.offset + 4) != 0 || + bytes[tag.offset + 8] != 3 || bytes[tag.offset + 9] != 3 || + bytes[tag.offset + 10] != 0 || bytes[tag.offset + 11] != 0) { + return lut; + } + let b_offset: i32 = png_read_u32_be(bytes, tag.offset + 12) as i32; + let matrix_offset: i32 = png_read_u32_be(bytes, tag.offset + 16) as i32; + let m_offset: i32 = png_read_u32_be(bytes, tag.offset + 20) as i32; + let clut_offset: i32 = png_read_u32_be(bytes, tag.offset + 24) as i32; + let a_offset: i32 = png_read_u32_be(bytes, tag.offset + 28) as i32; + if (a_offset < 32 || b_offset < 32 || clut_offset < 32 || + a_offset % 4 != 0 || b_offset % 4 != 0 || clut_offset % 4 != 0 || + a_offset >= tag.size || b_offset >= tag.size || + clut_offset >= tag.size || clut_offset == a_offset || + clut_offset == b_offset || matrix_offset != 0 || m_offset != 0) { + return lut; + } + let a_limit: i32 = png_icc_element_limit( + a_offset, b_offset, clut_offset, 0, tag.size + ); + let b_limit: i32 = png_icc_element_limit( + b_offset, a_offset, clut_offset, 0, tag.size + ); + let clut_limit: i32 = png_icc_element_limit( + clut_offset, a_offset, b_offset, 0, tag.size + ); + let a_curves: PngIccCurveTableResult = png_icc_curve_tables( + bytes, tag.offset, a_offset, a_limit + ); + if (a_curves.valid == 0) { return lut; } + let b_curves: PngIccCurveTableResult = png_icc_curve_tables( + bytes, tag.offset, b_offset, b_limit + ); + if (b_curves.valid == 0) { + free[i32](&a_curves.table); return lut; + } + if (clut_limit - clut_offset < 20) { + free[i32](&a_curves.table); free[i32](&b_curves.table); return lut; + } + let red_grid: i32 = bytes[tag.offset + clut_offset] as i32; + let green_grid: i32 = bytes[tag.offset + clut_offset + 1] as i32; + let blue_grid: i32 = bytes[tag.offset + clut_offset + 2] as i32; + if (red_grid < 2 || red_grid > 33 || green_grid < 2 || green_grid > 33 || + blue_grid < 2 || blue_grid > 33) { + free[i32](&a_curves.table); free[i32](&b_curves.table); return lut; + } + let unused: i32 = 3; + while (unused < 16) { + if (bytes[tag.offset + clut_offset + unused] != 0) { + free[i32](&a_curves.table); free[i32](&b_curves.table); return lut; + } + unused = unused + 1; + } + let precision: i32 = bytes[tag.offset + clut_offset + 16] as i32; + if ((precision != 1 && precision != 2) || + bytes[tag.offset + clut_offset + 17] != 0 || + bytes[tag.offset + clut_offset + 18] != 0 || + bytes[tag.offset + clut_offset + 19] != 0) { + free[i32](&a_curves.table); free[i32](&b_curves.table); return lut; + } + let entries64: i64 = (red_grid as i64) * (green_grid as i64) * + (blue_grid as i64) * 3; + let data_end: i32 = clut_offset + 20 + (entries64 as i32) * precision; + let aligned_end: i32 = (data_end + 3) & (0 - 4); + if (entries64 > 107811 || aligned_end != clut_limit) { + free[i32](&a_curves.table); free[i32](&b_curves.table); return lut; + } + let padding: i32 = data_end; + while (padding < aligned_end) { + if (bytes[tag.offset + padding] != 0) { + free[i32](&a_curves.table); free[i32](&b_curves.table); return lut; + } + padding = padding + 1; + } + lut.clut = make[i32](entries64 as i32); + let cursor: i32 = tag.offset + clut_offset + 20; + let index: i32 = 0; + while (index < entries64 as i32) { + if (precision == 1) { + push[i32](&lut.clut, (bytes[cursor] as i32) * 257); + cursor = cursor + 1; + } else { + push[i32](&lut.clut, png_read_u16_be(bytes, cursor)); + cursor = cursor + 2; + } + index = index + 1; + } + lut.input_tables = a_curves.table; + lut.output_tables = b_curves.table; + lut.input_entries = 256; lut.output_entries = 256; + lut.red_grid_points = red_grid; + lut.green_grid_points = green_grid; + lut.blue_grid_points = blue_grid; + lut.valid = 1; return lut; +} + +fn png_icc_matrix_profile(bytes: []u8) PngIccProfileResult { + let result: PngIccProfileResult = PngIccProfileResult{ + .transform = png_color_transform_identity(), + .red_curve = png_transfer_curve_invalid(), + .green_curve = png_transfer_curve_invalid(), + .blue_curve = png_transfer_curve_invalid(), + .lut = png_icc_lut_invalid(), .valid = 0, + }; + if (bytes.len < 132 || bytes.len > 4194304 || + png_read_u32_be(bytes, 0) != bytes.len as i64 || + (bytes[8] != 2 && bytes[8] != 4) || + !(png_data_is(bytes, 12, "scnr") != 0 || + png_data_is(bytes, 12, "mntr") != 0) || + png_data_is(bytes, 16, "RGB ") == 0 || + png_data_is(bytes, 20, "XYZ ") == 0 || + png_data_is(bytes, 36, "acsp") == 0 || + png_read_u32_be(bytes, 64) > 3) { return result; } + let reserved: i32 = 100; + while (reserved < 128) { + if (bytes[reserved] != 0) { return result; } + reserved = reserved + 1; + } + let illuminant: PngVectorF64 = PngVectorF64{ + .x = png_read_s15_fixed16(bytes, 68), + .y = png_read_s15_fixed16(bytes, 72), + .z = png_read_s15_fixed16(bytes, 76), + }; + if (png_f64_abs(illuminant.x - 0.9642) > 0.001 || + png_f64_abs(illuminant.y - 1.0) > 0.001 || + png_f64_abs(illuminant.z - 0.8249) > 0.001) { return result; } + let lut_tag: PngIccTag = png_icc_tag(bytes, "A2B0"); + if (lut_tag.valid == 0) { return result; } + if (lut_tag.found != 0) { + if (png_read_u32_be(bytes, 64) != 0) { return result; } + let lut: PngIccLut = png_icc_lut16(bytes, lut_tag); + if (png_data_is(bytes, lut_tag.offset, "mAB ") != 0) { + lut = png_icc_lut_atob(bytes, lut_tag); + } + if (lut.valid == 0) { return result; } + let target: PngMatrixF64 = PngMatrixF64{ + .m00 = 0.4360747, .m01 = 0.3850649, .m02 = 0.1430804, + .m10 = 0.2225045, .m11 = 0.7168786, .m12 = 0.0606169, + .m20 = 0.0139322, .m21 = 0.0971045, .m22 = 0.7141733, + }; + let target_inverse: PngMatrixF64Result = png_matrix_inverse(target); + if (target_inverse.valid == 0 || + png_matrix_finite_bounded(target_inverse.matrix, 8.0) == 0) { + png_icc_lut_free(&lut); return result; + } + result.transform = PngColorTransform{ + .m00 = png_quantize_matrix_value(target_inverse.matrix.m00), + .m01 = png_quantize_matrix_value(target_inverse.matrix.m01), + .m02 = png_quantize_matrix_value(target_inverse.matrix.m02), + .m10 = png_quantize_matrix_value(target_inverse.matrix.m10), + .m11 = png_quantize_matrix_value(target_inverse.matrix.m11), + .m12 = png_quantize_matrix_value(target_inverse.matrix.m12), + .m20 = png_quantize_matrix_value(target_inverse.matrix.m20), + .m21 = png_quantize_matrix_value(target_inverse.matrix.m21), + .m22 = png_quantize_matrix_value(target_inverse.matrix.m22), + .active = 1, .valid = 1, + }; + result.lut = lut; result.valid = 1; return result; + } + let red_xyz: PngIccVectorResult = png_icc_xyz(bytes, png_icc_tag(bytes, "rXYZ")); + let green_xyz: PngIccVectorResult = png_icc_xyz(bytes, png_icc_tag(bytes, "gXYZ")); + let blue_xyz: PngIccVectorResult = png_icc_xyz(bytes, png_icc_tag(bytes, "bXYZ")); + if (red_xyz.valid == 0 || green_xyz.valid == 0 || blue_xyz.valid == 0) { + return result; + } + let red_curve: PngTransferCurve = png_icc_curve( + bytes, png_icc_tag(bytes, "rTRC") + ); + let green_curve: PngTransferCurve = png_icc_curve( + bytes, png_icc_tag(bytes, "gTRC") + ); + let blue_curve: PngTransferCurve = png_icc_curve( + bytes, png_icc_tag(bytes, "bTRC") + ); + if (red_curve.valid == 0 || green_curve.valid == 0 || + blue_curve.valid == 0) { + png_transfer_curve_free(&red_curve); + png_transfer_curve_free(&green_curve); + png_transfer_curve_free(&blue_curve); + return result; + } + let source: PngMatrixF64 = PngMatrixF64{ + .m00 = red_xyz.value.x, .m01 = green_xyz.value.x, .m02 = blue_xyz.value.x, + .m10 = red_xyz.value.y, .m11 = green_xyz.value.y, .m12 = blue_xyz.value.y, + .m20 = red_xyz.value.z, .m21 = green_xyz.value.z, .m22 = blue_xyz.value.z, + }; + let target: PngMatrixF64 = PngMatrixF64{ + .m00 = 0.4360747, .m01 = 0.3850649, .m02 = 0.1430804, + .m10 = 0.2225045, .m11 = 0.7168786, .m12 = 0.0606169, + .m20 = 0.0139322, .m21 = 0.0971045, .m22 = 0.7141733, + }; + let target_inverse: PngMatrixF64Result = png_matrix_inverse(target); + if (target_inverse.valid == 0) { + png_transfer_curve_free(&red_curve); + png_transfer_curve_free(&green_curve); + png_transfer_curve_free(&blue_curve); + return result; + } + let matrix: PngMatrixF64 = png_matrix_multiply(target_inverse.matrix, source); + if (png_matrix_finite_bounded(matrix, 8.0) == 0) { + png_transfer_curve_free(&red_curve); + png_transfer_curve_free(&green_curve); + png_transfer_curve_free(&blue_curve); + return result; + } + result.transform = PngColorTransform{ + .m00 = png_quantize_matrix_value(matrix.m00), + .m01 = png_quantize_matrix_value(matrix.m01), + .m02 = png_quantize_matrix_value(matrix.m02), + .m10 = png_quantize_matrix_value(matrix.m10), + .m11 = png_quantize_matrix_value(matrix.m11), + .m12 = png_quantize_matrix_value(matrix.m12), + .m20 = png_quantize_matrix_value(matrix.m20), + .m21 = png_quantize_matrix_value(matrix.m21), + .m22 = png_quantize_matrix_value(matrix.m22), + .active = 1, .valid = 1, + }; + result.red_curve = red_curve; + result.green_curve = green_curve; + result.blue_curve = blue_curve; + result.valid = 1; + return result; +} + +fn png_iccp_profile(bytes: []u8, offset: i32, + length: i32) PngIccProfileResult { + let result: PngIccProfileResult = PngIccProfileResult{ + .transform = png_color_transform_identity(), + .red_curve = png_transfer_curve_invalid(), + .green_curve = png_transfer_curve_invalid(), + .blue_curve = png_transfer_curve_invalid(), + .lut = png_icc_lut_invalid(), .valid = 0, + }; + if (length < 9) { return result; } + let name_end: i32 = offset; + while (name_end < offset + length && name_end - offset < 80 && + bytes[name_end] != 0) { + let value: i32 = bytes[name_end] as i32; + if (!((value >= 32 && value <= 126) || value >= 161) || + ((name_end == offset || bytes[name_end - 1] == 32) && value == 32)) { + return result; + } + name_end = name_end + 1; + } + if (name_end == offset || name_end >= offset + length || + name_end - offset > 79 || bytes[name_end] != 0 || + bytes[name_end - 1] == 32 || name_end + 2 >= offset + length || + bytes[name_end + 1] != 0) { return result; } + let compressed_offset: i32 = name_end + 2; + let inflated: ZlibInflateResult = zlib_inflate( + bytes[compressed_offset..offset + length], 4194304 + ); + if (inflated.error != ZlibInflateError.none) { + zlib_inflate_free(&inflated); return result; + } + let parsed: PngIccProfileResult = png_icc_matrix_profile( + inflated.bytes.data[0..inflated.bytes.len] + ); + zlib_inflate_free(&inflated); + return parsed; +} + +fn png_color_channels(color_type: i32) i32 { + if (color_type == 0 || color_type == 3) { return 1; } + if (color_type == 2) { return 3; } + if (color_type == 4) { return 2; } + if (color_type == 6) { return 4; } + return 0; +} + +fn png_bit_depth_valid(color_type: i32, bit_depth: i32) i32 { + if (color_type == 0) { + return (bit_depth == 1 || bit_depth == 2 || bit_depth == 4 || + bit_depth == 8 || bit_depth == 16) as i32; + } + if (color_type == 2 || color_type == 4 || color_type == 6) { + return (bit_depth == 8 || bit_depth == 16) as i32; + } + if (color_type == 3) { + return (bit_depth == 1 || bit_depth == 2 || bit_depth == 4 || + bit_depth == 8) as i32; + } + return 0; +} + +fn png_copy_append(output: *ArrayList[u8], bytes: []u8) void { + let i: i32 = 0; + while (i < bytes.len) { push[u8](output, bytes[i]); i = i + 1; } +} + +fn png_decode_parse_fail(idat: *ArrayList[u8], palette: *ArrayList[u8], + transparency: *ArrayList[u8], error: PngDecodeError, + offset: i64) PngDecodeResult { + free[u8](idat); free[u8](palette); free[u8](transparency); + return png_decode_fail(error, offset); +} + +fn png_paeth(a: i32, b: i32, c: i32) i32 { + let estimate: i32 = a + b - c; + let distance_a: i32 = estimate - a; + let distance_b: i32 = estimate - b; + let distance_c: i32 = estimate - c; + if (distance_a < 0) { distance_a = 0 - distance_a; } + if (distance_b < 0) { distance_b = 0 - distance_b; } + if (distance_c < 0) { distance_c = 0 - distance_c; } + if (distance_a <= distance_b && distance_a <= distance_c) { return a; } + if (distance_b <= distance_c) { return b; } + return c; +} + +fn png_unfilter(filtered: []u8, height: i32, row_bytes: i32, + bytes_per_pixel: i32) PngScanlineResult { + let output: ArrayList[u8] = make[u8](height * row_bytes); + let y: i32 = 0; + while (y < height) { + let source_row: i32 = y * (row_bytes + 1); + let filter: i32 = filtered[source_row] as i32; + if (filter < 0 || filter > 4) { + return PngScanlineResult{ + .bytes = output, .error = PngDecodeError.invalid_filter, + .error_offset = source_row as i64, + }; + } + let x: i32 = 0; + while (x < row_bytes) { + let encoded: i32 = filtered[source_row + 1 + x] as i32; + let left: i32 = 0; + let above: i32 = 0; + let upper_left: i32 = 0; + if (x >= bytes_per_pixel) { + left = output.data[y * row_bytes + x - bytes_per_pixel] as i32; + } + if (y > 0) { above = output.data[(y - 1) * row_bytes + x] as i32; } + if (y > 0 && x >= bytes_per_pixel) { + upper_left = output.data[(y - 1) * row_bytes + + x - bytes_per_pixel] as i32; + } + let predictor: i32 = 0; + if (filter == 1) { predictor = left; } + else if (filter == 2) { predictor = above; } + else if (filter == 3) { predictor = (left + above) / 2; } + else if (filter == 4) { predictor = png_paeth(left, above, upper_left); } + push[u8](&output, ((encoded + predictor) & 255) as u8); + x = x + 1; + } + y = y + 1; + } + return PngScanlineResult{ + .bytes = output, .error = PngDecodeError.none, .error_offset = 0 - 1, + }; +} + +fn png_adam7_start_x(pass: i32) i32 { + if (pass == 1) { return 4; } + if (pass == 3) { return 2; } + if (pass == 5) { return 1; } + return 0; +} + +fn png_adam7_start_y(pass: i32) i32 { + if (pass == 2) { return 4; } + if (pass == 4) { return 2; } + if (pass == 6) { return 1; } + return 0; +} + +fn png_adam7_step_x(pass: i32) i32 { + if (pass <= 1) { return 8; } + if (pass <= 3) { return 4; } + if (pass <= 5) { return 2; } + return 1; +} + +fn png_adam7_step_y(pass: i32) i32 { + if (pass <= 1) { return 8; } + if (pass <= 3) { return 8 - ((pass - 2) * 4); } + if (pass <= 5) { return 4 - ((pass - 4) * 2); } + return 2; +} + +fn png_adam7_extent(size: i32, start: i32, step: i32) i32 { + if (size <= start) { return 0; } + return (size - start + step - 1) / step; +} + +fn png_adam7_expected_size(width: i32, height: i32, + channels: i32, bit_depth: i32) i64 { + let total: i64 = 0; + let pass: i32 = 0; + while (pass < 7) { + let pass_width: i32 = png_adam7_extent( + width, png_adam7_start_x(pass), png_adam7_step_x(pass) + ); + let pass_height: i32 = png_adam7_extent( + height, png_adam7_start_y(pass), png_adam7_step_y(pass) + ); + if (pass_width > 0 && pass_height > 0) { + let row_bits: i64 = (pass_width as i64) * + (channels as i64) * (bit_depth as i64); + total = total + (((row_bits + 7) / 8) + 1) * (pass_height as i64); + } + pass = pass + 1; + } + return total; +} + +fn png_sample(row: []u8, sample_index: i32, bit_depth: i32) i32 { + if (bit_depth == 8) { return row[sample_index] as i32; } + if (bit_depth == 16) { return png_read_u16_be(row, sample_index * 2); } + let bit_offset: i32 = sample_index * bit_depth; + let byte_index: i32 = bit_offset / 8; + let shift: i32 = 8 - bit_depth - (bit_offset % 8); + return ((row[byte_index] as i32) >> shift) & ((1 << bit_depth) - 1); +} + +fn png_write_packed_sample(row: []u8, sample_index: i32, + bit_depth: i32, sample: i32) void { + let bit_offset: i32 = sample_index * bit_depth; + let byte_index: i32 = bit_offset / 8; + let shift: i32 = 8 - bit_depth - (bit_offset % 8); + row[byte_index] = row[byte_index] | + ((sample << shift) & 255) as u8; +} + +fn png_deinterlace(filtered: []u8, width: i32, height: i32, + channels: i32, bit_depth: i32) PngScanlineResult { + let full_row_bits: i64 = (width as i64) * + (channels as i64) * (bit_depth as i64); + let full_row_bytes: i32 = ((full_row_bits + 7) / 8) as i32; + let output: ArrayList[u8] = make[u8](full_row_bytes * height); + let fill_index: i32 = 0; + while (fill_index < full_row_bytes * height) { + push[u8](&output, 0); fill_index = fill_index + 1; + } + let bytes_per_pixel: i32 = ((channels * bit_depth) + 7) / 8; + if (bytes_per_pixel < 1) { bytes_per_pixel = 1; } + let filtered_offset: i32 = 0; + let pass: i32 = 0; + while (pass < 7) { + let start_x: i32 = png_adam7_start_x(pass); + let start_y: i32 = png_adam7_start_y(pass); + let step_x: i32 = png_adam7_step_x(pass); + let step_y: i32 = png_adam7_step_y(pass); + let pass_width: i32 = png_adam7_extent(width, start_x, step_x); + let pass_height: i32 = png_adam7_extent(height, start_y, step_y); + if (pass_width > 0 && pass_height > 0) { + let pass_row_bits: i64 = (pass_width as i64) * + (channels as i64) * (bit_depth as i64); + let pass_row_bytes: i32 = ((pass_row_bits + 7) / 8) as i32; + let pass_filtered_size: i32 = (pass_row_bytes + 1) * pass_height; + let scanlines: PngScanlineResult = png_unfilter( + filtered[filtered_offset..filtered_offset + pass_filtered_size], + pass_height, pass_row_bytes, bytes_per_pixel + ); + if (scanlines.error != PngDecodeError.none) { + let error_offset: i64 = filtered_offset as i64 + + scanlines.error_offset; + free[u8](&output); + return PngScanlineResult{ + .bytes = scanlines.bytes, .error = scanlines.error, + .error_offset = error_offset, + }; + } + let pass_y: i32 = 0; + while (pass_y < pass_height) { + let destination_y: i32 = start_y + pass_y * step_y; + let source_row: []u8 = scanlines.bytes.data[ + pass_y * pass_row_bytes..(pass_y + 1) * pass_row_bytes + ]; + let destination_row: []u8 = output.data[ + destination_y * full_row_bytes.. + (destination_y + 1) * full_row_bytes + ]; + let pass_x: i32 = 0; + while (pass_x < pass_width) { + let destination_x: i32 = start_x + pass_x * step_x; + if (bit_depth < 8) { + png_write_packed_sample(destination_row, destination_x, + bit_depth, png_sample(source_row, pass_x, bit_depth)); + } else { + let pixel_bytes: i32 = channels * (bit_depth / 8); + let byte_index: i32 = 0; + while (byte_index < pixel_bytes) { + destination_row[destination_x * pixel_bytes + byte_index] = + source_row[pass_x * pixel_bytes + byte_index]; + byte_index = byte_index + 1; + } + } + pass_x = pass_x + 1; + } + pass_y = pass_y + 1; + } + free[u8](&scanlines.bytes); + filtered_offset = filtered_offset + pass_filtered_size; + } + pass = pass + 1; + } + return PngScanlineResult{ + .bytes = output, .error = PngDecodeError.none, .error_offset = 0 - 1, + }; +} + +fn png_reconstruct_scanlines(filtered: []u8, width: i32, height: i32, + row_bytes: i32, channels: i32, bit_depth: i32, + interlace: i32) PngScanlineResult { + let bytes_per_pixel: i32 = ((channels * bit_depth) + 7) / 8; + if (bytes_per_pixel < 1) { bytes_per_pixel = 1; } + if (interlace == 0) { + return png_unfilter(filtered, height, row_bytes, bytes_per_pixel); + } + return png_deinterlace(filtered, width, height, channels, bit_depth); +} + +fn png_sample_u8(sample: i32, bit_depth: i32) u8 { + if (bit_depth == 1) { return (sample * 255) as u8; } + if (bit_depth == 2) { return (sample * 85) as u8; } + if (bit_depth == 4) { return (sample * 17) as u8; } + if (bit_depth == 16) { return ((sample >> 8) & 255) as u8; } + return sample as u8; +} + +fn png_q30_one() i64 { return 1073741824; } +fn png_q20_one() i64 { return 1048576; } + +fn png_mul_q30(left: i64, right: i64) i64 { + return (left * right + png_q30_one() / 2) / png_q30_one(); +} + +fn png_exp2_factor(index: i32) i64 { + if (index == 1) { return 1518500250; } + if (index == 2) { return 1276901417; } + if (index == 3) { return 1170923762; } + if (index == 4) { return 1121280436; } + if (index == 5) { return 1097253708; } + if (index == 6) { return 1085434106; } + if (index == 7) { return 1079572136; } + if (index == 8) { return 1076653033; } + if (index == 9) { return 1075196443; } + if (index == 10) { return 1074468888; } + if (index == 11) { return 1074105294; } + if (index == 12) { return 1073923544; } + if (index == 13) { return 1073832680; } + if (index == 14) { return 1073787251; } + if (index == 15) { return 1073764537; } + if (index == 16) { return 1073753181; } + if (index == 17) { return 1073747502; } + if (index == 18) { return 1073744663; } + if (index == 19) { return 1073743244; } + return 1073742534; +} + +fn png_log2_q20(value: i64) i64 { + if (value <= 0) { return 0 - (64 * png_q20_one()); } + let normalized: i64 = value; + let integer: i64 = 0; + while (normalized < png_q30_one()) { + normalized = normalized * 2; integer = integer - 1; + } + let fraction: i64 = 0; + let index: i32 = 1; + while (index <= 20) { + normalized = png_mul_q30(normalized, normalized); + if (normalized >= 2 * png_q30_one()) { + normalized = normalized / 2; + fraction = fraction | (1 << (20 - index)) as i64; + } + index = index + 1; + } + return integer * png_q20_one() + fraction; +} + +fn png_exp2_q30(value: i64) i64 { + let integer: i64 = value / png_q20_one(); + let fraction: i64 = value % png_q20_one(); + if (fraction < 0) { + fraction = fraction + png_q20_one(); integer = integer - 1; + } + let result: i64 = png_q30_one(); + let index: i32 = 1; + while (index <= 20) { + if ((fraction & (1 << (20 - index)) as i64) != 0) { + result = png_mul_q30(result, png_exp2_factor(index)); + } + index = index + 1; + } + if (integer < 0) { + let shift: i64 = 0 - integer; + if (shift >= 31) { return 0; } + return result >> shift; + } + if (integer > 0) { + if (integer >= 31) { return 2147483647; } + return result << integer; + } + return result; +} + +fn png_pow_q30(value: i64, exponent_q20: i64) i64 { + if (value <= 0) { return 0; } + if (value >= png_q30_one()) { return png_q30_one(); } + return png_exp2_q30((png_log2_q20(value) * exponent_q20) / + png_q20_one()); +} + +fn png_linear_q30_to_srgb(linear_input: i64) u8 { + let linear: i64 = linear_input; + if (linear <= 0) { return 0; } + if (linear >= png_q30_one()) { return 255; } + let srgb: i64 = 0; + if (linear <= 3361671) { + srgb = (linear * 1292 + 50) / 100; + } else { + let encoded_root: i64 = png_pow_q30(linear, 436907); + srgb = png_mul_q30(encoded_root, 1132797624) - 59055800; + } + if (srgb <= 0) { return 0; } + if (srgb >= png_q30_one()) { return 255; } + return ((srgb * 255 + png_q30_one() / 2) / png_q30_one()) as u8; +} + +fn png_srgb_sample_to_linear_q30(sample: u8) i64 { + if (sample == 0) { return 0; } + if (sample == 255) { return png_q30_one(); } + let encoded: i64 = ((sample as i64) * png_q30_one() + 127) / 255; + if (encoded <= 43432857) { + return (encoded * 100 + 646) / 1292; + } + let adjusted: i64 = ((encoded + 59055800) * png_q30_one() + + 1132797624 / 2) / 1132797624; + return png_pow_q30(adjusted, 2516582); +} + +fn png_source_sample_to_linear_q30(sample: u8, gamma: i64) i64 { + if (sample == 0) { return 0; } + if (sample == 255) { return png_q30_one(); } + if (gamma == 0) { return png_srgb_sample_to_linear_q30(sample); } + let encoded: i64 = ((sample as i64) * png_q30_one() + 127) / 255; + let decode_exponent: i64 = (100000 * png_q20_one() + gamma / 2) / gamma; + return png_pow_q30(encoded, decode_exponent); +} + +fn png_q30_multiply(left: i64, right: i64) i64 { + return (left * right) / png_q30_one(); +} + +fn png_transfer_sample_to_linear_q30(sample: u8, gamma: i64, + curve: PngTransferCurve) i64 { + if (curve.kind == 0) { + return png_source_sample_to_linear_q30(sample, gamma); + } + if (curve.kind == 6 && curve.samples.len == 256) { + return curve.samples.data[sample as i32]; + } + let x: i64 = ((sample as i64) * png_q30_one() + 127) / 255; + let affine: i64 = png_q30_multiply(curve.a, x) + curve.b; + let value: i64 = 0; + if (curve.kind == 1) { + value = png_pow_q30(x, curve.g); + } else if (curve.kind == 2) { + if (affine >= 0) { value = png_pow_q30(affine, curve.g); } + } else if (curve.kind == 3) { + if (affine >= 0) { value = png_pow_q30(affine, curve.g) + curve.c; } + else { value = curve.c; } + } else if (curve.kind == 4) { + if (x >= curve.d) { value = png_pow_q30(affine, curve.g); } + else { value = png_q30_multiply(curve.c, x); } + } else if (curve.kind == 5) { + if (x >= curve.d) { + value = png_pow_q30(affine, curve.g) + curve.e; + } else { value = png_q30_multiply(curve.c, x) + curve.f; } + } + if (value < 0) { return 0; } + if (value > png_q30_one()) { return png_q30_one(); } + return value; +} + +fn png_gamma_sample_to_srgb(sample: u8, gamma: i64) u8 { + if (sample == 0 || gamma <= 0) { return 0; } + if (sample == 255) { return 255; } + return png_linear_q30_to_srgb( + png_source_sample_to_linear_q30(sample, gamma) + ); +} + +fn png_transform_linear_channel(a: i64, b: i64, c: i64, + red: i64, green: i64, blue: i64) i64 { + return (a * red + b * green + c * blue) / 16777216; +} + +fn png_icc_lerp_u16(lower: i64, upper: i64, remainder: i64, + denominator: i64) i64 { + return (lower * (denominator - remainder) + upper * remainder + + denominator / 2) / denominator; +} + +fn png_icc_table_sample(table: ArrayList[i32], channel: i32, + entries: i32, value: i64) i64 { + let scaled: i64 = value * ((entries - 1) as i64); + let lower: i32 = (scaled / 65535) as i32; + let remainder: i64 = scaled % 65535; + let upper: i32 = lower + (lower + 1 < entries) as i32; + let offset: i32 = channel * entries; + return png_icc_lerp_u16(table.data[offset + lower] as i64, + table.data[offset + upper] as i64, remainder, 65535); +} + +fn png_icc_clut_value(lut: PngIccLut, red: i32, green: i32, + blue: i32, channel: i32) i64 { + let index: i32 = ((red * lut.green_grid_points + green) * + lut.blue_grid_points + blue) * 3 + channel; + return lut.clut.data[index] as i64; +} + +fn png_icc_clut_channel(lut: PngIccLut, red: i64, green: i64, + blue: i64, channel: i32) i64 { + let red_scaled: i64 = red * ((lut.red_grid_points - 1) as i64); + let green_scaled: i64 = green * ((lut.green_grid_points - 1) as i64); + let blue_scaled: i64 = blue * ((lut.blue_grid_points - 1) as i64); + let red_lower: i32 = (red_scaled / 65535) as i32; + let green_lower: i32 = (green_scaled / 65535) as i32; + let blue_lower: i32 = (blue_scaled / 65535) as i32; + let red_upper: i32 = red_lower + + (red_lower + 1 < lut.red_grid_points) as i32; + let green_upper: i32 = green_lower + + (green_lower + 1 < lut.green_grid_points) as i32; + let blue_upper: i32 = blue_lower + + (blue_lower + 1 < lut.blue_grid_points) as i32; + let blue00: i64 = png_icc_lerp_u16( + png_icc_clut_value(lut, red_lower, green_lower, blue_lower, channel), + png_icc_clut_value(lut, red_lower, green_lower, blue_upper, channel), + blue_scaled % 65535, 65535 + ); + let blue01: i64 = png_icc_lerp_u16( + png_icc_clut_value(lut, red_lower, green_upper, blue_lower, channel), + png_icc_clut_value(lut, red_lower, green_upper, blue_upper, channel), + blue_scaled % 65535, 65535 + ); + let blue10: i64 = png_icc_lerp_u16( + png_icc_clut_value(lut, red_upper, green_lower, blue_lower, channel), + png_icc_clut_value(lut, red_upper, green_lower, blue_upper, channel), + blue_scaled % 65535, 65535 + ); + let blue11: i64 = png_icc_lerp_u16( + png_icc_clut_value(lut, red_upper, green_upper, blue_lower, channel), + png_icc_clut_value(lut, red_upper, green_upper, blue_upper, channel), + blue_scaled % 65535, 65535 + ); + let green0: i64 = png_icc_lerp_u16( + blue00, blue01, green_scaled % 65535, 65535 + ); + let green1: i64 = png_icc_lerp_u16( + blue10, blue11, green_scaled % 65535, 65535 + ); + return png_icc_lerp_u16( + green0, green1, red_scaled % 65535, 65535 + ); +} + +fn png_icc_lut_sample(lut: PngIccLut, red: u8, green: u8, + blue: u8) PngIccLutSample { + let input_red: i64 = png_icc_table_sample( + lut.input_tables, 0, lut.input_entries, (red as i64) * 257 + ); + let input_green: i64 = png_icc_table_sample( + lut.input_tables, 1, lut.input_entries, (green as i64) * 257 + ); + let input_blue: i64 = png_icc_table_sample( + lut.input_tables, 2, lut.input_entries, (blue as i64) * 257 + ); + let pcs_x: i64 = png_icc_table_sample( + lut.output_tables, 0, lut.output_entries, + png_icc_clut_channel(lut, input_red, input_green, input_blue, 0) + ); + let pcs_y: i64 = png_icc_table_sample( + lut.output_tables, 1, lut.output_entries, + png_icc_clut_channel(lut, input_red, input_green, input_blue, 1) + ); + let pcs_z: i64 = png_icc_table_sample( + lut.output_tables, 2, lut.output_entries, + png_icc_clut_channel(lut, input_red, input_green, input_blue, 2) + ); + return PngIccLutSample{ + .x = (pcs_x * 1073741824 + 16384) / 32768, + .y = (pcs_y * 1073741824 + 16384) / 32768, + .z = (pcs_z * 1073741824 + 16384) / 32768, + }; +} + +fn png_expand_rgba(raw: []u8, width: i32, height: i32, row_bytes: i32, + bit_depth: i32, color_type: i32, palette: ArrayList[u8], + transparency: ArrayList[u8], source_gamma: i64, + color_transform: PngColorTransform, + red_curve: PngTransferCurve, green_curve: PngTransferCurve, + blue_curve: PngTransferCurve, lut: PngIccLut, + profile_assumed: i32) PngDecodeResult { + let pixels: ArrayList[u8] = make[u8](width * height * 4); + let gamma_table: ArrayList[u8] = make[u8](256); + let red_linear: ArrayList[i64] = make[i64](256); + let green_linear: ArrayList[i64] = make[i64](256); + let blue_linear: ArrayList[i64] = make[i64](256); + if (color_transform.active != 0 && lut.valid == 0) { + let linear_sample: i32 = 0; + while (linear_sample < 256) { + push[i64](&red_linear, png_transfer_sample_to_linear_q30( + linear_sample as u8, source_gamma, red_curve + )); + push[i64](&green_linear, png_transfer_sample_to_linear_q30( + linear_sample as u8, source_gamma, green_curve + )); + push[i64](&blue_linear, png_transfer_sample_to_linear_q30( + linear_sample as u8, source_gamma, blue_curve + )); + linear_sample = linear_sample + 1; + } + } else if (source_gamma != 0) { + let gamma_sample: i32 = 0; + while (gamma_sample < 256) { + push[u8](&gamma_table, + png_gamma_sample_to_srgb(gamma_sample as u8, source_gamma)); + gamma_sample = gamma_sample + 1; + } + } + let transparent_gray: i32 = 0 - 1; + let transparent_red: i32 = 0 - 1; + let transparent_green: i32 = 0 - 1; + let transparent_blue: i32 = 0 - 1; + if (color_type == 0 && transparency.len == 2) { + transparent_gray = png_read_u16_be( + transparency.data[0..transparency.len], 0 + ); + } + if (color_type == 2 && transparency.len == 6) { + transparent_red = png_read_u16_be(transparency.data[0..transparency.len], 0); + transparent_green = png_read_u16_be(transparency.data[0..transparency.len], 2); + transparent_blue = png_read_u16_be(transparency.data[0..transparency.len], 4); + } + let y: i32 = 0; + while (y < height) { + let row: []u8 = raw[y * row_bytes..(y + 1) * row_bytes]; + let x: i32 = 0; + while (x < width) { + let red: u8 = 0; + let green: u8 = 0; + let blue: u8 = 0; + let alpha: u8 = 255; + if (color_type == 0) { + let gray: i32 = png_sample(row, x, bit_depth); + red = png_sample_u8(gray, bit_depth); + green = red; blue = red; + if (transparent_gray >= 0 && gray == transparent_gray) { alpha = 0; } + } else if (color_type == 2) { + let red_sample: i32 = png_sample(row, x * 3, bit_depth); + let green_sample: i32 = png_sample(row, x * 3 + 1, bit_depth); + let blue_sample: i32 = png_sample(row, x * 3 + 2, bit_depth); + red = png_sample_u8(red_sample, bit_depth); + green = png_sample_u8(green_sample, bit_depth); + blue = png_sample_u8(blue_sample, bit_depth); + if (red_sample == transparent_red && green_sample == transparent_green && + blue_sample == transparent_blue) { alpha = 0; } + } else if (color_type == 3) { + let index: i32 = png_sample(row, x, bit_depth); + if (index * 3 + 2 >= palette.len) { + free[i64](&red_linear); free[i64](&green_linear); + free[i64](&blue_linear); free[u8](&gamma_table); + free[u8](&pixels); + return png_decode_fail(PngDecodeError.palette_index, + (y * width + x) as i64); + } + red = palette.data[index * 3]; + green = palette.data[index * 3 + 1]; + blue = palette.data[index * 3 + 2]; + if (index < transparency.len) { alpha = transparency.data[index]; } + } else if (color_type == 4) { + let gray: i32 = png_sample(row, x * 2, bit_depth); + red = png_sample_u8(gray, bit_depth); + green = red; blue = red; + alpha = png_sample_u8(png_sample(row, x * 2 + 1, bit_depth), bit_depth); + } else { + red = png_sample_u8(png_sample(row, x * 4, bit_depth), bit_depth); + green = png_sample_u8(png_sample(row, x * 4 + 1, bit_depth), bit_depth); + blue = png_sample_u8(png_sample(row, x * 4 + 2, bit_depth), bit_depth); + alpha = png_sample_u8(png_sample(row, x * 4 + 3, bit_depth), bit_depth); + } + if (lut.valid != 0) { + let pcs: PngIccLutSample = png_icc_lut_sample( + lut, red, green, blue + ); + red = png_linear_q30_to_srgb(png_transform_linear_channel( + color_transform.m00, color_transform.m01, color_transform.m02, + pcs.x, pcs.y, pcs.z + )); + green = png_linear_q30_to_srgb(png_transform_linear_channel( + color_transform.m10, color_transform.m11, color_transform.m12, + pcs.x, pcs.y, pcs.z + )); + blue = png_linear_q30_to_srgb(png_transform_linear_channel( + color_transform.m20, color_transform.m21, color_transform.m22, + pcs.x, pcs.y, pcs.z + )); + } else if (red_linear.len == 256) { + let linear_red: i64 = red_linear.data[red as i32]; + let linear_green: i64 = green_linear.data[green as i32]; + let linear_blue: i64 = blue_linear.data[blue as i32]; + red = png_linear_q30_to_srgb(png_transform_linear_channel( + color_transform.m00, color_transform.m01, color_transform.m02, + linear_red, linear_green, linear_blue + )); + green = png_linear_q30_to_srgb(png_transform_linear_channel( + color_transform.m10, color_transform.m11, color_transform.m12, + linear_red, linear_green, linear_blue + )); + blue = png_linear_q30_to_srgb(png_transform_linear_channel( + color_transform.m20, color_transform.m21, color_transform.m22, + linear_red, linear_green, linear_blue + )); + } else if (gamma_table.len == 256) { + red = gamma_table.data[red as i32]; + green = gamma_table.data[green as i32]; + blue = gamma_table.data[blue as i32]; + } + push[u8](&pixels, red); push[u8](&pixels, green); + push[u8](&pixels, blue); push[u8](&pixels, alpha); + x = x + 1; + } + y = y + 1; + } + free[i64](&red_linear); free[i64](&green_linear); + free[i64](&blue_linear); + free[u8](&gamma_table); + return PngDecodeResult{ + .pixels = pixels, .width = width, .height = height, + .color_space = RenderColorSpace.srgb, + .profile_assumed = profile_assumed, + .error = PngDecodeError.none, .error_offset = 0 - 1, + }; +} + +fn png_decode(bytes: []u8) PngDecodeResult { + if (bytes.len as i64 > png_decode_input_limit()) { + return png_decode_fail(PngDecodeError.input_limit, bytes.len as i64); + } + if (bytes.len < 8) { return png_decode_fail(PngDecodeError.truncated, bytes.len as i64); } + if (png_signature_valid(bytes) == 0) { + return png_decode_fail(PngDecodeError.invalid_signature, 0); + } + + let idat: ArrayList[u8] = make[u8](256); + let palette: ArrayList[u8] = make[u8](256); + let transparency: ArrayList[u8] = make[u8](256); + let width: i32 = 0; + let height: i32 = 0; + let bit_depth: i32 = 0; + let color_type: i32 = 0; + let interlace: i32 = 0; + let saw_header: i32 = 0; + let saw_palette: i32 = 0; + let saw_transparency: i32 = 0; + let saw_idat: i32 = 0; + let idat_ended: i32 = 0; + let saw_end: i32 = 0; + let saw_srgb: i32 = 0; + let saw_gamma: i32 = 0; + let saw_chrm: i32 = 0; + let saw_icc: i32 = 0; + let source_gamma: i64 = 0; + let color_transform: PngColorTransform = png_color_transform_identity(); + let icc_data_offset: i32 = 0; + let icc_data_length: i32 = 0; + let icc_chunk_offset: i32 = 0; + let offset: i32 = 8; + + while (offset < bytes.len && saw_end == 0) { + if (offset + 12 > bytes.len) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.truncated, offset as i64); + } + let length64: i64 = png_read_u32_be(bytes, offset); + if (length64 > png_decode_input_limit() || + length64 > (bytes.len - offset - 12) as i64) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_length, offset as i64); + } + let length: i32 = length64 as i32; + let data_offset: i32 = offset + 8; + let crc_offset: i32 = data_offset + length; + if (png_chunk_type_valid(bytes, offset) == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_type, offset as i64); + } + let actual_crc: i64 = checksum_crc32(bytes[offset + 4..crc_offset]); + if (actual_crc != png_read_u32_be(bytes, crc_offset)) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_crc, offset as i64); + } + + if (png_chunk_is(bytes, offset, "IHDR") != 0) { + if (saw_header != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.duplicate_chunk, offset as i64); + } + if (offset != 8 || length != 13) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + let width64: i64 = png_read_u32_be(bytes, data_offset); + let height64: i64 = png_read_u32_be(bytes, data_offset + 4); + if (width64 <= 0 || height64 <= 0 || + width64 > png_decode_dimension_limit() as i64 || + height64 > png_decode_dimension_limit() as i64) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.pixel_limit, data_offset as i64); + } + width = width64 as i32; height = height64 as i32; + bit_depth = bytes[data_offset + 8] as i32; + color_type = bytes[data_offset + 9] as i32; + if (png_color_channels(color_type) == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_type, (data_offset + 9) as i64); + } + if (png_bit_depth_valid(color_type, bit_depth) == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_bit_depth, (data_offset + 8) as i64); + } + if (bytes[data_offset + 10] != 0 || bytes[data_offset + 11] != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_header, (data_offset + 10) as i64); + } + interlace = bytes[data_offset + 12] as i32; + if (interlace > 1) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_interlace, (data_offset + 12) as i64); + } + saw_header = 1; + } else if (saw_header == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } else if (png_chunk_is(bytes, offset, "PLTE") != 0) { + if (saw_palette != 0 || saw_idat != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + if (length == 0 || length > 768 || length % 3 != 0 || + color_type == 0 || color_type == 4) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_palette, data_offset as i64); + } + png_copy_append(&palette, bytes[data_offset..data_offset + length]); + saw_palette = 1; + } else if (png_chunk_is(bytes, offset, "tRNS") != 0) { + if (saw_transparency != 0 || saw_idat != 0 || color_type == 4 || + color_type == 6 || (color_type == 0 && length != 2) || + (color_type == 2 && length != 6) || + (color_type == 3 && (saw_palette == 0 || length == 0 || + length > palette.len / 3))) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_transparency, data_offset as i64); + } + if (color_type == 0 && bit_depth < 16 && + png_read_u16_be(bytes, data_offset) >= (1 << bit_depth)) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_transparency, data_offset as i64); + } + if (color_type == 2 && bit_depth == 8 && + (png_read_u16_be(bytes, data_offset) > 255 || + png_read_u16_be(bytes, data_offset + 2) > 255 || + png_read_u16_be(bytes, data_offset + 4) > 255)) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_transparency, data_offset as i64); + } + png_copy_append(&transparency, bytes[data_offset..data_offset + length]); + saw_transparency = 1; + } else if (png_chunk_is(bytes, offset, "IDAT") != 0) { + if (idat_ended != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + if (idat.len as i64 + length as i64 > png_decode_input_limit()) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.input_limit, data_offset as i64); + } + png_copy_append(&idat, bytes[data_offset..data_offset + length]); + saw_idat = 1; + } else if (png_chunk_is(bytes, offset, "IEND") != 0) { + if (length != 0 || saw_idat == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + saw_end = 1; + } else if (png_chunk_is(bytes, offset, "sRGB") != 0) { + if (saw_srgb != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.duplicate_chunk, offset as i64); + } + if (saw_palette != 0 || saw_idat != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + if (length != 1 || bytes[data_offset] > 3) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_profile, offset as i64); + } + saw_srgb = 1; + } else if (png_chunk_is(bytes, offset, "gAMA") != 0) { + if (saw_gamma != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.duplicate_chunk, offset as i64); + } + if (saw_palette != 0 || saw_idat != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + if (length != 4) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_profile, offset as i64); + } + source_gamma = png_read_u32_be(bytes, data_offset); + saw_gamma = 1; + } else if (png_chunk_is(bytes, offset, "cHRM") != 0) { + if (saw_chrm != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.duplicate_chunk, offset as i64); + } + if (saw_palette != 0 || saw_idat != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + color_transform = png_chrm_transform(bytes, data_offset, length); + if (color_transform.valid == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_profile, offset as i64); + } + saw_chrm = 1; + } else if (png_chunk_is(bytes, offset, "iCCP") != 0) { + if (saw_icc != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.duplicate_chunk, offset as i64); + } + if (saw_palette != 0 || saw_idat != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_chunk_order, offset as i64); + } + if (!(color_type == 2 || color_type == 3 || color_type == 6)) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_profile, offset as i64); + } + icc_data_offset = data_offset; + icc_data_length = length; + icc_chunk_offset = offset; + saw_icc = 1; + } else if (png_chunk_is(bytes, offset, "cICP") != 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_profile, offset as i64); + } else { + if ((bytes[offset + 4] & 32) == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unknown_critical_chunk, offset as i64); + } + } + if (saw_idat != 0 && png_chunk_is(bytes, offset, "IDAT") == 0 && + png_chunk_is(bytes, offset, "IEND") == 0) { idat_ended = 1; } + offset = crc_offset + 4; + } + + if (saw_end == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.truncated, offset as i64); + } + if (offset != bytes.len) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.trailing_data, offset as i64); + } + if (color_type == 3 && saw_palette == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.missing_palette, 8); + } + if (color_type == 3 && palette.len / 3 > (1 << bit_depth)) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.invalid_palette, 8); + } + if (saw_idat == 0 || idat.len == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.missing_image_data, 8); + } + + let channels: i32 = png_color_channels(color_type); + let row_bits: i64 = (width as i64) * (channels as i64) * (bit_depth as i64); + let row_bytes64: i64 = (row_bits + 7) / 8; + let expected64: i64 = (row_bytes64 + 1) * (height as i64); + if (interlace != 0) { + expected64 = png_adam7_expected_size(width, height, channels, bit_depth); + } + let pixel_bytes64: i64 = (width as i64) * (height as i64) * 4; + if (row_bytes64 <= 0 || expected64 <= 0 || + pixel_bytes64 > png_decode_pixel_byte_limit() || expected64 > 2147483647) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.pixel_limit, 8); + } + let icc_profile: PngIccProfileResult = PngIccProfileResult{ + .transform = png_color_transform_identity(), + .red_curve = png_transfer_curve_invalid(), + .green_curve = png_transfer_curve_invalid(), + .blue_curve = png_transfer_curve_invalid(), + .lut = png_icc_lut_invalid(), .valid = 0, + }; + if (saw_icc != 0) { + icc_profile = png_iccp_profile(bytes, icc_data_offset, icc_data_length); + if (icc_profile.valid == 0) { + return png_decode_parse_fail(&idat, &palette, &transparency, + PngDecodeError.unsupported_color_profile, icc_chunk_offset as i64); + } + } + let inflated: ZlibInflateResult = zlib_inflate( + idat.data[0..idat.len], expected64 + ); + free[u8](&idat); + if (inflated.error != ZlibInflateError.none) { + png_icc_profile_free(&icc_profile); + zlib_inflate_free(&inflated); free[u8](&palette); free[u8](&transparency); + return png_decode_fail(PngDecodeError.compression_error, 8); + } + if (inflated.bytes.len as i64 != expected64) { + png_icc_profile_free(&icc_profile); + zlib_inflate_free(&inflated); free[u8](&palette); free[u8](&transparency); + return png_decode_fail(PngDecodeError.decompressed_size, inflated.bytes.len as i64); + } + let scanlines: PngScanlineResult = png_reconstruct_scanlines( + inflated.bytes.data[0..inflated.bytes.len], width, height, + row_bytes64 as i32, channels, bit_depth, interlace + ); + zlib_inflate_free(&inflated); + if (scanlines.error != PngDecodeError.none) { + let error: PngDecodeError = scanlines.error; + let error_offset: i64 = scanlines.error_offset; + png_icc_profile_free(&icc_profile); + free[u8](&scanlines.bytes); free[u8](&palette); free[u8](&transparency); + return png_decode_fail(error, error_offset); + } + let conversion_gamma: i64 = source_gamma; + let effective_transform: PngColorTransform = color_transform; + let effective_red_curve: PngTransferCurve = png_transfer_curve_external(); + let effective_green_curve: PngTransferCurve = png_transfer_curve_external(); + let effective_blue_curve: PngTransferCurve = png_transfer_curve_external(); + if (saw_srgb != 0) { + conversion_gamma = 0; + effective_transform = png_color_transform_identity(); + } + if (saw_icc != 0) { + conversion_gamma = 0; + effective_transform = icc_profile.transform; + effective_red_curve = icc_profile.red_curve; + effective_green_curve = icc_profile.green_curve; + effective_blue_curve = icc_profile.blue_curve; + } + let result: PngDecodeResult = png_expand_rgba( + scanlines.bytes.data[0..scanlines.bytes.len], width, height, + row_bytes64 as i32, bit_depth, color_type, palette, transparency, + conversion_gamma, effective_transform, effective_red_curve, + effective_green_curve, effective_blue_curve, icc_profile.lut, + (saw_srgb == 0 && saw_icc == 0 && + !(source_gamma != 0 && saw_chrm != 0)) as i32 + ); + png_icc_profile_free(&icc_profile); + free[u8](&scanlines.bytes); free[u8](&palette); free[u8](&transparency); + return result; +} + +fn png_decode_resource_spec(decoded: PngDecodeResult, id: i64) RenderResourceSpec { + let spec: RenderResourceSpec = render_resource_spec( + id, RenderResourceKind.image, + decoded.pixels.data[0..decoded.pixels.len], image_resource_format_tag() + ); + spec.width = decoded.width as i64; + spec.height = decoded.height as i64; + spec.color_space = decoded.color_space; + return spec; +} diff --git a/src/render/png_encode.zag b/src/render/png_encode.zag new file mode 100644 index 0000000..91ff9ed --- /dev/null +++ b/src/render/png_encode.zag @@ -0,0 +1,115 @@ +@import("std:list") +@import("std:checksum") +@import("std:zlib_store") +@import("cpu_raster.zag") + +enum PngEncodeError { + none, + invalid_surface, + input_limit, + compression_error, +} + +struct PngEncodeResult { + bytes: ArrayList[u8], + error: PngEncodeError, +} + +fn png_write_u32_be(bytes: *ArrayList[u8], value: i64) void { + push[u8](bytes, ((value >> 24) & 255) as u8); + push[u8](bytes, ((value >> 16) & 255) as u8); + push[u8](bytes, ((value >> 8) & 255) as u8); + push[u8](bytes, (value & 255) as u8); +} + +fn png_write_chunk(output: *ArrayList[u8], chunk_type: []u8, data: []u8) void { + png_write_u32_be(output, data.len as i64); + let crc_input: ArrayList[u8] = make[u8](4 + data.len); + let i: i32 = 0; + while (i < 4) { + push[u8](output, chunk_type[i]); + push[u8](&crc_input, chunk_type[i]); + i = i + 1; + } + i = 0; + while (i < data.len) { + push[u8](output, data[i]); + push[u8](&crc_input, data[i]); + i = i + 1; + } + png_write_u32_be(output, + checksum_crc32(crc_input.data[0..crc_input.len])); + free[u8](&crc_input); +} + +fn png_encode_fail(error: PngEncodeError) PngEncodeResult { + return PngEncodeResult{ .bytes = make[u8](1), .error = error }; +} + +fn png_encode_surface(surface: CpuSurface) PngEncodeResult { + if (surface.width <= 0 || surface.height <= 0 || + surface.width > 8192 || surface.height > 8192) { + return png_encode_fail(PngEncodeError.invalid_surface); + } + let pixel_bytes: i64 = (surface.width as i64) * + (surface.height as i64) * 4; + if (pixel_bytes <= 0 || pixel_bytes != surface.pixels.len as i64) { + return png_encode_fail(PngEncodeError.invalid_surface); + } + let scanline_bytes: i64 = pixel_bytes + surface.height as i64; + if (scanline_bytes <= 0 || scanline_bytes > zlib_store_input_limit()) { + return png_encode_fail(PngEncodeError.input_limit); + } + let scanlines: ArrayList[u8] = make[u8](scanline_bytes as i32); + let y: i32 = 0; + while (y < surface.height) { + push[u8](&scanlines, 0); + let row_start: i32 = y * surface.width * 4; + let i: i32 = 0; + while (i < surface.width * 4) { + push[u8](&scanlines, surface.pixels.data[row_start + i]); + i = i + 1; + } + y = y + 1; + } + let compressed: ZlibStoreEncodeResult = zlib_store_encode( + scanlines.data[0..scanlines.len] + ); + free[u8](&scanlines); + if (compressed.error != ZlibStoreError.none) { + zlib_store_encode_free(&compressed); + return png_encode_fail(PngEncodeError.compression_error); + } + + let output: ArrayList[u8] = make[u8](compressed.bytes.len + 64); + push[u8](&output, 137); push[u8](&output, 80); + push[u8](&output, 78); push[u8](&output, 71); + push[u8](&output, 13); push[u8](&output, 10); + push[u8](&output, 26); push[u8](&output, 10); + + let header: ArrayList[u8] = make[u8](13); + png_write_u32_be(&header, surface.width as i64); + png_write_u32_be(&header, surface.height as i64); + push[u8](&header, 8); + push[u8](&header, 6); + push[u8](&header, 0); + push[u8](&header, 0); + push[u8](&header, 0); + png_write_chunk(&output, "IHDR", header.data[0..header.len]); + free[u8](&header); + + let srgb: ArrayList[u8] = make[u8](1); + push[u8](&srgb, 0); + png_write_chunk(&output, "sRGB", srgb.data[0..srgb.len]); + free[u8](&srgb); + + png_write_chunk(&output, "IDAT", + compressed.bytes.data[0..compressed.bytes.len]); + zlib_store_encode_free(&compressed); + png_write_chunk(&output, "IEND", ""); + return PngEncodeResult{ .bytes = output, .error = PngEncodeError.none }; +} + +fn png_encode_free(result: *PngEncodeResult) void { + free[u8](&result.*.bytes); +} diff --git a/src/render/resources.zag b/src/render/resources.zag new file mode 100644 index 0000000..cdb64ab --- /dev/null +++ b/src/render/resources.zag @@ -0,0 +1,363 @@ +@import("std:list") + +enum RenderResourceKind { + path, + image, + glyph_run, + effect, + font_data, + svg_source, + png_source, +} + +enum RenderColorSpace { + none, + srgb, + display_p3, + linear_srgb, +} + +enum RenderResourceError { + none, + invalid_configuration, + invalid_id, + invalid_payload, + invalid_metadata, + duplicate_id, + missing_resource, + kind_mismatch, + revision_conflict, + too_many_resources, + payload_limit, + total_limit, + sealed, + not_sealed, + content_mutated, +} + +struct RenderResourceSpec { + id: i64, + kind: RenderResourceKind, + payload: []u8, + revision: i64, + format_tag: i64, + width: i64, + height: i64, + color_space: RenderColorSpace, +} + +struct RenderResource { + id: i64, + kind: RenderResourceKind, + payload: ArrayList[u8], + revision: i64, + format_tag: i64, + width: i64, + height: i64, + color_space: RenderColorSpace, + content_hash: i64, +} + +struct RenderResourceStore { + resources: ArrayList[RenderResource], + max_resources: i32, + max_total_bytes: i64, + max_payload_bytes: i64, + total_bytes: i64, + revision: i64, + content_hash: i64, + sealed: i32, + last_error: RenderResourceError, + error_id: i64, +} + +fn render_resource_hard_count_limit() i32 { return 65536; } +fn render_resource_hard_total_limit() i64 { return 536870912; } +fn render_resource_hard_payload_limit() i64 { return 67108864; } + +fn render_resource_mix(hash: i64, value: i64) i64 { + let modulus: i64 = 2147483647; + let component: i64 = value % modulus; + if (component < 0) { component = component + modulus; } + return (hash * 131 + component) % modulus; +} + +fn render_resource_hash_bytes(hash: i64, payload: ArrayList[u8]) i64 { + let result: i64 = render_resource_mix(hash, payload.len as i64); + let i: i32 = 0; + while (i < payload.len) { + result = render_resource_mix(result, payload.data[i] as i64); + i = i + 1; + } + return result; +} + +fn render_resource_hash(resource: RenderResource) i64 { + let hash: i64 = render_resource_mix(17, resource.id); + hash = render_resource_mix(hash, resource.kind as i64); + hash = render_resource_mix(hash, resource.revision); + hash = render_resource_mix(hash, resource.format_tag); + hash = render_resource_mix(hash, resource.width); + hash = render_resource_mix(hash, resource.height); + hash = render_resource_mix(hash, resource.color_space as i64); + return render_resource_hash_bytes(hash, resource.payload); +} + +fn render_resource_store_hash(store: RenderResourceStore) i64 { + let hash: i64 = render_resource_mix(17, store.resources.len as i64); + hash = render_resource_mix(hash, store.max_resources as i64); + hash = render_resource_mix(hash, store.max_total_bytes); + hash = render_resource_mix(hash, store.max_payload_bytes); + hash = render_resource_mix(hash, store.total_bytes); + hash = render_resource_mix(hash, store.revision); + let i: i32 = 0; + while (i < store.resources.len) { + hash = render_resource_mix(hash, store.resources.data[i].content_hash); + i = i + 1; + } + return hash; +} + +fn render_resource_store_make(max_resources: i32, max_total_bytes: i64, + max_payload_bytes: i64) RenderResourceStore { + let store: RenderResourceStore = RenderResourceStore{ + .resources = make[RenderResource](8), + .max_resources = max_resources, + .max_total_bytes = max_total_bytes, + .max_payload_bytes = max_payload_bytes, + .total_bytes = 0, + .revision = 0, + .content_hash = 0, + .sealed = 0, + .last_error = RenderResourceError.none, + .error_id = 0, + }; + store.content_hash = render_resource_store_hash(store); + return store; +} + +fn render_resource_store_default() RenderResourceStore { + return render_resource_store_make(4096, 268435456, 33554432); +} + +fn render_resource_store_configuration_valid(store: RenderResourceStore) i32 { + return (store.max_resources > 0 && + store.max_resources <= render_resource_hard_count_limit() && + store.max_total_bytes > 0 && + store.max_total_bytes <= render_resource_hard_total_limit() && + store.max_payload_bytes > 0 && + store.max_payload_bytes <= render_resource_hard_payload_limit() && + store.max_payload_bytes <= store.max_total_bytes) as i32; +} + +fn render_resource_spec(id: i64, kind: RenderResourceKind, + payload: []u8, format_tag: i64) RenderResourceSpec { + return RenderResourceSpec{ + .id = id, .kind = kind, .payload = payload, .revision = 0, + .format_tag = format_tag, .width = 0, .height = 0, + .color_space = RenderColorSpace.none, + }; +} + +fn render_resource_fail(store: *RenderResourceStore, + error: RenderResourceError, id: i64) RenderResourceError { + store.*.last_error = error; + store.*.error_id = id; + return error; +} + +fn render_resource_spec_valid(spec: RenderResourceSpec) RenderResourceError { + if (spec.id <= 0) { return RenderResourceError.invalid_id; } + if (spec.payload.len <= 0) { return RenderResourceError.invalid_payload; } + if (spec.revision < 0 || spec.revision > 2147483647 || + spec.format_tag <= 0 || spec.width < 0 || + spec.height < 0 || spec.width > 1048576 || spec.height > 1048576) { + return RenderResourceError.invalid_metadata; + } + if (spec.kind == RenderResourceKind.image && + (spec.width <= 0 || spec.height <= 0 || + spec.color_space == RenderColorSpace.none)) { + return RenderResourceError.invalid_metadata; + } + return RenderResourceError.none; +} + +fn render_resource_payload_copy(payload: []u8) ArrayList[u8] { + let owned: ArrayList[u8] = make[u8](payload.len); + let i: i32 = 0; + while (i < payload.len) { + push[u8](&owned, payload[i]); + i = i + 1; + } + return owned; +} + +fn render_resource_from_spec(spec: RenderResourceSpec) RenderResource { + let resource: RenderResource = RenderResource{ + .id = spec.id, .kind = spec.kind, + .payload = render_resource_payload_copy(spec.payload), + .revision = spec.revision, .format_tag = spec.format_tag, + .width = spec.width, .height = spec.height, + .color_space = spec.color_space, .content_hash = 0, + }; + resource.content_hash = render_resource_hash(resource); + return resource; +} + +fn render_resource_find_index(store: RenderResourceStore, id: i64) i32 { + let low: i32 = 0; + let high: i32 = store.resources.len - 1; + while (low <= high) { + let middle: i32 = low + (high - low) / 2; + let candidate: i64 = store.resources.data[middle].id; + if (candidate == id) { return middle; } + if (candidate < id) { low = middle + 1; } else { high = middle - 1; } + } + return 0 - 1; +} + +fn render_resource_insert_index(store: RenderResourceStore, id: i64) i32 { + let i: i32 = 0; + while (i < store.resources.len && store.resources.data[i].id < id) { i = i + 1; } + return i; +} + +fn render_resource_store_rehash(store: *RenderResourceStore) void { + store.*.content_hash = render_resource_store_hash(store.*); +} + +fn render_resource_add(store: *RenderResourceStore, + spec: RenderResourceSpec) RenderResourceError { + if (store.*.sealed != 0) { + return render_resource_fail(store, RenderResourceError.sealed, spec.id); + } + if (render_resource_store_configuration_valid(store.*) == 0) { + return render_resource_fail(store, RenderResourceError.invalid_configuration, spec.id); + } + let valid: RenderResourceError = render_resource_spec_valid(spec); + if (valid != RenderResourceError.none) { return render_resource_fail(store, valid, spec.id); } + if (spec.revision != 0) { + return render_resource_fail(store, RenderResourceError.revision_conflict, spec.id); + } + if (render_resource_find_index(store.*, spec.id) >= 0) { + return render_resource_fail(store, RenderResourceError.duplicate_id, spec.id); + } + if (store.*.resources.len >= store.*.max_resources) { + return render_resource_fail(store, RenderResourceError.too_many_resources, spec.id); + } + if (spec.payload.len as i64 > store.*.max_payload_bytes) { + return render_resource_fail(store, RenderResourceError.payload_limit, spec.id); + } + if (spec.payload.len as i64 > store.*.max_total_bytes - store.*.total_bytes) { + return render_resource_fail(store, RenderResourceError.total_limit, spec.id); + } + + let resource: RenderResource = render_resource_from_spec(spec); + let index: i32 = render_resource_insert_index(store.*, spec.id); + push[RenderResource](&store.*.resources, resource); + let i: i32 = store.*.resources.len - 1; + while (i > index) { + store.*.resources.data[i] = store.*.resources.data[i - 1]; + i = i - 1; + } + store.*.resources.data[index] = resource; + store.*.total_bytes = store.*.total_bytes + spec.payload.len as i64; + store.*.revision = store.*.revision + 1; + render_resource_store_rehash(store); + store.*.last_error = RenderResourceError.none; + store.*.error_id = 0; + return RenderResourceError.none; +} + +fn render_resource_replace(store: *RenderResourceStore, + spec: RenderResourceSpec) RenderResourceError { + if (store.*.sealed != 0) { + return render_resource_fail(store, RenderResourceError.sealed, spec.id); + } + if (render_resource_store_configuration_valid(store.*) == 0) { + return render_resource_fail(store, RenderResourceError.invalid_configuration, spec.id); + } + let valid: RenderResourceError = render_resource_spec_valid(spec); + if (valid != RenderResourceError.none) { return render_resource_fail(store, valid, spec.id); } + let index: i32 = render_resource_find_index(store.*, spec.id); + if (index < 0) { + return render_resource_fail(store, RenderResourceError.missing_resource, spec.id); + } + let old: RenderResource = store.*.resources.data[index]; + if (old.kind != spec.kind) { + return render_resource_fail(store, RenderResourceError.kind_mismatch, spec.id); + } + if (spec.revision <= old.revision || spec.revision - old.revision != 1) { + return render_resource_fail(store, RenderResourceError.revision_conflict, spec.id); + } + if (spec.payload.len as i64 > store.*.max_payload_bytes) { + return render_resource_fail(store, RenderResourceError.payload_limit, spec.id); + } + let next_total: i64 = store.*.total_bytes - old.payload.len as i64 + spec.payload.len as i64; + if (next_total > store.*.max_total_bytes) { + return render_resource_fail(store, RenderResourceError.total_limit, spec.id); + } + let resource: RenderResource = render_resource_from_spec(spec); + free[u8](&store.*.resources.data[index].payload); + store.*.resources.data[index] = resource; + store.*.total_bytes = next_total; + store.*.revision = store.*.revision + 1; + render_resource_store_rehash(store); + store.*.last_error = RenderResourceError.none; + store.*.error_id = 0; + return RenderResourceError.none; +} + +fn render_resource_store_seal(store: *RenderResourceStore) RenderResourceError { + if (store.*.sealed != 0) { return RenderResourceError.none; } + if (render_resource_store_configuration_valid(store.*) == 0) { + return render_resource_fail(store, RenderResourceError.invalid_configuration, 0); + } + store.*.sealed = 1; + if (render_resource_store_verify(store.*) != RenderResourceError.none) { + store.*.sealed = 0; + return render_resource_fail(store, RenderResourceError.content_mutated, 0); + } + store.*.last_error = RenderResourceError.none; + store.*.error_id = 0; + return RenderResourceError.none; +} + +fn render_resource_store_verify(store: RenderResourceStore) RenderResourceError { + if (store.sealed == 0) { return RenderResourceError.not_sealed; } + if (render_resource_store_configuration_valid(store) == 0 || + store.resources.len > store.max_resources) { return RenderResourceError.content_mutated; } + let total: i64 = 0; + let expected_revision: i64 = 0; + let i: i32 = 0; + while (i < store.resources.len) { + let resource: RenderResource = store.resources.data[i]; + if (resource.id <= 0 || resource.payload.len <= 0 || + resource.payload.len as i64 > store.max_payload_bytes || + (i > 0 && store.resources.data[i - 1].id >= resource.id) || + render_resource_hash(resource) != resource.content_hash) { + return RenderResourceError.content_mutated; + } + total = total + resource.payload.len as i64; + if (resource.revision < 0 || resource.revision > 2147483647) { + return RenderResourceError.content_mutated; + } + expected_revision = expected_revision + resource.revision + 1; + if (total > store.max_total_bytes) { return RenderResourceError.content_mutated; } + i = i + 1; + } + if (total != store.total_bytes || store.revision != expected_revision || + render_resource_store_hash(store) != store.content_hash) { + return RenderResourceError.content_mutated; + } + return RenderResourceError.none; +} + +fn render_resource_store_free(store: *RenderResourceStore) void { + let i: i32 = 0; + while (i < store.*.resources.len) { + free[u8](&store.*.resources.data[i].payload); + i = i + 1; + } + free[RenderResource](&store.*.resources); +} diff --git a/src/replay/replay.zag b/src/replay/replay.zag new file mode 100644 index 0000000..e82599c --- /dev/null +++ b/src/replay/replay.zag @@ -0,0 +1,840 @@ +@import("std:list") +@import("../core/state.zag") +@import("../input/router.zag") +@import("../layout/flex.zag") +@import("../layout/flex_wrap.zag") +@import("../layout/grid.zag") +@import("../layout/overlay.zag") +@import("../layout/scroll.zag") +@import("../layout/virtual_list.zag") +@import("../layout/virtual_table.zag") +@import("../layout/virtual_tree.zag") +@import("../core/virtual_lifecycle.zag") +@import("../semantics/semantics.zag") +@import("../render/cpu_raster.zag") +@import("../motion/motion.zag") + +enum ReplayEventKind { + state_i64, + pointer, + clock, + backend_activate, + backend_loss, + backend_recover, +} + +enum ReplayBackendState { unavailable, active, lost } + +enum ReplayError { + none, + sealed, + not_sealed, + content_mutated, + invalid_event, + stale_state_revision, + clock_regression, + backend_transition, + input_rejected, + motion_error, + scene_error, +} + +struct ReplayEvent { + sequence: i64, + kind: ReplayEventKind, + a: i64, + b: i64, + c: i64, + d: i64, + e: i64, + f: i64, +} + +struct ReplayTape { + events: ArrayList[ReplayEvent], + sealed: i32, + revision: i64, + content_hash: i64, + last_error: ReplayError, + error_index: i32, +} + +struct ReplayResult { + error: ReplayError, + error_event: i32, + events_applied: i64, + state_value: i64, + state_revision: i64, + clock: i64, + backend_id: i64, + backend_generation: i64, + backend_state: ReplayBackendState, + input_hash: i64, + motion_hash: i64, + motion_value: i64, + motion_velocity: i64, + motion_status: MotionStatus, + semantics_hash: i64, + measure_hash: i64, + layout_hash: i64, + layout_lines: i32, + display_hash: i64, + cpu_hash: i64, + output_hash: i64, +} + +fn replay_event(kind: ReplayEventKind) ReplayEvent { + return ReplayEvent{ + .sequence = 0, + .kind = kind, + .a = 0, + .b = 0, + .c = 0, + .d = 0, + .e = 0, + .f = 0, + }; +} + +fn replay_state_event(state_id: i64, expected_revision: i64, value: i64) ReplayEvent { + let event: ReplayEvent = replay_event(ReplayEventKind.state_i64); + event.a = state_id; + event.b = expected_revision; + event.c = value; + return event; +} + +fn replay_pointer_event(pointer_id: i64, phase: PointerPhase, x: i64, y: i64) ReplayEvent { + let event: ReplayEvent = replay_event(ReplayEventKind.pointer); + event.a = pointer_id; + event.b = phase as i64; + event.c = x; + event.d = y; + return event; +} + +fn replay_clock_event(time: i64) ReplayEvent { + let event: ReplayEvent = replay_event(ReplayEventKind.clock); + event.a = time; + return event; +} + +fn replay_backend_activate_event(backend_id: i64) ReplayEvent { + let event: ReplayEvent = replay_event(ReplayEventKind.backend_activate); + event.a = backend_id; + return event; +} + +fn replay_backend_loss_event(backend_id: i64) ReplayEvent { + let event: ReplayEvent = replay_event(ReplayEventKind.backend_loss); + event.a = backend_id; + return event; +} + +fn replay_backend_recover_event(backend_id: i64) ReplayEvent { + let event: ReplayEvent = replay_event(ReplayEventKind.backend_recover); + event.a = backend_id; + return event; +} + +fn replay_tape_make() ReplayTape { + return ReplayTape{ + .events = make[ReplayEvent](32), + .sealed = 0, + .revision = 0, + .content_hash = 17, + .last_error = ReplayError.none, + .error_index = 0 - 1, + }; +} + +fn replay_tape_free(tape: *ReplayTape) void { + free[ReplayEvent](&tape.*.events); +} + +fn replay_hash_event(hash: i64, event: ReplayEvent) i64 { + let result: i64 = display_mix(hash, event.sequence); + result = display_mix(result, event.kind as i64); + result = display_mix(result, event.a); + result = display_mix(result, event.b); + result = display_mix(result, event.c); + result = display_mix(result, event.d); + result = display_mix(result, event.e); + return display_mix(result, event.f); +} + +fn replay_event_valid(event: ReplayEvent) i32 { + if (event.kind == ReplayEventKind.state_i64) { + return (event.a > 0 && event.b >= 0 && event.c >= -1000000 && event.c <= 1000000 && + event.d == 0 && event.e == 0 && event.f == 0) as i32; + } + if (event.kind == ReplayEventKind.pointer) { + return (event.a >= 0 && event.b >= PointerPhase.down as i64 && event.b <= PointerPhase.wheel as i64 && + event.c >= -1000000000 && event.c <= 1000000000 && + event.d >= -1000000000 && event.d <= 1000000000 && event.e == 0 && event.f == 0) as i32; + } + if (event.kind == ReplayEventKind.clock) { + return (event.a >= 0 && event.b == 0 && event.c == 0 && event.d == 0 && + event.e == 0 && event.f == 0) as i32; + } + if (event.kind == ReplayEventKind.backend_activate || + event.kind == ReplayEventKind.backend_loss || + event.kind == ReplayEventKind.backend_recover) { + return (event.a > 0 && event.b == 0 && event.c == 0 && event.d == 0 && + event.e == 0 && event.f == 0) as i32; + } + return 0; +} + +fn replay_tape_fail(tape: *ReplayTape, error: ReplayError) ReplayError { + tape.*.last_error = error; + tape.*.error_index = tape.*.events.len; + return error; +} + +fn replay_tape_push(tape: *ReplayTape, raw_event: ReplayEvent) ReplayError { + if (tape.*.sealed != 0) { return replay_tape_fail(tape, ReplayError.sealed); } + if (tape.*.events.len >= 1000000 || replay_event_valid(raw_event) == 0) { + return replay_tape_fail(tape, ReplayError.invalid_event); + } + let event: ReplayEvent = raw_event; + event.sequence = tape.*.revision + 1; + push[ReplayEvent](&tape.*.events, event); + tape.*.revision = tape.*.revision + 1; + tape.*.content_hash = replay_hash_event(tape.*.content_hash, event); + tape.*.last_error = ReplayError.none; + tape.*.error_index = 0 - 1; + return ReplayError.none; +} + +fn replay_tape_seal(tape: *ReplayTape) ReplayError { + if (tape.*.sealed != 0) { return ReplayError.none; } + tape.*.sealed = 1; + tape.*.last_error = ReplayError.none; + tape.*.error_index = 0 - 1; + return ReplayError.none; +} + +fn replay_tape_verify(tape: ReplayTape) ReplayError { + if (tape.sealed == 0) { return ReplayError.not_sealed; } + if (tape.revision != tape.events.len as i64) { return ReplayError.content_mutated; } + let hash: i64 = 17; + let i: i32 = 0; + while (i < tape.events.len) { + let event: ReplayEvent = tape.events.data[i]; + if (event.sequence != (i as i64) + 1 || replay_event_valid(event) == 0) { + return ReplayError.content_mutated; + } + hash = replay_hash_event(hash, event); + i = i + 1; + } + if (hash != tape.content_hash) { return ReplayError.content_mutated; } + return ReplayError.none; +} + +fn replay_result_error(error: ReplayError, event_index: i32, applied: i64) ReplayResult { + return ReplayResult{ + .error = error, + .error_event = event_index, + .events_applied = applied, + .state_value = 0, + .state_revision = 0, + .clock = 0, + .backend_id = 0, + .backend_generation = 0, + .backend_state = ReplayBackendState.unavailable, + .input_hash = 0, + .motion_hash = 0, + .motion_value = 0, + .motion_velocity = 0, + .motion_status = MotionStatus.settled, + .semantics_hash = 0, + .measure_hash = 0, + .layout_hash = 0, + .layout_lines = 0, + .display_hash = 0, + .cpu_hash = 0, + .output_hash = 0, + }; +} + +fn replay_hit_tree() HitTree { + let tree: HitTree = hit_tree_make(); + let root: HitNode = hit_node(node_key(100), hit_root_key(), rect(0, 0, 8 * unit_scale(), 4 * unit_scale())); + root.z_order = 0; + _ = hit_tree_add(&tree, root); + let button: HitNode = hit_node(node_key(101), node_key(100), rect(0, 0, 4 * unit_scale(), 4 * unit_scale())); + button.z_order = 1; + button.focusable = 1; + _ = hit_tree_add(&tree, button); + return tree; +} + +fn replay_hash_text(hash: i64, text: ArrayList[u8]) i64 { + let result: i64 = display_mix(hash, text.len as i64); + let i: i32 = 0; + while (i < text.len) { + result = display_mix(result, text.data[i] as i64); + i = i + 1; + } + return result; +} + +fn replay_semantics_hash(tree: SemanticsTree) i64 { + let hash: i64 = display_mix(17, tree.revision); + let i: i32 = 0; + while (i < tree.nodes.len) { + let node: SemanticsNode = tree.nodes.data[i]; + hash = display_mix(hash, node.id.value); + hash = display_mix(hash, node.id.generation); + hash = display_mix(hash, node.parent.value); + hash = display_mix(hash, node.parent.generation); + hash = display_mix(hash, node.role as i64); + hash = replay_hash_text(hash, node.name); + hash = replay_hash_text(hash, node.description); + hash = replay_hash_text(hash, node.value); + hash = display_mix(hash, node.action_mask); + hash = display_mix(hash, node.focus_order); + hash = display_mix(hash, node.disabled as i64); + hash = display_mix(hash, node.selected as i64); + hash = display_mix(hash, node.hidden as i64); + hash = display_mix(hash, node.live_region as i64); + hash = display_mix(hash, node.has_range as i64); + hash = display_mix(hash, node.range_min); + hash = display_mix(hash, node.range_max); + hash = display_mix(hash, node.range_value); + hash = display_mix(hash, node.range_step); + hash = display_mix(hash, node.text_length); + hash = display_mix(hash, node.selection_start); + hash = display_mix(hash, node.selection_end); + hash = display_mix(hash, node.has_bounds as i64); + hash = display_mix(hash, node.bounds.x); + hash = display_mix(hash, node.bounds.y); + hash = display_mix(hash, node.bounds.width); + hash = display_mix(hash, node.bounds.height); + hash = display_mix(hash, node.labelled_by.value); + hash = display_mix(hash, node.labelled_by.generation); + hash = display_mix(hash, node.described_by.value); + hash = display_mix(hash, node.described_by.generation); + hash = display_mix(hash, node.controls.value); + hash = display_mix(hash, node.controls.generation); + hash = display_mix(hash, node.row_count); + hash = display_mix(hash, node.column_count); + hash = display_mix(hash, node.row_index); + hash = display_mix(hash, node.column_index); + hash = display_mix(hash, node.row_span); + hash = display_mix(hash, node.column_span); + hash = display_mix(hash, node.level); + hash = display_mix(hash, node.set_size); + hash = display_mix(hash, node.position_in_set); + hash = display_mix(hash, node.expanded as i64); + i = i + 1; + } + return hash; +} + +fn replay_input_hash(router: InputRouter) i64 { + let hash: i64 = display_mix(17, router.events.len as i64); + let i: i32 = 0; + while (i < router.events.len) { + let event: RoutedEvent = router.events.data[i]; + hash = display_mix(hash, event.sequence); + hash = display_mix(hash, event.pointer_id); + hash = display_mix(hash, event.phase as i64); + hash = display_mix(hash, event.target.value); + hash = display_mix(hash, event.target.generation); + hash = display_mix(hash, event.local_x); + hash = display_mix(hash, event.local_y); + hash = display_mix(hash, event.captured as i64); + hash = display_mix(hash, event.status as i64); + i = i + 1; + } + hash = display_mix(hash, router.focus.value); + return display_mix(hash, router.focus.generation); +} + +fn replay_runtime_free(router: *InputRouter, tree: *HitTree, motion: *MotionScheduler) void { + input_router_free(router); + hit_tree_free(tree); + motion_scheduler_free(motion); +} + +fn replay_virtual_collections_hash(magnitude: i64) i64 { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let rows: VirtualListModel = virtual_model(100, 3000, unit_scale(), 0, magnitude, overrides); + let columns: ArrayList[VirtualTableColumn] = make[VirtualTableColumn](2); + let pinned: VirtualTableColumn = virtual_table_column( + 1, 3 * unit_scale(), 2 * unit_scale(), 4 * unit_scale() + ); + pinned.pinned = 1; + push[VirtualTableColumn](&columns, pinned); + push[VirtualTableColumn](&columns, virtual_table_column( + 2, 3 * unit_scale(), 2 * unit_scale(), 4 * unit_scale() + )); + let table: VirtualTableResult = virtual_table_layout( + virtual_table_model(77, rows, columns, magnitude), + size(8 * unit_scale(), 6 * unit_scale()), magnitude * unit_scale(), 0, + virtual_table_style(unit_scale(), unit_scale(), insets_all(0)) + ); + free[VirtualTableColumn](&columns); free[VirtualExtentOverride](&overrides); + if (table.error != VirtualTableError.none) { + virtual_table_free(&table); + return 0; + } + + let source: ArrayList[VirtualTreeNode] = make[VirtualTreeNode](2); + push[VirtualTreeNode](&source, virtual_tree_node( + 4000, 0, 0, 1, (magnitude != 0) as i32, unit_scale(), magnitude + )); + push[VirtualTreeNode](&source, virtual_tree_node( + 4001, 4000, 1, 0, 0, unit_scale(), magnitude + )); + let projection: VirtualTreeProjection = virtual_tree_project(source, magnitude); + free[VirtualTreeNode](&source); + if (projection.error != VirtualTreeError.none) { + virtual_tree_projection_free(&projection); virtual_table_free(&table); + return 0; + } + let tree: VirtualTreeResult = virtual_tree_layout( + projection, size(8 * unit_scale(), 6 * unit_scale()), 0, + virtual_tree_style(unit_scale(), 0, unit_scale(), insets_all(0)) + ); + if (tree.error != VirtualTreeError.none) { + virtual_tree_free(&tree); virtual_tree_projection_free(&projection); + virtual_table_free(&table); return 0; + } + let live_ids: ArrayList[i64] = make[i64](tree.placements.len); + let i: i32 = 0; + while (i < tree.placements.len) { + push[i64](&live_ids, tree.placements.data[i].id); + i = i + 1; + } + let store: VirtualRetainedStore = virtual_store_make(8); + let lifecycle: VirtualLifecycleResult = virtual_store_reconcile(&store, live_ids); + free[i64](&live_ids); + let hash: i64 = 0; + if (lifecycle.error == VirtualLifecycleError.none) { + hash = display_mix(display_mix(table.layout_hash, tree.layout_hash), lifecycle.lifecycle_hash); + } + virtual_store_free(&store); virtual_tree_free(&tree); + virtual_tree_projection_free(&projection); virtual_table_free(&table); + return hash; +} + +fn replay_build_scene(state_value: i64, backend_generation: i64) ReplayResult { + let result: ReplayResult = replay_result_error(ReplayError.none, 0 - 1, 0); + + let semantics: SemanticsTree = semantics_tree_make(); + let root_spec: SemanticsSpec = semantics_spec(node_key(100), semantic_root_key(), SemanticRole.application, "Replay scene"); + if (semantics_add(&semantics, root_spec) != SemanticsError.none) { + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let value_text: []u8 = "even"; + if (state_value % 2 != 0) { value_text = "odd"; } + let button_spec: SemanticsSpec = semantics_spec(node_key(101), node_key(100), SemanticRole.button, "Counter"); + button_spec.value = value_text; + button_spec.action_mask = semantic_actions(SemanticAction.activate); + button_spec.focus_order = 1; + button_spec.selected = ((state_value % 2) != 0) as i32; + if (semantics_add(&semantics, button_spec) != SemanticsError.none) { + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + result.semantics_hash = replay_semantics_hash(semantics); + + let magnitude: i64 = state_value % 3; + if (magnitude < 0) { magnitude = 0 - magnitude; } + let first_metrics: IntrinsicMetrics = intrinsic_metrics( + (2 + magnitude) * unit_scale(), 4 * unit_scale(), + (2 + magnitude) * unit_scale(), 4 * unit_scale(), + (2 + magnitude) * unit_scale(), 4 * unit_scale(), + 4 * unit_scale() + ); + let second_metrics: IntrinsicMetrics = intrinsic_metrics( + 2 * unit_scale(), 4 * unit_scale(), + 2 * unit_scale(), 4 * unit_scale(), + 2 * unit_scale(), 4 * unit_scale(), + 4 * unit_scale() + ); + let third_metrics: IntrinsicMetrics = intrinsic_metrics( + 2 * unit_scale(), 2 * unit_scale(), + 2 * unit_scale(), 2 * unit_scale(), + 2 * unit_scale(), 2 * unit_scale(), + 2 * unit_scale() + ); + let measurement: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node( + &measurement, intrinsic_group(1000, IntrinsicKind.row, unit_scale(), insets_all(0)) + ); + intrinsic_tree_add_node(&measurement, intrinsic_leaf(101, first_metrics, magnitude)); + intrinsic_tree_add_node(&measurement, intrinsic_leaf(102, second_metrics, 0)); + intrinsic_tree_add_node(&measurement, intrinsic_leaf(103, third_metrics, 0)); + intrinsic_tree_add_child(&measurement, 1000, 101); + intrinsic_tree_add_child(&measurement, 1000, 102); + intrinsic_tree_add_child(&measurement, 1000, 103); + let measured: MeasureResult = intrinsic_tree_measure( + measurement, 1000, + Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + } + ); + intrinsic_tree_free(&measurement); + if (measured.error != IntrinsicError.none || measured.visited_nodes != 4) { + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + result.measure_hash = measured.measure_hash; + + let items: ArrayList[FlexItem] = make[FlexItem](3); + let first: FlexItem = flex_item_intrinsic(101, first_metrics, FlexAxis.row, FlexPriority.required); + push[FlexItem](&items, first); + let second: FlexItem = flex_item_intrinsic(102, second_metrics, FlexAxis.row, FlexPriority.required); + push[FlexItem](&items, second); + let third: FlexItem = flex_item_intrinsic(103, third_metrics, FlexAxis.row, FlexPriority.required); + push[FlexItem](&items, third); + let style: FlexStyle = flex_style(FlexAxis.row, unit_scale(), insets_all(0)); + let wrap_style: FlexWrapStyle = flex_wrap_style(style, unit_scale()); + let layout: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(8 * unit_scale(), 6 * unit_scale()), wrap_style + ); + free[FlexItem](&items); + if (layout.error != FlexWrapError.none || layout.duplicate_ids != 0 || layout.placements.len != 3) { + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let overlay_items: ArrayList[OverlayItem] = make[OverlayItem](2); + let overlay_first: OverlayItem = overlay_item(101, first_metrics); + overlay_first.horizontal = PlacementAlignment.start; + overlay_first.vertical = PlacementAlignment.start; + push[OverlayItem](&overlay_items, overlay_first); + let overlay_second: OverlayItem = overlay_item(102, second_metrics); + overlay_second.horizontal = PlacementAlignment.end; + overlay_second.vertical = PlacementAlignment.end; + push[OverlayItem](&overlay_items, overlay_second); + let overlay_layout_result: OverlayResult = overlay_layout( + overlay_items, tight_constraints(8 * unit_scale(), 6 * unit_scale()), + overlay_style(insets_all(0), LayoutDirection.ltr) + ); + free[OverlayItem](&overlay_items); + if (overlay_layout_result.error != OverlayError.none) { + overlay_free(&overlay_layout_result); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + + let grid_columns: ArrayList[GridTrack] = make[GridTrack](2); + push[GridTrack](&grid_columns, grid_fraction(1, unit_scale(), unbounded_extent())); + push[GridTrack](&grid_columns, grid_fraction(1, unit_scale(), unbounded_extent())); + let grid_rows: ArrayList[GridTrack] = make[GridTrack](1); + push[GridTrack](&grid_rows, grid_intrinsic(unit_scale(), 4 * unit_scale(), 6 * unit_scale())); + let grid_items: ArrayList[GridItem] = make[GridItem](2); + push[GridItem](&grid_items, grid_item_at(101, 0, 0, first_metrics)); + push[GridItem](&grid_items, grid_item_at(102, 0, 1, second_metrics)); + let grid_layout_result: GridResult = grid_layout( + grid_items, grid_columns, grid_rows, + tight_constraints(8 * unit_scale(), 6 * unit_scale()), + grid_style(unit_scale(), 0, insets_all(0)) + ); + free[GridItem](&grid_items); free[GridTrack](&grid_columns); free[GridTrack](&grid_rows); + if (grid_layout_result.error != GridError.none) { + grid_free(&grid_layout_result); + overlay_free(&overlay_layout_result); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let virtual_overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let virtual_model_value: VirtualListModel = virtual_model( + 1000, 2000, unit_scale(), 0, magnitude, virtual_overrides + ); + let virtual_style_value: VirtualListStyle = virtual_style( + VirtualAxis.vertical, insets_all(0) + ); + let virtual_result: VirtualListResult = virtual_list_layout( + virtual_model_value, size(8 * unit_scale(), 6 * unit_scale()), + magnitude * unit_scale(), virtual_style_value + ); + free[VirtualExtentOverride](&virtual_overrides); + if (virtual_result.error != VirtualListError.none) { + virtual_free(&virtual_result); + grid_free(&grid_layout_result); + overlay_free(&overlay_layout_result); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let replay_scroll: ScrollState = scroll_state(virtual_result.viewport, virtual_result.content); + let replay_scroll_mutation: ScrollMutation = scroll_to( + &replay_scroll, 0, virtual_result.scroll_offset + ); + if (replay_scroll_mutation.error != ScrollError.none) { + virtual_free(&virtual_result); + grid_free(&grid_layout_result); + overlay_free(&overlay_layout_result); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let collections_hash: i64 = replay_virtual_collections_hash(magnitude); + if (collections_hash == 0) { + virtual_free(&virtual_result); + grid_free(&grid_layout_result); + overlay_free(&overlay_layout_result); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + result.layout_hash = display_mix( + display_mix( + display_mix(layout.layout_hash, overlay_layout_result.layout_hash), + grid_layout_result.layout_hash + ), + display_mix( + display_mix(virtual_result.layout_hash, scroll_state_hash(replay_scroll)), + collections_hash + ) + ); + result.layout_lines = layout.lines.len; + virtual_free(&virtual_result); + grid_free(&grid_layout_result); + overlay_free(&overlay_layout_result); + + let list: DisplayList = display_list_make(); + let replay_payload: []u8 = "REPLAY-RESOURCE-0"; + if (magnitude != 0) { replay_payload = "REPLAY-RESOURCE-1"; } + if (display_list_add_resource(&list, render_resource_spec( + 900, RenderResourceKind.effect, replay_payload, 1 + )) != RenderResourceError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let replay_path: PathData = path_make(PathFillRule.non_zero); + _ = path_move_to(&replay_path, path_point(unit_scale(), unit_scale())); + _ = path_line_to(&replay_path, + path_point((5 + magnitude) * unit_scale(), unit_scale())); + _ = path_quad_to(&replay_path, path_point(3 * unit_scale(), 5 * unit_scale()), + path_point(unit_scale(), unit_scale())); + _ = path_close(&replay_path); + if (path_seal(&replay_path) != PathError.none) { + path_free(&replay_path); + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let replay_path_payload: PathEncodeResult = path_encode(replay_path); + path_free(&replay_path); + let replay_path_error: RenderResourceError = display_list_add_resource(&list, + render_resource_spec(901, RenderResourceKind.path, + replay_path_payload.bytes.data[0..replay_path_payload.bytes.len], + path_resource_format_tag()) + ); + path_encode_free(&replay_path_payload); + if (replay_path_error != RenderResourceError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let replay_image_payload: []u8 = "RGBA"; + if (magnitude != 0) { replay_image_payload = "ZKUI"; } + let replay_image_spec: RenderResourceSpec = render_resource_spec( + 902, RenderResourceKind.image, replay_image_payload, + image_resource_format_tag() + ); + replay_image_spec.width = 1; + replay_image_spec.height = 1; + replay_image_spec.color_space = RenderColorSpace.srgb; + if (display_list_add_resource(&list, replay_image_spec) != + RenderResourceError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let first_op: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(101)); + first_op.bounds = layout.placements.data[0].frame; + let blue: i64 = (backend_generation % 8) * 4096; + first_op.paint = paint_rgba16(8192 + magnitude * 8192, 24576, 32768 + blue, 65535); + if (display_list_push(&list, first_op) != DisplayError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let second_op: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(102)); + second_op.bounds = layout.placements.data[1].frame; + second_op.paint = paint_rgba16(49152, 16384 + magnitude * 4096, 12288, 65535); + if (display_list_push(&list, second_op) != DisplayError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let third_op: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(103)); + third_op.bounds = layout.placements.data[2].frame; + third_op.paint = paint_rgba16(12288, 40960, 24576 + magnitude * 4096, 65535); + if (display_list_push(&list, third_op) != DisplayError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let path_op: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(104)); + path_op.resource_id = 901; + path_op.paint = paint_rgba16(57344, 49152, 8192, 32768); + if (display_list_push(&list, path_op) != DisplayError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + let image_op: DisplayOp = display_op(DisplayOpKind.draw_image, node_key(105)); + image_op.resource_id = 902; + image_op.bounds = rect(5 * unit_scale(), 0, 2 * unit_scale(), 2 * unit_scale()); + image_op.paint.alpha = 50000; + if (display_list_push(&list, image_op) != DisplayError.none || + display_list_seal(&list) != DisplayError.none) { + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + result.display_hash = list.content_hash; + + let raster: CpuRasterResult = cpu_rasterize(list, 8, 6); + if (raster.error != CpuRasterError.none) { + cpu_raster_free(&raster); + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return replay_result_error(ReplayError.scene_error, 0 - 1, 0); + } + result.cpu_hash = cpu_surface_hash(raster.surface); + + cpu_raster_free(&raster); + display_list_free(&list); + flex_wrap_free(&layout); + semantics_tree_free(&semantics); + return result; +} + +fn replay_execute(tape: ReplayTape) ReplayResult { + let verified: ReplayError = replay_tape_verify(tape); + if (verified != ReplayError.none) { return replay_result_error(verified, 0 - 1, 0); } + + let state: State[i64] = state_make[i64](1, 0); + let tree: HitTree = replay_hit_tree(); + let router: InputRouter = input_router_make(); + let motion: MotionScheduler = motion_scheduler_make(120); + _ = motion_set_clock(&motion, 0); + _ = motion_start_spring( + &motion, 900, 0, 0, spring_spec(220, 28), + MotionPreference.full, MotionReducedSubstitution.snap_to_end + ); + let clock: i64 = 0; + let backend_id: i64 = 0; + let backend_generation: i64 = 0; + let backend_state: ReplayBackendState = ReplayBackendState.unavailable; + let i: i32 = 0; + while (i < tape.events.len) { + let event: ReplayEvent = tape.events.data[i]; + if (event.kind == ReplayEventKind.state_i64) { + if (event.a != state.id || event.b != state.revision) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.stale_state_revision, i, i as i64); + } + _ = state_set[i64](&state, event.c); + if (motion_retarget( + &motion, 900, event.c * unit_scale(), MotionFrameReason.interrupt, + MotionPreference.full, MotionReducedSubstitution.snap_to_end + ) != MotionError.none) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.motion_error, i, i as i64); + } + } else if (event.kind == ReplayEventKind.pointer) { + let phase: PointerPhase = event.b as PointerPhase; + let routed: RouteResult = input_route_pointer(&router, tree, pointer_event(event.a, phase, event.c, event.d)); + if (routed.status != RouteStatus.accepted) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.input_rejected, i, i as i64); + } + } else if (event.kind == ReplayEventKind.clock) { + if (event.a < clock) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.clock_regression, i, i as i64); + } + if (motion_tick(&motion, event.a).error != MotionError.none) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.motion_error, i, i as i64); + } + clock = event.a; + } else if (event.kind == ReplayEventKind.backend_activate) { + if (backend_state != ReplayBackendState.unavailable) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.backend_transition, i, i as i64); + } + backend_id = event.a; + backend_generation = backend_generation + 1; + backend_state = ReplayBackendState.active; + } else if (event.kind == ReplayEventKind.backend_loss) { + if (backend_state != ReplayBackendState.active || event.a != backend_id) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.backend_transition, i, i as i64); + } + backend_state = ReplayBackendState.lost; + } else if (event.kind == ReplayEventKind.backend_recover) { + if (backend_state != ReplayBackendState.lost || event.a != backend_id) { + replay_runtime_free(&router, &tree, &motion); + return replay_result_error(ReplayError.backend_transition, i, i as i64); + } + backend_generation = backend_generation + 1; + backend_state = ReplayBackendState.active; + } + i = i + 1; + } + + let scene: ReplayResult = replay_build_scene(state.value, backend_generation); + if (scene.error != ReplayError.none) { + replay_runtime_free(&router, &tree, &motion); + return scene; + } + let result: ReplayResult = scene; + result.error = ReplayError.none; + result.error_event = 0 - 1; + result.events_applied = tape.events.len as i64; + result.state_value = state.value; + result.state_revision = state.revision; + result.clock = clock; + result.backend_id = backend_id; + result.backend_generation = backend_generation; + result.backend_state = backend_state; + result.input_hash = replay_input_hash(router); + result.motion_hash = motion_scheduler_hash(motion); + let motion_output: MotionSample = motion_sample(motion, 900); + result.motion_value = motion_output.value; + result.motion_velocity = motion_output.velocity; + result.motion_status = motion_output.status; + let hash: i64 = display_mix(17, result.state_value); + hash = display_mix(hash, result.state_revision); + hash = display_mix(hash, result.clock); + hash = display_mix(hash, result.backend_id); + hash = display_mix(hash, result.backend_generation); + hash = display_mix(hash, result.backend_state as i64); + hash = display_mix(hash, result.input_hash); + hash = display_mix(hash, result.motion_hash); + hash = display_mix(hash, result.semantics_hash); + hash = display_mix(hash, result.measure_hash); + hash = display_mix(hash, result.layout_hash); + hash = display_mix(hash, result.display_hash); + result.output_hash = display_mix(hash, result.cpu_hash); + + replay_runtime_free(&router, &tree, &motion); + return result; +} diff --git a/src/semantics/collections.zag b/src/semantics/collections.zag new file mode 100644 index 0000000..4828309 --- /dev/null +++ b/src/semantics/collections.zag @@ -0,0 +1,351 @@ +@import("std:list") +@import("semantics.zag") +@import("../layout/virtual_table.zag") +@import("../layout/virtual_tree.zag") + +enum CollectionSemanticsError { + none, + invalid_result, + missing_parent, + identity_collision, + semantic_error, +} + +struct CollectionSemanticsResult { + root_id: NodeKey, + nodes_added: i32, + live_items: i32, + logical_items: i64, + error: CollectionSemanticsError, + semantic_error: SemanticsError, + error_index: i32, +} + +fn collection_semantics_result(root_id: NodeKey) CollectionSemanticsResult { + return CollectionSemanticsResult{ + .root_id = root_id, .nodes_added = 0, .live_items = 0, .logical_items = 0, + .error = CollectionSemanticsError.none, .semantic_error = SemanticsError.none, + .error_index = 0 - 1, + }; +} + +fn collection_semantics_mix(hash: i64, value: i64) i64 { + return (hash * 1099511628211) ^ value; +} + +fn collection_semantic_key(root_id: NodeKey, kind: i64, + item_id: i64, column_id: i64) NodeKey { + let hash: i64 = collection_semantics_mix(17, root_id.value); + hash = collection_semantics_mix(hash, root_id.generation); + hash = collection_semantics_mix(hash, kind); + hash = collection_semantics_mix(hash, item_id); + hash = collection_semantics_mix(hash, column_id); + return node_key_scoped(hash, root_id.value ^ root_id.generation); +} + +fn semantic_table_row_key(root_id: NodeKey, row_id: i64, header: i32) NodeKey { + let kind: i64 = 100; + if (header != 0) { kind = 101; } + return collection_semantic_key(root_id, kind, row_id, 0); +} + +fn semantic_table_cell_key(root_id: NodeKey, cell_id: VirtualTableCellId) NodeKey { + let kind: i64 = 200; + if (cell_id.header != 0) { kind = 201; } + return collection_semantic_key(root_id, kind, cell_id.row_id, cell_id.column_id); +} + +fn semantic_tree_item_key(root_id: NodeKey, item_id: i64) NodeKey { + return collection_semantic_key(root_id, 300, item_id, 0); +} + +fn collection_key_preflight(tree: SemanticsTree, keys: *ArrayList[NodeKey], + key: NodeKey) i32 { + if (node_key_equal(key, semantic_root_key()) == 1 || + node_key_equal(key, semantic_none_key()) == 1) { return 0; } + if (semantics_find_index(tree, key) >= 0) { return 0; } + let i: i32 = 0; + while (i < keys.*.len) { + if (node_key_equal(keys.*.data[i], key) == 1) { return 0; } + i = i + 1; + } + push[NodeKey](keys, key); + return 1; +} + +fn collection_parent_valid(tree: SemanticsTree, parent: NodeKey) i32 { + return (node_key_equal(parent, semantic_root_key()) == 1 || + semantics_find_index(tree, parent) >= 0) as i32; +} + +fn collection_rect_valid(bounds: Rect) i32 { + return (bounds.x >= 0 - unbounded_extent() && bounds.x <= unbounded_extent() && + bounds.y >= 0 - unbounded_extent() && bounds.y <= unbounded_extent() && + bounds.width >= 0 && bounds.width <= unbounded_extent() && + bounds.height >= 0 && bounds.height <= unbounded_extent() && + bounds.x <= unbounded_extent() - bounds.width && + bounds.y <= unbounded_extent() - bounds.height) as i32; +} + +fn collection_table_cell_valid(table: VirtualTableResult, + cell: VirtualTableCell) i32 { + if (cell.id.table_id <= 0 || cell.id.row_id != cell.row_id || + cell.id.column_id != cell.column_id || cell.id.header != cell.header || + cell.column_id <= 0 || cell.column_index < 0 || + cell.column_index >= table.semantic_columns || + (cell.header != 0 && cell.header != 1) || collection_rect_valid(cell.frame) == 0) { + return 0; + } + if (cell.header != 0) { + return (cell.row_id == 0 && cell.row_index == 0 - 1) as i32; + } + return (cell.row_id > 0 && cell.row_index >= 0 && + cell.row_index < table.semantic_rows) as i32; +} + +fn collection_tree_placement_valid(layout: VirtualTreeResult, + placement: VirtualTreePlacement) i32 { + return (placement.id > 0 && placement.parent_id >= 0 && + placement.visible_index >= 0 && placement.visible_index < layout.semantic_count && + placement.depth >= 0 && placement.depth <= 512 && + (placement.has_children == 0 || placement.has_children == 1) && + (placement.expanded == 0 || placement.expanded == 1) && + (placement.has_children != 0 || placement.expanded == 0) && + collection_rect_valid(placement.frame) == 1 && + collection_rect_valid(placement.content_frame) == 1) as i32; +} + +fn collection_semantics_rollback(tree: *SemanticsTree, + checkpoint: SemanticsCheckpoint, result: *CollectionSemanticsResult, + error: SemanticsError, index: i32) void { + _ = semantics_tree_rollback(tree, checkpoint); + result.*.nodes_added = 0; + result.*.error = CollectionSemanticsError.semantic_error; + result.*.semantic_error = error; + result.*.error_index = index; +} + +fn collection_table_row_bounds(table: VirtualTableResult, + row_id: i64, header: i32) Rect { + let found: i32 = 0; + let bounds: Rect = rect(0, 0, 0, 0); + let i: i32 = 0; + while (i < table.cells.len) { + let cell: VirtualTableCell = table.cells.data[i]; + if (cell.row_id == row_id && cell.header == header) { + if (found == 0) { + bounds = cell.frame; + found = 1; + } else { + let left: i64 = bounds.x; + if (cell.frame.x < left) { left = cell.frame.x; } + let top: i64 = bounds.y; + if (cell.frame.y < top) { top = cell.frame.y; } + let right: i64 = bounds.x + bounds.width; + if (cell.frame.x + cell.frame.width > right) { + right = cell.frame.x + cell.frame.width; + } + let bottom: i64 = bounds.y + bounds.height; + if (cell.frame.y + cell.frame.height > bottom) { + bottom = cell.frame.y + cell.frame.height; + } + bounds = rect(left, top, right - left, bottom - top); + } + } + i = i + 1; + } + return bounds; +} + +fn collection_table_row_first(table: VirtualTableResult, + cell_index: i32) i32 { + let cell: VirtualTableCell = table.cells.data[cell_index]; + let i: i32 = 0; + while (i < cell_index) { + if (table.cells.data[i].row_id == cell.row_id && + table.cells.data[i].header == cell.header) { return 0; } + i = i + 1; + } + return 1; +} + +fn semantics_project_table(tree: *SemanticsTree, table: VirtualTableResult, + root_id: NodeKey, parent: NodeKey, name: []u8) CollectionSemanticsResult { + let result: CollectionSemanticsResult = collection_semantics_result(root_id); + result.live_items = table.cells.len; + result.logical_items = table.semantic_rows * table.semantic_columns as i64; + if (table.error != VirtualTableError.none || table.semantic_rows < 0 || + table.semantic_columns <= 0 || table.viewport.width <= 0 || table.viewport.height <= 0) { + result.error = CollectionSemanticsError.invalid_result; + result.error_index = table.error_index; + return result; + } + if (collection_parent_valid(tree.*, parent) == 0) { + result.error = CollectionSemanticsError.missing_parent; + return result; + } + + let keys: ArrayList[NodeKey] = make[NodeKey](table.cells.len * 2 + 2); + if (collection_key_preflight(tree.*, &keys, root_id) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.identity_collision; return result; + } + let i: i32 = 0; + while (i < table.cells.len) { + let cell: VirtualTableCell = table.cells.data[i]; + if (collection_table_cell_valid(table, cell) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.invalid_result; + result.error_index = i; return result; + } + if (collection_table_row_first(table, i) != 0 && + collection_key_preflight(tree.*, &keys, + semantic_table_row_key(root_id, cell.row_id, cell.header)) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.identity_collision; + result.error_index = i; return result; + } + if (collection_key_preflight(tree.*, &keys, + semantic_table_cell_key(root_id, cell.id)) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.identity_collision; + result.error_index = i; return result; + } + i = i + 1; + } + free[NodeKey](&keys); + + let checkpoint: SemanticsCheckpoint = semantics_tree_checkpoint(tree.*); + let root: SemanticsSpec = semantics_spec(root_id, parent, SemanticRole.table, name); + root.row_count = table.semantic_rows; + root.column_count = table.semantic_columns as i64; + root.has_bounds = 1; + root.bounds = rect(0, 0, table.viewport.width, table.viewport.height); + root.action_mask = semantic_actions(SemanticAction.scroll); + let added: SemanticsError = semantics_add(tree, root); + if (added != SemanticsError.none) { + result.error = CollectionSemanticsError.semantic_error; + result.semantic_error = added; + return result; + } + result.nodes_added = 1; + + i = 0; + while (i < table.cells.len) { + let cell: VirtualTableCell = table.cells.data[i]; + let row_id: NodeKey = semantic_table_row_key(root_id, cell.row_id, cell.header); + if (collection_table_row_first(table, i) != 0) { + let row: SemanticsSpec = semantics_spec(row_id, root_id, SemanticRole.row, ""); + row.has_bounds = 1; + row.bounds = collection_table_row_bounds(table, cell.row_id, cell.header); + if (cell.header == 0) { + row.row_index = cell.row_index; + row.row_span = 1; + } + let row_error: SemanticsError = semantics_add(tree, row); + if (row_error != SemanticsError.none) { + collection_semantics_rollback(tree, checkpoint, &result, row_error, i); + return result; + } + result.nodes_added = result.nodes_added + 1; + } + let cell_spec: SemanticsSpec = semantics_spec( + semantic_table_cell_key(root_id, cell.id), row_id, SemanticRole.cell, "" + ); + cell_spec.has_bounds = 1; + cell_spec.bounds = cell.frame; + cell_spec.column_index = cell.column_index as i64; + cell_spec.column_span = 1; + if (cell.header == 0) { + cell_spec.row_index = cell.row_index; + cell_spec.row_span = 1; + } + let cell_error: SemanticsError = semantics_add(tree, cell_spec); + if (cell_error != SemanticsError.none) { + collection_semantics_rollback(tree, checkpoint, &result, cell_error, i); + return result; + } + result.nodes_added = result.nodes_added + 1; + i = i + 1; + } + return result; +} + +fn semantics_project_tree(tree: *SemanticsTree, layout: VirtualTreeResult, + root_id: NodeKey, parent: NodeKey, name: []u8) CollectionSemanticsResult { + let result: CollectionSemanticsResult = collection_semantics_result(root_id); + result.live_items = layout.placements.len; + result.logical_items = layout.semantic_count; + if (layout.error != VirtualTreeError.none || layout.semantic_count < 0 || + layout.viewport.width <= 0 || layout.viewport.height <= 0) { + result.error = CollectionSemanticsError.invalid_result; + result.error_index = layout.error_index; + return result; + } + if (collection_parent_valid(tree.*, parent) == 0) { + result.error = CollectionSemanticsError.missing_parent; + return result; + } + let keys: ArrayList[NodeKey] = make[NodeKey](layout.placements.len + 1); + if (collection_key_preflight(tree.*, &keys, root_id) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.identity_collision; return result; + } + let i: i32 = 0; + while (i < layout.placements.len) { + if (collection_tree_placement_valid(layout, layout.placements.data[i]) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.invalid_result; + result.error_index = i; return result; + } + if (collection_key_preflight(tree.*, &keys, + semantic_tree_item_key(root_id, layout.placements.data[i].id)) == 0) { + free[NodeKey](&keys); result.error = CollectionSemanticsError.identity_collision; + result.error_index = i; return result; + } + i = i + 1; + } + free[NodeKey](&keys); + + let checkpoint: SemanticsCheckpoint = semantics_tree_checkpoint(tree.*); + let root: SemanticsSpec = semantics_spec(root_id, parent, SemanticRole.tree, name); + root.row_count = layout.semantic_count; + root.column_count = 1; + root.has_bounds = 1; + root.bounds = rect(0, 0, layout.viewport.width, layout.viewport.height); + root.action_mask = semantic_actions(SemanticAction.scroll); + let added: SemanticsError = semantics_add(tree, root); + if (added != SemanticsError.none) { + result.error = CollectionSemanticsError.semantic_error; + result.semantic_error = added; + return result; + } + result.nodes_added = 1; + i = 0; + while (i < layout.placements.len) { + let placement: VirtualTreePlacement = layout.placements.data[i]; + let item: SemanticsSpec = semantics_spec( + semantic_tree_item_key(root_id, placement.id), root_id, + SemanticRole.tree_item, "" + ); + item.has_bounds = 1; + item.bounds = placement.frame; + item.row_index = placement.visible_index; + item.row_span = 1; + item.column_index = 0; + item.column_span = 1; + item.level = placement.depth as i64 + 1; + item.set_size = layout.semantic_count; + item.position_in_set = placement.visible_index + 1; + if (placement.has_children != 0) { + item.expanded = placement.expanded; + if (placement.expanded != 0) { + item.action_mask = semantic_actions(SemanticAction.collapse); + } else { + item.action_mask = semantic_actions(SemanticAction.expand); + } + } + let item_error: SemanticsError = semantics_add(tree, item); + if (item_error != SemanticsError.none) { + collection_semantics_rollback(tree, checkpoint, &result, item_error, i); + return result; + } + result.nodes_added = result.nodes_added + 1; + i = i + 1; + } + return result; +} diff --git a/src/semantics/semantics.zag b/src/semantics/semantics.zag new file mode 100644 index 0000000..bff6c57 --- /dev/null +++ b/src/semantics/semantics.zag @@ -0,0 +1,524 @@ +@import("std:list") +@import("../core/view_contract.zag") +@import("../core/geometry.zag") + +enum SemanticRole { + application, + window, + group, + text, + button, + toggle, + checkbox, + radio, + slider, + progress, + text_field, + text_editor, + image, + link, + menu, + menu_item, + tab, + list, + list_item, + table, + row, + cell, + tree, + tree_item, + dialog, +} + +enum SemanticAction { + activate, + set_value, + increment, + decrement, + focus, + scroll, + expand, + collapse, + dismiss, + select, +} + +enum LiveRegion { off, polite, assertive } + +enum SemanticsError { + none, + invalid_id, + duplicate_id, + missing_parent, + duplicate_focus_order, + invalid_range, + invalid_text_selection, + invalid_bounds, + invalid_relationship, + invalid_collection, + invalid_state, +} + +struct SemanticsSpec { + id: NodeKey, + parent: NodeKey, + role: SemanticRole, + name: []u8, + description: []u8, + value: []u8, + action_mask: i64, + focus_order: i64, + disabled: i32, + selected: i32, + hidden: i32, + live_region: LiveRegion, + has_range: i32, + range_min: i64, + range_max: i64, + range_value: i64, + range_step: i64, + text_length: i64, + selection_start: i64, + selection_end: i64, + has_bounds: i32, + bounds: Rect, + labelled_by: NodeKey, + described_by: NodeKey, + controls: NodeKey, + row_count: i64, + column_count: i64, + row_index: i64, + column_index: i64, + row_span: i64, + column_span: i64, + level: i64, + set_size: i64, + position_in_set: i64, + expanded: i32, +} + +struct SemanticsNode { + id: NodeKey, + parent: NodeKey, + role: SemanticRole, + name: ArrayList[u8], + description: ArrayList[u8], + value: ArrayList[u8], + action_mask: i64, + focus_order: i64, + disabled: i32, + selected: i32, + hidden: i32, + live_region: LiveRegion, + has_range: i32, + range_min: i64, + range_max: i64, + range_value: i64, + range_step: i64, + text_length: i64, + selection_start: i64, + selection_end: i64, + has_bounds: i32, + bounds: Rect, + labelled_by: NodeKey, + described_by: NodeKey, + controls: NodeKey, + row_count: i64, + column_count: i64, + row_index: i64, + column_index: i64, + row_span: i64, + column_span: i64, + level: i64, + set_size: i64, + position_in_set: i64, + expanded: i32, +} + +struct SemanticsTree { + nodes: ArrayList[SemanticsNode], + revision: i64, + last_error: SemanticsError, + error_node: NodeKey, +} + +struct SemanticsCheckpoint { + length: i32, + revision: i64, + last_error: SemanticsError, + error_node: NodeKey, +} + +fn semantic_root_key() NodeKey { return node_key(0 - 1); } +fn semantic_none_key() NodeKey { return node_key(0 - 2); } + +fn semantic_action_bit(action: SemanticAction) i64 { + return switch (action) { + .activate => 1, + .set_value => 2, + .increment => 4, + .decrement => 8, + .focus => 16, + .scroll => 32, + .expand => 64, + .collapse => 128, + .dismiss => 256, + .select => 512, + }; +} + +fn semantic_actions(first: SemanticAction) i64 { + return semantic_action_bit(first); +} + +fn semantic_actions_add(mask: i64, action: SemanticAction) i64 { + return mask | semantic_action_bit(action); +} + +fn semantic_actions_has(mask: i64, action: SemanticAction) i32 { + return ((mask & semantic_action_bit(action)) != 0) as i32; +} + +fn semantic_generated_key(parent: NodeKey, role: SemanticRole, ordinal: i64) NodeKey { + let role_code: i64 = role as i64; + let value: i64 = parent.value * 1000003 + role_code * 1009 + ordinal + 17; + return node_key_scoped(value, parent.generation); +} + +fn semantics_spec(id: NodeKey, parent: NodeKey, role: SemanticRole, name: []u8) SemanticsSpec { + return SemanticsSpec{ + .id = id, + .parent = parent, + .role = role, + .name = name, + .description = "", + .value = "", + .action_mask = 0, + .focus_order = 0, + .disabled = 0, + .selected = 0, + .hidden = 0, + .live_region = LiveRegion.off, + .has_range = 0, + .range_min = 0, + .range_max = 0, + .range_value = 0, + .range_step = 0, + .text_length = 0, + .selection_start = 0, + .selection_end = 0, + .has_bounds = 0, + .bounds = rect(0, 0, 0, 0), + .labelled_by = semantic_none_key(), + .described_by = semantic_none_key(), + .controls = semantic_none_key(), + .row_count = 0 - 1, + .column_count = 0 - 1, + .row_index = 0 - 1, + .column_index = 0 - 1, + .row_span = 0, + .column_span = 0, + .level = 0, + .set_size = 0, + .position_in_set = 0, + .expanded = 0 - 1, + }; +} + +fn semantics_tree_make() SemanticsTree { + return SemanticsTree{ + .nodes = make[SemanticsNode](8), + .revision = 0, + .last_error = SemanticsError.none, + .error_node = semantic_root_key(), + }; +} + +fn semantic_text_copy(text: []u8) ArrayList[u8] { + let result: ArrayList[u8] = make[u8](text.len); + let i: i32 = 0; + while (i < text.len) { + push[u8](&result, text[i]); + i = i + 1; + } + return result; +} + +fn semantic_text_equal(actual: ArrayList[u8], expected: []u8) i32 { + if (actual.len != expected.len) { return 0; } + let i: i32 = 0; + while (i < actual.len) { + if (actual.data[i] != expected[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn semantic_evidence_mix(hash: i64, value: i64) i64 { + return (hash * 1099511628211) ^ value; +} + +fn semantic_text_evidence_hash(hash: i64, text: ArrayList[u8]) i64 { + let result: i64 = semantic_evidence_mix(hash, text.len as i64); + let i: i32 = 0; + while (i < text.len) { + result = semantic_evidence_mix(result, text.data[i] as i64); + i = i + 1; + } + return result; +} + +fn semantic_node_evidence_hash(node: SemanticsNode) i64 { + let hash: i64 = semantic_evidence_mix(17, node.id.value); + hash = semantic_evidence_mix(hash, node.id.generation); + hash = semantic_evidence_mix(hash, node.parent.value); + hash = semantic_evidence_mix(hash, node.parent.generation); + hash = semantic_evidence_mix(hash, node.role as i64); + hash = semantic_text_evidence_hash(hash, node.name); + hash = semantic_text_evidence_hash(hash, node.description); + hash = semantic_text_evidence_hash(hash, node.value); + hash = semantic_evidence_mix(hash, node.action_mask); + hash = semantic_evidence_mix(hash, node.focus_order); + hash = semantic_evidence_mix(hash, node.disabled as i64); + hash = semantic_evidence_mix(hash, node.selected as i64); + hash = semantic_evidence_mix(hash, node.hidden as i64); + hash = semantic_evidence_mix(hash, node.live_region as i64); + hash = semantic_evidence_mix(hash, node.has_range as i64); + hash = semantic_evidence_mix(hash, node.range_min); + hash = semantic_evidence_mix(hash, node.range_max); + hash = semantic_evidence_mix(hash, node.range_value); + hash = semantic_evidence_mix(hash, node.range_step); + hash = semantic_evidence_mix(hash, node.text_length); + hash = semantic_evidence_mix(hash, node.selection_start); + hash = semantic_evidence_mix(hash, node.selection_end); + hash = semantic_evidence_mix(hash, node.has_bounds as i64); + hash = semantic_evidence_mix(hash, node.bounds.x); + hash = semantic_evidence_mix(hash, node.bounds.y); + hash = semantic_evidence_mix(hash, node.bounds.width); + hash = semantic_evidence_mix(hash, node.bounds.height); + hash = semantic_evidence_mix(hash, node.labelled_by.value); + hash = semantic_evidence_mix(hash, node.labelled_by.generation); + hash = semantic_evidence_mix(hash, node.described_by.value); + hash = semantic_evidence_mix(hash, node.described_by.generation); + hash = semantic_evidence_mix(hash, node.controls.value); + hash = semantic_evidence_mix(hash, node.controls.generation); + hash = semantic_evidence_mix(hash, node.row_count); + hash = semantic_evidence_mix(hash, node.column_count); + hash = semantic_evidence_mix(hash, node.row_index); + hash = semantic_evidence_mix(hash, node.column_index); + hash = semantic_evidence_mix(hash, node.row_span); + hash = semantic_evidence_mix(hash, node.column_span); + hash = semantic_evidence_mix(hash, node.level); + hash = semantic_evidence_mix(hash, node.set_size); + hash = semantic_evidence_mix(hash, node.position_in_set); + return semantic_evidence_mix(hash, node.expanded as i64); +} + +fn semantics_find_index(tree: SemanticsTree, id: NodeKey) i32 { + let i: i32 = tree.nodes.len - 1; + while (i >= 0) { + if (node_key_equal(tree.nodes.data[i].id, id) == 1) { return i; } + i = i - 1; + } + return 0 - 1; +} + +fn semantics_fail(tree: *SemanticsTree, error: SemanticsError, id: NodeKey) SemanticsError { + tree.*.last_error = error; + tree.*.error_node = id; + return error; +} + +fn semantics_relationship_valid(tree: SemanticsTree, owner: NodeKey, target: NodeKey) i32 { + if (node_key_equal(target, semantic_none_key()) == 1) { return 1; } + if (node_key_equal(owner, target) == 1) { return 0; } + return (semantics_find_index(tree, target) >= 0) as i32; +} + +fn semantics_bounds_valid(spec: SemanticsSpec) i32 { + if (spec.has_bounds == 0) { return 1; } + if (spec.has_bounds != 1 || spec.bounds.width < 0 || spec.bounds.height < 0) { return 0; } + if (spec.bounds.x < 0 - unbounded_extent() || spec.bounds.x > unbounded_extent() || + spec.bounds.y < 0 - unbounded_extent() || spec.bounds.y > unbounded_extent() || + spec.bounds.width > unbounded_extent() || spec.bounds.height > unbounded_extent()) { return 0; } + if (spec.bounds.x > unbounded_extent() - spec.bounds.width || + spec.bounds.y > unbounded_extent() - spec.bounds.height) { return 0; } + return 1; +} + +fn semantics_collection_valid(spec: SemanticsSpec) i32 { + if (spec.row_count < 0 - 1 || spec.column_count < 0 - 1 || + spec.row_index < 0 - 1 || spec.column_index < 0 - 1 || + spec.row_span < 0 || spec.column_span < 0 || spec.level < 0 || + spec.set_size < 0 || spec.position_in_set < 0 || + spec.expanded < 0 - 1 || spec.expanded > 1) { return 0; } + if ((spec.row_index < 0 && spec.row_span != 0) || + (spec.row_index >= 0 && spec.row_span <= 0) || + (spec.column_index < 0 && spec.column_span != 0) || + (spec.column_index >= 0 && spec.column_span <= 0)) { return 0; } + if (spec.row_count >= 0 && spec.row_index >= 0 && + (spec.row_index >= spec.row_count || spec.row_span > spec.row_count - spec.row_index)) { return 0; } + if (spec.column_count >= 0 && spec.column_index >= 0 && + (spec.column_index >= spec.column_count || spec.column_span > spec.column_count - spec.column_index)) { return 0; } + if ((spec.set_size == 0 && spec.position_in_set != 0) || + (spec.set_size > 0 && (spec.position_in_set <= 0 || spec.position_in_set > spec.set_size))) { return 0; } + return 1; +} + +fn semantics_state_valid(spec: SemanticsSpec) i32 { + if ((spec.disabled != 0 && spec.disabled != 1) || + (spec.selected != 0 && spec.selected != 1) || + (spec.hidden != 0 && spec.hidden != 1) || + (spec.has_range != 0 && spec.has_range != 1) || + spec.action_mask < 0 || spec.action_mask > 1023 || spec.focus_order < 0) { return 0; } + if (spec.has_range == 0 && (spec.range_min != 0 || spec.range_max != 0 || + spec.range_value != 0 || spec.range_step != 0)) { return 0; } + if (spec.has_bounds == 0 && (spec.bounds.x != 0 || spec.bounds.y != 0 || + spec.bounds.width != 0 || spec.bounds.height != 0)) { return 0; } + return 1; +} + +fn semantics_add(tree: *SemanticsTree, spec: SemanticsSpec) SemanticsError { + if (node_key_equal(spec.id, semantic_root_key()) == 1 || + node_key_equal(spec.id, semantic_none_key()) == 1) { + return semantics_fail(tree, SemanticsError.invalid_id, spec.id); + } + if (semantics_find_index(tree.*, spec.id) >= 0) { + return semantics_fail(tree, SemanticsError.duplicate_id, spec.id); + } + if (node_key_equal(spec.parent, semantic_root_key()) == 0 && + semantics_find_index(tree.*, spec.parent) < 0) { + return semantics_fail(tree, SemanticsError.missing_parent, spec.id); + } + if (spec.focus_order > 0) { + let i: i32 = 0; + while (i < tree.*.nodes.len) { + if (tree.*.nodes.data[i].focus_order == spec.focus_order) { + return semantics_fail(tree, SemanticsError.duplicate_focus_order, spec.id); + } + i = i + 1; + } + } + if (spec.has_range != 0 && + (spec.range_min > spec.range_max || spec.range_value < spec.range_min || + spec.range_value > spec.range_max || spec.range_step <= 0)) { + return semantics_fail(tree, SemanticsError.invalid_range, spec.id); + } + if (spec.text_length < 0 || spec.selection_start < 0 || + spec.selection_end < spec.selection_start || spec.selection_end > spec.text_length) { + return semantics_fail(tree, SemanticsError.invalid_text_selection, spec.id); + } + if (semantics_bounds_valid(spec) == 0) { + return semantics_fail(tree, SemanticsError.invalid_bounds, spec.id); + } + if (semantics_relationship_valid(tree.*, spec.id, spec.labelled_by) == 0 || + semantics_relationship_valid(tree.*, spec.id, spec.described_by) == 0 || + semantics_relationship_valid(tree.*, spec.id, spec.controls) == 0) { + return semantics_fail(tree, SemanticsError.invalid_relationship, spec.id); + } + if (semantics_collection_valid(spec) == 0) { + return semantics_fail(tree, SemanticsError.invalid_collection, spec.id); + } + if (semantics_state_valid(spec) == 0) { + return semantics_fail(tree, SemanticsError.invalid_state, spec.id); + } + + let node: SemanticsNode = SemanticsNode{ + .id = spec.id, + .parent = spec.parent, + .role = spec.role, + .name = semantic_text_copy(spec.name), + .description = semantic_text_copy(spec.description), + .value = semantic_text_copy(spec.value), + .action_mask = spec.action_mask, + .focus_order = spec.focus_order, + .disabled = spec.disabled, + .selected = spec.selected, + .hidden = spec.hidden, + .live_region = spec.live_region, + .has_range = spec.has_range, + .range_min = spec.range_min, + .range_max = spec.range_max, + .range_value = spec.range_value, + .range_step = spec.range_step, + .text_length = spec.text_length, + .selection_start = spec.selection_start, + .selection_end = spec.selection_end, + .has_bounds = spec.has_bounds, + .bounds = spec.bounds, + .labelled_by = spec.labelled_by, + .described_by = spec.described_by, + .controls = spec.controls, + .row_count = spec.row_count, + .column_count = spec.column_count, + .row_index = spec.row_index, + .column_index = spec.column_index, + .row_span = spec.row_span, + .column_span = spec.column_span, + .level = spec.level, + .set_size = spec.set_size, + .position_in_set = spec.position_in_set, + .expanded = spec.expanded, + }; + push[SemanticsNode](&tree.*.nodes, node); + tree.*.revision = tree.*.revision + 1; + tree.*.last_error = SemanticsError.none; + tree.*.error_node = semantic_root_key(); + return SemanticsError.none; +} + +fn semantics_next_focus(tree: SemanticsTree, after_order: i64) NodeKey { + let found: i32 = 0; + let best_order: i64 = 0; + let best: NodeKey = semantic_root_key(); + let i: i32 = 0; + while (i < tree.nodes.len) { + let node: SemanticsNode = tree.nodes.data[i]; + if (node.focus_order > after_order && node.disabled == 0 && node.hidden == 0 && + (found == 0 || node.focus_order < best_order)) { + found = 1; + best_order = node.focus_order; + best = node.id; + } + i = i + 1; + } + return best; +} + +fn semantics_tree_free(tree: *SemanticsTree) void { + let i: i32 = 0; + while (i < tree.*.nodes.len) { + free[u8](&tree.*.nodes.data[i].name); + free[u8](&tree.*.nodes.data[i].description); + free[u8](&tree.*.nodes.data[i].value); + i = i + 1; + } + free[SemanticsNode](&tree.*.nodes); +} + +fn semantics_tree_checkpoint(tree: SemanticsTree) SemanticsCheckpoint { + return SemanticsCheckpoint{ + .length = tree.nodes.len, + .revision = tree.revision, + .last_error = tree.last_error, + .error_node = tree.error_node, + }; +} + +fn semantics_tree_rollback(tree: *SemanticsTree, + checkpoint: SemanticsCheckpoint) i32 { + if (checkpoint.length < 0 || checkpoint.length > tree.*.nodes.len || + checkpoint.revision < 0 || checkpoint.revision > tree.*.revision) { return 0; } + let i: i32 = checkpoint.length; + while (i < tree.*.nodes.len) { + free[u8](&tree.*.nodes.data[i].name); + free[u8](&tree.*.nodes.data[i].description); + free[u8](&tree.*.nodes.data[i].value); + i = i + 1; + } + tree.*.nodes.len = checkpoint.length; + tree.*.revision = checkpoint.revision; + tree.*.last_error = checkpoint.last_error; + tree.*.error_node = checkpoint.error_node; + return 1; +} diff --git a/tests/collection_semantics_contract.zag b/tests/collection_semantics_contract.zag new file mode 100644 index 0000000..ee3602f --- /dev/null +++ b/tests/collection_semantics_contract.zag @@ -0,0 +1,151 @@ +@import("std:list") +@import("../src/semantics/collections.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn table_semantics_contract(state: *TestState) void { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let rows: VirtualListModel = virtual_model(1000000, 1000, 20, 1, 3, overrides); + let columns: ArrayList[VirtualTableColumn] = make[VirtualTableColumn](4); + let pinned: VirtualTableColumn = virtual_table_column(10, 40, 30, 60); + pinned.pinned = 1; + push[VirtualTableColumn](&columns, pinned); + push[VirtualTableColumn](&columns, virtual_table_column(11, 50, 30, 80)); + push[VirtualTableColumn](&columns, virtual_table_column(12, 50, 30, 80)); + push[VirtualTableColumn](&columns, virtual_table_column(13, 50, 30, 80)); + let model: VirtualTableModel = virtual_table_model(7, rows, columns, 2); + let style: VirtualTableStyle = virtual_table_style(20, 2, insets_all(2)); + let scroll: i64 = virtual_item_offset(rows, + virtual_style(VirtualAxis.vertical, insets_all(0)), 500000); + let layout: VirtualTableResult = virtual_table_layout(model, size(180, 120), scroll, 20, style); + let tree: SemanticsTree = semantics_tree_make(); + let root_id: NodeKey = node_key_scoped(700, 2); + let projected: CollectionSemanticsResult = semantics_project_table( + &tree, layout, root_id, semantic_root_key(), "Parts" + ); + let root_index: i32 = semantics_find_index(tree, root_id); + expect(state, projected.error == CollectionSemanticsError.none && root_index >= 0 && + tree.nodes.data[root_index].row_count == 1000000 && + tree.nodes.data[root_index].column_count == 4, + "Table semantics retain full logical counts while materializing only live cells"); + expect(state, projected.live_items == layout.cells.len && + projected.nodes_added < 200 && tree.nodes.len == projected.nodes_added, + "Table semantic residency stays bounded by live layout output"); + + let target: VirtualTableCell = layout.cells.data[2]; + let target_id: NodeKey = semantic_table_cell_key(root_id, target.id); + let target_index: i32 = semantics_find_index(tree, target_id); + expect(state, target_index >= 0 && + tree.nodes.data[target_index].column_index == target.column_index as i64 && + tree.nodes.data[target_index].bounds.x == target.frame.x && + tree.nodes.data[target_index].has_bounds == 1, + "Table cells expose stable ID-first coordinates and exact layout geometry"); + expect(state, node_key_equal(target_id, semantic_table_cell_key(root_id, target.id)) == 1, + "Table cell semantic IDs are deterministic from authoritative composite identity"); + + let before: i32 = tree.nodes.len; + let duplicate: CollectionSemanticsResult = semantics_project_table( + &tree, layout, root_id, semantic_root_key(), "Parts" + ); + expect(state, duplicate.error == CollectionSemanticsError.identity_collision && + tree.nodes.len == before, + "Table identity collisions fail during preflight without partial tree mutation"); + + layout.cells.data[0].id.column_id = 999; + let malformed_tree: SemanticsTree = semantics_tree_make(); + let malformed: CollectionSemanticsResult = semantics_project_table( + &malformed_tree, layout, node_key(701), semantic_root_key(), "Malformed" + ); + expect(state, malformed.error == CollectionSemanticsError.invalid_result && + malformed.error_index == 0 && malformed_tree.nodes.len == 0, + "fabricated Table cell identity is rejected before semantic mutation"); + semantics_tree_free(&malformed_tree); + semantics_tree_free(&tree); + virtual_table_free(&layout); + free[VirtualTableColumn](&columns); + free[VirtualExtentOverride](&overrides); +} + +fn tree_source() ArrayList[VirtualTreeNode] { + let source: ArrayList[VirtualTreeNode] = make[VirtualTreeNode](5); + push[VirtualTreeNode](&source, virtual_tree_node(1, 0, 0, 1, 1, 12, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(2, 1, 1, 0, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(3, 1, 1, 1, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(4, 3, 2, 0, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(5, 0, 0, 0, 0, 10, 1)); + return source; +} + +fn tree_semantics_contract(state: *TestState) void { + let source: ArrayList[VirtualTreeNode] = tree_source(); + let projection: VirtualTreeProjection = virtual_tree_project(source, 1); + let style: VirtualTreeStyle = virtual_tree_style(10, 1, 4, insets_all(1)); + let layout: VirtualTreeResult = virtual_tree_layout(projection, size(100, 30), 0, style); + let tree: SemanticsTree = semantics_tree_make(); + let root_id: NodeKey = node_key_scoped(800, 4); + let projected: CollectionSemanticsResult = semantics_project_tree( + &tree, layout, root_id, semantic_root_key(), "Assembly" + ); + let root_index: i32 = semantics_find_index(tree, root_id); + expect(state, projected.error == CollectionSemanticsError.none && root_index >= 0 && + tree.nodes.data[root_index].row_count == layout.semantic_count && + projected.live_items == layout.placements.len, + "Tree semantics retain the full expanded projection count with bounded live nodes"); + + let expandable_id: NodeKey = semantic_tree_item_key(root_id, 3); + let expandable_index: i32 = semantics_find_index(tree, expandable_id); + expect(state, expandable_index >= 0 && + tree.nodes.data[expandable_index].level == 2 && + tree.nodes.data[expandable_index].expanded == 0 && + semantic_actions_has(tree.nodes.data[expandable_index].action_mask, + SemanticAction.expand) == 1, + "Tree items expose one-based level and truthful expansion action"); + let child_id: NodeKey = semantic_tree_item_key(root_id, 2); + let child_index: i32 = semantics_find_index(tree, child_id); + expect(state, child_index >= 0 && tree.nodes.data[child_index].position_in_set == 2 && + tree.nodes.data[child_index].bounds.x == layout.placements.data[1].frame.x, + "Tree semantic position and row bounds follow stable visible identity"); + + semantics_tree_free(&tree); + virtual_tree_free(&layout); + virtual_tree_projection_free(&projection); + free[VirtualTreeNode](&source); +} + +fn failure_contract(state: *TestState) void { + let tree: SemanticsTree = semantics_tree_make(); + let invalid: VirtualTreeResult = virtual_tree_result_error(VirtualTreeError.invalid_node, 4); + let result: CollectionSemanticsResult = semantics_project_tree( + &tree, invalid, node_key(90), semantic_root_key(), "Invalid" + ); + expect(state, result.error == CollectionSemanticsError.invalid_result && + result.error_index == 4 && tree.nodes.len == 0, + "invalid virtual layouts cannot leak partial semantic nodes"); + virtual_tree_free(&invalid); + semantics_tree_free(&tree); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + table_semantics_contract(&state); + tree_semantics_contract(&state); + failure_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Collection semantics contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/cpu_raster_contract.zag b/tests/cpu_raster_contract.zag new file mode 100644 index 0000000..292b041 --- /dev/null +++ b/tests/cpu_raster_contract.zag @@ -0,0 +1,418 @@ +@import("../src/render/cpu_raster.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn pixel(surface: CpuSurface, x: i32, y: i32, channel: i32) i64 { + return surface.pixels.data[(y * surface.width + x) * 4 + channel] as i64; +} + +fn path_scene(path: PathData, paint: Paint) DisplayList { + let list: DisplayList = display_list_make(); + let payload: PathEncodeResult = path_encode(path); + _ = display_list_add_resource(&list, + render_resource_spec(8, RenderResourceKind.path, + payload.bytes.data[0..payload.bytes.len], path_resource_format_tag())); + path_encode_free(&payload); + let draw: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(8)); + draw.resource_id = 8; + draw.paint = paint; + _ = display_list_push(&list, draw); + _ = display_list_seal(&list); + return list; +} + +fn image_scene(pixels: []u8, width: i64, height: i64, + bounds: Rect, color_space: RenderColorSpace, opacity: i64) DisplayList { + let list: DisplayList = display_list_make(); + let spec: RenderResourceSpec = render_resource_spec( + 18, RenderResourceKind.image, pixels, image_resource_format_tag() + ); + spec.width = width; + spec.height = height; + spec.color_space = color_space; + _ = display_list_add_resource(&list, spec); + let draw: DisplayOp = display_op(DisplayOpKind.draw_image, node_key(18)); + draw.resource_id = 18; + draw.bounds = bounds; + draw.paint.alpha = opacity; + _ = display_list_push(&list, draw); + _ = display_list_seal(&list); + return list; +} + +fn rgba_pixels_2x2() ArrayList[u8] { + let pixels: ArrayList[u8] = make[u8](16); + push[u8](&pixels, 255); push[u8](&pixels, 0); push[u8](&pixels, 0); push[u8](&pixels, 255); + push[u8](&pixels, 0); push[u8](&pixels, 255); push[u8](&pixels, 0); push[u8](&pixels, 255); + push[u8](&pixels, 0); push[u8](&pixels, 0); push[u8](&pixels, 255); push[u8](&pixels, 255); + push[u8](&pixels, 255); push[u8](&pixels, 255); push[u8](&pixels, 255); push[u8](&pixels, 0); + return pixels; +} + +fn square_path(fill_rule: PathFillRule, include_inner: i32) PathData { + let path: PathData = path_make(fill_rule); + _ = path_move_to(&path, path_point(0, 0)); + _ = path_line_to(&path, path_point(4 * unit_scale(), 0)); + _ = path_line_to(&path, path_point(4 * unit_scale(), 4 * unit_scale())); + _ = path_line_to(&path, path_point(0, 4 * unit_scale())); + _ = path_close(&path); + if (include_inner != 0) { + _ = path_move_to(&path, path_point(unit_scale(), unit_scale())); + _ = path_line_to(&path, path_point(3 * unit_scale(), unit_scale())); + _ = path_line_to(&path, path_point(3 * unit_scale(), 3 * unit_scale())); + _ = path_line_to(&path, path_point(unit_scale(), 3 * unit_scale())); + _ = path_close(&path); + } + _ = path_seal(&path); + return path; +} + +fn fill_scene(bounds: Rect, paint: Paint) DisplayList { + let list: DisplayList = display_list_make(); + let op: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(1)); + op.bounds = bounds; + op.paint = paint; + _ = display_list_push(&list, op); + _ = display_list_seal(&list); + return list; +} + +fn exact_fill_contract(state: *TestState) void { + let list: DisplayList = fill_scene(rect(0, 0, 2 * unit_scale(), 2 * unit_scale()), paint_rgba16(65535, 0, 0, 65535)); + let result: CpuRasterResult = cpu_rasterize(list, 3, 3); + expect(state, result.error == CpuRasterError.none && result.rendered_ops == 1, "sealed fill list rasterizes successfully"); + expect(state, pixel(result.surface, 0, 0, 0) == 255 && pixel(result.surface, 0, 0, 3) == 255, "opaque RGBA16 red resolves exactly to RGBA8"); + expect(state, pixel(result.surface, 1, 1, 0) == 255 && pixel(result.surface, 2, 2, 3) == 0, "fill bounds include only covered pixels"); + expect(state, result.pixels_touched == 4, "raster evidence counts touched pixels exactly"); + cpu_raster_free(&result); + display_list_free(&list); +} + +fn coverage_and_blend_contract(state: *TestState) void { + let half: DisplayList = fill_scene(rect(unit_scale() / 2, 0, unit_scale(), unit_scale()), paint_rgba16(65535, 0, 0, 65535)); + let edge: CpuRasterResult = cpu_rasterize(half, 2, 1); + expect(state, pixel(edge.surface, 0, 0, 0) == 255 && pixel(edge.surface, 0, 0, 3) == 128, "left fractional edge receives exact half coverage"); + expect(state, pixel(edge.surface, 1, 0, 0) == 255 && pixel(edge.surface, 1, 0, 3) == 128, "right fractional edge receives matching half coverage"); + cpu_raster_free(&edge); + display_list_free(&half); + + let blend: DisplayList = display_list_make(); + let blue: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(1)); + blue.bounds = rect(0, 0, unit_scale(), unit_scale()); + blue.paint = paint_rgba16(0, 0, 65535, 65535); + _ = display_list_push(&blend, blue); + let red: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(2)); + red.bounds = blue.bounds; + red.paint = paint_rgba16(65535, 0, 0, 32768); + _ = display_list_push(&blend, red); + _ = display_list_seal(&blend); + let blended: CpuRasterResult = cpu_rasterize(blend, 1, 1); + expect(state, pixel(blended.surface, 0, 0, 0) == 128 && pixel(blended.surface, 0, 0, 2) == 127 && pixel(blended.surface, 0, 0, 3) == 255, "source-over alpha is deterministic and rounded symmetrically"); + cpu_raster_free(&blended); + display_list_free(&blend); +} + +fn clip_and_transform_contract(state: *TestState) void { + let list: DisplayList = display_list_make(); + _ = display_list_push(&list, display_op(DisplayOpKind.save, node_key(1))); + let clip: DisplayOp = display_op(DisplayOpKind.clip_rect, node_key(1)); + clip.bounds = rect(unit_scale(), 0, 2 * unit_scale(), 2 * unit_scale()); + _ = display_list_push(&list, clip); + let transform: DisplayOp = display_op(DisplayOpKind.concat_transform, node_key(1)); + transform.transform.translate_x = unit_scale(); + _ = display_list_push(&list, transform); + let fill: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(1)); + fill.bounds = rect(0, 0, 3 * unit_scale(), 2 * unit_scale()); + fill.paint = paint_rgba16(0, 65535, 0, 65535); + _ = display_list_push(&list, fill); + _ = display_list_push(&list, display_op(DisplayOpKind.restore, node_key(1))); + _ = display_list_seal(&list); + let result: CpuRasterResult = cpu_rasterize(list, 4, 2); + expect(state, pixel(result.surface, 0, 0, 3) == 0 && pixel(result.surface, 1, 0, 1) == 255 && pixel(result.surface, 2, 1, 1) == 255 && pixel(result.surface, 3, 0, 3) == 0, "clip and translation compose in fixed-point logical space"); + expect(state, result.rendered_ops == 5 && result.pixels_touched == 4, "state operations and clipped work are counted exactly"); + cpu_raster_free(&result); + display_list_free(&list); +} + +fn deterministic_contract(state: *TestState) void { + let list: DisplayList = fill_scene(rect(10, 15, 3 * unit_scale(), 2 * unit_scale()), paint_rgba16(12000, 23000, 34000, 45000)); + let first: CpuRasterResult = cpu_rasterize(list, 5, 4); + let second: CpuRasterResult = cpu_rasterize(list, 5, 4); + expect(state, cpu_surface_hash(first.surface) == cpu_surface_hash(second.surface), "identical sealed lists produce identical CPU pixels"); + cpu_raster_free(&second); + cpu_raster_free(&first); + display_list_free(&list); +} + +fn stroke_rect_contract(state: *TestState) void { + let list: DisplayList = display_list_make(); + let stroke: DisplayOp = display_op(DisplayOpKind.stroke_rect, node_key(5)); + stroke.bounds = rect(unit_scale(), unit_scale(), + 4 * unit_scale(), 4 * unit_scale()); + stroke.paint = paint_rgba16(65535, 0, 65535, 65535); + stroke.paint.stroke_width = unit_scale(); + _ = display_list_push(&list, stroke); + _ = display_list_seal(&list); + let result: CpuRasterResult = cpu_rasterize(list, 6, 6); + expect(state, result.error == CpuRasterError.none && result.rendered_ops == 1 && + result.pixels_touched == 32, + "centered rectangle strokes rasterize as bounded analytic rings"); + expect(state, pixel(result.surface, 0, 2, 3) == 128 && + pixel(result.surface, 1, 2, 3) == 128 && + pixel(result.surface, 2, 2, 3) == 0, + "one logical pixel strokes split exact coverage across inner and outer edges"); + expect(state, pixel(result.surface, 0, 0, 3) == 64 && + pixel(result.surface, 1, 1, 3) == 191, + "stroke corners subtract inner area with deterministic rounding"); + cpu_raster_free(&result); + display_list_free(&list); +} + +fn image_contract(state: *TestState) void { + let pixels: ArrayList[u8] = rgba_pixels_2x2(); + let exact: DisplayList = image_scene(pixels.data[0..pixels.len], 2, 2, + rect(0, 0, 2 * unit_scale(), 2 * unit_scale()), + RenderColorSpace.srgb, 65535); + let exact_result: CpuRasterResult = cpu_rasterize(exact, 2, 2); + expect(state, exact_result.error == CpuRasterError.none && + exact_result.rendered_ops == 1 && exact_result.pixels_touched == 3, + "canonical decoded images render through the explicit draw-image operation"); + expect(state, pixel(exact_result.surface, 0, 0, 0) == 255 && + pixel(exact_result.surface, 1, 0, 1) == 255 && + pixel(exact_result.surface, 0, 1, 2) == 255 && + pixel(exact_result.surface, 1, 1, 3) == 0, + "one-to-one image rendering preserves exact RGBA8 texels including transparency"); + cpu_raster_free(&exact_result); + display_list_free(&exact); + free[u8](&pixels); + + let gradient: ArrayList[u8] = make[u8](8); + push[u8](&gradient, 255); push[u8](&gradient, 0); push[u8](&gradient, 0); push[u8](&gradient, 255); + push[u8](&gradient, 0); push[u8](&gradient, 0); push[u8](&gradient, 255); push[u8](&gradient, 255); + let scaled: DisplayList = image_scene(gradient.data[0..gradient.len], 2, 1, + rect(0, 0, 4 * unit_scale(), unit_scale()), RenderColorSpace.srgb, 65535); + let scaled_result: CpuRasterResult = cpu_rasterize(scaled, 4, 1); + expect(state, pixel(scaled_result.surface, 0, 0, 0) == 255 && + pixel(scaled_result.surface, 0, 0, 2) == 0 && + pixel(scaled_result.surface, 1, 0, 0) == 191 && + pixel(scaled_result.surface, 1, 0, 2) == 64 && + pixel(scaled_result.surface, 2, 0, 0) == 64 && + pixel(scaled_result.surface, 2, 0, 2) == 191 && + pixel(scaled_result.surface, 3, 0, 2) == 255, + "bilinear scaling is symmetric and aligns source and destination pixel centers"); + cpu_raster_free(&scaled_result); + display_list_free(&scaled); + free[u8](&gradient); + + let red: ArrayList[u8] = make[u8](4); + push[u8](&red, 255); push[u8](&red, 0); push[u8](&red, 0); push[u8](&red, 255); + let translucent: DisplayList = image_scene(red.data[0..red.len], 1, 1, + rect(unit_scale() / 2, 0, unit_scale(), unit_scale()), + RenderColorSpace.srgb, 32768); + let translucent_result: CpuRasterResult = cpu_rasterize(translucent, 2, 1); + expect(state, pixel(translucent_result.surface, 0, 0, 0) == 255 && + pixel(translucent_result.surface, 1, 0, 0) == 255 && + pixel(translucent_result.surface, 0, 0, 3) == 64 && + pixel(translucent_result.surface, 1, 0, 3) == 64, + "fractional image edges and operation opacity compose without dark fringes"); + cpu_raster_free(&translucent_result); + display_list_free(&translucent); + + let unsupported: DisplayList = image_scene(red.data[0..red.len], 1, 1, + rect(0, 0, unit_scale(), unit_scale()), RenderColorSpace.display_p3, 65535); + let unsupported_result: CpuRasterResult = cpu_rasterize(unsupported, 1, 1); + expect(state, unsupported_result.error == CpuRasterError.unsupported_operation && + unsupported_result.error_op == 0 && unsupported_result.pixels_touched == 0, + "unimplemented color conversion fails before image pixels are touched"); + cpu_raster_free(&unsupported_result); + display_list_free(&unsupported); + free[u8](&red); +} + +fn path_fill_contract(state: *TestState) void { + let triangle: PathData = path_make(PathFillRule.non_zero); + _ = path_move_to(&triangle, path_point(0, 0)); + _ = path_line_to(&triangle, path_point(2 * unit_scale(), 0)); + _ = path_line_to(&triangle, path_point(0, 2 * unit_scale())); + _ = path_close(&triangle); + _ = path_seal(&triangle); + let triangle_list: DisplayList = path_scene(triangle, + paint_rgba16(65535, 0, 0, 65535)); + let triangle_result: CpuRasterResult = cpu_rasterize(triangle_list, 3, 3); + expect(state, triangle_result.error == CpuRasterError.none && + triangle_result.rendered_ops == 1 && triangle_result.pixels_touched == 3, + "canonical line paths rasterize as one deterministic display operation"); + expect(state, pixel(triangle_result.surface, 0, 0, 3) == 255 && + pixel(triangle_result.surface, 1, 0, 3) == 112 && + pixel(triangle_result.surface, 0, 1, 3) == 112 && + pixel(triangle_result.surface, 1, 1, 3) == 0, + "8 by 8 path sampling produces exact symmetric diagonal coverage"); + cpu_raster_free(&triangle_result); + display_list_free(&triangle_list); + path_free(&triangle); + + let even_odd: PathData = square_path(PathFillRule.even_odd, 1); + let even_odd_list: DisplayList = path_scene(even_odd, + paint_rgba16(0, 65535, 0, 65535)); + let even_odd_result: CpuRasterResult = cpu_rasterize(even_odd_list, 4, 4); + expect(state, pixel(even_odd_result.surface, 0, 0, 1) == 255 && + pixel(even_odd_result.surface, 2, 2, 3) == 0, + "even-odd fill removes nested contours regardless of matching orientation"); + + let non_zero: PathData = square_path(PathFillRule.non_zero, 1); + let non_zero_list: DisplayList = path_scene(non_zero, + paint_rgba16(0, 0, 65535, 65535)); + let non_zero_result: CpuRasterResult = cpu_rasterize(non_zero_list, 4, 4); + expect(state, pixel(non_zero_result.surface, 2, 2, 2) == 255, + "non-zero fill retains nested contours with matching winding"); + cpu_raster_free(&non_zero_result); + display_list_free(&non_zero_list); + path_free(&non_zero); + cpu_raster_free(&even_odd_result); + display_list_free(&even_odd_list); + path_free(&even_odd); + + let curves: PathData = path_make(PathFillRule.non_zero); + _ = path_move_to(&curves, path_point(0, 3 * unit_scale())); + _ = path_quad_to(&curves, path_point(2 * unit_scale(), 0), + path_point(4 * unit_scale(), 3 * unit_scale())); + _ = path_cubic_to(&curves, path_point(3 * unit_scale(), 4 * unit_scale()), + path_point(unit_scale(), 4 * unit_scale()), path_point(0, 3 * unit_scale())); + _ = path_close(&curves); + _ = path_seal(&curves); + let curves_list: DisplayList = path_scene(curves, + paint_rgba16(40000, 30000, 20000, 65535)); + let first: CpuRasterResult = cpu_rasterize(curves_list, 5, 5); + let second: CpuRasterResult = cpu_rasterize(curves_list, 5, 5); + expect(state, first.error == CpuRasterError.none && first.pixels_touched > 0 && + cpu_surface_hash(first.surface) == cpu_surface_hash(second.surface), + "quadratic and cubic flattening produces stable nonempty CPU output"); + cpu_raster_free(&second); + cpu_raster_free(&first); + display_list_free(&curves_list); + path_free(&curves); +} + +fn path_work_limit_contract(state: *TestState) void { + let path: PathData = path_make(PathFillRule.non_zero); + _ = path_move_to(&path, path_point(0, 0)); + let i: i32 = 0; + while (i < 100) { + let y: i64 = 0; + if (i % 2 != 0) { y = 100 * unit_scale(); } + _ = path_line_to(&path, path_point((i as i64) * unit_scale(), y)); + i = i + 1; + } + _ = path_line_to(&path, path_point(100 * unit_scale(), 100 * unit_scale())); + _ = path_line_to(&path, path_point(0, 100 * unit_scale())); + _ = path_close(&path); + _ = path_seal(&path); + let list: DisplayList = path_scene(path, paint_rgba16(65535, 65535, 65535, 65535)); + let result: CpuRasterResult = cpu_rasterize(list, 100, 100); + expect(state, result.error == CpuRasterError.work_limit && result.error_op == 0 && + result.pixels_touched == 0, + "path raster work exceeding the deterministic budget fails before touching pixels"); + cpu_raster_free(&result); + display_list_free(&list); + path_free(&path); +} + +fn path_clip_transform_contract(state: *TestState) void { + let path: PathData = square_path(PathFillRule.non_zero, 0); + let payload: PathEncodeResult = path_encode(path); + let list: DisplayList = display_list_make(); + _ = display_list_add_resource(&list, + render_resource_spec(12, RenderResourceKind.path, + payload.bytes.data[0..payload.bytes.len], path_resource_format_tag())); + path_encode_free(&payload); + _ = display_list_push(&list, display_op(DisplayOpKind.save, node_key(12))); + let clip: DisplayOp = display_op(DisplayOpKind.clip_rect, node_key(12)); + clip.bounds = rect(2 * unit_scale(), 0, unit_scale(), 3 * unit_scale()); + _ = display_list_push(&list, clip); + let transform: DisplayOp = display_op(DisplayOpKind.concat_transform, node_key(12)); + transform.transform.translate_x = unit_scale(); + _ = display_list_push(&list, transform); + let draw: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(12)); + draw.resource_id = 12; + draw.paint = paint_rgba16(65535, 65535, 0, 65535); + _ = display_list_push(&list, draw); + _ = display_list_push(&list, display_op(DisplayOpKind.restore, node_key(12))); + _ = display_list_seal(&list); + let result: CpuRasterResult = cpu_rasterize(list, 5, 3); + expect(state, result.error == CpuRasterError.none && result.rendered_ops == 5 && + result.pixels_touched == 3, + "path rendering participates in the retained transform and clip stack"); + expect(state, pixel(result.surface, 1, 1, 3) == 0 && + pixel(result.surface, 2, 1, 0) == 255 && pixel(result.surface, 2, 1, 1) == 255 && + pixel(result.surface, 3, 1, 3) == 0, + "transformed path coverage is clipped at subpixel sample positions"); + cpu_raster_free(&result); + display_list_free(&list); + path_free(&path); +} + +fn failure_contract(state: *TestState) void { + let unsealed: DisplayList = display_list_make(); + let unsealed_result: CpuRasterResult = cpu_rasterize(unsealed, 2, 2); + expect(state, unsealed_result.error == CpuRasterError.unsealed_display_list, "unsealed display lists cannot reach the CPU oracle"); + cpu_raster_free(&unsealed_result); + display_list_free(&unsealed); + + let unsupported: DisplayList = display_list_make(); + let layer: DisplayOp = display_op(DisplayOpKind.begin_layer, node_key(1)); + layer.bounds = rect(0, 0, unit_scale(), unit_scale()); + layer.parameter = 65535; + _ = display_list_push(&unsupported, layer); + _ = display_list_push(&unsupported, display_op(DisplayOpKind.end_layer, node_key(1))); + _ = display_list_seal(&unsupported); + let unsupported_result: CpuRasterResult = cpu_rasterize(unsupported, 2, 2); + expect(state, unsupported_result.error == CpuRasterError.unsupported_operation && + unsupported_result.error_op == 0, + "unimplemented layer compositing fails closed at the exact operation"); + cpu_raster_free(&unsupported_result); + display_list_free(&unsupported); + + let invalid_surface: DisplayList = fill_scene(rect(0, 0, 1, 1), paint_rgba16(0, 0, 0, 0)); + let invalid_result: CpuRasterResult = cpu_rasterize(invalid_surface, 0, 10); + expect(state, invalid_result.error == CpuRasterError.invalid_surface, "invalid surface dimensions fail without allocation claims"); + cpu_raster_free(&invalid_result); + display_list_free(&invalid_surface); + + let extreme: DisplayList = fill_scene(rect(1000000000001, 0, 1, 1), paint_rgba16(0, 0, 0, 65535)); + let extreme_result: CpuRasterResult = cpu_rasterize(extreme, 2, 2); + expect(state, extreme_result.error == CpuRasterError.invalid_geometry && extreme_result.error_op == 0, "unsafe transform arithmetic is rejected before multiplication"); + cpu_raster_free(&extreme_result); + display_list_free(&extreme); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + exact_fill_contract(&state); + coverage_and_blend_contract(&state); + clip_and_transform_contract(&state); + deterministic_contract(&state); + stroke_rect_contract(&state); + image_contract(&state); + path_fill_contract(&state); + path_work_limit_contract(&state); + path_clip_transform_contract(&state); + failure_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("CPU raster contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/display_list_codec_contract.zag b/tests/display_list_codec_contract.zag new file mode 100644 index 0000000..29677dd --- /dev/null +++ b/tests/display_list_codec_contract.zag @@ -0,0 +1,280 @@ +@import("../src/render/display_list_codec.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn codec_path_payload(delta: i64) PathEncodeResult { + let path: PathData = path_make(PathFillRule.even_odd); + _ = path_move_to(&path, path_point(0 - unit_scale(), 0)); + _ = path_line_to(&path, path_point(4 * unit_scale() + delta, 0)); + _ = path_line_to(&path, path_point(0, 4 * unit_scale())); + _ = path_close(&path); + _ = path_seal(&path); + let encoded: PathEncodeResult = path_encode(path); + path_free(&path); + return encoded; +} + +fn codec_scene() DisplayList { + let list: DisplayList = display_list_make(); + let owner: NodeKey = node_key_scoped(42, 3); + let initial_payload: PathEncodeResult = codec_path_payload(0); + _ = display_list_add_resource(&list, + render_resource_spec(50, RenderResourceKind.path, + initial_payload.bytes.data[0..initial_payload.bytes.len], path_resource_format_tag())); + path_encode_free(&initial_payload); + let replacement_payload: PathEncodeResult = codec_path_payload(unit_scale()); + let replacement: RenderResourceSpec = render_resource_spec( + 50, RenderResourceKind.path, + replacement_payload.bytes.data[0..replacement_payload.bytes.len], path_resource_format_tag() + ); + replacement.revision = 1; + _ = display_list_replace_resource(&list, replacement); + path_encode_free(&replacement_payload); + _ = display_list_push(&list, display_op(DisplayOpKind.save, owner)); + let clip: DisplayOp = display_op(DisplayOpKind.clip_rect, owner); + clip.bounds = rect(0 - unit_scale(), 2 * unit_scale(), 80 * unit_scale(), 40 * unit_scale()); + _ = display_list_push(&list, clip); + let fill: DisplayOp = display_op(DisplayOpKind.fill_rect, owner); + fill.bounds = rect(3 * unit_scale(), 4 * unit_scale(), 20 * unit_scale(), 10 * unit_scale()); + fill.paint = paint_rgba16(1234, 2345, 3456, 4567); + _ = display_list_push(&list, fill); + let path: DisplayOp = display_op(DisplayOpKind.draw_path, owner); + path.resource_id = 50; + _ = display_list_push(&list, path); + _ = display_list_push(&list, display_op(DisplayOpKind.restore, owner)); + _ = display_list_seal(&list); + return list; +} + +fn bytes_equal(a: ArrayList[u8], b: ArrayList[u8]) i32 { + if (a.len != b.len) { return 0; } + let i: i32 = 0; + while (i < a.len) { + if (a.data[i] != b.data[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn round_trip_contract(state: *TestState) void { + let source: DisplayList = codec_scene(); + let encoded: DisplayEncodeResult = display_list_encode(source); + expect(state, encoded.error == DisplayCodecError.none && encoded.bytes.len == + 76 + 64 + source.resources.resources.data[0].payload.len + 5 * 160, + "sealed display list encodes resource records payload bytes and operations to the exact size"); + expect(state, encoded.bytes.data[0] == 90 && encoded.bytes.data[1] == 75 && encoded.bytes.data[2] == 68 && encoded.bytes.data[3] == 76, "codec carries the ZKDL magic"); + + let decoded: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, decoded.error == DisplayCodecError.none && decoded.list.sealed == 1, "valid bytes decode to a sealed display list"); + expect(state, decoded.list.content_hash == source.content_hash && decoded.list.revision == source.revision, "round trip preserves deterministic identity"); + expect(state, decoded.list.ops.data[2].owner.value == 42 && decoded.list.ops.data[2].owner.generation == 3, "round trip preserves retained ownership"); + expect(state, decoded.list.ops.data[1].bounds.x == 0 - unit_scale() && decoded.list.ops.data[2].paint.alpha == 4567, "round trip preserves signed geometry and RGBA16 paint"); + expect(state, decoded.list.resources.resources.len == 1 && + decoded.list.resources.resources.data[0].id == 50 && + decoded.list.resources.resources.data[0].revision == 1 && + decoded.list.resources.resources.data[0].payload.len == + source.resources.resources.data[0].payload.len && + decoded.list.resources.resources.data[0].format_tag == path_resource_format_tag() && + decoded.list.resources.revision == 2, + "round trip preserves replacement revision metadata and exact owned payload bytes"); + + let encoded_again: DisplayEncodeResult = display_list_encode(decoded.list); + expect(state, bytes_equal(encoded.bytes, encoded_again.bytes) == 1, "decode and re-encode are byte identical"); + display_encode_free(&encoded_again); + display_decode_free(&decoded); + display_encode_free(&encoded); + display_list_free(&source); +} + +fn malformed_contract(state: *TestState) void { + let source: DisplayList = codec_scene(); + let encoded: DisplayEncodeResult = display_list_encode(source); + + let saved: u8 = encoded.bytes.data[0]; + encoded.bytes.data[0] = 0; + let bad_magic: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_magic.error == DisplayCodecError.bad_magic && bad_magic.error_offset == 0, "bad magic fails at byte zero"); + display_decode_free(&bad_magic); + encoded.bytes.data[0] = saved; + + let saved_version: u8 = encoded.bytes.data[4]; + encoded.bytes.data[4] = 3; + let bad_version: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_version.error == DisplayCodecError.unsupported_version && bad_version.error_offset == 4, "unsupported versions fail visibly"); + display_decode_free(&bad_version); + encoded.bytes.data[4] = saved_version; + + let operation_offset: i32 = 76 + 64 + source.resources.resources.data[0].payload.len; + let saved_kind: u8 = encoded.bytes.data[operation_offset]; + encoded.bytes.data[operation_offset] = 99; + let bad_kind: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_kind.error == DisplayCodecError.invalid_kind && + bad_kind.error_offset == operation_offset as i64, + "unknown operation kinds fail at the exact record"); + display_decode_free(&bad_kind); + encoded.bytes.data[operation_offset] = saved_kind; + + let paint_offset: i32 = operation_offset + 2 * 160 + 56; + let saved_paint: u8 = encoded.bytes.data[paint_offset]; + encoded.bytes.data[paint_offset] = saved_paint + 1; + let bad_hash: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_hash.error == DisplayCodecError.hash_mismatch, "content tampering fails deterministic identity verification"); + display_decode_free(&bad_hash); + encoded.bytes.data[paint_offset] = saved_paint; + + let saved_resource_kind: u8 = encoded.bytes.data[76 + 8]; + encoded.bytes.data[76 + 8] = 99; + let bad_resource: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_resource.error == DisplayCodecError.invalid_resource && + bad_resource.error_offset == 76, + "unknown resource kinds fail at the exact resource record"); + display_decode_free(&bad_resource); + encoded.bytes.data[76 + 8] = saved_resource_kind; + + let saved_valid_kind: u8 = encoded.bytes.data[76 + 8]; + encoded.bytes.data[76 + 8] = RenderResourceKind.effect as u8; + let mismatched_resource: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, mismatched_resource.error == DisplayCodecError.invalid_operation, + "decoded resource kind mismatch fails before the referenced operation can seal"); + display_decode_free(&mismatched_resource); + encoded.bytes.data[76 + 8] = saved_valid_kind; + + let saved_format: u8 = encoded.bytes.data[76 + 24]; + encoded.bytes.data[76 + 24] = 0; + let bad_format: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_format.error == DisplayCodecError.invalid_resource && + bad_format.error_offset == 76, + "invalid resource metadata fails at its record before payload ownership"); + display_decode_free(&bad_format); + encoded.bytes.data[76 + 24] = saved_format; + + let saved_length: u8 = encoded.bytes.data[76 + 56]; + encoded.bytes.data[76 + 56] = 100; + let payload_bomb: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, payload_bomb.error == DisplayCodecError.invalid_payload && + payload_bomb.error_offset == 76 + 56, + "payload lengths cannot cross the declared bounded resource section"); + display_decode_free(&payload_bomb); + encoded.bytes.data[76 + 56] = saved_length; + + let saved_configuration: u8 = encoded.bytes.data[53]; + encoded.bytes.data[53] = 0; + let bad_configuration: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_configuration.error == DisplayCodecError.invalid_configuration && + bad_configuration.error_offset == 52, + "serialized allocation policy must remain within hard decoder limits"); + display_decode_free(&bad_configuration); + encoded.bytes.data[53] = saved_configuration; + + let saved_revision: u8 = encoded.bytes.data[44]; + encoded.bytes.data[44] = saved_revision - 1; + let bad_revision: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_revision.error == DisplayCodecError.invalid_operation, + "resource history revisions must reconstruct exactly before sealing"); + display_decode_free(&bad_revision); + encoded.bytes.data[44] = saved_revision; + + let reference_offset: i32 = operation_offset + 3 * 160 + 144; + let saved_reference: u8 = encoded.bytes.data[reference_offset]; + encoded.bytes.data[reference_offset] = 99; + let missing_reference: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, missing_reference.error == DisplayCodecError.invalid_operation, + "decoded operations cannot reference absent resource IDs"); + display_decode_free(&missing_reference); + encoded.bytes.data[reference_offset] = saved_reference; + + let saved_payload: u8 = encoded.bytes.data[76 + 64 + 8]; + encoded.bytes.data[76 + 64 + 8] = saved_payload + 1; + let bad_payload_hash: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, bad_payload_hash.error == DisplayCodecError.invalid_payload && + bad_payload_hash.error_offset == 76 + 64, + "malformed typed path payloads fail at their resource payload before operation decoding"); + display_decode_free(&bad_payload_hash); + encoded.bytes.data[76 + 64 + 8] = saved_payload; + + let truncated: ArrayList[u8] = make[u8](20); + let i: i32 = 0; + while (i < 20) { push[u8](&truncated, encoded.bytes.data[i]); i = i + 1; } + let short_result: DisplayDecodeResult = display_list_decode(truncated); + expect(state, short_result.error == DisplayCodecError.truncated && short_result.error_offset == 20, "truncated headers fail with the observed length"); + display_decode_free(&short_result); + free[u8](&truncated); + + push[u8](&encoded.bytes, 0); + let trailing: DisplayDecodeResult = display_list_decode(encoded.bytes); + expect(state, trailing.error == DisplayCodecError.trailing_data, "trailing bytes fail canonical decoding"); + display_decode_free(&trailing); + + display_encode_free(&encoded); + display_list_free(&source); +} + +fn encode_rejection_contract(state: *TestState) void { + let unsealed: DisplayList = display_list_make(); + let result: DisplayEncodeResult = display_list_encode(unsealed); + expect(state, result.error == DisplayCodecError.not_sealed && result.bytes.len == 0, "unsealed builders cannot be serialized as immutable replay input"); + display_encode_free(&result); + display_list_free(&unsealed); +} + +fn resource_payload_sweep(state: *TestState) void { + let stable: i32 = 1; + let payload_length: i32 = 1; + while (payload_length <= 64) { + let payload: ArrayList[u8] = make[u8](payload_length); + let i: i32 = 0; + while (i < payload_length) { + push[u8](&payload, ((i * 17 + payload_length) % 251) as u8); + i = i + 1; + } + let list: DisplayList = display_list_make_with_resource_limits(4, 256, 128); + if (display_list_add_resource(&list, render_resource_spec( + 70, RenderResourceKind.effect, payload.data[0..payload.len], 9 + )) != RenderResourceError.none) { stable = 0; } + if (display_list_seal(&list) != DisplayError.none) { stable = 0; } + let encoded: DisplayEncodeResult = display_list_encode(list); + let decoded: DisplayDecodeResult = display_list_decode(encoded.bytes); + if (encoded.error != DisplayCodecError.none || + decoded.error != DisplayCodecError.none || + decoded.list.resources.resources.len != 1 || + decoded.list.resources.resources.data[0].payload.len != payload_length || + decoded.list.content_hash != list.content_hash) { stable = 0; } + let encoded_again: DisplayEncodeResult = display_list_encode(decoded.list); + if (encoded_again.error != DisplayCodecError.none || + bytes_equal(encoded.bytes, encoded_again.bytes) == 0) { stable = 0; } + display_encode_free(&encoded_again); + display_decode_free(&decoded); + display_encode_free(&encoded); + display_list_free(&list); + free[u8](&payload); + payload_length = payload_length + 1; + } + expect(state, stable, + "64 payload lengths preserve canonical resource bytes through decode and re-encode"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + round_trip_contract(&state); + malformed_contract(&state); + encode_rejection_contract(&state); + resource_payload_sweep(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Display-list codec contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/display_list_contract.zag b/tests/display_list_contract.zag new file mode 100644 index 0000000..646cc9f --- /dev/null +++ b/tests/display_list_contract.zag @@ -0,0 +1,240 @@ +@import("../src/render/display_list.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn scene_path_payload(delta: i64) PathEncodeResult { + let path: PathData = path_make(PathFillRule.non_zero); + _ = path_move_to(&path, path_point(0, 0)); + _ = path_line_to(&path, path_point(10 * unit_scale() + delta, 0)); + _ = path_line_to(&path, path_point(5 * unit_scale(), 8 * unit_scale())); + _ = path_close(&path); + _ = path_seal(&path); + let encoded: PathEncodeResult = path_encode(path); + path_free(&path); + return encoded; +} + +fn build_scene(color: i64) DisplayList { + let list: DisplayList = display_list_make(); + let owner: NodeKey = node_key(7); + let path_payload: PathEncodeResult = scene_path_payload(0); + _ = display_list_add_resource(&list, + render_resource_spec(101, RenderResourceKind.path, + path_payload.bytes.data[0..path_payload.bytes.len], path_resource_format_tag())); + path_encode_free(&path_payload); + let image_resource: RenderResourceSpec = render_resource_spec( + 202, RenderResourceKind.image, "RGBA", image_resource_format_tag() + ); + image_resource.width = 1; image_resource.height = 1; + image_resource.color_space = RenderColorSpace.srgb; + _ = display_list_add_resource(&list, image_resource); + _ = display_list_add_resource(&list, + render_resource_spec(303, RenderResourceKind.glyph_run, "GLYPHS", 3)); + _ = display_list_add_resource(&list, + render_resource_spec(404, RenderResourceKind.effect, "EFFECT", 4)); + _ = display_list_push(&list, display_op(DisplayOpKind.save, owner)); + + let clip: DisplayOp = display_op(DisplayOpKind.clip_rect, owner); + clip.bounds = rect(0, 0, 200 * unit_scale(), 100 * unit_scale()); + _ = display_list_push(&list, clip); + + let transform: DisplayOp = display_op(DisplayOpKind.concat_transform, owner); + transform.transform.translate_x = 4 * unit_scale(); + transform.transform.translate_y = 8 * unit_scale(); + _ = display_list_push(&list, transform); + + let fill: DisplayOp = display_op(DisplayOpKind.fill_rect, owner); + fill.bounds = rect(0 - unit_scale(), 0, 80 * unit_scale(), 40 * unit_scale()); + fill.paint = paint_rgba16(color, 20000, 30000, 65535); + _ = display_list_push(&list, fill); + + let path: DisplayOp = display_op(DisplayOpKind.draw_path, owner); + path.resource_id = 101; + path.paint = paint_rgba16(65535, 65535, 65535, 50000); + _ = display_list_push(&list, path); + + let image: DisplayOp = display_op(DisplayOpKind.draw_image, owner); + image.resource_id = 202; + image.bounds = rect(10, 20, 32 * unit_scale(), 24 * unit_scale()); + _ = display_list_push(&list, image); + + let glyphs: DisplayOp = display_op(DisplayOpKind.draw_glyph_run, owner); + glyphs.resource_id = 303; + _ = display_list_push(&list, glyphs); + + let layer: DisplayOp = display_op(DisplayOpKind.begin_layer, owner); + layer.bounds = rect(0, 0, 100 * unit_scale(), 50 * unit_scale()); + layer.parameter = 50000; + _ = display_list_push(&list, layer); + + let effect: DisplayOp = display_op(DisplayOpKind.apply_effect, owner); + effect.resource_id = 404; + effect.parameter = 12 * unit_scale(); + _ = display_list_push(&list, effect); + _ = display_list_push(&list, display_op(DisplayOpKind.end_layer, owner)); + _ = display_list_push(&list, display_op(DisplayOpKind.restore, owner)); + return list; +} + +fn scene_contract(state: *TestState) void { + let first: DisplayList = build_scene(10000); + let second: DisplayList = build_scene(10000); + let changed: DisplayList = build_scene(10001); + expect(state, first.ops.len == 11 && first.stack_depth == 0 && first.layer_depth == 0, "display list records a balanced scene"); + expect(state, first.resources.resources.len == 4 && first.revision == 15, + "display list owns every referenced resource and includes resource revisions"); + expect(state, first.ops.data[4].kind == DisplayOpKind.draw_path && first.ops.data[5].kind == DisplayOpKind.draw_image && first.ops.data[6].kind == DisplayOpKind.draw_glyph_run, "paths, images, and glyph runs remain explicit operations"); + expect(state, first.ops.data[7].kind == DisplayOpKind.begin_layer && first.ops.data[8].kind == DisplayOpKind.apply_effect, "layers and effects remain explicit operations"); + expect(state, first.content_hash == second.content_hash, "identical scenes produce the same deterministic content hash"); + expect(state, first.content_hash != changed.content_hash, "one paint-channel change alters content identity"); + + let old_hash: i64 = first.content_hash; + let old_len: i32 = first.ops.len; + expect(state, display_list_seal(&first) == DisplayError.none && first.sealed == 1, "balanced display lists seal immutably"); + expect(state, display_list_verify(first) == DisplayError.none, "sealed content verifies against its deterministic identity"); + expect(state, display_list_push(&first, display_op(DisplayOpKind.save, node_key(7))) == DisplayError.sealed, "sealed display lists reject mutation"); + expect(state, first.ops.len == old_len && first.content_hash == old_hash, "rejected mutation preserves list content and identity"); + first.ops.data[3].paint.red = first.ops.data[3].paint.red + 1; + expect(state, display_list_verify(first) == DisplayError.content_mutated, "out-of-contract raw mutation is detected before rendering"); + first.ops.data[3].paint.red = first.ops.data[3].paint.red - 1; + expect(state, display_list_verify(first) == DisplayError.none, "restored sealed content verifies again"); + first.resources.resources.data[0].payload.data[0] = + first.resources.resources.data[0].payload.data[0] + 1; + expect(state, display_list_verify(first) == DisplayError.content_mutated, + "display verification detects owned resource payload mutation"); + first.resources.resources.data[0].payload.data[0] = + first.resources.resources.data[0].payload.data[0] - 1; + expect(state, display_list_verify(first) == DisplayError.none, + "restored resource bytes restore complete display identity"); + + display_list_free(&changed); + display_list_free(&second); + display_list_free(&first); +} + +fn rejection_contract(state: *TestState) void { + let list: DisplayList = display_list_make(); + expect(state, display_list_verify(list) == DisplayError.not_sealed, "unsealed builders cannot claim immutable verification"); + expect(state, display_list_push(&list, display_op(DisplayOpKind.restore, node_key(1))) == DisplayError.stack_underflow && list.ops.len == 0, "restore underflow fails before mutation"); + expect(state, display_list_push(&list, display_op(DisplayOpKind.end_layer, node_key(1))) == DisplayError.layer_underflow && list.ops.len == 0, "layer underflow fails before mutation"); + + let invalid_rect: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(1)); + invalid_rect.bounds = rect(0, 0, 0, 10); + expect(state, display_list_push(&list, invalid_rect) == DisplayError.invalid_geometry, "empty draw geometry fails visibly"); + + let missing_image: DisplayOp = display_op(DisplayOpKind.draw_image, node_key(1)); + missing_image.bounds = rect(0, 0, 10, 10); + expect(state, display_list_push(&list, missing_image) == DisplayError.invalid_resource, "resource operations require explicit identity"); + + let bad_layer: DisplayOp = display_op(DisplayOpKind.begin_layer, node_key(1)); + bad_layer.bounds = rect(0, 0, 10, 10); + bad_layer.parameter = 70000; + expect(state, display_list_push(&list, bad_layer) == DisplayError.invalid_parameter, "layer opacity outside RGBA16 range is rejected"); + + let bad_paint: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(1)); + bad_paint.bounds = rect(0, 0, 10, 10); + bad_paint.paint.red = 65536; + expect(state, display_list_push(&list, bad_paint) == DisplayError.invalid_parameter, "raw paint channels outside RGBA16 fail before mutation"); + + let zero_stroke: DisplayOp = display_op(DisplayOpKind.stroke_rect, node_key(1)); + zero_stroke.bounds = rect(0, 0, 10, 10); + zero_stroke.paint.stroke_width = 0; + expect(state, display_list_push(&list, zero_stroke) == DisplayError.invalid_parameter, + "rectangle strokes require a positive logical width"); + + let tinted_image: DisplayOp = display_op(DisplayOpKind.draw_image, node_key(1)); + tinted_image.bounds = rect(0, 0, 10, 10); + tinted_image.resource_id = 1; + tinted_image.paint.red = 1; + expect(state, display_list_push(&list, tinted_image) == DisplayError.invalid_parameter, + "draw-image paint keeps RGB and stroke fields canonical while alpha owns opacity"); + + _ = display_list_push(&list, display_op(DisplayOpKind.save, node_key(1))); + expect(state, display_list_seal(&list) == DisplayError.unbalanced_stack && list.sealed == 0, "unbalanced save state cannot seal"); + _ = display_list_push(&list, display_op(DisplayOpKind.restore, node_key(1))); + expect(state, display_list_seal(&list) == DisplayError.none && list.sealed == 1, "a corrected stack seals without rebuilding the list"); + display_list_free(&list); + + let missing: DisplayList = display_list_make(); + let missing_path: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(2)); + missing_path.resource_id = 99; + _ = display_list_push(&missing, missing_path); + expect(state, display_list_seal(&missing) == DisplayError.invalid_resource && + missing.error_index == 0 && missing.sealed == 0, + "sealing fails at the exact missing resource reference"); + display_list_free(&missing); + + let mismatched: DisplayList = display_list_make(); + _ = display_list_add_resource(&mismatched, + render_resource_spec(5, RenderResourceKind.effect, "FX", 1)); + let wrong_path: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(3)); + wrong_path.resource_id = 5; + _ = display_list_push(&mismatched, wrong_path); + expect(state, display_list_seal(&mismatched) == DisplayError.invalid_resource && + mismatched.error_index == 0, + "resource kind mismatches cannot masquerade as drawable payloads"); + display_list_free(&mismatched); + + let malformed: DisplayList = display_list_make(); + _ = display_list_add_resource(&malformed, + render_resource_spec(6, RenderResourceKind.path, "NOT-A-PATH", path_resource_format_tag())); + let malformed_path: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(4)); + malformed_path.resource_id = 6; + _ = display_list_push(&malformed, malformed_path); + expect(state, display_list_seal(&malformed) == DisplayError.invalid_resource && + malformed.error_index == 0 - 1, + "canonical path validation rejects opaque owned bytes before operation validation"); + display_list_free(&malformed); + + let malformed_image: DisplayList = display_list_make(); + let malformed_image_spec: RenderResourceSpec = render_resource_spec( + 7, RenderResourceKind.image, "RGB", image_resource_format_tag() + ); + malformed_image_spec.width = 1; + malformed_image_spec.height = 1; + malformed_image_spec.color_space = RenderColorSpace.srgb; + _ = display_list_add_resource(&malformed_image, malformed_image_spec); + expect(state, display_list_seal(&malformed_image) == DisplayError.invalid_resource && + malformed_image.error_index == 0 - 1, + "decoded image byte counts are validated before operation references"); + display_list_free(&malformed_image); +} + +fn deterministic_sweep(state: *TestState) void { + let expected_hash: i64 = 0; + let stable: i32 = 1; + let i: i32 = 0; + while (i < 50) { + let list: DisplayList = build_scene(12000); + if (i == 0) { expected_hash = list.content_hash; } + if (list.content_hash != expected_hash) { stable = 0; } + display_list_free(&list); + i = i + 1; + } + expect(state, stable, "50-build sweep preserves deterministic operation identity"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + scene_contract(&state); + rejection_contract(&state); + deterministic_sweep(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Display-list contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/flex_adaptive_contract.zag b/tests/flex_adaptive_contract.zag new file mode 100644 index 0000000..74730c8 --- /dev/null +++ b/tests/flex_adaptive_contract.zag @@ -0,0 +1,320 @@ +@import("../src/layout/flex_wrap.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn report_i64(label: []u8, value: i64) void { + let text: []u8 = _zag_i64_to_str(value); + _zag_print(label); _zag_println(text); _zag_str_free(text); +} + +fn wrap_items(count: i32, main: i64, cross: i64) ArrayList[FlexItem] { + let items: ArrayList[FlexItem] = make[FlexItem](count); + let i: i32 = 0; + while (i < count) { + push[FlexItem](&items, flex_item((i + 1) as i64, main, cross)); + i = i + 1; + } + return items; +} + +fn adaptive_contract(state: *TestState) void { + expect(state, flex_spacing(FlexSpacingToken.small, FlexDensity.standard) == 8 * unit_scale() && + flex_spacing(FlexSpacingToken.small, FlexDensity.compact) == 6 * unit_scale() && + flex_spacing(FlexSpacingToken.small, FlexDensity.touch) == 10 * unit_scale(), + "semantic spacing resolves to exact density-aware units"); + expect(state, flex_spacing(FlexSpacingToken.none, FlexDensity.touch) == 0 && + flex_spacing(FlexSpacingToken.xxlarge, FlexDensity.standard) == 32 * unit_scale(), + "spacing scale keeps explicit zero and largest token stable"); + + let context: FlexAdaptiveContext = flex_adaptive_context(size(800 * unit_scale(), 600 * unit_scale())); + context.density = FlexDensity.touch; + context.direction = LayoutDirection.rtl; + context.safe_area = Insets{ + .top = unit_scale(), .right = 2 * unit_scale(), + .bottom = 3 * unit_scale(), .left = 4 * unit_scale(), + }; + let style: FlexStyle = flex_adaptive_style( + FlexAxis.row, FlexSpacingToken.small, FlexSpacingToken.medium, context, 1 + ); + expect(state, style.gap == 10 * unit_scale() && style.direction == LayoutDirection.rtl && + style.padding.top == 16 * unit_scale() && style.padding.right == 17 * unit_scale() && + style.padding.bottom == 18 * unit_scale() && style.padding.left == 19 * unit_scale(), + "adaptive style composes token padding and physical safe areas exactly"); + + let compact: FlexAdaptiveContext = flex_adaptive_context(size(599 * unit_scale(), 800 * unit_scale())); + let medium: FlexAdaptiveContext = flex_adaptive_context(size(600 * unit_scale(), 800 * unit_scale())); + let expanded: FlexAdaptiveContext = flex_adaptive_context(size(1024 * unit_scale(), 800 * unit_scale())); + expect(state, flex_breakpoint(compact) == FlexBreakpoint.compact && + flex_breakpoint(medium) == FlexBreakpoint.medium && + flex_breakpoint(expanded) == FlexBreakpoint.expanded, + "breakpoint boundaries are exact in density-independent content width"); + medium.text_scale = 2 * unit_scale(); + expect(state, flex_breakpoint(medium) == FlexBreakpoint.compact, + "large text moves the same viewport to a safer compact composition"); + expanded.safe_area.left = 250 * unit_scale(); + expanded.safe_area.right = 250 * unit_scale(); + expect(state, flex_breakpoint(expanded) == FlexBreakpoint.compact && + flex_content_size(expanded).width == 524 * unit_scale(), + "safe areas participate in breakpoint truth rather than clipping afterward"); + expanded.safe_area.left = unbounded_extent(); + expanded.safe_area.right = unbounded_extent(); + expect(state, flex_content_size(expanded).width == 0, + "extreme platform insets clamp before content arithmetic"); + let extreme_style: FlexStyle = flex_adaptive_style( + FlexAxis.row, FlexSpacingToken.small, FlexSpacingToken.medium, expanded, 1 + ); + expect(state, extreme_style.padding.left == unbounded_extent() / 4 && + extreme_style.padding.right == unbounded_extent() / 4, + "token padding cannot push normalized safe areas outside bounded layout geometry"); +} + +fn row_wrap_contract(state: *TestState) void { + let items: ArrayList[FlexItem] = wrap_items(5, 3 * unit_scale(), 2 * unit_scale()); + let base: FlexStyle = flex_style(FlexAxis.row, unit_scale(), insets_all(unit_scale())); + let style: FlexWrapStyle = flex_wrap_style(base, unit_scale()); + let result: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 10 * unit_scale()), style + ); + expect(state, result.lines.len == 3 && result.lines.data[0].count == 2 && + result.lines.data[1].count == 2 && result.lines.data[2].count == 1 && result.wrapped == 1, + "row wrapping creates deterministic stable-ID line ranges"); + expect(state, result.placements.data[0].frame.x == unit_scale() && + result.placements.data[1].frame.x == 5 * unit_scale() && + result.placements.data[2].frame.x == unit_scale(), + "item gaps and line reset positions stay exact"); + expect(state, result.placements.data[0].frame.y == unit_scale() && + result.placements.data[2].frame.y == 4 * unit_scale() && + result.placements.data[4].frame.y == 7 * unit_scale(), + "line gaps and outer padding stay exact"); + expect(state, result.overflow_main == 0 && result.overflow_cross == 0 && + result.duplicate_ids == 0 && result.layout_hash != 0, + "wrapped layout reports clean overflow and deterministic identity"); + + let repeated: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 10 * unit_scale()), style + ); + expect(state, repeated.layout_hash == result.layout_hash, + "identical wrap inputs produce identical placement identity"); + flex_wrap_free(&repeated); + flex_wrap_free(&result); + free[FlexItem](&items); +} + +fn rtl_and_line_alignment_contract(state: *TestState) void { + let items: ArrayList[FlexItem] = wrap_items(5, 3 * unit_scale(), 2 * unit_scale()); + let base: FlexStyle = flex_style(FlexAxis.row, unit_scale(), insets_all(unit_scale())); + base.direction = LayoutDirection.rtl; + let style: FlexWrapStyle = flex_wrap_style(base, unit_scale()); + style.line_alignment = FlexLineAlignment.center; + let result: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 14 * unit_scale()), style + ); + expect(state, result.placements.data[0].frame.x == 6 * unit_scale() && + result.placements.data[1].frame.x == 2 * unit_scale(), + "RTL wrap reverses physical placement while preserving logical item order"); + expect(state, result.lines.data[0].cross_offset == 3 * unit_scale() && + result.placements.data[0].frame.y == 3 * unit_scale(), + "center line alignment distributes cross-axis room exactly"); + flex_wrap_free(&result); + + style.line_alignment = FlexLineAlignment.stretch; + let stretched: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 13 * unit_scale()), style + ); + expect(state, stretched.lines.data[0].cross_extent == 3 * unit_scale() && + stretched.lines.data[1].cross_offset == 5 * unit_scale() && + stretched.lines.data[2].cross_offset == 9 * unit_scale(), + "line stretching distributes the exact remainder without changing item size"); + flex_wrap_free(&stretched); + free[FlexItem](&items); +} + +fn column_wrap_contract(state: *TestState) void { + let items: ArrayList[FlexItem] = wrap_items(5, 3 * unit_scale(), 2 * unit_scale()); + let base: FlexStyle = flex_style(FlexAxis.column, unit_scale(), insets_all(unit_scale())); + let style: FlexWrapStyle = flex_wrap_style(base, unit_scale()); + let result: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 10 * unit_scale()), style + ); + expect(state, result.lines.len == 3 && result.placements.data[0].frame.x == unit_scale() && + result.placements.data[2].frame.x == 4 * unit_scale() && + result.placements.data[4].frame.x == 7 * unit_scale(), + "column wrapping advances physical columns with exact line gaps"); + expect(state, result.placements.data[0].frame.y == unit_scale() && + result.placements.data[1].frame.y == 5 * unit_scale(), + "column main-axis gaps remain exact"); + flex_wrap_free(&result); + + base.direction = LayoutDirection.rtl; + style.flex = base; + let rtl: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 10 * unit_scale()), style + ); + expect(state, rtl.placements.data[0].frame.x == 7 * unit_scale() && + rtl.placements.data[2].frame.x == 4 * unit_scale() && + rtl.placements.data[4].frame.x == unit_scale(), + "RTL column wrapping advances line bands from the physical right"); + flex_wrap_free(&rtl); + free[FlexItem](&items); +} + +fn baseline_and_growth_contract(state: *TestState) void { + let baseline_items: ArrayList[FlexItem] = make[FlexItem](2); + let a: FlexItem = flex_item(1, 3 * unit_scale(), 4 * unit_scale()); a.baseline = unit_scale(); + let b: FlexItem = flex_item(2, 3 * unit_scale(), 4 * unit_scale()); b.baseline = 3 * unit_scale(); + push[FlexItem](&baseline_items, a); push[FlexItem](&baseline_items, b); + let base: FlexStyle = flex_style(FlexAxis.row, unit_scale(), insets_all(unit_scale())); + base.cross_alignment = CrossAlignment.baseline; + let baseline_result: FlexWrapResult = flex_wrap_layout( + baseline_items, tight_constraints(10 * unit_scale(), 8 * unit_scale()), flex_wrap_style(base, 0) + ); + expect(state, baseline_result.lines.data[0].cross_extent == 6 * unit_scale() && + baseline_result.placements.data[0].baseline == baseline_result.placements.data[1].baseline, + "wrapped baseline lines reserve ascent plus descent without clipping"); + flex_wrap_free(&baseline_result); free[FlexItem](&baseline_items); + + let grow_items: ArrayList[FlexItem] = wrap_items(3, 2 * unit_scale(), 2 * unit_scale()); + grow_items.data[0].grow = 1; grow_items.data[1].grow = 1; grow_items.data[2].grow = 1; + let grow_style: FlexWrapStyle = flex_wrap_style( + flex_style(FlexAxis.row, unit_scale(), insets_all(unit_scale())), unit_scale() + ); + let grown: FlexWrapResult = flex_wrap_layout( + grow_items, tight_constraints(8 * unit_scale(), 7 * unit_scale()), grow_style + ); + if (grown.placements.data[0].frame.width != 5 * unit_scale() / 2 || + grown.placements.data[1].frame.width != 5 * unit_scale() / 2 || + grown.placements.data[2].frame.width != 6 * unit_scale()) { + report_i64(" grown line count=", grown.lines.len as i64); + report_i64(" first width=", grown.placements.data[0].frame.width); + report_i64(" second width=", grown.placements.data[1].frame.width); + report_i64(" third width=", grown.placements.data[2].frame.width); + } + expect(state, grown.lines.len == 2 && grown.placements.data[0].frame.width == 5 * unit_scale() / 2 && + grown.placements.data[1].frame.width == 5 * unit_scale() / 2 && + grown.placements.data[2].frame.width == 6 * unit_scale(), + "grow and exact remainder distribution execute independently per wrapped line"); + flex_wrap_free(&grown); free[FlexItem](&grow_items); +} + +fn overflow_and_property_contract(state: *TestState) void { + let items: ArrayList[FlexItem] = wrap_items(3, 5 * unit_scale(), 2 * unit_scale()); + let i: i32 = 0; + while (i < items.len) { items.data[i].min_main = 5 * unit_scale(); i = i + 1; } + let base: FlexStyle = flex_style(FlexAxis.row, unit_scale(), insets_all(unit_scale())); + let wrapped: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(6 * unit_scale(), 10 * unit_scale()), flex_wrap_style(base, unit_scale()) + ); + expect(state, wrapped.lines.len == 3 && wrapped.overflow_main == unit_scale(), + "oversized minimums wrap individually and retain exact main overflow truth"); + flex_wrap_free(&wrapped); + + let nowrap_style: FlexWrapStyle = flex_wrap_style(base, unit_scale()); + nowrap_style.wrap = 0; + let nowrap: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(6 * unit_scale(), 4 * unit_scale()), nowrap_style + ); + expect(state, nowrap.lines.len == 1 && nowrap.overflow_main == 13 * unit_scale() && nowrap.wrapped == 0, + "disabled wrapping preserves one line and reports all constrained overflow"); + flex_wrap_free(&nowrap); + + items.data[1].id = items.data[0].id; + let duplicate: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(6 * unit_scale(), 10 * unit_scale()), flex_wrap_style(base, unit_scale()) + ); + expect(state, duplicate.duplicate_ids == 1, + "duplicate stable IDs remain fail-visible through wrapped layout"); + flex_wrap_free(&duplicate); free[FlexItem](&items); + + let all_valid: i32 = 1; + let width: i64 = 8; + while (width <= 32) { + let sweep: ArrayList[FlexItem] = wrap_items(10, 2 * unit_scale(), unit_scale()); + let result: FlexWrapResult = flex_wrap_layout( + sweep, + Constraints{ + .min_width = width * unit_scale(), .max_width = width * unit_scale(), + .min_height = 0, .max_height = unbounded_extent(), + }, + flex_wrap_style(flex_style(FlexAxis.row, unit_scale(), insets_all(unit_scale())), unit_scale()) + ); + let j: i32 = 0; + while (j < result.placements.len) { + let frame: Rect = result.placements.data[j].frame; + if (result.placements.data[j].id != (j + 1) as i64 || frame.x < unit_scale() || + frame.x + frame.width > width * unit_scale() - unit_scale()) { all_valid = 0; } + j = j + 1; + } + flex_wrap_free(&result); free[FlexItem](&sweep); + width = width + 1; + } + expect(state, all_valid == 1, + "25-width wrap sweep preserves logical IDs and physical content bounds"); +} + +fn malformed_contract(state: *TestState) void { + let items: ArrayList[FlexItem] = wrap_items(2, 2 * unit_scale(), unit_scale()); + items.data[1].id = 0; + let style: FlexWrapStyle = flex_wrap_style( + flex_style(FlexAxis.row, unit_scale(), insets_all(0)), unit_scale() + ); + let malformed: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 4 * unit_scale()), style + ); + expect(state, malformed.error == FlexWrapError.invalid_item && malformed.error_index == 1 && + malformed.placements.len == 0 && malformed.lines.len == 0 && malformed.layout_hash != 0, + "malformed item geometry fails at the exact item before placement"); + flex_wrap_free(&malformed); + + items.data[1].id = 2; + let invalid_style: FlexWrapStyle = style; + invalid_style.line_gap = unbounded_extent() / 2; + let bad_style: FlexWrapResult = flex_wrap_layout( + items, tight_constraints(10 * unit_scale(), 4 * unit_scale()), invalid_style + ); + expect(state, bad_style.error == FlexWrapError.invalid_style && bad_style.placements.len == 0, + "unsafe style extents fail before layout arithmetic"); + flex_wrap_free(&bad_style); + + items.data[0].basis = unbounded_extent(); + items.data[1].basis = unbounded_extent(); + let overflow: FlexWrapResult = flex_wrap_layout( + items, + Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + }, + flex_wrap_style(flex_style(FlexAxis.row, 0, insets_all(0)), 0) + ); + expect(state, overflow.error == FlexWrapError.arithmetic_overflow && overflow.error_index == 1, + "unbounded intrinsic totals fail visibly instead of wrapping signed arithmetic"); + flex_wrap_free(&overflow); + free[FlexItem](&items); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + adaptive_contract(&state); + row_wrap_contract(&state); + rtl_and_line_alignment_contract(&state); + column_wrap_contract(&state); + baseline_and_growth_contract(&state); + overflow_and_property_contract(&state); + malformed_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Flex adaptive contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/flex_contract.zag b/tests/flex_contract.zag new file mode 100644 index 0000000..1b99bc7 --- /dev/null +++ b/tests/flex_contract.zag @@ -0,0 +1,178 @@ +@import("std:list") +@import("../src/layout/flex.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); + _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); + _zag_println(name); + } +} + +fn report_i64(label: []u8, value: i64) void { + let text: []u8 = _zag_i64_to_str(value); + _zag_print(label); + _zag_println(text); + _zag_str_free(text); +} + +fn item(id: i64, basis: i64, cross: i64) FlexItem { return flex_item(id, basis, cross); } + +fn fixed_row(state: *TestState) void { + let items: ArrayList[FlexItem] = make[FlexItem](3); + push[FlexItem](&items, item(1, 20, 10)); + push[FlexItem](&items, item(2, 30, 12)); + push[FlexItem](&items, item(3, 10, 8)); + let style: FlexStyle = flex_style(FlexAxis.row, 5, insets_all(4)); + let result: FlexResult = flex_layout(items, tight_constraints(78, 20), style); + expect(state, result.container.width == 78 && result.container.height == 20, "tight constraints define the container"); + expect(state, result.placements.data[0].frame.x == 4, "leading padding is exact"); + expect(state, result.placements.data[1].frame.x == 29, "gap is exact"); + expect(state, result.placements.data[2].frame.x == 64, "third placement is deterministic"); + flex_free(&result); + free[FlexItem](&items); +} + +fn growing_row(state: *TestState) void { + let items: ArrayList[FlexItem] = make[FlexItem](2); + let first: FlexItem = item(10, 20, 10); first.grow = 1; + let second: FlexItem = item(11, 20, 10); second.grow = 2; + push[FlexItem](&items, first); push[FlexItem](&items, second); + let result: FlexResult = flex_layout(items, tight_constraints(100, 10), flex_style(FlexAxis.row, 0, insets_all(0))); + expect(state, result.placements.data[0].frame.width == 40, "grow weight one receives one third"); + expect(state, result.placements.data[1].frame.width == 60, "last grow item receives exact remainder"); + expect(state, result.placements.data[1].frame.x == 40, "growth conserves the main extent"); + flex_free(&result); free[FlexItem](&items); +} + +fn rtl_row(state: *TestState) void { + let items: ArrayList[FlexItem] = make[FlexItem](2); + push[FlexItem](&items, item(1, 20, 10)); push[FlexItem](&items, item(2, 30, 10)); + let style: FlexStyle = flex_style(FlexAxis.row, 5, Insets{ .top = 0, .right = 7, .bottom = 0, .left = 3 }); + style.direction = LayoutDirection.rtl; + let result: FlexResult = flex_layout(items, tight_constraints(65, 10), style); + expect(state, result.placements.data[0].frame.x == 38, "RTL starts at the physical right inset"); + expect(state, result.placements.data[1].frame.x == 3, "RTL preserves logical order and exact gap"); + flex_free(&result); free[FlexItem](&items); +} + +fn alignment(state: *TestState) void { + let items: ArrayList[FlexItem] = make[FlexItem](2); + let a: FlexItem = item(1, 20, 12); a.baseline = 8; + let b: FlexItem = item(2, 20, 18); b.baseline = 14; + push[FlexItem](&items, a); push[FlexItem](&items, b); + let style: FlexStyle = flex_style(FlexAxis.row, 0, insets_all(2)); + style.cross_alignment = CrossAlignment.baseline; + expect(state, style.cross_alignment == CrossAlignment.baseline, "baseline style mutation persists before layout"); + let result: FlexResult = flex_layout(items, tight_constraints(40, 24), style); + if (result.placements.data[0].baseline != result.placements.data[1].baseline || + result.placements.data[0].frame.y != 8 || result.placements.data[1].frame.y != 2) { + report_i64(" first y=", result.placements.data[0].frame.y); + report_i64(" first baseline=", result.placements.data[0].baseline); + report_i64(" second y=", result.placements.data[1].frame.y); + report_i64(" second baseline=", result.placements.data[1].baseline); + } + expect(state, result.placements.data[0].baseline == result.placements.data[1].baseline, "baseline alignment is exact"); + expect(state, result.placements.data[0].frame.y == 8 && result.placements.data[1].frame.y == 2, "baseline offsets remain deterministic"); + flex_free(&result); free[FlexItem](&items); +} + +fn distribution(state: *TestState) void { + let items: ArrayList[FlexItem] = make[FlexItem](3); + push[FlexItem](&items, item(1, 10, 10)); push[FlexItem](&items, item(2, 10, 10)); push[FlexItem](&items, item(3, 10, 10)); + let style: FlexStyle = flex_style(FlexAxis.row, 0, insets_all(0)); + style.main_alignment = MainAlignment.space_between; + let result: FlexResult = flex_layout(items, tight_constraints(90, 10), style); + expect(state, result.placements.data[0].frame.x == 0, "space-between keeps first edge"); + expect(state, result.placements.data[1].frame.x == 40, "space-between distributes the first interval"); + expect(state, result.placements.data[2].frame.x == 80, "space-between keeps last edge"); + flex_free(&result); free[FlexItem](&items); +} + +fn safety_contracts(state: *TestState) void { + let normalized: Constraints = normalize_constraints(Constraints{ .min_width = 20, .max_width = 10, .min_height = -5, .max_height = -10 }); + expect(state, normalized.min_width == 20 && normalized.max_width == 20, "invalid width constraints collapse safely"); + expect(state, normalized.min_height == 0 && normalized.max_height == 0, "negative height constraints clamp safely"); + + let items: ArrayList[FlexItem] = make[FlexItem](2); + let a: FlexItem = item(7, 50, 10); a.min_main = 40; + let b: FlexItem = item(7, 50, 10); b.min_main = 40; + push[FlexItem](&items, a); push[FlexItem](&items, b); + let result: FlexResult = flex_layout(items, tight_constraints(60, 10), flex_style(FlexAxis.row, 0, insets_all(0))); + expect(state, result.duplicate_ids == 1, "duplicate stable IDs fail visibly"); + expect(state, result.overflow == 20, "min-size overflow is explicit"); + flex_free(&result); free[FlexItem](&items); + + let malformed: ArrayList[FlexItem] = make[FlexItem](2); + push[FlexItem](&malformed, item(1, 10, 10)); + push[FlexItem](&malformed, item(0, 10, 10)); + let bad_item: FlexResult = flex_layout( + malformed, tight_constraints(20, 10), flex_style(FlexAxis.row, 0, insets_all(0)) + ); + expect(state, bad_item.error == FlexError.invalid_item && bad_item.error_index == 1 && + bad_item.placements.len == 0, + "malformed single-line items fail at the exact index before placement"); + flex_free(&bad_item); + + malformed.data[1].id = 2; + malformed.data[0].basis = unbounded_extent(); + malformed.data[1].basis = unbounded_extent(); + let arithmetic: FlexResult = flex_layout( + malformed, + Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + }, + flex_style(FlexAxis.row, 0, insets_all(0)) + ); + expect(state, arithmetic.error == FlexError.arithmetic_overflow && arithmetic.error_index == 1, + "single-line intrinsic totals fail visibly before signed overflow"); + flex_free(&arithmetic); free[FlexItem](&malformed); +} + +fn property_sweep(state: *TestState) void { + let width: i64 = 64; + let all_valid: i32 = 1; + while (width <= 256) { + let items: ArrayList[FlexItem] = make[FlexItem](3); + let a: FlexItem = item(1, 8, 10); a.grow = 1; + let b: FlexItem = item(2, 13, 10); b.grow = 2; + let c: FlexItem = item(3, 5, 10); c.grow = 1; + push[FlexItem](&items, a); push[FlexItem](&items, b); push[FlexItem](&items, c); + let result: FlexResult = flex_layout(items, tight_constraints(width, 10), flex_style(FlexAxis.row, 2, insets_all(1))); + let last: Rect = result.placements.data[2].frame; + if (result.overflow != 0 || last.x + last.width != width - 1 || + rect_intersects(result.placements.data[0].frame, result.placements.data[1].frame) != 0 || + rect_intersects(result.placements.data[1].frame, result.placements.data[2].frame) != 0) { + all_valid = 0; + } + flex_free(&result); free[FlexItem](&items); + width = width + 1; + } + expect(state, all_valid, "193-width property sweep conserves bounds and gaps"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + fixed_row(&state); + growing_row(&state); + rtl_row(&state); + alignment(&state); + distribution(&state); + safety_contracts(&state); + property_sweep(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Flex contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/grid_contract.zag b/tests/grid_contract.zag new file mode 100644 index 0000000..34cd9a7 --- /dev/null +++ b/tests/grid_contract.zag @@ -0,0 +1,208 @@ +@import("std:list") +@import("../src/layout/grid.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn metrics(min_width: i64, min_height: i64, width: i64, height: i64, + max_width: i64, max_height: i64, baseline: i64) IntrinsicMetrics { + return intrinsic_metrics(min_width, min_height, width, height, max_width, max_height, baseline); +} + +fn basic_grid_contract(state: *TestState) void { + let columns: ArrayList[GridTrack] = make[GridTrack](3); + push[GridTrack](&columns, grid_fixed(4)); + push[GridTrack](&columns, grid_fraction(1, 2, 20)); + push[GridTrack](&columns, grid_fraction(2, 2, 20)); + let rows: ArrayList[GridTrack] = make[GridTrack](2); + push[GridTrack](&rows, grid_intrinsic(2, 4, 10)); + push[GridTrack](&rows, grid_fixed(3)); + let items: ArrayList[GridItem] = make[GridItem](3); + push[GridItem](&items, grid_item_at(1, 0, 0, metrics(2, 2, 4, 2, 8, 8, 1))); + let automatic: GridItem = grid_item(2, metrics(1, 1, 2, 2, 8, 8, 1)); + push[GridItem](&items, automatic); + let spanning: GridItem = grid_item_at(3, 1, 1, metrics(4, 2, 10, 3, 30, 6, 2)); + spanning.column_span = 2; + spanning.z_order = 7; + push[GridItem](&items, spanning); + let style: GridStyle = grid_style(1, 1, insets_all(1)); + style.vertical_content = ContentAlignment.space_between; + let result: GridResult = grid_layout(items, columns, rows, tight_constraints(30, 12), style); + expect(state, result.error == GridError.none && result.columns.len == 3 && result.rows.len == 2 && + result.columns.data[0].extent == 4 && result.columns.data[1].extent == 8 && + result.columns.data[2].extent == 14, + "grid resolves fixed and weighted fraction tracks against exact available width"); + expect(state, result.placements.data[1].row == 0 && result.placements.data[1].column == 1 && + result.placements.data[1].frame.x == 6, + "grid auto placement is deterministic row-major and preserves input identity"); + expect(state, result.placements.data[2].frame.x == 6 && result.placements.data[2].frame.y == 8 && + result.placements.data[2].frame.width == 23 && result.placements.data[2].frame.height == 3 && + result.placements.data[2].z_order == 7, + "grid spans include interior gaps and retained z-order without geometry ambiguity"); + expect(state, result.overflow_width == 0 && result.overflow_height == 0 && result.layout_hash != 0, + "grid exposes clean per-axis overflow and stable nonzero layout identity"); + let repeated: GridResult = grid_layout(items, columns, rows, tight_constraints(30, 12), style); + expect(state, repeated.layout_hash == result.layout_hash, + "identical grid inputs produce identical track and placement identity"); + grid_free(&repeated); grid_free(&result); + free[GridItem](&items); free[GridTrack](&columns); free[GridTrack](&rows); +} + +fn span_intrinsic_and_rtl_contract(state: *TestState) void { + let columns: ArrayList[GridTrack] = make[GridTrack](2); + push[GridTrack](&columns, grid_intrinsic(2, 2, 20)); + push[GridTrack](&columns, grid_intrinsic(2, 2, 20)); + let rows: ArrayList[GridTrack] = make[GridTrack](1); + push[GridTrack](&rows, grid_intrinsic(2, 3, 10)); + let items: ArrayList[GridItem] = make[GridItem](1); + let span: GridItem = grid_item_at(1, 0, 0, metrics(8, 2, 15, 3, 30, 8, 2)); + span.column_span = 2; + span.horizontal = PlacementAlignment.start; + span.vertical = PlacementAlignment.start; + push[GridItem](&items, span); + let loose: Constraints = Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + }; + let ltr_style: GridStyle = grid_style(1, 1, insets_all(1)); + let ltr: GridResult = grid_layout(items, columns, rows, loose, ltr_style); + expect(state, ltr.container.width == 17 && ltr.container.height == 5 && + ltr.columns.data[0].extent + ltr.columns.data[1].extent + 1 == 15, + "spanning intrinsic content grows eligible tracks and determines an unbounded container"); + let rtl_style: GridStyle = ltr_style; + rtl_style.direction = LayoutDirection.rtl; + let rtl: GridResult = grid_layout(items, columns, rows, loose, rtl_style); + expect(state, rtl.columns.data[0].offset > rtl.columns.data[1].offset && + rtl.placements.data[0].frame.x == 1 && rtl.layout_hash != ltr.layout_hash, + "RTL mirrors logical grid columns and records direction in deterministic identity"); + grid_free(&rtl); grid_free(<r); + free[GridItem](&items); free[GridTrack](&columns); free[GridTrack](&rows); +} + +fn baseline_contract(state: *TestState) void { + let columns: ArrayList[GridTrack] = make[GridTrack](2); + push[GridTrack](&columns, grid_fixed(10)); push[GridTrack](&columns, grid_fixed(10)); + let rows: ArrayList[GridTrack] = make[GridTrack](1); + push[GridTrack](&rows, grid_intrinsic(5, 6, 12)); + let items: ArrayList[GridItem] = make[GridItem](2); + let first: GridItem = grid_item_at(1, 0, 0, metrics(2, 2, 4, 4, 8, 8, 3)); + first.horizontal = PlacementAlignment.center; first.vertical = PlacementAlignment.baseline; + push[GridItem](&items, first); + let second: GridItem = grid_item_at(2, 0, 1, metrics(2, 2, 4, 5, 8, 8, 1)); + second.horizontal = PlacementAlignment.center; second.vertical = PlacementAlignment.baseline; + push[GridItem](&items, second); + let result: GridResult = grid_layout(items, columns, rows, tight_constraints(23, 8), + grid_style(1, 0, insets_all(1))); + expect(state, result.placements.data[0].baseline == result.placements.data[1].baseline && + result.placements.data[0].frame.y == 1 && result.placements.data[1].frame.y == 3, + "single-row baseline alignment preserves typography across unequal glyph boxes"); + grid_free(&result); free[GridItem](&items); free[GridTrack](&columns); free[GridTrack](&rows); +} + +fn collision_and_failure_contract(state: *TestState) void { + let columns: ArrayList[GridTrack] = make[GridTrack](1); push[GridTrack](&columns, grid_fixed(8)); + let rows: ArrayList[GridTrack] = make[GridTrack](1); push[GridTrack](&rows, grid_fixed(8)); + let items: ArrayList[GridItem] = make[GridItem](2); + push[GridItem](&items, grid_item_at(1, 0, 0, metrics(1, 1, 2, 2, 4, 4, 1))); + push[GridItem](&items, grid_item_at(2, 0, 0, metrics(1, 1, 2, 2, 4, 4, 1))); + let style: GridStyle = grid_style(0, 0, insets_all(0)); + let collision: GridResult = grid_layout(items, columns, rows, tight_constraints(8, 8), style); + expect(state, collision.error == GridError.collision && collision.error_index == 1 && + collision.placements.len == 0, + "explicit grid collisions fail at the exact item by default"); + grid_free(&collision); + style.allow_overlap = 1; + let overlap: GridResult = grid_layout(items, columns, rows, tight_constraints(8, 8), style); + expect(state, overlap.error == GridError.none && overlap.overlap_cells == 1 && + overlap.placements.len == 2, + "explicit overlap is opt-in and reports every occupied collision cell"); + grid_free(&overlap); + items.data[1].id = 1; + let duplicate: GridResult = grid_layout(items, columns, rows, tight_constraints(8, 8), style); + expect(state, duplicate.error == GridError.duplicate_id && duplicate.error_index == 1, + "duplicate grid IDs fail before placement"); + grid_free(&duplicate); + items.data[1].id = 2; items.data[0].row = 0 - 1; items.data[0].column = 0 - 1; + items.data[1].row = 0 - 1; items.data[1].column = 0 - 1; style.allow_overlap = 0; + let full: GridResult = grid_layout(items, columns, rows, tight_constraints(8, 8), style); + expect(state, full.error == GridError.no_space && full.error_index == 1, + "auto placement fails closed when no unoccupied span remains"); + grid_free(&full); + items.data[1].row = 0; items.data[1].column = 0 - 1; + let partial: GridResult = grid_layout(items, columns, rows, tight_constraints(8, 8), style); + expect(state, partial.error == GridError.invalid_item && partial.error_index == 1, + "partial auto coordinates are rejected instead of guessed"); + grid_free(&partial); + free[GridItem](&items); free[GridTrack](&columns); free[GridTrack](&rows); + + let bad_columns: ArrayList[GridTrack] = make[GridTrack](1); + push[GridTrack](&bad_columns, grid_fraction(0, 1, 4)); + let valid_rows: ArrayList[GridTrack] = make[GridTrack](1); push[GridTrack](&valid_rows, grid_fixed(2)); + let empty: ArrayList[GridItem] = make[GridItem](1); + let bad_track: GridResult = grid_layout(empty, bad_columns, valid_rows, tight_constraints(4, 2), + grid_style(0, 0, insets_all(0))); + expect(state, bad_track.error == GridError.invalid_track && bad_track.error_index == 0, + "zero-weight fraction tracks fail typed validation"); + grid_free(&bad_track); free[GridItem](&empty); free[GridTrack](&bad_columns); free[GridTrack](&valid_rows); +} + +fn overflow_and_property_contract(state: *TestState) void { + let fixed_columns: ArrayList[GridTrack] = make[GridTrack](2); + push[GridTrack](&fixed_columns, grid_fixed(8)); push[GridTrack](&fixed_columns, grid_fixed(8)); + let fixed_rows: ArrayList[GridTrack] = make[GridTrack](1); push[GridTrack](&fixed_rows, grid_fixed(4)); + let empty: ArrayList[GridItem] = make[GridItem](1); + let overflow: GridResult = grid_layout(empty, fixed_columns, fixed_rows, tight_constraints(12, 4), + grid_style(1, 0, insets_all(0))); + expect(state, overflow.error == GridError.none && overflow.overflow_width == 5 && + overflow.columns.data[1].offset == 9, + "unshrinkable fixed tracks remain exact and expose quantified overflow"); + grid_free(&overflow); free[GridItem](&empty); + free[GridTrack](&fixed_columns); free[GridTrack](&fixed_rows); + + let all_valid: i32 = 1; + let extent: i64 = 7; + while (extent <= 128) { + let columns: ArrayList[GridTrack] = make[GridTrack](2); + push[GridTrack](&columns, grid_fraction(1, 1, unbounded_extent())); + push[GridTrack](&columns, grid_fraction(3, 1, unbounded_extent())); + let rows: ArrayList[GridTrack] = make[GridTrack](1); push[GridTrack](&rows, grid_fraction(1, 1, unbounded_extent())); + let items: ArrayList[GridItem] = make[GridItem](2); + push[GridItem](&items, grid_item_at(1, 0, 0, metrics(1, 1, 1, 1, unbounded_extent(), unbounded_extent(), 1))); + push[GridItem](&items, grid_item_at(2, 0, 1, metrics(1, 1, 1, 1, unbounded_extent(), unbounded_extent(), 1))); + let result: GridResult = grid_layout(items, columns, rows, tight_constraints(extent, 7), + grid_style(1, 0, insets_all(1))); + if (result.error != GridError.none || result.columns.data[0].offset != 1 || + result.columns.data[1].offset != 2 + result.columns.data[0].extent || + result.columns.data[1].offset + result.columns.data[1].extent != extent - 1 || + result.overflow_width != 0) { all_valid = 0; } + grid_free(&result); free[GridItem](&items); free[GridTrack](&columns); free[GridTrack](&rows); + extent = extent + 1; + } + expect(state, all_valid == 1, + "122-extent fraction sweep fills exact padded bounds without drift or hidden overflow"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + basic_grid_contract(&state); + span_intrinsic_and_rtl_contract(&state); + baseline_contract(&state); + collision_and_failure_contract(&state); + overflow_and_property_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Grid contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/image_contract.zag b/tests/image_contract.zag new file mode 100644 index 0000000..51bbd84 --- /dev/null +++ b/tests/image_contract.zag @@ -0,0 +1,92 @@ +@import("../src/render/image.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn test_resource(kind: RenderResourceKind, payload: []u8, format_tag: i64, + width: i64, height: i64, color_space: RenderColorSpace) RenderResource { + let spec: RenderResourceSpec = render_resource_spec(1, kind, payload, format_tag); + spec.width = width; + spec.height = height; + spec.color_space = color_space; + return render_resource_from_spec(spec); +} + +fn resource_free(resource: *RenderResource) void { + free[u8](&resource.*.payload); +} + +fn validation_contract(state: *TestState) void { + let valid: RenderResource = test_resource(RenderResourceKind.image, + "RGBA", image_resource_format_tag(), 1, 1, RenderColorSpace.srgb); + expect(state, image_resource_validate(valid) == ImagePayloadError.none, + "one RGBA8 texel with explicit sRGB metadata is canonical"); + resource_free(&valid); + + let wrong_kind: RenderResource = test_resource(RenderResourceKind.path, + "RGBA", image_resource_format_tag(), 1, 1, RenderColorSpace.srgb); + expect(state, image_resource_validate(wrong_kind) == ImagePayloadError.wrong_kind, + "non-image resources cannot masquerade as decoded pixels"); + resource_free(&wrong_kind); + + let wrong_format: RenderResource = test_resource(RenderResourceKind.image, + "RGBA", 2, 1, 1, RenderColorSpace.srgb); + expect(state, image_resource_validate(wrong_format) == + ImagePayloadError.unsupported_format, + "unknown decoded-image schemas fail closed"); + resource_free(&wrong_format); + + let wrong_size: RenderResource = test_resource(RenderResourceKind.image, + "RGB", image_resource_format_tag(), 1, 1, RenderColorSpace.srgb); + expect(state, image_resource_validate(wrong_size) == ImagePayloadError.size_mismatch, + "RGBA8 payload length must match dimensions exactly"); + resource_free(&wrong_size); + + let no_color: RenderResource = test_resource(RenderResourceKind.image, + "RGBA", image_resource_format_tag(), 1, 1, RenderColorSpace.none); + expect(state, image_resource_validate(no_color) == + ImagePayloadError.invalid_color_space, + "decoded images require explicit color-space truth"); + resource_free(&no_color); + + let no_dimensions: RenderResource = test_resource(RenderResourceKind.image, + "RGBA", image_resource_format_tag(), 0, 1, RenderColorSpace.srgb); + expect(state, image_resource_validate(no_dimensions) == + ImagePayloadError.invalid_dimensions, + "zero dimensions fail before size arithmetic"); + resource_free(&no_dimensions); +} + +fn bounds_contract(state: *TestState) void { + expect(state, image_payload_expected_bytes(1, 1) == 4 && + image_payload_expected_bytes(1920, 1080) == 8294400, + "decoded RGBA8 byte accounting is exact"); + expect(state, image_payload_expected_bytes(0, 1) == 0 && + image_payload_expected_bytes(1048577, 1) == 0, + "invalid dimensions never produce allocation sizes"); + expect(state, image_payload_expected_bytes(1048576, 1048576) == 0, + "multiplication beyond the hard byte domain fails without overflow"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + validation_contract(&state); + bounds_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Image contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/input_contract.zag b/tests/input_contract.zag new file mode 100644 index 0000000..c31a7a1 --- /dev/null +++ b/tests/input_contract.zag @@ -0,0 +1,125 @@ +@import("../src/input/router.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn basic_tree() HitTree { + let tree: HitTree = hit_tree_make(); + let root: HitNode = hit_node(node_key(1), hit_root_key(), rect(0, 0, 200, 200)); + root.z_order = 0; + _ = hit_tree_add(&tree, root); + let bottom: HitNode = hit_node(node_key(2), node_key(1), rect(0, 0, 100, 100)); + bottom.z_order = 1; + bottom.focusable = 1; + _ = hit_tree_add(&tree, bottom); + let top: HitNode = hit_node(node_key(3), node_key(1), rect(0, 0, 100, 100)); + top.z_order = 2; + top.focusable = 1; + top.has_clip = 1; + top.clip = rect(10, 10, 50, 50); + _ = hit_tree_add(&tree, top); + return tree; +} + +fn hit_contract(state: *TestState) void { + let tree: HitTree = basic_tree(); + let overlap: HitResult = hit_test(tree, 20, 20); + let clipped: HitResult = hit_test(tree, 5, 5); + expect(state, overlap.found == 1 && overlap.id.value == 3, "highest z-order clipped node wins overlap"); + expect(state, clipped.found == 1 && clipped.id.value == 2, "local clip excludes the top node without hiding lower content"); + + let rotated: HitNode = hit_node(node_key(4), node_key(1), rect(0, 0, 64, 64)); + rotated.z_order = 5; + rotated.transform = Transform{ + .scale_x = 0, .skew_x = 0 - unit_scale(), + .skew_y = unit_scale(), .scale_y = 0, + .translate_x = 128, .translate_y = 0, + }; + _ = hit_tree_add(&tree, rotated); + let transformed: HitResult = hit_test(tree, 96, 32); + expect(state, transformed.id.value == 4 && transformed.local_x == 32 && transformed.local_y == 32, "affine inverse returns exact local coordinates for rotated content"); + hit_tree_free(&tree); +} + +fn validation_contract(state: *TestState) void { + let tree: HitTree = hit_tree_make(); + let root: HitNode = hit_node(node_key(10), hit_root_key(), rect(0, 0, 10, 10)); + _ = hit_tree_add(&tree, root); + expect(state, hit_tree_add(&tree, root) == HitTreeError.duplicate_id && tree.nodes.len == 1, "duplicate hit IDs fail before mutation"); + let orphan: HitNode = hit_node(node_key(11), node_key(99), rect(0, 0, 10, 10)); + expect(state, hit_tree_add(&tree, orphan) == HitTreeError.missing_parent, "missing hit parent fails visibly"); + let singular: HitNode = hit_node(node_key(12), node_key(10), rect(0, 0, 10, 10)); + singular.transform.scale_x = 0; + expect(state, hit_tree_add(&tree, singular) == HitTreeError.singular_transform, "singular transforms never enter the hit tree"); + let extreme: HitNode = hit_node(node_key(13), node_key(10), rect(0, 0, 10, 10)); + extreme.transform.translate_x = 1000000001; + expect(state, hit_tree_add(&tree, extreme) == HitTreeError.invalid_transform, "unsafe transform translation never enters the hit tree"); + expect(state, hit_test(tree, 1000000001, 0).found == 0, "unsafe direct hit coordinates fail closed before affine arithmetic"); + hit_tree_free(&tree); +} + +fn routing_contract(state: *TestState) void { + let tree: HitTree = basic_tree(); + let router: InputRouter = input_router_make(); + expect(state, input_request_capture(&router, tree, 7, node_key(3)) == 0, "capture cannot target a pointer before its down phase"); + let down: RouteResult = input_route_pointer(&router, tree, pointer_event(7, PointerPhase.down, 20, 20)); + expect(state, down.hit.id.value == 3 && router.focus.value == 3 && down.captured == 0, "pointer down targets top node and advances focus"); + expect(state, input_request_capture(&router, tree, 7, node_key(3)) == 1, "enabled node can capture an active pointer identity"); + let outside: RouteResult = input_route_pointer(&router, tree, pointer_event(7, PointerPhase.move, 500, 500)); + expect(state, outside.hit.id.value == 3 && outside.captured == 1 && outside.hit.local_x == 500, "captured move remains ID-routed outside bounds with local coordinates"); + let up: RouteResult = input_route_pointer(&router, tree, pointer_event(7, PointerPhase.up, 500, 500)); + expect(state, up.captured == 1 && input_capture_index(router, 7) < 0, "pointer up routes to capture then releases it"); + let after: RouteResult = input_route_pointer(&router, tree, pointer_event(7, PointerPhase.move, 500, 500)); + expect(state, after.hit.found == 0 && after.captured == 0, "uncaptured outside move has no phantom target"); + expect(state, router.events.len == 4 && router.events.data[3].sequence == 4, "input evidence is one monotonic ordered stream"); + + let invalid: RouteResult = input_route_pointer(&router, tree, pointer_event(0 - 1, PointerPhase.down, 0, 0)); + expect(state, invalid.status == RouteStatus.invalid_pointer && invalid.sequence == 5, "invalid pointers fail visibly without escaping the event log"); + expect(state, input_request_focus(&router, tree, node_key(1)) == 0 && input_request_focus(&router, tree, node_key(2)) == 1, "focus requests enforce enabled focusable targets"); + input_router_free(&router); + hit_tree_free(&tree); +} + +fn stale_capture_contract(state: *TestState) void { + let tree: HitTree = basic_tree(); + let router: InputRouter = input_router_make(); + _ = input_route_pointer(&router, tree, pointer_event(9, PointerPhase.down, 20, 20)); + _ = input_request_capture(&router, tree, 9, node_key(3)); + tree.nodes.data[2].enabled = 0; + let result: RouteResult = input_route_pointer(&router, tree, pointer_event(9, PointerPhase.move, 20, 20)); + expect(state, result.captured == 0 && result.hit.id.value == 2 && input_capture_index(router, 9) < 0, "disabled capture target releases and reroutes from current truth"); + _ = input_route_pointer(&router, tree, pointer_event(10, PointerPhase.down, 20, 20)); + _ = input_request_capture(&router, tree, 10, node_key(2)); + let cancelled: RouteResult = input_route_pointer(&router, tree, pointer_event(10, PointerPhase.cancel, 500, 500)); + let after_cancel: RouteResult = input_route_pointer(&router, tree, pointer_event(10, PointerPhase.move, 500, 500)); + expect(state, cancelled.captured == 1 && cancelled.hit.id.value == 2 && + input_capture_index(router, 10) < 0 && input_active_index(router, 10) < 0 && + after_cancel.hit.found == 0, "cancel routes once to capture then clears active pointer state"); + input_router_free(&router); + hit_tree_free(&tree); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + hit_contract(&state); + validation_contract(&state); + routing_contract(&state); + stale_capture_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Input contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/measure_contract.zag b/tests/measure_contract.zag new file mode 100644 index 0000000..621c41a --- /dev/null +++ b/tests/measure_contract.zag @@ -0,0 +1,420 @@ +@import("std:list") +@import("../src/core/state.zag") +@import("../src/layout/invalidation.zag") +@import("../src/layout/flex.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn leaf_metrics(width: i64, height: i64, baseline: i64) IntrinsicMetrics { + return intrinsic_metrics( + width / 2, height / 2, + width, height, + width * 2, height * 2, + baseline + ); +} + +fn aggregation_contract(state: *TestState) void { + let tree: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&tree, intrinsic_group(100, IntrinsicKind.row, 2, insets_all(3))); + intrinsic_tree_add_node(&tree, intrinsic_leaf(1, leaf_metrics(20, 10, 7), 0)); + intrinsic_tree_add_node(&tree, intrinsic_leaf(2, leaf_metrics(30, 16, 11), 0)); + intrinsic_tree_add_child(&tree, 100, 1); + intrinsic_tree_add_child(&tree, 100, 2); + let row: MeasureResult = intrinsic_tree_measure( + tree, 100, + Constraints{ .min_width = 0, .max_width = unbounded_extent(), .min_height = 0, .max_height = unbounded_extent() } + ); + expect(state, row.error == IntrinsicError.none && row.visited_nodes == 3, + "intrinsic row visits its exact retained subtree"); + expect(state, row.intrinsic.minimum.width == 33 && row.intrinsic.preferred.width == 58 && + row.intrinsic.maximum.width == 108, + "row intrinsic widths aggregate children gaps and padding exactly"); + expect(state, row.intrinsic.preferred.height == 22 && row.baseline == 14, + "row baseline aggregation reserves ascent descent and physical padding"); + expect(state, row.resolved.width == 58 && row.resolved.height == 22 && + row.width_rule == MeasureRule.intrinsic_preferred && row.height_rule == MeasureRule.intrinsic_preferred, + "unbounded constraints preserve preferred intrinsic size"); + + let constrained: MeasureResult = intrinsic_tree_measure(tree, 100, tight_constraints(40, 12)); + expect(state, constrained.resolved.width == 40 && constrained.resolved.height == 12 && + constrained.width_rule == MeasureRule.maximum_constraint && + constrained.height_rule == MeasureRule.maximum_constraint, + "tight constraints retain the exact rule that overrode preferred size"); + expect(state, constrained.overflow_width == 0 && constrained.overflow_height == 2 && + constrained.baseline == 12, + "resolved baselines clamp safely while minimum overflow remains explicit"); + let expanded: MeasureResult = intrinsic_tree_measure( + tree, 100, + Constraints{ .min_width = 100, .max_width = 120, .min_height = 30, .max_height = 40 } + ); + expect(state, expanded.resolved.width == 100 && expanded.resolved.height == 30 && + expanded.width_rule == MeasureRule.minimum_constraint && + expanded.height_rule == MeasureRule.minimum_constraint, + "minimum constraints expand intrinsic preference with explicit per-axis rules"); + intrinsic_tree_free(&tree); +} + +fn column_overlay_contract(state: *TestState) void { + let tree: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&tree, intrinsic_group(200, IntrinsicKind.column, 4, insets_all(2))); + intrinsic_tree_add_node(&tree, intrinsic_group(201, IntrinsicKind.overlay, 0, insets_all(1))); + intrinsic_tree_add_node(&tree, intrinsic_leaf(1, leaf_metrics(20, 10, 6), 1)); + intrinsic_tree_add_node(&tree, intrinsic_leaf(2, leaf_metrics(30, 12, 8), 2)); + intrinsic_tree_add_node(&tree, intrinsic_leaf(3, leaf_metrics(8, 6, 4), 3)); + intrinsic_tree_add_child(&tree, 201, 1); + intrinsic_tree_add_child(&tree, 201, 2); + intrinsic_tree_add_child(&tree, 200, 201); + intrinsic_tree_add_child(&tree, 200, 3); + let result: MeasureResult = intrinsic_tree_measure( + tree, 200, + Constraints{ .min_width = 0, .max_width = unbounded_extent(), .min_height = 0, .max_height = unbounded_extent() } + ); + expect(state, result.error == IntrinsicError.none && result.visited_nodes == 5, + "nested column and overlay measurement traverses each retained node once"); + expect(state, result.intrinsic.preferred.width == 36 && result.intrinsic.preferred.height == 28, + "overlay max and column sum aggregation compose deterministically"); + expect(state, result.baseline == 11 && result.measure_hash != 0, + "column baseline follows its first child and measurement retains identity"); + + let repeated: MeasureResult = intrinsic_tree_measure( + tree, 200, + Constraints{ .min_width = 0, .max_width = unbounded_extent(), .min_height = 0, .max_height = unbounded_extent() } + ); + expect(state, repeated.measure_hash == result.measure_hash, + "identical intrinsic trees produce bit-identical measurement identity"); + let changed_constraints: MeasureResult = intrinsic_tree_measure( + tree, 200, + Constraints{ + .min_width = 0, .max_width = unbounded_extent() - 1, + .min_height = 0, .max_height = unbounded_extent(), + } + ); + expect(state, changed_constraints.resolved.width == result.resolved.width && + changed_constraints.measure_hash != result.measure_hash, + "constraint input remains visible in identity even when resolved size is unchanged"); + tree.nodes.data[4].metrics.preferred.width = 9; + tree.nodes.data[4].metrics.maximum.width = 16; + let changed: MeasureResult = intrinsic_tree_measure( + tree, 200, + Constraints{ .min_width = 0, .max_width = unbounded_extent(), .min_height = 0, .max_height = unbounded_extent() } + ); + expect(state, changed.measure_hash != result.measure_hash, + "intrinsic content changes alter deterministic measurement identity"); + intrinsic_tree_free(&tree); +} + +fn flex_priority_contract(state: *TestState) void { + let low_metrics: IntrinsicMetrics = intrinsic_metrics(10, 10, 30, 10, 60, 10, 8); + let high_metrics: IntrinsicMetrics = intrinsic_metrics(10, 10, 30, 10, 60, 10, 8); + let required_metrics: IntrinsicMetrics = intrinsic_metrics(20, 10, 20, 10, 20, 10, 8); + let items: ArrayList[FlexItem] = make[FlexItem](3); + push[FlexItem](&items, flex_item_intrinsic(1, low_metrics, FlexAxis.row, FlexPriority.low)); + push[FlexItem](&items, flex_item_intrinsic(2, high_metrics, FlexAxis.row, FlexPriority.high)); + push[FlexItem](&items, flex_item_intrinsic(3, required_metrics, FlexAxis.row, FlexPriority.required)); + expect(state, items.data[0].shrink == 4 && items.data[1].shrink == 1 && + items.data[2].shrink == 0 && items.data[2].min_main == 20, + "Flex priorities map to explicit shrink resistance and intrinsic bounds"); + let result: FlexResult = flex_layout( + items, tight_constraints(60, 10), flex_style(FlexAxis.row, 0, insets_all(0)) + ); + expect(state, result.error == FlexError.none && result.placements.data[2].frame.width == 20, + "required intrinsic items preserve their exact extent under pressure"); + expect(state, result.placements.data[0].frame.width < result.placements.data[1].frame.width, + "lower priority content yields more space than higher priority content"); + flex_free(&result); + free[FlexItem](&items); + + let column: FlexItem = flex_item_intrinsic(4, low_metrics, FlexAxis.column, FlexPriority.normal); + expect(state, column.basis == low_metrics.preferred.height && + column.min_main == low_metrics.minimum.height && column.max_main == low_metrics.maximum.height && + column.cross == low_metrics.preferred.width, + "column intrinsic conversion maps vertical main and horizontal cross axes explicitly"); +} + +fn invalidation_contract(state: *TestState) void { + let width: State[i64] = state_make[i64](700, 20); + let measure_context: ViewContext = view_context_make(node_key_scoped(10, 2)); + let layout_context: ViewContext = view_context_make(node_key_scoped(11, 4)); + _ = state_read[i64](&width, &measure_context); + _ = state_read[i64](&width, &layout_context); + let trace: LayoutTrace = layout_trace_make(); + layout_trace_record( + &trace, measure_context, node_key(100), LayoutPhase.measure, LayoutReason.intrinsic_content + ); + layout_trace_record( + &trace, layout_context, node_key(101), LayoutPhase.layout, LayoutReason.breakpoint + ); + expect(state, trace.dependencies.len == 2, + "layout trace retains independent measure and placement dependencies"); + layout_trace_record( + &trace, measure_context, node_key(100), LayoutPhase.measure, LayoutReason.intrinsic_content + ); + expect(state, trace.dependencies.len == 2, + "duplicate dependency reads collapse without losing their phase or rule"); + + let change: StateChange = state_set[i64](&width, 30); + let invalidated: LayoutInvalidationResult = layout_trace_invalidate(trace, change); + expect(state, invalidated.causes.len == 2, + "one state change reports every affected layout operation"); + expect(state, invalidated.causes.data[0].node.value == 10 && + invalidated.causes.data[0].node.generation == 2 && + invalidated.causes.data[0].ancestor.value == 100 && + invalidated.causes.data[0].phase == LayoutPhase.measure && + invalidated.causes.data[0].reason == LayoutReason.intrinsic_content, + "measurement invalidation names the exact reader generation ancestor phase and rule"); + expect(state, invalidated.causes.data[1].node.value == 11 && + invalidated.causes.data[1].ancestor.value == 101 && + invalidated.causes.data[1].phase == LayoutPhase.layout && + invalidated.causes.data[1].reason == LayoutReason.breakpoint && + invalidated.causes.data[1].read_revision == 0 && invalidated.causes.data[1].to_revision == 1, + "placement invalidation retains the exact state revision edge and breakpoint reason"); + layout_invalidation_free(&invalidated); + + let unrelated: LayoutInvalidationResult = layout_trace_invalidate( + trace, StateChange{ .state_id = 999, .from_revision = 0, .to_revision = 1 } + ); + expect(state, unrelated.causes.len == 0, + "unread state produces no phantom layout work"); + layout_invalidation_free(&unrelated); + layout_trace_free(&trace); + view_context_free(&measure_context); + view_context_free(&layout_context); +} + +fn failure_contract(state: *TestState) void { + let duplicate: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&duplicate, intrinsic_leaf(1, leaf_metrics(10, 10, 5), 0)); + intrinsic_tree_add_node(&duplicate, intrinsic_leaf(1, leaf_metrics(10, 10, 5), 0)); + let duplicate_result: MeasureResult = intrinsic_tree_measure(duplicate, 1, tight_constraints(10, 10)); + expect(state, duplicate_result.error == IntrinsicError.duplicate_node && + duplicate_result.error_node == 1 && duplicate_result.error_edge == 1, + "duplicate retained IDs fail at the exact node before measurement"); + intrinsic_tree_free(&duplicate); + + let missing: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&missing, intrinsic_group(10, IntrinsicKind.row, 0, insets_all(0))); + intrinsic_tree_add_child(&missing, 10, 99); + let missing_result: MeasureResult = intrinsic_tree_measure(missing, 10, tight_constraints(10, 10)); + expect(state, missing_result.error == IntrinsicError.missing_child && + missing_result.error_node == 99 && missing_result.error_edge == 0, + "missing intrinsic children fail at the exact edge"); + intrinsic_tree_free(&missing); + + let missing_parent: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&missing_parent, intrinsic_leaf(11, leaf_metrics(10, 10, 5), 0)); + intrinsic_tree_add_child(&missing_parent, 99, 11); + let missing_parent_result: MeasureResult = intrinsic_tree_measure( + missing_parent, 11, tight_constraints(10, 10) + ); + expect(state, missing_parent_result.error == IntrinsicError.missing_node && + missing_parent_result.error_node == 99 && missing_parent_result.error_edge == 0, + "missing intrinsic parents fail as missing retained nodes rather than child errors"); + intrinsic_tree_free(&missing_parent); + + let orphan: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&orphan, intrinsic_group(10, IntrinsicKind.row, 0, insets_all(0))); + intrinsic_tree_add_node(&orphan, intrinsic_leaf(11, leaf_metrics(10, 10, 5), 0)); + let orphan_result: MeasureResult = intrinsic_tree_measure(orphan, 10, tight_constraints(10, 10)); + expect(state, orphan_result.error == IntrinsicError.orphan_node && + orphan_result.error_node == 11 && orphan_result.visited_nodes == 1, + "retained intrinsic nodes cannot silently remain outside the measured root tree"); + intrinsic_tree_free(&orphan); + + let shared: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&shared, intrinsic_group(10, IntrinsicKind.row, 0, insets_all(0))); + intrinsic_tree_add_node(&shared, intrinsic_group(11, IntrinsicKind.column, 0, insets_all(0))); + intrinsic_tree_add_node(&shared, intrinsic_leaf(12, leaf_metrics(10, 10, 5), 0)); + intrinsic_tree_add_child(&shared, 10, 12); + intrinsic_tree_add_child(&shared, 11, 12); + intrinsic_tree_add_child(&shared, 10, 11); + let shared_result: MeasureResult = intrinsic_tree_measure(shared, 10, tight_constraints(10, 10)); + expect(state, shared_result.error == IntrinsicError.multiple_parents && + shared_result.error_node == 12 && shared_result.error_edge == 1, + "one retained intrinsic node cannot have multiple layout owners"); + intrinsic_tree_free(&shared); + + let leaf_parent: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&leaf_parent, intrinsic_leaf(10, leaf_metrics(10, 10, 5), 0)); + intrinsic_tree_add_node(&leaf_parent, intrinsic_leaf(11, leaf_metrics(10, 10, 5), 0)); + intrinsic_tree_add_child(&leaf_parent, 10, 11); + let leaf_parent_result: MeasureResult = intrinsic_tree_measure( + leaf_parent, 10, tight_constraints(10, 10) + ); + expect(state, leaf_parent_result.error == IntrinsicError.invalid_edge && + leaf_parent_result.error_node == 10 && leaf_parent_result.error_edge == 0, + "leaf nodes cannot masquerade as intrinsic layout containers"); + intrinsic_tree_free(&leaf_parent); + + let cycle: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&cycle, intrinsic_group(10, IntrinsicKind.row, 0, insets_all(0))); + intrinsic_tree_add_node(&cycle, intrinsic_group(11, IntrinsicKind.column, 0, insets_all(0))); + intrinsic_tree_add_child(&cycle, 10, 11); + intrinsic_tree_add_child(&cycle, 11, 10); + let cycle_result: MeasureResult = intrinsic_tree_measure(cycle, 10, tight_constraints(10, 10)); + expect(state, cycle_result.error == IntrinsicError.cycle && cycle_result.error_node == 10 && + cycle_result.error_edge == 1, + "intrinsic cycles fail visibly without recursive runaway"); + intrinsic_tree_free(&cycle); + + let malformed: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&malformed, intrinsic_leaf( + 1, intrinsic_metrics(20, 10, 10, 10, 30, 20, 5), 0 + )); + let malformed_result: MeasureResult = intrinsic_tree_measure(malformed, 1, tight_constraints(10, 10)); + expect(state, malformed_result.error == IntrinsicError.invalid_node && malformed_result.error_node == 1, + "nonmonotonic intrinsic ranges fail before constraint resolution"); + intrinsic_tree_free(&malformed); + + let overflow: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&overflow, intrinsic_group(10, IntrinsicKind.row, 0, insets_all(0))); + intrinsic_tree_add_node(&overflow, intrinsic_leaf( + 1, intrinsic_metrics(unbounded_extent(), 1, unbounded_extent(), 1, unbounded_extent(), 1, 1), 0 + )); + intrinsic_tree_add_node(&overflow, intrinsic_leaf( + 2, intrinsic_metrics(1, 1, 1, 1, 1, 1, 1), 0 + )); + intrinsic_tree_add_child(&overflow, 10, 1); + intrinsic_tree_add_child(&overflow, 10, 2); + let overflow_result: MeasureResult = intrinsic_tree_measure( + overflow, 10, + Constraints{ .min_width = 0, .max_width = unbounded_extent(), .min_height = 0, .max_height = unbounded_extent() } + ); + expect(state, overflow_result.error == IntrinsicError.arithmetic_overflow && + overflow_result.error_node == 10 && overflow_result.error_edge == 1, + "finite intrinsic aggregation cannot wrap bounded geometry"); + intrinsic_tree_free(&overflow); + + let deep: IntrinsicTree = intrinsic_tree_make(); + let node_id: i64 = 1; + while (node_id <= (intrinsic_depth_limit() as i64) + 1) { + if (node_id == (intrinsic_depth_limit() as i64) + 1) { + intrinsic_tree_add_node(&deep, intrinsic_leaf(node_id, leaf_metrics(1, 1, 1), 0)); + } else { + intrinsic_tree_add_node(&deep, intrinsic_group(node_id, IntrinsicKind.column, 0, insets_all(0))); + intrinsic_tree_add_child(&deep, node_id, node_id + 1); + } + node_id = node_id + 1; + } + let deep_result: MeasureResult = intrinsic_tree_measure( + deep, 1, + Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + } + ); + expect(state, deep_result.error == IntrinsicError.depth_exceeded && + deep_result.error_node == (intrinsic_depth_limit() as i64) + 1 && + deep_result.visited_nodes == intrinsic_depth_limit(), + "adversarial retained depth fails before exhausting the native call stack"); + intrinsic_tree_free(&deep); +} + +fn stability_contract(state: *TestState) void { + let tree: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&tree, intrinsic_leaf(1, leaf_metrics(20, 10, 7), 4)); + let stability: IntrinsicStability = intrinsic_stability_make(); + let first: IntrinsicStabilityResult = intrinsic_stability_observe(&stability, tree); + expect(state, first.error == IntrinsicError.none && stability.witnesses.len == 1, + "first intrinsic observation retains a revisioned stability witness"); + + tree.nodes.data[0].metrics.preferred.width = 21; + let unstable: IntrinsicStabilityResult = intrinsic_stability_observe(&stability, tree); + expect(state, unstable.error == IntrinsicError.unstable_measurement && unstable.node_id == 1 && + unstable.previous_hash != unstable.current_hash, + "changed intrinsic input at one revision fails as unstable measurement"); + + tree.nodes.data[0].revision = 5; + let revised: IntrinsicStabilityResult = intrinsic_stability_observe(&stability, tree); + expect(state, revised.error == IntrinsicError.none && stability.witnesses.data[0].revision == 5, + "an explicit content revision permits and records changed intrinsic input"); + tree.nodes.data[0].revision = 4; + let stale: IntrinsicStabilityResult = intrinsic_stability_observe(&stability, tree); + expect(state, stale.error == IntrinsicError.invalid_node && stale.node_id == 1, + "intrinsic content revisions cannot regress behind retained stability truth"); + intrinsic_stability_free(&stability); + intrinsic_tree_free(&tree); +} + +fn property_contract(state: *TestState) void { + let all_valid: i32 = 1; + let width: i64 = 1; + while (width <= 256) { + let tree: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&tree, intrinsic_leaf(1, leaf_metrics(100, 40, 25), width)); + let result: MeasureResult = intrinsic_tree_measure(tree, 1, tight_constraints(width, width / 2)); + if (result.error != IntrinsicError.none || result.resolved.width != width || + result.resolved.height != width / 2 || result.baseline > result.resolved.height) { + all_valid = 0; + } + intrinsic_tree_free(&tree); + width = width + 1; + } + expect(state, all_valid == 1, + "256-constraint sweep preserves exact bounded size and baseline invariants"); + + let all_rows_valid: i32 = 1; + let count: i64 = 1; + while (count <= 32) { + let gap: i64 = 0; + while (gap <= 8) { + let tree: IntrinsicTree = intrinsic_tree_make(); + intrinsic_tree_add_node(&tree, intrinsic_group(1000, IntrinsicKind.row, gap, insets_all(1))); + let expected_width: i64 = 2; + let child: i64 = 1; + while (child <= count) { + let width: i64 = (child % 7) + 1; + expected_width = expected_width + width; + if (child > 1) { expected_width = expected_width + gap; } + intrinsic_tree_add_node(&tree, intrinsic_leaf( + child, intrinsic_metrics(width, 1, width, 2, width, 3, 1), child + )); + intrinsic_tree_add_child(&tree, 1000, child); + child = child + 1; + } + let measured: MeasureResult = intrinsic_tree_measure( + tree, 1000, + Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + } + ); + if (measured.error != IntrinsicError.none || + measured.intrinsic.preferred.width != expected_width || + measured.visited_nodes != count + 1) { all_rows_valid = 0; } + intrinsic_tree_free(&tree); + gap = gap + 1; + } + count = count + 1; + } + expect(state, all_rows_valid == 1, + "288-tree aggregation sweep conserves child extents gaps padding and ownership"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + aggregation_contract(&state); + column_overlay_contract(&state); + flex_priority_contract(&state); + invalidation_contract(&state); + failure_contract(&state); + stability_contract(&state); + property_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Measurement contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/motion_contract.zag b/tests/motion_contract.zag new file mode 100644 index 0000000..a2eb713 --- /dev/null +++ b/tests/motion_contract.zag @@ -0,0 +1,227 @@ +@import("../src/motion/motion.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn timeline() ArrayList[MotionKeyframe] { + let frames: ArrayList[MotionKeyframe] = make[MotionKeyframe](3); + push[MotionKeyframe](&frames, MotionKeyframe{ .time_us = 0, .value = 0 }); + push[MotionKeyframe](&frames, MotionKeyframe{ .time_us = 100000, .value = 10 * unit_scale() }); + push[MotionKeyframe](&frames, MotionKeyframe{ .time_us = 200000, .value = 0 }); + return frames; +} + +fn idle_and_refresh_contract(state: *TestState) void { + let scheduler: MotionScheduler = motion_scheduler_make(60); + let first: MotionTickResult = motion_tick(&scheduler, 1000); + let idle: MotionTickResult = motion_tick(&scheduler, 2000); + expect(state, first.error == MotionError.none && idle.did_work == 0 && + idle.request_next_frame == 0 && scheduler.events.len == 0, + "idle clocks perform no motion work and schedule no phantom frame"); + expect(state, motion_set_refresh_rate(&scheduler, 120) == MotionError.none && + scheduler.refresh_hz == 120 && scheduler.frame_interval_us == 8333, + "refresh-rate changes update the explicit frame deadline"); + expect(state, scheduler.events.len == 1 && + scheduler.events.data[0].reason == MotionFrameReason.refresh_rate_change, + "refresh-rate changes retain an observable frame reason"); + expect(state, motion_set_refresh_rate(&scheduler, 12) == MotionError.invalid_spec && + scheduler.refresh_hz == 120, + "invalid display rates fail without mutating scheduler truth"); + motion_scheduler_free(&scheduler); +} + +fn spring_determinism_contract(state: *TestState) void { + let split: MotionScheduler = motion_scheduler_make(120); + let grouped: MotionScheduler = motion_scheduler_make(120); + _ = motion_set_clock(&split, 0); + _ = motion_set_clock(&grouped, 0); + let spec: SpringSpec = spring_spec(220, 28); + _ = motion_start_spring(&split, 1, 0, 10 * unit_scale(), spec, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + _ = motion_start_spring(&grouped, 1, 0, 10 * unit_scale(), spec, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + _ = motion_tick(&split, 8333); + _ = motion_tick(&split, 16666); + _ = motion_tick(&split, 25000); + _ = motion_tick(&grouped, 25000); + let a: MotionSample = motion_sample(split, 1); + let b: MotionSample = motion_sample(grouped, 1); + expect(state, a.value == b.value && a.velocity == b.velocity && a.status == b.status, + "fixed-step springs are independent of refresh callback grouping"); + expect(state, a.value > 0 && a.value < 10 * unit_scale() && a.velocity > 0, + "spring integration advances position and velocity without teleporting"); + expect(state, motion_needs_frame(split) == 1 && motion_tick(&split, 25000).request_next_frame == 1, + "running motion requests another frame even when a callback repeats the same clock"); + + let replay_a: MotionScheduler = motion_scheduler_make(120); + let replay_b: MotionScheduler = motion_scheduler_make(120); + _ = motion_set_clock(&replay_a, 0); _ = motion_set_clock(&replay_b, 0); + _ = motion_start_spring(&replay_a, 8, 0, 5 * unit_scale(), spec, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + _ = motion_start_spring(&replay_b, 8, 0, 5 * unit_scale(), spec, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + _ = motion_tick(&replay_a, 8333); _ = motion_tick(&replay_b, 8333); + _ = motion_tick(&replay_a, 16666); _ = motion_tick(&replay_b, 16666); + expect(state, motion_scheduler_hash(replay_a) == motion_scheduler_hash(replay_b), + "identical supplied clocks produce identical full motion evidence"); + + let settling: MotionScheduler = motion_scheduler_make(120); + _ = motion_set_clock(&settling, 0); + _ = motion_start_spring(&settling, 9, 0, 5 * unit_scale(), spec, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + _ = motion_tick(&settling, 1000000); + _ = motion_tick(&settling, 2000000); + _ = motion_tick(&settling, 3000000); + _ = motion_tick(&settling, 4000000); + let last: MotionTickResult = motion_tick(&settling, 5000000); + let settled: MotionSample = motion_sample(settling, 9); + let settled_events: i32 = settling.events.len; + let after: MotionTickResult = motion_tick(&settling, 5001000); + expect(state, settled.status == MotionStatus.settled && settled.value == 5 * unit_scale() && + settled.velocity == 0 && last.request_next_frame == 0 && after.did_work == 0 && + settling.events.len == settled_events, + "settled springs snap exactly and produce no continuous idle work"); + motion_scheduler_free(&settling); + motion_scheduler_free(&replay_a); motion_scheduler_free(&replay_b); + motion_scheduler_free(&split); motion_scheduler_free(&grouped); +} + +fn continuity_contract(state: *TestState) void { + let scheduler: MotionScheduler = motion_scheduler_make(120); + _ = motion_set_clock(&scheduler, 0); + _ = motion_start_spring(&scheduler, 2, 0, 12 * unit_scale(), spring_spec(240, 30), MotionPreference.full, MotionReducedSubstitution.snap_to_end); + _ = motion_tick(&scheduler, 50000); + let before: MotionSample = motion_sample(scheduler, 2); + _ = motion_retarget(&scheduler, 2, 18 * unit_scale(), MotionFrameReason.interrupt, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + let interrupted: MotionSample = motion_sample(scheduler, 2); + expect(state, interrupted.value == before.value && interrupted.velocity == before.velocity && + interrupted.target == 18 * unit_scale(), + "interruption preserves exact position and incoming velocity"); + _ = motion_retarget(&scheduler, 2, 0, MotionFrameReason.reverse, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + let reversed: MotionSample = motion_sample(scheduler, 2); + expect(state, reversed.value == before.value && reversed.velocity == before.velocity && reversed.target == 0, + "reversal preserves exact position and incoming velocity"); + _ = motion_retarget(&scheduler, 2, 20 * unit_scale(), MotionFrameReason.resize, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + let resized: MotionSample = motion_sample(scheduler, 2); + expect(state, resized.value == before.value && resized.velocity == before.velocity && resized.target == 20 * unit_scale(), + "resize retargeting preserves continuity while changing destination"); + _ = motion_gesture_handoff(&scheduler, 2, 7 * unit_scale(), 90 * unit_scale(), 15 * unit_scale()); + let handed: MotionSample = motion_sample(scheduler, 2); + expect(state, handed.value == 7 * unit_scale() && handed.velocity == 90 * unit_scale() && + handed.target == 15 * unit_scale() && handed.status == MotionStatus.running, + "gesture handoff transfers position and velocity exactly"); + expect(state, scheduler.events.data[scheduler.events.len - 4].reason == MotionFrameReason.interrupt && + scheduler.events.data[scheduler.events.len - 3].reason == MotionFrameReason.reverse && + scheduler.events.data[scheduler.events.len - 2].reason == MotionFrameReason.resize && + scheduler.events.data[scheduler.events.len - 1].reason == MotionFrameReason.gesture_handoff, + "continuity changes retain ordered semantic frame reasons"); + motion_scheduler_free(&scheduler); +} + +fn keyframe_contract(state: *TestState) void { + let frames: ArrayList[MotionKeyframe] = timeline(); + let scheduler: MotionScheduler = motion_scheduler_make(60); + _ = motion_set_clock(&scheduler, 0); + _ = motion_start_keyframes(&scheduler, 3, frames, MotionPreference.full, MotionReducedSubstitution.snap_to_end); + frames.data[1].value = 999; + _ = motion_tick(&scheduler, 50000); + let rising: MotionSample = motion_sample(scheduler, 3); + expect(state, rising.value == 5 * unit_scale() && rising.velocity == 100 * unit_scale(), + "keyframes interpolate exact position and segment velocity from owned data"); + _ = motion_tick(&scheduler, 150000); + let falling: MotionSample = motion_sample(scheduler, 3); + expect(state, falling.value == 5 * unit_scale() && falling.velocity == -100 * unit_scale(), + "keyframe direction changes preserve exact deterministic velocity"); + let final_tick: MotionTickResult = motion_tick(&scheduler, 200000); + let finished: MotionSample = motion_sample(scheduler, 3); + expect(state, finished.value == 0 && finished.velocity == 0 && finished.status == MotionStatus.settled && + final_tick.request_next_frame == 0, + "keyframe timelines settle exactly and stop scheduling frames"); + let event_count: i32 = scheduler.events.len; + let idle: MotionTickResult = motion_tick(&scheduler, 201000); + expect(state, idle.did_work == 0 && scheduler.events.len == event_count, + "settled timelines remain completely idle"); + free[MotionKeyframe](&frames); + motion_scheduler_free(&scheduler); +} + +fn reduced_motion_contract(state: *TestState) void { + let scheduler: MotionScheduler = motion_scheduler_make(60); + _ = motion_set_clock(&scheduler, 0); + _ = motion_start_spring(&scheduler, 4, 0, 20 * unit_scale(), spring_spec(220, 28), MotionPreference.reduced, MotionReducedSubstitution.snap_to_end); + let snapped: MotionSample = motion_sample(scheduler, 4); + expect(state, snapped.value == 20 * unit_scale() && snapped.status == MotionStatus.settled && + scheduler.events.data[0].reason == MotionFrameReason.reduced_motion, + "reduced motion uses an explicit snap substitution for spatial movement"); + + let fade: ArrayList[MotionKeyframe] = make[MotionKeyframe](2); + push[MotionKeyframe](&fade, MotionKeyframe{ .time_us = 0, .value = 0 }); + push[MotionKeyframe](&fade, MotionKeyframe{ .time_us = 80000, .value = unit_scale() }); + _ = motion_start_keyframes(&scheduler, 5, fade, MotionPreference.reduced, MotionReducedSubstitution.opacity_fade); + _ = motion_tick(&scheduler, 40000); + let faded: MotionSample = motion_sample(scheduler, 5); + expect(state, faded.value == unit_scale() / 2 && faded.status == MotionStatus.running, + "reduced motion can use a caller-authored opacity substitution timeline"); + free[MotionKeyframe](&fade); + motion_scheduler_free(&scheduler); +} + +fn failure_contract(state: *TestState) void { + let scheduler: MotionScheduler = motion_scheduler_make(60); + expect(state, motion_start_spring(&scheduler, 1, 0, 64, spring_spec(220, 28), MotionPreference.full, MotionReducedSubstitution.snap_to_end) == MotionError.invalid_spec, + "tracks cannot start without an authoritative supplied clock"); + _ = motion_set_clock(&scheduler, 1000); + _ = motion_start_spring(&scheduler, 1, 0, 64, spring_spec(220, 28), MotionPreference.full, MotionReducedSubstitution.snap_to_end); + expect(state, motion_start_spring(&scheduler, 1, 0, 64, spring_spec(220, 28), MotionPreference.full, MotionReducedSubstitution.snap_to_end) == MotionError.duplicate_id && scheduler.tracks.len == 1, + "duplicate stable motion IDs fail before allocation"); + let before: MotionSample = motion_sample(scheduler, 1); + expect(state, motion_set_clock(&scheduler, 2000) == MotionError.clock_advance_requires_tick && + scheduler.now_us == 1000, + "running motion cannot be skipped by advancing the clock outside tick"); + expect(state, motion_tick(&scheduler, 999).error == MotionError.clock_regression && + scheduler.now_us == 1000 && motion_sample(scheduler, 1).value == before.value, + "clock regression fails before scheduler or track mutation"); + expect(state, motion_tick(&scheduler, 60001001).error == MotionError.clock_step_too_large && + scheduler.now_us == 1000, + "unbounded suspend jumps fail visibly before expensive catch-up"); + expect(state, motion_retarget(&scheduler, 99, 0, MotionFrameReason.interrupt, MotionPreference.full, MotionReducedSubstitution.snap_to_end) == MotionError.missing_track, + "retargeting a missing stable ID fails visibly"); + + let malformed: ArrayList[MotionKeyframe] = make[MotionKeyframe](2); + push[MotionKeyframe](&malformed, MotionKeyframe{ .time_us = 100, .value = 0 }); + push[MotionKeyframe](&malformed, MotionKeyframe{ .time_us = 50, .value = 64 }); + expect(state, motion_start_keyframes(&scheduler, 2, malformed, MotionPreference.full, MotionReducedSubstitution.snap_to_end) == MotionError.invalid_keyframes && scheduler.tracks.len == 1, + "malformed keyframe order fails before ownership transfer"); + free[MotionKeyframe](&malformed); + + let spatial_fade: ArrayList[MotionKeyframe] = make[MotionKeyframe](2); + push[MotionKeyframe](&spatial_fade, MotionKeyframe{ .time_us = 0, .value = 0 }); + push[MotionKeyframe](&spatial_fade, MotionKeyframe{ .time_us = 80000, .value = 2 * unit_scale() }); + expect(state, motion_start_keyframes(&scheduler, 3, spatial_fade, MotionPreference.reduced, MotionReducedSubstitution.opacity_fade) == MotionError.invalid_keyframes, + "reduced opacity substitution rejects spatial values masquerading as a fade"); + expect(state, motion_start_spring(&scheduler, 4, 0, 64, spring_spec(220, 28), MotionPreference.reduced, MotionReducedSubstitution.opacity_fade) == MotionError.invalid_spec, + "spatial springs require an explicit snap reduced-motion substitution"); + free[MotionKeyframe](&spatial_fade); + motion_scheduler_free(&scheduler); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + idle_and_refresh_contract(&state); + spring_determinism_contract(&state); + continuity_contract(&state); + keyframe_contract(&state); + reduced_motion_contract(&state); + failure_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Motion contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/overlay_contract.zag b/tests/overlay_contract.zag new file mode 100644 index 0000000..98cd10d --- /dev/null +++ b/tests/overlay_contract.zag @@ -0,0 +1,203 @@ +@import("std:list") +@import("../src/layout/overlay.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn metrics(min_width: i64, min_height: i64, width: i64, height: i64) IntrinsicMetrics { + return intrinsic_metrics(min_width, min_height, width, height, width * 2, height * 2, height - 1); +} + +fn alignment_contract(state: *TestState) void { + let items: ArrayList[OverlayItem] = make[OverlayItem](4); + let start: OverlayItem = overlay_item(1, metrics(2, 2, 4, 3)); + start.insets = insets_all(1); + start.z_order = 2; + push[OverlayItem](&items, start); + let centered: OverlayItem = overlay_item(2, metrics(2, 2, 4, 4)); + centered.horizontal = PlacementAlignment.center; + centered.vertical = PlacementAlignment.center; + centered.z_order = 1; + push[OverlayItem](&items, centered); + let ended: OverlayItem = overlay_item(3, metrics(2, 2, 3, 2)); + ended.horizontal = PlacementAlignment.end; + ended.vertical = PlacementAlignment.end; + push[OverlayItem](&items, ended); + let stretched: OverlayItem = overlay_item(4, intrinsic_metrics(3, 2, 4, 3, 20, 20, 2)); + stretched.horizontal = PlacementAlignment.stretch; + stretched.vertical = PlacementAlignment.stretch; + push[OverlayItem](&items, stretched); + + let result: OverlayResult = overlay_layout( + items, tight_constraints(20, 14), overlay_style(insets_all(2), LayoutDirection.ltr) + ); + expect(state, result.error == OverlayError.none && result.placements.len == 4 && + result.container.width == 20 && result.container.height == 14, + "overlay resolves every stable item inside exact constrained bounds"); + expect(state, result.placements.data[0].frame.x == 3 && result.placements.data[0].frame.y == 3 && + result.placements.data[0].z_order == 2, + "logical start composes outer padding item insets and retained z-order"); + expect(state, result.placements.data[1].frame.x == 8 && result.placements.data[1].frame.y == 5, + "overlay center alignment distributes odd-independent free space deterministically"); + expect(state, result.placements.data[2].frame.x == 15 && result.placements.data[2].frame.y == 10, + "overlay end alignment reaches the physical trailing content edges"); + expect(state, result.placements.data[3].frame.x == 2 && result.placements.data[3].frame.y == 2 && + result.placements.data[3].frame.width == 16 && result.placements.data[3].frame.height == 10, + "overlay stretch consumes available content without discarding intrinsic maxima"); + expect(state, result.placements.data[0].baseline == 5 && result.layout_hash != 0 && + result.overflow_width == 0 && result.overflow_height == 0, + "overlay retains baseline identity and explicit clean overflow truth"); + + let repeated: OverlayResult = overlay_layout( + items, tight_constraints(20, 14), overlay_style(insets_all(2), LayoutDirection.ltr) + ); + expect(state, repeated.layout_hash == result.layout_hash, + "identical overlay inputs produce identical layout identity"); + overlay_free(&repeated); + overlay_free(&result); + free[OverlayItem](&items); +} + +fn rtl_contract(state: *TestState) void { + let items: ArrayList[OverlayItem] = make[OverlayItem](2); + let start: OverlayItem = overlay_item(1, metrics(2, 2, 4, 3)); + push[OverlayItem](&items, start); + let end: OverlayItem = overlay_item(2, metrics(2, 2, 4, 3)); + end.horizontal = PlacementAlignment.end; + push[OverlayItem](&items, end); + let rtl: OverlayResult = overlay_layout( + items, tight_constraints(20, 10), overlay_style(insets_all(2), LayoutDirection.rtl) + ); + expect(state, rtl.placements.data[0].frame.x == 14 && rtl.placements.data[1].frame.x == 2, + "RTL swaps logical start and end while preserving physical padding truth"); + let ltr: OverlayResult = overlay_layout( + items, tight_constraints(20, 10), overlay_style(insets_all(2), LayoutDirection.ltr) + ); + expect(state, rtl.layout_hash != ltr.layout_hash, + "layout identity records direction even when other overlay inputs match"); + overlay_free(<r); overlay_free(&rtl); free[OverlayItem](&items); +} + +fn intrinsic_and_overflow_contract(state: *TestState) void { + let items: ArrayList[OverlayItem] = make[OverlayItem](1); + let item: OverlayItem = overlay_item(1, metrics(8, 6, 12, 10)); + item.insets = Insets{ .top = 2, .right = 3, .bottom = 4, .left = 5 }; + push[OverlayItem](&items, item); + let intrinsic: OverlayResult = overlay_layout( + items, + Constraints{ + .min_width = 0, .max_width = unbounded_extent(), + .min_height = 0, .max_height = unbounded_extent(), + }, + overlay_style(insets_all(1), LayoutDirection.ltr) + ); + expect(state, intrinsic.container.width == 22 && intrinsic.container.height == 18, + "unbounded overlay container derives exact preferred size from padding and insets"); + overlay_free(&intrinsic); + + let tight: OverlayResult = overlay_layout( + items, tight_constraints(10, 8), overlay_style(insets_all(1), LayoutDirection.ltr) + ); + expect(state, tight.placements.data[0].frame.width == 8 && tight.placements.data[0].frame.height == 6 && + tight.overflow_width == 8 && tight.overflow_height == 6, + "minimum intrinsic overlay size remains visible as per-axis overflow under pressure"); + overlay_free(&tight); free[OverlayItem](&items); + + let empty: ArrayList[OverlayItem] = make[OverlayItem](1); + let padding_overflow: OverlayResult = overlay_layout( + empty, tight_constraints(4, 4), overlay_style(insets_all(3), LayoutDirection.ltr) + ); + expect(state, padding_overflow.overflow_width == 2 && padding_overflow.overflow_height == 2, + "overlay reports outer padding overflow even without children"); + overlay_free(&padding_overflow); free[OverlayItem](&empty); +} + +fn failure_contract(state: *TestState) void { + let items: ArrayList[OverlayItem] = make[OverlayItem](2); + push[OverlayItem](&items, overlay_item(1, metrics(1, 1, 2, 2))); + push[OverlayItem](&items, overlay_item(1, metrics(1, 1, 2, 2))); + let duplicate: OverlayResult = overlay_layout( + items, tight_constraints(10, 10), overlay_style(insets_all(0), LayoutDirection.ltr) + ); + expect(state, duplicate.error == OverlayError.duplicate_id && duplicate.error_index == 1 && + duplicate.placements.len == 0 && duplicate.layout_hash != 0, + "duplicate overlay IDs fail at the exact item with owned empty output"); + overlay_free(&duplicate); + + items.data[1].id = 2; + items.data[1].horizontal = PlacementAlignment.baseline; + let bad_alignment: OverlayResult = overlay_layout( + items, tight_constraints(10, 10), overlay_style(insets_all(0), LayoutDirection.ltr) + ); + expect(state, bad_alignment.error == OverlayError.invalid_item && bad_alignment.error_index == 1, + "unsupported overlay baseline alignment fails instead of pretending to align"); + overlay_free(&bad_alignment); + + items.data[1].horizontal = PlacementAlignment.start; + items.data[1].metrics.preferred.width = 0; + let malformed: OverlayResult = overlay_layout( + items, tight_constraints(10, 10), overlay_style(insets_all(0), LayoutDirection.ltr) + ); + expect(state, malformed.error == OverlayError.invalid_item && malformed.error_index == 1, + "nonmonotonic overlay intrinsics fail before placement"); + overlay_free(&malformed); + + items.data[1].metrics = metrics(1, 1, 2, 2); + items.data[0].insets.left = unbounded_extent(); + let unsafe_result: OverlayResult = overlay_layout( + items, tight_constraints(10, 10), overlay_style(insets_all(0), LayoutDirection.ltr) + ); + expect(state, unsafe_result.error == OverlayError.invalid_item && unsafe_result.error_index == 0, + "unsafe overlay insets fail before bounded arithmetic"); + overlay_free(&unsafe_result); free[OverlayItem](&items); +} + +fn property_contract(state: *TestState) void { + let all_valid: i32 = 1; + let extent: i64 = 4; + while (extent <= 256) { + let items: ArrayList[OverlayItem] = make[OverlayItem](1); + let item: OverlayItem = overlay_item(1, intrinsic_metrics(1, 1, 2, 2, unbounded_extent(), unbounded_extent(), 1)); + item.horizontal = PlacementAlignment.stretch; + item.vertical = PlacementAlignment.stretch; + push[OverlayItem](&items, item); + let result: OverlayResult = overlay_layout( + items, tight_constraints(extent, extent), overlay_style(insets_all(1), LayoutDirection.ltr) + ); + let frame: Rect = result.placements.data[0].frame; + if (result.error != OverlayError.none || frame.x != 1 || frame.y != 1 || + frame.width != extent - 2 || frame.height != extent - 2 || + frame.x + frame.width != extent - 1 || frame.y + frame.height != extent - 1) { + all_valid = 0; + } + overlay_free(&result); free[OverlayItem](&items); + extent = extent + 1; + } + expect(state, all_valid == 1, + "253-extent overlay sweep preserves exact padding and stretch bounds"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + alignment_contract(&state); + rtl_contract(&state); + intrinsic_and_overflow_contract(&state); + failure_contract(&state); + property_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Overlay contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/path_contract.zag b/tests/path_contract.zag new file mode 100644 index 0000000..f5a6bf3 --- /dev/null +++ b/tests/path_contract.zag @@ -0,0 +1,197 @@ +@import("../src/render/path.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn bytes_equal(a: ArrayList[u8], b: ArrayList[u8]) i32 { + if (a.len != b.len) { return 0; } + let i: i32 = 0; + while (i < a.len) { + if (a.data[i] != b.data[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn complete_path(fill_rule: PathFillRule) PathData { + let path: PathData = path_make(fill_rule); + _ = path_move_to(&path, path_point(0 - unit_scale(), 2 * unit_scale())); + _ = path_line_to(&path, path_point(3 * unit_scale(), 2 * unit_scale())); + _ = path_quad_to(&path, path_point(4 * unit_scale(), 3 * unit_scale()), + path_point(3 * unit_scale(), 4 * unit_scale())); + _ = path_cubic_to(&path, + path_point(2 * unit_scale(), 5 * unit_scale()), + path_point(0, 5 * unit_scale()), + path_point(0 - unit_scale(), 4 * unit_scale())); + _ = path_close(&path); + _ = path_move_to(&path, path_point(0, 0)); + _ = path_line_to(&path, path_point(unit_scale(), unit_scale())); + _ = path_seal(&path); + return path; +} + +fn builder_contract(state: *TestState) void { + let path: PathData = path_make(PathFillRule.non_zero); + expect(state, path_line_to(&path, path_point(1, 1)) == PathError.invalid_sequence && + path.commands.len == 0, "line commands require an open contour"); + expect(state, path_close(&path) == PathError.invalid_sequence && path.commands.len == 0, + "close commands require an open contour"); + expect(state, path_move_to(&path, path_point(0, 0)) == PathError.none && + path_line_to(&path, path_point(unit_scale(), 0)) == PathError.none && + path_close(&path) == PathError.none, + "move line and close build a canonical contour"); + expect(state, path_line_to(&path, path_point(0, unit_scale())) == PathError.invalid_sequence, + "closed contours require a new move before geometry"); + expect(state, path_move_to(&path, path_point(unbounded_extent() + 1, 0)) == + PathError.invalid_geometry, "coordinates outside the shared fixed-point domain fail before ownership changes"); + expect(state, path_seal(&path) == PathError.none && path_verify(path) == PathError.none, + "valid paths seal with deterministic immutable identity"); + expect(state, path_move_to(&path, path_point(1, 1)) == PathError.sealed, + "sealed paths reject builder mutation"); + let original: i64 = path.commands.data[1].point_1.x; + path.commands.data[1].point_1.x = original + 1; + expect(state, path_verify(path) == PathError.content_mutated, + "raw command mutation is detected by immutable verification"); + path.commands.data[1].point_1.x = original; + expect(state, path_verify(path) == PathError.none, + "restoring exact command bytes restores path identity"); + path_free(&path); + + let empty: PathData = path_make(PathFillRule.even_odd); + expect(state, path_seal(&empty) == PathError.empty && empty.sealed == 0, + "empty paths cannot claim renderable identity"); + path_free(&empty); +} + +fn codec_contract(state: *TestState) void { + let source: PathData = complete_path(PathFillRule.even_odd); + let encoded: PathEncodeResult = path_encode(source); + expect(state, encoded.error == PathError.none && encoded.bytes.len == + path_codec_header_size() as i32 + source.commands.len * path_codec_command_size() as i32, + "canonical path encoding has one bounded fixed-width record per command"); + expect(state, encoded.bytes.data[0] == 90 && encoded.bytes.data[1] == 75 && + encoded.bytes.data[6] == 48 && encoded.bytes.data[7] == 49, + "path bytes carry the ZKPATH01 magic"); + let decoded: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, decoded.error == PathError.none && decoded.path.sealed == 1 && + decoded.path.fill_rule == PathFillRule.even_odd, + "valid bytes decode to the requested fill rule and a sealed path"); + expect(state, decoded.path.commands.len == source.commands.len && + decoded.path.commands.data[0].point_1.x == 0 - unit_scale() && + decoded.path.commands.data[3].verb == PathVerb.cubic_to, + "round trip preserves signed coordinates and every curve verb"); + expect(state, decoded.path.content_hash == source.content_hash, + "round trip preserves deterministic path identity"); + let encoded_again: PathEncodeResult = path_encode(decoded.path); + expect(state, bytes_equal(encoded.bytes, encoded_again.bytes) != 0, + "decode and re-encode are byte identical"); + path_encode_free(&encoded_again); + path_free(&decoded.path); + path_encode_free(&encoded); + path_free(&source); +} + +fn malformed_contract(state: *TestState) void { + let source: PathData = complete_path(PathFillRule.non_zero); + let encoded: PathEncodeResult = path_encode(source); + + let saved_magic: u8 = encoded.bytes.data[0]; + encoded.bytes.data[0] = 0; + let bad_magic: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, bad_magic.error == PathError.bad_magic && bad_magic.error_offset == 0, + "bad path magic fails at byte zero"); + path_free(&bad_magic.path); encoded.bytes.data[0] = saved_magic; + + let saved_version: u8 = encoded.bytes.data[8]; + encoded.bytes.data[8] = 2; + let bad_version: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, bad_version.error == PathError.unsupported_version && bad_version.error_offset == 8, + "unknown path versions fail visibly"); + path_free(&bad_version.path); encoded.bytes.data[8] = saved_version; + + let saved_fill: u8 = encoded.bytes.data[16]; + encoded.bytes.data[16] = 2; + let bad_fill: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, bad_fill.error == PathError.invalid_fill_rule && bad_fill.error_offset == 16, + "unknown fill rules fail before command allocation"); + path_free(&bad_fill.path); encoded.bytes.data[16] = saved_fill; + + let first_command: i32 = path_codec_header_size() as i32; + let saved_verb: u8 = encoded.bytes.data[first_command]; + encoded.bytes.data[first_command] = 9; + let bad_verb: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, bad_verb.error == PathError.invalid_verb && + bad_verb.error_offset == first_command as i64, + "unknown path verbs fail at the exact command record"); + path_free(&bad_verb.path); encoded.bytes.data[first_command] = saved_verb; + + let unused_coordinate: i32 = first_command + 24; + let saved_unused: u8 = encoded.bytes.data[unused_coordinate]; + encoded.bytes.data[unused_coordinate] = 1; + let noncanonical: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, noncanonical.error == PathError.invalid_geometry && + noncanonical.error_offset == first_command as i64, + "unused coordinate fields must be canonical zeroes"); + path_free(&noncanonical.path); encoded.bytes.data[unused_coordinate] = saved_unused; + + let saved_hash: u8 = encoded.bytes.data[32]; + encoded.bytes.data[32] = saved_hash + 1; + let bad_hash: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, bad_hash.error == PathError.hash_mismatch && bad_hash.error_offset == 32, + "path payload tampering fails deterministic identity verification"); + path_free(&bad_hash.path); encoded.bytes.data[32] = saved_hash; + + let truncated: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len - 1]); + expect(state, truncated.error == PathError.truncated && + truncated.error_offset == encoded.bytes.len as i64 - 1, + "truncated command data reports the observed length"); + path_free(&truncated.path); + + push[u8](&encoded.bytes, 0); + let trailing: PathDecodeResult = path_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, trailing.error == PathError.trailing_data, + "trailing path bytes fail canonical decoding"); + path_free(&trailing.path); + + path_encode_free(&encoded); + path_free(&source); +} + +fn deterministic_sweep(state: *TestState) void { + let expected: i64 = 0; + let stable: i32 = 1; + let i: i32 = 0; + while (i < 100) { + let path: PathData = complete_path(PathFillRule.non_zero); + if (i == 0) { expected = path.content_hash; } + if (path.content_hash != expected || path_verify(path) != PathError.none) { stable = 0; } + path_free(&path); + i = i + 1; + } + expect(state, stable, "100-build sweep preserves canonical path identity"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + builder_contract(&state); + codec_contract(&state); + malformed_contract(&state); + deterministic_sweep(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Path contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/png_decode_contract.zag b/tests/png_decode_contract.zag new file mode 100644 index 0000000..32d146a --- /dev/null +++ b/tests/png_decode_contract.zag @@ -0,0 +1,1540 @@ +@import("../src/render/png_decode.zag") +@import("../src/render/png_encode.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn pixels_equal(decoded: PngDecodeResult, surface: CpuSurface) i32 { + if (decoded.pixels.len != surface.pixels.len) { return 0; } + let i: i32 = 0; + while (i < decoded.pixels.len) { + if (decoded.pixels.data[i] != surface.pixels.data[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn decoded_image_scene(decoded: PngDecodeResult, size: i32) DisplayList { + let list: DisplayList = display_list_make(); + let spec: RenderResourceSpec = png_decode_resource_spec(decoded, 701); + _ = display_list_add_resource(&list, spec); + let draw: DisplayOp = display_op(DisplayOpKind.draw_image, node_key(701)); + draw.resource_id = 701; + draw.bounds = rect(0, 0, + (size as i64) * unit_scale(), (size as i64) * unit_scale()); + draw.paint.alpha = 65535; + _ = display_list_push(&list, draw); + _ = display_list_seal(&list); + return list; +} + +fn fixture_png(width: i32, height: i32, bit_depth: i32, color_type: i32, + scanlines: []u8, palette: []u8, transparency: []u8) PngEncodeResult { + let compressed: ZlibStoreEncodeResult = zlib_store_encode(scanlines); + let output: ArrayList[u8] = make[u8](compressed.bytes.len + 128 + + palette.len + transparency.len); + push[u8](&output, 137); push[u8](&output, 80); + push[u8](&output, 78); push[u8](&output, 71); + push[u8](&output, 13); push[u8](&output, 10); + push[u8](&output, 26); push[u8](&output, 10); + let header: ArrayList[u8] = make[u8](13); + png_write_u32_be(&header, width as i64); + png_write_u32_be(&header, height as i64); + push[u8](&header, bit_depth as u8); push[u8](&header, color_type as u8); + push[u8](&header, 0); push[u8](&header, 0); push[u8](&header, 0); + png_write_chunk(&output, "IHDR", header.data[0..header.len]); + free[u8](&header); + if (palette.len > 0) { png_write_chunk(&output, "PLTE", palette); } + if (transparency.len > 0) { png_write_chunk(&output, "tRNS", transparency); } + png_write_chunk(&output, "IDAT", compressed.bytes.data[0..compressed.bytes.len]); + png_write_chunk(&output, "IEND", ""); + zlib_store_encode_free(&compressed); + return PngEncodeResult{ .bytes = output, .error = PngEncodeError.none }; +} + +fn fixture_png_compressed(width: i32, height: i32, compressed: []u8) PngEncodeResult { + let output: ArrayList[u8] = make[u8](compressed.len + 64); + push[u8](&output, 137); push[u8](&output, 80); + push[u8](&output, 78); push[u8](&output, 71); + push[u8](&output, 13); push[u8](&output, 10); + push[u8](&output, 26); push[u8](&output, 10); + let header: ArrayList[u8] = make[u8](13); + png_write_u32_be(&header, width as i64); png_write_u32_be(&header, height as i64); + push[u8](&header, 8); push[u8](&header, 6); + push[u8](&header, 0); push[u8](&header, 0); push[u8](&header, 0); + png_write_chunk(&output, "IHDR", header.data[0..header.len]); + free[u8](&header); + png_write_chunk(&output, "IDAT", compressed); + png_write_chunk(&output, "IEND", ""); + return PngEncodeResult{ .bytes = output, .error = PngEncodeError.none }; +} + +fn fixture_filtered_rows(raw: ArrayList[u8], width: i32, height: i32, + bytes_per_pixel: i32) ArrayList[u8] { + let row_bytes: i32 = width * bytes_per_pixel; + let filtered: ArrayList[u8] = make[u8](raw.len + height); + let y: i32 = 0; + while (y < height) { + let filter: i32 = y % 5; + push[u8](&filtered, filter as u8); + let x: i32 = 0; + while (x < row_bytes) { + let value: i32 = raw.data[y * row_bytes + x] as i32; + let left: i32 = 0; + let above: i32 = 0; + let upper_left: i32 = 0; + if (x >= bytes_per_pixel) { + left = raw.data[y * row_bytes + x - bytes_per_pixel] as i32; + } + if (y > 0) { above = raw.data[(y - 1) * row_bytes + x] as i32; } + if (y > 0 && x >= bytes_per_pixel) { + upper_left = raw.data[(y - 1) * row_bytes + + x - bytes_per_pixel] as i32; + } + let predictor: i32 = 0; + if (filter == 1) { predictor = left; } + else if (filter == 2) { predictor = above; } + else if (filter == 3) { predictor = (left + above) / 2; } + else if (filter == 4) { predictor = png_paeth(left, above, upper_left); } + push[u8](&filtered, ((value - predictor) & 255) as u8); + x = x + 1; + } + y = y + 1; + } + return filtered; +} + +fn fixture_png_interlaced(width: i32, height: i32, bit_depth: i32, + color_type: i32, scanlines: []u8, palette: []u8, + transparency: []u8) PngEncodeResult { + let compressed: ZlibStoreEncodeResult = zlib_store_encode(scanlines); + let output: ArrayList[u8] = make[u8](compressed.bytes.len + 128 + + palette.len + transparency.len); + push[u8](&output, 137); push[u8](&output, 80); + push[u8](&output, 78); push[u8](&output, 71); + push[u8](&output, 13); push[u8](&output, 10); + push[u8](&output, 26); push[u8](&output, 10); + let header: ArrayList[u8] = make[u8](13); + png_write_u32_be(&header, width as i64); + png_write_u32_be(&header, height as i64); + push[u8](&header, bit_depth as u8); push[u8](&header, color_type as u8); + push[u8](&header, 0); push[u8](&header, 0); push[u8](&header, 1); + png_write_chunk(&output, "IHDR", header.data[0..header.len]); + free[u8](&header); + if (palette.len > 0) { png_write_chunk(&output, "PLTE", palette); } + if (transparency.len > 0) { png_write_chunk(&output, "tRNS", transparency); } + png_write_chunk(&output, "IDAT", compressed.bytes.data[0..compressed.bytes.len]); + png_write_chunk(&output, "IEND", ""); + zlib_store_encode_free(&compressed); + return PngEncodeResult{ .bytes = output, .error = PngEncodeError.none }; +} + +fn fixture_adam7_rgba8(width: i32, height: i32, + pixels: []u8) PngEncodeResult { + let filtered: ArrayList[u8] = make[u8](pixels.len + height * 7); + let pass: i32 = 0; + while (pass < 7) { + let start_x: i32 = png_adam7_start_x(pass); + let start_y: i32 = png_adam7_start_y(pass); + let step_x: i32 = png_adam7_step_x(pass); + let step_y: i32 = png_adam7_step_y(pass); + let pass_width: i32 = png_adam7_extent(width, start_x, step_x); + let pass_height: i32 = png_adam7_extent(height, start_y, step_y); + if (pass_width > 0 && pass_height > 0) { + let pass_pixels: ArrayList[u8] = make[u8](pass_width * pass_height * 4); + let pass_y: i32 = 0; + while (pass_y < pass_height) { + let pass_x: i32 = 0; + while (pass_x < pass_width) { + let source_x: i32 = start_x + pass_x * step_x; + let source_y: i32 = start_y + pass_y * step_y; + let source: i32 = (source_y * width + source_x) * 4; + let channel: i32 = 0; + while (channel < 4) { + push[u8](&pass_pixels, pixels[source + channel]); + channel = channel + 1; + } + pass_x = pass_x + 1; + } + pass_y = pass_y + 1; + } + let pass_filtered: ArrayList[u8] = fixture_filtered_rows( + pass_pixels, pass_width, pass_height, 4 + ); + png_copy_append(&filtered, + pass_filtered.data[0..pass_filtered.len]); + free[u8](&pass_filtered); free[u8](&pass_pixels); + } + pass = pass + 1; + } + let result: PngEncodeResult = fixture_png_interlaced( + width, height, 8, 6, filtered.data[0..filtered.len], "", "" + ); + free[u8](&filtered); + return result; +} + +fn fixture_write_u32_be(bytes: *ArrayList[u8], offset: i32, value: i64) void { + bytes.*.data[offset] = ((value >> 24) & 255) as u8; + bytes.*.data[offset + 1] = ((value >> 16) & 255) as u8; + bytes.*.data[offset + 2] = ((value >> 8) & 255) as u8; + bytes.*.data[offset + 3] = (value & 255) as u8; +} + +fn fixture_rewrite_crc(bytes: *ArrayList[u8], chunk_offset: i32) void { + let length: i32 = png_read_u32_be( + bytes.*.data[0..bytes.*.len], chunk_offset + ) as i32; + let crc_offset: i32 = chunk_offset + 8 + length; + let crc: i64 = checksum_crc32( + bytes.*.data[chunk_offset + 4..crc_offset] + ); + fixture_write_u32_be(bytes, crc_offset, crc); +} + +fn fixture_write_signature(bytes: *ArrayList[u8], offset: i32, + signature: []u8) void { + let i: i32 = 0; + while (i < 4) { bytes.*.data[offset + i] = signature[i]; i = i + 1; } +} + +fn fixture_write_u16_be(bytes: *ArrayList[u8], offset: i32, + value: i32) void { + bytes.*.data[offset] = ((value >> 8) & 255) as u8; + bytes.*.data[offset + 1] = (value & 255) as u8; +} + +fn fixture_icc_tag(bytes: *ArrayList[u8], entry: i32, signature: []u8, + data_offset: i32, data_size: i32) void { + fixture_write_signature(bytes, entry, signature); + fixture_write_u32_be(bytes, entry + 4, data_offset); + fixture_write_u32_be(bytes, entry + 8, data_size); +} + +fn fixture_icc_xyz(bytes: *ArrayList[u8], offset: i32, + x: i64, y: i64, z: i64) void { + fixture_write_signature(bytes, offset, "XYZ "); + fixture_write_u32_be(bytes, offset + 8, x); + fixture_write_u32_be(bytes, offset + 12, y); + fixture_write_u32_be(bytes, offset + 16, z); +} + +fn fixture_matrix_icc(kind: i32) ArrayList[u8] { + let bytes: ArrayList[u8] = make[u8](320); + let i: i32 = 0; + while (i < 320) { push[u8](&bytes, 0); i = i + 1; } + fixture_write_u32_be(&bytes, 0, 320); + bytes.data[8] = 4; + fixture_write_signature(&bytes, 12, "mntr"); + fixture_write_signature(&bytes, 16, "RGB "); + fixture_write_signature(&bytes, 20, "XYZ "); + fixture_write_signature(&bytes, 36, "acsp"); + fixture_write_u32_be(&bytes, 68, 63190); + fixture_write_u32_be(&bytes, 72, 65536); + fixture_write_u32_be(&bytes, 76, 54061); + fixture_write_u32_be(&bytes, 128, 6); + fixture_icc_tag(&bytes, 132, "rXYZ", 204, 20); + fixture_icc_tag(&bytes, 144, "gXYZ", 224, 20); + fixture_icc_tag(&bytes, 156, "bXYZ", 244, 20); + fixture_icc_tag(&bytes, 168, "rTRC", 264, 12); + fixture_icc_tag(&bytes, 180, "gTRC", 264, 12); + fixture_icc_tag(&bytes, 192, "bTRC", 264, 12); + if (kind == 2) { + fixture_icc_xyz(&bytes, 204, 33759, 15807, 0 - 69); + fixture_icc_xyz(&bytes, 224, 19135, 45367, 2745); + fixture_icc_xyz(&bytes, 244, 10296, 4363, 51385); + } else { + fixture_icc_xyz(&bytes, 204, 28579, 14582, 913); + fixture_icc_xyz(&bytes, 224, 25236, 46981, 6364); + fixture_icc_xyz(&bytes, 244, 9377, 3973, 46804); + } + fixture_write_signature(&bytes, 264, "curv"); + if (kind == 3) { fixture_write_u32_be(&bytes, 136, 4000000); } + if (kind == 4) { + fixture_write_u32_be(&bytes, 176, 16); + fixture_write_u32_be(&bytes, 188, 16); + fixture_write_u32_be(&bytes, 200, 16); + fixture_write_u32_be(&bytes, 272, 2); + bytes.data[278] = 255; bytes.data[279] = 255; + } + if (kind == 8) { + fixture_write_u32_be(&bytes, 176, 16); + fixture_write_u32_be(&bytes, 188, 16); + fixture_write_u32_be(&bytes, 200, 16); + fixture_write_signature(&bytes, 264, "para"); + fixture_write_u32_be(&bytes, 276, 65536); + } + if (kind == 10) { fixture_write_signature(&bytes, 144, "rXYZ"); } + if (kind == 11) { fixture_write_u32_be(&bytes, 0, 319); } + if (kind == 13) { + fixture_write_u32_be(&bytes, 176, 32); + fixture_write_u32_be(&bytes, 188, 32); + fixture_write_u32_be(&bytes, 200, 32); + fixture_write_signature(&bytes, 264, "para"); + bytes.data[273] = 3; + fixture_write_u32_be(&bytes, 276, 157286); + fixture_write_u32_be(&bytes, 280, 62119); + fixture_write_u32_be(&bytes, 284, 3417); + fixture_write_u32_be(&bytes, 288, 5072); + fixture_write_u32_be(&bytes, 292, 2651); + } + if (kind == 14 || kind == 15 || kind == 16) { + let curve_size: i32 = 24; + let function_type: i32 = 1; + if (kind == 15) { curve_size = 28; function_type = 2; } + if (kind == 16) { curve_size = 40; function_type = 4; } + fixture_write_u32_be(&bytes, 176, curve_size); + fixture_write_u32_be(&bytes, 188, curve_size); + fixture_write_u32_be(&bytes, 200, curve_size); + fixture_write_signature(&bytes, 264, "para"); + bytes.data[273] = function_type as u8; + fixture_write_u32_be(&bytes, 276, 65536); + fixture_write_u32_be(&bytes, 280, 65536); + fixture_write_u32_be(&bytes, 284, 0); + if (kind == 15) { fixture_write_u32_be(&bytes, 288, 0); } + if (kind == 16) { + fixture_write_u32_be(&bytes, 288, 65536); + fixture_write_u32_be(&bytes, 292, 32768); + fixture_write_u32_be(&bytes, 296, 0); + fixture_write_u32_be(&bytes, 300, 0); + } + } + if (kind == 17) { + fixture_icc_tag(&bytes, 168, "rTRC", 264, 16); + fixture_icc_tag(&bytes, 180, "gTRC", 280, 16); + fixture_icc_tag(&bytes, 192, "bTRC", 296, 16); + fixture_write_signature(&bytes, 264, "para"); + fixture_write_u32_be(&bytes, 276, 65536); + fixture_write_signature(&bytes, 280, "para"); + fixture_write_u32_be(&bytes, 292, 131072); + fixture_write_signature(&bytes, 296, "para"); + fixture_write_u32_be(&bytes, 308, 32768); + } + if (kind == 18) { + fixture_write_u32_be(&bytes, 176, 20); + fixture_write_u32_be(&bytes, 188, 20); + fixture_write_u32_be(&bytes, 200, 20); + fixture_write_signature(&bytes, 264, "curv"); + fixture_write_u32_be(&bytes, 272, 4); + bytes.data[278] = 64; + bytes.data[280] = 192; + bytes.data[282] = 255; bytes.data[283] = 255; + } + if (kind == 19) { + fixture_write_signature(&bytes, 264, "curv"); + fixture_write_u32_be(&bytes, 272, 4097); + } + if (kind == 20) { + fixture_write_u32_be(&bytes, 176, 18); + fixture_write_u32_be(&bytes, 188, 18); + fixture_write_u32_be(&bytes, 200, 18); + fixture_write_signature(&bytes, 264, "curv"); + fixture_write_u32_be(&bytes, 272, 4); + } + return bytes; +} + +fn fixture_lut16_icc(kind: i32) ArrayList[u8] { + let bytes: ArrayList[u8] = make[u8](268); + let i: i32 = 0; + while (i < 268) { push[u8](&bytes, 0); i = i + 1; } + fixture_write_u32_be(&bytes, 0, 268); + bytes.data[8] = 4; + fixture_write_signature(&bytes, 12, "mntr"); + fixture_write_signature(&bytes, 16, "RGB "); + fixture_write_signature(&bytes, 20, "XYZ "); + fixture_write_signature(&bytes, 36, "acsp"); + fixture_write_u32_be(&bytes, 68, 63190); + fixture_write_u32_be(&bytes, 72, 65536); + fixture_write_u32_be(&bytes, 76, 54061); + fixture_write_u32_be(&bytes, 128, 1); + fixture_icc_tag(&bytes, 132, "A2B0", 144, 124); + fixture_write_signature(&bytes, 144, "mft2"); + bytes.data[152] = 3; bytes.data[153] = 3; bytes.data[154] = 2; + fixture_write_u32_be(&bytes, 156, 65536); + fixture_write_u32_be(&bytes, 172, 65536); + fixture_write_u32_be(&bytes, 188, 65536); + fixture_write_u16_be(&bytes, 192, 2); + fixture_write_u16_be(&bytes, 194, 2); + let input_offset: i32 = 196; + let channel: i32 = 0; + while (channel < 3) { + fixture_write_u16_be(&bytes, input_offset + channel * 4, 0); + fixture_write_u16_be(&bytes, input_offset + channel * 4 + 2, 65535); + channel = channel + 1; + } + let clut_offset: i32 = 208; + let red: i32 = 0; + let clut_index: i32 = 0; + while (red < 2) { + let green: i32 = 0; + while (green < 2) { + let blue: i32 = 0; + while (blue < 2) { + fixture_write_u16_be(&bytes, clut_offset + clut_index * 2, + red * 14289 + green * 12618 + blue * 4688); + fixture_write_u16_be(&bytes, clut_offset + clut_index * 2 + 2, + red * 7291 + green * 23491 + blue * 1986); + fixture_write_u16_be(&bytes, clut_offset + clut_index * 2 + 4, + red * 457 + green * 3182 + blue * 23402); + clut_index = clut_index + 3; + blue = blue + 1; + } + green = green + 1; + } + red = red + 1; + } + let output_offset: i32 = 256; + channel = 0; + while (channel < 3) { + fixture_write_u16_be(&bytes, output_offset + channel * 4, 0); + fixture_write_u16_be(&bytes, output_offset + channel * 4 + 2, 65535); + channel = channel + 1; + } + if (kind == 22) { bytes.data[154] = 34; } + if (kind == 23) { fixture_write_u32_be(&bytes, 160, 1); } + if (kind == 24) { fixture_write_signature(&bytes, 144, "mft1"); } + if (kind == 25) { fixture_write_u32_be(&bytes, 140, 122); } + if (kind == 26) { fixture_write_u16_be(&bytes, 192, 1); } + if (kind == 27) { fixture_write_u32_be(&bytes, 64, 1); } + return bytes; +} + +fn fixture_mab_icc(kind: i32) ArrayList[u8] { + let bytes: ArrayList[u8] = make[u8](340); + let i: i32 = 0; + while (i < 340) { push[u8](&bytes, 0); i = i + 1; } + fixture_write_u32_be(&bytes, 0, 340); + bytes.data[8] = 4; + fixture_write_signature(&bytes, 12, "mntr"); + fixture_write_signature(&bytes, 16, "RGB "); + fixture_write_signature(&bytes, 20, "XYZ "); + fixture_write_signature(&bytes, 36, "acsp"); + fixture_write_u32_be(&bytes, 68, 63190); + fixture_write_u32_be(&bytes, 72, 65536); + fixture_write_u32_be(&bytes, 76, 54061); + fixture_write_u32_be(&bytes, 128, 1); + fixture_icc_tag(&bytes, 132, "A2B0", 144, 196); + fixture_write_signature(&bytes, 144, "mAB "); + bytes.data[152] = 3; bytes.data[153] = 3; + fixture_write_u32_be(&bytes, 156, 160); + fixture_write_u32_be(&bytes, 168, 68); + fixture_write_u32_be(&bytes, 172, 32); + let curve: i32 = 0; + while (curve < 3) { + fixture_write_signature(&bytes, 176 + curve * 12, "curv"); + curve = curve + 1; + } + let clut: i32 = 212; + bytes.data[clut] = 2; bytes.data[clut + 1] = 3; + bytes.data[clut + 2] = 2; bytes.data[clut + 16] = 2; + let red: i32 = 0; + let clut_index: i32 = 0; + while (red < 2) { + let green: i32 = 0; + while (green < 3) { + let blue: i32 = 0; + while (blue < 2) { + fixture_write_u16_be(&bytes, clut + 20 + clut_index * 2, + red * 14289 + (green * 12618 + 1) / 2 + blue * 4688); + fixture_write_u16_be(&bytes, clut + 22 + clut_index * 2, + red * 7291 + (green * 23491 + 1) / 2 + blue * 1986); + fixture_write_u16_be(&bytes, clut + 24 + clut_index * 2, + red * 457 + (green * 3182 + 1) / 2 + blue * 23402); + clut_index = clut_index + 3; blue = blue + 1; + } + green = green + 1; + } + red = red + 1; + } + curve = 0; + while (curve < 3) { + fixture_write_signature(&bytes, 304 + curve * 12, "curv"); + curve = curve + 1; + } + if (kind == 29) { bytes.data[clut + 3] = 1; } + if (kind == 30) { fixture_write_u32_be(&bytes, 156, 0); } + if (kind == 31) { fixture_write_signature(&bytes, 176, "bad!"); } + if (kind == 32) { bytes.data[clut + 16] = 3; } + if (kind == 33) { fixture_write_u32_be(&bytes, 168, 32); } + return bytes; +} + +fn fixture_profile_png(gamma_count: i32, chromaticity_kind: i32, + gamma_value: i64, include_srgb: i32, icc_kind: i32) PngEncodeResult { + let compressed: ZlibStoreEncodeResult = zlib_store_encode("\x00\x11\x22\x33\xff"); + let output: ArrayList[u8] = make[u8](compressed.bytes.len + 160); + push[u8](&output, 137); push[u8](&output, 80); + push[u8](&output, 78); push[u8](&output, 71); + push[u8](&output, 13); push[u8](&output, 10); + push[u8](&output, 26); push[u8](&output, 10); + let header: ArrayList[u8] = make[u8](13); + png_write_u32_be(&header, 1); png_write_u32_be(&header, 1); + push[u8](&header, 8); push[u8](&header, 6); + push[u8](&header, 0); push[u8](&header, 0); push[u8](&header, 0); + png_write_chunk(&output, "IHDR", header.data[0..header.len]); + free[u8](&header); + if (include_srgb != 0) { png_write_chunk(&output, "sRGB", "\x00"); } + let gamma: ArrayList[u8] = make[u8](4); + png_write_u32_be(&gamma, gamma_value); + let gamma_index: i32 = 0; + while (gamma_index < gamma_count) { + png_write_chunk(&output, "gAMA", gamma.data[0..gamma.len]); + gamma_index = gamma_index + 1; + } + free[u8](&gamma); + if (icc_kind == 9) { + png_write_chunk(&output, "cICP", "\x01\x0d\x00\x01"); + } else if (icc_kind != 0) { + let profile: ArrayList[u8] = fixture_matrix_icc(icc_kind); + if (icc_kind >= 21) { + free[u8](&profile); profile = fixture_lut16_icc(icc_kind); + } + if (icc_kind >= 28) { + free[u8](&profile); profile = fixture_mab_icc(icc_kind); + } + let compressed_profile: ZlibStoreEncodeResult = zlib_store_encode( + profile.data[0..profile.len] + ); + if (icc_kind == 12 && compressed_profile.bytes.len > 3) { + compressed_profile.bytes.data[2] = + compressed_profile.bytes.data[2] ^ 1; + } + let iccp: ArrayList[u8] = make[u8](compressed_profile.bytes.len + 16); + let name: []u8 = "Zagkit matrix"; + if (icc_kind == 5) { push[u8](&iccp, 32); } + let name_index: i32 = 0; + while (name_index < name.len) { + push[u8](&iccp, name[name_index]); name_index = name_index + 1; + } + push[u8](&iccp, 0); + if (icc_kind == 6) { push[u8](&iccp, 1); } + else { push[u8](&iccp, 0); } + png_copy_append(&iccp, + compressed_profile.bytes.data[0..compressed_profile.bytes.len]); + png_write_chunk(&output, "iCCP", iccp.data[0..iccp.len]); + if (icc_kind == 7) { + png_write_chunk(&output, "iCCP", iccp.data[0..iccp.len]); + } + free[u8](&iccp); zlib_store_encode_free(&compressed_profile); + free[u8](&profile); + } + if (chromaticity_kind != 0) { + let chromaticity: ArrayList[u8] = make[u8](32); + let white_x: i64 = 31270; let white_y: i64 = 32900; + let red_x: i64 = 64000; let red_y: i64 = 33000; + let green_x: i64 = 30000; let green_y: i64 = 60000; + let blue_x: i64 = 15000; let blue_y: i64 = 6000; + if (chromaticity_kind == 2) { + red_x = 68000; red_y = 32000; + green_x = 26500; green_y = 69000; + } else if (chromaticity_kind == 3) { + white_x = 34567; white_y = 35850; + } else if (chromaticity_kind == 4) { + green_x = red_x; green_y = red_y; + } + png_write_u32_be(&chromaticity, white_x); + png_write_u32_be(&chromaticity, white_y); + png_write_u32_be(&chromaticity, red_x); + png_write_u32_be(&chromaticity, red_y); + png_write_u32_be(&chromaticity, green_x); + png_write_u32_be(&chromaticity, green_y); + png_write_u32_be(&chromaticity, blue_x); + png_write_u32_be(&chromaticity, blue_y); + png_write_chunk(&output, "cHRM", chromaticity.data[0..chromaticity.len]); + free[u8](&chromaticity); + } + png_write_chunk(&output, "IDAT", compressed.bytes.data[0..compressed.bytes.len]); + png_write_chunk(&output, "IEND", ""); + zlib_store_encode_free(&compressed); + return PngEncodeResult{ .bytes = output, .error = PngEncodeError.none }; +} + +fn round_trip_contract(state: *TestState) void { + let surface: CpuSurface = cpu_surface_make(3, 2); + let i: i32 = 0; + while (i < surface.pixels.len) { + surface.pixels.data[i] = ((i * 43 + 19) % 256) as u8; + i = i + 1; + } + let encoded: PngEncodeResult = png_encode_surface(surface); + let decoded: PngDecodeResult = png_decode( + encoded.bytes.data[0..encoded.bytes.len] + ); + expect(state, decoded.error == PngDecodeError.none && + decoded.width == 3 && decoded.height == 2 && + decoded.color_space == RenderColorSpace.srgb && + decoded.profile_assumed == 0 && + pixels_equal(decoded, surface) != 0, + "canonical screenshot PNG decodes to exact owned RGBA8 pixels"); + + let store: RenderResourceStore = render_resource_store_default(); + let spec: RenderResourceSpec = png_decode_resource_spec(decoded, 91); + expect(state, render_resource_add(&store, spec) == RenderResourceError.none && + store.resources.data[0].width == 3 && store.resources.data[0].height == 2 && + image_resource_validate(store.resources.data[0]) == ImagePayloadError.none, + "decoded PNG becomes a canonical owned image resource"); + render_resource_store_free(&store); + png_decode_free(&decoded); + png_encode_free(&encoded); + cpu_surface_free(&surface); +} + +fn filter_contract(state: *TestState) void { + let raw: ArrayList[u8] = make[u8](40); + let i: i32 = 0; + while (i < 40) { push[u8](&raw, ((i * 31 + 7) % 256) as u8); i = i + 1; } + let scanlines: ArrayList[u8] = fixture_filtered_rows(raw, 2, 5, 4); + let fixture: PngEncodeResult = fixture_png( + 2, 5, 8, 6, scanlines.data[0..scanlines.len], "", "" + ); + let decoded: PngDecodeResult = png_decode( + fixture.bytes.data[0..fixture.bytes.len] + ); + let exact: i32 = 1; + if (decoded.pixels.len != raw.len) { exact = 0; } + i = 0; + while (i < decoded.pixels.len && i < raw.len) { + if (decoded.pixels.data[i] != raw.data[i]) { exact = 0; } + i = i + 1; + } + expect(state, decoded.error == PngDecodeError.none && exact != 0, + "None, Sub, Up, Average, and Paeth filters reconstruct exact bytes"); + png_decode_free(&decoded); png_encode_free(&fixture); + free[u8](&scanlines); free[u8](&raw); +} + +fn scale_render_contract(state: *TestState) void { + let surface: CpuSurface = cpu_surface_make(4, 4); + let y: i32 = 0; + while (y < 4) { + let x: i32 = 0; + while (x < 4) { + let at: i32 = (y * 4 + x) * 4; + surface.pixels.data[at] = (x * 71 + y * 13) as u8; + surface.pixels.data[at + 1] = (x * 19 + y * 53) as u8; + surface.pixels.data[at + 2] = (x * 37 + y * 29) as u8; + surface.pixels.data[at + 3] = (64 + x * 31 + y * 17) as u8; + x = x + 1; + } + y = y + 1; + } + let encoded: PngEncodeResult = png_encode_surface(surface); + let decoded: PngDecodeResult = png_decode( + encoded.bytes.data[0..encoded.bytes.len] + ); + let exact: i32 = decoded.error == PngDecodeError.none; + let scale_index: i32 = 0; + while (scale_index < 5 && exact != 0) { + let size: i32 = 4; + if (scale_index == 1) { size = 5; } + else if (scale_index == 2) { size = 6; } + else if (scale_index == 3) { size = 8; } + else if (scale_index == 4) { size = 12; } + let expected_hash: i64 = 1974955247; + if (scale_index == 1) { expected_hash = 1140701745; } + else if (scale_index == 2) { expected_hash = 24860848; } + else if (scale_index == 3) { expected_hash = 989379801; } + else if (scale_index == 4) { expected_hash = 692663035; } + let list: DisplayList = decoded_image_scene(decoded, size); + let first: CpuRasterResult = cpu_rasterize(list, size, size); + let second: CpuRasterResult = cpu_rasterize(list, size, size); + let first_hash: i64 = cpu_surface_hash(first.surface); + let second_hash: i64 = cpu_surface_hash(second.surface); + if (first.error != CpuRasterError.none || + second.error != CpuRasterError.none || first_hash == 0 || + first_hash != second_hash || first_hash != expected_hash) { exact = 0; } + cpu_raster_free(&second); cpu_raster_free(&first); + display_list_free(&list); scale_index = scale_index + 1; + } + expect(state, exact, + "decoded PNG resources match CPU goldens at 1x 1.25x 1.5x 2x and 3x"); + png_decode_free(&decoded); png_encode_free(&encoded); cpu_surface_free(&surface); +} + +fn color_contract(state: *TestState) void { + let gray_fixture: PngEncodeResult = fixture_png( + 8, 1, 1, 0, "\x00\xaa", "", "\x00\x00" + ); + let gray: PngDecodeResult = png_decode( + gray_fixture.bytes.data[0..gray_fixture.bytes.len] + ); + expect(state, gray.error == PngDecodeError.none && gray.pixels.len == 32 && + gray.profile_assumed != 0 && + gray.pixels.data[0] == 255 && gray.pixels.data[3] == 255 && + gray.pixels.data[4] == 0 && gray.pixels.data[7] == 0, + "packed one-bit grayscale expands and applies transparent gray"); + png_decode_free(&gray); png_encode_free(&gray_fixture); + + let indexed_fixture: PngEncodeResult = fixture_png( + 4, 1, 2, 3, "\x00\x1b", + "\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\xff", + "\xff\x80\x40\x00" + ); + let indexed: PngDecodeResult = png_decode( + indexed_fixture.bytes.data[0..indexed_fixture.bytes.len] + ); + expect(state, indexed.error == PngDecodeError.none && indexed.pixels.len == 16 && + indexed.pixels.data[0] == 255 && indexed.pixels.data[3] == 255 && + indexed.pixels.data[5] == 255 && indexed.pixels.data[7] == 128 && + indexed.pixels.data[10] == 255 && indexed.pixels.data[11] == 64 && + indexed.pixels.data[12] == 255 && indexed.pixels.data[15] == 0, + "packed indexed color expands palette entries and per-index alpha"); + png_decode_free(&indexed); png_encode_free(&indexed_fixture); + + let rgb16_fixture: PngEncodeResult = fixture_png( + 1, 1, 16, 2, "\x00\x12\x34\x56\x78\x9a\xbc", "", + "\x12\x34\x56\x78\x9a\xbc" + ); + let rgb16: PngDecodeResult = png_decode( + rgb16_fixture.bytes.data[0..rgb16_fixture.bytes.len] + ); + expect(state, rgb16.error == PngDecodeError.none && rgb16.pixels.len == 4 && + rgb16.pixels.data[0] == 18 && rgb16.pixels.data[1] == 86 && + rgb16.pixels.data[2] == 154 && rgb16.pixels.data[3] == 0, + "RGB16 uses deterministic high-byte conversion and exact tRNS matching"); + png_decode_free(&rgb16); png_encode_free(&rgb16_fixture); + + let gray_alpha_fixture: PngEncodeResult = fixture_png( + 1, 1, 16, 4, "\x00\xab\xcd\x40\x80", "", "" + ); + let gray_alpha: PngDecodeResult = png_decode( + gray_alpha_fixture.bytes.data[0..gray_alpha_fixture.bytes.len] + ); + expect(state, gray_alpha.error == PngDecodeError.none && + gray_alpha.pixels.data[0] == 171 && gray_alpha.pixels.data[1] == 171 && + gray_alpha.pixels.data[2] == 171 && gray_alpha.pixels.data[3] == 64, + "grayscale-alpha16 expands to canonical RGBA8"); + png_decode_free(&gray_alpha); png_encode_free(&gray_alpha_fixture); + + let rgba16_fixture: PngEncodeResult = fixture_png( + 1, 1, 16, 6, "\x00\x10\x20\x30\x40\x50\x60\x70\x80", "", "" + ); + let rgba16: PngDecodeResult = png_decode( + rgba16_fixture.bytes.data[0..rgba16_fixture.bytes.len] + ); + expect(state, rgba16.error == PngDecodeError.none && + rgba16.pixels.data[0] == 16 && rgba16.pixels.data[1] == 48 && + rgba16.pixels.data[2] == 80 && rgba16.pixels.data[3] == 112, + "RGBA16 expands each channel with one documented conversion policy"); + png_decode_free(&rgba16); png_encode_free(&rgba16_fixture); +} + +fn color_profile_contract(state: *TestState) void { + let declared_fixture: PngEncodeResult = fixture_profile_png(1, 1, 45455, 0, 0); + let declared: PngDecodeResult = png_decode( + declared_fixture.bytes.data[0..declared_fixture.bytes.len] + ); + expect(state, declared.error == PngDecodeError.none && + declared.profile_assumed == 0 && declared.pixels.data[0] == 9 && + declared.pixels.data[1] == 28 && declared.pixels.data[2] == 48, + "declared gamma and standard primaries convert without assumed profile truth"); + png_decode_free(&declared); png_encode_free(&declared_fixture); + + let gamma_only_fixture: PngEncodeResult = fixture_profile_png(1, 0, 45455, 0, 0); + let gamma_only: PngDecodeResult = png_decode( + gamma_only_fixture.bytes.data[0..gamma_only_fixture.bytes.len] + ); + expect(state, gamma_only.error == PngDecodeError.none && + gamma_only.profile_assumed != 0, + "gamma without declared primaries keeps the fallback-profile truth visible"); + png_decode_free(&gamma_only); png_encode_free(&gamma_only_fixture); + + let linear_fixture: PngEncodeResult = fixture_profile_png(1, 0, 100000, 0, 0); + let linear: PngDecodeResult = png_decode( + linear_fixture.bytes.data[0..linear_fixture.bytes.len] + ); + expect(state, linear.error == PngDecodeError.none && + linear.profile_assumed != 0 && linear.pixels.data[0] == 73 && + linear.pixels.data[1] == 102 && linear.pixels.data[2] == 124 && + linear.pixels.data[3] == 255, + "linear-gamma samples convert deterministically into canonical sRGB"); + png_decode_free(&linear); png_encode_free(&linear_fixture); + + let zero_fixture: PngEncodeResult = fixture_profile_png(1, 0, 0, 0, 0); + let zero: PngDecodeResult = png_decode( + zero_fixture.bytes.data[0..zero_fixture.bytes.len] + ); + expect(state, zero.error == PngDecodeError.none && + zero.profile_assumed != 0 && zero.pixels.data[0] == 17 && + zero.pixels.data[1] == 34 && zero.pixels.data[2] == 51, + "meaningless zero gamma is ignored while fallback truth stays visible"); + png_decode_free(&zero); png_encode_free(&zero_fixture); + + let p3_fixture: PngEncodeResult = fixture_profile_png(1, 2, 100000, 0, 0); + let p3: PngDecodeResult = png_decode( + p3_fixture.bytes.data[0..p3_fixture.bytes.len] + ); + expect(state, p3.error == PngDecodeError.none && + p3.profile_assumed == 0 && p3.pixels.data[0] == 64 && + p3.pixels.data[1] == 103 && p3.pixels.data[2] == 126, + "Display P3 primaries convert through the bounded linear matrix path"); + png_decode_free(&p3); png_encode_free(&p3_fixture); + + let adapted_fixture: PngEncodeResult = fixture_profile_png(1, 3, 100000, 0, 0); + let adapted: PngDecodeResult = png_decode( + adapted_fixture.bytes.data[0..adapted_fixture.bytes.len] + ); + expect(state, adapted.error == PngDecodeError.none && + adapted.profile_assumed == 0 && adapted.pixels.data[0] == 69 && + adapted.pixels.data[1] == 102 && adapted.pixels.data[2] == 123, + "non-D65 white points use deterministic Bradford adaptation"); + png_decode_free(&adapted); png_encode_free(&adapted_fixture); + + let assumed_fixture: PngEncodeResult = fixture_profile_png(0, 2, 0, 0, 0); + let assumed: PngDecodeResult = png_decode( + assumed_fixture.bytes.data[0..assumed_fixture.bytes.len] + ); + expect(state, assumed.error == PngDecodeError.none && + assumed.profile_assumed != 0 && assumed.pixels.data[0] == 11 && + assumed.pixels.data[1] == 35 && assumed.pixels.data[2] == 53, + "chromaticities without a transfer curve preserve fallback-profile truth"); + png_decode_free(&assumed); png_encode_free(&assumed_fixture); + + let precedence_fixture: PngEncodeResult = fixture_profile_png(1, 2, 100000, 1, 0); + let precedence: PngDecodeResult = png_decode( + precedence_fixture.bytes.data[0..precedence_fixture.bytes.len] + ); + expect(state, precedence.error == PngDecodeError.none && + precedence.profile_assumed == 0 && precedence.pixels.data[0] == 17 && + precedence.pixels.data[1] == 34 && precedence.pixels.data[2] == 51, + "explicit sRGB takes precedence over accompanying gamma and chromaticities"); + png_decode_free(&precedence); png_encode_free(&precedence_fixture); + + let duplicate_fixture: PngEncodeResult = fixture_profile_png(2, 0, 45455, 0, 0); + let duplicate: PngDecodeResult = png_decode( + duplicate_fixture.bytes.data[0..duplicate_fixture.bytes.len] + ); + expect(state, duplicate.error == PngDecodeError.duplicate_chunk, + "duplicate color metadata fails before image decompression"); + png_decode_free(&duplicate); png_encode_free(&duplicate_fixture); + + let singular_fixture: PngEncodeResult = fixture_profile_png(1, 4, 45455, 0, 0); + let singular: PngDecodeResult = png_decode( + singular_fixture.bytes.data[0..singular_fixture.bytes.len] + ); + expect(state, singular.error == PngDecodeError.unsupported_color_profile, + "singular chromaticities fail before decompression or conversion"); + png_decode_free(&singular); png_encode_free(&singular_fixture); + + let icc_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 1); + let icc: PngDecodeResult = png_decode( + icc_fixture.bytes.data[0..icc_fixture.bytes.len] + ); + expect(state, icc.error == PngDecodeError.none && icc.profile_assumed == 0 && + icc.pixels.data[0] == 73 && icc.pixels.data[1] == 102 && + icc.pixels.data[2] == 124, + "bounded ICC matrix and identity TRCs execute as declared profile truth"); + png_decode_free(&icc); png_encode_free(&icc_fixture); + + let icc_p3_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 2); + let icc_p3: PngDecodeResult = png_decode( + icc_p3_fixture.bytes.data[0..icc_p3_fixture.bytes.len] + ); + expect(state, icc_p3.error == PngDecodeError.none && + icc_p3.profile_assumed == 0 && icc_p3.pixels.data[0] == 64 && + icc_p3.pixels.data[1] == 103 && icc_p3.pixels.data[2] == 126, + "Display P3 ICC matrix columns convert through the D50 PCS"); + png_decode_free(&icc_p3); png_encode_free(&icc_p3_fixture); + + let icc_precedence_fixture: PngEncodeResult = fixture_profile_png( + 1, 1, 45455, 1, 2 + ); + let icc_precedence: PngDecodeResult = png_decode( + icc_precedence_fixture.bytes.data[0..icc_precedence_fixture.bytes.len] + ); + expect(state, icc_precedence.error == PngDecodeError.none && + icc_precedence.pixels.data[0] == 64 && + icc_precedence.pixels.data[1] == 103 && + icc_precedence.pixels.data[2] == 126, + "understood iCCP takes precedence over compatibility color chunks"); + png_decode_free(&icc_precedence); + png_encode_free(&icc_precedence_fixture); + + let bad_icc_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 3); + let bad_icc: PngDecodeResult = png_decode( + bad_icc_fixture.bytes.data[0..bad_icc_fixture.bytes.len] + ); + expect(state, bad_icc.error == PngDecodeError.unsupported_color_profile, + "ICC tag offsets cannot escape the bounded decompressed profile"); + png_decode_free(&bad_icc); png_encode_free(&bad_icc_fixture); + + let lut_icc_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 4); + let lut_icc: PngDecodeResult = png_decode( + lut_icc_fixture.bytes.data[0..lut_icc_fixture.bytes.len] + ); + expect(state, lut_icc.error == PngDecodeError.none && + lut_icc.profile_assumed == 0 && lut_icc.pixels.data[0] == 73 && + lut_icc.pixels.data[1] == 102 && lut_icc.pixels.data[2] == 124, + "two-entry sampled ICC curves interpolate the complete input domain"); + png_decode_free(&lut_icc); png_encode_free(&lut_icc_fixture); + + let sampled_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 18); + let sampled: PngDecodeResult = png_decode( + sampled_fixture.bytes.data[0..sampled_fixture.bytes.len] + ); + expect(state, sampled.error == PngDecodeError.none && + sampled.profile_assumed == 0 && sampled.pixels.data[0] == 63 && + sampled.pixels.data[1] == 89 && sampled.pixels.data[2] == 108, + "multi-entry sampled ICC curves use deterministic linear interpolation"); + png_decode_free(&sampled); + let sampled_repeat_valid: i32 = 1; + let sampled_repeat: i32 = 0; + while (sampled_repeat < 4096 && sampled_repeat_valid != 0) { + let repeated_sampled: PngDecodeResult = png_decode( + sampled_fixture.bytes.data[0..sampled_fixture.bytes.len] + ); + if (repeated_sampled.error != PngDecodeError.none || + repeated_sampled.pixels.data[0] != 63 || + repeated_sampled.pixels.data[1] != 89 || + repeated_sampled.pixels.data[2] != 108) { + sampled_repeat_valid = 0; + } + png_decode_free(&repeated_sampled); + sampled_repeat = sampled_repeat + 1; + } + expect(state, sampled_repeat_valid != 0 && sampled_repeat == 4096, + "4096 sampled ICC parse decode and cleanup cycles preserve exact output"); + png_encode_free(&sampled_fixture); + + let lut16_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 21); + let lut16: PngDecodeResult = png_decode( + lut16_fixture.bytes.data[0..lut16_fixture.bytes.len] + ); + expect(state, lut16.error == PngDecodeError.none && + lut16.profile_assumed == 0 && lut16.pixels.data[0] == 73 && + lut16.pixels.data[1] == 102 && lut16.pixels.data[2] == 124, + "ICC lut16 input tables CLUT and output tables execute in declared order"); + png_decode_free(&lut16); + let lut16_repeat_valid: i32 = 1; + let lut16_repeat: i32 = 0; + while (lut16_repeat < 4096 && lut16_repeat_valid != 0) { + let repeated_lut16: PngDecodeResult = png_decode( + lut16_fixture.bytes.data[0..lut16_fixture.bytes.len] + ); + if (repeated_lut16.error != PngDecodeError.none || + repeated_lut16.pixels.data[0] != 73 || + repeated_lut16.pixels.data[1] != 102 || + repeated_lut16.pixels.data[2] != 124) { + lut16_repeat_valid = 0; + } + png_decode_free(&repeated_lut16); + lut16_repeat = lut16_repeat + 1; + } + expect(state, lut16_repeat_valid != 0 && lut16_repeat == 4096, + "4096 ICC lut16 parse decode and cleanup cycles preserve exact output"); + png_encode_free(&lut16_fixture); + + let bad_lut_grid_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 22 + ); + let bad_lut_grid: PngDecodeResult = png_decode( + bad_lut_grid_fixture.bytes.data[0..bad_lut_grid_fixture.bytes.len] + ); + expect(state, bad_lut_grid.error == PngDecodeError.unsupported_color_profile, + "ICC lut16 grids above the bounded three dimensional ceiling fail closed"); + png_decode_free(&bad_lut_grid); png_encode_free(&bad_lut_grid_fixture); + + let bad_lut_matrix_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 23 + ); + let bad_lut_matrix: PngDecodeResult = png_decode( + bad_lut_matrix_fixture.bytes.data[0..bad_lut_matrix_fixture.bytes.len] + ); + expect(state, bad_lut_matrix.error == PngDecodeError.unsupported_color_profile, + "RGB to PCS lut16 profiles require the ICC mandated identity matrix"); + png_decode_free(&bad_lut_matrix); png_encode_free(&bad_lut_matrix_fixture); + + let ambiguous_lut8_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 24 + ); + let ambiguous_lut8: PngDecodeResult = png_decode( + ambiguous_lut8_fixture.bytes.data[0..ambiguous_lut8_fixture.bytes.len] + ); + expect(state, ambiguous_lut8.error == PngDecodeError.unsupported_color_profile, + "ambiguous lut8 PCSXYZ profiles remain an explicit unsupported path"); + png_decode_free(&ambiguous_lut8); png_encode_free(&ambiguous_lut8_fixture); + + let truncated_lut_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 25 + ); + let truncated_lut: PngDecodeResult = png_decode( + truncated_lut_fixture.bytes.data[0..truncated_lut_fixture.bytes.len] + ); + expect(state, truncated_lut.error == PngDecodeError.unsupported_color_profile, + "ICC lut16 tables must consume exactly their declared bounded tag"); + png_decode_free(&truncated_lut); png_encode_free(&truncated_lut_fixture); + + let short_lut_table_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 26 + ); + let short_lut_table: PngDecodeResult = png_decode( + short_lut_table_fixture.bytes.data[0..short_lut_table_fixture.bytes.len] + ); + expect(state, short_lut_table.error == PngDecodeError.unsupported_color_profile, + "ICC lut16 one dimensional tables require at least two entries"); + png_decode_free(&short_lut_table); png_encode_free(&short_lut_table_fixture); + + let wrong_lut_intent_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 27 + ); + let wrong_lut_intent: PngDecodeResult = png_decode( + wrong_lut_intent_fixture.bytes.data[0..wrong_lut_intent_fixture.bytes.len] + ); + expect(state, + wrong_lut_intent.error == PngDecodeError.unsupported_color_profile, + "A2B0 LUT execution cannot impersonate a non-perceptual rendering intent"); + png_decode_free(&wrong_lut_intent); + png_encode_free(&wrong_lut_intent_fixture); + + let mab_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 28); + let mab: PngDecodeResult = png_decode( + mab_fixture.bytes.data[0..mab_fixture.bytes.len] + ); + expect(state, mab.error == PngDecodeError.none && mab.profile_assumed == 0 && + mab.pixels.data[0] == 73 && mab.pixels.data[1] == 102 && + mab.pixels.data[2] == 124, + "ICC mAB A curves nonuniform CLUT and B curves execute in declared order"); + png_decode_free(&mab); + let mab_repeat_valid: i32 = 1; + let mab_repeat: i32 = 0; + while (mab_repeat < 4096 && mab_repeat_valid != 0) { + let repeated_mab: PngDecodeResult = png_decode( + mab_fixture.bytes.data[0..mab_fixture.bytes.len] + ); + if (repeated_mab.error != PngDecodeError.none || + repeated_mab.pixels.data[0] != 73 || + repeated_mab.pixels.data[1] != 102 || + repeated_mab.pixels.data[2] != 124) { mab_repeat_valid = 0; } + png_decode_free(&repeated_mab); mab_repeat = mab_repeat + 1; + } + expect(state, mab_repeat_valid != 0 && mab_repeat == 4096, + "4096 ICC mAB curve CLUT and cleanup cycles preserve exact output"); + png_encode_free(&mab_fixture); + + let mab_bad_unused_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 29 + ); + let mab_bad_unused: PngDecodeResult = png_decode( + mab_bad_unused_fixture.bytes.data[0..mab_bad_unused_fixture.bytes.len] + ); + expect(state, mab_bad_unused.error == PngDecodeError.unsupported_color_profile, + "ICC mAB unused CLUT dimensions must remain canonical zeroes"); + png_decode_free(&mab_bad_unused); png_encode_free(&mab_bad_unused_fixture); + + let mab_missing_b_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 30 + ); + let mab_missing_b: PngDecodeResult = png_decode( + mab_missing_b_fixture.bytes.data[0..mab_missing_b_fixture.bytes.len] + ); + expect(state, mab_missing_b.error == PngDecodeError.unsupported_color_profile, + "ICC mAB A CLUT B execution requires the complete permitted combination"); + png_decode_free(&mab_missing_b); png_encode_free(&mab_missing_b_fixture); + + let mab_bad_curve_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 31 + ); + let mab_bad_curve: PngDecodeResult = png_decode( + mab_bad_curve_fixture.bytes.data[0..mab_bad_curve_fixture.bytes.len] + ); + expect(state, mab_bad_curve.error == PngDecodeError.unsupported_color_profile, + "ICC mAB embedded curve types fail before CLUT ownership escapes"); + png_decode_free(&mab_bad_curve); png_encode_free(&mab_bad_curve_fixture); + + let mab_bad_precision_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 32 + ); + let mab_bad_precision: PngDecodeResult = png_decode( + mab_bad_precision_fixture.bytes.data[0..mab_bad_precision_fixture.bytes.len] + ); + expect(state, + mab_bad_precision.error == PngDecodeError.unsupported_color_profile, + "ICC mAB CLUT precision is restricted to declared 8 or 16 bit data"); + png_decode_free(&mab_bad_precision); + png_encode_free(&mab_bad_precision_fixture); + + let mab_overlap_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 33 + ); + let mab_overlap: PngDecodeResult = png_decode( + mab_overlap_fixture.bytes.data[0..mab_overlap_fixture.bytes.len] + ); + expect(state, mab_overlap.error == PngDecodeError.unsupported_color_profile, + "ICC mAB CLUT bytes cannot overlap an embedded curve sequence"); + png_decode_free(&mab_overlap); png_encode_free(&mab_overlap_fixture); + + let oversized_curve_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 19 + ); + let oversized_curve: PngDecodeResult = png_decode( + oversized_curve_fixture.bytes.data[0..oversized_curve_fixture.bytes.len] + ); + expect(state, oversized_curve.error == PngDecodeError.unsupported_color_profile, + "sampled ICC curve counts above the execution ceiling fail before allocation"); + png_decode_free(&oversized_curve); png_encode_free(&oversized_curve_fixture); + + let truncated_curve_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 20 + ); + let truncated_curve: PngDecodeResult = png_decode( + truncated_curve_fixture.bytes.data[0..truncated_curve_fixture.bytes.len] + ); + expect(state, truncated_curve.error == PngDecodeError.unsupported_color_profile, + "sampled ICC curve tables cannot cross their declared tag bounds"); + png_decode_free(&truncated_curve); png_encode_free(&truncated_curve_fixture); + + let bad_name_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 5); + let bad_name: PngDecodeResult = png_decode( + bad_name_fixture.bytes.data[0..bad_name_fixture.bytes.len] + ); + expect(state, bad_name.error == PngDecodeError.unsupported_color_profile, + "iCCP profile names reject leading and consecutive spaces"); + png_decode_free(&bad_name); png_encode_free(&bad_name_fixture); + + let bad_method_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 6); + let bad_method: PngDecodeResult = png_decode( + bad_method_fixture.bytes.data[0..bad_method_fixture.bytes.len] + ); + expect(state, bad_method.error == PngDecodeError.unsupported_color_profile, + "unknown iCCP compression methods fail before profile inflate"); + png_decode_free(&bad_method); png_encode_free(&bad_method_fixture); + + let duplicate_icc_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 7); + let duplicate_icc: PngDecodeResult = png_decode( + duplicate_icc_fixture.bytes.data[0..duplicate_icc_fixture.bytes.len] + ); + expect(state, duplicate_icc.error == PngDecodeError.duplicate_chunk, + "duplicate iCCP chunks fail before image decompression"); + png_decode_free(&duplicate_icc); png_encode_free(&duplicate_icc_fixture); + + let parametric_icc_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 8 + ); + let parametric_icc: PngDecodeResult = png_decode( + parametric_icc_fixture.bytes.data[0..parametric_icc_fixture.bytes.len] + ); + expect(state, parametric_icc.error == PngDecodeError.none && + parametric_icc.profile_assumed == 0 && + parametric_icc.pixels.data[0] == 73 && + parametric_icc.pixels.data[1] == 102 && + parametric_icc.pixels.data[2] == 124, + "ICC parametric gamma curves execute through the same linear pipeline"); + png_decode_free(¶metric_icc); + png_encode_free(¶metric_icc_fixture); + + let srgb_curve_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 13 + ); + let srgb_curve: PngDecodeResult = png_decode( + srgb_curve_fixture.bytes.data[0..srgb_curve_fixture.bytes.len] + ); + expect(state, srgb_curve.error == PngDecodeError.none && + srgb_curve.profile_assumed == 0 && srgb_curve.pixels.data[0] == 17 && + srgb_curve.pixels.data[1] == 34 && srgb_curve.pixels.data[2] == 51, + "ICC type-3 sRGB parametric curves execute both piecewise branches"); + png_decode_free(&srgb_curve); png_encode_free(&srgb_curve_fixture); + + let type1_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 14); + let type1_curve: PngDecodeResult = png_decode( + type1_fixture.bytes.data[0..type1_fixture.bytes.len] + ); + let type2_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 15); + let type2_curve: PngDecodeResult = png_decode( + type2_fixture.bytes.data[0..type2_fixture.bytes.len] + ); + let type4_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 16); + let type4_curve: PngDecodeResult = png_decode( + type4_fixture.bytes.data[0..type4_fixture.bytes.len] + ); + expect(state, type1_curve.error == PngDecodeError.none && + type2_curve.error == PngDecodeError.none && + type4_curve.error == PngDecodeError.none && + type1_curve.pixels.data[0] == 73 && type1_curve.pixels.data[1] == 102 && + type1_curve.pixels.data[2] == 124 && + type2_curve.pixels.data[0] == 73 && type2_curve.pixels.data[1] == 102 && + type2_curve.pixels.data[2] == 124 && + type4_curve.pixels.data[0] == 73 && type4_curve.pixels.data[1] == 102 && + type4_curve.pixels.data[2] == 124, + "ICC parametric curve types 1 2 and 4 execute their declared equations"); + png_decode_free(&type1_curve); png_encode_free(&type1_fixture); + png_decode_free(&type2_curve); png_encode_free(&type2_fixture); + png_decode_free(&type4_curve); png_encode_free(&type4_fixture); + + let channel_curve_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 17 + ); + let channel_curve: PngDecodeResult = png_decode( + channel_curve_fixture.bytes.data[0..channel_curve_fixture.bytes.len] + ); + expect(state, channel_curve.error == PngDecodeError.none && + channel_curve.profile_assumed == 0 && + channel_curve.pixels.data[0] == 73 && + channel_curve.pixels.data[1] == 36 && + channel_curve.pixels.data[2] == 178, + "differing ICC channel TRCs execute independently before matrix conversion"); + png_decode_free(&channel_curve); png_encode_free(&channel_curve_fixture); + + let cicp_fixture: PngEncodeResult = fixture_profile_png(0, 0, 0, 0, 9); + let cicp: PngDecodeResult = png_decode( + cicp_fixture.bytes.data[0..cicp_fixture.bytes.len] + ); + expect(state, cicp.error == PngDecodeError.unsupported_color_profile, + "unimplemented higher-precedence cICP metadata fails explicitly"); + png_decode_free(&cicp); png_encode_free(&cicp_fixture); + + let duplicate_tag_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 10 + ); + let duplicate_tag: PngDecodeResult = png_decode( + duplicate_tag_fixture.bytes.data[0..duplicate_tag_fixture.bytes.len] + ); + expect(state, duplicate_tag.error == PngDecodeError.unsupported_color_profile, + "duplicate required ICC tags fail without ambiguous profile execution"); + png_decode_free(&duplicate_tag); png_encode_free(&duplicate_tag_fixture); + + let size_mismatch_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 11 + ); + let size_mismatch: PngDecodeResult = png_decode( + size_mismatch_fixture.bytes.data[0..size_mismatch_fixture.bytes.len] + ); + expect(state, size_mismatch.error == PngDecodeError.unsupported_color_profile, + "ICC declared size must equal the bounded decompressed profile"); + png_decode_free(&size_mismatch); png_encode_free(&size_mismatch_fixture); + + let broken_iccp_fixture: PngEncodeResult = fixture_profile_png( + 0, 0, 0, 0, 12 + ); + let broken_iccp: PngDecodeResult = png_decode( + broken_iccp_fixture.bytes.data[0..broken_iccp_fixture.bytes.len] + ); + expect(state, broken_iccp.error == PngDecodeError.unsupported_color_profile, + "corrupt iCCP zlib streams fail before ICC tag parsing"); + png_decode_free(&broken_iccp); png_encode_free(&broken_iccp_fixture); + + let ramp: ArrayList[u8] = make[u8](256); + let sample: i32 = 0; + while (sample < 256) { + push[u8](&ramp, png_gamma_sample_to_srgb(sample as u8, 100000)); + sample = sample + 1; + } + let ramps_exact: i32 = checksum_crc32(ramp.data[0..ramp.len]) == 1696635244; + free[u8](&ramp); + let gamma_values: ArrayList[i64] = make[i64](7); + push[i64](&gamma_values, 45455); push[i64](&gamma_values, 70000); + push[i64](&gamma_values, 55000); + push[i64](&gamma_values, 35000); push[i64](&gamma_values, 220000); + push[i64](&gamma_values, 25000); push[i64](&gamma_values, 500000); + let expected_crcs: ArrayList[i64] = make[i64](7); + push[i64](&expected_crcs, 247783021); push[i64](&expected_crcs, 3885669558); + push[i64](&expected_crcs, 4144236837); + push[i64](&expected_crcs, 2933270737); push[i64](&expected_crcs, 2066708568); + push[i64](&expected_crcs, 726426539); push[i64](&expected_crcs, 3244028137); + let gamma_index: i32 = 0; + while (gamma_index < gamma_values.len && ramps_exact != 0) { + let values: ArrayList[u8] = make[u8](256); + sample = 0; + while (sample < 256) { + push[u8](&values, png_gamma_sample_to_srgb( + sample as u8, gamma_values.data[gamma_index] + )); + sample = sample + 1; + } + if (checksum_crc32(values.data[0..values.len]) != + expected_crcs.data[gamma_index]) { ramps_exact = 0; } + free[u8](&values); gamma_index = gamma_index + 1; + } + expect(state, ramps_exact, + "eight full gamma ramps match independent sRGB transfer references"); + free[i64](&expected_crcs); free[i64](&gamma_values); +} + +fn interlace_contract(state: *TestState) void { + let pixels: ArrayList[u8] = make[u8](17 * 17 * 4); + let y: i32 = 0; + while (y < 17) { + let x: i32 = 0; + while (x < 17) { + push[u8](&pixels, ((x * 17 + y * 3) & 255) as u8); + push[u8](&pixels, ((x * 5 + y * 29) & 255) as u8); + push[u8](&pixels, ((x * 41 + y * 7) & 255) as u8); + push[u8](&pixels, ((x * 11 + y * 13) & 255) as u8); + x = x + 1; + } + y = y + 1; + } + let fixture: PngEncodeResult = fixture_adam7_rgba8( + 17, 17, pixels.data[0..pixels.len] + ); + let decoded: PngDecodeResult = png_decode( + fixture.bytes.data[0..fixture.bytes.len] + ); + let exact: i32 = decoded.error == PngDecodeError.none && + decoded.pixels.len == pixels.len; + let i: i32 = 0; + while (i < decoded.pixels.len && i < pixels.len) { + if (decoded.pixels.data[i] != pixels.data[i]) { exact = 0; } + i = i + 1; + } + expect(state, exact, + "all seven Adam7 passes reconstruct exact RGBA8 pixels through every filter"); + png_decode_free(&decoded); png_encode_free(&fixture); free[u8](&pixels); + + let sweep_exact: i32 = 1; + let sweep_height: i32 = 1; + while (sweep_height <= 12 && sweep_exact != 0) { + let sweep_width: i32 = 1; + while (sweep_width <= 12 && sweep_exact != 0) { + let sweep_pixels: ArrayList[u8] = make[u8]( + sweep_width * sweep_height * 4 + ); + let sweep_index: i32 = 0; + while (sweep_index < sweep_width * sweep_height * 4) { + push[u8](&sweep_pixels, + ((sweep_index * 37 + sweep_width * 11 + sweep_height) & 255) as u8); + sweep_index = sweep_index + 1; + } + let sweep_fixture: PngEncodeResult = fixture_adam7_rgba8( + sweep_width, sweep_height, + sweep_pixels.data[0..sweep_pixels.len] + ); + let sweep_decoded: PngDecodeResult = png_decode( + sweep_fixture.bytes.data[0..sweep_fixture.bytes.len] + ); + if (sweep_decoded.error != PngDecodeError.none || + sweep_decoded.pixels.len != sweep_pixels.len) { sweep_exact = 0; } + sweep_index = 0; + while (sweep_index < sweep_decoded.pixels.len && + sweep_index < sweep_pixels.len && sweep_exact != 0) { + if (sweep_decoded.pixels.data[sweep_index] != + sweep_pixels.data[sweep_index]) { sweep_exact = 0; } + sweep_index = sweep_index + 1; + } + png_decode_free(&sweep_decoded); png_encode_free(&sweep_fixture); + free[u8](&sweep_pixels); sweep_width = sweep_width + 1; + } + sweep_height = sweep_height + 1; + } + expect(state, sweep_exact, + "144 Adam7 dimension combinations preserve empty-pass and edge geometry"); + + let packed: ArrayList[u8] = make[u8](128); + let pass: i32 = 0; + while (pass < 7) { + let start_x: i32 = png_adam7_start_x(pass); + let start_y: i32 = png_adam7_start_y(pass); + let step_x: i32 = png_adam7_step_x(pass); + let step_y: i32 = png_adam7_step_y(pass); + let pass_width: i32 = png_adam7_extent(9, start_x, step_x); + let pass_height: i32 = png_adam7_extent(9, start_y, step_y); + if (pass_width > 0 && pass_height > 0) { + let row_bytes: i32 = (pass_width + 7) / 8; + let pass_y: i32 = 0; + while (pass_y < pass_height) { + push[u8](&packed, 0); + let row: ArrayList[u8] = make[u8](row_bytes); + let fill: i32 = 0; + while (fill < row_bytes) { push[u8](&row, 0); fill = fill + 1; } + let pass_x: i32 = 0; + while (pass_x < pass_width) { + let source_x: i32 = start_x + pass_x * step_x; + let source_y: i32 = start_y + pass_y * step_y; + png_write_packed_sample(row.data[0..row.len], pass_x, 1, + (source_x + source_y) & 1); + pass_x = pass_x + 1; + } + png_copy_append(&packed, row.data[0..row.len]); + free[u8](&row); pass_y = pass_y + 1; + } + } + pass = pass + 1; + } + let packed_fixture: PngEncodeResult = fixture_png_interlaced( + 9, 9, 1, 0, packed.data[0..packed.len], "", "" + ); + let packed_decoded: PngDecodeResult = png_decode( + packed_fixture.bytes.data[0..packed_fixture.bytes.len] + ); + let packed_exact: i32 = packed_decoded.error == PngDecodeError.none && + packed_decoded.pixels.len == 9 * 9 * 4; + y = 0; + while (y < 9 && packed_exact != 0) { + let x: i32 = 0; + while (x < 9) { + let expected: u8 = (((x + y) & 1) * 255) as u8; + let at: i32 = (y * 9 + x) * 4; + if (packed_decoded.pixels.data[at] != expected || + packed_decoded.pixels.data[at + 1] != expected || + packed_decoded.pixels.data[at + 2] != expected || + packed_decoded.pixels.data[at + 3] != 255) { packed_exact = 0; } + x = x + 1; + } + y = y + 1; + } + expect(state, packed_exact, + "Adam7 deinterlaces packed sub-byte samples without losing bit identity"); + png_decode_free(&packed_decoded); png_encode_free(&packed_fixture); + free[u8](&packed); +} + +fn dynamic_inflate_contract(state: *TestState) void { + let compressed: []u8 = + "\x78\xda\xed\xd0\x4d\x2b\xc3\x71\x00\xc0\xf1\x7f\xcd\x43\x5b\xda\xf2\xb0\x9a\x30\x31\x99\x96\x95\xa1\xa1\xad\x96\x95\x45\x8b\x28\x5a\x91\x87\x3c\xd5\x6a\x21\x4b\x16\x59\x4d\x34\x69\x42\xab\x95\xa7\x3c\x64\x25\x2b\x8b\x16\x4d\x71\xe1\xa0\xe4\xb2\x8b\x94\x03\x17\x0e\x4a\x2e\x2e\x52\x5f\x2f\xc4\xef\xf3\x12\x3e\x92\x24\x49\xc8\xd2\x33\xc9\x50\x28\x91\xab\xd4\x64\xe5\x15\xa2\xca\xd7\x91\xa3\x35\xa0\xd6\x99\xd0\x54\xd4\x53\x60\xb4\xa1\xad\x76\x50\x52\xd7\x4a\x99\xb5\x13\x7d\x63\x0f\x06\xc7\x20\x46\xa7\x9b\xaa\xf6\x09\x6a\xba\xa6\x31\x77\xfb\x69\xe8\x5f\xc4\x3a\x1c\xc2\xe6\x0e\x63\x1f\xdb\xa4\xc9\xbb\x4f\xb3\xef\x08\xe7\x5c\x9c\xb6\xf9\x73\x3a\x82\x57\x74\x86\x6e\x71\xad\xdf\xd3\x1d\x49\xd1\xbb\xf5\xc4\xc0\xde\x0b\x43\xd1\x77\x46\x8f\x3f\x71\xc7\xbf\xf1\x24\x7e\x19\x4f\xca\x98\xbc\x56\x30\x75\x93\x8d\xef\x4e\xc3\xec\x43\x31\xfe\x54\x39\x81\x47\x23\x0b\xcf\xb5\x04\x5f\x2d\x2c\xbf\xd9\x59\xf9\x68\x61\xed\xab\x9d\xf0\xb7\x8b\xc8\x4f\x1f\x1b\x8c\xb0\x9d\xe6\x61\x57\xee\xe5\x40\x39\x43\x34\x37\xc0\x91\x66\x89\x58\xd1\x2a\x27\xa5\x11\x4e\xf5\x3b\x24\x2a\x0f\xb9\x30\xc5\xb8\x34\x9f\x71\x65\x49\x22\x89\x00\x11\x20\x02\x44\x80\x08\x10\x01\x22\x40\x04\x88\x00\x11\x20\x02\x44\x80\x08\x10\x01\xff\x2f\xe0\x0f\xd7\xee\xee\x79"; + let fixture: PngEncodeResult = fixture_png_compressed(64, 16, compressed); + let decoded: PngDecodeResult = png_decode(fixture.bytes.data[0..fixture.bytes.len]); + let exact: i32 = decoded.error == PngDecodeError.none && decoded.pixels.len == 4096; + let y: i32 = 0; + while (y < 16 && exact != 0) { + let x: i32 = 0; + while (x < 64) { + let at: i32 = (y * 64 + x) * 4; + if (decoded.pixels.data[at] != ((x * 3) % 256) as u8 || + decoded.pixels.data[at + 1] != ((x * 5) % 256) as u8 || + decoded.pixels.data[at + 2] != ((x * 7) % 256) as u8 || + decoded.pixels.data[at + 3] != 255) { exact = 0; } + x = x + 1; + } + y = y + 1; + } + expect(state, exact, + "PNG IDAT accepts independently generated dynamic-Huffman zlib streams"); + png_decode_free(&decoded); png_encode_free(&fixture); +} + +fn rejection_contract(state: *TestState) void { + let signature: PngDecodeResult = png_decode("not a png"); + expect(state, signature.error == PngDecodeError.invalid_signature, + "invalid signatures fail before chunk parsing"); + png_decode_free(&signature); + + let surface: CpuSurface = cpu_surface_make(1, 1); + let encoded: PngEncodeResult = png_encode_surface(surface); + encoded.bytes.data[29] = encoded.bytes.data[29] ^ 1; + let crc: PngDecodeResult = png_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, crc.error == PngDecodeError.invalid_chunk_crc, + "chunk corruption fails CRC validation before decompression"); + png_decode_free(&crc); + encoded.bytes.data[29] = encoded.bytes.data[29] ^ 1; + + push[u8](&encoded.bytes, 0); + let trailing: PngDecodeResult = png_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, trailing.error == PngDecodeError.trailing_data, + "bytes after IEND fail canonical full-file decoding"); + png_decode_free(&trailing); + encoded.bytes.len = encoded.bytes.len - 1; + + let every_prefix_rejected: i32 = 1; + let length: i32 = 0; + while (length < encoded.bytes.len) { + let prefix: PngDecodeResult = png_decode(encoded.bytes.data[0..length]); + if (prefix.error == PngDecodeError.none) { every_prefix_rejected = 0; } + png_decode_free(&prefix); + length = length + 1; + } + expect(state, every_prefix_rejected, + "every strict PNG prefix fails without reading beyond available bytes"); + + encoded.bytes.data[28] = 2; + fixture_rewrite_crc(&encoded.bytes, 8); + let interlace: PngDecodeResult = png_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, interlace.error == PngDecodeError.unsupported_interlace, + "unknown PNG interlace methods fail before decompression"); + png_decode_free(&interlace); + encoded.bytes.data[28] = 0; + fixture_rewrite_crc(&encoded.bytes, 8); + + let iend_offset: i32 = encoded.bytes.len - 12; + encoded.bytes.data[iend_offset + 6] = 'n'; + fixture_rewrite_crc(&encoded.bytes, iend_offset); + let reserved_type: PngDecodeResult = png_decode( + encoded.bytes.data[0..encoded.bytes.len] + ); + expect(state, reserved_type.error == PngDecodeError.invalid_chunk_type, + "chunk type reserved bits fail even when the attacker repairs the CRC"); + png_decode_free(&reserved_type); + encoded.bytes.data[iend_offset + 6] = 'N'; + fixture_rewrite_crc(&encoded.bytes, iend_offset); + + fixture_write_u32_be(&encoded.bytes, 16, 9000); + fixture_rewrite_crc(&encoded.bytes, 8); + let oversized: PngDecodeResult = png_decode(encoded.bytes.data[0..encoded.bytes.len]); + expect(state, oversized.error == PngDecodeError.pixel_limit, + "oversized dimensions fail before allocation or decompression"); + png_decode_free(&oversized); + png_encode_free(&encoded); + cpu_surface_free(&surface); + + let bad_filter_fixture: PngEncodeResult = fixture_png( + 1, 1, 8, 6, "\x05\x00\x00\x00\xff", "", "" + ); + let bad_filter: PngDecodeResult = png_decode( + bad_filter_fixture.bytes.data[0..bad_filter_fixture.bytes.len] + ); + expect(state, bad_filter.error == PngDecodeError.invalid_filter, + "unknown scanline filters fail explicitly"); + png_decode_free(&bad_filter); png_encode_free(&bad_filter_fixture); + + let expanding: ZlibStoreEncodeResult = zlib_store_encode( + "\x00\x00\x00\x00\xff\x7f" + ); + let expansion_fixture: PngEncodeResult = fixture_png_compressed( + 1, 1, expanding.bytes.data[0..expanding.bytes.len] + ); + let expansion: PngDecodeResult = png_decode( + expansion_fixture.bytes.data[0..expansion_fixture.bytes.len] + ); + expect(state, expansion.error == PngDecodeError.compression_error, + "compressed expansion beyond exact scanline size hits the inflate ceiling"); + png_decode_free(&expansion); png_encode_free(&expansion_fixture); + zlib_store_encode_free(&expanding); + + let missing_palette_fixture: PngEncodeResult = fixture_png( + 1, 1, 1, 3, "\x00\x00", "", "" + ); + let missing_palette: PngDecodeResult = png_decode( + missing_palette_fixture.bytes.data[0..missing_palette_fixture.bytes.len] + ); + expect(state, missing_palette.error == PngDecodeError.missing_palette, + "indexed images require a palette before inflate"); + png_decode_free(&missing_palette); png_encode_free(&missing_palette_fixture); + + let bad_index_fixture: PngEncodeResult = fixture_png( + 1, 1, 1, 3, "\x00\x80", "\xff\x00\x00", "" + ); + let bad_index: PngDecodeResult = png_decode( + bad_index_fixture.bytes.data[0..bad_index_fixture.bytes.len] + ); + expect(state, bad_index.error == PngDecodeError.palette_index, + "palette indices outside declared entries fail without out-of-bounds access"); + png_decode_free(&bad_index); png_encode_free(&bad_index_fixture); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + round_trip_contract(&state); + filter_contract(&state); + scale_render_contract(&state); + color_contract(&state); + color_profile_contract(&state); + interlace_contract(&state); + dynamic_inflate_contract(&state); + rejection_contract(&state); + _zag_print("PNG decode contract: pass="); + let passed: []u8 = _zag_i64_to_str(state.passed as i64); + _zag_print(passed); _zag_str_free(passed); + _zag_print(" fail="); + let failed: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_println(failed); _zag_str_free(failed); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/png_decode_fuzz.zag b/tests/png_decode_fuzz.zag new file mode 100644 index 0000000..5a33f32 --- /dev/null +++ b/tests/png_decode_fuzz.zag @@ -0,0 +1,211 @@ +@import("../src/render/png_decode.zag") +@import("../src/render/png_encode.zag") + +struct FuzzState { passed: i32, failed: i32 } + +fn fuzz_expect(state: *FuzzState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn fuzz_next(random: *i64) i64 { + random.* = (random.* * 1103515245 + 12345) & 2147483647; + return random.*; +} + +fn fuzz_copy(bytes: []u8) ArrayList[u8] { + let copy: ArrayList[u8] = make[u8](bytes.len); + let i: i32 = 0; + while (i < bytes.len) { push[u8](©, bytes[i]); i = i + 1; } + return copy; +} + +fn fuzz_result_valid(result: PngDecodeResult) i32 { + if (result.error == PngDecodeError.none) { + if (result.width <= 0 || result.height <= 0 || + result.color_space != RenderColorSpace.srgb || + result.error_offset != 0 - 1 || + (result.profile_assumed != 0 && result.profile_assumed != 1)) { return 0; } + let expected: i64 = (result.width as i64) * + (result.height as i64) * 4; + return (expected > 0 && result.pixels.len as i64 == expected) as i32; + } + return (result.width == 0 && result.height == 0 && + result.color_space == RenderColorSpace.none && + result.pixels.len == 0 && result.profile_assumed == 0) as i32; +} + +fn fuzz_seed_surface() CpuSurface { + let surface: CpuSurface = cpu_surface_make(8, 8); + let i: i32 = 0; + while (i < surface.pixels.len) { + surface.pixels.data[i] = ((i * 73 + 29) & 255) as u8; + i = i + 1; + } + return surface; +} + +fn arbitrary_bytes_contract(state: *FuzzState) void { + let random: i64 = 881726454; + let valid: i32 = 1; + let case_index: i32 = 0; + while (case_index < 20000 && valid != 0) { + let length: i32 = (fuzz_next(&random) % 513) as i32; + let bytes: ArrayList[u8] = make[u8](length); + let i: i32 = 0; + while (i < length) { + push[u8](&bytes, (fuzz_next(&random) & 255) as u8); + i = i + 1; + } + let result: PngDecodeResult = png_decode(bytes.data[0..bytes.len]); + if (fuzz_result_valid(result) == 0) { valid = 0; } + png_decode_free(&result); free[u8](&bytes); + case_index = case_index + 1; + } + fuzz_expect(state, valid != 0 && case_index == 20000, + "20000 deterministic arbitrary byte streams remain bounded and owned"); +} + +fn mutation_contract(state: *FuzzState) void { + let surface: CpuSurface = fuzz_seed_surface(); + let encoded: PngEncodeResult = png_encode_surface(surface); + let random: i64 = 19349663; + let valid: i32 = 1; + let accepted: i32 = 0; + let case_index: i32 = 0; + while (case_index < 20000 && valid != 0) { + let bytes: ArrayList[u8] = fuzz_copy( + encoded.bytes.data[0..encoded.bytes.len] + ); + let changes: i32 = 1 + (fuzz_next(&random) % 4) as i32; + let change: i32 = 0; + while (change < changes) { + let index: i32 = (fuzz_next(&random) % bytes.len) as i32; + let bit: i32 = (fuzz_next(&random) % 8) as i32; + bytes.data[index] = bytes.data[index] ^ (1 << bit) as u8; + change = change + 1; + } + if (case_index % 5 == 0) { + bytes.len = (fuzz_next(&random) % bytes.len) as i32; + } + let result: PngDecodeResult = png_decode(bytes.data[0..bytes.len]); + if (fuzz_result_valid(result) == 0) { valid = 0; } + if (result.error == PngDecodeError.none) { accepted = accepted + 1; } + png_decode_free(&result); free[u8](&bytes); + case_index = case_index + 1; + } + fuzz_expect(state, valid != 0 && case_index == 20000 && accepted < 100, + "20000 structured PNG mutations fail closed or retain canonical output"); + png_encode_free(&encoded); cpu_surface_free(&surface); +} + +fn icc_profile_contract(state: *FuzzState) void { + let random: i64 = 297121507; + let valid: i32 = 1; + let case_index: i32 = 0; + while (case_index < 20000 && valid != 0) { + let length: i32 = (fuzz_next(&random) % 1025) as i32; + let bytes: ArrayList[u8] = make[u8](length); + let i: i32 = 0; + while (i < length) { + push[u8](&bytes, (fuzz_next(&random) & 255) as u8); + i = i + 1; + } + let profile: PngIccProfileResult = png_icc_matrix_profile( + bytes.data[0..bytes.len] + ); + if ((profile.valid != 0 && profile.valid != 1) || + (profile.lut.valid != 0 && profile.lut.valid != 1) || + (profile.valid != 0 && (profile.transform.valid == 0 || + profile.transform.active == 0 || + (profile.lut.valid == 0 && ( + profile.red_curve.valid == 0 || + profile.green_curve.valid == 0 || + profile.blue_curve.valid == 0 || + profile.red_curve.kind < 1 || profile.red_curve.kind > 6 || + profile.green_curve.kind < 1 || + profile.green_curve.kind > 6 || + profile.blue_curve.kind < 1 || + profile.blue_curve.kind > 6)) || + (profile.lut.valid != 0 && ( + profile.lut.red_grid_points < 2 || + profile.lut.red_grid_points > 33 || + profile.lut.green_grid_points < 2 || + profile.lut.green_grid_points > 33 || + profile.lut.blue_grid_points < 2 || + profile.lut.blue_grid_points > 33 || + profile.lut.input_entries < 2 || + profile.lut.input_entries > 4096 || + profile.lut.output_entries < 2 || + profile.lut.output_entries > 4096 || + profile.lut.input_tables.len != + profile.lut.input_entries * 3 || + profile.lut.clut.len != profile.lut.red_grid_points * + profile.lut.green_grid_points * + profile.lut.blue_grid_points * 3 || + profile.lut.output_tables.len != + profile.lut.output_entries * 3))))) { + valid = 0; + } + png_icc_profile_free(&profile); + free[u8](&bytes); case_index = case_index + 1; + } + fuzz_expect(state, valid != 0 && case_index == 20000, + "20000 arbitrary ICC profiles remain bounded and fail closed"); +} + +fn prefix_and_repeat_contract(state: *FuzzState) void { + let surface: CpuSurface = fuzz_seed_surface(); + let encoded: PngEncodeResult = png_encode_surface(surface); + let valid: i32 = 1; + let length: i32 = 0; + while (length < encoded.bytes.len && valid != 0) { + let result: PngDecodeResult = png_decode(encoded.bytes.data[0..length]); + if (result.error == PngDecodeError.none || fuzz_result_valid(result) == 0) { + valid = 0; + } + png_decode_free(&result); length = length + 1; + } + fuzz_expect(state, valid != 0, + "every strict canonical prefix preserves failure-result ownership invariants"); + + let expected_crc: i64 = checksum_crc32( + surface.pixels.data[0..surface.pixels.len] + ); + let repeat: i32 = 0; + while (repeat < 4096 && valid != 0) { + let result: PngDecodeResult = png_decode( + encoded.bytes.data[0..encoded.bytes.len] + ); + if (fuzz_result_valid(result) == 0 || + result.error != PngDecodeError.none || + checksum_crc32(result.pixels.data[0..result.pixels.len]) != expected_crc) { + valid = 0; + } + png_decode_free(&result); repeat = repeat + 1; + } + fuzz_expect(state, valid != 0 && repeat == 4096, + "4096 decode and cleanup cycles preserve exact pixels without state leakage"); + png_encode_free(&encoded); cpu_surface_free(&surface); +} + +fn main() i32 { + let state: FuzzState = FuzzState{ .passed = 0, .failed = 0 }; + arbitrary_bytes_contract(&state); + mutation_contract(&state); + icc_profile_contract(&state); + prefix_and_repeat_contract(&state); + _zag_print("PNG decode fuzz: pass="); + let passed: []u8 = _zag_i64_to_str(state.passed as i64); + _zag_print(passed); _zag_str_free(passed); + _zag_print(" fail="); + let failed: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_println(failed); _zag_str_free(failed); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/png_encode_contract.zag b/tests/png_encode_contract.zag new file mode 100644 index 0000000..b40b3a0 --- /dev/null +++ b/tests/png_encode_contract.zag @@ -0,0 +1,149 @@ +@import("../src/render/png_encode.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn read_u32_be(bytes: ArrayList[u8], offset: i32) i64 { + return ((bytes.data[offset] as i64) << 24) | + ((bytes.data[offset + 1] as i64) << 16) | + ((bytes.data[offset + 2] as i64) << 8) | + (bytes.data[offset + 3] as i64); +} + +fn bytes_equal(a: ArrayList[u8], b: ArrayList[u8]) i32 { + if (a.len != b.len) { return 0; } + let i: i32 = 0; + while (i < a.len) { + if (a.data[i] != b.data[i]) { return 0; } + i = i + 1; + } + return 1; +} + +fn chunk_crc_valid(bytes: ArrayList[u8], offset: i32) i32 { + let length: i32 = read_u32_be(bytes, offset) as i32; + if (length < 0 || offset + 12 + length > bytes.len) { return 0; } + let crc_input: ArrayList[u8] = make[u8](4 + length); + let i: i32 = 0; + while (i < 4 + length) { + push[u8](&crc_input, bytes.data[offset + 4 + i]); + i = i + 1; + } + let expected: i64 = checksum_crc32(crc_input.data[0..crc_input.len]); + free[u8](&crc_input); + return (expected == read_u32_be(bytes, offset + 8 + length)) as i32; +} + +fn signature_and_chunks_contract(state: *TestState) void { + let surface: CpuSurface = cpu_surface_make(1, 1); + surface.pixels.data[0] = 17; + surface.pixels.data[1] = 34; + surface.pixels.data[2] = 51; + surface.pixels.data[3] = 68; + let png: PngEncodeResult = png_encode_surface(surface); + expect(state, png.error == PngEncodeError.none && png.bytes.len == 86, + "one RGBA8 pixel produces the exact canonical PNG size"); + expect(state, png.bytes.data[0] == 137 && png.bytes.data[1] == 80 && + png.bytes.data[2] == 78 && png.bytes.data[3] == 71 && + png.bytes.data[4] == 13 && png.bytes.data[5] == 10 && + png.bytes.data[6] == 26 && png.bytes.data[7] == 10, + "snapshot bytes begin with the PNG signature"); + + let ihdr: i32 = 8; + let srgb: i32 = ihdr + 25; + let idat: i32 = srgb + 13; + let idat_length: i32 = read_u32_be(png.bytes, idat) as i32; + let iend: i32 = idat + 12 + idat_length; + expect(state, read_u32_be(png.bytes, ihdr) == 13 && + png.bytes.data[ihdr + 4] == 73 && png.bytes.data[ihdr + 5] == 72 && + png.bytes.data[ihdr + 6] == 68 && png.bytes.data[ihdr + 7] == 82 && + read_u32_be(png.bytes, ihdr + 8) == 1 && + read_u32_be(png.bytes, ihdr + 12) == 1 && + png.bytes.data[ihdr + 16] == 8 && png.bytes.data[ihdr + 17] == 6, + "IHDR declares one by one noninterlaced RGBA8 pixels"); + expect(state, png.bytes.data[srgb + 4] == 115 && + png.bytes.data[srgb + 5] == 82 && png.bytes.data[srgb + 6] == 71 && + png.bytes.data[srgb + 7] == 66 && png.bytes.data[srgb + 8] == 0, + "snapshot color intent is explicitly sRGB perceptual"); + expect(state, chunk_crc_valid(png.bytes, ihdr) != 0 && + chunk_crc_valid(png.bytes, srgb) != 0 && + chunk_crc_valid(png.bytes, idat) != 0 && + chunk_crc_valid(png.bytes, iend) != 0, + "every PNG chunk carries a valid IEEE CRC32"); + expect(state, png.bytes.data[iend + 4] == 73 && + png.bytes.data[iend + 5] == 69 && png.bytes.data[iend + 6] == 78 && + png.bytes.data[iend + 7] == 68 && iend + 12 == png.bytes.len, + "IEND terminates the file with no trailing bytes"); + + let decoded: ZlibStoreDecodeResult = zlib_store_decode( + png.bytes.data[idat + 8..idat + 8 + idat_length] + ); + expect(state, decoded.error == ZlibStoreError.none && decoded.bytes.len == 5 && + decoded.bytes.data[0] == 0 && decoded.bytes.data[1] == 17 && + decoded.bytes.data[2] == 34 && decoded.bytes.data[3] == 51 && + decoded.bytes.data[4] == 68, + "IDAT decodes to filter zero followed by exact RGBA bytes"); + zlib_store_decode_free(&decoded); + png_encode_free(&png); + cpu_surface_free(&surface); +} + +fn determinism_contract(state: *TestState) void { + let surface: CpuSurface = cpu_surface_make(3, 2); + let i: i32 = 0; + while (i < surface.pixels.len) { + surface.pixels.data[i] = ((i * 29 + 7) % 256) as u8; + i = i + 1; + } + let first: PngEncodeResult = png_encode_surface(surface); + let second: PngEncodeResult = png_encode_surface(surface); + expect(state, first.error == PngEncodeError.none && + second.error == PngEncodeError.none && bytes_equal(first.bytes, second.bytes) != 0, + "identical CPU surfaces produce byte-identical PNG snapshots"); + png_encode_free(&second); + png_encode_free(&first); + cpu_surface_free(&surface); +} + +fn rejection_contract(state: *TestState) void { + let invalid: CpuSurface = cpu_surface_make(0, 1); + let invalid_result: PngEncodeResult = png_encode_surface(invalid); + expect(state, invalid_result.error == PngEncodeError.invalid_surface && + invalid_result.bytes.len == 0, + "invalid surfaces cannot become screenshot evidence"); + png_encode_free(&invalid_result); + cpu_surface_free(&invalid); + + let mutated: CpuSurface = cpu_surface_make(1, 1); + mutated.pixels.len = 3; + let mutated_result: PngEncodeResult = png_encode_surface(mutated); + expect(state, mutated_result.error == PngEncodeError.invalid_surface, + "surface byte-count mutation fails before PNG encoding"); + png_encode_free(&mutated_result); + mutated.pixels.len = 4; + cpu_surface_free(&mutated); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + signature_and_chunks_contract(&state); + determinism_contract(&state); + rejection_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("PNG encode contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/render_resources_contract.zag b/tests/render_resources_contract.zag new file mode 100644 index 0000000..11b3b57 --- /dev/null +++ b/tests/render_resources_contract.zag @@ -0,0 +1,142 @@ +@import("../src/render/resources.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn ownership_contract(state: *TestState) void { + let store: RenderResourceStore = render_resource_store_make(8, 128, 64); + let path: RenderResourceSpec = render_resource_spec(20, RenderResourceKind.path, "PATH-A", 1); + let image: RenderResourceSpec = render_resource_spec(10, RenderResourceKind.image, "RGBA", 2); + image.width = 1; image.height = 1; image.color_space = RenderColorSpace.srgb; + expect(state, render_resource_add(&store, path) == RenderResourceError.none && + render_resource_add(&store, image) == RenderResourceError.none, + "resource store accepts valid typed payloads with explicit metadata"); + expect(state, store.resources.len == 2 && store.resources.data[0].id == 10 && + store.resources.data[1].id == 20 && store.total_bytes == 10, + "owned resources are kept in canonical stable-ID order with exact byte accounting"); + expect(state, store.resources.data[1].payload.len == 6 && + store.resources.data[1].payload.data[5] == 65, + "resource payload bytes are copied into store ownership"); + expect(state, render_resource_find_index(store, 10) == 0 && + render_resource_find_index(store, 20) == 1 && + render_resource_find_index(store, 99) < 0, + "binary lookup resolves exact IDs without fabricating missing resources"); + + let before_hash: i64 = store.content_hash; + let replacement: RenderResourceSpec = render_resource_spec(20, RenderResourceKind.path, "PATH-BB", 1); + replacement.revision = 1; + expect(state, render_resource_replace(&store, replacement) == RenderResourceError.none && + store.resources.data[1].revision == 1 && store.total_bytes == 11 && + store.content_hash != before_hash, + "revisioned replacement preserves identity and updates owned bytes and content truth"); + let stale: RenderResourceSpec = replacement; stale.revision = 1; + expect(state, render_resource_replace(&store, stale) == RenderResourceError.revision_conflict && + store.resources.data[1].payload.len == 7, + "stale replacement fails before payload mutation"); + let wrong_kind: RenderResourceSpec = render_resource_spec(20, RenderResourceKind.svg_source, "", 3); + wrong_kind.revision = 2; + expect(state, render_resource_replace(&store, wrong_kind) == RenderResourceError.kind_mismatch, + "resource identity cannot silently change kind during replacement"); + + expect(state, render_resource_store_seal(&store) == RenderResourceError.none && + render_resource_store_verify(store) == RenderResourceError.none, + "sealed resource stores verify complete owned content identity"); + expect(state, render_resource_add(&store, render_resource_spec(30, + RenderResourceKind.effect, "FX", 1)) == RenderResourceError.sealed, + "sealed resource stores reject further mutation"); + store.resources.data[0].payload.data[0] = store.resources.data[0].payload.data[0] + 1; + expect(state, render_resource_store_verify(store) == RenderResourceError.content_mutated, + "out-of-contract payload mutation is detected before rendering"); + render_resource_store_free(&store); +} + +fn canonical_contract(state: *TestState) void { + let forward: RenderResourceStore = render_resource_store_make(4, 64, 32); + let reverse: RenderResourceStore = render_resource_store_make(4, 64, 32); + let one: RenderResourceSpec = render_resource_spec(1, RenderResourceKind.svg_source, "", 10); + let two: RenderResourceSpec = render_resource_spec(2, RenderResourceKind.png_source, "PNG", 11); + _ = render_resource_add(&forward, one); _ = render_resource_add(&forward, two); + _ = render_resource_add(&reverse, two); _ = render_resource_add(&reverse, one); + expect(state, forward.content_hash == reverse.content_hash && + forward.resources.data[0].id == reverse.resources.data[0].id, + "insertion order cannot change canonical resource identity"); + _ = render_resource_store_seal(&forward); _ = render_resource_store_seal(&reverse); + expect(state, render_resource_store_verify(forward) == RenderResourceError.none && + render_resource_store_verify(reverse) == RenderResourceError.none, + "canonical stores remain independently verifiable after sealing"); + render_resource_store_free(&reverse); render_resource_store_free(&forward); +} + +fn failure_contract(state: *TestState) void { + let invalid: RenderResourceStore = render_resource_store_make(0, 8, 8); + expect(state, render_resource_add(&invalid, render_resource_spec(1, + RenderResourceKind.path, "A", 1)) == RenderResourceError.invalid_configuration, + "invalid allocation policy fails before payload ownership"); + render_resource_store_free(&invalid); + + let store: RenderResourceStore = render_resource_store_make(2, 7, 4); + expect(state, render_resource_add(&store, render_resource_spec(0, + RenderResourceKind.path, "A", 1)) == RenderResourceError.invalid_id, + "nonpositive resource IDs fail visibly"); + expect(state, render_resource_add(&store, render_resource_spec(1, + RenderResourceKind.path, "", 1)) == RenderResourceError.invalid_payload, + "empty resource payloads cannot claim an asset"); + let late_revision: RenderResourceSpec = render_resource_spec(1, + RenderResourceKind.path, "A", 1); + late_revision.revision = 4; + expect(state, render_resource_add(&store, late_revision) == RenderResourceError.revision_conflict, + "new resource IDs must begin at canonical revision zero"); + let bad_image: RenderResourceSpec = render_resource_spec(1, + RenderResourceKind.image, "RGBA", 2); + expect(state, render_resource_add(&store, bad_image) == RenderResourceError.invalid_metadata, + "decoded images require dimensions and color-space truth"); + expect(state, render_resource_add(&store, render_resource_spec(1, + RenderResourceKind.path, "12345", 1)) == RenderResourceError.payload_limit, + "per-resource byte ceilings fail before copying payloads"); + _ = render_resource_add(&store, render_resource_spec(1, + RenderResourceKind.path, "1234", 1)); + expect(state, render_resource_add(&store, render_resource_spec(1, + RenderResourceKind.path, "12", 1)) == RenderResourceError.duplicate_id, + "duplicate stable resource IDs fail before ownership changes"); + expect(state, render_resource_add(&store, render_resource_spec(2, + RenderResourceKind.effect, "1234", 1)) == RenderResourceError.total_limit && + store.resources.len == 1 && store.total_bytes == 4, + "total byte ceilings are transactional and preserve prior resources"); + expect(state, render_resource_replace(&store, render_resource_spec(9, + RenderResourceKind.path, "A", 1)) == RenderResourceError.missing_resource, + "replacement of a missing ID fails without becoming insertion"); + expect(state, render_resource_store_verify(store) == RenderResourceError.not_sealed, + "unsealed stores cannot masquerade as immutable render evidence"); + render_resource_store_free(&store); + + let count_limited: RenderResourceStore = render_resource_store_make(1, 8, 4); + _ = render_resource_add(&count_limited, render_resource_spec(1, + RenderResourceKind.path, "A", 1)); + expect(state, render_resource_add(&count_limited, render_resource_spec(2, + RenderResourceKind.effect, "B", 1)) == RenderResourceError.too_many_resources, + "resource-count ceilings fail before allocating a second payload"); + render_resource_store_free(&count_limited); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + ownership_contract(&state); + canonical_contract(&state); + failure_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Render resources contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/replay_contract.zag b/tests/replay_contract.zag new file mode 100644 index 0000000..2b5a5af --- /dev/null +++ b/tests/replay_contract.zag @@ -0,0 +1,190 @@ +@import("../src/replay/replay.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn complete_tape(final_value: i64, pointer_x: i64) ReplayTape { + let tape: ReplayTape = replay_tape_make(); + _ = replay_tape_push(&tape, replay_backend_activate_event(7)); + _ = replay_tape_push(&tape, replay_clock_event(0)); + _ = replay_tape_push(&tape, replay_state_event(1, 0, 1)); + _ = replay_tape_push(&tape, replay_pointer_event(9, PointerPhase.down, pointer_x, unit_scale())); + _ = replay_tape_push(&tape, replay_pointer_event(9, PointerPhase.move, pointer_x + unit_scale(), unit_scale())); + _ = replay_tape_push(&tape, replay_pointer_event(9, PointerPhase.up, pointer_x + unit_scale(), unit_scale())); + _ = replay_tape_push(&tape, replay_clock_event(10000)); + _ = replay_tape_push(&tape, replay_state_event(1, 1, final_value)); + _ = replay_tape_push(&tape, replay_backend_loss_event(7)); + _ = replay_tape_push(&tape, replay_backend_recover_event(7)); + _ = replay_tape_push(&tape, replay_clock_event(20000)); + _ = replay_tape_seal(&tape); + return tape; +} + +fn results_equal(a: ReplayResult, b: ReplayResult) i32 { + return (a.error == b.error && a.error_event == b.error_event && + a.events_applied == b.events_applied && a.state_value == b.state_value && + a.state_revision == b.state_revision && a.clock == b.clock && + a.backend_id == b.backend_id && a.backend_generation == b.backend_generation && + a.backend_state == b.backend_state && a.input_hash == b.input_hash && + a.motion_hash == b.motion_hash && + a.motion_value == b.motion_value && a.motion_velocity == b.motion_velocity && + a.motion_status == b.motion_status && + a.semantics_hash == b.semantics_hash && a.measure_hash == b.measure_hash && + a.layout_hash == b.layout_hash && + a.layout_lines == b.layout_lines && + a.display_hash == b.display_hash && a.cpu_hash == b.cpu_hash && + a.output_hash == b.output_hash) as i32; +} + +fn deterministic_contract(state: *TestState) void { + let tape: ReplayTape = complete_tape(2, unit_scale()); + let first: ReplayResult = replay_execute(tape); + let second: ReplayResult = replay_execute(tape); + expect(state, replay_tape_verify(tape) == ReplayError.none && tape.events.len == 11, + "sealed tape preserves one canonical ordered event stream"); + expect(state, first.error == ReplayError.none && first.events_applied == 11, + "complete tape applies every event without hidden skips"); + expect(state, first.state_value == 2 && first.state_revision == 2 && first.clock == 20000, + "state revisions and monotonic clock replay exactly"); + expect(state, first.backend_id == 7 && first.backend_generation == 2 && + first.backend_state == ReplayBackendState.active, + "backend activation loss and recovery remain observable in final truth"); + expect(state, first.input_hash != 0 && first.motion_hash != 0 && first.semantics_hash != 0 && + first.measure_hash != 0 && first.layout_hash != 0 && + first.display_hash != 0 && first.cpu_hash != 0 && first.output_hash != 0, + "replay regenerates input motion semantics measurement layout display and CPU identities"); + expect(state, first.motion_value > 0 && first.motion_velocity > 0 && + first.motion_status == MotionStatus.running, + "replayed clock samples advance interruptible motion state"); + expect(state, first.layout_lines == 2, + "replayed state regenerates the expected wrapped Flex line structure"); + expect(state, results_equal(first, second) == 1, + "two executions of the same sealed tape are bit-identical"); + replay_tape_free(&tape); +} + +fn mutation_contract(state: *TestState) void { + let tape: ReplayTape = complete_tape(2, unit_scale()); + tape.events.data[2].c = 99; + let mutated: ReplayResult = replay_execute(tape); + expect(state, replay_tape_verify(tape) == ReplayError.content_mutated && + mutated.error == ReplayError.content_mutated && mutated.events_applied == 0, + "sealed tape mutation fails before any event is applied"); + replay_tape_free(&tape); + + let open: ReplayTape = replay_tape_make(); + _ = replay_tape_push(&open, replay_clock_event(1)); + expect(state, replay_execute(open).error == ReplayError.not_sealed, + "unsealed tapes cannot masquerade as replay evidence"); + _ = replay_tape_seal(&open); + expect(state, replay_tape_push(&open, replay_clock_event(2)) == ReplayError.sealed && open.events.len == 1, + "sealed tapes reject appended events without mutation"); + replay_tape_free(&open); + + let invalid: ReplayTape = replay_tape_make(); + expect(state, replay_tape_push(&invalid, replay_clock_event(0 - 1)) == ReplayError.invalid_event && + invalid.events.len == 0 && invalid.revision == 0, + "invalid events fail before tape revision or hash changes"); + let noncanonical: ReplayEvent = replay_clock_event(1); + noncanonical.b = 1; + expect(state, replay_tape_push(&invalid, noncanonical) == ReplayError.invalid_event && invalid.events.len == 0, + "unused event fields must stay zero for one canonical tape representation"); + replay_tape_free(&invalid); +} + +fn state_and_clock_failure_contract(state: *TestState) void { + let stale: ReplayTape = replay_tape_make(); + _ = replay_tape_push(&stale, replay_clock_event(1)); + _ = replay_tape_push(&stale, replay_state_event(1, 4, 3)); + _ = replay_tape_seal(&stale); + let stale_result: ReplayResult = replay_execute(stale); + expect(state, stale_result.error == ReplayError.stale_state_revision && + stale_result.error_event == 1 && stale_result.events_applied == 1, + "stale state revisions fail at the exact ordered event"); + replay_tape_free(&stale); + + let regressed: ReplayTape = replay_tape_make(); + _ = replay_tape_push(®ressed, replay_clock_event(10)); + _ = replay_tape_push(®ressed, replay_clock_event(9)); + _ = replay_tape_seal(®ressed); + let clock_result: ReplayResult = replay_execute(regressed); + expect(state, clock_result.error == ReplayError.clock_regression && + clock_result.error_event == 1 && clock_result.events_applied == 1, + "clock regression fails at the exact ordered event"); + replay_tape_free(®ressed); +} + +fn backend_failure_contract(state: *TestState) void { + let loss: ReplayTape = replay_tape_make(); + _ = replay_tape_push(&loss, replay_backend_loss_event(7)); + _ = replay_tape_seal(&loss); + expect(state, replay_execute(loss).error == ReplayError.backend_transition, + "device loss without an active backend fails closed"); + replay_tape_free(&loss); + + let recover: ReplayTape = replay_tape_make(); + _ = replay_tape_push(&recover, replay_backend_activate_event(7)); + _ = replay_tape_push(&recover, replay_backend_recover_event(7)); + _ = replay_tape_seal(&recover); + let recover_result: ReplayResult = replay_execute(recover); + expect(state, recover_result.error == ReplayError.backend_transition && recover_result.error_event == 1, + "recovery without a recorded loss fails closed"); + replay_tape_free(&recover); + + let wrong: ReplayTape = replay_tape_make(); + _ = replay_tape_push(&wrong, replay_backend_activate_event(7)); + _ = replay_tape_push(&wrong, replay_backend_loss_event(8)); + _ = replay_tape_seal(&wrong); + expect(state, replay_execute(wrong).error == ReplayError.backend_transition, + "backend identity cannot silently change during loss"); + replay_tape_free(&wrong); +} + +fn differential_contract(state: *TestState) void { + let baseline: ReplayTape = complete_tape(2, unit_scale()); + let changed_state: ReplayTape = complete_tape(3, unit_scale()); + let changed_pointer: ReplayTape = complete_tape(2, 6 * unit_scale()); + let a: ReplayResult = replay_execute(baseline); + let b: ReplayResult = replay_execute(changed_state); + let c: ReplayResult = replay_execute(changed_pointer); + expect(state, a.output_hash != b.output_hash && a.motion_hash != b.motion_hash && + a.semantics_hash != b.semantics_hash && a.measure_hash != b.measure_hash && + a.layout_hash != b.layout_hash && a.display_hash != b.display_hash && a.cpu_hash != b.cpu_hash, + "state changes propagate through motion semantics measurement layout display and CPU output"); + expect(state, a.layout_lines != b.layout_lines, + "state replay crosses Flex wrapping boundaries deterministically"); + expect(state, a.input_hash != c.input_hash && a.motion_hash == c.motion_hash && + a.semantics_hash == c.semantics_hash && a.measure_hash == c.measure_hash && + a.layout_hash == c.layout_hash && a.display_hash == c.display_hash && a.cpu_hash == c.cpu_hash && + a.output_hash != c.output_hash, + "pointer differences change input identity without contaminating scene generation"); + replay_tape_free(&baseline); + replay_tape_free(&changed_state); + replay_tape_free(&changed_pointer); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + deterministic_contract(&state); + mutation_contract(&state); + state_and_clock_failure_contract(&state); + backend_failure_contract(&state); + differential_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Replay contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/scroll_virtual_contract.zag b/tests/scroll_virtual_contract.zag new file mode 100644 index 0000000..e787145 --- /dev/null +++ b/tests/scroll_virtual_contract.zag @@ -0,0 +1,243 @@ +@import("std:list") +@import("../src/layout/scroll.zag") +@import("../src/layout/virtual_list.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn scroll_contract(state: *TestState) void { + let value: ScrollState = scroll_state(size(100, 80), size(300, 200)); + let first: ScrollMutation = scroll_by(&value, 50, 30); + expect(state, first.error == ScrollError.none && first.consumed_x == 50 && + first.consumed_y == 30 && first.unconsumed_x == 0 && first.unconsumed_y == 0 && + value.offset_x == 50 && value.offset_y == 30 && value.revision == 1, + "scroll consumes exact in-bounds deltas and advances one revision"); + let after_first_hash: i64 = scroll_state_hash(value); + let edge: ScrollMutation = scroll_by(&value, 500, 500); + expect(state, edge.offset_x == 200 && edge.offset_y == 120 && + edge.consumed_x == 150 && edge.consumed_y == 90 && + edge.unconsumed_x == 350 && edge.unconsumed_y == 410 && value.revision == 2, + "scroll exposes unconsumed boundary deltas for nested gesture handoff"); + let stopped: ScrollMutation = scroll_by(&value, 10, 10); + expect(state, stopped.changed == 0 && stopped.consumed_x == 0 && stopped.consumed_y == 0 && + stopped.unconsumed_x == 10 && stopped.unconsumed_y == 10 && value.revision == 2, + "boundary pressure performs no phantom state revision"); + let repeated: ScrollState = scroll_state(size(100, 80), size(300, 200)); + let repeated_result: ScrollMutation = scroll_by(&repeated, 50, 30); + expect(state, repeated_result.mutation_hash == first.mutation_hash && + scroll_state_hash(repeated) == after_first_hash, + "identical scroll inputs produce identical mutation and retained-state identity"); +} + +fn reveal_anchor_and_rtl_contract(state: *TestState) void { + let value: ScrollState = scroll_state(size(100, 80), size(300, 200)); + let reveal: ScrollMutation = scroll_reveal( + &value, rect(150, 90, 20, 10), ScrollAlignment.nearest, ScrollAlignment.nearest + ); + expect(state, reveal.error == ScrollError.none && value.offset_x == 70 && value.offset_y == 20, + "nearest reveal moves only enough to expose the complete target"); + let centered: ScrollMutation = scroll_reveal( + &value, rect(200, 100, 20, 20), ScrollAlignment.center, ScrollAlignment.center + ); + expect(state, centered.offset_x == 160 && centered.offset_y == 70, + "center reveal resolves both axes against the authoritative viewport"); + let logical: ScrollMutation = scroll_to(&value, 20, 40); + let ltr: Rect = scroll_visible_rect(value, LayoutDirection.ltr); + let rtl: Rect = scroll_visible_rect(value, LayoutDirection.rtl); + expect(state, logical.error == ScrollError.none && ltr.x == 20 && rtl.x == 180 && + ltr.y == rtl.y && ltr.width == 100 && ltr.height == 80, + "logical horizontal scroll start mirrors to the physical RTL content edge"); + + let anchor: ScrollAnchor = scroll_anchor(40, 160, 20, 20); + let reconciled: ScrollMutation = scroll_reconcile(&value, size(120, 90), size(360, 260), anchor); + expect(state, reconciled.error == ScrollError.none && value.offset_x == 20 && value.offset_y == 140 && + value.viewport.width == 120 && value.content.height == 260 && reconciled.changed == 1, + "content reconciliation preserves an explicit anchor viewport position"); + let before_hash: i64 = scroll_state_hash(value); + let same: ScrollMutation = scroll_reconcile(&value, value.viewport, value.content, + scroll_anchor(value.offset_x, value.offset_y, 0, 0)); + expect(state, same.changed == 0 && same.revision == value.revision && + scroll_state_hash(value) == before_hash, + "identical anchored reconciliation stays completely idle"); + + let malformed: ScrollState = scroll_state(size(0 - 1, 20), size(30, 30)); + let rejected: ScrollMutation = scroll_by(&malformed, 1, 1); + expect(state, rejected.error == ScrollError.invalid_viewport && rejected.changed == 0, + "malformed scroll state fails before mutation"); + let bad_target: ScrollMutation = scroll_reveal(&value, rect(0 - 1, 0, 2, 2), + ScrollAlignment.nearest, ScrollAlignment.nearest); + expect(state, bad_target.error == ScrollError.invalid_target, + "malformed reveal geometry fails closed"); +} + +fn million_row_contract(state: *TestState) void { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let model: VirtualListModel = virtual_model(1000000, 1000, 40, 2, 7, overrides); + let style: VirtualListStyle = virtual_style(VirtualAxis.vertical, + Insets{ .top = 10, .right = 5, .bottom = 10, .left = 5 }); + let target_offset: i64 = virtual_item_offset(model, style, 500000) + 5; + let result: VirtualListResult = virtual_list_layout(model, size(300, 200), target_offset, style); + expect(state, result.error == VirtualListError.none && result.semantic_count == 1000000 && + result.first_visible == 500000 && result.last_visible == 500004, + "million-row layout finds the exact semantic visible range without a linear scan"); + expect(state, result.first_live == 499998 && result.last_live == 500006 && + result.placements.len == 9 && result.examined_items < 64, + "million-row virtualization keeps live nodes and examined work independently bounded"); + expect(state, result.placements.data[2].id == 501000 && + result.placements.data[2].index == 500000 && result.placements.data[2].frame.y == 0 - 5 && + result.placements.data[2].frame.x == 5 && result.placements.data[2].frame.width == 290, + "virtual placement retains stable IDs and exact viewport-local geometry"); + let repeated: VirtualListResult = virtual_list_layout(model, size(300, 200), target_offset, style); + expect(state, repeated.layout_hash == result.layout_hash && result.layout_hash != 0, + "identical million-row inputs produce bit-identical virtual layout identity"); + virtual_free(&repeated); virtual_free(&result); free[VirtualExtentOverride](&overrides); +} + +fn variable_anchor_contract(state: *TestState) void { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](2); + push[VirtualExtentOverride](&overrides, virtual_override(2, 30, 1)); + push[VirtualExtentOverride](&overrides, virtual_override(5, 5, 1)); + let model: VirtualListModel = virtual_model(100, 10, 10, 1, 1, overrides); + let style: VirtualListStyle = virtual_style(VirtualAxis.vertical, insets_all(0)); + let old_scroll: i64 = virtual_item_offset(model, style, 5); + let first: VirtualListResult = virtual_list_layout(model, size(100, 25), old_scroll, style); + let anchor: VirtualAnchor = virtual_anchor_at(model, style, 5, old_scroll); + expect(state, first.error == VirtualListError.none && first.first_visible == 5 && + first.placements.data[(first.first_visible - first.first_live) as i32].frame.height == 5 && + anchor.id == 15 && anchor.viewport_offset == 0, + "sparse variable extents preserve stable item identity and measured height revisions"); + overrides.data[0].extent = 50; overrides.data[0].revision = 2; + let changed: VirtualListModel = virtual_model(100, 10, 10, 1, 2, overrides); + let resolution: VirtualAnchorResolution = virtual_resolve_anchor( + changed, style, anchor, 25, old_scroll + ); + let second: VirtualListResult = virtual_list_layout( + changed, size(100, 25), resolution.scroll_offset, style + ); + expect(state, resolution.error == VirtualListError.none && resolution.changed == 1 && + resolution.scroll_offset == old_scroll + 20 && second.first_visible == 5 && + second.placements.data[(5 - second.first_live) as i32].frame.y == 0, + "anchor reconciliation absorbs size changes above the viewport without visible jumps"); + expect(state, second.layout_hash != first.layout_hash, + "extent revisions and changed geometry alter deterministic virtual identity"); + virtual_free(&second); virtual_free(&first); free[VirtualExtentOverride](&overrides); +} + +fn rtl_empty_and_failure_contract(state: *TestState) void { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let model: VirtualListModel = virtual_model(3, 1, 10, 2, 0, overrides); + let ltr_style: VirtualListStyle = virtual_style(VirtualAxis.horizontal, insets_all(1)); + let ltr: VirtualListResult = virtual_list_layout(model, size(30, 20), 0, ltr_style); + let rtl_style: VirtualListStyle = ltr_style; rtl_style.direction = LayoutDirection.rtl; + let rtl: VirtualListResult = virtual_list_layout(model, size(30, 20), 0, rtl_style); + expect(state, ltr.error == VirtualListError.none && rtl.error == VirtualListError.none && + ltr.placements.data[0].frame.x == 1 && rtl.placements.data[0].frame.x == 19 && + rtl.placements.data[1].frame.x == 7 && rtl.layout_hash != ltr.layout_hash, + "horizontal virtualization mirrors logical item order under RTL"); + virtual_free(&rtl); virtual_free(<r); + + let empty_model: VirtualListModel = virtual_model(0, 1, 10, 0, 0, overrides); + let empty: VirtualListResult = virtual_list_layout(empty_model, size(40, 30), 50, + virtual_style(VirtualAxis.vertical, insets_all(3))); + expect(state, empty.error == VirtualListError.none && empty.placements.len == 0 && + empty.content.height == 6 && empty.scroll_offset == 0 && empty.first_visible == 0 - 1, + "empty virtual collections retain padding and canonical empty semantic ranges"); + virtual_free(&empty); + + let padded_model: VirtualListModel = virtual_model(1, 1, 10, 0, 0, overrides); + let padded_style: VirtualListStyle = virtual_style(VirtualAxis.vertical, + Insets{ .top = 50, .right = 0, .bottom = 0, .left = 0 }); + let padding_only: VirtualListResult = virtual_list_layout( + padded_model, size(40, 30), 0, padded_style + ); + expect(state, padding_only.error == VirtualListError.none && + padding_only.semantic_count == 1 && padding_only.placements.len == 0 && + padding_only.first_visible == 0 - 1, + "a viewport containing only leading padding reports no phantom visible item"); + virtual_free(&padding_only); + + let dense_model: VirtualListModel = virtual_model(1000, 1, 1, 0, 0, overrides); + let dense_style: VirtualListStyle = virtual_style(VirtualAxis.vertical, insets_all(0)); + dense_style.max_live_items = 10; + let limited: VirtualListResult = virtual_list_layout(dense_model, size(30, 100), 0, dense_style); + expect(state, limited.error == VirtualListError.live_limit_exceeded && limited.error_index == 0, + "live-node ceilings fail before unbounded viewport allocation"); + virtual_free(&limited); + free[VirtualExtentOverride](&overrides); + + let malformed_overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](2); + push[VirtualExtentOverride](&malformed_overrides, virtual_override(5, 10, 0)); + push[VirtualExtentOverride](&malformed_overrides, virtual_override(4, 10, 0)); + let malformed_model: VirtualListModel = virtual_model(10, 1, 10, 0, 0, malformed_overrides); + let malformed: VirtualListResult = virtual_list_layout(malformed_model, size(30, 30), 0, + virtual_style(VirtualAxis.vertical, insets_all(0))); + expect(state, malformed.error == VirtualListError.invalid_override && malformed.error_index == 1, + "unsorted sparse extent overrides fail at the exact record"); + virtual_free(&malformed); free[VirtualExtentOverride](&malformed_overrides); + + let huge_overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let huge_model: VirtualListModel = virtual_model(1000000000, 1, + unbounded_extent() / 4, 0, 0, huge_overrides); + let huge: VirtualListResult = virtual_list_layout(huge_model, size(30, 30), 0, + virtual_style(VirtualAxis.vertical, insets_all(0))); + expect(state, huge.error == VirtualListError.arithmetic_overflow, + "billion-row content extent overflow fails before multiplication or allocation"); + virtual_free(&huge); free[VirtualExtentOverride](&huge_overrides); + + let scroll_overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let scroll_model: VirtualListModel = virtual_model(2, 1, 10, 0, 0, scroll_overrides); + let bad_scroll: VirtualListResult = virtual_list_layout(scroll_model, size(30, 30), + unbounded_extent() * 2 + 1, virtual_style(VirtualAxis.vertical, insets_all(0))); + expect(state, bad_scroll.error == VirtualListError.invalid_scroll, + "out-of-contract scroll coordinates fail with an exact typed reason"); + virtual_free(&bad_scroll); free[VirtualExtentOverride](&scroll_overrides); +} + +fn property_contract(state: *TestState) void { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let model: VirtualListModel = virtual_model(10000, 100, 8, 1, 3, overrides); + let style: VirtualListStyle = virtual_style(VirtualAxis.vertical, insets_all(1)); + let valid: i32 = 1; + let index: i64 = 0; + while (index < 1000) { + let offset: i64 = virtual_item_offset(model, style, index); + let result: VirtualListResult = virtual_list_layout(model, size(80, 64), offset, style); + if (result.error != VirtualListError.none || result.first_visible != index || + result.placements.len > 12 || result.examined_items > 48 || + result.placements.data[(index - result.first_live) as i32].id != 100 + index || + result.placements.data[(index - result.first_live) as i32].frame.y != 0) { + valid = 0; + } + virtual_free(&result); + index = index + 37; + } + expect(state, valid == 1, + "28-position virtual sweep preserves stable IDs exact anchors and logarithmic bounded work"); + free[VirtualExtentOverride](&overrides); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + scroll_contract(&state); + reveal_anchor_and_rtl_contract(&state); + million_row_contract(&state); + variable_anchor_contract(&state); + rtl_empty_and_failure_contract(&state); + property_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Scroll/virtual contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/semantics_contract.zag b/tests/semantics_contract.zag new file mode 100644 index 0000000..a8b6134 --- /dev/null +++ b/tests/semantics_contract.zag @@ -0,0 +1,215 @@ +@import("../src/semantics/semantics.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn tree_contract(state: *TestState) void { + let tree: SemanticsTree = semantics_tree_make(); + let root_id: NodeKey = node_key(1); + let root: SemanticsSpec = semantics_spec(root_id, semantic_root_key(), SemanticRole.window, "Document"); + expect(state, semantics_add(&tree, root) == SemanticsError.none, "a semantic root is accepted"); + + let save_id: NodeKey = node_key(2); + let save: SemanticsSpec = semantics_spec(save_id, root_id, SemanticRole.button, "Save"); + save.action_mask = semantic_actions(SemanticAction.activate); + save.action_mask = semantic_actions_add(save.action_mask, SemanticAction.focus); + save.focus_order = 2; + save.selected = 1; + save.live_region = LiveRegion.polite; + save.description = "Writes the current document"; + save.has_bounds = 1; + save.bounds = rect(8, 16, 96, 32); + expect(state, semantics_add(&tree, save) == SemanticsError.none, "a named actionable child is accepted"); + + let field_id: NodeKey = node_key(3); + let field: SemanticsSpec = semantics_spec(field_id, root_id, SemanticRole.text_field, "Part name"); + field.value = "Bracket"; + field.action_mask = semantic_actions_add(semantic_actions(SemanticAction.set_value), SemanticAction.focus); + field.focus_order = 1; + field.text_length = 7; + field.selection_start = 0; + field.selection_end = 7; + expect(state, semantics_add(&tree, field) == SemanticsError.none, "editable text navigation data is accepted"); + + let slider_id: NodeKey = node_key(4); + let slider: SemanticsSpec = semantics_spec(slider_id, root_id, SemanticRole.slider, "Zoom"); + slider.value = "100 percent"; + slider.action_mask = semantic_actions_add(semantic_actions(SemanticAction.increment), SemanticAction.decrement); + slider.has_range = 1; + slider.range_min = 25; + slider.range_max = 400; + slider.range_value = 100; + slider.range_step = 25; + slider.described_by = save_id; + expect(state, semantics_add(&tree, slider) == SemanticsError.none, "range semantics preserve value and step"); + + let save_index: i32 = semantics_find_index(tree, save_id); + let field_index: i32 = semantics_find_index(tree, field_id); + let slider_index: i32 = semantics_find_index(tree, slider_id); + expect(state, save_index >= 0 && semantic_text_equal(tree.nodes.data[save_index].name, "Save") == 1, "owned semantic names are queryable by stable ID"); + expect(state, semantic_text_equal(tree.nodes.data[save_index].description, "Writes the current document") == 1 && tree.nodes.data[save_index].bounds.width == 96, "owned descriptions and exact logical bounds are inspectable"); + expect(state, semantic_actions_has(tree.nodes.data[save_index].action_mask, SemanticAction.activate) == 1 && semantic_actions_has(tree.nodes.data[save_index].action_mask, SemanticAction.scroll) == 0, "semantic actions are explicit capabilities"); + expect(state, tree.nodes.data[save_index].selected == 1 && tree.nodes.data[save_index].live_region == LiveRegion.polite, "selection and live-region state remain explicit"); + expect(state, field_index >= 0 && semantic_text_equal(tree.nodes.data[field_index].value, "Bracket") == 1 && tree.nodes.data[field_index].selection_end == 7, "text value and selection remain inspectable"); + expect(state, slider_index >= 0 && tree.nodes.data[slider_index].range_value == 100 && tree.nodes.data[slider_index].range_step == 25, "range value remains inspectable"); + expect(state, node_key_equal(tree.nodes.data[slider_index].described_by, save_id) == 1, "semantic relationships retain their stable target ID"); + expect(state, node_key_equal(semantics_next_focus(tree, 0), field_id) == 1 && node_key_equal(semantics_next_focus(tree, 1), save_id) == 1, "focus order is deterministic rather than insertion based"); + let disabled: SemanticsSpec = semantics_spec(node_key(5), root_id, SemanticRole.button, "Unavailable"); + disabled.action_mask = semantic_actions(SemanticAction.focus); + disabled.focus_order = 3; + disabled.disabled = 1; + _ = semantics_add(&tree, disabled); + expect(state, node_key_equal(semantics_next_focus(tree, 2), semantic_root_key()) == 1, "disabled nodes are excluded from focus traversal"); + expect(state, tree.nodes.len == 5 && tree.revision == 5, "tree revision advances exactly once per accepted node"); + semantics_tree_free(&tree); +} + +fn rejection_contract(state: *TestState) void { + let tree: SemanticsTree = semantics_tree_make(); + let root_id: NodeKey = node_key(10); + let root: SemanticsSpec = semantics_spec(root_id, semantic_root_key(), SemanticRole.window, "Root"); + _ = semantics_add(&tree, root); + + let duplicate: SemanticsSpec = semantics_spec(root_id, semantic_root_key(), SemanticRole.button, "Duplicate"); + expect(state, semantics_add(&tree, duplicate) == SemanticsError.duplicate_id && tree.nodes.len == 1, "duplicate IDs fail without mutating the tree"); + + let reserved: SemanticsSpec = semantics_spec(semantic_root_key(), semantic_root_key(), SemanticRole.button, "Reserved"); + expect(state, semantics_add(&tree, reserved) == SemanticsError.invalid_id && tree.nodes.len == 1, + "reserved sentinel IDs cannot masquerade as semantic nodes"); + + let orphan: SemanticsSpec = semantics_spec(node_key(11), node_key(999), SemanticRole.button, "Orphan"); + expect(state, semantics_add(&tree, orphan) == SemanticsError.missing_parent && node_key_equal(tree.error_node, node_key(11)) == 1, "missing parents report the exact rejected node"); + + let first_focus: SemanticsSpec = semantics_spec(node_key(12), root_id, SemanticRole.button, "First"); + first_focus.focus_order = 1; + _ = semantics_add(&tree, first_focus); + let duplicate_focus: SemanticsSpec = semantics_spec(node_key(13), root_id, SemanticRole.button, "Second"); + duplicate_focus.focus_order = 1; + expect(state, semantics_add(&tree, duplicate_focus) == SemanticsError.duplicate_focus_order, "duplicate explicit focus order fails visibly"); + + let bad_range: SemanticsSpec = semantics_spec(node_key(14), root_id, SemanticRole.slider, "Bad range"); + bad_range.has_range = 1; + bad_range.range_min = 10; + bad_range.range_max = 5; + bad_range.range_value = 7; + bad_range.range_step = 0; + expect(state, semantics_add(&tree, bad_range) == SemanticsError.invalid_range, "malformed range semantics are rejected"); + + let bad_text: SemanticsSpec = semantics_spec(node_key(15), root_id, SemanticRole.text_field, "Bad selection"); + bad_text.text_length = 4; + bad_text.selection_start = 3; + bad_text.selection_end = 5; + expect(state, semantics_add(&tree, bad_text) == SemanticsError.invalid_text_selection, "out-of-bounds text selection is rejected"); + + let bad_bounds: SemanticsSpec = semantics_spec(node_key(16), root_id, SemanticRole.button, "Bad bounds"); + bad_bounds.has_bounds = 1; + bad_bounds.bounds = rect(0, 0, 0 - 1, 10); + expect(state, semantics_add(&tree, bad_bounds) == SemanticsError.invalid_bounds, "negative semantic geometry is rejected without mutation"); + + let dangling: SemanticsSpec = semantics_spec(node_key(17), root_id, SemanticRole.button, "Dangling relation"); + dangling.labelled_by = node_key(999); + expect(state, semantics_add(&tree, dangling) == SemanticsError.invalid_relationship, "dangling semantic relationships fail closed"); + + let bad_collection: SemanticsSpec = semantics_spec(node_key(18), root_id, SemanticRole.cell, "Bad cell"); + bad_collection.row_index = 4; + bad_collection.row_span = 0; + expect(state, semantics_add(&tree, bad_collection) == SemanticsError.invalid_collection, "collection indices require a positive span"); + + let bad_state: SemanticsSpec = semantics_spec(node_key(19), root_id, SemanticRole.button, "Bad state"); + bad_state.selected = 2; + expect(state, semantics_add(&tree, bad_state) == SemanticsError.invalid_state, + "noncanonical semantic flags fail instead of leaking ambiguous state"); + semantics_tree_free(&tree); +} + +fn collection_metadata_contract(state: *TestState) void { + let tree: SemanticsTree = semantics_tree_make(); + let table_id: NodeKey = node_key(30); + let table: SemanticsSpec = semantics_spec(table_id, semantic_root_key(), SemanticRole.table, "Parts"); + table.row_count = 1000000; + table.column_count = 12; + table.has_bounds = 1; + table.bounds = rect(0, 0, 1200, 800); + expect(state, semantics_add(&tree, table) == SemanticsError.none, "virtual collection roots retain full logical row and column counts"); + + let cell: SemanticsSpec = semantics_spec(node_key(31), table_id, SemanticRole.cell, "Mass"); + cell.row_index = 999999; + cell.column_index = 11; + cell.row_span = 1; + cell.column_span = 1; + expect(state, semantics_add(&tree, cell) == SemanticsError.none, "live cells retain zero-based logical collection coordinates"); + + let item: SemanticsSpec = semantics_spec(node_key(32), table_id, SemanticRole.tree_item, "Assembly"); + item.level = 3; + item.set_size = 1000000; + item.position_in_set = 500001; + item.expanded = 1; + expect(state, semantics_add(&tree, item) == SemanticsError.none, "tree level, set position, and expansion truth are explicit"); + let index: i32 = semantics_find_index(tree, node_key(32)); + expect(state, index >= 0 && tree.nodes.data[index].level == 3 && tree.nodes.data[index].expanded == 1, "tree metadata remains inspectable after ownership transfer"); + semantics_tree_free(&tree); +} + +fn generated_identity_contract(state: *TestState) void { + let parent: NodeKey = node_key_scoped(50, 3); + let stable: i32 = node_key_equal( + semantic_generated_key(parent, SemanticRole.button, 7), + semantic_generated_key(parent, SemanticRole.button, 7) + ); + let unique: i32 = 1; + let i: i64 = 0; + while (i < 100) { + let j: i64 = i + 1; + while (j < 100) { + if (node_key_equal( + semantic_generated_key(parent, SemanticRole.list_item, i), + semantic_generated_key(parent, SemanticRole.list_item, j) + ) == 1) { unique = 0; } + j = j + 1; + } + i = i + 1; + } + expect(state, stable == 1, "generated semantic IDs are deterministic for the same structural input"); + expect(state, unique == 1, "100 generated sibling IDs remain unique"); + expect(state, semantic_generated_key(parent, SemanticRole.button, 7).generation == 3, "generated IDs preserve parent scope generation"); +} + +fn truncate_contract(state: *TestState) void { + let tree: SemanticsTree = semantics_tree_make(); + _ = semantics_add(&tree, semantics_spec(node_key(60), semantic_root_key(), SemanticRole.window, "Root")); + let checkpoint: SemanticsCheckpoint = semantics_tree_checkpoint(tree); + _ = semantics_add(&tree, semantics_spec(node_key(61), node_key(60), SemanticRole.button, "Temporary")); + let rolled_back: i32 = semantics_tree_rollback(&tree, checkpoint); + expect(state, rolled_back == 1 && tree.nodes.len == 1 && + tree.revision == checkpoint.revision && + semantics_find_index(tree, node_key(61)) < 0 && + tree.last_error == SemanticsError.none, + "semantic transactions can roll back owned nodes and exact revision truth"); + semantics_tree_free(&tree); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + tree_contract(&state); + rejection_contract(&state); + collection_metadata_contract(&state); + generated_identity_contract(&state); + truncate_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Semantics contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/state_reconcile_contract.zag b/tests/state_reconcile_contract.zag new file mode 100644 index 0000000..8480496 --- /dev/null +++ b/tests/state_reconcile_contract.zag @@ -0,0 +1,140 @@ +@import("std:list") +@import("../src/core/state.zag") +@import("../src/core/reconcile.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn state_contract(state: *TestState) void { + let count: State[i64] = state_make[i64](7, 10); + let ctx: ViewContext = view_context_make(node_key(100)); + expect(state, state_read[i64](&count, &ctx) == 10, "state read returns the typed value"); + expect(state, state_read[i64](&count, &ctx) == 10 && ctx.reads.len == 1, "duplicate reads collapse to one exact dependency"); + + let change: StateChange = state_set[i64](&count, 20); + expect(state, change.from_revision == 0 && change.to_revision == 1, "state change records exact revisions"); + expect(state, view_context_invalidated(ctx, change) == 1, "the recorded reader invalidates for its state change"); + let cause: InvalidationCause = view_context_invalidation(ctx, change); + expect(state, cause.found == 1 && cause.reader.value == 100 && cause.state_id == 7 && cause.read_revision == 0 && cause.from_revision == 0 && cause.to_revision == 1, "invalidation names the exact read, reader, and revision edge"); + expect(state, view_context_invalidated(ctx, StateChange{ .state_id = 8, .from_revision = 0, .to_revision = 1 }) == 0, "unread state does not invalidate the reader"); + + let binding: Binding[i64] = binding_make[i64](&count, 55); + let action: Action = binding_set[i64](binding, 30, node_key_scoped(101, 2)); + expect(state, binding_read[i64](binding, &ctx) == 30, "binding reads and writes the target state"); + expect(state, action.id == 55 && action.target_state_id == 7 && action.from_revision == 1 && action.to_revision == 2, "binding emits an auditable action"); + expect(state, view_context_invalidated(ctx, change) == 0, "a fresh read is not invalidated by an older change"); + + view_context_free(&ctx); +} + +fn reconciliation_property_contract(state: *TestState) void { + let previous: ArrayList[RenderNode] = make[RenderNode](5); + let key: i64 = 0; + while (key < 5) { + push[RenderNode](&previous, render_node(node_key(key), 10 + key, 100 + key, 1000 + key)); + key = key + 1; + } + + let case_index: i64 = 0; + let all_stable: i32 = 1; + while (case_index < 20) { + let specs: ArrayList[ViewSpec] = make[ViewSpec](5); + let step: i64 = 1; + if ((case_index % 2) == 1) { step = 3; } + let i: i64 = 0; + while (i < 5) { + let permuted_key: i64 = (case_index + i * step) % 5; + push[ViewSpec](&specs, view_spec(node_key(permuted_key), 10 + permuted_key, 100 + permuted_key)); + i = i + 1; + } + let next_instance: i64 = 9000; + let result: ReconcileResult = reconcile(previous, specs, &next_instance); + if (result.reused != 5 || result.inserted != 0 || result.updated != 0 || result.removed != 0) { all_stable = 0; } + i = 0; + while (i < result.nodes.len) { + if (result.nodes.data[i].instance_id != 1000 + result.nodes.data[i].key.value) { all_stable = 0; } + i = i + 1; + } + reconcile_free(&result); + free[ViewSpec](&specs); + case_index = case_index + 1; + } + expect(state, all_stable, "20-permutation sweep preserves identity without phantom work"); + free[RenderNode](&previous); +} + +fn environment_contract(state: *TestState) void { + let env: Environment = environment_make(); + expect(state, environment_get(env, 1, 99) == 99, "environment miss uses its explicit fallback"); + let first: i64 = environment_set(&env, 1, 42); + let same: i64 = environment_set(&env, 1, 42); + let changed: i64 = environment_set(&env, 1, 43); + expect(state, first == 1 && same == 1 && changed == 2, "environment revisions change only when values change"); + expect(state, environment_get(env, 1, 0) == 43, "environment returns the latest value"); + environment_free(&env); +} + +fn reconciliation_contract(state: *TestState) void { + let previous: ArrayList[RenderNode] = make[RenderNode](2); + push[RenderNode](&previous, render_node(node_key(1), 10, 100, 40)); + push[RenderNode](&previous, render_node(node_key(2), 20, 200, 41)); + + let next: ArrayList[ViewSpec] = make[ViewSpec](3); + push[ViewSpec](&next, view_spec(node_key(2), 20, 200)); + push[ViewSpec](&next, view_spec(node_key(1), 10, 101)); + push[ViewSpec](&next, view_spec(node_key(3), 30, 300)); + let next_instance: i64 = 100; + let first: ReconcileResult = reconcile(previous, next, &next_instance); + + expect(state, first.reused == 2 && first.updated == 1 && first.inserted == 1 && first.removed == 0, "reconciliation reports reuse, update, and insertion exactly"); + expect(state, first.nodes.data[0].instance_id == 41 && first.nodes.data[1].instance_id == 40 && first.nodes.data[2].instance_id == 100, "stable keys preserve identity through reorder"); + expect(state, first.nodes.data[1].generation == 1, "fingerprint change advances only the updated node generation"); + + let replacement_specs: ArrayList[ViewSpec] = make[ViewSpec](2); + push[ViewSpec](&replacement_specs, view_spec(node_key(2), 99, 201)); + push[ViewSpec](&replacement_specs, view_spec(node_key(3), 30, 300)); + let second: ReconcileResult = reconcile(first.nodes, replacement_specs, &next_instance); + expect(state, second.inserted == 1 && second.reused == 1 && second.removed == 2, "kind replacement and missing keys retire old nodes"); + expect(state, second.nodes.data[0].instance_id == 101 && second.nodes.data[1].instance_id == 100, "replacement gets a new identity while retained key stays stable"); + + let duplicate_specs: ArrayList[ViewSpec] = make[ViewSpec](2); + push[ViewSpec](&duplicate_specs, view_spec(node_key(9), 1, 1)); + push[ViewSpec](&duplicate_specs, view_spec(node_key(9), 1, 2)); + let empty: ArrayList[RenderNode] = make[RenderNode](1); + let duplicate: ReconcileResult = reconcile(empty, duplicate_specs, &next_instance); + expect(state, duplicate.duplicate_keys == 1 && duplicate.nodes.len == 1, "duplicate keys fail visibly and deterministically keep the first node"); + + reconcile_free(&duplicate); + free[RenderNode](&empty); + free[ViewSpec](&duplicate_specs); + reconcile_free(&second); + free[ViewSpec](&replacement_specs); + reconcile_free(&first); + free[ViewSpec](&next); + free[RenderNode](&previous); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + state_contract(&state); + environment_contract(&state); + reconciliation_contract(&state); + reconciliation_property_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("State/reconcile contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/talkback_contract.zag b/tests/talkback_contract.zag new file mode 100644 index 0000000..ed8a5b2 --- /dev/null +++ b/tests/talkback_contract.zag @@ -0,0 +1,127 @@ +@import("../src/automation/talkback.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn make_tree() SemanticsTree { + let tree: SemanticsTree = semantics_tree_make(); + let root: SemanticsSpec = semantics_spec(node_key(1), semantic_root_key(), SemanticRole.window, "Window"); + _ = semantics_add(&tree, root); + let button: SemanticsSpec = semantics_spec(node_key(2), node_key(1), SemanticRole.button, "Create part"); + button.action_mask = semantic_actions_add(semantic_actions(SemanticAction.activate), SemanticAction.focus); + button.description = "Creates a part in the active document"; + button.has_bounds = 1; + button.bounds = rect(10, 20, 120, 32); + button.selected = 1; + _ = semantics_add(&tree, button); + let field: SemanticsSpec = semantics_spec(node_key(3), node_key(1), SemanticRole.text_field, "Part name"); + field.action_mask = semantic_actions_add(semantic_actions(SemanticAction.set_value), SemanticAction.focus); + _ = semantics_add(&tree, field); + return tree; +} + +fn id_target_contract(state: *TestState) void { + let tree: SemanticsTree = make_tree(); + let capabilities: TalkbackCapabilities = talkback_capabilities(); + let session: TalkbackSession = talkback_session_make(capabilities); + let viewport: Rect = rect(0, 0, 800 * unit_scale(), 600 * unit_scale()); + + let query: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(1, TalkbackCommand.query, node_key(2), tree.revision)); + let click: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(2, TalkbackCommand.click, node_key(2), tree.revision)); + let type_field: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(3, TalkbackCommand.type_text, node_key(3), tree.revision)); + let type_button: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(4, TalkbackCommand.type_text, node_key(2), tree.revision)); + expect(state, query.status == TalkbackStatus.accepted && click.status == TalkbackStatus.accepted && type_field.status == TalkbackStatus.accepted, "ID queries and declared semantic actions are accepted"); + expect(state, query.resolved_node_index == semantics_find_index(tree, node_key(2)) && query.result_count == 1, "ID queries resolve to an inspectable semantic node"); + expect(state, query.resolved_role == SemanticRole.button && query.has_bounds == 1 && + query.bounds.x == 10 && query.bounds.width == 120 && query.action_mask == + semantic_actions_add(semantic_actions(SemanticAction.activate), SemanticAction.focus), + "Talkback query evidence exposes role, exact bounds, and declared actions"); + expect(state, query.name_length == 11 && query.description_length == 37 && + query.value_length == 0 && query.state_flags == 2 && query.evidence_hash != 0, + "Talkback query evidence fingerprints owned text and explicit state"); + expect(state, click.emitted_action == 1 && query.emitted_action == 0, "responses distinguish emitted actions from read-only queries"); + expect(state, type_button.status == TalkbackStatus.action_unavailable, "an ID action absent from semantics fails visibly"); + + let stale: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(5, TalkbackCommand.click, node_key(2), tree.revision - 1)); + let missing: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(6, TalkbackCommand.click, node_key(99), tree.revision)); + expect(state, stale.status == TalkbackStatus.stale_semantics, "stale semantic revisions cannot silently target replacement nodes"); + expect(state, missing.status == TalkbackStatus.target_not_found, "missing ID targets return a structured failure"); + + let unsupported: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(7, TalkbackCommand.drag, node_key(2), tree.revision)); + let bad_timeout_request: TalkbackRequest = talkback_request(8, TalkbackCommand.query, node_key(2), tree.revision); + bad_timeout_request.timeout_ms = 0; + let bad_timeout: TalkbackResponse = talkback_dispatch(&session, tree, viewport, bad_timeout_request); + expect(state, unsupported.status == TalkbackStatus.unsupported_command, "unimplemented commands remain fail-closed in capability truth"); + expect(state, bad_timeout.status == TalkbackStatus.invalid_timeout, "invalid timeout policy is rejected before dispatch"); + expect(state, session.events.len == 8 && session.events.data[7].sequence == 8, "accepted and rejected requests share one ordered evidence log"); + + talkback_session_free(&session); + semantics_tree_free(&tree); +} + +fn pixel_fallback_contract(state: *TestState) void { + let tree: SemanticsTree = make_tree(); + let capabilities: TalkbackCapabilities = talkback_capabilities(); + let session: TalkbackSession = talkback_session_make(capabilities); + let viewport: Rect = rect(0, 0, 100 * unit_scale(), 80 * unit_scale()); + + let denied_request: TalkbackRequest = talkback_pixel_request(20, TalkbackCommand.click, 20, 20, 2, 1); + let denied: TalkbackResponse = talkback_dispatch(&session, tree, viewport, denied_request); + expect(state, denied.status == TalkbackStatus.pixel_fallback_disabled, "pixel fallback is disabled unless the backend advertises it"); + expect(state, denied.target_kind == TalkbackTargetKind.pixel && session.events.data[0].target_kind == TalkbackTargetKind.pixel, "pixel attempts are never reported as ID targeting"); + + session.capabilities.pixel_fallback = 1; + session.capabilities.id_actions = 0; + let accepted_request: TalkbackRequest = talkback_pixel_request(21, TalkbackCommand.click, 100, 80, 2, 1); + let accepted: TalkbackResponse = talkback_dispatch(&session, tree, viewport, accepted_request); + let outside_request: TalkbackRequest = talkback_pixel_request(22, TalkbackCommand.click, 201, 40, 2, 1); + let outside: TalkbackResponse = talkback_dispatch(&session, tree, viewport, outside_request); + expect(state, accepted.status == TalkbackStatus.accepted, "pixel fallback resolves physical pixels through recorded display scale"); + expect(state, accepted.emitted_action == 1, "pixel action emission is independent from ID-action capability"); + expect(state, outside.status == TalkbackStatus.pixel_out_of_bounds, "scaled pixel targets outside the viewport fail visibly"); + expect(state, session.events.data[1].scale_numerator == 2 && session.events.data[1].scale_denominator == 1 && session.events.data[1].pixel_x == 100, "pixel evidence retains coordinates and scale"); + + talkback_session_free(&session); + semantics_tree_free(&tree); +} + +fn capability_contract(state: *TestState) void { + let tree: SemanticsTree = make_tree(); + let capabilities: TalkbackCapabilities = talkback_capabilities(); + let session: TalkbackSession = talkback_session_make(capabilities); + let viewport: Rect = rect(0, 0, 1, 1); + let report: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(30, TalkbackCommand.capability_report, semantic_root_key(), 0 - 1)); + let screenshot: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(31, TalkbackCommand.screenshot, semantic_root_key(), 0 - 1)); + let replay: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(32, TalkbackCommand.replay, semantic_root_key(), 0 - 1)); + let assertion: TalkbackResponse = talkback_dispatch(&session, tree, viewport, talkback_request(33, TalkbackCommand.assert_value, node_key(2), tree.revision)); + expect(state, report.status == TalkbackStatus.accepted, "capability reporting is always queryable"); + expect(state, screenshot.status == TalkbackStatus.unsupported_command && replay.status == TalkbackStatus.unsupported_command, "unavailable screenshot and replay capabilities fail closed"); + expect(state, assertion.status == TalkbackStatus.unsupported_command, "assertions remain unavailable until payload comparison is implemented"); + expect(state, capabilities.protocol_major == 0 && capabilities.protocol_minor == 2, "the experimental protocol version is explicit"); + talkback_session_free(&session); + semantics_tree_free(&tree); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + id_target_contract(&state); + pixel_fallback_contract(&state); + capability_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Zagkit Talkback contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); + _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tests/virtual_collections_contract.zag b/tests/virtual_collections_contract.zag new file mode 100644 index 0000000..f578d16 --- /dev/null +++ b/tests/virtual_collections_contract.zag @@ -0,0 +1,325 @@ +@import("std:list") +@import("../src/core/virtual_lifecycle.zag") +@import("../src/layout/virtual_table.zag") +@import("../src/layout/virtual_tree.zag") + +struct TestState { passed: i32, failed: i32 } + +fn expect(state: *TestState, condition: i32, name: []u8) void { + if (condition != 0) { + state.*.passed = state.*.passed + 1; + _zag_print("ok "); _zag_println(name); + } else { + state.*.failed = state.*.failed + 1; + _zag_print("FAIL "); _zag_println(name); + } +} + +fn table_contract(state: *TestState) void { + let row_overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let rows: VirtualListModel = virtual_model(1000000, 1000, 20, 1, 3, row_overrides); + let columns: ArrayList[VirtualTableColumn] = make[VirtualTableColumn](6); + let pinned: VirtualTableColumn = virtual_table_column(10, 40, 30, 60); + pinned.pinned = 1; pinned.resizable = 0; + push[VirtualTableColumn](&columns, pinned); + let i: i64 = 1; + while (i < 6) { push[VirtualTableColumn](&columns, virtual_table_column(10 + i, 50, 30, 80)); i = i + 1; } + let model: VirtualTableModel = virtual_table_model(7, rows, columns, 2); + let style: VirtualTableStyle = virtual_table_style(20, 2, insets_all(2)); + let vertical: i64 = virtual_item_offset(rows, + virtual_style(VirtualAxis.vertical, insets_all(0)), 500000); + let result: VirtualTableResult = virtual_table_layout( + model, size(180, 120), vertical, 60, style + ); + expect(state, result.error == VirtualTableError.none && result.semantic_rows == 1000000 && + result.semantic_columns == 6 && result.first_visible_row == 500000 && + result.last_visible_row == 500004, + "million-row Table projects the exact semantic row range without materializing the collection"); + expect(state, result.first_live_row == 499998 && result.last_live_row == 500006 && + result.first_visible_column == 2 && result.last_visible_column == 4 && + result.pinned_columns == 1 && result.cells.len == 60, + "Table bounds two-axis live cells while retaining pinned and overscanned columns"); + expect(state, result.cells.data[0].header == 1 && result.cells.data[0].pinned == 1 && + result.cells.data[0].frame.x == 2 && result.cells.data[1 + 2].row_index == 500000 && + result.cells.data[1 + 2].frame.y == 22, + "pinned headers and rows remain fixed while the body scrolls to a stable row ID"); + expect(state, result.cells.data[20].column_index == 2 && result.cells.data[20].frame.x == 36 && + result.max_horizontal_scroll == 124 && result.layout_hash != 0, + "scrolling columns resolve exact logical offsets and maximum horizontal range"); + let repeated: VirtualTableResult = virtual_table_layout(model, size(180, 120), vertical, 60, style); + expect(state, repeated.layout_hash == result.layout_hash, + "identical Table inputs produce identical cell identity and geometry"); + let rtl_style: VirtualTableStyle = style; rtl_style.direction = LayoutDirection.rtl; + let rtl: VirtualTableResult = virtual_table_layout(model, size(180, 120), vertical, 60, rtl_style); + expect(state, rtl.cells.data[0].frame.x == 138 && rtl.layout_hash != result.layout_hash, + "RTL mirrors pinned and scrolling Table columns without changing semantic order"); + virtual_table_free(&rtl); virtual_table_free(&repeated); virtual_table_free(&result); + + let resize: VirtualTableResizeResult = virtual_table_resize(&model, 12, 100); + expect(state, resize.error == VirtualTableError.none && resize.old_width == 50 && + resize.width == 80 && resize.consumed == 30 && resize.unconsumed == 70 && + resize.changed == 1 && model.revision == 3, + "Table resize exposes clamped consumption and advances exact model and column revisions"); + let fixed_resize: VirtualTableResizeResult = virtual_table_resize(&model, 10, 10); + expect(state, fixed_resize.error == VirtualTableError.column_not_resizable && + fixed_resize.consumed == 0 && fixed_resize.unconsumed == 10, + "nonresizable pinned columns reject mutation without losing requested delta truth"); + style.max_live_cells = 20; + let limited: VirtualTableResult = virtual_table_layout(model, size(180, 120), vertical, 60, style); + expect(state, limited.error == VirtualTableError.cell_limit_exceeded, + "Table cell residency ceilings fail before cell allocation"); + virtual_table_free(&limited); + free[VirtualTableColumn](&columns); free[VirtualExtentOverride](&row_overrides); +} + +fn table_failure_contract(state: *TestState) void { + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let rows: VirtualListModel = virtual_model(10, 1, 10, 0, 0, overrides); + let columns: ArrayList[VirtualTableColumn] = make[VirtualTableColumn](3); + push[VirtualTableColumn](&columns, virtual_table_column(1, 20, 10, 30)); + let late_pinned: VirtualTableColumn = virtual_table_column(2, 20, 10, 30); late_pinned.pinned = 1; + push[VirtualTableColumn](&columns, late_pinned); + let malformed: VirtualTableResult = virtual_table_layout( + virtual_table_model(1, rows, columns, 0), size(100, 80), 0, 0, + virtual_table_style(10, 1, insets_all(0)) + ); + expect(state, malformed.error == VirtualTableError.invalid_pinned_order && malformed.error_index == 1, + "pinned Table columns must form one deterministic logical-leading region"); + virtual_table_free(&malformed); + columns.data[1].pinned = 0; columns.data[1].id = 1; + let duplicate: VirtualTableResult = virtual_table_layout( + virtual_table_model(1, rows, columns, 0), size(100, 80), 0, 0, + virtual_table_style(10, 1, insets_all(0)) + ); + expect(state, duplicate.error == VirtualTableError.duplicate_column && duplicate.error_index == 1, + "duplicate Table column IDs fail at the exact column before layout"); + virtual_table_free(&duplicate); + free[VirtualTableColumn](&columns); free[VirtualExtentOverride](&overrides); +} + +fn tree_source() ArrayList[VirtualTreeNode] { + let source: ArrayList[VirtualTreeNode] = make[VirtualTreeNode](8); + push[VirtualTreeNode](&source, virtual_tree_node(1, 0, 0, 1, 1, 12, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(2, 1, 1, 0, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(3, 1, 1, 1, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(4, 3, 2, 0, 0, 8, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(5, 3, 2, 0, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(6, 1, 1, 0, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(7, 0, 0, 1, 0, 10, 1)); + push[VirtualTreeNode](&source, virtual_tree_node(8, 7, 1, 0, 0, 10, 1)); + return source; +} + +fn tree_contract(state: *TestState) void { + let source: ArrayList[VirtualTreeNode] = tree_source(); + let collapsed: VirtualTreeProjection = virtual_tree_project(source, 1); + expect(state, collapsed.error == VirtualTreeError.none && collapsed.source_count == 8 && + collapsed.nodes.len == 5 && collapsed.nodes.data[0].id == 1 && + collapsed.nodes.data[2].id == 3 && collapsed.nodes.data[3].id == 6 && + collapsed.nodes.data[4].id == 7, + "Tree projection removes collapsed descendants while preserving source and sibling identity"); + let style: VirtualTreeStyle = virtual_tree_style(10, 1, 4, insets_all(1)); + let ltr: VirtualTreeResult = virtual_tree_layout(collapsed, size(100, 30), 0, style); + expect(state, ltr.error == VirtualTreeError.none && ltr.semantic_count == 5 && + ltr.first_visible == 0 && ltr.last_visible == 2 && ltr.placements.len == 5, + "Tree virtualizes the expanded semantic projection with exact visible and live ranges"); + expect(state, ltr.placements.data[1].id == 2 && ltr.placements.data[1].depth == 1 && + ltr.placements.data[1].content_frame.x == 5 && + ltr.placements.data[1].content_frame.width == 94, + "Tree indentation is logical and preserves the full selectable row frame"); + let rtl_style: VirtualTreeStyle = style; rtl_style.direction = LayoutDirection.rtl; + let rtl: VirtualTreeResult = virtual_tree_layout(collapsed, size(100, 30), 0, rtl_style); + expect(state, rtl.placements.data[1].content_frame.x == 1 && + rtl.placements.data[1].content_frame.width == 94 && rtl.layout_hash != ltr.layout_hash, + "RTL moves Tree indentation to logical start without reordering nodes"); + + source.data[2].expanded = 1; source.data[2].revision = 2; + source.data[6].expanded = 1; source.data[6].revision = 2; + let expanded: VirtualTreeProjection = virtual_tree_project(source, 2); + expect(state, expanded.error == VirtualTreeError.none && expanded.nodes.len == 8 && + expanded.projection_hash != collapsed.projection_hash, + "expansion revisions restore descendants and alter deterministic projection identity"); + let expanded_layout: VirtualTreeResult = virtual_tree_layout(expanded, size(100, 120), 0, style); + expect(state, expanded_layout.semantic_count == 8 && expanded_layout.placements.data[3].id == 4 && + expanded_layout.placements.data[3].parent_id == 3 && + expanded_layout.placements.data[3].source_index == 3, + "expanded Tree placement retains parent identity and original source index"); + virtual_tree_free(&expanded_layout); virtual_tree_projection_free(&expanded); + virtual_tree_free(&rtl); virtual_tree_free(<r); virtual_tree_projection_free(&collapsed); + free[VirtualTreeNode](&source); +} + +fn tree_failure_contract(state: *TestState) void { + let source: ArrayList[VirtualTreeNode] = tree_source(); + source.data[2].id = 2; + let duplicate: VirtualTreeProjection = virtual_tree_project(source, 1); + expect(state, duplicate.error == VirtualTreeError.duplicate_id && duplicate.error_index == 2, + "duplicate Tree IDs fail before visible projection"); + virtual_tree_projection_free(&duplicate); + source.data[2].id = 3; source.data[3].depth = 3; + let depth: VirtualTreeProjection = virtual_tree_project(source, 1); + expect(state, depth.error == VirtualTreeError.invalid_depth && depth.error_index == 3, + "Tree depth jumps fail at the exact malformed preorder node"); + virtual_tree_projection_free(&depth); + source.data[3].depth = 2; source.data[3].parent_id = 999; + let parent: VirtualTreeProjection = virtual_tree_project(source, 1); + expect(state, parent.error == VirtualTreeError.invalid_parent && parent.error_index == 3, + "Tree parent mismatches fail instead of inferring ownership"); + virtual_tree_projection_free(&parent); + source.data[3].parent_id = 3; source.data[2].has_children = 0; source.data[2].expanded = 0; + let leaf_parent: VirtualTreeProjection = virtual_tree_project(source, 1); + expect(state, leaf_parent.error == VirtualTreeError.invalid_parent && leaf_parent.error_index == 3, + "Tree leaves cannot silently own child nodes"); + virtual_tree_projection_free(&leaf_parent); free[VirtualTreeNode](&source); +} + +fn id_range(start: i64, count: i32) ArrayList[i64] { + let ids: ArrayList[i64] = make[i64](count); + let i: i32 = 0; + while (i < count) { push[i64](&ids, start + i as i64); i = i + 1; } + return ids; +} + +fn lifecycle_contract(state: *TestState) void { + let store: VirtualRetainedStore = virtual_store_make(12); + let first_ids: ArrayList[i64] = id_range(100, 10); + let first: VirtualLifecycleResult = virtual_store_reconcile(&store, first_ids); + expect(state, first.error == VirtualLifecycleError.none && first.inserted == 10 && + first.live == 10 && first.resident == 10, + "initial virtual residency allocates one retained instance per live stable ID"); + let focus_index: i32 = virtual_store_find(store, 105); + let focus_instance: i64 = store.entries.data[focus_index].instance_id; + let focus_generation: i64 = store.entries.data[focus_index].generation; + expect(state, virtual_store_focus(&store, 105) == VirtualLifecycleError.none, + "virtual focus attaches to a retained stable ID"); + + let second_ids: ArrayList[i64] = id_range(200, 10); + let second: VirtualLifecycleResult = virtual_store_reconcile(&store, second_ids); + expect(state, second.error == VirtualLifecycleError.none && second.live == 10 && + second.resident <= 12 && second.evicted == 10 && + virtual_store_find(store, 105) >= 0 && store.entries.data[virtual_store_find(store, 105)].live == 0, + "offscreen focus remains retained while nonfocused instances recycle within capacity"); + + let return_ids: ArrayList[i64] = id_range(300, 9); + return_ids.data[0] = 105; + let returned: VirtualLifecycleResult = virtual_store_reconcile(&store, return_ids); + let restored_index: i32 = virtual_store_find(store, 105); + expect(state, returned.error == VirtualLifecycleError.none && returned.focus_restored == 1 && + store.entries.data[restored_index].instance_id == focus_instance && + store.entries.data[restored_index].generation == focus_generation && + store.entries.data[restored_index].live == 1, + "returning focused IDs restore the exact retained instance without generation drift"); + let before_hash: i64 = returned.lifecycle_hash; + let identical: VirtualLifecycleResult = virtual_store_reconcile(&store, return_ids); + expect(state, identical.rebound == 0 && identical.inserted == 0 && + identical.lifecycle_hash == before_hash, + "identical live residency performs no lifecycle revision or identity churn"); + + let duplicate_ids: ArrayList[i64] = id_range(400, 2); duplicate_ids.data[1] = 400; + let duplicate: VirtualLifecycleResult = virtual_store_reconcile(&store, duplicate_ids); + expect(state, duplicate.error == VirtualLifecycleError.duplicate_id && duplicate.error_index == 1, + "duplicate live IDs fail before retained-store mutation"); + free[i64](&duplicate_ids); free[i64](&return_ids); free[i64](&second_ids); free[i64](&first_ids); + virtual_store_free(&store); + + let tight: VirtualRetainedStore = virtual_store_make(4); + let focus_ids: ArrayList[i64] = id_range(1, 1); _ = virtual_store_reconcile(&tight, focus_ids); + _ = virtual_store_focus(&tight, 1); + let full_ids: ArrayList[i64] = id_range(10, 4); + let full: VirtualLifecycleResult = virtual_store_reconcile(&tight, full_ids); + expect(state, full.error == VirtualLifecycleError.too_many_live_nodes && + virtual_store_find(tight, 1) >= 0, + "offscreen focus consumes explicit capacity instead of being silently recycled"); + free[i64](&full_ids); free[i64](&focus_ids); virtual_store_free(&tight); +} + +fn property_contract(state: *TestState) void { + let valid: i32 = 1; + let overrides: ArrayList[VirtualExtentOverride] = make[VirtualExtentOverride](1); + let rows: VirtualListModel = virtual_model(10000, 100, 10, 1, 0, overrides); + let columns: ArrayList[VirtualTableColumn] = make[VirtualTableColumn](5); + let pinned: VirtualTableColumn = virtual_table_column(1, 20, 20, 20); pinned.pinned = 1; + push[VirtualTableColumn](&columns, pinned); + let i: i64 = 1; + while (i < 5) { push[VirtualTableColumn](&columns, virtual_table_column(1 + i, 30, 20, 50)); i = i + 1; } + let table: VirtualTableModel = virtual_table_model(9, rows, columns, 0); + let table_style: VirtualTableStyle = virtual_table_style(10, 1, insets_all(1)); + let scroll: i64 = 0; + while (scroll <= 80) { + let result: VirtualTableResult = virtual_table_layout(table, size(90, 70), 500, scroll, table_style); + if (result.error != VirtualTableError.none || result.cells.len > 64 || + result.cells.data[0].frame.x != 1 || result.semantic_rows != 10000 || + result.first_live_row > result.first_visible_row || + result.last_live_row < result.last_visible_row) { valid = 0; } + virtual_table_free(&result); + scroll = scroll + 7; + } + free[VirtualTableColumn](&columns); free[VirtualExtentOverride](&overrides); + + let tree: ArrayList[VirtualTreeNode] = tree_source(); + let mask: i32 = 0; + while (mask < 4) { + tree.data[2].expanded = mask & 1; + tree.data[6].expanded = (mask >> 1) & 1; + let projection: VirtualTreeProjection = virtual_tree_project(tree, mask as i64); + let expected: i32 = 5 + ((mask & 1) * 2) + ((mask >> 1) & 1); + if (projection.error != VirtualTreeError.none || projection.nodes.len != expected) { valid = 0; } + let j: i32 = 0; + while (j < projection.nodes.len) { + let k: i32 = j + 1; + while (k < projection.nodes.len) { + if (projection.nodes.data[j].id == projection.nodes.data[k].id) { valid = 0; } + k = k + 1; + } + j = j + 1; + } + virtual_tree_projection_free(&projection); + mask = mask + 1; + } + free[VirtualTreeNode](&tree); + + let wide_tree: ArrayList[VirtualTreeNode] = make[VirtualTreeNode](4096); + let wide_index: i32 = 0; + while (wide_index < 4096) { + push[VirtualTreeNode](&wide_tree, virtual_tree_node( + 10000 + wide_index as i64, 0, 0, 0, 0, 8, 0 + )); + wide_index = wide_index + 1; + } + let wide_projection: VirtualTreeProjection = virtual_tree_project(wide_tree, 0); + if (wide_projection.error != VirtualTreeError.none || wide_projection.nodes.len != 4096) { + valid = 0; + } + virtual_tree_projection_free(&wide_projection); free[VirtualTreeNode](&wide_tree); + + let store: VirtualRetainedStore = virtual_store_make(16); + let window: i32 = 0; + while (window < 100) { + let ids: ArrayList[i64] = id_range(1000 + window as i64 * 10, 10); + let lifecycle: VirtualLifecycleResult = virtual_store_reconcile(&store, ids); + if (lifecycle.error != VirtualLifecycleError.none || lifecycle.live != 10 || + lifecycle.resident > 16 || store.entries.len > 16) { valid = 0; } + free[i64](&ids); + window = window + 1; + } + virtual_store_free(&store); + expect(state, valid == 1, + "Table scroll 4096-node Tree expansion and 100-window recycling sweeps preserve bounded stable identity"); +} + +fn main() i32 { + let state: TestState = TestState{ .passed = 0, .failed = 0 }; + table_contract(&state); + table_failure_contract(&state); + tree_contract(&state); + tree_failure_contract(&state); + lifecycle_contract(&state); + property_contract(&state); + let passed_text: []u8 = _zag_i64_to_str(state.passed as i64); + let failed_text: []u8 = _zag_i64_to_str(state.failed as i64); + _zag_print("Virtual collections contract: pass="); _zag_print(passed_text); + _zag_print(" fail="); _zag_println(failed_text); + _zag_str_free(passed_text); _zag_str_free(failed_text); + if (state.failed == 0) { return 0; } + return 1; +} diff --git a/tools/check-contracts.sh b/tools/check-contracts.sh new file mode 100755 index 0000000..094e888 --- /dev/null +++ b/tools/check-contracts.sh @@ -0,0 +1,255 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$root" + +fail() { + printf 'contract check: FAIL: %s\n' "$1" >&2 + exit 1 +} + +require_file() { + [ -f "$1" ] || fail "missing required file: $1" +} + +require_json() { + require_file "$1" + jq -e . "$1" >/dev/null || fail "invalid JSON: $1" +} + +command -v jq >/dev/null 2>&1 || fail "jq is required to validate repository contracts" + +for path in \ + LICENSE README.md GOAL.md CHANGELOG.md CONTRIBUTING.md GOVERNANCE.md VERSIONING.md \ + SUPPORT.md SECURITY.md DEPENDENCIES.md ROADMAP.md zag.mod \ + docs/design/visual-direction.md docs/quality/release-gates.md \ + docs/automation/talkback.md \ + docs/architecture/README.md docs/milestones/0000-product-contract.md \ + benchmarks/README.md; do + require_file "$path" +done + +grep -q 'Run strict headless contracts' .github/workflows/contracts.yml \ + || fail "CI no longer executes strict headless contracts" +grep -q 'branches: \[main\]' .github/workflows/contracts.yml \ + || fail "CI no longer limits duplicate push validation to main" +grep -q 'contracts/toolchain.json' .github/workflows/contracts.yml \ + || fail "CI no longer resolves its Zag revision from the toolchain contract" +grep -q 'Bootstrap pinned Zag source' .github/workflows/contracts.yml \ + || fail "CI no longer self-hosts the pinned Zag source before testing" +grep -q 'zag-fixed-${{ runner.os }}-${{ steps.toolchain.outputs.zag_sha }}' .github/workflows/contracts.yml \ + || fail "CI no longer caches fixed-point Zag by exact source revision" +grep -q -- '--analyze-strict' tools/test-headless.sh \ + || fail "headless tests no longer enforce strict Zag analysis" + +for path in \ + docs/rfcs/0000-rfc-process.md \ + docs/rfcs/0001-product-and-platform-contract.md \ + docs/rfcs/0002-declarative-core-and-rendering.md \ + docs/rfcs/0003-text-semantics-and-input.md \ + docs/rfcs/0004-platform-seams-and-backend-truth.md \ + docs/rfcs/0005-quality-and-release-contract.md \ + docs/rfcs/0006-flex-talkback-visual-fidelity-and-prismstudio.md; do + require_file "$path" + grep -q 'Status: Accepted' "$path" || fail "$path is not accepted" +done + +grep -q 'Goal status: \*\*Active\*\*' GOAL.md || fail "master goal is not active" +checkbox_count=$(grep -Ec '^- \[[ x]\] `[A-Z0-9][A-Z0-9-]+` ' GOAL.md || true) +[ "$checkbox_count" -ge 80 ] || fail "master goal checklist lost required coverage" +checklist_ids=$(grep -E '^- \[[ x]\] `[A-Z0-9][A-Z0-9-]+` ' GOAL.md | sed -E 's/^- \[[ x]\] `([^`]+)`.*/\1/') +unique_checklist_ids=$(printf '%s\n' "$checklist_ids" | sort -u | wc -l | tr -d ' ') +[ "$unique_checklist_ids" -eq "$checkbox_count" ] || fail "master goal checklist IDs are not unique" +if grep -E '^- \[x\] ' GOAL.md | grep -vq ' — Evidence: '; then + fail "checked goal item is missing Evidence" +fi +if grep -E '^- \[ \] ' GOAL.md | grep -vq ' — Exit: '; then + fail "unchecked goal item is missing an Exit condition" +fi +for id in G1-SOURCE-FIRST G2-FLEX G3-FONTS G3-SVG G3-PNG G3-GLASS \ + G4-TALKBACK-IDS G4-TALKBACK-PIXELS G5-LINUX-POLISH G6-SHELL \ + G6-SCREENSHOTS G6-POLISH G7-ONE-POINT-ZERO; do + printf '%s\n' "$checklist_ids" | grep -qx "$id" || fail "master goal is missing $id" +done + +for path in contracts/toolchain.json contracts/platforms.json \ + contracts/upstream-zag.json contracts/components.json \ + contracts/benchmark-scenes.json contracts/talkback-protocol.json; do + require_json "$path" +done + +manifest_version=$(awk -F '"' '/^[[:space:]]*version[[:space:]]*=/{print $2; exit}' zag.mod) +manifest_edition=$(awk -F '"' '/^[[:space:]]*edition[[:space:]]*=/{print $2; exit}' zag.mod) +contract_version=$(jq -r '.zagkit_version' contracts/toolchain.json) +platform_version=$(jq -r '.generated_for' contracts/platforms.json) +component_version=$(jq -r '.generated_for' contracts/components.json) +benchmark_version=$(jq -r '.generated_for' contracts/benchmark-scenes.json) + +[ -n "$manifest_version" ] || fail "zag.mod has no version" +printf '%s\n' "$manifest_version" | grep -Eq '^0\.[0-9]+\.[0-9]+-(experimental|alpha|beta)\.[0-9]+$' \ + || fail "pre-1.0 version must carry an experimental, alpha, or beta prerelease" +[ "$contract_version" = "$manifest_version" ] || fail "toolchain version does not match zag.mod" +[ "$platform_version" = "$manifest_version" ] || fail "platform version does not match zag.mod" +[ "$component_version" = "$manifest_version" ] || fail "component version does not match zag.mod" +[ "$benchmark_version" = "$manifest_version" ] || fail "benchmark version does not match zag.mod" +[ "$manifest_edition" = "$(jq -r '.zag.edition' contracts/toolchain.json)" ] \ + || fail "Zag edition does not match zag.mod" + +compiler_commit=$(jq -r '.zag.commit' contracts/toolchain.json) +printf '%s\n' "$compiler_commit" | grep -Eq '^[0-9a-f]{40}$' \ + || fail "toolchain commit is not an exact lowercase Git SHA" +jq -e '.zag.repository == "https://github.com/Sylorlabs/zag" and + (.zag.resolved_ref | startswith("refs/heads/")) and + (.zag.compiler_version | length > 0) and + (.audited_on | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$")) and + (.evidence | length >= 3)' contracts/toolchain.json >/dev/null \ + || fail "toolchain record is incomplete" + +expected_platforms='android ios linux macos windows' +actual_platforms=$(jq -r '.platforms[].id' contracts/platforms.json | sort | tr '\n' ' ' | sed 's/ $//') +required_platforms=$(jq -r '.one_point_zero.required_platforms[]' contracts/platforms.json | sort | tr '\n' ' ' | sed 's/ $//') +[ "$actual_platforms" = "$expected_platforms" ] || fail "platform matrix must contain exactly the five required families" +[ "$required_platforms" = "$expected_platforms" ] || fail "1.0 gate must require exactly the five platform families" + +jq -e ' + .status_vocabulary == ["supported", "experimental", "unavailable"] and + (.required_capabilities | sort) as $required | + all(.platforms[]; + (.public_channel | IN("experimental", "alpha", "beta", "stable")) and + ((.capabilities | map(.id) | sort) == $required) and + ((.capabilities | map(.id) | unique | length) == ($required | length)) and + all(.capabilities[]; + (.state | IN("supported", "experimental", "unavailable")) and + (.reason | type == "string" and length > 0) and + (.evidence | type == "array") and + (if .state == "unavailable" then true else (.evidence | length > 0) end) + ) + ) and + (.one_point_zero.blocked == true) and + all(.platforms[].capabilities[]; .state == "unavailable") +' contracts/platforms.json >/dev/null || fail "platform capability truth is incomplete or inflated" + +ledger_commit=$(jq -r '.audited_commit' contracts/upstream-zag.json) +[ "$ledger_commit" = "$compiler_commit" ] || fail "upstream ledger and toolchain pin audit different Zag commits" +jq -e ' + (.entries | length >= 15) and + ((.entries | map(.id) | unique | length) == (.entries | length)) and + all(.entries[]; + (.state | IN("available", "partial", "missing")) and + (.category | type == "string" and length > 0) and + (.needed_by | type == "array" and length > 0) and + all(.needed_by[]; . >= 2 and . <= 7) and + (.evidence | type == "string" and length > 20) and + (.exit_gate | type == "string" and length > 20) + ) +' contracts/upstream-zag.json >/dev/null || fail "upstream prerequisite ledger is incomplete" + +for id in \ + target-linux-arm64 target-darwin-macho target-windows-pe-coff \ + target-ios-arm64 target-android-arm64 abi-objective-c abi-com abi-jni \ + abi-callbacks abi-aggregates resource-embedding dynamic-platform-loading \ + main-loop-and-workers package-resolution incremental-and-reload-hooks; do + jq -e --arg id "$id" 'any(.entries[]; .id == $id)' contracts/upstream-zag.json >/dev/null \ + || fail "upstream prerequisite is missing: $id" +done + +jq -e ' + (.visual_direction_gate.state == "blocked") and + (.components | length >= 40) and + ((.components | map(.id) | unique | length) == (.components | length)) and + all(.components[]; + (.status == "planned") and + (.milestone >= 2 and .milestone <= 4) and + (.semantic_roles | type == "array" and length > 0) and + (.inputs | type == "array") and + (.adaptive | type == "array" and length > 0) + ) +' contracts/components.json >/dev/null || fail "component inventory or visual production gate is incomplete" + +jq -e ' + (.result_state == "specifications-only") and + (.scenes | length >= 10) and + ((.scenes | map(.id) | unique | length) == (.scenes | length)) and + all(.scenes[]; + (.status == "specification") and + (.milestone >= 2 and .milestone <= 4) and + (.canonical_interaction | type == "string" and length > 20) and + (.stresses | length >= 3) and + (.assertions | length >= 4) + ) and + (.variant_matrix.scale_factors | length >= 5) and + (.variant_matrix.themes == ["light", "dark"]) and + (.variant_matrix.contrast | length == 2) and + (.variant_matrix.directions | sort == ["ltr", "rtl"]) and + (.variant_matrix.text_scales | length >= 3) and + (.variant_matrix.motion | length == 2) and + (.variant_matrix.locales | length >= 7) and + (.global_performance_contract.frame_deadline_p99_fraction_max == 1.0) and + (.global_performance_contract.scroll_refresh_hz_on_supported_reference_hardware == 120) and + (.global_performance_contract.idle_layouts_per_second_max == 0) and + (.global_performance_contract.idle_paints_per_second_max == 0) and + (.global_performance_contract.unexplained_two_frame_stalls_per_ten_minutes_max == 0) and + (.global_performance_contract.regression_without_reviewed_waiver_percent_max == 5.0) +' contracts/benchmark-scenes.json >/dev/null || fail "benchmark scene contract is incomplete" + +jq -e ' + .name == "Zagkit Talkback" and + .android_assistive_technology == "Android TalkBack" and + (.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+-experimental$")) and + .targeting.primary == "semantic-id" and + .targeting.pixel_fallback.capability_gated == true and + .targeting.pixel_fallback.record_coordinates == true and + .targeting.pixel_fallback.record_scale == true and + .targeting.pixel_fallback.may_report_as_id_target == false and + (.commands | length == 16) and + ((.commands | unique | length) == (.commands | length)) and + (.statuses | length >= 9) and + ((.statuses | unique | length) == (.statuses | length)) and + (.request_fields | index("target-kind") != null) and + (.request_fields | index("semantics-revision") != null) and + (.response_fields | index("event-sequence") != null) and + (.response_fields | index("emitted-action") != null) and + .event_contract.ordered == true and + .event_contract.records_accepted_and_rejected == true and + .current_runtime_truth."native-transport" == "unavailable" +' contracts/talkback-protocol.json >/dev/null || fail "Zagkit Talkback protocol contract is incomplete" + +if [ -d .toolchain/zag/.git ]; then + git -C .toolchain/zag cat-file -e "$compiler_commit^{commit}" 2>/dev/null \ + || fail "CI Zag checkout does not contain the pinned commit" + [ "$(git -C .toolchain/zag rev-parse HEAD)" = "$compiler_commit" ] \ + || fail "CI Zag checkout HEAD does not equal the pinned commit" + printf 'contract check: verified pinned Zag commit in CI checkout\n' +elif [ -d ../zag/.git ]; then + git -C ../zag cat-file -e "$compiler_commit^{commit}" 2>/dev/null \ + || fail "neighboring Zag repository does not contain the pinned commit" + printf 'contract check: verified pinned Zag commit in neighboring checkout\n' +else + printf 'contract check: neighboring Zag checkout absent; exact SHA format validated only\n' +fi + +for source_root in src packages platform; do + if [ -d "$source_root" ] && find "$source_root" -type f \ + \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.cxx' \ + -o -name '*.zig' -o -name '*.rs' \) -print -quit | grep -q .; then + fail "foreign implementation source found under $source_root" + fi +done + +find . \( -path './.git' -o -path './.toolchain' \) -prune -o -name '*.md' -type f -print | sort | +while IFS= read -r markdown_file; do + markdown_base=$(dirname "$markdown_file") + grep -oE '\]\([^)]+\)' "$markdown_file" | + sed -e 's/^](//' -e 's/)$//' | + while IFS= read -r target; do + case "$target" in + ''|'#'*|http://*|https://*|mailto:*) continue ;; + esac + link_path=${target%%#*} + [ -e "$markdown_base/$link_path" ] \ + || fail "broken Markdown link in $markdown_file: $target" + done +done + +printf 'contract check: PASS (%s, Zag %s)\n' "$manifest_version" "$compiler_commit" diff --git a/tools/render-headless-reference.sh b/tools/render-headless-reference.sh new file mode 100755 index 0000000..8b09d6b --- /dev/null +++ b/tools/render-headless-reference.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +ZNC=${ZNC:-/home/micah/Desktop/Sylorlabs/zag/zag-poc/znc} + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +TMP=$(mktemp -d /tmp/zagkit-reference.XXXXXX) +trap 'find "$TMP" -depth -delete' EXIT + +cd "$ROOT" +"$ZNC" tools/render-headless-reference.zag \ + --no-zagd --analyze-strict --no-foreground-cache \ + -o "$TMP/render-headless-reference" +"$TMP/render-headless-reference" "$1" diff --git a/tools/render-headless-reference.zag b/tools/render-headless-reference.zag new file mode 100644 index 0000000..5a85a35 --- /dev/null +++ b/tools/render-headless-reference.zag @@ -0,0 +1,140 @@ +@import("../src/render/png_encode.zag") + +fn reference_push_fill(list: *DisplayList, owner: i64, bounds: Rect, + paint: Paint) i32 { + let op: DisplayOp = display_op(DisplayOpKind.fill_rect, node_key(owner)); + op.bounds = bounds; + op.paint = paint; + return (display_list_push(list, op) == DisplayError.none) as i32; +} + +fn reference_push_stroke(list: *DisplayList, owner: i64, bounds: Rect, + paint: Paint, width: i64) i32 { + let op: DisplayOp = display_op(DisplayOpKind.stroke_rect, node_key(owner)); + op.bounds = bounds; + op.paint = paint; + op.paint.stroke_width = width; + return (display_list_push(list, op) == DisplayError.none) as i32; +} + +fn reference_add_accent_path(list: *DisplayList) i32 { + let path: PathData = path_make(PathFillRule.non_zero); + _ = path_move_to(&path, path_point(238 * unit_scale(), 48 * unit_scale())); + _ = path_cubic_to(&path, + path_point(280 * unit_scale(), 34 * unit_scale()), + path_point(302 * unit_scale(), 70 * unit_scale()), + path_point(276 * unit_scale(), 98 * unit_scale())); + _ = path_quad_to(&path, + path_point(252 * unit_scale(), 116 * unit_scale()), + path_point(232 * unit_scale(), 88 * unit_scale())); + _ = path_close(&path); + if (path_seal(&path) != PathError.none) { + path_free(&path); + return 0; + } + let payload: PathEncodeResult = path_encode(path); + path_free(&path); + let error: RenderResourceError = display_list_add_resource(list, + render_resource_spec(700, RenderResourceKind.path, + payload.bytes.data[0..payload.bytes.len], path_resource_format_tag()) + ); + path_encode_free(&payload); + if (error != RenderResourceError.none) { return 0; } + let op: DisplayOp = display_op(DisplayOpKind.draw_path, node_key(700)); + op.resource_id = 700; + op.paint = paint_rgba16(24576, 49152, 65535, 56000); + return (display_list_push(list, op) == DisplayError.none) as i32; +} + +fn reference_add_image(list: *DisplayList) i32 { + let pixels: ArrayList[u8] = make[u8](16); + push[u8](&pixels, 35); push[u8](&pixels, 217); push[u8](&pixels, 255); push[u8](&pixels, 255); + push[u8](&pixels, 120); push[u8](&pixels, 80); push[u8](&pixels, 255); push[u8](&pixels, 255); + push[u8](&pixels, 245); push[u8](&pixels, 120); push[u8](&pixels, 185); push[u8](&pixels, 255); + push[u8](&pixels, 20); push[u8](&pixels, 38); push[u8](&pixels, 78); push[u8](&pixels, 255); + let spec: RenderResourceSpec = render_resource_spec( + 701, RenderResourceKind.image, pixels.data[0..pixels.len], + image_resource_format_tag() + ); + spec.width = 2; + spec.height = 2; + spec.color_space = RenderColorSpace.srgb; + let error: RenderResourceError = display_list_add_resource(list, spec); + free[u8](&pixels); + if (error != RenderResourceError.none) { return 0; } + let op: DisplayOp = display_op(DisplayOpKind.draw_image, node_key(701)); + op.resource_id = 701; + op.bounds = rect(228 * unit_scale(), 118 * unit_scale(), + 70 * unit_scale(), 54 * unit_scale()); + op.paint.alpha = 52000; + return (display_list_push(list, op) == DisplayError.none) as i32; +} + +fn reference_scene() DisplayList { + let list: DisplayList = display_list_make(); + _ = reference_push_fill(&list, 1, rect(0, 0, 320 * unit_scale(), 200 * unit_scale()), + paint_rgba16(2600, 3900, 8200, 65535)); + _ = reference_push_fill(&list, 2, rect(0, 0, 320 * unit_scale(), 28 * unit_scale()), + paint_rgba16(6200, 8200, 15000, 62000)); + _ = reference_push_stroke(&list, 3, rect(0, 28 * unit_scale(), + 320 * unit_scale(), unit_scale()), paint_rgba16(16000, 22000, 36000, 36000), + unit_scale()); + _ = reference_push_fill(&list, 4, rect(12 * unit_scale(), 42 * unit_scale(), + 72 * unit_scale(), 144 * unit_scale()), paint_rgba16(7000, 10000, 18000, 52000)); + _ = reference_push_stroke(&list, 5, rect(12 * unit_scale(), 42 * unit_scale(), + 72 * unit_scale(), 144 * unit_scale()), paint_rgba16(18000, 26000, 43000, 42000), + unit_scale()); + _ = reference_push_fill(&list, 6, rect(96 * unit_scale(), 42 * unit_scale(), + 112 * unit_scale(), 62 * unit_scale()), paint_rgba16(10000, 13500, 23500, 56000)); + _ = reference_push_stroke(&list, 7, rect(96 * unit_scale(), 42 * unit_scale(), + 112 * unit_scale(), 62 * unit_scale()), paint_rgba16(22000, 34000, 54000, 46000), + unit_scale()); + _ = reference_push_fill(&list, 8, rect(96 * unit_scale(), 116 * unit_scale(), + 112 * unit_scale(), 70 * unit_scale()), paint_rgba16(8200, 11200, 20500, 54000)); + _ = reference_push_stroke(&list, 9, rect(96 * unit_scale(), 116 * unit_scale(), + 112 * unit_scale(), 70 * unit_scale()), paint_rgba16(18000, 30000, 50000, 42000), + unit_scale()); + _ = reference_push_fill(&list, 10, rect(24 * unit_scale(), 56 * unit_scale(), + 48 * unit_scale(), 8 * unit_scale()), paint_rgba16(12000, 52000, 65535, 57000)); + _ = reference_push_fill(&list, 11, rect(24 * unit_scale(), 76 * unit_scale(), + 36 * unit_scale(), 5 * unit_scale()), paint_rgba16(26000, 33000, 47000, 50000)); + _ = reference_push_fill(&list, 12, rect(24 * unit_scale(), 91 * unit_scale(), + 44 * unit_scale(), 5 * unit_scale()), paint_rgba16(26000, 33000, 47000, 50000)); + _ = reference_add_accent_path(&list); + _ = reference_add_image(&list); + _ = display_list_seal(&list); + return list; +} + +fn main() i32 { + if (_zag_argc() != 2) { + _zag_eprintln("usage: render-headless-reference "); + return 2; + } + let list: DisplayList = reference_scene(); + if (display_list_verify(list) != DisplayError.none) { + display_list_free(&list); + return 3; + } + let raster: CpuRasterResult = cpu_rasterize(list, 320, 200); + if (raster.error != CpuRasterError.none) { + cpu_raster_free(&raster); + display_list_free(&list); + return 4; + } + let png: PngEncodeResult = png_encode_surface(raster.surface); + if (png.error != PngEncodeError.none) { + png_encode_free(&png); + cpu_raster_free(&raster); + display_list_free(&list); + return 5; + } + let write_error: i32 = _zag_write_file( + _zag_arg(1), png.bytes.data[0..png.bytes.len] + ); + png_encode_free(&png); + cpu_raster_free(&raster); + display_list_free(&list); + if (write_error != 0) { return 6; } + return 0; +} diff --git a/tools/test-headless.sh b/tools/test-headless.sh new file mode 100755 index 0000000..051e5f4 --- /dev/null +++ b/tools/test-headless.sh @@ -0,0 +1,71 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +znc=${ZNC:-/home/micah/Desktop/Sylorlabs/zag/zag-poc/znc} + +[ -x "$znc" ] || { + printf 'headless test: FAIL: Zag compiler not executable: %s\n' "$znc" >&2 + exit 1 +} + +tmp=$(mktemp -d /tmp/zagkit-headless.XXXXXX) +cleanup() { rm -rf "$tmp"; } +trap cleanup EXIT + +cd "$root" +"$znc" tests/flex_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/flex-contract" +"$tmp/flex-contract" +"$znc" tests/flex_adaptive_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/flex-adaptive-contract" +"$tmp/flex-adaptive-contract" +"$znc" tests/measure_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/measure-contract" +"$tmp/measure-contract" +"$znc" tests/overlay_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/overlay-contract" +"$tmp/overlay-contract" +"$znc" tests/grid_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/grid-contract" +"$tmp/grid-contract" +"$znc" tests/scroll_virtual_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/scroll-virtual-contract" +"$tmp/scroll-virtual-contract" +"$znc" tests/virtual_collections_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/virtual-collections-contract" +"$tmp/virtual-collections-contract" +"$znc" tests/state_reconcile_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/state-reconcile-contract" +"$tmp/state-reconcile-contract" +"$znc" tests/semantics_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/semantics-contract" +"$tmp/semantics-contract" +"$znc" tests/collection_semantics_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/collection-semantics-contract" +"$tmp/collection-semantics-contract" +"$znc" tests/talkback_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/talkback-contract" +"$tmp/talkback-contract" +"$znc" tests/render_resources_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/render-resources-contract" +"$tmp/render-resources-contract" +"$znc" tests/path_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/path-contract" +"$tmp/path-contract" +"$znc" tests/image_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/image-contract" +"$tmp/image-contract" +"$znc" tests/png_encode_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/png-encode-contract" +"$tmp/png-encode-contract" +"$znc" tests/png_decode_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/png-decode-contract" +"$tmp/png-decode-contract" +"$znc" tests/png_decode_fuzz.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/png-decode-fuzz" +"$tmp/png-decode-fuzz" +"$znc" tools/render-headless-reference.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/render-headless-reference" +"$tmp/render-headless-reference" "$tmp/reference-first.png" +"$tmp/render-headless-reference" "$tmp/reference-second.png" +[ "$(wc -c < "$tmp/reference-first.png")" -gt 100000 ] +[ "$(od -An -tx1 -N8 "$tmp/reference-first.png" | tr -d ' \n')" = "89504e470d0a1a0a" ] +cmp -s "$tmp/reference-first.png" "$tmp/reference-second.png" +printf 'Reference snapshot contract: pass=3 fail=0\n' +"$znc" tests/display_list_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/display-list-contract" +"$tmp/display-list-contract" +"$znc" tests/display_list_codec_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/display-list-codec-contract" +"$tmp/display-list-codec-contract" +"$znc" tests/cpu_raster_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/cpu-raster-contract" +"$tmp/cpu-raster-contract" +"$znc" tests/input_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/input-contract" +"$tmp/input-contract" +"$znc" tests/replay_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/replay-contract" +"$tmp/replay-contract" +"$znc" tests/motion_contract.zag --no-zagd --analyze-strict --no-foreground-cache -o "$tmp/motion-contract" +"$tmp/motion-contract" + +printf 'headless test: PASS (state, reconciliation, intrinsic measurement, constraints, Flex, Grid, Overlay, scroll, virtual list, Table, Tree, recycling, collection semantics, Talkback, owned render resources, canonical paths and images, bounded PNG decode, display lists, CPU shape and image raster, deterministic PNG snapshots, input, replay, and motion)\n' diff --git a/zag.mod b/zag.mod new file mode 100644 index 0000000..fc46da1 --- /dev/null +++ b/zag.mod @@ -0,0 +1,3 @@ +name = "zagkit" +version = "0.1.0-experimental.0" +edition = "2026"