Skip to content

perf(video): move NV12 conversion to GPU - #14

Merged
programmersd21 merged 3 commits into
programmersd21:mainfrom
Luquatic:perf/video-pipeline
Aug 11, 2026
Merged

perf(video): move NV12 conversion to GPU#14
programmersd21 merged 3 commits into
programmersd21:mainfrom
Luquatic:perf/video-pipeline

Conversation

@Luquatic

@Luquatic Luquatic commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve NV12 frames after hardware decode and convert YUV to RGB in a reusable wgpu render pass
  • remove redundant full-frame copies, honor configured preload depth, and apply decoder backpressure before GPU readback
  • present only newly due frames, enforce the live video FPS cap, and make decoder handoffs and seeks generation-safe
  • retain the existing packed RGBA fallback for unsupported formats

Performance

Measured with the same H.264 3840x2160@60 wallpaper using NVDEC on an NVIDIA RTX 5090:

Metric Before After
CPU ~100-115% ~21.7%
Steady memory ~550-670 MiB ~310 MiB
Host frame payload 33.2 MiB RGBA 12.4 MiB NV12

The standalone NVDEC probe decoded and packed 300 frames at 353.5 FPS.

Validation

  • cargo fmt --all --check
  • cargo check --workspace
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace (50 passed)
  • runtime tests: 4K60 playback, restart/restore, blank/restore, static-to-video transitions, rapid mixed-resolution video replacement, seek, pause, and resume

Scope

This optimizes SDR NV12 playback with BT.601, BT.709, and BT.2020 non-constant-luminance matrix/range handling. HDR and P010 output remain out of scope.

Summary by Sourcery

Optimize video playback by preserving NV12 frames through the decode pipeline and performing YUV-to-RGB conversion on the GPU while tightening scheduling, buffering, and seek behavior for live video.

New Features:

  • Add GPU-based NV12-to-RGB conversion pipeline and shader, with reusable video textures and YUV color matrix/range handling.
  • Support both NV12 and RGBA video frame data formats across the decoder, scheduler, renderer, and preview paths.

Enhancements:

  • Make video playback generation-aware so decoders and frames are safely scoped to the active commit and can be stopped per generation.
  • Honor configurable preload depth and apply decoder backpressure before frame upload to better align decode throughput with presentation pacing.
  • Cap live video presentation rate via an optional max FPS setting and only present newly due frames based on scheduler timing.
  • Refine video color handling by selecting appropriate BT.601/709/2020 matrices and full/limited ranges from ffmpeg metadata and resolution heuristics.
  • Validate both the effects and NV12 conversion WGSL shaders at build time and add tests for YUV conversion coefficient and packing helpers.

Tests:

  • Add unit tests for packed row copying, NV12 plane handling, and YUV color matrix/range selection.
  • Extend shader tests to cover the new NV12 conversion shader and add tests for YUV conversion coefficient selection in the renderer.

Summary by CodeRabbit

  • New Features
    • Added native NV12 video playback with accurate YUV-to-RGB conversion.
    • Added support for BT.601, BT.709, and BT.2020 color formats, including limited and full ranges.
    • Added configurable frame preloading and optional maximum presentation rates.
    • Preserved support for RGBA video frames.
  • Bug Fixes
    • Improved playback smoothness, seeking reliability, and stale-frame handling.
    • Added frame and dimension validation with safer fallback rendering.

@sourcery-ai

sourcery-ai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduces a GPU-based NV12→RGB conversion path and generation-aware video playback pipeline that preserves hardware-decoded NV12 frames, enforces backpressure and FPS caps, and reuses per-video conversion resources across daemon and preview flows while retaining RGBA fallback and validating new shaders and color handling utilities.

Sequence diagram for generation-safe NV12 playback with GPU conversion and FPS cap

