export: GPU-resident decode for software + HDR export (Stage 3c-export pt 1)
The software and HDR export paths already composite on the shared device (the eframe device, via render_next_video_frame), so they can consume the same hardware-decoded NV12 GPU frames the preview uses — no CPU download/upload: - ExportGpuResources gains the Nv12BlitPipeline; the export compositor's Video arm branches on inst.gpu (NV12 blit, mirroring stage.rs) vs the RGBA upload path. - render_frame_to_gpu_rgba takes a hardware_ok flag: true for the software video + image export (shared device), false for the zero-copy path (own device, must download). render_frame_to_yuv10_hdr sets it true. Drops the 4K software/HDR-export decode wall (HW decode + GPU composite, no per-frame RGBA upload). The common Linux H.264 zero-copy path is unchanged (separate device) — that's pt 2. Falls back to software when no shared device / importer (flag is harmless then: get_frame returns CPU anyway). Compiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
33dec6f327
commit
bb3369b709
|
|
@ -625,6 +625,7 @@ impl ExportOrchestrator {
|
||||||
floating_selection,
|
floating_selection,
|
||||||
state.settings.allow_transparency,
|
state.settings.allow_transparency,
|
||||||
raster_store,
|
raster_store,
|
||||||
|
true, // image export composites on the shared device
|
||||||
)?;
|
)?;
|
||||||
queue.submit(Some(encoder.finish()));
|
queue.submit(Some(encoder.finish()));
|
||||||
|
|
||||||
|
|
@ -1393,6 +1394,7 @@ impl ExportOrchestrator {
|
||||||
None, // No floating selection during video export
|
None, // No floating selection during video export
|
||||||
false, // Video export is never transparent
|
false, // Video export is never transparent
|
||||||
raster_store,
|
raster_store,
|
||||||
|
true, // software export composites on the shared device → may use HW frames
|
||||||
)?;
|
)?;
|
||||||
let render_end = Instant::now();
|
let render_end = Instant::now();
|
||||||
|
|
||||||
|
|
@ -1498,6 +1500,7 @@ impl ExportOrchestrator {
|
||||||
None,
|
None,
|
||||||
false,
|
false,
|
||||||
Some(&raster_store),
|
Some(&raster_store),
|
||||||
|
false, // zero-copy runs on its own device → download HW frames to CPU
|
||||||
) {
|
) {
|
||||||
Ok(cmd) => cmd,
|
Ok(cmd) => cmd,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,8 @@ pub struct ExportGpuResources {
|
||||||
pub linear_to_srgb_sampler: wgpu::Sampler,
|
pub linear_to_srgb_sampler: wgpu::Sampler,
|
||||||
/// Canvas blit pipeline for raster/video/float layers (bypasses Vello).
|
/// Canvas blit pipeline for raster/video/float layers (bypasses Vello).
|
||||||
pub canvas_blit: crate::gpu_brush::CanvasBlitPipeline,
|
pub canvas_blit: crate::gpu_brush::CanvasBlitPipeline,
|
||||||
|
/// NV12→linear blit for hardware-decoded video frames (export on the shared device).
|
||||||
|
pub nv12_blit: crate::nv12_blit::Nv12BlitPipeline,
|
||||||
/// Per-keyframe GPU texture cache for raster layers during export.
|
/// Per-keyframe GPU texture cache for raster layers during export.
|
||||||
pub raster_cache: std::collections::HashMap<uuid::Uuid, crate::gpu_brush::CanvasPair>,
|
pub raster_cache: std::collections::HashMap<uuid::Uuid, crate::gpu_brush::CanvasPair>,
|
||||||
/// Cached HDR accumulator state after the (static) background is composited in. The document
|
/// Cached HDR accumulator state after the (static) background is composited in. The document
|
||||||
|
|
@ -287,6 +289,7 @@ impl ExportGpuResources {
|
||||||
});
|
});
|
||||||
|
|
||||||
let canvas_blit = crate::gpu_brush::CanvasBlitPipeline::new(device);
|
let canvas_blit = crate::gpu_brush::CanvasBlitPipeline::new(device);
|
||||||
|
let nv12_blit = crate::nv12_blit::Nv12BlitPipeline::new(device);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
buffer_pool,
|
buffer_pool,
|
||||||
|
|
@ -306,6 +309,7 @@ impl ExportGpuResources {
|
||||||
linear_to_srgb_bind_group_layout,
|
linear_to_srgb_bind_group_layout,
|
||||||
linear_to_srgb_sampler,
|
linear_to_srgb_sampler,
|
||||||
canvas_blit,
|
canvas_blit,
|
||||||
|
nv12_blit,
|
||||||
raster_cache: std::collections::HashMap::new(),
|
raster_cache: std::collections::HashMap::new(),
|
||||||
cached_bg_hdr: None,
|
cached_bg_hdr: None,
|
||||||
hdr_pipeline: None,
|
hdr_pipeline: None,
|
||||||
|
|
@ -959,16 +963,26 @@ fn composite_document_to_hdr(
|
||||||
}
|
}
|
||||||
RenderedLayerType::Video { instances } => {
|
RenderedLayerType::Video { instances } => {
|
||||||
for inst in instances {
|
for inst in instances {
|
||||||
if inst.rgba_data.is_empty() { continue; }
|
if inst.gpu.is_none() && inst.rgba_data.is_empty() { continue; }
|
||||||
let hdr_layer_handle = gpu_resources.buffer_pool.acquire(device, hdr_spec);
|
let hdr_layer_handle = gpu_resources.buffer_pool.acquire(device, hdr_spec);
|
||||||
if let Some(hdr_layer_view) = gpu_resources.buffer_pool.get_view(hdr_layer_handle) {
|
if let Some(hdr_layer_view) = gpu_resources.buffer_pool.get_view(hdr_layer_handle) {
|
||||||
|
let bt = crate::gpu_brush::BlitTransform::new(inst.transform, inst.width, inst.height, width, height);
|
||||||
|
if let Some(gpu) = &inst.gpu {
|
||||||
|
// Hardware-decoded NV12 plane textures → linear, no CPU upload.
|
||||||
|
let y_view = gpu.y.create_view(&Default::default());
|
||||||
|
let uv_view = gpu.uv.create_view(&Default::default());
|
||||||
|
gpu_resources.nv12_blit.blit(
|
||||||
|
device, queue, &y_view, &uv_view, hdr_layer_view, &bt,
|
||||||
|
gpu.full_range, gpu.coeffs, gpu.transfer, gpu.primaries,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
// Upload raw sRGB straight-alpha bytes into an sRGB texture; the GPU
|
// Upload raw sRGB straight-alpha bytes into an sRGB texture; the GPU
|
||||||
// decodes to linear on sample (no per-pixel CPU conversion). Blit with
|
// decodes to linear on sample (no per-pixel CPU conversion). Blit with
|
||||||
// blit_straight so the shader doesn't unpremultiply.
|
// blit_straight so the shader doesn't unpremultiply.
|
||||||
let tex = upload_transient_texture(device, queue, &inst.rgba_data, inst.width, inst.height, wgpu::TextureFormat::Rgba8UnormSrgb, Some("export_video_frame_tex"));
|
let tex = upload_transient_texture(device, queue, &inst.rgba_data, inst.width, inst.height, wgpu::TextureFormat::Rgba8UnormSrgb, Some("export_video_frame_tex"));
|
||||||
let tex_view = tex.create_view(&Default::default());
|
let tex_view = tex.create_view(&Default::default());
|
||||||
let bt = crate::gpu_brush::BlitTransform::new(inst.transform, inst.width, inst.height, width, height);
|
|
||||||
gpu_resources.canvas_blit.blit_straight(device, queue, &tex_view, hdr_layer_view, &bt, None);
|
gpu_resources.canvas_blit.blit_straight(device, queue, &tex_view, hdr_layer_view, &bt, None);
|
||||||
|
}
|
||||||
let compositor_layer = CompositorLayer::new(hdr_layer_handle, inst.opacity, lightningbeam_core::gpu::BlendMode::Normal);
|
let compositor_layer = CompositorLayer::new(hdr_layer_handle, inst.opacity, lightningbeam_core::gpu::BlendMode::Normal);
|
||||||
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("export_video_composite") });
|
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("export_video_composite") });
|
||||||
gpu_resources.compositor.composite(device, queue, &mut enc, &[compositor_layer], &gpu_resources.buffer_pool, &gpu_resources.hdr_texture_view, None);
|
gpu_resources.compositor.composite(device, queue, &mut enc, &[compositor_layer], &gpu_resources.buffer_pool, &gpu_resources.hdr_texture_view, None);
|
||||||
|
|
@ -1401,9 +1415,9 @@ pub fn render_frame_to_yuv10_hdr(
|
||||||
Affine::IDENTITY
|
Affine::IDENTITY
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export composites on a separate device → force software video frames.
|
// HDR export composites on the shared device, so it can consume hardware-decoded GPU frames.
|
||||||
if let Ok(mut vm) = video_manager.lock() {
|
if let Ok(mut vm) = video_manager.lock() {
|
||||||
vm.set_render_hardware_ok(false);
|
vm.set_render_hardware_ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let composite_result = render_document_for_compositing(
|
let composite_result = render_document_for_compositing(
|
||||||
|
|
@ -1437,6 +1451,9 @@ pub fn render_frame_to_gpu_rgba(
|
||||||
floating_selection: Option<&lightningbeam_core::selection::RasterFloatingSelection>,
|
floating_selection: Option<&lightningbeam_core::selection::RasterFloatingSelection>,
|
||||||
allow_transparency: bool,
|
allow_transparency: bool,
|
||||||
raster_store: Option<&lightningbeam_core::raster_store::RasterStore>,
|
raster_store: Option<&lightningbeam_core::raster_store::RasterStore>,
|
||||||
|
// True when compositing on the shared device (software/image export) → may consume
|
||||||
|
// hardware-decoded GPU frames; false for the zero-copy path on its own device.
|
||||||
|
hardware_ok: bool,
|
||||||
) -> Result<wgpu::CommandEncoder, String> {
|
) -> Result<wgpu::CommandEncoder, String> {
|
||||||
use vello::kurbo::Affine;
|
use vello::kurbo::Affine;
|
||||||
|
|
||||||
|
|
@ -1468,9 +1485,10 @@ pub fn render_frame_to_gpu_rgba(
|
||||||
Affine::IDENTITY
|
Affine::IDENTITY
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export composites on a separate device — force software frames (see above).
|
// GPU frames are usable only on the shared device (software/image export); the zero-copy path
|
||||||
|
// runs on its own device and must download to CPU.
|
||||||
if let Ok(mut vm) = video_manager.lock() {
|
if let Ok(mut vm) = video_manager.lock() {
|
||||||
vm.set_render_hardware_ok(false);
|
vm.set_render_hardware_ok(hardware_ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render document for compositing (returns per-layer scenes)
|
// Render document for compositing (returns per-layer scenes)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue