use std::ffi::c_void;
use noesis_bevy::render_device::WgpuRenderDevice;
use noesis_runtime::render_device::types::{
Batch, BlendMode, MinMagFilter, MipFilter, RenderState, SamplerState, Shader, StencilMode,
TextureFormat, UniformData, WrapMode,
};
use noesis_runtime::render_device::{RenderDevice, TextureDesc};
const TARGET_W: u32 = 32;
const TARGET_H: u32 = 32;
const TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const BYTES_PER_ROW: u32 = 256;
const CLEAR: [u8; 4] = [0, 0, 64, 255];
#[test]
fn path_pattern_clamp_and_repeat_draw_their_variants() {
if let (Ok(name), Ok(key)) = (
std::env::var("NOESIS_LICENSE_NAME"),
std::env::var("NOESIS_LICENSE_KEY"),
) {
noesis_runtime::set_license(&name, &key);
}
noesis_runtime::init();
pollster::block_on(run_test());
noesis_runtime::shutdown();
}
#[allow(clippy::too_many_lines)]
async fn run_test() {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
})
.await
.expect("no wgpu adapter");
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("noesis_runtime pattern-wrap test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::default(),
experimental_features: wgpu::ExperimentalFeatures::default(),
trace: wgpu::Trace::Off,
})
.await
.expect("no wgpu device");
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("pattern-wrap target"),
size: wgpu::Extent3d {
width: TARGET_W,
height: TARGET_H,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TARGET_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
{
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("clear"),
});
enc.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("clear"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: f64::from(CLEAR[0]) / 255.0,
g: f64::from(CLEAR[1]) / 255.0,
b: f64::from(CLEAR[2]) / 255.0,
a: f64::from(CLEAR[3]) / 255.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
queue.submit(Some(enc.finish()));
}
let device_view = target.create_view(&wgpu::TextureViewDescriptor::default());
let mut rd = WgpuRenderDevice::new(device.clone(), queue.clone());
rd.set_onscreen_target(device_view, TARGET_W, TARGET_H);
let pattern_texels: [u8; 16] = [
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
];
let level_data = [&pattern_texels[..]];
let pattern_binding = rd.create_texture(TextureDesc {
label: "2x2 pattern",
width: 2,
height: 2,
num_levels: 1,
format: TextureFormat::Rgba8,
data: Some(&level_data),
});
let sampler_state = SamplerState::new(
WrapMode::ClampToEdge,
MinMagFilter::Nearest,
MipFilter::Disabled,
);
rd.test_set_forced_pattern(Some((pattern_binding.handle, sampler_state)));
let mut vb: Vec<u8> = Vec::new();
push_pos_tex0_rect(&mut vb, [-1.0, -1.0], [0.0, 1.0], [0.25, 0.25, 0.75, 0.75]);
push_pos_tex0_rect(&mut vb, [0.0, -1.0], [1.0, 1.0], [0.25, 0.25, 0.75, 0.75]);
push_pos_tex0_rect(&mut vb, [-1.0, 1.0], [0.0, 0.0], [0.25, 0.25, 0.75, 0.75]);
push_pos_tex0_rect(&mut vb, [0.0, 1.0], [1.0, 0.0], [0.25, 0.25, 0.75, 0.75]);
assert_eq!(vb.len(), 96);
push_pos_tex0_rect_tile(
&mut vb,
[0.0, -1.0],
[0.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
);
push_pos_tex0_rect_tile(
&mut vb,
[1.0, -1.0],
[2.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
);
push_pos_tex0_rect_tile(
&mut vb,
[0.0, 1.0],
[0.0, 0.0],
[0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
);
push_pos_tex0_rect_tile(
&mut vb,
[1.0, 1.0],
[2.0, 0.0],
[0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
);
assert_eq!(vb.len(), 96 + 160);
let quad_idx = [0u16, 1, 2, 1, 3, 2];
let mut ib = Vec::with_capacity(24);
for _ in 0..2 {
for i in quad_idx {
ib.extend_from_slice(&i.to_le_bytes());
}
}
let identity_mat: [f32; 16] = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let ps_uniform0: [f32; 4] = [1.0, 0.0, 0.0, 0.0];
rd.begin_onscreen_render();
rd.map_vertices(vb.len() as u32).copy_from_slice(&vb);
rd.unmap_vertices();
rd.map_indices(ib.len() as u32).copy_from_slice(&ib);
rd.unmap_indices();
let clamp = make_pattern_batch(
Shader::PATH_PATTERN_CLAMP,
0,
0,
4,
6,
&identity_mat,
&ps_uniform0,
sampler_state,
);
rd.draw_batch(&clamp);
let repeat = make_pattern_batch(
Shader::PATH_PATTERN_REPEAT,
96,
6,
4,
6,
&identity_mat,
&ps_uniform0,
sampler_state,
);
rd.draw_batch(&repeat);
rd.end_onscreen_render();
rd.test_set_forced_pattern(None);
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback"),
size: u64::from(BYTES_PER_ROW) * u64::from(TARGET_H),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
{
let mut enc = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("readback"),
});
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(BYTES_PER_ROW),
rows_per_image: Some(TARGET_H),
},
},
wgpu::Extent3d {
width: TARGET_W,
height: TARGET_H,
depth_or_array_layers: 1,
},
);
queue.submit(Some(enc.finish()));
}
let slice = readback.slice(..);
let (sender, receiver) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
sender.send(result).expect("readback send");
});
let _ = device.poll(wgpu::PollType::wait_indefinitely());
receiver.recv().expect("readback recv").expect("map");
let data = slice.get_mapped_range();
let pixel = |x: u32, y: u32| -> [u8; 4] {
let offset = (y * BYTES_PER_ROW + x * 4) as usize;
[
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]
};
let top_left = pixel(5, 10);
assert_eq!(
top_left,
[255, 0, 0, 255],
"CLAMP inside-rect top-left quadrant = red; got {top_left:?}",
);
let bottom_right = pixel(10, 20);
assert_eq!(
bottom_right,
[255, 255, 0, 255],
"CLAMP inside-rect bottom-right quadrant = yellow; got {bottom_right:?}",
);
let top_right = pixel(8, 10);
assert_eq!(
top_right,
[0, 255, 0, 255],
"CLAMP inside-rect top-right quadrant = green; got {top_right:?}",
);
let outside_nw = pixel(2, 2);
assert_eq!(
outside_nw,
[0, 0, 0, 0],
"CLAMP outside-rect → transparent black; got {outside_nw:?}",
);
let outside_se = pixel(12, 27);
assert_eq!(
outside_se,
[0, 0, 0, 0],
"CLAMP outside-rect → transparent black; got {outside_se:?}",
);
let tile0_top = pixel(17, 8);
let tile1_top = pixel(25, 8);
assert_eq!(
tile0_top,
[255, 0, 0, 255],
"REPEAT tile 0 top-left = red; got {tile0_top:?}",
);
assert_eq!(
tile0_top, tile1_top,
"REPEAT: two x-offsets into the same tile-local coord must match. \
got tile0={tile0_top:?}, tile1={tile1_top:?}",
);
let tile0_bot = pixel(17, 24);
assert_eq!(
tile0_bot,
[0, 0, 255, 255],
"REPEAT tile 0 bottom-left = blue; got {tile0_bot:?}",
);
drop(data);
readback.unmap();
}
fn push_pos_tex0_rect(out: &mut Vec<u8>, pos: [f32; 2], uv0: [f32; 2], rect: [f32; 4]) {
out.extend_from_slice(&pos[0].to_le_bytes());
out.extend_from_slice(&pos[1].to_le_bytes());
out.extend_from_slice(&uv0[0].to_le_bytes());
out.extend_from_slice(&uv0[1].to_le_bytes());
for v in rect {
let q = (v.clamp(0.0, 1.0) * f32::from(u16::MAX)).round() as u16;
out.extend_from_slice(&q.to_le_bytes());
}
}
fn push_pos_tex0_rect_tile(
out: &mut Vec<u8>,
pos: [f32; 2],
uv0: [f32; 2],
rect: [f32; 4],
tile: [f32; 4],
) {
push_pos_tex0_rect(out, pos, uv0, rect);
for v in tile {
out.extend_from_slice(&v.to_le_bytes());
}
}
#[allow(clippy::too_many_arguments)]
fn make_pattern_batch(
shader: Shader,
vertex_offset: u32,
start_index: u32,
num_vertices: u32,
num_indices: u32,
vs_uniforms: &[f32; 16],
ps_uniforms: &[f32; 4],
pattern_sampler: SamplerState,
) -> Batch {
Batch {
shader,
render_state: RenderState::new(true, BlendMode::Src, StencilMode::Disabled, false),
stencil_ref: 0,
single_pass_stereo: false,
vertex_offset,
num_vertices,
start_index,
num_indices,
pattern: std::ptr::dangling_mut(),
ramps: std::ptr::null_mut(),
image: std::ptr::null_mut(),
glyphs: std::ptr::null_mut(),
shadow: std::ptr::null_mut(),
pattern_sampler,
ramps_sampler: SamplerState::default(),
image_sampler: SamplerState::default(),
glyphs_sampler: SamplerState::default(),
shadow_sampler: SamplerState::default(),
vertex_uniforms: [
UniformData {
values: vs_uniforms.as_ptr().cast::<c_void>(),
num_dwords: 16,
hash: 1,
},
UniformData::default(),
],
pixel_uniforms: [
UniformData {
values: ps_uniforms.as_ptr().cast::<c_void>(),
num_dwords: 4,
hash: 2,
},
UniformData::default(),
],
pixel_shader: std::ptr::null_mut(),
}
}