#![cfg(feature = "test-utils")]
use std::num::NonZeroU64;
use std::sync::{Arc, Mutex};
use noesis_runtime::render_device::ffi::noesis_test_run_frame_scenario;
use noesis_runtime::render_device::types::{Batch, DeviceCaps, Shader, TextureFormat, Tile};
use noesis_runtime::render_device::{
RenderDevice, RenderTargetBinding, RenderTargetDesc, RenderTargetHandle, TextureBinding,
TextureDesc, TextureHandle, TextureRect, register,
};
#[derive(Debug, Clone, PartialEq)]
enum Op {
GetCaps,
CreateTexture {
label: String,
width: u32,
height: u32,
num_levels: u32,
format: TextureFormat,
has_init_data: bool,
},
UpdateTexture {
handle: u64,
level: u32,
rect: TextureRect,
bytes_len: usize,
},
EndUpdatingTextures(Vec<u64>),
DropTexture(u64),
CreateRenderTarget {
label: String,
width: u32,
height: u32,
sample_count: u32,
needs_stencil: bool,
},
CloneRenderTarget {
label: String,
src: u64,
},
DropRenderTarget(u64),
BeginOffscreenRender,
EndOffscreenRender,
BeginOnscreenRender,
EndOnscreenRender,
SetRenderTarget(u64),
BeginTile {
handle: u64,
tile: Tile,
},
EndTile(u64),
ResolveRenderTarget {
handle: u64,
tiles: Vec<Tile>,
},
MapVertices(u32),
UnmapVertices,
MapIndices(u32),
UnmapIndices,
DrawBatch {
shader: u8,
num_vertices: u32,
num_indices: u32,
},
}
struct MockDevice {
ops: Arc<Mutex<Vec<Op>>>,
next_handle: u64,
vb: Vec<u8>,
ib: Vec<u8>,
}
impl MockDevice {
fn new(ops: Arc<Mutex<Vec<Op>>>) -> Self {
Self {
ops,
next_handle: 1,
vb: vec![0; 512 * 1024],
ib: vec![0; 128 * 1024],
}
}
fn alloc_handle(&mut self) -> NonZeroU64 {
let h = self.next_handle;
self.next_handle += 1;
NonZeroU64::new(h).expect("alloc_handle starts at 1")
}
fn push(&self, op: Op) {
self.ops.lock().expect("ops poisoned").push(op);
}
}
impl RenderDevice for MockDevice {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn caps(&self) -> DeviceCaps {
self.push(Op::GetCaps);
DeviceCaps::default()
}
fn create_texture(&mut self, desc: TextureDesc<'_>) -> TextureBinding {
self.push(Op::CreateTexture {
label: desc.label.to_owned(),
width: desc.width,
height: desc.height,
num_levels: desc.num_levels,
format: desc.format,
has_init_data: desc.data.is_some(),
});
TextureBinding {
handle: TextureHandle(self.alloc_handle()),
width: desc.width,
height: desc.height,
has_mipmaps: desc.num_levels > 1,
inverted: false,
has_alpha: !matches!(desc.format, TextureFormat::Rgbx8),
}
}
fn update_texture(
&mut self,
handle: TextureHandle,
level: u32,
rect: TextureRect,
data: &[u8],
) {
self.push(Op::UpdateTexture {
handle: handle.0.get(),
level,
rect,
bytes_len: data.len(),
});
}
fn end_updating_textures(&mut self, textures: &[TextureHandle]) {
self.push(Op::EndUpdatingTextures(
textures.iter().map(|h| h.0.get()).collect(),
));
}
fn drop_texture(&mut self, handle: TextureHandle) {
self.push(Op::DropTexture(handle.0.get()));
}
fn create_render_target(&mut self, desc: RenderTargetDesc<'_>) -> RenderTargetBinding {
self.push(Op::CreateRenderTarget {
label: desc.label.to_owned(),
width: desc.width,
height: desc.height,
sample_count: desc.sample_count,
needs_stencil: desc.needs_stencil,
});
let rt_handle = RenderTargetHandle(self.alloc_handle());
let tex_handle = TextureHandle(self.alloc_handle());
RenderTargetBinding {
handle: rt_handle,
resolve_texture: TextureBinding {
handle: tex_handle,
width: desc.width,
height: desc.height,
has_mipmaps: false,
inverted: false,
has_alpha: true,
},
}
}
fn clone_render_target(&mut self, label: &str, src: RenderTargetHandle) -> RenderTargetBinding {
self.push(Op::CloneRenderTarget {
label: label.to_owned(),
src: src.0.get(),
});
let rt_handle = RenderTargetHandle(self.alloc_handle());
let tex_handle = TextureHandle(self.alloc_handle());
RenderTargetBinding {
handle: rt_handle,
resolve_texture: TextureBinding {
handle: tex_handle,
width: 0,
height: 0,
has_mipmaps: false,
inverted: false,
has_alpha: true,
},
}
}
fn drop_render_target(&mut self, handle: RenderTargetHandle) {
self.push(Op::DropRenderTarget(handle.0.get()));
}
fn begin_offscreen_render(&mut self) {
self.push(Op::BeginOffscreenRender);
}
fn end_offscreen_render(&mut self) {
self.push(Op::EndOffscreenRender);
}
fn begin_onscreen_render(&mut self) {
self.push(Op::BeginOnscreenRender);
}
fn end_onscreen_render(&mut self) {
self.push(Op::EndOnscreenRender);
}
fn set_render_target(&mut self, handle: RenderTargetHandle) {
self.push(Op::SetRenderTarget(handle.0.get()));
}
fn begin_tile(&mut self, handle: RenderTargetHandle, tile: Tile) {
self.push(Op::BeginTile {
handle: handle.0.get(),
tile,
});
}
fn end_tile(&mut self, handle: RenderTargetHandle) {
self.push(Op::EndTile(handle.0.get()));
}
fn resolve_render_target(&mut self, handle: RenderTargetHandle, tiles: &[Tile]) {
self.push(Op::ResolveRenderTarget {
handle: handle.0.get(),
tiles: tiles.to_vec(),
});
}
fn map_vertices(&mut self, bytes: u32) -> &mut [u8] {
self.push(Op::MapVertices(bytes));
&mut self.vb[..bytes as usize]
}
fn unmap_vertices(&mut self) {
self.push(Op::UnmapVertices);
}
fn map_indices(&mut self, bytes: u32) -> &mut [u8] {
self.push(Op::MapIndices(bytes));
&mut self.ib[..bytes as usize]
}
fn unmap_indices(&mut self) {
self.push(Op::UnmapIndices);
}
fn draw_batch(&mut self, batch: &Batch) {
self.push(Op::DrawBatch {
shader: batch.shader.0,
num_vertices: batch.num_vertices,
num_indices: batch.num_indices,
});
}
}
#[test]
#[allow(clippy::too_many_lines)] fn frame_scenario_records_expected_op_sequence() {
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();
let log: Arc<Mutex<Vec<Op>>> = Arc::new(Mutex::new(Vec::new()));
let registered = register(MockDevice::new(Arc::clone(&log)));
unsafe { noesis_test_run_frame_scenario(registered.raw()) };
drop(registered);
let ops = log.lock().expect("ops poisoned");
let full_tile = Tile {
x: 0,
y: 0,
width: 256,
height: 256,
};
let expected: Vec<Op> = vec![
Op::GetCaps,
Op::CreateTexture {
label: "t_immutable".to_owned(),
width: 4,
height: 4,
num_levels: 1,
format: TextureFormat::Rgba8,
has_init_data: true,
},
Op::CreateTexture {
label: "t_dynamic".to_owned(),
width: 16,
height: 16,
num_levels: 1,
format: TextureFormat::R8,
has_init_data: false,
},
Op::UpdateTexture {
handle: 2,
level: 0,
rect: TextureRect {
x: 2,
y: 2,
width: 4,
height: 4,
},
bytes_len: 16,
},
Op::EndUpdatingTextures(vec![2]),
Op::CreateRenderTarget {
label: "rt_main".to_owned(),
width: 256,
height: 256,
sample_count: 1,
needs_stencil: true,
},
Op::BeginOffscreenRender,
Op::SetRenderTarget(3),
Op::BeginTile {
handle: 3,
tile: full_tile,
},
Op::MapVertices(96),
Op::UnmapVertices,
Op::MapIndices(36),
Op::UnmapIndices,
Op::DrawBatch {
shader: Shader::PATH_SOLID.0,
num_vertices: 4,
num_indices: 6,
},
Op::EndTile(3),
Op::ResolveRenderTarget {
handle: 3,
tiles: vec![full_tile],
},
Op::EndOffscreenRender,
Op::BeginOnscreenRender,
Op::MapVertices(96),
Op::UnmapVertices,
Op::MapIndices(36),
Op::UnmapIndices,
Op::DrawBatch {
shader: Shader::RGBA.0,
num_vertices: 4,
num_indices: 6,
},
Op::EndOnscreenRender,
Op::CloneRenderTarget {
label: "rt_clone".to_owned(),
src: 3,
},
Op::DropRenderTarget(5),
Op::DropTexture(6),
Op::DropRenderTarget(3),
Op::DropTexture(4),
Op::DropTexture(2),
Op::DropTexture(1),
];
assert_eq!(
ops.len(),
expected.len(),
"op count mismatch\n actual ({}): {:#?}\n expected ({}): {:#?}",
ops.len(),
&*ops,
expected.len(),
expected,
);
for (i, (got, want)) in ops.iter().zip(expected.iter()).enumerate() {
assert_eq!(got, want, "op {i} mismatch");
}
drop(ops);
noesis_runtime::shutdown();
}