Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ lazy_static = "1.4"
# For async frame scheduling (optional)
crossbeam-channel = "0.5"

# GPU acceleration (runtime detection)
wgpu = "0.19"
pollster = "0.3" # Async runtime for wgpu
bytemuck = { version = "1.14", features = ["derive"] } # Safe casting for GPU buffers

# Raw Vulkan access for DMA-BUF zero-copy
ash = "0.38" # Vulkan bindings for external memory export
gpu-allocator = { version = "0.25", default-features = false, features = ["vulkan"] }

# Linux-specific for DMA-BUF
[target.'cfg(target_os = "linux")'.dependencies]
nix = { version = "0.28", features = ["fs", "mman"] } # For mmap and file descriptor handling

[build-dependencies]
# For generating bindings to VLC and DeckLink C APIs
bindgen = "0.69"
Expand Down
125 changes: 3 additions & 122 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ PREFIX ?= /usr
VLC_PLUGIN_DIR ?= $(PREFIX)/lib/vlc/plugins/video_output
VLC_CACHE_GEN ?= $(PREFIX)/lib/vlc/vlc-cache-gen

# Local user installation paths
LOCAL_VLC_PLUGIN_DIR := $(HOME)/.local/lib/vlc/plugins/video_output

# Docker configuration
DOCKER := docker
DOCKER_IMAGE_BASE := vlc-decklink-base
Expand Down Expand Up @@ -118,9 +121,6 @@ install-debug: debug
-$(VLC_CACHE_GEN) $(DESTDIR)$(PREFIX)/lib/vlc/plugins 2>/dev/null || true
@echo "Debug installation complete."

# Local user installation paths
LOCAL_VLC_PLUGIN_DIR := $(HOME)/.local/lib/vlc/plugins/video_output

install-local: debug
@echo "Installing debug plugin to $(LOCAL_VLC_PLUGIN_DIR)..."
install -d $(LOCAL_VLC_PLUGIN_DIR)
Expand All @@ -129,82 +129,18 @@ install-local: debug
-rm -f $(HOME)/.cache/vlc/plugins*.dat 2>/dev/null || true
@echo "Local installation complete. Restart VLC to load the plugin."

uninstall:
@echo "Removing plugin from $(VLC_PLUGIN_DIR)..."
rm -f $(DESTDIR)$(VLC_PLUGIN_DIR)/$(PLUGIN_NAME).so
-$(VLC_CACHE_GEN) $(DESTDIR)$(PREFIX)/lib/vlc/plugins 2>/dev/null || true
@echo "Uninstallation complete."

# =============================================================================
# Development Targets (run in Docker)
# =============================================================================

test: ensure-image
@echo "Running tests in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_BASE) cargo test

check: ensure-image
@echo "Checking code in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_BASE) cargo check

clippy: ensure-dev-image
@echo "Running clippy in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_DEV) cargo clippy -- -D warnings

fmt: ensure-dev-image
@echo "Formatting code in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_DEV) cargo fmt

# =============================================================================
# C++ Wrapper Tests (run in Docker)
# =============================================================================

# Build and run unit tests (no hardware required)
test-unit: ensure-dev-image
@echo "Building and running unit tests in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_DEV) sh -c '\
mkdir -p tests/build && \
cd tests/build && \
cmake -DCMAKE_BUILD_TYPE=Debug .. && \
make test_decklink_wrapper && \
./test_decklink_wrapper'

# Build and run integration tests (requires hardware - use docker-shell with --privileged)
test-integration: ensure-dev-image
@echo "Building integration tests in Docker..."
@echo "NOTE: For hardware tests, use 'make docker-shell-hw' and run manually"
$(DOCKER_RUN) $(DOCKER_IMAGE_DEV) sh -c '\
mkdir -p tests/build && \
cd tests/build && \
cmake -DCMAKE_BUILD_TYPE=Debug .. && \
make test_decklink_integration'
@echo ""
@echo "Integration tests built. To run with hardware:"
@echo " make docker-shell-hw"
@echo " cd tests/build && ./test_decklink_integration"

# Build all wrapper tests
test-wrapper: ensure-dev-image
@echo "Building all wrapper tests in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_DEV) sh -c '\
mkdir -p tests/build && \
cd tests/build && \
cmake -DCMAKE_BUILD_TYPE=Debug .. && \
make'

# Interactive shell with hardware access (for running integration tests)
docker-shell-hw: ensure-dev-image
@echo "Starting Docker shell with hardware access..."
@echo "Inside the container, run: cd tests/build && ./test_decklink_integration"
$(DOCKER) run --rm -it --privileged \
-v /dev:/dev \
-v $(CURDIR):$(DOCKER_WORKDIR) \
-w $(DOCKER_WORKDIR) \
$(DOCKER_IMAGE_DEV) /bin/bash

fmt-check: ensure-dev-image
@echo "Checking code formatting in Docker..."
$(DOCKER_RUN) $(DOCKER_IMAGE_DEV) cargo fmt --check

