lumen-server 0.2.2

Embeddable render service primitives and local HTTP server for Lumen.
Documentation
#![cfg(all(target_os = "linux", feature = "cuda", feature = "vulkan"))]

use std::{
    fs,
    path::PathBuf,
    time::{SystemTime, UNIX_EPOCH},
};

use lumen_ffmpeg::{
    CudaDriver, DecodeMode, EncodeMode, GpuBackend, GpuVideoFrame, GpuVideoInput, InputContext,
    MuxedEncoder, VideoCodec, VideoDecoder, VideoDecoderConfig, VideoEncoderConfig,
    import_owned_vulkan_opaque_fd_image,
};

#[tokio::test]
async fn imports_exportable_wgpu_vulkan_texture_into_cuda() {
    if std::env::var_os("LUMEN_TEST_VK_CUDA").is_none() {
        eprintln!("set LUMEN_TEST_VK_CUDA=1 to run Vulkan-to-CUDA hardware interop smoke test");
        return;
    }

    let renderer = lumen_gpu::Renderer::new()
        .await
        .expect("create Vulkan-capable wgpu renderer");
    let size = lumen_gpu::Size::new(640, 360);
    let exportable = renderer
        .create_exportable_vulkan_texture(
            Some("lumen vk-cuda smoke texture"),
            size,
            lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
            lumen_gpu::wgpu::TextureUsages::COPY_DST
                | lumen_gpu::wgpu::TextureUsages::COPY_SRC
                | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING,
        )
        .expect("create exportable Vulkan texture");

    let driver = CudaDriver::load().expect("load CUDA driver");
    let _context = driver
        .create_primary_context()
        .expect("create CUDA context");
    let imported = import_owned_vulkan_opaque_fd_image(
        &driver,
        exportable
            .memory_fd()
            .try_clone()
            .expect("duplicate Vulkan memory fd for CUDA import"),
        exportable.allocation_size(),
        size.width,
        size.height,
    )
    .expect("import exportable Vulkan memory into CUDA");

    assert_ne!(imported.mipmapped_array_raw(), 0);
    assert_ne!(imported.level_zero_raw(), 0);
}

#[tokio::test]
async fn imports_wgpu_vulkan_texture_into_cuda_and_encodes_with_nvenc() {
    if std::env::var_os("LUMEN_TEST_VK_CUDA_NVENC").is_none() {
        eprintln!("set LUMEN_TEST_VK_CUDA_NVENC=1 to run Vulkan-to-CUDA NVENC smoke test");
        return;
    }

    let renderer = lumen_gpu::Renderer::new()
        .await
        .expect("create Vulkan-capable wgpu renderer");
    let size = lumen_gpu::Size::new(640, 360);
    let exportable = renderer
        .create_exportable_vulkan_texture(
            Some("lumen vk-cuda-nvenc smoke texture"),
            size,
            lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
            lumen_gpu::wgpu::TextureUsages::COPY_DST
                | lumen_gpu::wgpu::TextureUsages::COPY_SRC
                | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING,
        )
        .expect("create exportable Vulkan texture");

    let rgba = rgba_gradient(size.width, size.height);
    renderer.queue.write_texture(
        exportable.texture().as_image_copy(),
        &rgba,
        lumen_gpu::wgpu::TexelCopyBufferLayout {
            offset: 0,
            bytes_per_row: Some(size.width * 4),
            rows_per_image: Some(size.height),
        },
        size.as_extent(),
    );
    renderer
        .device
        .poll(lumen_gpu::wgpu::PollType::wait_indefinitely())
        .expect("wait for Vulkan texture upload");

    let driver = CudaDriver::load().expect("load CUDA driver");
    let context = driver
        .create_primary_context()
        .expect("create CUDA primary context");
    let imported = import_owned_vulkan_opaque_fd_image(
        &driver,
        exportable
            .memory_fd()
            .try_clone()
            .expect("duplicate Vulkan memory fd for CUDA import"),
        exportable.allocation_size(),
        size.width,
        size.height,
    )
    .expect("import exportable Vulkan memory into CUDA");
    let cuda_frame = driver
        .allocate_rgba_frame(size.width, size.height)
        .expect("allocate CUDA RGBA frame");
    context
        .set_current()
        .expect("restore CUDA primary context before copy");
    driver
        .copy_image_to_rgba_frame(&imported, &cuda_frame)
        .expect("copy imported Vulkan image into CUDA RGBA frame");

    let path = temp_path("vk_cuda_nvenc", "mp4");
    let mut config = VideoEncoderConfig::cpu_rgba(size.width, size.height, 30, VideoCodec::H264);
    config.mode = EncodeMode::GpuTexture(GpuBackend::Cuda);
    config.bit_rate = 2_000_000;
    let mut encoder =
        MuxedEncoder::create(path.to_string_lossy().to_string(), config).expect("create NVENC");
    for pts in 0..30 {
        let frame = cuda_frame.as_video_frame(Some(pts));
        let input = GpuVideoInput::Cuda(&frame);
        encoder.write_gpu_frame(&input).expect("write CUDA frame");
    }
    encoder.finish().expect("finish NVENC encode");

    let metadata = fs::metadata(&path).expect("encoded output exists");
    assert!(metadata.len() > 0);
    let _ = fs::remove_file(path);
}

