ctate/code-diff
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
87fa3f92d0 |
Video playback: AVFoundation through the media surface, replayable end to end (#184)
* Add the video playback tier: platform seam, effects channel, journal v9
- One video channel mirroring audio end to end: loadVideo with the local-then-URL cascade and surface claim, transport verbs (play/pause/stop/seek/volume/mute/loop), key-stamped events, honest failed/rejected degrades, and automation-snapshot mirrors
- Pixels never enter the core: the platform decoder pushes RGBA8 through a copyable VideoFrameSink into the media-surface texture channel the load claimed
- Journal format v9: the .video effect-result kind (code 13) and platform-event tag (code 25) append the video fields after the v8 channel fields and journal every event verbatim for byte-identical replay with no producer attached
* Cover the video tier: lifecycle, frames, teardown, replay identity
- Fake and real executor batteries mirror the audio suite: request capture, event round trips, transport mirrors, rejection classes, cascade order, staged-host degrade, straggler swallowing, and the quit-while-playing stop hook
- Sink coverage proves decoded frames reach the claimed surface, replace/stop release the claim, and a stale sink push lands inert
- A recorded playback replays byte-identical into a decoder-less host with no producer attached, fingerprint and model equal; journal codecs round-trip the video event and effect shapes
* Export the video effect types and decline video on the embed host
- native_sdk.EffectVideo/EffectVideoEventKind/EffectVideoSource ride the SDK roots like their audio twins
- The mobile embed host declines video_playback until a shim registers a real decoder, the audio honesty rule
* Decode video on macOS with AVFoundation; teach on Windows and Linux
- One AVPlayer in the AppKit host: AVPlayerItemVideoOutput frames fitted to the sink's pixel budget, BGRA-to-RGBA vImage swizzle, a 1/60s run-loop frame pump plus the audio tier's 0.5s position clock, loop wraps without a completion, and paused seeks still paint their frame
- The frame sink crosses the C ABI as a callconv(.c) trampoline over the runtime's VideoFrameSink; a released claim answers 1 and the host stops its pump
- Windows and Linux stage the capability honestly: video_playback reports false and the load verbs answer a named teaching plus error.UnsupportedService; the CEF host stubs the video C ABI like audio
* Add Cmd.videoLoad and videoCtl to the TypeScript tier at opcodes 0x17/0x18
- videoLoad claims the named media-surface, resolves the local-then-URL cascade, and bakes autoplay/loop/muted into one flags byte; videoCtl drives play/pause/stop/seek/volume/muted/loop by verb ordinal with an f64 value
- The event arm is the seven-field record matched by NAME with the exact five-member state union both directions, the audio arm convention; refusals teach with the NS1027/NS1029/NS1030 vocabulary
- The host routes events on the TSVI key namespace, parks loads under the fake executor like audio, and covers the wire byte-for-byte in the package, host, and e2e suites
* Declare video from markup: the <video> element, house chrome, reconciler
- Element code 68 with flag attrs controls/autoplay/loop/muted at codes 82-85 (src rides the existing attr name; registry law keeps names unique); a leaf on all three surfaces — validator, interpreter, compiled engine — rejecting children like image
- ui.video composes the playback surface on the framework-owned surface id plus runtime-consumed transport chrome (ghost play/pause, proportional scrub slider, clipped time readouts in the built-in bar's register); presence IS playback: the ui-app reconciler loads on src change, applies loop/muted deltas, and stops when the element leaves the view
- Handler-less playback stays honest under replay: platform video events steer the channel mirrors while journaled effect records remain the only Msg source, so house-chrome sessions replay with live readouts and identical fingerprints
* Scope declarative video ownership to the playback it started
- The reconciler stops or retunes the channel only while the active key is the declaration's own: an update handler that loaded its own playback owns the single player, and a departing <video> element must not kill it
* Add the video-player example: house chrome and custom controls
- Player screen is one declarative ui.video with the house transport chrome; Custom screen composes its own bar (transport, +/-10s, proportional scrub, volume, mute, loop) from the command vocabulary over a bare media surface
- No bundled media: the launch argument or the source field names a local clip or http(s) URL, so a live macOS check is one command away
- Headless tests drive the whole transport with the fake executor's synthetic events, the automation widget path, and the null decoder behind the declarative screen
* Document the video element across the docs surfaces
- Components page, catalog entry, native-ui element table row, and the LSP/vocab doc strings for src/controls/autoplay/loop/muted
- Deterministic preview scenes render the placeholder with the house chrome (regenerated video webps only; the untouched catalog keeps its committed renders)
- Changelog fragment states the new element, the command vocabulary, the staged platforms, and the journal v9 break
* Resolve video attribute docs in the markup LSP hover path
- attributeDoc consults the video scoped table so controls/autoplay/loop/muted hover like every registry attribute; the coverage pin now names the video composite
* Make the video-player example's docs and status line honest
- README run instructions use the real CLI verbs: native dev for build-and-run, or native build plus the zig-out binary with the clip argument (the argument passthrough shape)
- The status line now matches each screen's ownership model: the declarative Player screen shows a static teaching (its transport state lives in the runtime-owned chrome, so an event-fed status there could only lie or stall), while the Custom screen keeps the event-fed loading/dimensions/finished/failed line it owns; tests pin both across load, playback, pause, and completion
- Direct-SDK examples handle the runtime's video event in their exhaustive event switches
* Stage video events non-lossily and deliver fed terminals under replay
- Video events leave the lossy pending ring for their own non-lossy stage (the image/channel discipline): a loop-side .rejected or .failed is its load call's only terminal and a fed event is one recorded delivery, so a burst past the ring's capacity must never evict one — pinned by a 40-rejection burst test
- Fed events capture the handler at feed time and deliver their journaled values verbatim when the channel no longer resolves them: under replay the platform .failed event that follows the record applies the channel reset first, and delivery-time resolution silently dropped the Msg the recording dispatched — pinned by a recorded mid-playback failure replaying fingerprint-identical
- loadVideo's deterministic refusal classes extract to the pure videoLoadRejected, one source of truth a caller-side validator can consult
* Refuse an invalid video_load before it re-routes the bridge entry
- The engine keeps the current playback when it rejects a load, but the bridge re-keyed its single routing entry optimistically first: a refused replacement left the surviving stream's events and transport verbs answering to the refused key and arm
- The bridge now consults the engine's own videoLoadRejected gate before committing the entry and stages the rejection Msg to the refused arm directly (stageLoopMsg, the channel-admission precedent), leaving the entry and the engine untouched
- Pinned: a rejected replacement delivers its rejection while the live stream keeps its events and its wire-key gate
* Drive the house video chrome from keys and rebuild it in every window
- Keyboard activation (Enter/Space) of the transport's play/pause control now drives the video channel exactly like the pointer release: the control advertises Play/Pause to focus and accessibility, so the intent must act instead of being consumed silently — pinned beside the pointer-toggle test
- Runtime-consumed control paths and handler-less video events rebuild through one helper that follows dispatch's discipline: the main canvas against ITS window (a control event from a secondary window used to target that window with the main canvas label and error) and the window slots after it, so a <video controls> declared in a secondary window's tree repaints from the moved mirrors
* Fail a macOS video load whose conversion buffer cannot allocate
- The frame tap's reusable BGRA-to-RGBA conversion buffer failing to allocate used to degrade to a silent zero-frame pump while the load still acknowledged LOADED: playback reported positions forever and could never deliver a pixel
- videoAttachOutputForItem now reports the failure and the status hop answers with the FAILED terminal instead of the acknowledgment - the honest degrade; an audio-only item (no video geometry) keeps its documented honest-absence path
* Journal handler-less video terminals and quarantine stale fed events
- Loop-side video terminals now stage and journal with no Msg handler bound (the image arm's rule): a declarative playback binds no handler, but its synchronous .failed is executor truth — the record is what replays the channel reset, and the staged delivery's wake re-renders the chrome
- A fed event whose staged key no longer names the live playback (replaced before its drain, or a replayed platform .failed already applied the terminal) delivers its staged values verbatim and leaves the live channel alone — applying a replaced stream's terminal would reset the replacement
- A journal-fed video record claiming a millisecond or dimension scalar at or past 2^53 refuses replay as a damaged record at the gate (no recorder writes one, and the TS tier's exact-integer widening would trap on it) — pinned by a hand-patched journal
* Route every video event by the key of the load that produced it
- The bridge's engine key now carries the issuing load's event-arm tag in its low byte (videoKeyForTag): a staged synchronous .failed that delivers AFTER a replacing load re-keyed the single entry still routes the arm of the load it answers, instead of handing the old stream's failure to the replacement's arm
- videoEventMsg routes by the event's own key tag, never the mutable entry's; the wire-key gate on transport verbs is unchanged
- Pinned: a replaced load's straggling terminal routes its own arm while the replacement's stream keeps its own; the request-key pins move to the tagged shape
* Reconcile <video src> from every window's tree and repaint Msg-less failures
- Secondary-window builds now feed the video reconciler: Ui.video promises that declaring the element IS the playback in every window's tree, but slot builds discarded the declaration — the main canvas wins when both declare (one player, one owner), the first declaring slot keeps ownership until it stops declaring or its window closes (reconcile-close included)
- The main build stamps the video mirrors it rendered; drainEffects compares that stamp after a Msg-less drain and re-renders the chrome when they moved — a handler-less declarative playback's synchronous failure no longer leaves controls advertising a playback that is gone
* Paint the poster frame for paused macOS video loads
- A load acknowledged while paused (autoplay = false, the documented poster-frame shape) ran no frame timer and never pumped its first decoded frame: the surface held the placeholder until the user played, paused, or seeked
- LOADED now arms a bounded first-frame hunt: the frame timer polls until the first frame pushes (a paused load then stops the timer; a playing one keeps it), surrendering honestly after ~3s if the output never yields one
* Restart the macOS frame timer when buffered playback actually begins
- The poster hunt could stop the pixel clock while AVPlayer was still waiting to play (a remote autoplay stream yielding its poster mid-buffer, or a hunt surrendering past its bound), and nothing restarted it when the waiting phase ended: audio played and positions ticked with no frames flowing
- The timeControlStatus hop now arms the frame timer whenever playback reports rolling (idempotent beside videoPlay's own arm), for local and remote sources alike
* Deliver a staged video terminal past stop instead of panicking
- A Cmd.batch([videoLoad, videoStop]) on a host whose load fails at once stages the .failed before stop retires the bridge entry; the drain then hit the entry gate's panic even though the terminal is the load call's only answer
- videoEventMsg drops the entry gate: every event reaching it was produced by a bridge-issued load and carries its arm tag in its own key (the engine swallows its post-stop stragglers itself), so routing needs no entry at all - pinned by the load-then-stop batch shape
* Retain every window's video declaration and promote on the owner's close
- The reconciler kept only the owning window's <video src>: closing that window (or its declaration vanishing) stopped playback and left another window's mounted video inactive until an unrelated rebuild
- Slot declarations now retain one entry per window (the table matches the window budget); when the owner closes or stops declaring, the next retained declaration promotes inside the same dispatch - pinned by a two-window close-promotes test with no on_close Msg and no rebuild
* Retry the frame wake for identical pushes still awaiting adoption
- A refused frame-wake request leaves pending clear so a retry can land, but the push-boundary fingerprint short-circuit returned before the wake: a static frame pushed again after a transient request_frame_fn refusal short-circuited forever and the staged bytes were never adopted
- An identical push now falls through to the wake while bytes are still staged; only an adopted frame's repeat stays a pure no-op
* Expose videoPlayback through the bridge alias and the TS feature type
- platformFeatureFromString gains the videoPlayback camel-case alias every other feature carries, so window.zero.platform.supports("videoPlayback") answers the platform truth instead of InvalidPlatformFeature
- NativeSdkPlatformFeature adds video_playback/videoPlayback so TS clients can compile the support query
* Stamp every platform video event with the load that produced it
- The engine mints a token per loadVideo, passes it through the load seam, and every host echoes it in each event: takeVideoMsg swallows an event whose token is not the current load's, so a replaced playback's queued terminal can neither reset the replacement, release its claim, nor route through its handler - pinned by a stale-failed-after-replace regression
- macOS hardening on the same theme: the item end/failure notification blocks re-check note.object against the current item (a removeObserver cannot recall a block already enqueued on the main queue), and the host carries the token across its terminal emits' teardown
- The journaled video platform event carries the token, so replay's mirror steering swallows stale recorded events exactly as live (the re-run loads mint the same deterministic sequence); the null platform echoes tokens like the real hosts and its fake position advance saturates instead of trapping on hostile deltas
* Pair replayed video deliveries with their events; token-gate fed entries; stop abandoned players
- A recorded video Msg dispatched synchronously inside its platform event's dispatch, but replay delivered the fed record at the NEXT drain: an update loading the next clip from its completion handler ran too late and the new clip's .loaded event was swallowed against the old load's token - takeVideoMsg under replay now pops the fed head (the journal's contiguity puts each event's record immediately before it) and dispatches it during the same event, pinned by a chained-load playlist session replaying byte-identical
- Pending video entries carry the load token that staged them, and delivery resolves against the live channel only while it still IS that load: the public key alone cannot tell two loads under one app key apart, and a stale fed terminal resolving against a same-key replacement would have reset it
- failVideoChannel silences the platform player it abandons (best effort): a load that succeeded before a later step refused kept its player decoding while the reset channel forgot it, and the inactive channel skipped it at teardown too
* Keep refused and uninstalled declarations out of the video reconciler
- A declared src the engine's own gates refuse (a malformed URL, say) no longer commits the reconciler's tracked ownership: the running playback keeps its identity - so removing the element later stops IT, instead of hashing the refused source and stranding the playback forever; the refused src is remembered separately and taught once, never re-attempted every rebuild - pinned by a refused-declaration ownership test
- A secondary window's declaration is captured only after its build pass and layout succeed and the tree installs: a build whose layout errors never displays, so its declaration never steers the playback either
* Link CoreVideo wherever appkit_host.m builds standalone
- The frame pump's CVPixelBuffer calls are real CoreVideo symbols: the central build gained the framework, but the generated-app template and the standalone example builds still linked only AVFoundation - a generated macOS project failed to link
- The template and every example build.zig that compiles the AppKit host now link CoreVideo beside AVFoundation
* Gate the poster hunt on a real push and clamp millisecond CMTimes
- videoPumpFrame initialized its push result to the success code, so a NULL base address or failed vImage permutation ended the poster hunt and stopped the frame timer with nothing on the surface; a sentinel now keeps the hunt alive until a push actually returns success
- NativeSdkCMTimeFromMs narrowed the u64 millisecond position straight into the signed CMTimeValue, turning absurd seeks negative; the value now clamps at INT64_MAX so AVFoundation clamps to the duration as documented
* Retire the null video player when a non-looping playback completes
- advanceVideo left the fake player loaded after its completion, so post-completion transport calls succeeded where a live host's torn-down player refuses them (and a replayed play could emit a second completion)
- the completion now unloads before the event returns, matching the live hosts' retire-before-emit order; tests pin the unload and the failed-play degrade path
* Stop the custom playback when an empty source commits
- loadCustom returned early on an empty committed source, leaving the previous video rolling under a status line that said "no source"
- an empty commit now stops the playback and resets the transport mirrors, with a test pinning the stopped channel and the honest status line
* Route replayed video records by the journaled load identity
- Every .video effect record now carries the producing load's token; replay feeds through feedVideoRecord, which resolves the token against the live channel or a retired-load park instead of binding the record to whatever the channel holds at feed time.
- loadVideo and stopVideo park the outgoing load's identity under replay, so a synchronous terminal staged inside the very dispatch that then replaced or stopped its playback still delivers the recorded Msg with the recorded identity - never EffectNotFound, never a reset of a same-key replacement the recording kept playing.
- The replay pairing in takeVideoMsg now token-gates fed entries against the platform event, and two session tests pin the batch shapes: load-then-stop and load-then-replace under one app key.
* Check the Core Video lock result before touching the pixel buffer
- A failed CVPixelBufferLockBaseAddress never maps the buffer, so reading the base address was undefined and the unconditional unlock unbalanced the lock count; the frame now drops with the buffer released.
- The poster hunt stays latched across the dropped frame - only an actual push verdict may end it, the converted-but-unpushed rule.
* Document why rate > 0 is transport intent at the playing derivations
- Per the AVPlayer.h contract, a playback waiting in AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate keeps rate at the requested value ("not currently effective but instead indicates the rate at which playback will start or resume"), so a stalled-but-unpaused stream already reports playing=1 + buffering=1 and the transport control offers Pause.
- AVPlayer resets rate to 0.0 on its own only with waits-to-minimize-stalling disabled - the local-file configuration, where playback would not self-resume and offering Play is the honest affordance.
* Keep the controls-bearing video element zero-intrinsic
- The <video controls> wrap column now adopts the media sizing contract (WidgetLayoutStyle.zero_intrinsic): the transport bar's intrinsic size never leaks into the element, so an unsized element in a hug container measures zero like the bare surface instead of rendering a controls-only strip.
- The wrap clips its content, so chrome cannot paint past a box the layout granted nothing; declared width/height stay definite through the frame and min/max bounds.
- A ui test pins both shapes: zero in a hug container, surface-above-bar inside a declared 320x180 box.
* Bound video scalars at delivery, sweep replay parks, republish flag deltas
- Platform video events clamp position, duration, and dimensions into the exact-integer delivery window (max_effect_video_scalar_exclusive, 2^53) at takeVideoMsg, whatever a host or embedder reports - the engine-side guarantee behind replay's damage gate, so an honest recording can never be refused as damage; a past-window readout now records and replays clamped, pinned by test.
- Replay-side retired video identities release at the first drain-pass boundary after parking (any journaled record for them feeds before that pass's event dispatches), so a long replayed playlist parks and releases one entry per clip instead of accumulating them; pinned at the channel level.
- Same-src declarative loop/muted deltas republish the runtime mirror in the same reconcile, so an automation snapshot taken after the flip reports the new value instead of the one published before the rebuild.
* Journal the video cascade's resolved source for replay
- loadVideo now journals a Msg-less .video_load record (the .clock/.env discipline) carrying which source the recording host's cascade resolved - a missing local file that fell through to the url is filesystem truth the replayed fake load cannot re-probe.
- Replay queues each record and the replayed load consumes it by its deterministic token, so videoSnapshot() and the automation mirror report .stream with the optimistic buffering flag exactly as the recording did, handler-less declarative playbacks included.
- Pinned by a record/replay test on an assets-absent host; the handler-less house-chrome pin now expects exactly the one Msg-less record.
* Keep buffering honest across paused streams and widen the fake's loop wrap
- Buffering means an un-paused stream waiting for bytes: a fresh URL load now starts the flag from the autoplay intent (engine mirror and replayed cascade resolution alike), the macOS host derives it purely from timeControlStatus instead of presetting it, and pauseVideo clears it - a conforming host emits no pause acknowledgment that could.
- The macOS time-control observer stays silent when the transition lands on paused: pause emits nothing by contract and position reports are for playback in motion; waiting and playing transitions still emit.
- The null platform's advanceVideo runs its loop wrap in widened arithmetic so a past-u64 delta lands on the exact residue instead of a saturated one; pinned along with the paused-stream and pause-clears-buffering shapes.
* Spill the replayed cascade-resolution queue past its inline capacity
- Loads per dispatch are unbounded by contract and every .video_load record lands before the dispatch's event, so the replay-side queue now grows geometrically past its inline 64 (freed when it empties and at deinit) instead of refusing the 65th record as a false divergence - the pending stages' non-lossy discipline.
- Pinned by a record/replay burst one past the inline capacity on an assets-absent host.
* Cross-check the journaled key on every replayed video pairing
- Reminted tokens pair records with replayed loads by position; the journaled key now proves the load at that position is the one the recording issued - feedVideoRecord refuses a mismatch as divergence, and a cascade resolution consumed under a different key latches one for the finish check.
- Replay now ends with a consistency check (the .finish replay control): a structurally valid journal whose queued cascade resolutions the replayed timeline never claimed fails as ReplayEffectDivergence instead of reporting success.
- Pinned at the channel level: wrong-key feeds refuse, an unclaimed or misclaimed resolution fails finishReplay, and the honest pairing stays silent.
* Copy the captured video declaration and verify the load bijection under replay
- The main-canvas <video src> capture now copies the source out of the build arena into app-owned storage (the slot captures' rule): a later failed rebuild resets the arena the old capture borrowed, and a window-close reconcile could hash or load overwritten bytes.
- Every non-rejected real load now journals exactly one .video_load record - refused cascades included (failVideoLoad) - so the replayed loads and the journaled records form a bijection: a replayed load with no record at its position, and a record whose position was never issued, both latch divergence for the finish check.
- The journal semantic epoch bumps to 2: identical bytes, changed replay meaning - an earlier recording's failed loads would replay as false divergence, so old journals refuse with the re-record teaching. Pinned: the extra-load shape fails finishReplay and the captured src aliases the app-owned buffer.
* Run the replay finish check even when no records were fed
- A latched video-load divergence can exist with zero fed effect records (the recording journaled nothing; the replayed timeline loaded anyway), so the end-of-journal consistency check now runs unconditionally; hookless apps answer ReplayUnsupported, which is honestly nothing-to-check.
- Pinned: a zero-record recording replayed into a build that declares a video fails as ReplayEffectDivergence instead of reporting success.
* Refuse non-http(s) schemes at the videoLoadUrl seam
- The seam documents streaming-only and promises to reject bad arguments before the platform is asked, but forwarded any scheme - a direct caller could hand file:///... to a host whose media stack opens it (AVPlayer does); the scheme gate now holds at the seam for every caller, matching the engine's own load validation.
- Pinned: file:, ftp:, and unparseable URLs refuse as InvalidVideoOptions without reaching the platform; https passes.
* Retire the null platform's video player before the load probes refuse
- The macOS host stops the previous player before the file-existence probe, so a refused load leaves no playback behind; the null platform now models the same ordering in videoLoad and videoLoadUrl instead of returning early with the replaced player still emitting frames and events under its old token.
- Pinned: a VideoSourceNotFound load leaves nothing loaded, no pending acknowledgment, and no position ticks.
* Fail frame-less video loads and keep the seek mirror on platform truth
- An asset with no video geometry (an audio-only file loaded as video) now fails the load instead of acknowledging a playback that can never paint - sound over a permanently blank surface is the same broken promise as the conversion-buffer failure the attach path already refuses.
- seekVideo applies the platform seek before moving the mirror: a player the host already retired (a completed non-looping playback) refuses the call, and the snapshot and house slider must stay on the frame actually on the glass; pinned in the post-completion transport test.
* Refuse post-completion seeks on the fake video channel too
- A non-looping natural end retires the platform player (retire-before-emit), so a later seek refuses live and the mirror keeps the terminal position; the fake executor now latches the completion and refuses identically, so replayed chrome and snapshots land exactly where the recording left them instead of scrubbing a player that no longer exists.
- Pinned by a record/replay parity test: a scrub after the natural end keeps position at the duration on both sides, model and fingerprints identical.
* Pair replayed video deliveries from any stage position; stop cancels the TS stream
- The replay pairing in takeVideoMsg now scans the pending stage for the event's fed entry instead of checking only the head: a regenerated loop-side rejection staged earlier in the same dispatch keeps its own drain-time order and no longer reverses the recorded Msg order; pinned by a record/replay ordering test.
- VideoMsgFn may return null - the adapter tier's cancel gate - and the TS bridge uses it to honor Cmd.videoStop's wire contract (stop CLOSES the stream, no events for the key after this): a staged synchronous terminal from the very batch that stopped the stream drains through the engine but never reaches the app, and reopening the tag lifts the latch.
- The house videoMsg constructor never returns null, so Zig-native apps keep the one-terminal-per-load delivery whole; the bridge pin now asserts the swallow and the reopen.
* Cancel the key's staged video answers inside the engine at TS stop
- Cmd.videoStop's cancel now runs where the answers live: stopVideoCancel removes every staged-but-undrained entry for the key before the channel goes idle, so nothing for a stopped stream can reach the app - even the synchronous terminal of a load-fail-stop batch, and even when a later load reuses the same event tag before the drain (a bridge-side tag latch could not tell those generations apart).
- A cancelled answer never journals, so replay regenerates and cancels the same entries and the timelines stay identical with no extra machinery; the VideoMsgFn seam returns to plain Msg and the Zig-native stopVideo keeps its one-terminal-per-load delivery.
- The bridge pin now covers the same-tag reopen-before-drain shape.
* Mirror synchronous load refusals under replay; token-scope ownership and cancel
- The .video_load record's video_kind now carries the load's outcome: a refusal reset the live channel before loadVideo returned, so the replayed fake load resets at the same instant (parking its identity for the journaled terminal) and a snapshot an update reads inside the very dispatch matches the recording's; semantic epoch bumps to 3 (same bytes, the field gained meaning). Pinned by a record/replay probe test.
- Declarative video ownership now rides the load token the reconciler captured at its own load, never the derived key alone - the key is a pure function of the source string, and a manual load carrying it must survive declaration removal and flag deltas untouched; pinned.
- stopVideoCancel is token-scoped: the stopped stream is the latest accepted load, so a replaced predecessor sharing the arm's public key keeps its owed terminal - only stop cancels; pinned at the channel level.
* Refuse post-completion play on the fake video channel too
- A non-looping natural end retires the platform player, so play meets an absent player live: one .failed terminal and the channel resets before playVideo returns; the fake executor now models the same refusal (failVideoChannel on plain fake; under replay just the identity park and reset, because the journaled terminal delivers itself), so a snapshot an update reads right after its own play answers the same on every executor.
- Pinned by a record/replay probe test: the mid-dispatch snapshot reads inactive on both sides, the terminal delivers at its recorded wake, and models and fingerprints match.
* Break the video timer retain cycles and gate video-load outcomes at replay
- Both video timers (the 1/60 pixel clock and the 500ms readout) are now weak-host block timers that self-invalidate when the host is gone: a target-selector repeating timer retains its target through the run loop, so a host destroyed mid-playback without an orderly stop could never dealloc - leaving AVPlayer, the timers, and the conversion buffer permanently retained.
- Replay refuses a .video_load record whose video_kind is neither .loaded nor .failed as a damaged journal: the recorder writes exactly those two outcomes, and anything else would steer the replayed fake load into a state the recording never had; pinned by a hand-patched-journal test.
* Commit the video declaration at install; provenance-gate video rejections
- The main-canvas <video src> capture now stages during the build and commits only when the rebuild installs: a build that fails downstream never mounted, and the retained tree on the glass still shows the old declaration - a later reconcile acting on the unmounted capture would stop or replace a playback the presented tree still declares.
- Replay refuses a .video record whose kind and token are recorder-impossible: rejections stamp token 0 (a refused load never minted one) and every delivery carries its minted token, so a .rejected re-stamped onto a delivered record can no longer slip through the regeneration skip and silently omit the recorded Msg; pinned by a hand-patched-journal test.
* Fail the replay finish check on undelivered fed video results
- A fed record is one recorded delivery, and the event that consumed it live follows it in every honest journal - so a fed entry still staged when the journal ends means truncation or hand-editing, and finishReplay now refuses instead of reporting success; regenerated loop-side answers may honestly outlive the last drain on both timelines and stay exempt.
- Pinned: a fed-but-undelivered result fails the finish check and the same result delivered makes it silent again.
* Restart a finished playback from Play instead of failing it
- A non-looping natural end retires the player, so Play answered with one failed event and the seek was guaranteed to spring back - a broken response to a valid finished state. Effects.restartVideo is the resume path: a fresh load of the channel's own remembered source with autoplay, keeping key, surface, handler, and the loop and mute flags, journaling like any load.
- The house transport's toggle restarts when the completion latch is set and its scrub disables (Play is the live affordance); the snapshot and chrome state expose completed so custom players can do the same - the video-player example's Play now restarts its finished clip.
- Pinned: clicking the house toggle after the natural end issues a fresh platform load and the playback runs again.
* Validate replayed video payloads and source shapes; retire the example's dead seeks
- The replay pairing now requires the fed record's payload to equal what the platform event resolves to: the recorder journals every delivery verbatim from that event, so an altered kind or scalar around an intact identity refuses as divergence instead of handing the app a Msg the mirrors contradict; pinned by a hand-patched-journal test.
- A journaled cascade resolution must be one the load's request shape could select (.local needs a path, .stream needs a url); an impossible pairing latches divergence for the finish check, pinned at the channel level.
- The video-player example's seek-family controls (slider, back, forward) disable after a natural completion - a retired player refuses seeks and the thumb would spring back; Play stays live and restarts.
* Document the video output's vended-size contract at the frame tap
- The pixel-buffer width/height attributes are client requirements AVPlayerItemVideoOutput satisfies by scaling every vended frame, not hints: a 3840x2160 H.264 source tapped with fitted 1866x1050 attributes vends 1866x1050 buffers, so a 4K playback never hits the oversized-frame drop - that guard is defense in depth against a hypothetical non-conforming host, and the comment at the attributes site now says so.
* Keep declared ownership across restart; saturate past-window seeks
- The house transport's restart mints a fresh load identity, so the declarative reconciler re-captures its ownership token when the finished playback was its own - removal and same-source flag deltas keep working after a replay-from-end; pinned in the restart test.
- The TS bridge no longer rewinds a finite seek past the exact-integer window to zero: it saturates just below the window, where the engine's duration clamp lands it at the end - an oversized forward seek means the end, and only NaN and negatives (not millisecond offsets at all) seek to 0; pinned at the bridge level.
* Class non-finite seeks as invalid offsets, never oversized forward seeks
- Infinity reached the past-window saturation branch and sought an active video to its end, though the literal validation rejects non-finite offsets: the saturation now requires a FINITE value, so Infinity seeks to 0 like NaN and negatives - not millisecond offsets at all.
- Pinned at the bridge level beside the finite past-window seek.
* Keep the reserved video surface id inside the f64 exact window
- Surface ids ride the TS wire as f64, and a source-less <video> with custom controls is fed by the app's own Cmd.videoLoad naming exactly the reserved id - the old value sat near 2^63, so the bridge's exact-integer validation rejected it and the element could only ever show its placeholder from TypeScript.
- The id moves below 2^53 (mnemonic preserved, bit 63 clear, still a valid producer id reserved by convention); pinned: in the window, outside the derived-texture namespace, and round-trips the f64 wire unchanged.
* Deterministic seek mirror, journaled handler presence, and baked rotation
- The seek mirror now gates on the deterministic completion latch on every executor: a completed playback keeps its terminal position live and replayed alike (the platform is not even asked - the retired player could only refuse), and residual platform verdicts are fire-and-forget so an exotic host's answer can never diverge replay's mirrors from the recording's.
- Every .video record journals whether its delivery dispatched a Msg, and feedVideoRecord requires the replayed handler presence to agree - a record whose Msg silently vanished, or a Msg live never dispatched, refuses as divergence instead of a silent consume.
- Rotated media renders upright: a track with a non-identity preferredTransform gets a properties-of-asset video composition baked into the vended frames (verified against a portrait-flagged H.264 asset: the raw tap vends encoded orientation, the composition vends display orientation); identity-transform assets skip the render pass and track-less streams have no transform to bake.
* Repaint the chrome when the reconcile moves playback; seek on slider key steps
- A src change loading an autoplaying replacement reconciles after the build installs, so the just-installed chrome advertised the OLD transport state while its control acted on the new one - Play on the label, pause in effect. The rebuild now runs one guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point. Pinned: the replacement's pause glyph shows in the same build.
- Keyboard and assistive steps on the house seek slider arrive as set_value intents with no widget change event behind them; they now map their fraction onto the duration and drive the channel like the pointer scrub instead of being consumed silently for the next tick to snap back. Pinned with a focused arrowright step.
* Repaint secondary-window chrome when its own reconcile moves playback
- The slot rebuild reconciles its captured declaration after the slot tree installs, so a first mount of an autoplaying <video controls> in a secondary window rendered its chrome from the still-inactive snapshot - disabled Play over a playing video - until some later platform event, seconds away on a slow stream. The slot path now runs the main rebuild's guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point.
- Pinned: the slot's first installed build shows an enabled pause toggle.
* Latch missing video records; scope bridge verbs to their own stream
- A recorded video event arriving under replay with a handler bound and no fed record before it now latches divergence for the finish check: the recorder journals every handled delivery immediately before its event, so absence is truncation or hand-editing - never a Msg to drop silently; pinned at the channel level.
- Bridge video verbs now prove ownership with the load identity their own accepted load minted (Effects.videoMintedToken): a wire key whose stream was since replaced by a load the bridge never issued - a declarative element's - no-ops its transport verbs, and its stop cancels only its own stream's staged answers while the playback on the channel survives untouched; pinned at the bridge level.
* State the replace-is-not-stop rule at the videoLoad contract
- A replaced load still delivers the terminal it owes, routed to its own event arm - the never-silent promise applies to the replaced stream's failure exactly as to any other, and only Cmd.videoStop cancels undelivered answers. The wire contract now says so explicitly where the open-or-replace semantics are defined; the bridge's routing and the arm-separation pin already enforce it.
* Remember volume across a failed load; gate impossible terminal payloads
- Volume is a remembered preference the next load re-applies, so the bridge's ownership gate now lets videoSetVolume through when the channel is IDLE - a failed load's handler routinely sets it before retrying, and with nobody's playback on the channel there is nothing to protect; a foreign live playback still gates it. Pinned both ways.
- Replay refuses a .video record whose payload shape the recorder never writes for its kind: terminals deliver with playing and buffering false and no dimensions, and a completion pins position to the duration - a synchronously failed load's record has no platform event behind it to cross-check, so the gate is where a hand-set width refuses; pinned by a hand-patched-journal test.
|
||
|
|
2eb9424c63 |
Native context menus on Windows and Linux; point-anchored fallback everywhere (#172)
* Anchor the context-menu fallback surface at the click point - Thread the secondary click's pointer location through the fallback request into Ui.finalize, so the synthesized surface opens at the click like a native menu instead of the target row's bottom-left corner - Give WidgetAnchor a point mode: a zero-size anchor rect at an explicit window-space point, reusing the existing flip-above and window-clamp edge rules - Cover the point mode's placement, bottom-edge flip, and horizontal clamp at the geometry level, and the click-point mount end to end on a wide row Co-authored-by: startewho <898009+startewho@users.noreply.github.com> * Present native Windows context menus through TrackPopupMenu - Mirror the tray popup discipline for the runtime's declared items: presentation defers to a fresh loop turn, TPM_RETURNCMD | TPM_NONOTIFY with SetForegroundWindow and the WM_NULL post, logical view-local request coordinates inverted through the presenting HWND's DPI scale and ClientToScreen - Selection and dismissal emit the same journaled context_menu_action payload the macOS host produces (token echo, item id, 0 for dismissal), so session replay stays shape-identical across platforms - Flip .context_menus to the tray's system-engine gate and unit-test the pure seam parts (item translation with separators and disabled flags, the action-event mapping); the TrackPopupMenu modal loop itself is only exercisable on a live Windows session Co-authored-by: startewho <898009+startewho@users.noreply.github.com> * Present native Linux context menus through GtkPopoverMenu - Build a sectioned GMenu from the declared items (separators split sections, disabled items ride action enablement) wired to a per-invocation GSimpleActionGroup inserted on the presenting view widget, pointed at the click with a 1x1 rect in the widget's logical coordinates - the same space the inbound pointer path reports in - Emit exactly one journaled context_menu_action per request (selection from the item action, dismissal from a one-turn-later teardown idle after the popover closes), the same token-echo payload the macOS and Windows hosts produce; refs stay balanced with weak pointers covering widgets that die while the menu is up - Flip .context_menus to the system-engine gate, keep the new code outside the WebKitGTK stub fences, and update the docs, skill, and README claims that named native context menus macOS-only Co-authored-by: startewho <898009+startewho@users.noreply.github.com> * Gate context-menu resolution on per-request tokens - Mint a fresh correlation token per presented (or automation-armed) request and check it before clearing the pending request, so a superseded menu's late dismissal can never resolve or clear its successor - even when both target the same widget. - Give each GTK popover menu a per-invocation action-group namespace: the deferred teardown of a superseded menu now removes only its own group instead of the one the replacement just inserted on the same parent. - Windows and macOS need no gate by construction (TrackPopupMenu blocks the loop thread and emits inline from a moved-out request; the macOS presentation block captures its token as a local while popUpMenuPositioningItem blocks the main queue) - noted at both fix sites. * Add the native context-menus changelog fragment - One feature fragment covering the Windows and Linux presenters, the .context_menus capability on both system-engine hosts, and the fallback surface's click-point anchoring. * Cancel pending GTK context-menu teardown idles at host destroy - Track every queued teardown idle on the host, linked through the menu states themselves: the popover's closed handler pushes the receipt the moment it exists, and native_sdk_context_menu_free unlinks on every exit path - A superseded menu's deferred teardown captures its state and the host raw while host->context_menu tracks only the current menu, so destroy now removes each pending source and frees its captured state inline instead of letting the idle fire into a freed host - The weak pointers keep covering widgets dying before the menu state; this covers the host dying, and cancelling deliberately drops the superseded request's owed dismissal since the runtime gating on that token is torn down with the host * Re-point the notes context-menu token assertions at the recorded token - Context-menu correlation tokens are minted per request and opaque, so the tests assert the null platform recorded a nonzero token instead of expecting the widget id * Escape mnemonic ampersands in Windows context-menu and tray labels - AppendMenuW treats & as a mnemonic marker, so an authored label like "R&D" rendered with the ampersand eaten and an accidental mnemonic armed; labels now cross the ABI with & doubled - The escape lives in the shared translation helper covering both the app context-menu path and the tray path, whose labels are app-supplied too; a label the pool cannot hold passes through raw rather than truncated - Pin the escaped output and the pool-exhaustion passthrough in the item-translation unit tests * Escape mnemonic underscores in GTK context-menu labels - GtkPopoverMenu treats _ in an item label as a mnemonic marker, so an authored label like "Save_As" rendered with the underscore eaten and an accidental mnemonic armed; labels now cross the ABI with _ doubled - The escape lives in the context-menu translation helper (the GTK host strndup-copies every label before returning, so the caller stack pool is safe); a label the pool cannot hold passes through raw rather than truncated, and the menu bar keeps _ untouched as its intentional GTK mnemonic convention - Pin the escaped output, the underscore-free pointer passthrough, and the pool-exhaustion passthrough in the item-translation unit tests * Map GDK pointer buttons explicitly to the runtime's ordering - GDK numbers secondary=3 and middle=2; the old subtract-one sent right clicks to the runtime as middle (never opening the menu) and middle clicks as secondary (opening it); press and release now share an explicit GDK-to-runtime switch, and the interactive-move stash keeps raw GDK numbering for gdk_toplevel_begin_move - The Linux canvas smoke now owns its Xvfb display and drives a real xdotool right-click through a task row's declared context menu, asserting the selection's Msg dispatch ('1 done') in the snapshot - ui-inbox task rows declare an honest one-item menu (Toggle done) so the smoke exercises an app-declared menu end to end * Resolve context-menu selections from a present-time snapshot - GTK popovers are asynchronous: a rebuild while the menu is open (timers or effects reordering conditional items) could redirect the visible selection through the live tree's handler table - The runtime now emits canvas_widget_context_menu_shown after a native present; UiApp snapshots the shown items' dispatch Msgs keyed by the request token, and a token-matching selection resolves from that snapshot on every platform - never the rebuilt tree - The automation verb and the fallback surface keep live-tree resolution: both validate against the tree they show * Bump the automation protocol to v8 for per-request menu tokens - Recorded context_menu_action tokens changed from widget ids to per-request generations; a v7 journal's selections would be silently swallowed by the token gate instead of refused - The version handshake now turns that into the loud preamble mismatch, and the changelog fragment states the break deliberately * Deep-copy context-menu snapshot slice payloads at present time - The present-time selection snapshot copied MsgT by value, so a menu held open across two rebuilds dispatched slice payloads pointing into reset build-arena storage - Snapshot Msgs now deep-copy every reachable slice into a token-scoped arena, released on resolve, supersession, and teardown - Regression test presents a menu over an arena-derived payload, rebuilds twice with sentinel bytes, and asserts the dispatched Msg carries the bytes the user saw * Drive the smoke's context-menu popover by pointer under Xvfb - Xvfb has no window manager, so the GTK popover never receives keyboard focus and xdotool Down/Return died on the canvas beneath it; the menu's shown event fired but no selection ever dispatched - Locate the popover's override-redirect X window by diffing the root's children across the right-click (topmost new viewable window) and click its center — the row declares exactly one arrowless item, so the center is the item - Failure diagnostics now dump the X window list alongside the snapshot and app log; the workflow installs x11-utils for xwininfo * Sharpen the snapshot copy: const slices only, exact types, loud OOM - Mutable slice payloads pass by reference (update may write through app-owned storage; a copy would swallow the writes), while const slices copy with their declared alignment and sentinel and error unions recurse into successful payloads - An out-of-memory copy now logs which item it disarmed instead of failing silently - Direct unit test pins the copy semantics per shape; the integration test drives the error-union arm through the two-rebuild race * Refuse un-copyable context-menu payload shapes with teaching errors - A mutable slice's aliasing is unknowable to the deferred snapshot (arena bytes must be copied, model-owned storage must not be), so it is a compile-time teaching error instead of a silent guess either way - Slice-bearing fixed arrays are refused too: an array's length says nothing about which elements are initialized, so walking a count-plus-buffer payload would interpret its undefined tail as slices; slice-free arrays and untagged unions still pass by value as plain bytes - typeCanReachSlice gates the refusals so only genuinely un-copyable shapes are rejected; the unit test now covers the scalar count-plus-buffer shape with an undefined tail * Scope the snapshot copy to const slices with a hop budget and a loud fallback - The copier now walks only the blessed arena payload shape (const slices through structs, tagged unions, optionals, and error unions); mutable slices, fixed arrays, untagged unions, and non-slice pointers pass through under the deferred-Msg rule that their storage outlives the menu, so no payload shape is a compile error - A slice-hop budget bounds the walk, turning cyclic or absurdly deep payload graphs into a handled error instead of unbounded recursion - A failed copy (out of memory or over budget) keeps the visible item live: it dispatches the uncopied value with a warning naming the item, never a silent no-op * Pin the presenting build's arena while its context menu is open - Replace the snapshot payload copier outright: while a presented menu's token-keyed snapshot is armed, the arena generation that built the presented tree is exempt from the rebuild reset, so selection dispatches the ORIGINAL Msg value - same bytes and same pointer identity as the fallback surface and the automation verb, with no payload-shape restrictions, no copy allocation after present, and no recursive walk - A new canvas_widget_context_menu_dismissed runtime notice releases the pin (and disarms the snapshot) when the menu closes without a selection; selection and supersession release it as before, and growth while a menu is open is bounded by its open span - The regression test now also asserts pointer identity end to end and the pin's release on selection and dismissal * Key the menu pin by window identity and double-buffer rebuilds under it - The pin names its canvas by stable window identity, never slot index: removing a window swap-moves another slot into its place, and an index-keyed pin would protect the wrong arena; the pin-owning window's teardown now releases the snapshot and pin before its arenas deinit - While pinned, every rebuild of that canvas routes through the partner arena on its normal reset cadence (the consecutive-build pattern the clearance retry already runs), so memory under an open menu holds at two trees regardless of rebuild count - ANY superseding presentation or dispatch releases the previous app-menu request - a default edit/copy menu and the automation verb's direct dispatch now send the dismissed notice, not just app-over-app presentations - Tests: pin follows its window across slot compaction (bytes and address), window removal releases, cross-kind and automation supersession release, and capacity stays flat across 14 rebuilds under an open menu * Name the superseded menu's view in its dismissal and commit the successor first - The pending request now carries a bounded copy of its view label, so the superseded-menu dismissal notice names the canvas it was presented on instead of an empty label (raw apps tracking per-canvas menu state need the correlation, and the view may already be gone) - The replacement pending commits before the fallible dismissed-notice dispatch: if an app handler errors on the notice, the runtime's expected token still matches the menu the platform accepted, so the successor stays selectable - Runtime-level test pins both: the notice carries the old token and the real view label, and the successor menu resolves after the supersession * Keep the automation menu verb's synthetic selection ahead of a failing dismissal notice - The widget-context-menu verb captures the superseded-menu dismissal notice's error and re-raises it only after the synthetic selection dispatches: unlike a presented menu, whose outcome the platform delivers later regardless, that dispatch is the armed request's only outcome, so no error path may leave a pending token with no presented menu and no delivered outcome - The notice keeps its place in the event order (the old menu's dismissal before the successor's outcome), and its error still surfaces after the runtime's bookkeeping settles - Test pins the supersession under an erroring dismissal handler: the selection still dispatches, the error still propagates, and no pending request survives * Keep menu state sound when rebuilds fail or handlers close views - A rebuild routed into the live tree's arena under an open menu's pin now drops the tree reference when the pass fails after its reset: handlers go quiet until the next successful rebuild instead of dangling into reset, partially rewritten storage, and the pinned snapshot still resolves the presented payload - The superseded-menu dismissal notice runs arbitrary app code that can close views and compact their indices, so the shown event and the automation verb's synthetic selection now name their view from the committed request's own copy (showMenu returns it) instead of re-reading a cached index that a compaction can point at another view or another window's canvas - Tests pin all three seams: the failed over-budget rebuild under a pin drops then restores the tree with the presented menu intact, and both supersession paths keep naming the presenting view after the dismissal handler closes a different one * Restore a dropped tree on menu resolution and refuse a superseded automation verb - A menu resolution that dispatches no Msg (dismissal, out-of-range or unmapped-item swallow) now restores a live tree the pinned-rebuild guard dropped: no Msg-driven rebuild is coming, and without a handler table every event silently no-ops until an unrelated resize or effect rebuilds - The widget-context-menu verb now checks its freshly armed token survived the dismissal notice: a handler that synchronously presents a superseding menu replaces the pending request, so the verb refuses with ContextMenuSuperseded instead of reporting success while the token gate swallows its synthetic action - Tests pin both: a dismissal after the failed over-budget rebuild rebuilds the tree once the model fits again, and the superseded verb errors by name while the handler's successor menu stays resolvable * Announce nothing for a presentation superseded during its own notice - showMenu now reports a three-way outcome: shown, refused, or superseded — the dismissal notice's app code can synchronously present a successor menu that replaces the freshly committed request, and a late shown event for it would overwrite the successor's snapshot with a token the action gate no longer accepts, stranding the stale pin - A superseded presentation announces nothing and never falls back to the anchored surface, which would mount a second menu under the successor's native one - Test pins the chain: menu A superseded by B whose notice presents C — three presentations, two announcements, the last carrying C's token, and C resolves normally * Release the menu pin before its selection dispatches and refuse a closed-view verb - A snapshot selection's Msg is stored by value and its pinned-arena payloads are consumed by update itself, so the pin now releases before the dispatch: the rebuild routes into the partner arena naturally, and a Msg whose update breaks a build budget fails the rebuild without resetting the live arena — input keeps working on the previous tree and the app's controls can recover the model - The widget-context-menu verb revalidates its target view after the dismissal notice: a handler that closed it leaves a request that can never resolve, so the verb disarms it and refuses with ContextMenuViewClosed instead of reporting success while the action dispatch silently drops the selection - Tests pin both: the poison selection leaves the tree live and a real click on the recovery control rebuilds in budget, and the closed-view verb errors by name with no orphaned token * Disarm a presentation whose view died mid-notice and restore the tree before menu resolution - showMenu now rechecks the presenting view after the dismissal notice: a handler that closed it leaves a request whose action can never deliver (or never arrive), so the request disarms with a view_closed outcome — never announced, never the anchored fallback on a dead view - Arming a shown snapshot and resolving a snapshot-less selection both restore a live tree the pinned-rebuild guard dropped: a menu presented while the model was unbuildable still resolves once the model recovers, instead of falling through a null tree and dispatching nothing - Tests pin both: the closed-view presentation stays silent, its token inert, a fresh present arms cleanly — and the snapshot-less selection dispatches the restored build's payload --------- Co-authored-by: startewho <898009+startewho@users.noreply.github.com> |
||
|
|
b47111069c |
Native SDK: the complete toolkit for building native desktop apps (#67)
zero-native becomes the Native SDK. Apps are authored as native markup plus Zig on a deterministic runtime and rendered by the toolkit's own engine into real OS windows — no browser, no WebView, no interpreter in the binary. - Desktop is complete on macOS, Windows, and Linux: native rendering with per-platform titlebar fidelity, audio playback with streaming, a verified track cache, and real spectrum analysis, native context menus, packaging with sealed code signing, and a deterministic automation and record-replay story. - Experimental iOS and Android host tiers ship behind the same app manifest: simulator and emulator dev loops, archive-ready packaging, real platform tab bars and push navigation, with embedding over the C ABI underneath. - The docs site, component catalog, theme packs, showcase apps, and CHANGELOG carry the full account. |