feat(nodes): extract Slint overlay into standalone video::slint node#237
feat(nodes): extract Slint overlay into standalone video::slint node#237staging-devin-ai-integration[bot] wants to merge 13 commits intomainfrom
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Add a new video source node (video::slint) that renders .slint UI components to RGBA8 frames via the software renderer. The node has no inputs and a single broadcast output, following the same lifecycle as colorbars (Ready → Start → Running). Key design decisions: - All Slint operations run on a single shared std::thread (lazily slint::platform::set_platform() is process-global. - Each node gets a unique NodeId (UUID) and communicates with the shared thread through tagged work items and per-node result channels. - A thread-local RefCell<Option<Rc<dyn WindowAdapter>>> is swapped before each definition.create() / component.show() call so every ComponentInstance is associated with its own MinimalSoftwareWindow. - The new 'slint' feature does NOT depend on 'compositor', keeping the two fully decoupled. - Properties can be set at init and updated at runtime via UpdateParams. - Property keyframes cycle at a configurable interval. - frame_count == 0 gives infinite real-time output; frame_count > 0 produces exactly N frames then stops. Sample pipelines updated to use standalone slint nodes wired as compositor inputs instead of embedding slint_overlays in compositor config. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
9769088 to
b079b13
Compare
| fn validate_slint_asset_path(path: &str) -> Result<(), String> { | ||
| if path.contains("..") || !path.starts_with("samples/slint/") { | ||
| return Err(format!( | ||
| "Invalid slint_file: must start with 'samples/slint/' and not contain '..': {path}" | ||
| )); | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
📝 Info: Path validation does not constrain Slint compiler import resolution
validate_slint_asset_path at line 178 ensures the initial slint_file path starts with samples/slint/ and contains no ... However, the Slint compiler's build_from_path resolves import directives within the .slint file, which could reference files outside the sandbox via absolute paths or relative paths that resolve beyond samples/slint/. This follows the same limitation as compositor/overlay.rs:48 (validate_asset_path) which validates the initial path but cannot control downstream file I/O. For the current use case (server-controlled .slint files in the repo), this is acceptable, but it's worth noting if user-supplied .slint files are ever supported.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
There was a problem hiding this comment.
Acknowledged. The trust boundary here is filesystem access — .slint files must be placed under samples/slint/ by someone with write access to the working directory. Slint's build_from_path does follow import directives, so a malicious .slint file could reference paths outside the directory. This is acceptable for the current use case (server-side rendering of trusted assets) but worth documenting if the feature ever expands to user-uploaded content. I'll add a brief comment to validate_slint_asset_path noting this limitation.
…rde default to slint_file - Switch result channel from std::sync::mpsc to tokio::sync::mpsc so the async run() method uses .recv().await instead of blocking the tokio worker thread. The Slint thread side uses blocking_send(). - Add #[serde(default)] to SlintConfig.slint_file so partial UpdateParams JSON (e.g. only properties) deserializes correctly instead of silently failing. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
|
|
||
| // Pump Slint's internal animation timers so time-based animations | ||
| // (e.g. slide-in transitions) advance on each tick. | ||
| slint::platform::update_timers_and_animations(); |
There was a problem hiding this comment.
📝 Info: update_timers_and_animations is process-global but clock-based, safe for multi-node
The slint::platform::update_timers_and_animations() call at line 316 is invoked for EVERY Render work item from ANY node. Since it's process-global, calling it for node A also advances timers for node B's component. However, this function is clock-based (uses wall-clock elapsed time), not call-count-based. Calling it multiple times in quick succession simply updates to the current time — the second call sees near-zero elapsed time. So multiple nodes rendering on the shared thread won't cause timers to run faster than real time.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
There was a problem hiding this comment.
Acknowledged. update_timers_and_animations() is indeed process-global, so it runs N times per frame cycle when N nodes are active. Since Slint timers are wall-clock-based, the extra calls should be benign — the timer callback fires based on elapsed real time, not call count. The scoreboard.slint Timer increments elapsed per tick, but the tick interval is set to 1s, so additional calls to update_timers_and_animations within the same second won't cause extra timer firings. Worth verifying with real multi-node testing, but not a correctness issue in practice.
…f replacing whole config
Partial UpdateParams JSON (e.g. {"properties": {"score": 5}}) would
silently fail because the deserialized SlintConfig had an empty slint_file
which failed validate(). Now UpdateParams merges only the runtime-changeable
fields (properties, property_keyframes, keyframe_interval) into the existing
config, preserving immutable init-time fields.
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
… UpdateParams
Partial UpdateParams JSON (e.g. {"properties": {"home_score": 4}})
was replacing the entire properties map, dropping unmentioned keys.
Now uses extend() to merge only the provided keys.
property_keyframes and keyframe_interval are treated as init-time config
and left unchanged by merge_update, since serde defaults make it
impossible to distinguish absent fields from user-provided empty values.
Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
The scoreboard's Slint Timer is wall-clock-based and doesn't advance meaningfully in batch mode (frame_count > 0) where frames render faster than real-time. Replace the oneshot demo with a static watermark badge that works correctly in both batch and real-time modes. The scoreboard remains available in the dynamic pipeline (video_moq_slint_scoreboard.yml) where frame_count: 0 ensures real-time pacing and timers work as expected. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
| fn validate_slint_asset_path(path: &str) -> Result<(), String> { | ||
| if path.contains("..") || !path.starts_with("samples/slint/") { | ||
| return Err(format!( | ||
| "Invalid slint_file: must start with 'samples/slint/' and not contain '..': {path}" | ||
| )); | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
📝 Info: Path validation does not restrict Slint import statements
validate_slint_asset_path at crates/nodes/src/video/slint.rs:165-172 validates the top-level .slint file path (must start with samples/slint/, no ..). However, the Slint compiler's build_from_path (line 394) resolves import statements within the .slint file, which could reference paths outside the sandbox (e.g., import { Foo } from "/etc/some_file"). This is a defense-in-depth concern rather than a practical vulnerability because: (1) an attacker would need write access to place a crafted .slint file inside samples/slint/; (2) the Slint compiler only parses .slint syntax, it won't expose arbitrary file contents; (3) the worst case is a compilation error from invalid imports. Still, if the threat model evolves, consider using Compiler::set_include_paths to restrict import resolution.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
Without an explicit request_redraw(), MinimalSoftwareWindow's draw_if_needed() only invokes the render closure when the window is marked dirty. For static UIs (no property changes, no active timers) the window stops being dirty after the initial show(), causing draw_if_needed() to skip rendering and leave the pixel buffer zeroed (fully transparent). This made overlays like the watermark invisible. Calling request_redraw() before every draw_if_needed() guarantees a full repaint each frame. With RepaintBufferType::NewBuffer there is no wasted incremental-diff cost — the renderer always paints the entire scene anyway. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Static UIs and frames between keyframe boundaries now reuse the previously rendered RGBA8 buffer instead of re-rendering through the Slint software renderer and premultiplied-to-straight conversion on every frame. Re-rendering is triggered only when: - The node is first registered (dirty = true) - An UpdateConfig work item arrives (runtime property change) - The keyframe index crosses a boundary The frame counter still advances on cached frames so keyframe boundaries are detected at the correct time. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
…d frames - Remove invalid 'vertical-alignment' property from Rectangle in watermark.slint — this caused Slint compilation to fail silently, producing no frames at all (the actual root cause of the invisible watermark). - Cache rendered RGBA8 buffer on the shared Slint thread. Static UIs and frames between keyframe boundaries now reuse the previously rendered buffer instead of re-rendering on every frame. Re-rendering triggers only on: first frame, UpdateConfig, or keyframe boundary. The frame counter still advances on cached frames so keyframe boundaries are detected at the correct time. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
- Wrap accent dot in a VerticalLayout with alignment:center so it is vertically centred within the badge row (previously top-aligned). - Shrink badge to 180×44 px for a tighter, cleaner look. - Slightly increase border-radius and padding for better proportions. - Use semi-bold LIVE tagline with higher-contrast white-70% colour. - Update pipeline YAML to match new badge dimensions (x=1080). Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
…rm errors Critical fixes: - Always call update_timers_and_animations() unconditionally in the Render handler so Slint Timer/animation state advances even when frames are served from cache. - Add static_ui config flag (default false) to opt into frame caching for truly static UIs; dynamic UIs always re-render every frame. - Replace blocking_send with try_send on the result channel to avoid head-of-line blocking on the shared Slint thread when one node's consumer is slow. Suggestions: - Propagate set_platform() error instead of silently discarding it. - Clear CURRENT_WINDOW thread-local after component.show() to prevent stale Rc references. - Replace spin_on (busy-wait) with pollster::block_on for .slint compilation; yields to the OS instead of burning CPU. - Document merge_update limitation in dynamic pipeline YAML comments. Nits: - Add #[serde(default)] at struct level on SlintConfig (matches ColorBarsConfig pattern). - Simplify registration to use SlintConfig::default(). - Change SAFETY comment to Invariant: (SAFETY is for unsafe blocks). Watermark pipeline updated with static_ui: true. Signed-off-by: StreamKit Devin <devin@streamkit.dev>
Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
| }] | ||
| } | ||
|
|
||
| #[allow(clippy::too_many_lines)] |
There was a problem hiding this comment.
🟡 Lint suppression #[allow(clippy::too_many_lines)] lacks required rationale comment (AGENTS.md violation)
AGENTS.md mandates: "Do not blindly suppress lint warnings or errors with ignore/exception rules. Instead, consider refactoring or improving the code to address the underlying issue. If an exception is truly necessary, it must include a comment explaining the rationale." The #[allow(clippy::too_many_lines)] at line 649 has no accompanying comment. Some other lint suppressions in this file do include comments (e.g., crates/nodes/src/video/slint.rs:263 has // Receiver must be owned by this thread), but this one does not.
| #[allow(clippy::too_many_lines)] | |
| #[allow(clippy::too_many_lines)] // Node run loop follows Ready→Start→RenderLoop→Cleanup lifecycle; splitting would fragment the linear control flow without reducing complexity |
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
| // Unregister from the shared thread so it can drop our instance. | ||
| let _ = thread_handle.work_tx.send(SlintWorkItem::Unregister { node_id }); |
There was a problem hiding this comment.
🚩 Shared Slint thread: node state leak on task cancellation
If a node's tokio task is cancelled (dropped mid-await) rather than exiting cleanly, the cleanup code at crates/nodes/src/video/slint.rs:855 that sends SlintWorkItem::Unregister never executes. The NodeState (including the SlintInstance with its Rc-based window and component) would remain in the shared thread's nodes HashMap indefinitely. The shared thread does have a partial cleanup path via TrySendError::Closed at line 365-367 — if a subsequent Render work item arrives after the receiver is dropped, the node is removed. However, if no Render is pending when the task is cancelled, the state leaks until process exit. A Drop guard or scope-exit pattern could ensure Unregister is always sent.
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
Port the Slint renderer from PR #237 into a standalone native plugin using the NativeSourceNode trait from PR #238. This keeps slint and slint-interpreter as plugin-only dependencies, avoiding committing them to the core workspace. The plugin renders .slint files to RGBA8 video frames at configurable resolution and frame rate. All Slint operations run on a shared dedicated thread with channel-based message passing to handle the non-Send constraint of Slint types. Includes: - Plugin crate with config, shared thread, and NativeSourceNode impl - justfile build/lint/fix/copy targets - CI format check and clippy in lint-simple job - Marketplace metadata (plugin.yml) and regenerated official-plugins.json - Docs reference page and updated plugin index (9 -> 10) - Sample .slint files (watermark, scoreboard, lower_third) - Sample oneshot pipeline using plugin::native::slint Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
Replace simplified steps-based oneshot pipeline with the full compositor pipeline from PR #237 (colorbars + watermark overlay → compositor → VP9 → WebM → http_output). Add the missing dynamic pipeline (scoreboard + lower-third overlays composited onto colorbars, streamed via MoQ). Update docs example to match the correct node-based format. All pipelines use plugin::native::slint instead of video::slint. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com>
|
Closing in favor of plugin |
* feat: add native Slint UI plugin Port the Slint renderer from PR #237 into a standalone native plugin using the NativeSourceNode trait from PR #238. This keeps slint and slint-interpreter as plugin-only dependencies, avoiding committing them to the core workspace. The plugin renders .slint files to RGBA8 video frames at configurable resolution and frame rate. All Slint operations run on a shared dedicated thread with channel-based message passing to handle the non-Send constraint of Slint types. Includes: - Plugin crate with config, shared thread, and NativeSourceNode impl - justfile build/lint/fix/copy targets - CI format check and clippy in lint-simple job - Marketplace metadata (plugin.yml) and regenerated official-plugins.json - Docs reference page and updated plugin index (9 -> 10) - Sample .slint files (watermark, scoreboard, lower_third) - Sample oneshot pipeline using plugin::native::slint Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: port sample pipelines from PR #237 with plugin node kind Replace simplified steps-based oneshot pipeline with the full compositor pipeline from PR #237 (colorbars + watermark overlay → compositor → VP9 → WebM → http_output). Add the missing dynamic pipeline (scoreboard + lower-third overlays composited onto colorbars, streamed via MoQ). Update docs example to match the correct node-based format. All pipelines use plugin::native::slint instead of video::slint. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: rename output pin to 'out' and add fontconfig dep to marketplace CI - Rename plugin output pin from 'video' to 'out' to match the convention used by all built-in nodes. The pipeline compiler's 'needs' syntax defaults from_pin to 'out', so using a different name broke graph wiring. - Add libfontconfig1-dev to marketplace-build.yml system deps so the Slint plugin can be compiled by build_official_plugins.sh. - Update docs to reflect the pin name change. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix: support parameterless construction for source config probe The host probes source plugins by calling create_instance with no params to read source_config(). The Slint plugin requires slint_file which fails validation with default config. Return a lightweight probe-only instance when params is None so the host correctly detects is_source=true and uses run_source() instead of run_processor(). Without this fix the watermark node was treated as a processor, crashed immediately (no input pins), and the compositor produced output with only colorbars (no overlay). Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(plugin/slint): address review findings — scope guard, clone, naming, validation - Add ClearWindow scope guard so CURRENT_WINDOW thread-local is cleared even if definition.create() or component.show() fails via early return - Remove unnecessary data.clone() in static-UI render path — store first, then clone from cache (saves ~3.7 MB allocation per cache-miss render) - Rename lib from slint_plugin to slint, matching the naming convention of all other native plugins (whisper, kokoro, vad, etc.) - Add MAX_DIMENSION (7680 / 8K) upper bound on width/height to guard against config typos that would attempt multi-GB buffer allocations - Reject absolute paths in validate_slint_asset_path() as defense-in-depth - Document update_timers_and_animations() idempotency for multi-instance usage on the shared Slint thread Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(compositor): frame-aligned sync in oneshot mode prevents asymmetric drain Cherry-picked from PR #238 branch (84364c6). In oneshot mode, verify all active slots have pending frames before dequeuing any, so fast sources aren't consumed ahead of slower ones. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(plugin-native): re-query source config from live instance for per-instance max_ticks The load-time probe creates a default instance (null params) to detect source plugins, so max_ticks is always 0 (infinite). Per-instance params like frame_count: 300 were never applied to the tick loop limit. Now run_source() re-queries get_source_config() on the live instance after it's created with actual params, so the tick loop respects the configured frame_count. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * style: format wrapper.rs Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(plugin-native): use if-let instead of match for clippy single_match_else Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(plugin-native): avoid begin_call() leak when get_source_config is None Split the tuple pattern into chained and_then/map so begin_call() is only invoked when get_source_config is Some, preventing an in_flight_calls counter leak in the else branch. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * improve graphics * feat(client): add declarative overlay controls in client section (#240) * feat(client): add declarative overlay controls in client section Add a `controls` array to `ClientSection` so pipeline authors can declare interactive widgets (toggle, text, number, button) targeting specific node properties. The StreamView renders these controls automatically when a session is active, sending `TuneNodeAsync` / `UpdateParams` messages on interaction. Rust changes: - New `ControlType` enum and `ControlConfig` struct in `yaml.rs` - Extend `ClientSection` with `controls: Option<Vec<ControlConfig>>` - Add `name` field to `NodeInfo` for node-name validation - Lint rules: `control-unknown-node`, `control-number-no-bounds` - Register new types for TypeScript generation UI changes: - New `OverlayControls` component with toggle/text/number/button widgets - New `buildParamUpdate()` utility for dot-notation → nested JSON - Integrate `OverlayControls` into `StreamView` Sample: - Add controls to `video_moq_slint_scoreboard.yml` for scoreboard and lower-third properties Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(controls): stabilize debounce/throttle with ref pattern, add useTuneNode hook Address Devin Review feedback: 1. TextControl: store onSend in a ref so the debounce closure is stable across re-renders. Pending timers now always call the latest callback instead of leaking stale intermediate values. 2. NumberControl: apply the same ref pattern for onSend in the throttle closure for consistency, preventing similar issues. 3. Extract useTuneNode hook from useSession — a lightweight hook that only provides tuneNodeConfig without subscribing to pipeline or connection state, avoiding unnecessary re-renders in OverlayControls. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(controls): address review feedback — dead code, guards, stale state, docs 1. Remove dead isDraggingRef in NumberControl (written but never read). 2. Guard buildParamUpdate against empty/malformed paths — filter empty segments, throw on zero valid segments. Add unit tests covering single/multi-segment, empty, dot-only, double-dot, and leading/ trailing dot paths. 3. Fix ToggleControl stale checked on rapid double-click — use functional updater (setChecked(prev => ...)) with onSendRef pattern matching TextControl/NumberControl. 4. Update misleading 'stable' comment on makeSend to accurately describe that a new closure is created per render but child controls absorb this via onSendRef. 5. Move getWebSocketService() to module scope in useTuneNode since it returns a singleton — avoids a new reference on every render and keeps tuneNodeConfig deps minimal (only sessionId). 6. Document that ControlConfig.default is a UI-only hint — it seeds local widget state but is not sent to the server on mount. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * fix(controls): deep-merge partial nested updates in useTuneNode useTuneNode now reads the current nodeParamsAtom state and deep-merges the partial config before writing, so sibling nested properties are preserved. Previously, two controls targeting the same node but different nested paths (e.g. properties.home_score and properties.away_score) would clobber each other because writeNodeParams does a shallow top-level merge. Add deepMerge utility to controlProps.ts — recursively merges plain objects, replaces arrays and primitives wholesale. Includes 8 unit tests covering nested merge, array replacement, type transitions, successive merges, and immutability. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> * test(e2e): add Playwright test for overlay controls Add overlay-controls.spec.ts exercising all four control types (toggle, text, number, button). The test: 1. Selects the new 'Test: Overlay Controls' sample pipeline (colorbars → sink, no plugins/MoQ required). 2. Creates a session and verifies the Pipeline Controls section renders with correct labels and group headings. 3. Exercises each control type and asserts the correct TuneNodeAsync / UpdateParams WebSocket payload is sent: - Toggle: sends { draw_time: false } - Text: sends { label: "World" } after 300ms debounce - Slider: sends { properties: { width: 800 } } (dot-notation path) - Button: sends { reset: true } 4. Asserts no unexpected console errors. 5. Destroys the session and cleans up. Also adds data-testid='overlay-controls' to the OverlayControls component for scoped locators, and fixes a TS6 useRef() arity error. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> --------- Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-authored-by: StreamKit Devin <devin@streamkit.dev> Co-authored-by: Claudio Costa <cstcld91@gmail.com> * fix(lint): include controls in mode-mismatch-oneshot check The `controls` field is dynamic-only but was not included in the `has_dynamic_fields` check in `lint_client_section`, so a oneshot pipeline with a `controls` section would not trigger the `mode-mismatch-oneshot` warning. Add it to the check and update the warning message and doc comment accordingly. Adds a unit test verifying the warning fires for controls-only oneshot pipelines. Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-Authored-By: Claudio Costa <cstcld91@gmail.com> --------- Signed-off-by: Devin AI <devin@cognition.ai> Signed-off-by: StreamKit Devin <devin@streamkit.dev> Co-authored-by: StreamKit Devin <devin@streamkit.dev> Co-authored-by: Claudio Costa <cstcld91@gmail.com> Co-authored-by: staging-devin-ai-integration[bot] <166158716+staging-devin-ai-integration[bot]@users.noreply.github.com>
Summary
Extracts the Slint UI overlay PoC from the compositor into a standalone
video::slintsource node. The node compiles a.slintfile at init, renders RGBA8 frames at a configurable resolution/fps, and outputs them like any other video source (e.g.video::colorbars). Its output can be wired into the compositor as a regular layer.Key design decisions
SlintNodeinstances share a single dedicatedstd::thread(lazily spawned viaOnceLock).slint::platform::set_platformis process-global; Slint types are!Send(Rc-based). Each node communicates via tagged work items and per-node result channels.CURRENT_WINDOWthread-local is swapped before eachdefinition.create()/component.show()so each instance gets its ownMinimalSoftwareWindow. Cleared aftershow()to prevent stale references.static_uiconfig flag: Whentrue, frames are cached and reused until properties change (viaUpdateParamsor keyframe cycling). Whenfalse(default), every frame is re-rendered so Slint timers and animations advance correctly.try_sendinstead ofblocking_sendto avoid head-of-line blocking on the shared thread. If a node's consumer is slow, frames are dropped rather than stalling all other nodes.pollster::block_onreplacesspin_onfor.slintcompilation — yields to the OS instead of busy-waiting.set_platformerror propagated instead of silently discarded.slintfeature does not depend oncompositor. The compositor compiles cleanly without any Slint code.Changes
crates/nodes/src/video/slint.rs— standalone node withSlintConfig, shared thread, rendering internalsslintincrates/nodes/Cargo.tomlandapps/skit/Cargo.tomlslint_overlaycode from compositor (mod.rs,config.rs,kernel.rs,slint_overlay.rs)video::slintnodes wired as compositor inputssamples/slint/watermark.slint+samples/pipelines/oneshot/video_slint_watermark.yml(static overlay demo)Review & Testing Checklist for Human
just skit -- pipeline run samples/pipelines/oneshot/video_slint_watermark.yml— verify the watermark badge renders correctly on the colorbars outputUpdateParamsupdates properties as expectedstatic_ui: truecaches frames correctly (watermark) andstatic_ui: falsere-renders every frame (scoreboard)cargo build --features compositorcompiles without Slint depstry_sendframe-drop behavior under load — the channel capacity is 2; in normal operation it should never be fullNotes
Unregisterisn't sent). TheTrySendError::Closedpath provides partial cleanup when a subsequentRenderarrives, but aDropguard would be more robust. Left as a future improvement.update_timers_and_animations()is process-global and clock-based — safe for multi-node usage on the shared thread.propertiesare mergeable at runtime viaUpdateParams; init-time fields are documented as non-updatable in the YAML samples.Link to Devin session: https://staging.itsdev.in/sessions/f92d676cdb5c491faa63537281ab4db4
Requested by: @streamer45