editor: run on a shared VAAPI-capable wgpu device (Stage 3a)

Foundation for hardware video decode in BOTH preview and export: wgpu textures
can't cross devices, and a hardware-decoded frame is a DMA-BUF-imported texture
that needs the import extensions (only addable via wgpu-hal device_from_raw). So
eframe + the compositor + decode + encode must share ONE custom device.

- vk_device::create_windowed(): the existing import-capable DrmDevice plus
  VK_KHR_swapchain (the WSI surface instance extensions are already enabled by
  Instance::init), for use as the editor's main device.
- main.rs: on Linux, build the shared device and inject it into eframe via
  WgpuSetup::Existing (the egui fork supports it); fall back to wgpu's normal
  device + software decode on any failure, on other platforms, or via
  LB_NO_SHARED_DEVICE. The DrmDevice's wgpu handles are cloned into eframe
  (Arc-backed), so the VkDevice persists with them.

Runtime-verified: the editor launches and renders identically (canvas/vello,
video, panels) on the shared device, and the env-var fallback works.
This commit is contained in:
Skyler Lehmkuhl 2026-06-26 01:11:36 -04:00
parent 255e16434e
commit ca612d7807
2 changed files with 81 additions and 34 deletions

View File

@ -22,12 +22,20 @@ pub struct DrmDevice {
pub raw_instance: ash::Instance, pub raw_instance: ash::Instance,
} }
/// Create the device, or `Err` if Vulkan/the extension isn't available (caller falls back). /// Create a headless DMA-BUF-import device (encoder/decoder), or `Err` if Vulkan/the extension
/// isn't available (caller falls back).
pub fn create() -> Result<DrmDevice, String> { pub fn create() -> Result<DrmDevice, String> {
unsafe { create_inner() } unsafe { create_inner(false) }
} }
unsafe fn create_inner() -> Result<DrmDevice, String> { /// Like [`create`] but also enables `VK_KHR_swapchain` so the device can present to a window —
/// for use as the editor's **shared** wgpu device (eframe + compositor + decode + encode all on
/// one device, so hardware-decoded DMA-BUF textures are usable by the preview compositor).
pub fn create_windowed() -> Result<DrmDevice, String> {
unsafe { create_inner(true) }
}
unsafe fn create_inner(windowed: bool) -> Result<DrmDevice, String> {
use wgpu_hal::vulkan::Api as Vk; use wgpu_hal::vulkan::Api as Vk;
// Bring the HAL Instance trait into scope for `init` / `enumerate_adapters`. // Bring the HAL Instance trait into scope for `init` / `enumerate_adapters`.
use wgpu_hal::Instance as _; use wgpu_hal::Instance as _;
@ -72,14 +80,19 @@ unsafe fn create_inner() -> Result<DrmDevice, String> {
exposed.adapter.required_device_extensions(exposed.features); exposed.adapter.required_device_extensions(exposed.features);
// Only the genuine extensions; external_memory / bind_memory2 / ycbcr / format_list // Only the genuine extensions; external_memory / bind_memory2 / ycbcr / format_list
// are core in Vulkan 1.1+ (this device is 1.3) so they need no enabling. // are core in Vulkan 1.1+ (this device is 1.3) so they need no enabling.
let extra: &[&'static CStr] = &[ let mut extra: Vec<&'static CStr> = vec![
ash::ext::image_drm_format_modifier::NAME, ash::ext::image_drm_format_modifier::NAME,
ash::khr::external_memory_fd::NAME, ash::khr::external_memory_fd::NAME,
ash::ext::external_memory_dma_buf::NAME, ash::ext::external_memory_dma_buf::NAME,
ash::ext::queue_family_foreign::NAME, ash::ext::queue_family_foreign::NAME,
]; ];
// Presentation (windowed shared device only): the WSI surface instance extensions are already
// enabled by `Instance::init`; the device needs the swapchain extension to present.
if windowed {
extra.push(ash::khr::swapchain::NAME);
}
for e in extra { for e in extra {
if !ext_names.contains(e) { if !ext_names.contains(&e) {
ext_names.push(e); ext_names.push(e);
} }
} }

View File

@ -79,6 +79,40 @@ struct Args {
cpu_renderer: bool, cpu_renderer: bool,
} }
/// The default eframe wgpu device setup (wgpu picks the adapter/device). Used on non-Linux, when
/// the shared VAAPI device is unavailable, or when disabled via `LB_NO_SHARED_DEVICE`.
fn lb_default_wgpu_setup() -> egui_wgpu::WgpuSetup {
egui_wgpu::WgpuSetup::CreateNew(egui_wgpu::WgpuSetupCreateNew {
device_descriptor: std::sync::Arc::new(|adapter| {
let features = adapter.features();
// Request SHADER_F16 if available — needed on Mesa/llvmpipe for vello's
// unpack2x16float (enables the SHADER_F16_IN_F32 downlevel capability).
// TIMESTAMP_QUERY(+INSIDE_ENCODERS) drives the F3 GPU composite timer
// (gpu_timer.rs); both are optional and no-op when unsupported.
let optional_features = wgpu::Features::SHADER_F16
| wgpu::Features::TIMESTAMP_QUERY
| wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS;
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
wgpu::Limits::default()
};
wgpu::DeviceDescriptor {
label: Some("lightningbeam wgpu device"),
required_features: features & optional_features,
required_limits: wgpu::Limits {
max_texture_dimension_2d: 8192,
..base_limits
},
..Default::default()
}
}),
..Default::default()
})
}
fn main() -> eframe::Result { fn main() -> eframe::Result {
println!("🚀 Starting Lightningbeam Editor..."); println!("🚀 Starting Lightningbeam Editor...");
@ -168,38 +202,38 @@ fn main() -> eframe::Result {
viewport_builder = viewport_builder.with_icon(icon); viewport_builder = viewport_builder.with_icon(icon);
} }
// Prefer the shared VAAPI-capable wgpu device on Linux: eframe + the compositor + hardware
// video decode all run on one device, so decoded DMA-BUF frames are usable by the preview
// compositor (decode/encode can't share textures across devices). Falls back to wgpu's normal
// device (software video decode) when unavailable, on other platforms, or via LB_NO_SHARED_DEVICE.
// The DrmDevice's wgpu handles are cloned into eframe (Arc-backed, keep the device alive); the
// raw VkDevice persists with them.
#[allow(unused_mut)]
let mut wgpu_setup = lb_default_wgpu_setup();
#[cfg(target_os = "linux")]
if std::env::var("LB_NO_SHARED_DEVICE").is_ok() {
println!("🖥 Shared device disabled via LB_NO_SHARED_DEVICE; default wgpu device (software video decode)");
} else {
match gpu_video_encoder::vk_device::create_windowed() {
Ok(drm) => {
println!("🖥 Using shared VAAPI-capable wgpu device (hardware video decode enabled)");
wgpu_setup = egui_wgpu::WgpuSetup::Existing(egui_wgpu::WgpuSetupExisting {
instance: drm.instance.clone(),
adapter: drm.adapter.clone(),
device: drm.device.clone(),
queue: drm.queue.clone(),
});
}
Err(e) => {
println!("🖥 Shared device unavailable ({e}); default wgpu device (software video decode)");
}
}
}
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: viewport_builder, viewport: viewport_builder,
wgpu_options: egui_wgpu::WgpuConfiguration { wgpu_options: egui_wgpu::WgpuConfiguration {
wgpu_setup: egui_wgpu::WgpuSetup::CreateNew(egui_wgpu::WgpuSetupCreateNew { wgpu_setup,
device_descriptor: std::sync::Arc::new(|adapter| {
let features = adapter.features();
// Request SHADER_F16 if available — needed on Mesa/llvmpipe for vello's
// unpack2x16float (enables the SHADER_F16_IN_F32 downlevel capability).
// TIMESTAMP_QUERY(+INSIDE_ENCODERS) drives the F3 GPU composite timer
// (gpu_timer.rs); both are optional and no-op when unsupported.
let optional_features = wgpu::Features::SHADER_F16
| wgpu::Features::TIMESTAMP_QUERY
| wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS;
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
wgpu::Limits::default()
};
wgpu::DeviceDescriptor {
label: Some("lightningbeam wgpu device"),
required_features: features & optional_features,
required_limits: wgpu::Limits {
max_texture_dimension_2d: 8192,
..base_limits
},
..Default::default()
}
}),
..Default::default()
}),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()