use std::sync::{Arc, Mutex};
use crate::gfx::backend::{ChunkMesh, DeviceCapabilities, FrameParams, GpuProfile, RenderBackend};
use crate::gfx::backend_init::{BackendInit, ShadowParams, SwapchainConfig};
use crate::gfx::error::{RenderError, RenderResult};
use crate::gfx::input::RenderInput;
use crate::gfx::mesh_payload::{SkinnedVertex, Vertex};
use crate::gfx::render_types::{DrawObject, MaterialUniforms, SkinnedDrawObject};
use crate::gfx::scene_flow::SceneControl;
pub(crate) struct TestHooks {
pub settings: crate::config::Settings,
pub gpu_profile: GpuProfile,
pub(crate) backend_factory: BackendFactory,
}
pub(crate) type BackendFactory =
Box<dyn FnMut(BackendInit<'_>) -> Option<Box<dyn RenderBackend>> + Send>;
pub(crate) struct InitSnapshot {
pub(crate) window_width: u32,
pub(crate) window_height: u32,
pub(crate) window_title: String,
pub clear_color: [f32; 4],
pub(crate) frames_in_flight: usize,
pub vsync: bool,
pub vertex_count: usize,
pub index_count: usize,
pub(crate) draw_objects: Vec<DrawObject>,
pub(crate) instanced_cluster_count: usize,
pub(crate) n_skinned: usize,
pub texture_count: usize,
pub(crate) text_atlas_count: usize,
pub shadows: ShadowParams,
pub(crate) anisotropy: u32,
pub(crate) scene_required: bool,
pub fog: bool,
pub(crate) taa_enabled: bool,
pub(crate) ssao_on: bool,
pub(crate) rt_reflections_on: bool,
pub(crate) rt_dynamic: concinnity_core::render::rt_geom::RtDynamicMode,
pub(crate) rt_skinned_geometry: bool,
pub(crate) shader_stage_lens: Vec<(usize, usize)>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Call {
CaptureCursor,
WaitIdle,
ReloadWorld,
TakeInput,
DrawFrame {
world_hidden: bool,
text_calls: usize,
cam_pos: [f32; 3],
view_mode: crate::gfx::view_modes::ViewMode,
show: crate::gfx::view_modes::ShowFlags,
},
UpdateView([[f32; 4]; 4]),
UpdateModel(usize),
RetireDrawObject(usize),
UploadSkinned {
vertices: usize,
draws: usize,
},
UpdateSkinnedPose(usize),
UpdateSkinnedModel(usize),
RevealSkinnedInstance(usize),
EvictTextureSlot(usize),
UpdateTextureSlot {
slot: usize,
w: u32,
h: u32,
},
EvictMesh(usize),
UploadMesh {
draw_idx: usize,
vertices: usize,
indices: usize,
},
SeedMeshStreaming,
SetupChunkStreaming,
AddChunkMesh,
RemoveChunkMesh(usize),
SetChunkModel(usize),
SetUiCursorHidden(bool),
SetMenuMode(bool),
SetCameraCapture(bool),
SetReflectionProbes(usize),
SetVsync(bool),
SetWindowMode(crate::components::WindowMode),
SetWindowSize(u32, u32),
SetDisplayMode(crate::gfx::display_mode::DisplayMode),
SetAmbientIntensity(f32),
UpdateDirectionalLights(Vec<([f32; 3], [f32; 3], f32)>),
UpdateFogSettings(Option<crate::gfx::volumetric_fog::FogSettings>),
SetKeymap,
SetShadowUpdate,
SetShadowDistance(u32),
SetShadowCascades(u32),
UpdatePostProcess,
ApplyQualitySettings,
UpdateQualityParams,
CloneStaticDrawObject {
src: usize,
new_idx: usize,
},
UpdateVisibility {
draw_idx: usize,
visible: bool,
},
SetDrawMaterial {
draw_idx: usize,
texture_slot: usize,
normal_map_slot: usize,
},
SetDrawCullDistance(usize, f32),
SetFade(f32),
}
pub(crate) struct MockState {
pub calls: Vec<Call>,
pub init: Option<InitSnapshot>,
pub models: std::collections::HashMap<usize, [[f32; 4]; 4]>,
pub visibility: std::collections::HashMap<usize, bool>,
pub window_closed: bool,
pub(crate) fail_draw: Option<RenderError>,
pub(crate) fail_reload: Option<String>,
pub(crate) next_input: RenderInput,
pub(crate) logical_size: (f32, f32),
pub caps: DeviceCapabilities,
}
impl Default for MockState {
fn default() -> Self {
Self {
calls: Vec::new(),
init: None,
models: std::collections::HashMap::new(),
visibility: std::collections::HashMap::new(),
window_closed: false,
fail_draw: None,
fail_reload: None,
next_input: RenderInput::default(),
logical_size: (1280.0, 720.0),
caps: DeviceCapabilities::ALL,
}
}
}
impl MockState {
pub(crate) fn saw(&self, call: &Call) -> bool {
self.calls.contains(call)
}
pub(crate) fn draw_frames(&self) -> usize {
self.calls
.iter()
.filter(|c| matches!(c, Call::DrawFrame { .. }))
.count()
}
pub(crate) fn last_draw_frame(&self) -> Option<Call> {
self.calls
.iter()
.rev()
.find(|c| matches!(c, Call::DrawFrame { .. }))
.cloned()
}
}
pub(crate) struct MockBackend {
state: Arc<Mutex<MockState>>,
hot_swap: Option<SwapchainConfig>,
}
fn record_init(state: &Arc<Mutex<MockState>>, init: BackendInit<'_>) {
let mut s = state.lock().unwrap();
s.init = Some(InitSnapshot {
shader_stage_lens: init
.shaders
.iter()
.map(|sh| (sh.vert.len(), sh.frag.len()))
.collect(),
window_width: init.window.width,
window_height: init.window.height,
window_title: init.window.title.clone(),
clear_color: init.clear_color,
frames_in_flight: init.frames_in_flight,
vsync: init.vsync,
vertex_count: init.scene.vertices.len(),
index_count: init.scene.indices.len(),
draw_objects: init.scene.draw_objects,
instanced_cluster_count: init.scene.instanced_clusters.len(),
n_skinned: init.scene.n_skinned,
texture_count: init.media.textures.len(),
text_atlas_count: init.media.text_atlases.len(),
shadows: init.shadows,
anisotropy: init.anisotropy,
scene_required: init.requirements.scene,
fog: init.fx.fog.is_some(),
taa_enabled: init.post.taa_enabled,
ssao_on: init.post.ssao.is_some(),
rt_reflections_on: init.post.rt_reflections.is_some(),
rt_dynamic: init.post.rt_dynamic,
rt_skinned_geometry: init.post.rt_skinned_geometry,
});
}
pub(crate) fn recording_backend() -> (Arc<Mutex<MockState>>, MockBackend) {
let state = Arc::new(Mutex::new(MockState::default()));
let backend = MockBackend {
state: Arc::clone(&state),
hot_swap: None,
};
(state, backend)
}
pub(crate) fn recording_hooks() -> (Arc<Mutex<MockState>>, TestHooks) {
recording_hooks_with(crate::config::Settings::default(), GpuProfile::UNKNOWN)
}
pub(crate) fn recording_hooks_with(
settings: crate::config::Settings,
gpu_profile: GpuProfile,
) -> (Arc<Mutex<MockState>>, TestHooks) {
let state = Arc::new(Mutex::new(MockState::default()));
let factory_state = Arc::clone(&state);
let backend_factory: BackendFactory = Box::new(move |init: BackendInit<'_>| {
let hot_swap = Some(init.swapchain_config());
record_init(&factory_state, init);
Some(Box::new(MockBackend {
state: Arc::clone(&factory_state),
hot_swap,
}) as Box<dyn RenderBackend>)
});
(
state,
TestHooks {
settings,
gpu_profile,
backend_factory,
},
)
}
impl MockBackend {
fn record(&self, call: Call) {
self.state.lock().unwrap().calls.push(call);
}
pub(crate) fn transplant(
state: Arc<Mutex<MockState>>,
hot_swap: Option<SwapchainConfig>,
) -> MockBackend {
MockBackend { state, hot_swap }
}
}
impl SceneControl for MockBackend {
fn update_visibility(&mut self, draw_idx: usize, visible: bool) {
let mut s = self.state.lock().unwrap();
s.visibility.insert(draw_idx, visible);
s.calls.push(Call::UpdateVisibility { draw_idx, visible });
}
fn set_fade(&mut self, fade: f32) {
self.record(Call::SetFade(fade));
}
}
impl RenderBackend for MockBackend {
fn window_closed(&mut self) -> bool {
self.state.lock().unwrap().window_closed
}
fn capture_cursor(&mut self) {
self.record(Call::CaptureCursor);
}
fn take_input(&mut self) -> RenderInput {
let mut s = self.state.lock().unwrap();
s.calls.push(Call::TakeInput);
std::mem::take(&mut s.next_input)
}
fn wait_idle(&self) {
self.record(Call::WaitIdle);
}
fn hot_swap_config(&self) -> Option<SwapchainConfig> {
self.hot_swap
}
fn reload_world(&mut self, init: BackendInit<'_>) -> RenderResult<()> {
let fail = self.state.lock().unwrap().fail_reload.clone();
self.record(Call::ReloadWorld);
if let Some(e) = fail {
return Err(e.into());
}
record_init(&self.state, init);
Ok(())
}
fn draw_frame(&mut self, params: FrameParams<'_>) -> RenderResult<()> {
let mut s = self.state.lock().unwrap();
s.calls.push(Call::DrawFrame {
world_hidden: params.world_hidden,
text_calls: params.text_calls.len(),
cam_pos: params.cam_pos,
view_mode: params.view_mode,
show: params.show,
});
match &s.fail_draw {
Some(e) => Err(e.clone()),
None => Ok(()),
}
}
fn update_view(&mut self, matrix: [[f32; 4]; 4]) {
self.record(Call::UpdateView(matrix));
}
fn update_models(&mut self, updates: &[(u32, [[f32; 4]; 4])]) {
let mut s = self.state.lock().unwrap();
for &(index, model) in updates {
s.models.insert(index as usize, model);
s.calls.push(Call::UpdateModel(index as usize));
}
}
fn update_skinned_models(&mut self, updates: &[(u32, [[f32; 4]; 4])]) {
let mut s = self.state.lock().unwrap();
for &(index, _model) in updates {
s.calls.push(Call::UpdateSkinnedModel(index as usize));
}
}
fn retire_draw_object(&mut self, draw_idx: usize) {
self.record(Call::RetireDrawObject(draw_idx));
}
fn upload_skinned(
&mut self,
vertices: &[SkinnedVertex],
_indices: &[u32],
draw_objects: Vec<SkinnedDrawObject>,
_vert_bytes: &[u8],
_frag_bytes: &[u8],
_shadow_bytes: &[u8],
) -> RenderResult<()> {
self.record(Call::UploadSkinned {
vertices: vertices.len(),
draws: draw_objects.len(),
});
Ok(())
}
fn update_skinned_pose(&mut self, skinned_index: usize, _matrices: &[[[f32; 4]; 4]]) {
self.record(Call::UpdateSkinnedPose(skinned_index));
}
fn reveal_skinned_instance(&mut self, instance_index: usize, _model: [[f32; 4]; 4]) {
self.record(Call::RevealSkinnedInstance(instance_index));
}
fn evict_texture_slot(&mut self, slot: usize) -> Result<(), String> {
self.record(Call::EvictTextureSlot(slot));
Ok(())
}
fn update_texture_slot(
&mut self,
slot: usize,
image: &crate::bake::texture::TextureImage,
) -> RenderResult<()> {
self.record(Call::UpdateTextureSlot {
slot,
w: image.width(),
h: image.height(),
});
Ok(())
}
fn evict_mesh(&mut self, draw_idx: usize, _retire_frame: u64) -> Result<(), String> {
self.record(Call::EvictMesh(draw_idx));
Ok(())
}
fn upload_mesh(
&mut self,
draw_idx: usize,
verts: &[Vertex],
idxs: &[u16],
_frame: u64,
) -> RenderResult<()> {
self.record(Call::UploadMesh {
draw_idx,
vertices: verts.len(),
indices: idxs.len(),
});
Ok(())
}
fn seed_mesh_streaming(
&mut self,
_vtx_offset: u64,
_vtx_bytes: u64,
_idx_offset: u64,
_idx_bytes: u64,
) {
self.record(Call::SeedMeshStreaming);
}
fn setup_chunk_streaming(
&mut self,
_chunk_vtx_bytes: usize,
_chunk_idx_bytes: usize,
_texture_slot: usize,
_normal_map_slot: usize,
) -> RenderResult<()> {
self.record(Call::SetupChunkStreaming);
Ok(())
}
fn add_chunk_mesh(
&mut self,
_mesh: ChunkMesh<'_>,
_dst: crate::gfx::draw_slot::SlotAlloc,
) -> RenderResult<()> {
self.record(Call::AddChunkMesh);
Ok(())
}
fn remove_chunk_mesh(&mut self, draw_idx: usize, _retire_frame: u64) -> Result<(), String> {
self.record(Call::RemoveChunkMesh(draw_idx));
Ok(())
}
fn set_chunk_model(&mut self, draw_idx: usize, _model: [[f32; 4]; 4]) -> Result<(), String> {
self.record(Call::SetChunkModel(draw_idx));
Ok(())
}
fn logical_size(&self) -> (f32, f32) {
self.state.lock().unwrap().logical_size
}
fn capabilities(&self) -> DeviceCapabilities {
self.state.lock().unwrap().caps
}
fn set_ui_cursor_hidden(&mut self, hidden: bool) {
self.record(Call::SetUiCursorHidden(hidden));
}
fn set_menu_mode(&mut self, on: bool) {
self.record(Call::SetMenuMode(on));
}
fn set_camera_capture(&mut self, capture: bool) {
self.record(Call::SetCameraCapture(capture));
}
fn set_reflection_probes(&mut self, probes: &[crate::gfx::reflection_probe::ProbePlacement]) {
self.record(Call::SetReflectionProbes(probes.len()));
}
fn set_vsync(&mut self, on: bool) {
self.record(Call::SetVsync(on));
}
fn set_window_mode(&mut self, mode: crate::components::WindowMode) {
self.record(Call::SetWindowMode(mode));
}
fn set_window_size(&mut self, width: u32, height: u32) {
self.record(Call::SetWindowSize(width, height));
}
fn set_display_mode(&mut self, mode: crate::gfx::display_mode::DisplayMode) {
self.record(Call::SetDisplayMode(mode));
}
fn set_ambient_intensity(&mut self, value: f32) {
self.record(Call::SetAmbientIntensity(value));
}
fn update_directional_lights(&mut self, lights: &[crate::components::DirectionalLight]) {
self.record(Call::UpdateDirectionalLights(
lights
.iter()
.map(|l| (l.direction, l.color, l.intensity))
.collect(),
));
}
fn update_fog_settings(&mut self, settings: Option<crate::gfx::volumetric_fog::FogSettings>) {
self.record(Call::UpdateFogSettings(settings));
}
fn set_keymap(&mut self, _keymap: &crate::gfx::keymap::KeyMap) {
self.record(Call::SetKeymap);
}
fn set_shadow_update(&mut self, _update: crate::components::ShadowUpdate) {
self.record(Call::SetShadowUpdate);
}
fn set_shadow_distance(&mut self, distance: u32) {
self.record(Call::SetShadowDistance(distance));
}
fn set_shadow_cascades(&mut self, count: u32) {
self.record(Call::SetShadowCascades(count));
}
fn update_post_process(&mut self, _tunables: crate::gfx::render_types::PostProcessTunables) {
self.record(Call::UpdatePostProcess);
}
fn apply_quality_settings(&mut self, _settings: crate::gfx::backend::QualitySettings) {
self.record(Call::ApplyQualitySettings);
}
fn update_quality_params(&mut self, _settings: crate::gfx::backend::QualitySettings) {
self.record(Call::UpdateQualityParams);
}
fn clone_static_draw_object(
&mut self,
src_draw_idx: usize,
_model: [[f32; 4]; 4],
dst: crate::gfx::draw_slot::SlotAlloc,
) -> Result<(), String> {
use crate::gfx::draw_slot::SlotAlloc;
let new_idx = match dst {
SlotAlloc::Reuse(i) | SlotAlloc::Append(i) => i,
};
self.record(Call::CloneStaticDrawObject {
src: src_draw_idx,
new_idx,
});
Ok(())
}
fn set_draw_material(
&mut self,
draw_idx: usize,
_material: MaterialUniforms,
texture_slot: usize,
normal_map_slot: usize,
) {
self.record(Call::SetDrawMaterial {
draw_idx,
texture_slot,
normal_map_slot,
});
}
fn set_draw_cull_distance(&mut self, draw_idx: usize, cull_distance: f32) {
self.record(Call::SetDrawCullDistance(draw_idx, cull_distance));
}
}