doc: ensure-image
@echo "Generating documentation in Docker..."
Expand Down Expand Up @@ -251,58 +187,3 @@ docker-shell: ensure-dev-image
docker-clean:
@echo "Removing Docker images..."
-$(DOCKER) rmi $(DOCKER_IMAGE_BASE) $(DOCKER_IMAGE_DEV) $(DOCKER_IMAGE_BUILDER) vlc-decklink-runtime 2>/dev/null || true

# =============================================================================
# Help Target
# =============================================================================

help:
@echo "VLC DeckLink Plugin Build System"
@echo ""
@echo "All build commands run inside Docker containers automatically."
@echo "Docker images are built on-demand if they don't exist."
@echo ""
@echo "Build targets:"
@echo " make - Build plugin in release mode (in Docker)"
@echo " make debug - Build plugin in debug mode (in Docker)"
@echo " make clean - Remove build artifacts"
@echo " make distclean - Remove all generated files"
@echo ""
@echo "Installation targets (run on host, uses Docker-built artifacts):"
@echo " sudo make install - Install release plugin to VLC plugin directory"
@echo " sudo make install-debug- Install debug plugin"
@echo " sudo make uninstall - Remove plugin from VLC plugin directory"
@echo ""
@echo "Development targets (run in Docker):"
@echo " make test - Run Rust tests"
@echo " make check - Check code compiles"
@echo " make clippy - Run clippy linter"
@echo " make fmt - Format code"
@echo " make fmt-check - Check code formatting"
@echo " make doc - Generate documentation"
@echo ""
@echo "C++ wrapper test targets:"
@echo " make test-unit - Run unit tests (no hardware required)"
@echo " make test-integration - Build integration tests"
@echo " make test-wrapper - Build all wrapper tests"
@echo " make docker-shell-hw - Shell with hardware access for integration tests"
@echo ""
@echo "Docker image targets:"
@echo " make docker-base - Build base Docker image"
@echo " make docker-dev - Build dev Docker image (with extra tools)"
@echo " make docker-builder - Build complete builder image"
@echo " make docker-runtime - Build minimal runtime image"
@echo " make docker-shell - Start interactive Docker shell"
@echo " make docker-clean - Remove Docker images"
@echo ""
@echo "Configuration variables:"
@echo " PREFIX - Installation prefix (default: /usr)"
@echo " VLC_PLUGIN_DIR - VLC plugin directory"
@echo ""
@echo "Examples:"
@echo " make # Build release (in Docker)"
@echo " sudo make install # Install to system"
@echo " make docker-shell # Interactive dev environment"
@echo " make test # Run Rust tests (in Docker)"
@echo " make test-unit # Run C++ wrapper unit tests"
@echo " make docker-shell-hw # Shell for hardware integration tests"
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ into the root of this repository.

```bash
# Install release build (requires root)
sudo make install
make release

# This copies the plugin to /usr/lib/vlc/plugins/video_output/
# and regenerates the VLC plugin cache
# Copy the plugin to /usr/lib/vlc/plugins/video_output/
```