#[tokio::test]
async fn decodes_cuda_frame_into_exportable_vulkan_texture() {
    let Some(path) = std::env::var_os("LUMEN_TEST_CUDA_DECODE_MEDIA") else {
        eprintln!(
            "set LUMEN_TEST_CUDA_DECODE_MEDIA=/path/to/video.mp4 to run CUDA decode-to-Vulkan smoke test"
        );
        return;
    };

    let mut input = InputContext::open(path.to_string_lossy().to_string()).expect("open media");
    let stream = input.best_video_stream().expect("select video stream");
    let mut decoder = VideoDecoder::open(
        &input,
        VideoDecoderConfig {
            stream_index: stream.stream_index,
            mode: DecodeMode::Gpu(GpuBackend::Cuda),
        },
    )
    .expect("open CUDA decoder");

    let decoded = 'decode: loop {
        let Some(packet) = input.read_packet().expect("read packet") else {
            panic!("media ended before a CUDA frame was decoded");
        };
        decoder.send_packet(&packet).expect("send packet");
        if let Some(frame) = decoder.receive_gpu_frame().expect("receive CUDA frame") {
            break 'decode frame;
        }
    };
    let GpuVideoFrame::Cuda(decoded) = decoded else {
        panic!("CUDA decoder returned a non-CUDA frame");
    };
    let (width, height) = decoded.dimensions();

    let renderer = lumen_gpu::Renderer::new()
        .await
        .expect("create Vulkan-capable wgpu renderer");
    let size = lumen_gpu::Size::new(width, height);
    let exportable = renderer
        .create_exportable_vulkan_texture(
            Some("lumen cuda-decode-to-vulkan smoke texture"),
            size,
            lumen_gpu::wgpu::TextureFormat::Rgba8Unorm,
            lumen_gpu::wgpu::TextureUsages::COPY_DST
                | lumen_gpu::wgpu::TextureUsages::COPY_SRC
                | lumen_gpu::wgpu::TextureUsages::TEXTURE_BINDING,
        )
        .expect("create exportable Vulkan texture");

    let driver = CudaDriver::load().expect("load CUDA driver");
    let context = driver
        .create_primary_context()
        .expect("create CUDA primary context");
    let converter = driver
        .create_nv12_to_rgba_converter(&context)
        .expect("create CUDA NV12 converter");
    let rgba = driver
        .allocate_rgba_frame(width, height)
        .expect("allocate CUDA RGBA frame");
    let imported = import_owned_vulkan_opaque_fd_image(
        &driver,
        exportable
            .memory_fd()
            .try_clone()
            .expect("duplicate Vulkan memory fd for CUDA import"),
        exportable.allocation_size(),
        width,
        height,
    )
    .expect("import exportable Vulkan memory into CUDA");

    context
        .set_current()
        .expect("restore CUDA primary context before conversion");
    converter
        .convert(&decoded, &rgba)
        .expect("convert NVDEC frame into CUDA RGBA");
    driver
        .copy_rgba_frame_to_image(&rgba, &imported)
        .expect("copy CUDA RGBA into exported Vulkan texture");
    driver
        .synchronize_context()
        .expect("wait for CUDA writes before Vulkan observes the texture");

    assert_ne!(imported.level_zero_raw(), 0);
}

fn rgba_gradient(width: u32, height: u32) -> Vec<u8> {
    let mut rgba = vec![0; width as usize * height as usize * 4];
    for y in 0..height {
        for x in 0..width {
            let offset = (y as usize * width as usize + x as usize) * 4;
            rgba[offset] = (x % 256) as u8;
            rgba[offset + 1] = (y % 256) as u8;
            rgba[offset + 2] = 0x80;
            rgba[offset + 3] = 0xff;
        }
    }
    rgba
}

fn temp_path(name: &str, extension: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_nanos();
    std::env::temp_dir().join(format!("lumen_server_{name}_{unique}.{extension}"))
}