You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/src/app/docs/app-zon/page.mdx
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -168,6 +168,10 @@ A fuller manifest for an app that also [embeds web content](/docs/frontend) and
168
168
<td><code>persist</code></td>
169
169
<td>Engine-owned Model snapshot config: monotonic <code>version</code>, optional <code>debounce_ms</code>, and the <code>restore</code> Msg routes (<code>ok</code>/<code>none</code>/<code>err</code>). Requires <code>"persist"</code> in capabilities — see <ahref="/docs/persistence">Model Persistence</a></td>
170
170
</tr>
171
+
<tr>
172
+
<td><code>images</code></td>
173
+
<td>Optional registered-image budget: <code>.images = .{ .max_image_pixel_bytes = 8_388_608 }</code>. The default is 1 MiB and accepted values are 1–8 MiB. Encoded photos decode aspect-preservingly to fit; storage is lazy per used slot, but 16 fully used 8 MiB slots are a declared 128 MiB high-water. See <ahref="/docs/dynamic-images">Dynamic Images</a>.</td>
174
+
</tr>
171
175
<tr>
172
176
<td><code>bridge</code></td>
173
177
<td>Bridge command policies (see <ahref="/docs/bridge">Bridge</a>)</td>
Copy file name to clipboardExpand all lines: docs/src/app/docs/dynamic-images/page.mdx
+13-3Lines changed: 13 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -93,7 +93,7 @@ Prefer **omitting** `cachePath` for URL sources: when the app wiring configures
93
93
Exactly one event arm dispatches per load — a five-field record matched by field name: `id`, `state`, `width`, `height`, `status`. `id` echoes the requested ImageId, so two loads in flight at once share one arm and still tell their results apart (that is what the example's `cover: msg.id` reads); `status` is the HTTP status for url loads that performed an exchange, and **0 when none occurred** — local paths and cache hits — so a `loaded` with status 0 is honest signal that the pixels came without a network round trip, never a fabricated 200; `loaded` means the pixels are registered and drawable; everything else names what actually happened:
94
94
95
95
-**Source classes** — `not_found` (missing local file, no url), `io_failed` (a local read failure), `connect_failed` / `tls_failed` / `protocol_failed` / `timed_out` (the fetch taxonomy, on the fetch machinery's own timeout), `http_status` (a non-2xx answer, with the status carried through — an error page is not an image, so the body is discarded).
96
-
-**Decode and registry classes** — the same errors the direct registration API raises: `decode_failed`, `unsupported` (a host without a codec), `too_large`, `registry_full`, and `alloc_failed` (the host refused the memory the registration needed — resource exhaustion, not corrupt bytes: the same source may load once memory frees, so it is never reported as `decode_failed`).
96
+
-**Decode and registry classes** — `decode_failed`, `unsupported` (a host without a codec), `registry_full`, and `alloc_failed` (the host refused the memory the registration needed — resource exhaustion, not corrupt bytes: the same source may load once memory frees, so it is never reported as `decode_failed`). `too_large` normally means the encoded source exceeded 8 MiB; platform codecs fit decoded pixels to the app's declared target.
97
97
-**Discipline classes** — `rejected` (an invalid id, no source at all, or a duplicate live id: one load per id at a time, the spawn rule — a load in flight is never replaced implicitly) and `cancelled` (`Cmd.imageCancel(id)` ended the load).
98
98
99
99
`Cmd.imageCancel(id)` is the load's cancel — image loads are keyed by their numeric id, so the string-keyed `Cmd.cancel` never touches them. Cancel is **loud**, the spawn discipline: the one terminal still arrives, as the load's own event arm with state `cancelled`, and the id is free for a fresh load once it lands (a slow CDN fetch no longer pins its id against a retry). Aimed at an id with no live load it no-ops — whatever it targeted already delivered its terminal.
@@ -106,15 +106,25 @@ Unlike a load, unregister is **synchronous registry surgery, not an effect**: no
106
106
107
107
## Limits, honestly
108
108
109
-
Decode limits are the registered-image limits, fixed and loud: **16 slots** of **1 MiB decoded pixels** each (512×512 RGBA8 — avatar and cover-art scale, not photo scale), with `Cmd.imageUnregister` releasing a slot when the app is done with an image. The encoded source is bounded at **1.25 MiB** from every source alike, and over-bound sources fail whole with `too_large` — never a silently cropped decode. Loaded pixels live in the existing registered-image storage; there is no separate pool to size. The framework bundles no codecs: bytes decode through CGImageSource on macOS, gdk-pixbuf on GTK, WIC on Windows, and the mobile hosts' embed image service — a host without one answers `unsupported`, never silence.
109
+
The default is **16 slots** with a **1 MiB decoded-pixel target per slot**. That target is not a refusal: platform codecs decode photo-scale sources down, preserving aspect, until `width × height × 4` fits. A wide 1024×256 image already fits and stays that size; a 640×480 image registers at a smaller geometry, and the result's `width`/`height` report exactly what views draw. The encoded source has its own flat **8 MiB** bound; an over-bound source fails whole with `too_large`, never as truncated bytes.
110
+
111
+
Image-centric apps can raise the target in `app.zon`, up to the hard 8 MiB ceiling:
The value is frozen at startup. Pixel blocks and decode scratch allocate lazily, so declaring a raise costs nothing until an image is used; each used registry slot is a whole-budget block, however, so filling all 16 slots at 8 MiB is a declared **128 MiB** high-water. The source bound stays 8 MiB regardless of this setting.
118
+
119
+
This gives three clear tiers: decode-to-fit is the default for feeds, galleries, avatars, and covers; the manifest raise serves wallpaper and image-forward apps that want more display-scale detail; pixel-exact editing, source-resolution zoom, and gigapixel tiling belong in an app-owned `gpu_surface` or [media-surface producer](/docs/media-producers), not the registry. The framework still bundles no codecs: bytes decode through CGImageSource on Apple platforms, gdk-pixbuf on GTK, WIC on Windows, BitmapFactory on Android, and the embed image service — a host without one answers `unsupported`.
110
120
111
121
For textures **produced** by your own renderer at video rates — a decoder, a camera, mpv — this is the wrong tool: that is the [media surface](/docs/media-producers)'s dynamic texture channel. `imageLoad` is for images that exist as encoded bytes somewhere and should become long-lived registered pixels.
112
122
113
123
## Recorded sessions replay byte-identical, offline
114
124
115
125
The loaded bytes **are** the effect result, and the session journal treats them that way. When a recorded session performs an image load, the encoded source bytes are written — at effect-result time — into a content-addressed blob store beside the journal (`blobs/<sha256[..16]>` in the session directory), and the journal record carries the hash and length. Loading the same bytes twice stores one blob: content addressing is deduplication — and the deduplicating probe verifies the existing blob's bytes before trusting its name, so a damaged blob is repaired in place on the next same-bytes recording rather than sealing a journal replay would refuse.
116
126
117
-
Replay reads the blob, re-runs the same decode and registration with the recorded bytes, and delivers the recorded result — **byte-identical and fully offline**: the original file, the network, and the cache are never consulted, and the fingerprint checkpoints verify the replayed session against the recording frame by frame. A journal whose `blobs/` directory is missing or damaged refuses loudly (the bytes are verified against their address) rather than replaying a different session. The blob record kind is journal format **v7** — older journals are refused at the preamble with the standard re-record teaching, the format's usual honest break.
127
+
Replay reads the blob, re-runs decode and registration with the recorded bytes, and delivers the recorded result — **byte-identical and fully offline**: the original file, the network, and the cache are never consulted, and the fingerprint checkpoints verify the replayed session against the recording frame by frame. If the current manifest lowers the image budget after recording, the recorded result dimensions still replay verbatim while best-effort presentation re-decodes to the current budget; any resulting screenshot difference is a verification mismatch, not false journal damage. A journal whose `blobs/` directory is missing or damaged refuses loudly (the bytes are verified against their address) rather than replaying a different session. The blob record kind is journal format **v7** — older journals are refused at the preamble with the standard re-record teaching, the format's usual honest break.
118
128
119
129
```sh
120
130
NATIVE_SDK_SESSION_RECORD=session/app.journal native run # records blobs/ beside the journal
`fx.registerImage(id, width, height, rgba8)` registers already-decoded pixels (the runtime copies them; your buffer is free on return), `fx.registerImageBytes(id, bytes)` decodes through the platform codec first, and `fx.unregisterImage(id)` frees the slot. Re-registering an id replaces its pixels and every view repaints — GPU caches re-upload off the changed content fingerprint, no invalidation calls. For caches, mint fresh ids (effect-key style, monotonically increasing) and unregister the evictee — never re-key different content onto a live id. Outside `UiApp`, the same registry is `Runtime.registerCanvasImage`/`registerCanvasImageBytes`/`unregisterCanvasImage`.
577
577
578
-
Capacities are fixed and loud (`canvas_limits`): `max_registered_canvas_images` slots (16) of `max_registered_canvas_image_pixel_bytes` each (1 MiB — 512×512 RGBA8, avatar/icon scale), with `error.ImageRegistryFull`, `error.ImageTooLarge`, `error.ImageDecodeFailed`, and `error.UnsupportedService`(a platform without a codec) never silent. Registered images render everywhere the canvas does: live presentation (GPU packet and software paths), `renderCanvasScreenshot`, and automation screenshots. A draw referencing an id that is not (or no longer) registered skips — a pure view cannot fail presentation with a transient loading state. In tests, the null platform's `image_decode` flag enables a deterministic decoder for the strictPNG subset `canvas.png.writeRgba8` emits, so raw RGBA fixtures exercise the full decode→register→draw path without bundling a codec.
578
+
Capacities are fixed and loud (`canvas_limits`): 16 slots with a 1 MiB decoded-pixel target by default. Encoded photos decode aspect-preservingly to fit, while raw `fx.registerImage` pixels remain strict. Image-centric apps may raise the startup-frozen target through app.zon `.images.max_image_pixel_bytes`, up to 8 MiB; storage is lazy per used slot, but filling all 16 ceiling-sized slots is a declared 128 MiB high-water. Every encoded entry point, including direct `fx.registerImageBytes`, shares the flat 8 MiB source bound. `error.ImageRegistryFull`, `error.ImageTooLarge` (encoded source, raw pixels, or a codec-contract violation), `error.ImageDecodeFailed`, and `error.UnsupportedService`are never silent. Registered images render everywhere the canvas does: live presentation, screenshots, and automation. A missing id simply draws its fallback. In tests, the null platform's deterministic strict-PNG decoder also pins exact decode-to-fit dimensions and pixels.
Copy file name to clipboardExpand all lines: skill-data/native-ui/SKILL.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1003,7 +1003,7 @@ Rules:
1003
1003
1004
1004
-`fx.registerImage(id, w, h, rgba8)` takes already-decoded straight-alpha RGBA8 (exactly `w*h*4` bytes; the runtime copies — your buffer is free on return). `fx.registerImageBytes(id, bytes)` decodes first. `fx.unregisterImage(id)` frees the slot. Outside UiApp: `Runtime.registerCanvasImage` / `registerCanvasImageBytes` / `unregisterCanvasImage`.
1005
1005
- Re-registering an id replaces the pixels; every view repaints and GPU caches re-upload off the changed content fingerprint — no invalidation calls. For caches, mint fresh ids (effect-key style, monotonically increasing) and `unregisterImage` the evictee — never re-key different content onto a live id.
- Bounded and loud (`canvas_limits`): 16 slots, a 1 MiB decoded-pixel TARGET by default. Encoded images decode aspect-preservingly to fit, so real photos load and the reported dimensions are the registered geometry; raw-pixel `registerImage` remains strict. Image-centric apps may declare app.zon `.images = .{ .max_image_pixel_bytes = 8388608 }` (accepted range 1–8 MiB). Allocation is lazy per used slot, but 16 fully used 8 MiB slots are a declared 128 MiB high-water. Every encoded entry point, including direct `fx.registerImageBytes`, shares the separate flat 8 MiB source bound. Editors needing source-resolution pixels, 1:1 zoom, or tiles own that pipeline through a `gpu_surface` or media-surface producer, not this registry. Errors: `error.ImageRegistryFull`, `error.ImageTooLarge` (encoded source, raw pixels, or a codec-contract violation), `error.ImageDecodeFailed`, `error.InvalidImageId`/`InvalidImageDimensions`, `error.UnsupportedService`.
1007
1007
- A draw referencing an unregistered id skips — a transient loading state can never fail presentation. `ui.avatar` clips a set image to the circle (`cover` fit) and renders the initials argument otherwise.
1008
1008
- Registered images render in live presentation AND `renderCanvasScreenshot`/automation screenshots, so goldens can assert on them.
1009
1009
- Deterministic tests: `harness.null_platform.image_decode = true` enables a strict decoder for the exact PNG subset `canvas.png.writeRgba8` emits — encode a raw RGBA fixture with the canvas PNG writer and drive the full decode→register→draw path with no bundled codec (`src/runtime/canvas_image_tests.zig` is the reference).
0 commit comments