## Architecture
Expand Down
1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ fn main() {
vlc_module_build
.define("__PLUGIN__", None)
.define("MODULE_STRING", "\"decklink\"")
.define("DECKLINK_GPU", None) // Enable GPU option in VLC settings
.flag("-fvisibility=default"); // Ensure symbols are visible

vlc_module_build.compile("vlc_module");
Expand Down
119 changes: 119 additions & 0 deletions src/decklink/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,26 @@ impl VideoFrame {
}
}
}
ScalingAlgorithm::LanczosGpu => {
// ══════════════════════════════════════════════════════════════════
// LANCZOS GPU (UYVY FALLBACK)
// ══════════════════════════════════════════════════════════════════
// GPU scaling only works with RGBA. For UYVY, fall back to CPU bilinear.
// ══════════════════════════════════════════════════════════════════
use std::sync::atomic::{AtomicBool, Ordering};
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
log::warn!("GPU scaling requested but pixel format is UYVY - \
GPU only supports RGBA/BGRA. Falling back to CPU Bilinear. \
Set pixel format to 'bgra' for GPU acceleration.");
}
Self::scale_uyvy(
src, src_row_bytes, src_width, src_height,
dst, dst_row_bytes, dst_width, dst_height,
scaled_width, scaled_height, x_offset, y_offset,
ScalingAlgorithm::Bilinear,
);
}
ScalingAlgorithm::Bicubic | ScalingAlgorithm::Lanczos => {
// ══════════════════════════════════════════════════════════════════
// BICUBIC / LANCZOS (UYVY FALLBACK)
Expand Down Expand Up @@ -995,6 +1015,105 @@ impl VideoFrame {
}
}
}
ScalingAlgorithm::LanczosGpu => {
// ══════════════════════════════════════════════════════════════════
// LANCZOS GPU-ACCELERATED RESAMPLING
// ══════════════════════════════════════════════════════════════════
// Uses wgpu compute shaders for ~10x speedup over CPU Lanczos.
// Falls back to CPU Lanczos if GPU is unavailable.
//
// The GPU path:
// 1. Uploads source pixels to GPU memory
// 2. Runs separable 2-pass Lanczos (horizontal then vertical)
// 3. Downloads result to CPU memory
// 4. Copies to destination buffer with centering offset
// ══════════════════════════════════════════════════════════════════
use crate::gpu;
use crate::plugin::debug_counters;

// Try GPU scaling
let mut gpu_scaler = gpu::SCALER.lock();
if let Some(ref mut scaler) = *gpu_scaler {
// Create temporary buffer for GPU output (full scaled size)
let gpu_dst_size = scaled_width * scaled_height * 4;
let mut gpu_dst = vec![0u8; gpu_dst_size];

let gpu_start = std::time::Instant::now();
match scaler.scale_lanczos(
src, src_width, src_height,
&mut gpu_dst, scaled_width, scaled_height,
) {
Ok(()) => {
let gpu_time = gpu_start.elapsed();
debug_counters::record_gpu_frame(gpu_time.as_micros() as u64);

// Copy GPU result to destination with centering offset
for y in 0..actual_height {
let src_row_start = y * scaled_width * 4;
let dst_row_start = (y_offset + y) * dst_row_bytes + x_offset * bytes_per_pixel;
let copy_bytes = actual_width * bytes_per_pixel;

if src_row_start + copy_bytes <= gpu_dst.len() &&
dst_row_start + copy_bytes <= dst.len() {
dst[dst_row_start..dst_row_start + copy_bytes]
.copy_from_slice(&gpu_dst[src_row_start..src_row_start + copy_bytes]);
}
}
return; // Success, exit the match
}
Err(e) => {
log::warn!("GPU scaling failed, falling back to CPU: {}", e);
// Fall through to CPU path below
}
}
} else {
log::warn!("GPU scaler not available, falling back to CPU Lanczos");
}

// Fallback: CPU Lanczos (same as ScalingAlgorithm::Lanczos)
const LANCZOS_A: f32 = 3.0;
for dst_y in 0..actual_height {
let src_y_f = (dst_y as f32 + 0.5) * scale_y - 0.5;
let src_y_i = src_y_f.floor() as isize;
let y_frac = src_y_f - src_y_i as f32;

let dst_row_start = (y_offset + dst_y) * dst_row_bytes + x_offset * bytes_per_pixel;

for dst_x in 0..actual_width {
let src_x_f = (dst_x as f32 + 0.5) * scale_x - 0.5;
let src_x_i = src_x_f.floor() as isize;
let x_frac = src_x_f - src_x_i as f32;

let dst_offset = dst_row_start + dst_x * bytes_per_pixel;
if dst_offset + bytes_per_pixel > dst.len() { continue; }

for c in 0..4 {
let mut sum = 0.0f32;
let mut weight_sum = 0.0f32;
let a = LANCZOS_A as isize;
for ky in -a + 1..=a {
let sy = (src_y_i + ky).clamp(0, src_height as isize - 1) as usize;
let wy = Self::lanczos_weight(ky as f32 - y_frac, LANCZOS_A);
for kx in -a + 1..=a {
let sx = (src_x_i + kx).clamp(0, src_width as isize - 1) as usize;
let wx = Self::lanczos_weight(kx as f32 - x_frac, LANCZOS_A);
let offset = sy * src_row_bytes + sx * bytes_per_pixel + c;
if offset < src.len() {
let w = wx * wy;
sum += src[offset] as f32 * w;
weight_sum += w;
}
}
}
dst[dst_offset + c] = if weight_sum > 0.0 {
(sum / weight_sum).clamp(0.0, 255.0) as u8
} else {
0
};
}
}
}
}
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/decklink/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,14 @@ impl Output {
/// The DeckLink SDK will display the frame at the computed timestamp
/// and call back when done (if a callback is registered).
pub fn schedule_frame(&mut self, frame: &super::VideoFrame) -> Result<()> {
self.schedule_frame_handle(frame.handle())
}

/// Schedule a raw frame handle for future display at the next slot.
///
/// This is used by the zero-copy scaler which manages its own frame handles.
/// The handle must be a valid DeckLink frame created via the DeckLink API.
pub fn schedule_frame_handle(&mut self, handle: DeckLinkVideoFrameHandle) -> Result<()> {
let playback = self.scheduled_playback.as_mut()
.ok_or_else(|| DeckLinkError::ConfigurationError(
"Scheduled playback not enabled".to_string()
Expand All @@ -359,7 +367,7 @@ impl Output {
let result = unsafe {
decklink_schedule_frame(
self.handle,
frame.handle(),
handle,
display_time,
duration,
time_scale,
Expand Down
Loading