sequenceDiagram
    actor User
    participant Daemon
    participant RenderState
    participant VideoPlayback
    participant VideoDecoder
    participant Renderer
    participant LivePacer

    User->>Daemon: select video
    Daemon->>RenderState: commit_video(path)
    RenderState->>VideoPlayback: start(path, hw_accel, preload_frames, generation)
    VideoPlayback->>VideoDecoder: with_preload(path, hw_accel, preload_frames)
    VideoDecoder-->>VideoPlayback: VideoMetadata(NV12/RGBA)
    RenderState->>Renderer: create_video_texture(metadata.width, metadata.height)
    RenderState->>VideoPlayback: wait_first_frame(timeout)
    VideoPlayback-->>RenderState: first VideoFrame{data: VideoFrameData}
    RenderState->>Renderer: update_video_texture(video_texture, frame.data)
    Note over VideoDecoder: decode_hw NV12
    VideoDecoder->>VideoDecoder: copy_nv12_planes()
    VideoDecoder-->>VideoPlayback: next_frame()

    loop play_video
        Daemon->>LivePacer: wait_until(previous + min_frame_interval)
        Daemon->>VideoPlayback: next_frame_in_generation(generation)
        VideoPlayback-->>Daemon: VideoFrame or None
        alt frame available
            Daemon->>Renderer: update_video_texture(video_texture, frame.data)
        else no frame
            Daemon->>VideoPlayback: time_until_next_frame_in_generation(generation)
            VideoPlayback-->>Daemon: wait_duration
            Daemon->>LivePacer: wait_until(now + wait_duration)
        end
        Daemon->>Renderer: present(FrameRequest{bg_bind, new_bind})
        Renderer-->>Daemon: FrameStatus::Presented
        Daemon->>LivePacer: record last_present
    end

    Daemon->>VideoPlayback: stop_generation(generation)
    VideoPlayback->>VideoDecoder: drop()
Loading

File-Level Changes

Change Details Files
Add GPU NV12→RGB conversion pipeline and per-video texture abstraction to the renderer.
  • Introduce VideoTexture struct encapsulating NV12 luma/chroma planes, RGB output texture, conversion uniform buffer, and bind groups.
  • Create NV12 bind group layout and dedicated render pipeline using a new WGSL shader for YUV→RGB conversion into an sRGB RGBA output.
  • Implement create_video_texture and update_video_texture to allocate plane/output textures, upload NV12 data, update conversion coefficients, and draw a full-screen triangle pass per frame.
  • Add YuvConversion uniform struct with matrix/range coefficient selection and unit tests to verify BT.601/709/2020 and limited/full-range parameters.
wallr-core/src/renderer/mod.rs
Preserve NV12 frames from the decoder, add YUV color metadata, and enforce backpressure and seek generation safety in the decode loop.
  • Introduce VideoFrameData enum (Rgba and Nv12 variants) plus YuvColorInfo/YuvMatrix/YuvRange types and expose them via the video module.
  • Extend VideoDecoder to support configurable preload depth, a seek_epoch for generation-aware seeks, and a bounded frame channel sized by preload_frames.
  • Rework the decode loop to avoid unconditional RGBA scaling for NV12: copy NV12 planes into packed luma/chroma buffers, select YUV matrix/range based on ffmpeg color metadata and resolution, and only fall back to RGBA conversion when needed.
  • Insert a backpressure loop that stops decoding when the frame queue is full, responding to pause/seek/stop and seek epoch changes before performing GPU-facing readback and conversion.
  • Add helpers for copying packed rows and NV12 planes, plus unit tests for packing behavior and YUV matrix/range selection; update metadata pixel_format to reflect NV12/RGBA.
  • Make DecoderControl::Seek carry a generation epoch and propagate it through seek handling and seek() API, resetting queues on seek without racing old frames.
wallr-core/src/video/decoder.rs
wallr-core/src/video/mod.rs
Make video playback generation-aware, remove current_frame tracking, and add helpers to query time until next frame for FPS pacing.
  • Store a generation on VideoPlayback and use it to guard decoder/scheduler access, stopping only the matching generation via stop_generation.
  • Adjust start() to accept preload_frames and generation and to construct decoders via VideoDecoder::with_preload.
  • Replace next_frame with generation-aware variants (next_frame_in_generation and internal dispatcher) that return due frames without maintaining a separate current_frame cache.
  • Add time_until_next_frame and time_until_next_frame_in_generation to expose scheduler timing for the next frame in the pending queue.
  • Update scheduler’s ScheduledFrame to carry VideoFrameData instead of raw bytes and adjust tests accordingly.
