use uzor_urx_core::Scene;
use crate::factory::{SurfaceMode, WindowRenderState};
use crate::metrics::RenderMetrics;
use crate::submit::SubmitParams;
fn with_urx_scene<R>(
state: &mut WindowRenderState,
submit: impl FnOnce(&mut WindowRenderState, &Scene, u64) -> R,
) -> Option<R> {
let context = state.urx_ctx.take()?;
let result = submit(state, context.scene(), context.revision());
state.urx_ctx = Some(context);
Some(result)
}
pub fn submit_urx_cpu(state: &mut WindowRenderState, metrics: &mut RenderMetrics) -> bool {
with_urx_scene(state, |state, scene, _revision| submit_urx_cpu_scene(state, scene, metrics))
.unwrap_or(false)
}
fn submit_urx_cpu_scene(
state: &mut WindowRenderState,
scene: &Scene,
metrics: &mut RenderMetrics,
) -> bool {
enum SurfaceKind { Gpu, Software, #[cfg(target_arch = "wasm32")] Canvas2d }
let (width, height, kind) = match &state.surface {
SurfaceMode::Gpu { surface, .. } => (surface.config.width, surface.config.height, SurfaceKind::Gpu),
#[cfg(not(target_arch = "wasm32"))]
SurfaceMode::Software { width, height, .. } => (*width, *height, SurfaceKind::Software),
#[cfg(target_arch = "wasm32")]
SurfaceMode::Canvas2d { .. } => return false,
};
if width == 0 || height == 0 { return false; }
ensure_urx_cpu_resources(state, width, height);
let r2t_t0 = std::time::Instant::now();
let render_ok = {
let backend = match state.urx_cpu_backend.as_ref() { Some(b) => b, None => return false };
let pixmap = match state.urx_cpu_pixmap.as_mut() { Some(p) => p, None => return false };
pixmap.fill([0, 0, 0, 0]);
match backend.render(scene, pixmap) {
Ok(_) => true,
Err(e) => { eprintln!("[render-hub] urx-cpu render error: {:?}", e); false }
}
};
if !render_ok { return false; }
metrics.render_to_texture_us = r2t_t0.elapsed().as_micros() as u64;
match kind {
SurfaceKind::Gpu => {
let (pix_ptr, pix_len, cw, ch) = {
let pixmap = state.urx_cpu_pixmap.as_ref().expect("pixmap inited above");
let pix = pixmap.pixels();
(pix.as_ptr(), pix.len(), pixmap.width(), pixmap.height())
};
if let SurfaceMode::Gpu { gpu_pool, surface, dev_id } = &mut state.surface {
let device = &gpu_pool.devices[*dev_id].device;
let queue = &gpu_pool.devices[*dev_id].queue;
let pix: &[u8] = unsafe { std::slice::from_raw_parts(pix_ptr, pix_len) };
if !pix.is_empty() && cw == width && ch == height {
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 },
);
}
let present_t0 = std::time::Instant::now();
let lost = crate::submit::blit_and_present_urx(surface, device, queue);
metrics.present_us = present_t0.elapsed().as_micros() as u64;
return lost;
}
false
}
#[cfg(not(target_arch = "wasm32"))]
SurfaceKind::Software => {
let (pix_ptr, pix_len, cw, ch) = {
let pixmap = state.urx_cpu_pixmap.as_ref().expect("pixmap inited above");
(pixmap.pixels().as_ptr(), pixmap.pixels().len(), pixmap.width(), pixmap.height())
};
if let SurfaceMode::Software { presenter, .. } = &mut state.surface {
let pix: &[u8] = unsafe { std::slice::from_raw_parts(pix_ptr, pix_len) };
presenter.present(pix, cw, ch);
}
false
}
#[cfg(target_arch = "wasm32")]
SurfaceKind::Canvas2d => false,
}
}
fn ensure_urx_cpu_resources(state: &mut WindowRenderState, width: u32, height: u32) {
if state.urx_cpu_backend.is_none() {
state.urx_cpu_backend = Some(uzor_urx_cpu::CpuBackend::new());
}
let need_new = match &state.urx_cpu_pixmap {
None => true,
Some(p) => p.width() != width || p.height() != height,
};
if need_new {
state.urx_cpu_pixmap = Some(uzor_urx_cpu::Pixmap::new(width, height));
}
}
fn native_urx_sample_count(msaa_samples: u8) -> u32 {
match msaa_samples {
8 | 16 => 4,
_ => 1,
}
}
fn native_urx_renderer_needs_rebuild(current: Option<u32>, requested: u32) -> bool {
current != Some(requested)
}
pub fn submit_urx_wgpu(
state: &mut WindowRenderState,
params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> bool {
with_urx_scene(state, |state, scene, revision| {
submit_urx_wgpu_scene(state, scene, revision, params, metrics)
})
.unwrap_or(false)
}
fn submit_urx_wgpu_scene(
state: &mut WindowRenderState,
scene: &Scene,
scene_revision: u64,
params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> bool {
let profiled_command_count = scene.len();
let sample_count = native_urx_sample_count(params.msaa_samples);
let SurfaceMode::Gpu { ref gpu_pool, ref mut surface, dev_id } = state.surface else {
eprintln!("[render-hub] urx_wgpu requires SurfaceMode::Gpu");
return false;
};
let width = surface.config.width;
let height = surface.config.height;
if width == 0 || height == 0 { return false; }
let device = gpu_pool.devices[dev_id].device.clone();
let queue = gpu_pool.devices[dev_id].queue.clone();
let surface_texture = match surface.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return false,
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
surface.surface.configure(&device, &surface.config);
return false;
}
wgpu::CurrentSurfaceTexture::Validation => return false,
};
let surface_view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("uzor-render-hub:urx-wgpu-native"),
});
let current_sample_count = state.urx_native_renderer.as_ref().map(|renderer| renderer.sample_count());
if native_urx_renderer_needs_rebuild(current_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 r2t_t0 = std::time::Instant::now();
if let Some(renderer) = state.urx_native_renderer.as_mut() {
if let Err(e) = renderer.render_retained_into_encoder(
scene,
scene_revision,
&mut encoder,
&surface.target_view,
uzor_urx_wgpu::Viewport { width, height },
) {
eprintln!("[render-hub] submit_urx_wgpu native render error: {:?}", e);
}
}
metrics.render_to_texture_us = r2t_t0.elapsed().as_micros() as u64;
let present_t0 = std::time::Instant::now();
surface.blitter.copy(&device, &mut encoder, &surface.target_view, &surface_view);
queue.submit([encoder.finish()]);
surface_texture.present();
metrics.present_us = present_t0.elapsed().as_micros() as u64;
if std::env::var_os("UZOR_PROFILE_FRAMES").is_some() {
eprintln!(
"[uzor-urx-profile] revision={scene_revision} commands={profiled_command_count} \
encode_us={} present_us={}",
metrics.render_to_texture_us,
metrics.present_us,
);
}
false
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RegionSubmitOutcome {
pub skipped: bool,
pub surface_lost: bool,
pub dirty_regions: u32,
}
pub fn submit_urx_regions(
state: &mut WindowRenderState,
_params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> RegionSubmitOutcome {
let Some(engine) = state.urx_engine.as_mut() else {
return RegionSubmitOutcome::default();
};
if engine.needs_paint().is_none() {
return RegionSubmitOutcome { skipped: true, ..Default::default() };
}
let (width, height) = match &state.surface {
SurfaceMode::Gpu { surface, .. } => (surface.config.width, surface.config.height),
#[cfg(not(target_arch = "wasm32"))]
SurfaceMode::Software { width, height, .. } => (*width, *height),
#[cfg(target_arch = "wasm32")]
SurfaceMode::Canvas2d { .. } => return RegionSubmitOutcome::default(),
};
if width == 0 || height == 0 { return RegionSubmitOutcome::default(); }
let need_new = match &state.urx_cpu_pixmap {
None => true,
Some(p) => p.width() != width || p.height() != height,
};
if need_new {
state.urx_cpu_pixmap = Some(uzor_urx_cpu::Pixmap::new(width, height));
if let Some(engine) = state.urx_engine.as_mut() {
engine.invalidate_all();
}
}
let r2t_t0 = std::time::Instant::now();
let dirty_regions = {
let engine = state.urx_engine.as_mut().expect("checked above");
let pixmap = state.urx_cpu_pixmap.as_mut().expect("inited above");
match engine.render(uzor_urx_engine::RenderTarget::Cpu(pixmap)) {
Ok(stats) => stats.regions_dirty,
Err(e) => {
eprintln!("[render-hub] urx region render error: {:?}", e);
return RegionSubmitOutcome::default();
}
}
};
metrics.render_to_texture_us = r2t_t0.elapsed().as_micros() as u64;
let (pix_ptr, pix_len, cw, ch) = {
let pixmap = state.urx_cpu_pixmap.as_ref().expect("inited above");
let pix = pixmap.pixels();
(pix.as_ptr(), pix.len(), pixmap.width(), pixmap.height())
};
match &mut state.surface {
SurfaceMode::Gpu { gpu_pool, surface, dev_id } => {
let device = &gpu_pool.devices[*dev_id].device;
let queue = &gpu_pool.devices[*dev_id].queue;
let pix: &[u8] = unsafe { std::slice::from_raw_parts(pix_ptr, pix_len) };
if !pix.is_empty() && cw == width && ch == height {
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 },
);
}
let present_t0 = std::time::Instant::now();
let lost = crate::submit::blit_and_present_urx(surface, device, queue);
metrics.present_us = present_t0.elapsed().as_micros() as u64;
RegionSubmitOutcome { skipped: false, surface_lost: lost, dirty_regions }
}
#[cfg(not(target_arch = "wasm32"))]
SurfaceMode::Software { presenter, .. } => {
let pix: &[u8] = unsafe { std::slice::from_raw_parts(pix_ptr, pix_len) };
presenter.present(pix, cw, ch);
RegionSubmitOutcome { skipped: false, surface_lost: false, dirty_regions }
}
#[cfg(target_arch = "wasm32")]
SurfaceMode::Canvas2d { .. } => RegionSubmitOutcome::default(),
}
}
pub fn submit_urx_hybrid(
state: &mut WindowRenderState,
params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> bool {
with_urx_scene(state, |state, scene, _revision| {
submit_urx_hybrid_scene(state, scene, params, metrics)
})
.unwrap_or(false)
}
fn submit_urx_hybrid_scene(
state: &mut WindowRenderState,
scene: &Scene,
params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> bool {
let SurfaceMode::Gpu { ref gpu_pool, ref mut surface, dev_id } = state.surface else {
eprintln!("[render-hub] urx_hybrid requires SurfaceMode::Gpu");
return false;
};
let width = surface.config.width;
let height = surface.config.height;
if width == 0 || height == 0 { return false; }
let device = &gpu_pool.devices[dev_id].device;
let queue = &gpu_pool.devices[dev_id].queue;
if state.urx_cpu_backend.is_none() {
state.urx_cpu_backend = Some(uzor_urx_cpu::CpuBackend::new());
}
if state.urx_hybrid_backend.is_none() {
state.urx_hybrid_backend = Some(uzor_urx_hybrid::HybridBackend::new());
}
let need_new_pixmap = match &state.urx_cpu_pixmap {
None => true,
Some(p) => p.width() != width || p.height() != height,
};
if need_new_pixmap {
state.urx_cpu_pixmap = Some(uzor_urx_cpu::Pixmap::new(width, height));
}
let r2t_t0 = std::time::Instant::now();
let render_ok = {
let backend = state.urx_cpu_backend.as_ref().expect("inited above");
let pixmap = state.urx_cpu_pixmap.as_mut().expect("inited above");
pixmap.fill([0, 0, 0, 0]);
backend.render(scene, pixmap).is_ok()
};
if !render_ok { return false; }
let region_id = uzor_urx_core::region::RegionId(0);
let pixmap_clone = state.urx_cpu_pixmap.as_ref().expect("inited above").clone();
if let Some(ref mut hb) = state.urx_hybrid_backend {
hb.upsert_region_pixmap(device, queue, region_id, &pixmap_clone);
}
metrics.render_to_texture_us = r2t_t0.elapsed().as_micros() as u64;
let surface_texture = match surface.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return false,
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
surface.surface.configure(device, &surface.config);
return false;
}
wgpu::CurrentSurfaceTexture::Validation => return false,
};
let surface_view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
let format = surface_texture.texture.format();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("urx-hybrid-encoder"),
});
let _ = params;
let present_t0 = std::time::Instant::now();
let instances: [(uzor_urx_core::region::RegionId, uzor_urx_hybrid::QuadInstance); 1] = [
(region_id, uzor_urx_hybrid::QuadInstance::new(
0.0, 0.0, width as f32, height as f32,
)),
];
if let Some(ref mut hb) = state.urx_hybrid_backend {
hb.composite(
device, queue, &mut encoder, &surface_view, format,
width, height, &instances,
);
}
queue.submit(std::iter::once(encoder.finish()));
surface_texture.present();
metrics.present_us = present_t0.elapsed().as_micros() as u64;
false
}
pub fn submit_urx_wgpu_full(
state: &mut WindowRenderState,
params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> bool {
with_urx_scene(state, |state, scene, _revision| {
submit_urx_wgpu_full_scene(state, scene, params, metrics)
})
.unwrap_or(false)
}
fn submit_urx_wgpu_full_scene(
state: &mut WindowRenderState,
scene: &Scene,
params: &SubmitParams,
metrics: &mut RenderMetrics,
) -> bool {
let SurfaceMode::Gpu { ref gpu_pool, ref mut surface, dev_id } = state.surface else {
eprintln!("[render-hub] urx_wgpu_full requires SurfaceMode::Gpu");
return false;
};
let width = surface.config.width;
let height = surface.config.height;
if width == 0 || height == 0 { return false; }
let device = gpu_pool.devices[dev_id].device.clone();
let queue = gpu_pool.devices[dev_id].queue.clone();
if state.urx_wgpu_full_backend.is_none() {
let surface_texture_format = surface.config.format;
state.urx_wgpu_full_backend = Some(uzor_urx_wgpu_full::WgpuFullBackend::new(
device.clone(), queue.clone(), surface_texture_format,
));
}
if let Some(ref mut backend) = state.urx_wgpu_full_backend {
backend.resize(width, height);
}
let surface_texture = match surface.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t) | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded => return false,
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
surface.surface.configure(&device, &surface.config);
return false;
}
wgpu::CurrentSurfaceTexture::Validation => return false,
};
let surface_view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("urx-wgpu-full-encoder"),
});
let _ = params;
let r2t_t0 = std::time::Instant::now();
if let Some(ref mut backend) = state.urx_wgpu_full_backend {
if let Err(e) = backend.submit(scene, &mut encoder, &surface_view) {
eprintln!("[render-hub] urx_wgpu_full submit error: {:?}", e);
}
}
queue.submit(std::iter::once(encoder.finish()));
metrics.render_to_texture_us = r2t_t0.elapsed().as_micros() as u64;
let present_t0 = std::time::Instant::now();
surface_texture.present();
metrics.present_us = present_t0.elapsed().as_micros() as u64;
false
}
#[cfg(test)]
mod tests {
use uzor::layout::window::SoftwarePresenter;
use uzor::render::ShapeHelpers;
use uzor_urx_core::math::{Affine, Brush, Color, Gradient};
use uzor_urx_core::scene::{DrawCommand, Scene};
use uzor_urx_core::Rect;
struct NoopPresenter;
impl SoftwarePresenter for NoopPresenter {
fn present(&mut self, _pixels: &[u8], _width: u32, _height: u32) {}
fn resize(&mut self, _width: u32, _height: u32) {}
}
#[test]
fn with_urx_scene_restores_and_retains_submitted_scene() {
let mut state = crate::factory::WindowRenderState::new_cpu(64, 64, Box::new(NoopPresenter));
let mut context = uzor_render_urx::UrxRenderContext::new(1.0);
context.begin_frame(64, 64);
for x in 0..32 {
ShapeHelpers::fill_rect(&mut context, x as f64, 0.0, 1.0, 1.0);
}
let capacity = context.scene().commands.capacity();
let revision = context.revision();
state.urx_ctx = Some(context);
let submitted_len = super::with_urx_scene(&mut state, |state, scene, submitted_revision| {
assert!(state.urx_ctx.is_none());
assert_eq!(submitted_revision, revision);
scene.len()
});
assert_eq!(submitted_len, Some(32));
let context = state.urx_ctx.as_ref().expect("context must be restored after submit");
assert_eq!(context.scene().len(), 32);
assert_eq!(context.scene().commands.capacity(), capacity);
assert_eq!(context.revision(), revision);
}
#[test]
fn native_urx_msaa_requests_resolve_to_supported_pipeline_counts() {
assert_eq!(super::native_urx_sample_count(0), 1);
assert_eq!(super::native_urx_sample_count(1), 1);
assert_eq!(super::native_urx_sample_count(4), 1);
assert_eq!(super::native_urx_sample_count(8), 4);
assert_eq!(super::native_urx_sample_count(16), 4);
}
#[test]
fn native_urx_renderer_rebuild_gate_only_tracks_resolved_count_changes() {
assert!(super::native_urx_renderer_needs_rebuild(None, 1));
assert!(!super::native_urx_renderer_needs_rebuild(Some(1), 1));
assert!(super::native_urx_renderer_needs_rebuild(Some(1), 4));
assert!(!super::native_urx_renderer_needs_rebuild(Some(4), 4));
}
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-submit-urx-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 readback_rgba(device: &wgpu::Device, queue: &wgpu::Queue, texture: &wgpu::Texture, width: u32, height: u32) -> Vec<u8> {
let aligned_stride = (width * 4 + 255) & !255;
let buf_size = (aligned_stride * height) as u64;
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("uzor-render-hub-submit-urx-test-readback"),
size: buf_size,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
enc.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(enc.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
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 raw = slice.get_mapped_range();
let mut out = Vec::with_capacity((width * height * 4) as usize);
for row in 0..height as usize {
let row_start = row * aligned_stride as usize;
let row_end = row_start + (width * 4) as usize;
out.extend_from_slice(&raw[row_start..row_end]);
}
drop(raw);
staging.unmap();
out
}
#[test]
#[ignore = "needs a headless GPU adapter"]
fn native_cutover_renders_a_radial_gradient_not_a_degraded_solid() {
let Some((device, queue)) = test_device() else { return };
const SIZE: u32 = 64;
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
let mut renderer = uzor_urx_wgpu::NativeUrxRenderer::new(device.clone(), queue.clone(), FORMAT);
let mut scene = Scene::new();
scene.push(DrawCommand::FillRect {
rect: Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64),
radii: None,
brush: Brush::Solid(Color::from_rgba8(20, 20, 20, 255)),
transform: Affine::IDENTITY,
});
let gradient = Gradient::new_radial((32.0, 32.0), 30.0).with_stops([
(0.0f32, Color::from_rgba8(230, 60, 60, 255)),
(1.0f32, Color::from_rgba8(60, 90, 230, 255)),
]);
scene.push(DrawCommand::FillRect {
rect: Rect::new(2.0, 2.0, 62.0, 62.0),
radii: None,
brush: Brush::Gradient(gradient),
transform: Affine::IDENTITY,
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("uzor-render-hub-submit-urx-test-target"),
size: wgpu::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
renderer
.render_into_encoder(&scene, &mut encoder, &view, uzor_urx_wgpu::Viewport { width: SIZE, height: SIZE })
.expect("a well-formed gradient scene must not error");
queue.submit(Some(encoder.finish()));
let pixels = readback_rgba(&device, &queue, &texture, SIZE, SIZE);
let idx = ((32u32 * SIZE + 12u32) * 4) as usize;
let px = [pixels[idx], pixels[idx + 1], pixels[idx + 2], pixels[idx + 3]];
assert_ne!(
px, [230, 60, 60, 255],
"must be a real interpolated gradient colour, not a flat first-stop solid (the old adapter's degrade): got {px:?}"
);
assert_ne!(px, [20, 20, 20, 255], "must be inside the gradient rect, not reading the background: got {px:?}");
}
}