use uzor::render::RenderContext;
use crate::factory::{
Submit3DError, SurfaceMode, UrxComposeOverlayCache, UrxComposeOverlayDynamic, WindowRenderState,
};
#[derive(Debug, Clone)]
pub struct Compose3DJob {
pub camera: uzor_urx_3d::PerspectiveCamera,
pub dst_x: u32,
pub dst_y: u32,
pub dst_w: u32,
pub dst_h: u32,
}
#[derive(Debug, Clone)]
pub struct ComposeParticlesJob {
pub id: u64,
pub camera: uzor_urx_3d::PerspectiveCamera,
pub dst_x: u32,
pub dst_y: u32,
pub dst_w: u32,
pub dst_h: u32,
}
pub struct CachedOverlayJob {
pub key: u64,
pub paint: Box<dyn FnMut(&mut dyn RenderContext)>,
}
#[derive(Debug, Clone, Copy)]
pub struct ComposedOutcome {
pub jobs_rendered: u32,
pub jobs_skipped: u32,
pub surface_lost: bool,
}
pub fn submit_urx_composed(
state: &mut WindowRenderState,
base_color: [f32; 4],
jobs: &[Compose3DJob],
overlay: Option<Box<dyn FnMut(&mut dyn RenderContext)>>,
cached_overlay: Option<CachedOverlayJob>,
) -> Result<ComposedOutcome, Submit3DError> {
submit_urx_composed_impl(
state,
None,
base_color,
jobs,
overlay,
cached_overlay,
)
}
pub fn submit_urx_composed_with_scene(
state: &mut WindowRenderState,
scene: &uzor_urx_3d::Scene3D,
base_color: [f32; 4],
jobs: &[Compose3DJob],
overlay: Option<Box<dyn FnMut(&mut dyn RenderContext)>>,
cached_overlay: Option<CachedOverlayJob>,
) -> Result<ComposedOutcome, Submit3DError> {
submit_urx_composed_impl(
state,
Some(scene),
base_color,
jobs,
overlay,
cached_overlay,
)
}
fn submit_urx_composed_impl(
state: &mut WindowRenderState,
scene_override: Option<&uzor_urx_3d::Scene3D>,
base_color: [f32; 4],
jobs: &[Compose3DJob],
mut overlay: Option<Box<dyn FnMut(&mut dyn RenderContext)>>,
mut cached_overlay: Option<CachedOverlayJob>,
) -> Result<ComposedOutcome, Submit3DError> {
let (surf_w, surf_h, surface_format) = match &state.surface {
SurfaceMode::Gpu { surface, .. } => (
surface.config.width,
surface.config.height,
surface.config.format,
),
_ => return Err(Submit3DError::NotGpuSurface),
};
if surf_w == 0 || surf_h == 0 { return Err(Submit3DError::ZeroSizedSurface); }
let (device, queue) = match &state.surface {
SurfaceMode::Gpu { gpu_pool, dev_id, .. } => (
gpu_pool.devices[*dev_id].device.clone(),
gpu_pool.devices[*dev_id].queue.clone(),
),
_ => return Err(Submit3DError::NotGpuSurface),
};
let urx_scene_opt = state.urx_ctx.as_mut().map(|c| c.take_scene());
if state.capture_3d_enabled {
state.ensure_capture_3d(&device, surf_w, surf_h, surface_format);
}
let SurfaceMode::Gpu { surface, .. } = &mut state.surface else {
return Err(Submit3DError::NotGpuSurface);
};
let frame = match surface.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
surface.surface.configure(&device, &surface.config);
return Ok(ComposedOutcome { jobs_rendered: 0, jobs_skipped: jobs.len() as u32, surface_lost: true });
}
_ => return Ok(ComposedOutcome { jobs_rendered: 0, jobs_skipped: jobs.len() as u32, surface_lost: true }),
};
let swap_view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("uzor-render-hub:compose"),
});
let urx_backend = state.active_urx.unwrap_or(uzor::UrxBackend::Cpu);
let backend_resolved = match urx_backend {
uzor::UrxBackend::Auto => uzor::UrxBackend::Wgpu, b => b,
};
let full_cover_3d_job = jobs.iter().any(|job| {
job.dst_x == 0 && job.dst_y == 0 && job.dst_w >= surf_w && job.dst_h >= surf_h
});
let skip_2d_pass = urx_scene_opt.is_none() && full_cover_3d_job;
if !skip_2d_pass {
match backend_resolved {
uzor::UrxBackend::Cpu => {
compose_urx_cpu_into_swap(
state, &device, &queue, &mut encoder, &swap_view,
surf_w, surf_h, urx_scene_opt, base_color,
);
}
uzor::UrxBackend::Wgpu | uzor::UrxBackend::Hybrid | uzor::UrxBackend::WgpuFull | uzor::UrxBackend::Auto => {
compose_urx_native_into_swap(
state, &device, &queue, &mut encoder, &swap_view,
surf_w, surf_h, urx_scene_opt, base_color,
);
}
}
}
let mut jobs_rendered = 0u32;
let mut jobs_skipped = 0u32;
for job in jobs {
let dx = job.dst_x.min(surf_w.saturating_sub(1));
let dy = job.dst_y.min(surf_h.saturating_sub(1));
let dw = job.dst_w.min(surf_w.saturating_sub(dx));
let dh = job.dst_h.min(surf_h.saturating_sub(dy));
if dw == 0 || dh == 0 {
jobs_skipped += 1;
continue;
}
if state.urx_renderer_3d.is_none() {
let mut r3d = uzor_urx_3d::Renderer3D::new(&device, &queue, surface_format, (dw, dh), 1024);
r3d.set_sample_count(&device, state.urx_compose_msaa_sample_count);
if let Some(px) = state.urx_compose_edge_width_px {
r3d.set_edge_width_px(px);
}
state.urx_renderer_3d = Some(r3d);
}
if scene_override.is_none() && state.urx_scene_3d.is_none() {
state.urx_scene_3d = Some(uzor_urx_3d::Scene3D::new());
}
let need_new_target = match &state.urx_offscreen_3d {
None => true,
Some(o) => o.width != dw || o.height != dh || o.format != surface_format,
};
if need_new_target {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("urx-3d-offscreen-compose"),
size: wgpu::Extent3d { width: dw, height: dh, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: surface_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
state.urx_offscreen_3d = Some(crate::factory::UrxOffscreen3D {
texture, view, width: dw, height: dh, format: surface_format,
});
}
let r3d_ok = {
let r3d = match state.urx_renderer_3d.as_mut() { Some(r) => r, None => { jobs_skipped += 1; continue; } };
let scene = match scene_override.or(state.urx_scene_3d.as_ref()) {
Some(s) => s,
None => { jobs_skipped += 1; continue; }
};
let off = match state.urx_offscreen_3d.as_ref() { Some(o) => o, None => { jobs_skipped += 1; continue; } };
r3d.render(&device, &queue, &mut encoder, &off.view, &job.camera, scene);
true
};
if !r3d_ok { jobs_skipped += 1; continue; }
let off = state.urx_offscreen_3d.as_ref().unwrap();
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: &off.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &frame.texture,
mip_level: 0,
origin: wgpu::Origin3d { x: dx, y: dy, z: 0 },
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d { width: dw, height: dh, depth_or_array_layers: 1 },
);
if let Some(cap) = state.urx_capture_3d.as_ref() {
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: &off.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &cap.texture,
mip_level: 0,
origin: wgpu::Origin3d { x: dx, y: dy, z: 0 },
aspect: wgpu::TextureAspect::All,
},
wgpu::Extent3d { width: dw, height: dh, depth_or_array_layers: 1 },
);
}
jobs_rendered += 1;
}
if let Some(cached_job) = cached_overlay.as_mut() {
let needs_refresh = state.urx_compose_overlay_cache.as_ref().is_none_or(|cache| {
cache.key != cached_job.key || cache.width != surf_w || cache.height != surf_h
});
if needs_refresh {
if state.urx_cached_overlay_renderer.is_none() {
state.urx_cached_overlay_renderer = Some(uzor_urx_wgpu::NativeUrxRenderer::new(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
));
}
let uploaded = match state.urx_cached_overlay_renderer.as_mut() {
Some(renderer) => build_overlay_texture_native(
&device,
&queue,
&mut encoder,
renderer,
surf_w,
surf_h,
cached_job.paint.as_mut(),
),
None => None,
};
state.urx_compose_overlay_cache = uploaded.map(|uploaded| UrxComposeOverlayCache {
key: cached_job.key,
texture: uploaded.texture,
view: uploaded.view,
width: surf_w,
height: surf_h,
});
}
if let Some(cache) = state.urx_compose_overlay_cache.as_ref() {
blit_overlay_onto(
&device,
&mut encoder,
&cache.view,
&swap_view,
surface_format,
&mut state.urx_compose_overlay_blitter,
);
if let Some(cap) = state.urx_capture_3d.as_ref() {
blit_overlay_onto(
&device,
&mut encoder,
&cache.view,
&cap.view,
surface_format,
&mut state.urx_compose_overlay_blitter,
);
}
}
}
if let Some(overlay_fn) = overlay.as_mut() {
if surf_w > 0 && surf_h > 0 {
if state.urx_dynamic_overlay_renderer.is_none() {
state.urx_dynamic_overlay_renderer = Some(uzor_urx_wgpu::NativeUrxRenderer::new(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
));
}
let mut ctx = uzor_render_urx::UrxRenderContext::new(1.0);
ctx.begin_frame(surf_w, surf_h);
overlay_fn(&mut ctx);
let scene = ctx.take_scene();
let needs_new = state
.urx_compose_overlay_dynamic
.as_ref()
.is_none_or(|d| d.width != surf_w || d.height != surf_h);
if needs_new {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("uzor-render-hub:compose-overlay-dynamic"),
size: wgpu::Extent3d { width: surf_w, height: surf_h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: OVERLAY_TEXTURE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
state.urx_compose_overlay_dynamic =
Some(UrxComposeOverlayDynamic { texture, view, width: surf_w, height: surf_h });
}
if let Some(dynamic) = state.urx_compose_overlay_dynamic.as_ref() {
if let Some(renderer) = state.urx_dynamic_overlay_renderer.as_mut() {
if let Err(e) = renderer.render_into_encoder(
&scene,
&mut encoder,
&dynamic.view,
uzor_urx_wgpu::Viewport { width: surf_w, height: surf_h },
) {
eprintln!("[render-hub] compose urx-native dynamic-overlay render error: {:?}", e);
}
}
blit_overlay_onto(
&device,
&mut encoder,
&dynamic.view,
&swap_view,
surface_format,
&mut state.urx_compose_overlay_blitter,
);
if let Some(cap) = state.urx_capture_3d.as_ref() {
blit_overlay_onto(
&device,
&mut encoder,
&dynamic.view,
&cap.view,
surface_format,
&mut state.urx_compose_overlay_blitter,
);
}
}
}
}
queue.submit([encoder.finish()]);
frame.present();
Ok(ComposedOutcome { jobs_rendered, jobs_skipped, surface_lost: false })
}
pub fn submit_particles_composed(
state: &mut WindowRenderState,
base_color: [f32; 4],
jobs: &[ComposeParticlesJob],
) -> Result<ComposedOutcome, Submit3DError> {
let (surf_w, surf_h, surface_format) = match &state.surface {
SurfaceMode::Gpu { surface, .. } => (
surface.config.width, surface.config.height, surface.config.format,
),
_ => return Err(Submit3DError::NotGpuSurface),
};
if surf_w == 0 || surf_h == 0 { return Err(Submit3DError::ZeroSizedSurface); }
let (device, queue) = match &state.surface {
SurfaceMode::Gpu { gpu_pool, dev_id, .. } => (
gpu_pool.devices[*dev_id].device.clone(),
gpu_pool.devices[*dev_id].queue.clone(),
),
_ => return Err(Submit3DError::NotGpuSurface),
};
if state.capture_3d_enabled {
state.ensure_capture_3d(&device, surf_w, surf_h, surface_format);
}
let SurfaceMode::Gpu { surface, .. } = &mut state.surface else {
return Err(Submit3DError::NotGpuSurface);
};
let frame = match surface.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
surface.surface.configure(&device, &surface.config);
return Ok(ComposedOutcome { jobs_rendered: 0, jobs_skipped: jobs.len() as u32, surface_lost: true });
}
_ => return Ok(ComposedOutcome { jobs_rendered: 0, jobs_skipped: jobs.len() as u32, surface_lost: true }),
};
let swap_view = frame.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("uzor-render-hub:particles-compose"),
});
{
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("particles-compose:clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &swap_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: base_color[0] as f64, g: base_color[1] as f64,
b: base_color[2] as f64, a: base_color[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
if let Some(cap) = state.urx_capture_3d.as_ref() {
let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("particles-compose:clear-capture"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &cap.view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: base_color[0] as f64, g: base_color[1] as f64,
b: base_color[2] as f64, a: base_color[3] as f64,
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
if state.urx_renderer_3d.is_none() {
state.urx_renderer_3d = Some(uzor_urx_3d::Renderer3D::new(
&device, &queue, surface_format, (surf_w.max(1), surf_h.max(1)), 1024,
));
}
if state.urx_scene_3d.is_none() {
state.urx_scene_3d = Some(uzor_urx_3d::Scene3D::new());
}
let mut jobs_rendered = 0u32;
let mut jobs_skipped = 0u32;
for job in jobs {
let dx = job.dst_x.min(surf_w.saturating_sub(1));
let dy = job.dst_y.min(surf_h.saturating_sub(1));
let dw = job.dst_w.min(surf_w.saturating_sub(dx));
let dh = job.dst_h.min(surf_h.saturating_sub(dy));
if dw == 0 || dh == 0 { jobs_skipped += 1; continue; }
if !state.urx_particles.contains_key(&job.id) { jobs_skipped += 1; continue; }
let need_new = match &state.urx_offscreen_3d {
None => true,
Some(o) => o.width != dw || o.height != dh || o.format != surface_format,
};
if need_new {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("urx-particles-offscreen-compose"),
size: wgpu::Extent3d { width: dw, height: dh, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: surface_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
state.urx_offscreen_3d = Some(crate::factory::UrxOffscreen3D {
texture, view, width: dw, height: dh, format: surface_format,
});
}
{
let r3d = match state.urx_renderer_3d.as_mut() { Some(r) => r, None => { jobs_skipped += 1; continue; } };
let scene = match state.urx_scene_3d.as_ref() { Some(s) => s, None => { jobs_skipped += 1; continue; } };
let parts = match state.urx_particles.get(&job.id) { Some(p) => p, None => { jobs_skipped += 1; continue; } };
let off = match state.urx_offscreen_3d.as_ref() { Some(o) => o, None => { jobs_skipped += 1; continue; } };
r3d.render_with_particles(&device, &queue, &mut encoder, &off.view, &job.camera, scene, parts);
}
let off = state.urx_offscreen_3d.as_ref().unwrap();
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo { texture: &off.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyTextureInfo { texture: &frame.texture, mip_level: 0, origin: wgpu::Origin3d { x: dx, y: dy, z: 0 }, aspect: wgpu::TextureAspect::All },
wgpu::Extent3d { width: dw, height: dh, depth_or_array_layers: 1 },
);
if let Some(cap) = state.urx_capture_3d.as_ref() {
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo { texture: &off.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyTextureInfo { texture: &cap.texture, mip_level: 0, origin: wgpu::Origin3d { x: dx, y: dy, z: 0 }, aspect: wgpu::TextureAspect::All },
wgpu::Extent3d { width: dw, height: dh, depth_or_array_layers: 1 },
);
}
jobs_rendered += 1;
}
queue.submit([encoder.finish()]);
frame.present();
Ok(ComposedOutcome { jobs_rendered, jobs_skipped, surface_lost: false })
}
fn compose_urx_cpu_into_swap(
state: &mut WindowRenderState,
_device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
swap_view: &wgpu::TextureView,
surf_w: u32,
surf_h: u32,
scene_opt: Option<uzor_urx_core::Scene>,
base_color: [f32; 4],
) {
if state.urx_cpu_backend.is_none() {
state.urx_cpu_backend = Some(uzor_urx_cpu::CpuBackend::new());
}
let need_new_pix = match &state.urx_cpu_pixmap {
None => true,
Some(p) => p.width() != surf_w || p.height() != surf_h,
};
if need_new_pix {
state.urx_cpu_pixmap = Some(uzor_urx_cpu::Pixmap::new(surf_w, surf_h));
}
let bg_rgba = [
(base_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
(base_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
(base_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
(base_color[3] * 255.0).round().clamp(0.0, 255.0) as u8,
];
{
let backend = state.urx_cpu_backend.as_ref().expect("inited above");
let pixmap = state.urx_cpu_pixmap.as_mut().expect("inited above");
pixmap.fill(bg_rgba);
if let Some(scene) = scene_opt {
if let Err(e) = backend.render(&scene, pixmap) {
eprintln!("[render-hub] compose urx-cpu render error: {:?}", e);
}
}
}
let SurfaceMode::Gpu { surface, .. } = &mut state.surface else { return };
let (cw, ch, pix_ptr, pix_len) = {
let pixmap = state.urx_cpu_pixmap.as_ref().expect("inited above");
(pixmap.width(), pixmap.height(), pixmap.pixels().as_ptr(), pixmap.pixels().len())
};
if cw == surf_w && ch == surf_h && pix_len > 0 {
let pix: &[u8] = unsafe { std::slice::from_raw_parts(pix_ptr, pix_len) };
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &surface.target_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
pix,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * cw),
rows_per_image: Some(ch),
},
wgpu::Extent3d { width: cw, height: ch, depth_or_array_layers: 1 },
);
}
surface.blitter.copy(_device, encoder, &surface.target_view, swap_view);
if let Some(cap) = state.urx_capture_3d.as_ref() {
surface.blitter.copy(_device, encoder, &surface.target_view, &cap.view);
}
let _ = base_color;
}
fn scene_with_opaque_background(
surf_w: u32,
surf_h: u32,
base_color: [f32; 4],
scene_opt: Option<uzor_urx_core::Scene>,
) -> uzor_urx_core::Scene {
let bg_rgba = [
(base_color[0] * 255.0).round().clamp(0.0, 255.0) as u8,
(base_color[1] * 255.0).round().clamp(0.0, 255.0) as u8,
(base_color[2] * 255.0).round().clamp(0.0, 255.0) as u8,
(base_color[3] * 255.0).round().clamp(0.0, 255.0) as u8,
];
let mut scene = uzor_urx_core::Scene::new();
scene.push(uzor_urx_core::DrawCommand::FillRect {
rect: uzor_urx_core::Rect::new(0.0, 0.0, surf_w as f64, surf_h as f64),
radii: None,
brush: uzor_urx_core::math::Brush::Solid(uzor_urx_core::math::Color::from_rgba8(
bg_rgba[0], bg_rgba[1], bg_rgba[2], bg_rgba[3],
)),
transform: uzor_urx_core::Affine::IDENTITY,
});
scene.commands.extend(scene_opt.map(|s| s.commands).unwrap_or_default());
scene
}
fn compose_urx_native_into_swap(
state: &mut WindowRenderState,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
swap_view: &wgpu::TextureView,
surf_w: u32,
surf_h: u32,
scene_opt: Option<uzor_urx_core::Scene>,
base_color: [f32; 4],
) {
let sample_count = if state.urx_compose_msaa_sample_count > 1 { 4 } else { 1 };
if state
.urx_native_renderer
.as_ref()
.is_none_or(|renderer| renderer.sample_count() != sample_count)
{
state.urx_native_renderer = Some(uzor_urx_wgpu::NativeUrxRenderer::with_sample_count(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
sample_count,
));
}
let scene = scene_with_opaque_background(surf_w, surf_h, base_color, scene_opt);
let SurfaceMode::Gpu { surface, .. } = &mut state.surface else { return };
if let Some(renderer) = state.urx_native_renderer.as_mut() {
if let Err(e) = renderer.render_into_encoder(
&scene,
encoder,
&surface.target_view,
uzor_urx_wgpu::Viewport { width: surf_w, height: surf_h },
) {
eprintln!("[render-hub] compose urx-native render error: {:?}", e);
}
}
surface.blitter.copy(device, encoder, &surface.target_view, swap_view);
if let Some(cap) = state.urx_capture_3d.as_ref() {
surface.blitter.copy(device, encoder, &surface.target_view, &cap.view);
}
}
const OVERLAY_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
struct UploadedOverlay {
texture: wgpu::Texture,
view: wgpu::TextureView,
}
fn build_overlay_texture_native(
device: &wgpu::Device,
_queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
renderer: &mut uzor_urx_wgpu::NativeUrxRenderer,
surf_w: u32,
surf_h: u32,
overlay_fn: &mut dyn FnMut(&mut dyn RenderContext),
) -> Option<UploadedOverlay> {
if surf_w == 0 || surf_h == 0 {
return None;
}
let mut ctx = uzor_render_urx::UrxRenderContext::new(1.0);
ctx.begin_frame(surf_w, surf_h);
overlay_fn(&mut ctx);
let scene = ctx.take_scene();
let overlay_tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("uzor-render-hub:compose-overlay"),
size: wgpu::Extent3d { width: surf_w, height: surf_h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: OVERLAY_TEXTURE_FORMAT,
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let view = overlay_tex.create_view(&wgpu::TextureViewDescriptor::default());
if let Err(e) = renderer.render_into_encoder(
&scene,
encoder,
&view,
uzor_urx_wgpu::Viewport { width: surf_w, height: surf_h },
) {
eprintln!("[render-hub] compose urx-native overlay render error: {:?}", e);
}
Some(UploadedOverlay { texture: overlay_tex, view })
}
fn blit_overlay_onto(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
overlay_view: &wgpu::TextureView,
target_view: &wgpu::TextureView,
target_format: wgpu::TextureFormat,
cached_blitter: &mut Option<(wgpu::TextureFormat, wgpu::util::TextureBlitter)>,
) {
let needs_rebuild = cached_blitter.as_ref().is_none_or(|(format, _)| *format != target_format);
if needs_rebuild {
let blitter = wgpu::util::TextureBlitterBuilder::new(device, target_format)
.blend_state(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING)
.build();
*cached_blitter = Some((target_format, blitter));
}
if let Some((_, blitter)) = cached_blitter.as_ref() {
blitter.copy(device, encoder, overlay_view, target_view);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("uzor-render-hub-compose-test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::default(),
}))
.ok()
}
fn rgba_texture(
device: &wgpu::Device,
label: &'static str,
width: u32,
height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
fn readback_pixel(
device: &wgpu::Device,
queue: &wgpu::Queue,
texture: &wgpu::Texture,
width: u32,
height: u32,
x: u32,
y: u32,
) -> [u8; 4] {
let aligned_stride = (width * 4 + 255) & !255;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("uzor-render-hub-compose-test-readback"),
size: (aligned_stride * height) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(aligned_stride),
rows_per_image: Some(height),
},
},
wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
);
queue.submit(Some(encoder.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = tx.send(result);
});
let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
rx.recv()
.expect("map_async callback channel closed before firing")
.expect("staging buffer map failed");
let mapped = slice.get_mapped_range();
let offset = (y * aligned_stride + x * 4) as usize;
let pixel = [mapped[offset], mapped[offset + 1], mapped[offset + 2], mapped[offset + 3]];
drop(mapped);
staging.unmap();
pixel
}
fn solid_scene(width: u32, height: u32, color: [u8; 4]) -> uzor_urx_core::Scene {
let mut scene = uzor_urx_core::Scene::new();
scene.push(uzor_urx_core::DrawCommand::FillRect {
rect: uzor_urx_core::Rect::new(0.0, 0.0, width as f64, height as f64),
radii: None,
brush: uzor_urx_core::math::Brush::Solid(
uzor_urx_core::math::Color::from_rgba8(color[0], color[1], color[2], color[3]),
),
transform: uzor_urx_core::Affine::IDENTITY,
});
scene
}
fn probe_scene() -> uzor_urx_core::Scene {
let mut scene = uzor_urx_core::Scene::new();
scene.push(uzor_urx_core::DrawCommand::FillRect {
rect: uzor_urx_core::Rect::new(1.0, 2.0, 3.0, 4.0),
radii: None,
brush: uzor_urx_core::math::Brush::Solid(uzor_urx_core::math::Color::from_rgba8(9, 8, 7, 6)),
transform: uzor_urx_core::Affine::IDENTITY,
});
scene
}
#[test]
fn opaque_background_is_first_and_consumer_commands_follow_in_order() {
let scene = scene_with_opaque_background(64, 32, [0.5, 0.25, 0.75, 1.0], Some(probe_scene()));
assert_eq!(scene.commands.len(), 2, "background + the one consumer command");
match &scene.commands[0] {
uzor_urx_core::DrawCommand::FillRect { rect, radii, brush, transform } => {
assert_eq!(*rect, uzor_urx_core::Rect::new(0.0, 0.0, 64.0, 32.0), "background must cover the full surface");
assert!(radii.is_none(), "background is a plain rect, no radii");
assert_eq!(*transform, uzor_urx_core::Affine::IDENTITY);
match brush {
uzor_urx_core::math::Brush::Solid(c) => {
let rgba = c.to_rgba8();
assert_eq!([rgba.r, rgba.g, rgba.b, rgba.a], [128, 64, 191, 255]);
}
other => panic!("background brush must be Solid, got {other:?}"),
}
}
other => panic!("first command must be the background FillRect, got {other:?}"),
}
match &scene.commands[1] {
uzor_urx_core::DrawCommand::FillRect { rect, .. } => {
assert_eq!(*rect, uzor_urx_core::Rect::new(1.0, 2.0, 3.0, 4.0), "consumer command must follow unchanged, in order");
}
other => panic!("second command must be the consumer's own FillRect, got {other:?}"),
}
}
#[test]
fn none_scene_emits_only_the_background_command() {
let scene = scene_with_opaque_background(10, 20, [1.0, 1.0, 1.0, 1.0], None);
assert_eq!(scene.commands.len(), 1, "no consumer scene -> just the background");
assert!(matches!(&scene.commands[0], uzor_urx_core::DrawCommand::FillRect { .. }));
}
#[test]
fn background_rect_tracks_the_supplied_surface_size() {
let scene = scene_with_opaque_background(1920, 1080, [0.0, 0.0, 0.0, 1.0], None);
match &scene.commands[0] {
uzor_urx_core::DrawCommand::FillRect { rect, .. } => {
assert_eq!(*rect, uzor_urx_core::Rect::new(0.0, 0.0, 1920.0, 1080.0));
}
_ => unreachable!("checked above"),
}
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn independent_native_renderers_preserve_cached_and_dynamic_overlay_payloads_before_single_submit() {
let Some((device, queue)) = test_device() else { return };
const SIZE: u32 = 32;
let (cached_texture, cached_view) =
rgba_texture(&device, "uzor-render-hub-compose-test-cached", SIZE, SIZE);
let (dynamic_texture, dynamic_view) =
rgba_texture(&device, "uzor-render-hub-compose-test-dynamic", SIZE, SIZE);
let mut cached_renderer = uzor_urx_wgpu::NativeUrxRenderer::new(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
);
let mut dynamic_renderer = uzor_urx_wgpu::NativeUrxRenderer::new(
device.clone(),
queue.clone(),
wgpu::TextureFormat::Rgba8Unorm,
);
let cached_scene = solid_scene(SIZE, SIZE, [210, 35, 45, 255]);
let dynamic_scene = solid_scene(SIZE, SIZE, [35, 210, 80, 255]);
let viewport = uzor_urx_wgpu::Viewport { width: SIZE, height: SIZE };
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("uzor-render-hub-compose-test-shared-encoder"),
});
cached_renderer
.render_into_encoder(&cached_scene, &mut encoder, &cached_view, viewport)
.expect("cached overlay scene must record");
dynamic_renderer
.render_into_encoder(&dynamic_scene, &mut encoder, &dynamic_view, viewport)
.expect("dynamic overlay scene must record");
queue.submit(Some(encoder.finish()));
assert_eq!(
readback_pixel(&device, &queue, &cached_texture, SIZE, SIZE, 16, 16),
[210, 35, 45, 255],
"the dynamic upload must not replace the cached overlay payload",
);
assert_eq!(
readback_pixel(&device, &queue, &dynamic_texture, SIZE, SIZE, 16, 16),
[35, 210, 80, 255],
"the dynamic overlay must retain its own payload",
);
}
}