wallr-core/src/video/playback.rs
wallr-core/src/video/scheduler.rs
Wire NV12 video textures, preload configuration, and FPS cap through daemon render state and live playback, including generation-safe handoff and pacing.
  • Extend RenderState and CommitData with preload_frames, max_fps, and an optional VideoTexture used for video playback and transitions.
  • Change video_playback.start calls to pass hw_accel, preload_frames, and the current generation, and use renderer.create_video_texture based on metadata dimensions.
  • During commit, wait for the first decoded frame if available, upload it via update_video_texture, and store the VideoTexture in CommitData; otherwise rely on WebGPU’s black initialization.
  • Rewrite play_video to reuse the committed VideoTexture, enforce optional max_fps via LivePacer and min_frame_interval, and only fetch frames in the matching generation using next_frame_in_generation/time_until_next_frame_in_generation.
  • Avoid unnecessary uploads by skipping unchanged frames, and stop only the matching playback generation via stop_generation when presents fail or generation changes.
  • Update daemon render state construction to read preload_frames and max_fps from config for both main and sync paths.
wallr-core/src/daemon/mod.rs
Update preview application to use the new video texture pipeline and NV12 conversion, plus shader module bundling and validation for the NV12 WGSL shader.
  • Change PreviewApp to hold a renderer::VideoTexture instead of raw wgpu texture/bind group and to create it via create_video_texture on video start.
  • On first frame and subsequent updates, call update_video_texture and abort the preview on conversion errors.
  • Use the VideoTexture’s bind group when rendering preview frames, matching the daemon’s path.
  • Bundle the new nv12_to_rgb.wgsl shader, expose it via NV12_TO_RGB_SHADER constant, and extend shader tests with a shared validate helper that checks both effects and NV12 shaders parse and validate with naga.
wallr-core/src/preview/mod.rs
wallr-core/src/shader/mod.rs
wallr-core/shaders/nv12_to_rgb.wgsl

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 33a24f11-bee2-49d6-a8a7-24a072f717f2

📥 Commits

Reviewing files that changed from the base of the PR and between 6042c0a and 6d59a01.

📒 Files selected for processing (2)
  • wallr-core/src/daemon/mod.rs
  • wallr-core/src/renderer/mod.rs

📝 Walkthrough

Walkthrough

The video pipeline now preserves NV12 frames and YUV metadata, converts them to sRGB through a dedicated WGSL renderer pipeline, and uses generation-aware playback with configurable preloading and FPS pacing in daemon and preview paths.

Changes

Video pipeline

Layer / File(s) Summary
Frame data and decoder output
wallr-core/src/video/decoder.rs, wallr-core/src/video/mod.rs, wallr-core/src/video/scheduler.rs
Video frames support RGBA and NV12 payloads with YUV metadata. Decoder preloading, seek epochs, backpressure, plane packing, and color selection are included.
Generation-aware playback
wallr-core/src/video/playback.rs
Playback tracks generations, rejects stale frames, supports preload capacity and timing queries, and stops only matching generations.
NV12 renderer conversion
wallr-core/src/renderer/mod.rs, wallr-core/src/shader/mod.rs, wallr-core/shaders/nv12_to_rgb.wgsl
The renderer creates video textures, uploads RGBA or NV12 data, applies YUV conversion coefficients, and renders NV12 output through the sRGB pipeline. Shader validation and conversion tests were added.
Daemon and preview presentation
wallr-core/src/daemon/mod.rs, wallr-core/src/preview/mod.rs
Daemon and preview playback retain VideoTexture resources, upload initial and live frames, apply preload and FPS settings, validate dimensions and upload results, and render through shared bind groups.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VideoDecoder
  participant VideoPlayback
  participant Renderer
  participant DaemonOrPreview
  VideoDecoder->>VideoPlayback: decoded VideoFrame
  VideoPlayback->>DaemonOrPreview: generation-specific frame
  DaemonOrPreview->>Renderer: update_video_texture
  Renderer->>DaemonOrPreview: VideoTexture bind groups
  DaemonOrPreview->>Renderer: render video texture
Loading

Possibly related PRs

Suggested reviewers: programmersd21

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: moving NV12-to-RGB conversion from the CPU to the GPU.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
wallr-core/src/video/playback.rs (1)

57-68: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the generation contract on the public generation-scoped methods.

stop_generation, next_frame_in_generation, and time_until_next_frame_in_generation are public and silently no-op when the generation does not match. wallr-core/src/daemon/mod.rs relies on that behavior in play_video to avoid killing a successor's playback. A short doc comment records the contract for future callers.

The lock order is consistent with next_frame_for_generation and seek (decoder, then scheduler, then pending), so no deadlock is introduced.

📝 Proposed doc comments
+    /// Stops playback only if `generation` is still the active generation.
+    /// A newer `start` call makes this a no-op, so a superseded caller cannot
+    /// stop its successor's playback.
     pub fn stop_generation(&self, generation: u64) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wallr-core/src/video/playback.rs` around lines 57 - 68, Add concise public
documentation to stop_generation, next_frame_in_generation, and
time_until_next_frame_in_generation describing the generation-match contract and
that mismatched generations are silently ignored without affecting successor
playback. Preserve the existing lock ordering and behavior.
wallr-core/src/video/decoder.rs (1)

495-536: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Backpressure exit discards the decoded frame on pause.

If a Pause control arrives while the queue is full, the loop sets interrupted and breaks out of the receive_frame loop. The already-decoded frame is dropped, plus any other frames still buffered in the decoder. After Resume, playback continues from the next packet read, so a short visual jump is possible.

The stop and seek paths do not care about the dropped frame. Only the pause path loses a valid frame. Consider keeping the frame and re-checking the queue after resume, or accept the drop and document it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wallr-core/src/video/decoder.rs` around lines 495 - 536, The backpressure
handling in the decoder’s receive_frame loop drops an already-decoded frame when
Pause arrives. Preserve that frame across pause by distinguishing pause
interruption from seek/stop, retaining the decoded frame and resuming
queue-capacity checks after Resume; keep existing stop and seek behavior
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wallr-core/shaders/nv12_to_rgb.wgsl`:
- Around line 28-34: Align the NV12 shader transfer-function convention with the
RGBA fallback and the effects shader’s expected input. Update sdr_to_linear and
the output texture/view configuration together so both decode paths produce
consistently encoded mid-tones, preserving the chosen sRGB-equivalent or
BT.709-linear convention across nv12_to_rgb and the fallback upload path.

In `@wallr-core/src/daemon/mod.rs`:
- Around line 738-749: Update the video texture setup in the first-frame commit
path to derive its dimensions from first_frame when present, then create the
texture using those dimensions before calling update_video_texture. Preserve the
metadata dimensions as the fallback when no first frame exists, including the
existing black-texture behavior.
- Around line 1249-1253: Update the dimension-mismatch branch in the playback
frame loop to log the mismatch once and wait before retrying, rather than
immediately continuing. Ensure the wait also applies when last_present is None,
preventing repeated mismatched frames from spinning on the playback mutex while
preserving the existing generation-change handling.

---

Nitpick comments:
In `@wallr-core/src/video/decoder.rs`:
- Around line 495-536: The backpressure handling in the decoder’s receive_frame
loop drops an already-decoded frame when Pause arrives. Preserve that frame
across pause by distinguishing pause interruption from seek/stop, retaining the
decoded frame and resuming queue-capacity checks after Resume; keep existing
stop and seek behavior unchanged.

In `@wallr-core/src/video/playback.rs`:
- Around line 57-68: Add concise public documentation to stop_generation,
next_frame_in_generation, and time_until_next_frame_in_generation describing the
generation-match contract and that mismatched generations are silently ignored
without affecting successor playback. Preserve the existing lock ordering and
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 910d7b30-0355-4f00-9da4-b699db041505

📥 Commits

Reviewing files that changed from the base of the PR and between 7052017 and 57eaa3f.

📒 Files selected for processing (9)
  • wallr-core/shaders/nv12_to_rgb.wgsl
  • wallr-core/src/daemon/mod.rs
  • wallr-core/src/preview/mod.rs
  • wallr-core/src/renderer/mod.rs
  • wallr-core/src/shader/mod.rs
  • wallr-core/src/video/decoder.rs
  • wallr-core/src/video/mod.rs
  • wallr-core/src/video/playback.rs
  • wallr-core/src/video/scheduler.rs

Comment thread wallr-core/shaders/nv12_to_rgb.wgsl Outdated
Comment thread wallr-core/src/daemon/mod.rs Outdated
Comment thread wallr-core/src/daemon/mod.rs Outdated
@programmersd21
programmersd21 merged commit 6d8d256 into programmersd21:main Aug 11, 2026
1 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants