use concinnity_core::bake::texture;
use concinnity_core::components::AaMode;
use concinnity_core::components::CharacterRig;
use concinnity_core::components::Children;
use concinnity_core::components::FrameInput;
use concinnity_core::components::GlobalTransform;
use concinnity_core::components::HDR_MULTISAMPLE_COUNT;
use concinnity_core::components::IndirectLighting;
use concinnity_core::components::Lifetime;
use concinnity_core::components::Parent;
use concinnity_core::components::PostProcessConfig;
use concinnity_core::components::ShaderPrograms;
use concinnity_core::components::SkeletonPose;
use concinnity_core::components::SkinnedMesh;
use concinnity_core::components::Spawner;
use concinnity_core::components::Transform;
use concinnity_core::components::UiAction;
use concinnity_core::components::UpscaleQuality;
use concinnity_core::components::compiled_programs;
use concinnity_core::components::{
Camera3D, DespawnRequest, GraphicsConfig, HitRegion, Material, Prop, RenderHandle,
ReparentRequest, Scene, SceneCommand, Shader, SpawnRequest, Sprite, StreamingConfig, TextLabel,
Window,
};
use concinnity_core::ecs::Entity;
use concinnity_core::ecs::FontHandle;
use concinnity_core::ecs::FrameRateCap;
use concinnity_core::ecs::GpuMemoryPressure;
use concinnity_core::ecs::HiddenAssets;
use concinnity_core::ecs::MaterialHandle;
use concinnity_core::ecs::MenuActive;
use concinnity_core::ecs::MenuOverride;
use concinnity_core::ecs::MeshHandle;
use concinnity_core::ecs::PickIndex;
use concinnity_core::ecs::Ref;
use concinnity_core::ecs::ScreenStack;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::ecs::{ComponentSlot, PayloadLocator, StepResult, TextureHandle, World};
use concinnity_core::gfx::chunk_coord;
use concinnity_core::gfx::mesh_payload;
use concinnity_core::gfx::render_types::{DrawIndex, SkinnedIndex};
use concinnity_core::render::backend;
use concinnity_core::render::backend::{GpuProfile, GpuTier, GpuVendor};
use concinnity_core::render::backend_init;
use concinnity_core::render::backend_init::SwapchainConfig;
use concinnity_core::render::error;
use concinnity_core::render::text;
use concinnity_core::resource::ColorLutTable;
use concinnity_core::resource::EnvironmentMapTable;
use concinnity_core::resource::FontTable;
use concinnity_core::resource::MaterialTable;
use concinnity_core::resource::MeshTable;
use concinnity_core::resource::SkinnedMeshTable;
use concinnity_core::resource::TextureTable;
use concinnity_core::settings::SettingKey;
use concinnity_core::transform::propagation;
use concinnity_host::store::blob::BlobData;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use super::GraphicsSystem;
use crate::app::run::LaunchRequest;
use crate::gfx::mock_backend::{
Call, MockBackend, MockState, TestHooks, recording_hooks, recording_hooks_with,
};
use crate::gfx::quality_preset::{QualityPreset, resolve_ceiling};
use crate::gfx::render_config::{GraphicsBaseline, resolve_graphics};
const MESH: AssetId = AssetId(1);
const TEX: AssetId = AssetId(2);
const MAT: AssetId = AssetId(3);
const PROP: AssetId = AssetId(4);
type PendingAdd = Box<dyn FnOnce(&mut World)>;
struct WorldBuilder {
pending: Vec<PendingAdd>,
section: Vec<u8>,
texture_records: Vec<concinnity_core::ecs::ResourceRecord>,
material_records: Vec<concinnity_core::ecs::ResourceRecord>,
mesh_records: Vec<concinnity_core::ecs::ResourceRecord>,
font_records: Vec<concinnity_core::ecs::ResourceRecord>,
color_lut_records: Vec<concinnity_core::ecs::ResourceRecord>,
env_map_records: Vec<concinnity_core::ecs::ResourceRecord>,
skinned_records: Vec<concinnity_core::ecs::ResourceRecord>,
}
impl WorldBuilder {
fn new() -> Self {
Self {
pending: Vec::new(),
section: Vec::new(),
texture_records: Vec::new(),
material_records: Vec::new(),
mesh_records: Vec::new(),
font_records: Vec::new(),
color_lut_records: Vec::new(),
env_map_records: Vec::new(),
skinned_records: Vec::new(),
}
}
fn payload(&mut self, bytes: &[u8]) -> PayloadLocator {
let offset = self.section.len() as u64;
self.section.extend_from_slice(bytes);
PayloadLocator {
blob_index: 0,
offset,
len: bytes.len() as u64,
}
}
fn push<C: ComponentSlot>(&mut self, c: C) {
self.pending.push(Box::new(move |world| {
world.push(c);
}));
}
fn push_identified<C: ComponentSlot>(&mut self, id: AssetId, c: C) {
self.pending.push(Box::new(move |world| {
world.push_identified(id, c);
}));
}
fn push_shader(&mut self, entries: &[&str]) {
let container = ShaderPrograms {
name: "shader".into(),
vertex: None,
fragment: concinnity_core::components::ShaderSource {
path: "shaders/wall.hlsl".into(),
text: "float4 shade(VertexOut v, GpuObjectData od) { return 1.0; }".into(),
},
programs: entries
.iter()
.map(|e| compiled_programs::CompiledProgram {
entry: e.to_string(),
source_digest: 1,
artifact: b"program-bytes".to_vec(),
})
.collect(),
};
let locator = self.payload(&container.encode().expect("encode shader payload"));
self.push(Shader {
locator: Some(locator),
..Default::default()
});
}
fn push_shaders(&mut self) {
self.push_shader(&["vertex_main_bindless", "fragment_main_bindless"]);
}
fn push_textured_quad(&mut self, mesh: AssetId, _tex: AssetId, mat: AssetId, prop: AssetId) {
let mesh_loc = self.payload(&quad_mesh_payload());
let _ = mesh;
let mesh_handle = MeshHandle::new(self.mesh_records.len() as u32);
self.mesh_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::Mesh,
handle: mesh_handle.0,
payload: Some(mesh_loc),
data_bytes: Vec::new(),
});
let tex_loc = self.payload(&texture_payload(2, 2));
let handle = self.texture_records.len() as u32;
self.texture_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::Texture,
handle,
payload: Some(tex_loc),
data_bytes: Vec::new(),
});
let _ = mat;
let mat_handle = self.material_records.len() as u32;
let mat_bytes = postcard::to_allocvec(&Material {
albedo: Some(TextureHandle::new(0)),
..Default::default()
})
.unwrap();
self.material_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::Material,
handle: mat_handle,
payload: None,
data_bytes: mat_bytes,
});
self.push_identified(
prop,
Prop {
mesh: Some(mesh_handle),
material: Some(MaterialHandle::new(mat_handle)),
position: [1.0, 2.0, 3.0],
..Default::default()
},
);
}
fn build(mut self) -> World {
let mut world = World::from_payloads(Box::new(BlobData::new(vec![Some(self.section)])));
world.insert_resource(TextureTable::from_records(&mut self.texture_records));
world.insert_resource(MaterialTable::from_records(&mut self.material_records));
world.insert_resource(MeshTable::from_records(&mut self.mesh_records));
world.insert_resource(FontTable::from_records(&mut self.font_records));
world.insert_resource(ColorLutTable::from_records(&mut self.color_lut_records));
world.insert_resource(EnvironmentMapTable::from_records(&mut self.env_map_records));
world.insert_resource(SkinnedMeshTable::from_records(&mut self.skinned_records));
for add in self.pending {
add(&mut world);
}
world
}
}
fn quad_mesh_payload() -> Vec<u8> {
let v = |pos: [f32; 3], uv: [f32; 2]| {
(
pos,
[0.0, 1.0, 0.0],
[1.0, 0.0, 0.0],
[0.75, 0.74, 0.72],
uv,
)
};
let vertices = [
v([0.0, 0.0, 0.0], [0.0, 0.0]),
v([1.0, 0.0, 0.0], [1.0, 0.0]),
v([1.0, 0.0, 1.0], [1.0, 1.0]),
v([0.0, 0.0, 1.0], [0.0, 1.0]),
];
mesh_payload::serialize(&vertices, &[0, 1, 2, 0, 2, 3])
}
fn texture_payload(w: u32, h: u32) -> Vec<u8> {
texture::serialize(&texture::TextureImage::rgba8(
w,
h,
vec![0x7Fu8; (w * h * 4) as usize],
))
}
fn scene_builder() -> WorldBuilder {
titled_scene("mock world")
}
fn titled_scene(title: &str) -> WorldBuilder {
let mut b = WorldBuilder::new();
b.push(Window {
title: title.to_string(),
width: 640,
height: 360,
..Default::default()
});
b.push(GraphicsConfig {
clear_color: [0.1, 0.2, 0.3, 1.0],
..Default::default()
});
b.push_shaders();
b.push(Camera3D::bake(Default::default()));
b.push_textured_quad(MESH, TEX, MAT, PROP);
b
}
fn init_graphics(world: &mut World, hooks: TestHooks) -> GraphicsSystem {
let mut gs = GraphicsSystem::new(None);
gs.test_hooks = Some(hooks);
let mut ctx = world.context();
crate::ecs::decompose::run(&mut ctx);
gs.run_init(&mut ctx);
gs
}
#[test]
fn a_first_launch_persists_the_auto_preset_into_the_state_tree() {
let dir = concinnity_testing::TempTree::new();
let tree = concinnity_host::store::paths::StateTree::at(dir.path());
let (_state, hooks) = recording_hooks();
assert!(hooks.settings.graphics.quality_preset.is_none());
let mut world = scene_builder().build();
let mut gs = GraphicsSystem::new(Some(&tree));
gs.test_hooks = Some(hooks);
{
let mut ctx = world.context();
crate::ecs::decompose::run(&mut ctx);
gs.run_init(&mut ctx);
}
assert!(!gs.failed);
assert_eq!(
crate::config::Settings::load(Some(&tree))
.graphics
.quality_preset,
Some(crate::gfx::quality_preset::QualityPreset::Auto)
);
}
fn backend_parked(world: &World) -> bool {
world
.resource::<crate::ecs::ActiveRenderBackend>()
.is_some_and(|slot| slot.0.is_some())
}
fn step(gs: &mut GraphicsSystem, world: &mut World) -> StepResult {
use concinnity_core::ecs::System;
let mut ctx = world.context();
crate::gfx::overlay::OverlaySystem::new().step(&mut ctx);
crate::spawn::SpawnSystem::new().step(&mut ctx);
crate::settings::system::SettingsSystem::new().step(&mut ctx);
crate::gfx::streaming::system::StreamingSystem::new().step(&mut ctx);
let result = gs.run_step(&mut ctx);
if result != StepResult::Stop {
if ctx.query::<FrameInput>().next().is_none() {
ctx.push(FrameInput::default());
}
crate::input::system::InputSystem::new().step(&mut ctx);
}
result
}
fn lock(state: &Arc<Mutex<MockState>>) -> std::sync::MutexGuard<'_, MockState> {
state.lock().unwrap()
}
#[test]
fn init_builds_draw_list_and_render_handles() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed, "init must succeed");
assert!(backend_parked(&world), "mock backend parked in the slot");
let s = lock(&state);
let init = s.init.as_ref().expect("factory captured the BackendInit");
assert_eq!(init.window_width, 640);
assert_eq!(init.window_height, 360);
assert_eq!(init.window_title, "mock world");
assert_eq!(init.clear_color, [0.1, 0.2, 0.3, 1.0]);
assert_eq!(init.frames_in_flight, 2);
assert_eq!(init.draw_objects.len(), 1);
assert_eq!(init.vertex_count, 4);
assert_eq!(init.index_count, 6);
let draw = &init.draw_objects[0];
assert_eq!(draw.index_count, 6);
assert_eq!(draw.texture_slot, 0, "material albedo resolves to slot 0");
assert!(draw.visible);
assert_eq!(draw.model[3][0], 1.0);
assert_eq!(draw.model[3][1], 2.0);
assert_eq!(draw.model[3][2], 3.0);
assert_eq!(init.texture_count, 1);
assert_eq!(init.text_atlas_count, 0, "no fonts or sprite textures");
assert_eq!(init.n_skinned, 0);
assert_eq!(init.instanced_cluster_count, 0);
assert!(init.scene_required, "a mesh world requires the scene chain");
assert!(!init.fog, "no VolumetricFog declared");
drop(s);
let ctx = world.context();
let handles: Vec<Vec<DrawIndex>> = ctx
.query::<RenderHandle>()
.map(|h| h.draws.to_vec())
.collect();
assert_eq!(handles, vec![vec![DrawIndex(0)]]);
let globals: Vec<[[f32; 4]; 4]> = ctx.query::<GlobalTransform>().map(|g| g.0).collect();
assert_eq!(globals.len(), 1);
assert_eq!(globals[0][3][0], 1.0);
}
#[test]
fn init_parks_overlay_assets_with_the_hud_chips() {
use crate::gfx::overlay::OverlayAssets;
use concinnity_core::components::{DebugHud, StatHud};
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(StatHud {
fps_label: Some(Ref::new(AssetId(10))),
vram_label: Some(Ref::new(AssetId(11))),
..Default::default()
});
b.push(DebugHud {
mouse_label: Some(Ref::new(AssetId(20))),
passes_label: Some(Ref::new(AssetId(21))),
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed, "init must succeed");
let overlay = world
.resource::<OverlayAssets>()
.expect("OverlayAssets parked at init");
assert_eq!(
overlay.stat_hud_chips,
vec![AssetId(10), AssetId(11)],
"StatHud chip ids survive to the parked OverlayAssets"
);
assert_eq!(
overlay.debug_hud_chips,
vec![AssetId(20), AssetId(21)],
"DebugHud chip ids survive to the parked OverlayAssets"
);
}
#[test]
fn take_backend_yields_the_backend_once() {
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
assert!(
crate::ecs::ActiveRenderBackend::take(world.context().resources).is_some(),
"the built backend is taken"
);
assert!(
crate::ecs::ActiveRenderBackend::take(world.context().resources).is_none(),
"and only once"
);
}
#[test]
fn pending_backend_reuses_instance_and_ends_with_new_world() {
let (state_a, hooks_a) = recording_hooks();
let mut world_a = titled_scene("world A").build();
let _gs_a = init_graphics(&mut world_a, hooks_a);
let backend_a = crate::ecs::ActiveRenderBackend::take(world_a.context().resources)
.expect("world A built a backend");
let (state_b, hooks_b) = recording_hooks();
let mut world_b = titled_scene("world B").build();
world_b.insert_resource(crate::live_edit::PendingBackend(backend_a));
let gs_b = init_graphics(&mut world_b, hooks_b);
assert!(!gs_b.failed);
assert!(backend_parked(&world_b), "the reused backend is installed");
let a = lock(&state_a);
assert!(
a.saw(&Call::ReloadWorld),
"the transplanted backend was reloaded, not rebuilt"
);
assert_eq!(
a.init.as_ref().unwrap().window_title,
"world B",
"the reused backend now carries the new world's content"
);
drop(a);
assert!(
state_b.lock().unwrap().init.is_none(),
"world B never built a fresh backend"
);
}
#[test]
fn pending_backend_swapchain_change_forces_full_rebuild() {
let (state_t, _unused) = recording_hooks();
let transplant = MockBackend::transplant(
Arc::clone(&state_t),
Some(SwapchainConfig {
frames_in_flight: 3,
hdr_display: false,
hdr_pq: false,
}),
);
let (state_b, hooks_b) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(crate::live_edit::PendingBackend(Box::new(transplant)));
let gs = init_graphics(&mut world, hooks_b);
assert!(!gs.failed);
assert!(backend_parked(&world));
let t = lock(&state_t);
assert!(
t.saw(&Call::WaitIdle),
"the mismatched backend is idled before it is dropped"
);
assert!(
!t.saw(&Call::ReloadWorld),
"no reload is attempted across a swapchain change"
);
drop(t);
assert!(
state_b.lock().unwrap().init.is_some(),
"a fresh backend is built for the changed swapchain"
);
}
#[test]
fn pending_backend_hdr_change_forces_full_rebuild() {
let (state_t, _unused) = recording_hooks();
let transplant = MockBackend::transplant(
Arc::clone(&state_t),
Some(SwapchainConfig {
frames_in_flight: 2,
hdr_display: true,
hdr_pq: false,
}),
);
let (state_b, hooks_b) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(crate::live_edit::PendingBackend(Box::new(transplant)));
let gs = init_graphics(&mut world, hooks_b);
assert!(!gs.failed);
let t = lock(&state_t);
assert!(
t.saw(&Call::WaitIdle),
"the HDR-mismatched backend is idled"
);
assert!(!t.saw(&Call::ReloadWorld), "no reload across an HDR change");
drop(t);
assert!(
state_b.lock().unwrap().init.is_some(),
"a fresh backend is built for the changed HDR output"
);
}
#[test]
fn reload_world_failure_marks_graphics_failed() {
let (state_a, hooks_a) = recording_hooks();
let mut world_a = scene_builder().build();
let _gs_a = init_graphics(&mut world_a, hooks_a);
let backend_a = crate::ecs::ActiveRenderBackend::take(world_a.context().resources).unwrap();
state_a.lock().unwrap().fail_reload = Some(error::RenderError::DeviceLost {
reason: error::DeviceLostReason::Removed,
detail: "boom".to_string(),
});
let (_state_b, hooks_b) = recording_hooks();
let mut world_b = scene_builder().build();
world_b.insert_resource(crate::live_edit::PendingBackend(backend_a));
let gs_b = init_graphics(&mut world_b, hooks_b);
assert!(gs_b.failed, "a failed reload marks the system failed");
assert!(!backend_parked(&world_b));
assert!(state_a.lock().unwrap().saw(&Call::ReloadWorld));
}
#[test]
fn init_pushes_startup_backend_state() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
let s = lock(&state);
assert!(s.saw(&Call::SetMenuMode(false)));
assert!(s.saw(&Call::RequestCursorCapture));
assert!(s.saw(&Call::SetAmbientIntensity(1.0)));
assert!(s.saw(&Call::SetKeymap));
assert!(
s.calls
.iter()
.any(|c| matches!(c, Call::SetReflectionProbes(_)))
);
assert!(!s.calls.iter().any(|c| matches!(c, Call::SetWindowMode(_))));
}
#[test]
fn menu_driven_init_skips_the_first_person_cursor_grab() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(MenuOverride(Some(true)));
let _gs = init_graphics(&mut world, hooks);
let s = lock(&state);
assert!(
!s.saw(&Call::RequestCursorCapture),
"a menu/editor driver owns capture; init must not auto-grab"
);
assert!(s.saw(&Call::SetMenuMode(false)));
}
#[test]
fn ui_only_world_trims_scene_features() {
let (state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window::default());
b.push(GraphicsConfig::default());
b.push_shaders();
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert!(init.draw_objects.is_empty());
assert!(!init.scene_required, "no 3D content: scene chain trimmed");
assert_eq!(init.shadows.map_size, 0, "shadows trimmed with the scene");
assert!(!init.taa_enabled);
assert!(
!init.ssao_enabled,
"screen-space effects trimmed with the scene"
);
assert!(!s.saw(&Call::RequestCursorCapture));
}
#[test]
fn persisted_settings_override_authored_config() {
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Custom);
settings.graphics.vsync = Some(true);
settings.graphics.fps_cap = Some(30);
settings.graphics.frames_in_flight = Some(3);
settings.graphics.shadow_map_size = Some(1024);
let (state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut world = scene_builder().build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let live = settings_state(&world);
assert_eq!(
live.graphics.fps_cap, 30,
"persisted cap overrides the world's 0"
);
assert_eq!(live.quality_preset, QualityPreset::Custom);
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert!(init.vsync, "persisted vsync overrides the authored false");
assert_eq!(init.frames_in_flight, 3);
assert_eq!(init.shadows.map_size, 1024, "explicit override wins");
}
#[test]
fn low_preset_ceiling_clamps_quality_knobs() {
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Low);
let (state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert_eq!(init.shadows.map_size, 1024);
assert_eq!(init.shadows.cadence.distance, 40);
assert_eq!(init.shadows.cadence.cascades, 2);
assert_eq!(init.anisotropy, 4);
let live = settings_state(&world);
assert_eq!(live.authored.shadow_map_size, 4096);
assert_eq!(live.authored.shadow_cadence.distance, 80);
assert_eq!(live.authored.shadow_cadence.cascades, 4);
assert_eq!(live.authored.anisotropy, 16);
}
fn post_settings_under(
world: &GraphicsBaseline,
user: &crate::config::GraphicsSettings,
preset: QualityPreset,
) -> backend_init::PostSettings {
let ceiling = resolve_ceiling(preset, &GpuProfile::UNKNOWN);
let graphics = resolve_graphics(world, user, &ceiling);
super::init::post_settings(&graphics, world, &LaunchRequest::default())
}
#[test]
fn post_settings_hdr_samples_follow_the_clamped_aa_mode() {
let post = PostProcessConfig {
aa_mode: AaMode::Taa,
temporal_upscaling: false,
..Default::default()
};
let world = GraphicsBaseline::new(None, Some(&post), None);
let user = crate::config::GraphicsSettings::default();
let unclamped = post_settings_under(&world, &user, QualityPreset::Custom);
assert!(unclamped.taa_enabled);
assert_eq!(unclamped.hdr_samples, 1);
let clamped = post_settings_under(&world, &user, QualityPreset::Low);
assert!(!clamped.taa_enabled);
assert_eq!(clamped.hdr_samples, HDR_MULTISAMPLE_COUNT);
}
#[test]
fn post_settings_upscale_scale_is_native_without_a_post_process_config() {
let user = crate::config::GraphicsSettings {
render_scale: Some(UpscaleQuality::Performance),
..Default::default()
};
let undeclared = GraphicsBaseline::default();
let native = post_settings_under(&undeclared, &user, QualityPreset::Custom);
assert_eq!(native.upscale_scale, 1.0);
let declared = GraphicsBaseline::new(None, Some(&PostProcessConfig::default()), None);
let scaled = post_settings_under(&declared, &user, QualityPreset::Custom);
assert_eq!(scaled.upscale_scale, UpscaleQuality::Performance.scale());
}
#[test]
fn auto_preset_resolves_ceiling_from_gpu_tier() {
let profile = GpuProfile {
vendor: GpuVendor::Intel,
tier: GpuTier::Integrated,
memory_budget_bytes: 0,
unified_memory: false,
discrete: false,
};
let (state, hooks) = recording_hooks_with(crate::config::Settings::default(), profile);
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
assert_eq!(settings_state(&world).quality_preset, QualityPreset::Auto);
let s = lock(&state);
assert_eq!(s.init.as_ref().unwrap().shadows.map_size, 1024);
}
#[test]
fn a_world_without_a_shader_renders_with_the_engine_program() {
let (state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window::default());
b.push(GraphicsConfig::default());
b.push_textured_quad(MESH, TEX, MAT, PROP);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed, "no Shader is the common case, not a failure");
let s = lock(&state);
let init = s.init.as_ref().expect("backend constructed");
assert_eq!(init.shader_program_counts, vec![0]);
}
#[test]
fn a_world_with_a_shader_hands_its_programs_to_the_backend() {
let (state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window::default());
b.push(GraphicsConfig::default());
b.push_shaders();
b.push_textured_quad(MESH, TEX, MAT, PROP);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
let init = s.init.as_ref().expect("backend constructed");
assert_eq!(init.shader_program_counts, vec![2]);
}
#[test]
fn first_declared_scene_applies_start_visibility() {
let scene_a = AssetId(20);
let scene_b = AssetId(21);
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_textured_quad(AssetId(5), AssetId(6), AssetId(7), AssetId(8));
let mut world = b.build();
{
let mut ctx = world.context();
let first = ctx.entity_of(PROP).expect("the first prop is identified");
for (entity, prop) in ctx.query_mut_with_entity::<Prop>() {
prop.scene = Some(if entity == first {
Ref::new(scene_a)
} else {
Ref::new(scene_b)
});
}
ctx.push_identified(scene_a, Scene { camera_shot: None });
ctx.push_identified(scene_b, Scene { camera_shot: None });
}
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
{
let s = lock(&state);
assert_eq!(
s.visibility.get(&DrawIndex(0)),
Some(&true),
"scene A prop visible"
);
assert_eq!(
s.visibility.get(&DrawIndex(1)),
Some(&false),
"scene B prop hidden"
);
}
{
let mut ctx = world.context();
ctx.events_mut::<SceneCommand>().send(SceneCommand {
scene: scene_b,
transition: concinnity_core::components::SceneTransition::Cut,
});
}
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let s = lock(&state);
assert_eq!(s.visibility.get(&DrawIndex(0)), Some(&false));
assert_eq!(s.visibility.get(&DrawIndex(1)), Some(&true));
}
#[derive(Clone, Default)]
struct Captured(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for Captured {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Captured {
fn text(&self) -> String {
String::from_utf8(self.0.lock().unwrap().clone()).unwrap()
}
}
#[test]
fn jump_to_undeclared_scene_warns_and_changes_nothing() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world
.context()
.push_identified(AssetId(20), Scene { camera_shot: None });
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
lock(&state).visibility.clear();
world
.context()
.events_mut::<SceneCommand>()
.send(SceneCommand {
scene: AssetId(99),
transition: concinnity_core::components::SceneTransition::Cut,
});
let captured = Captured::default();
let writer = captured.clone();
let subscriber = tracing_subscriber::fmt()
.with_ansi(false)
.with_max_level(tracing::Level::WARN)
.with_writer(move || writer.clone())
.finish();
let result = tracing::subscriber::with_default(subscriber, || step(&mut gs, &mut world));
assert_eq!(result, StepResult::Continue);
let log = captured.text();
assert!(
log.contains("scene jump to 99 rejected: UnknownScene"),
"{log}"
);
assert!(lock(&state).visibility.is_empty());
}
#[test]
fn extract_hands_the_spent_overlay_list_back() {
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
for _ in 0..2 {
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(
world
.resource::<crate::gfx::overlay::OverlayRecycle>()
.is_some(),
"extraction returned the spent draw list"
);
}
}
#[test]
fn frame_steps_draw_and_publish_input() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
for _ in 0..2 {
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
}
lock(&state).next_input.mouse_x = 33.0;
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(gs.frame_count, 3);
let s = lock(&state);
assert_eq!(s.draw_frames(), 3);
assert_eq!(
s.calls
.iter()
.filter(|c| matches!(c, Call::UpdateView(_)))
.count(),
3
);
assert_eq!(s.calls.iter().filter(|c| **c == Call::TakeInput).count(), 3);
drop(s);
let ctx = world.context();
let inputs: Vec<&FrameInput> = ctx.query::<FrameInput>().collect();
assert_eq!(inputs.len(), 1, "previous snapshot drained, one deposited");
let res = ctx.resource::<FrameInput>().expect("resource published");
assert_eq!(res.mouse_x, 33.0);
assert_eq!(res.viewport, [1280.0, 720.0]);
assert!(
ctx.resource::<MenuActive>().is_some(),
"menu state published every frame"
);
}
#[test]
fn a_serial_frame_records_its_submission_as_the_render_handoff() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
lock(&state).draw_duration = std::time::Duration::from_millis(3);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let ctx = world.context();
ctx.profile.record_system("GraphicsSystem", 0);
ctx.profile.begin_frame();
let handoffs = ctx.profile.render_handoffs();
assert_eq!(handoffs.len(), 1);
assert_eq!(handoffs[0].0, "GraphicsSystem");
assert!(handoffs[0].1 >= 3_000, "{} us", handoffs[0].1);
}
#[test]
fn camera_state_reaches_the_backend_each_frame() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
step(&mut gs, &mut world);
let moved_view = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[-7.0, -8.0, -9.0, 1.0],
];
{
let mut ctx = world.context();
let cam = ctx.query_mut::<Camera3D>().next().unwrap();
cam.position = [7.0, 8.0, 9.0];
cam.view_matrix = moved_view;
}
step(&mut gs, &mut world);
let s = lock(&state);
match s.last_draw_frame() {
Some(Call::DrawFrame { cam_pos, .. }) => assert_eq!(cam_pos, [7.0, 8.0, 9.0]),
other => panic!("expected a DrawFrame, got {other:?}"),
}
assert!(
s.saw(&Call::UpdateView(moved_view)),
"the moved view matrix reaches update_view"
);
}
#[test]
fn transform_edit_pushes_new_model_matrix() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
let t = ctx.query_mut::<Transform>().next().unwrap();
t.position = [10.0, 20.0, 30.0];
}
step(&mut gs, &mut world);
let s = lock(&state);
assert!(s.saw(&Call::UpdateModel(DrawIndex(0))));
let model = s.models.get(&DrawIndex(0)).expect("slot 0 model pushed");
assert_eq!(model[3][0], 10.0);
assert_eq!(model[3][1], 20.0);
assert_eq!(model[3][2], 30.0);
}
#[test]
fn model_matrices_push_only_on_change() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
let model_pushes = |s: &MockState| {
s.calls
.iter()
.filter(|c| matches!(c, Call::UpdateModel(_)))
.count()
};
step(&mut gs, &mut world);
assert_eq!(model_pushes(&lock(&state)), 1, "first frame seeds the slot");
for _ in 0..2 {
lock(&state).calls.clear();
step(&mut gs, &mut world);
assert_eq!(
model_pushes(&lock(&state)),
0,
"static frame pushed a model"
);
}
{
let mut ctx = world.context();
let t = ctx.query_mut::<Transform>().next().unwrap();
t.position = [4.0, 5.0, 6.0];
}
lock(&state).calls.clear();
step(&mut gs, &mut world);
{
let s = lock(&state);
assert_eq!(model_pushes(&s), 1, "one move, one update");
assert_eq!(s.models.get(&DrawIndex(0)).expect("slot 0")[3][0], 4.0);
}
lock(&state).calls.clear();
step(&mut gs, &mut world);
assert_eq!(
model_pushes(&lock(&state)),
0,
"settled frame pushed a model"
);
}
#[test]
fn opaque_menu_backdrop_hides_world_and_freezes_gameplay_input() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(HitRegion::default());
b.push_identified(
AssetId(40),
Sprite {
x: 0.0,
y: 0.0,
width: 1280.0,
height: 720.0,
tint: [0.0, 0.0, 0.0, 1.0],
visible: true,
screen: Some(Ref::new(AssetId(41))),
..Default::default()
},
);
let mut world = b.build();
world.context().insert_resource(ScreenStack {
layers: [(AssetId(41), 1)].into_iter().collect(),
pauses_world: true,
captures_input: true,
});
let mut gs = init_graphics(&mut world, hooks);
assert!(lock(&state).saw(&Call::SetMenuMode(true)));
lock(&state).next_input.forward = true;
step(&mut gs, &mut world);
{
let s = lock(&state);
match s.last_draw_frame() {
Some(Call::DrawFrame { world_hidden, .. }) => {
assert!(world_hidden, "opaque full-canvas backdrop skips the world");
}
other => panic!("expected a DrawFrame, got {other:?}"),
}
assert!(s.saw(&Call::SetCameraCapture(false)));
}
{
let ctx = world.context();
assert!(ctx.resource::<MenuActive>().unwrap().0);
let input = ctx.resource::<FrameInput>().unwrap();
assert!(!input.forward, "gameplay input frozen behind the menu");
}
{
let mut ctx = world.context();
let sprite = ctx.query_mut::<Sprite>().next().unwrap();
sprite.tint[3] = 0.5;
}
step(&mut gs, &mut world);
let s = lock(&state);
match s.last_draw_frame() {
Some(Call::DrawFrame { world_hidden, .. }) => {
assert!(!world_hidden, "translucent dim keeps the world render");
}
other => panic!("expected a DrawFrame, got {other:?}"),
}
}
#[test]
fn menu_override_true_forces_cursor_free_and_freezes_input() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
assert!(lock(&state).saw(&Call::SetMenuMode(false)));
world.insert_resource(MenuOverride(Some(true)));
lock(&state).next_input.forward = true;
step(&mut gs, &mut world);
let s = lock(&state);
assert!(
s.saw(&Call::SetMenuMode(true)),
"override forces backend menu mode"
);
assert!(
s.saw(&Call::SetCameraCapture(false)),
"override releases the cursor"
);
drop(s);
let ctx = world.context();
assert!(
ctx.resource::<MenuActive>().unwrap().0,
"freeze resource set"
);
assert!(
!ctx.resource::<FrameInput>().unwrap().forward,
"gameplay input frozen in edit mode"
);
}
#[test]
fn menu_override_false_captures_cursor_and_runs_input() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
world.insert_resource(MenuOverride(Some(false)));
lock(&state).next_input.forward = true;
step(&mut gs, &mut world);
let s = lock(&state);
assert!(
s.saw(&Call::SetCameraCapture(true)),
"override captures the cursor"
);
drop(s);
let ctx = world.context();
assert!(!ctx.resource::<MenuActive>().unwrap().0, "not frozen");
assert!(
ctx.resource::<FrameInput>().unwrap().forward,
"gameplay input runs in play mode"
);
}
#[test]
fn window_close_stops_after_wait_idle() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
lock(&state).window_closed = true;
assert_eq!(step(&mut gs, &mut world), StepResult::Stop);
let s = lock(&state);
assert!(s.saw(&Call::WaitIdle));
assert_eq!(s.draw_frames(), 0, "no frame drawn after the close");
}
#[test]
fn unclassified_draw_error_skips_bounded_then_stops() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
lock(&state).fail_draw = Some(error::RenderError::Other("boom".to_string()));
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Stop);
assert!(lock(&state).saw(&Call::WaitIdle));
}
#[test]
fn successful_frame_resets_the_failure_streak() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
for _ in 0..3 {
lock(&state).fail_draw = Some(error::RenderError::Other("boom".to_string()));
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
lock(&state).fail_draw = None;
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
}
}
#[test]
fn device_lost_stops_immediately_without_gpu_drain() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
lock(&state).fail_draw = Some(error::RenderError::DeviceLost {
reason: error::DeviceLostReason::Removed,
detail: "test".to_string(),
});
assert_eq!(step(&mut gs, &mut world), StepResult::Stop);
assert!(
!lock(&state).saw(&Call::WaitIdle),
"must not wait on a lost device"
);
}
#[test]
fn swapchain_out_of_date_skips_the_frame() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
lock(&state).fail_draw = Some(error::RenderError::SwapchainOutOfDate);
for _ in 0..5 {
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
}
}
#[test]
fn a_dropped_frame_hands_its_history_reset_to_the_next() {
use concinnity_core::render::history_reset::{HistoryResetCauses, PendingHistoryReset};
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let captured = Captured::default();
let writer = captured.clone();
let subscriber = tracing_subscriber::fmt()
.with_ansi(false)
.with_max_level(tracing::Level::INFO)
.with_writer(move || writer.clone())
.finish();
tracing::subscriber::with_default(subscriber, || {
PendingHistoryReset::raise(&mut world.context(), HistoryResetCauses::SETTINGS_CHANGE);
lock(&state).fail_draw = Some(error::RenderError::SwapchainOutOfDate);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
lock(&state).fail_draw = None;
lock(&state).calls.clear();
for _ in 0..2 {
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
}
});
let log = captured.text();
assert_eq!(log.matches("temporal history reset: ").count(), 1, "{log}");
let resets: Vec<bool> = lock(&state)
.calls
.iter()
.filter_map(|c| match c {
Call::DrawFrame { history_reset, .. } => Some(*history_reset),
_ => None,
})
.collect();
assert_eq!(resets, [true, false]);
}
#[test]
fn out_of_device_memory_publishes_pressure_and_continues() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
lock(&state).fail_draw = Some(error::RenderError::OutOfDeviceMemory("test".to_string()));
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let pressure = *world
.context()
.resource::<GpuMemoryPressure>()
.expect("pressure resource published");
assert_eq!(pressure.events, 2);
}
#[test]
fn max_frames_stops_the_run() {
let (_state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window::default());
b.push(GraphicsConfig {
max_frames: Some(2),
..Default::default()
});
b.push_shaders();
b.push(Camera3D::bake(Default::default()));
b.push_textured_quad(MESH, TEX, MAT, PROP);
let mut world = b.build();
let mut gs = init_graphics(&mut world, hooks);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Stop);
}
#[test]
fn pipelined_max_frames_counts_frames_sent() {
let (_state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window::default());
b.push(GraphicsConfig {
max_frames: Some(3),
..Default::default()
});
b.push_shaders();
b.push(Camera3D::bake(Default::default()));
b.push_textured_quad(MESH, TEX, MAT, PROP);
let mut world = b.build();
let mut gs = init_graphics(&mut world, hooks);
let (snapshot_tx, snapshot_rx) = std::sync::mpsc::sync_channel(0);
let (_feedback_tx, feedback_rx) = std::sync::mpsc::channel();
world
.context()
.insert_resource(crate::ecs::PipelinedFrames(Some(
crate::ecs::PipelineChannels {
snapshot_tx,
feedback_rx,
},
)));
let drain = std::thread::spawn(move || snapshot_rx.into_iter().count());
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert_eq!(step(&mut gs, &mut world), StepResult::Stop);
world
.context()
.remove_resource::<crate::ecs::PipelinedFrames>();
assert_eq!(drain.join().expect("drain thread"), 3);
}
#[test]
fn spawn_request_clones_template_draw_slot() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
ctx.events_mut::<SpawnRequest>().send(SpawnRequest {
template: PROP,
name: Some(AssetId(900)),
transform: Transform {
position: [5.0, 0.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
},
lifetime_secs: None,
});
}
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(lock(&state).saw(&Call::CloneStaticDrawObject {
src: DrawIndex(0),
new_idx: DrawIndex(1)
}));
let ctx = world.context();
let mut handles: Vec<Vec<DrawIndex>> = ctx
.query::<RenderHandle>()
.map(|h| h.draws.to_vec())
.collect();
handles.sort();
assert_eq!(
handles,
vec![vec![DrawIndex(0)], vec![DrawIndex(1)]],
"spawned copy owns slot 1"
);
}
#[test]
fn visibility_request_switches_slots_and_hidden_tag() {
use concinnity_core::components::{Hidden, VisibilityRequest};
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
ctx.events_mut::<VisibilityRequest>()
.send(VisibilityRequest {
target: PROP.into(),
visible: false,
});
}
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(lock(&state).saw(&Call::UpdateVisibility {
draw_idx: DrawIndex(0),
visible: false
}));
assert_eq!(
world.context().query::<Hidden>().count(),
1,
"hide tags the entity"
);
{
let mut ctx = world.context();
ctx.events_mut::<VisibilityRequest>()
.send(VisibilityRequest {
target: PROP.into(),
visible: true,
});
}
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(lock(&state).saw(&Call::UpdateVisibility {
draw_idx: DrawIndex(0),
visible: true
}));
assert_eq!(
world.context().query::<Hidden>().count(),
0,
"show clears the tag"
);
}
#[test]
fn despawn_request_retires_draw_slots() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
ctx.events_mut::<DespawnRequest>().send(DespawnRequest {
target: PROP.into(),
});
}
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(lock(&state).saw(&Call::RetireDrawObject(DrawIndex(0))));
let ctx = world.context();
assert_eq!(
ctx.query::<RenderHandle>().count(),
0,
"despawned entity gone"
);
}
fn streaming_stats(world: &World) -> crate::gfx::streaming::system::StreamingStats {
world
.resource::<crate::gfx::streaming::system::StreamingState>()
.expect("StreamingState parked at init")
.streaming_stats()
}
#[test]
fn streaming_init_evicts_streamable_slots() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(StreamingConfig::default());
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(gs.texture_streamer.is_none());
assert!(gs.mesh_streamer.is_none());
let stats = streaming_stats(&world);
assert!(stats.texture.is_some(), "texture pool streaming");
assert!(stats.mesh.is_some(), "mesh pool streaming");
let s = lock(&state);
assert!(s.saw(&Call::EvictTextureSlot(0)));
assert!(s.saw(&Call::EvictMesh(DrawIndex(0))));
}
#[test]
fn texture_streaming_uploads_evicted_slots() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(StreamingConfig::default());
let mut world = b.build();
let mut gs = init_graphics(&mut world, hooks);
let deadline = Instant::now() + Duration::from_secs(10);
loop {
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
if lock(&state).calls.iter().any(|c| {
matches!(
c,
Call::UpdateTextureSlot {
slot: 0,
w: 2,
h: 2
}
)
}) {
break;
}
assert!(
Instant::now() < deadline,
"texture never streamed back in: {:?}",
lock(&state).calls
);
std::thread::sleep(Duration::from_millis(2));
}
let (resident, pending, unloaded) = streaming_stats(&world).texture.unwrap();
assert_eq!((resident, pending, unloaded), (1, 0, 0));
}
#[test]
fn mesh_streaming_reuploads_evicted_geometry() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(StreamingConfig::default());
let mut world = b.build();
let mut gs = init_graphics(&mut world, hooks);
let deadline = Instant::now() + Duration::from_secs(10);
loop {
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
if lock(&state).calls.iter().any(|c| {
matches!(
c,
Call::UploadMesh {
draw_idx: DrawIndex(0),
vertices: 4,
indices: 6,
}
)
}) {
break;
}
assert!(
Instant::now() < deadline,
"mesh never streamed back in: {:?}",
lock(&state).calls
);
std::thread::sleep(Duration::from_millis(2));
}
let (resident, pending, unloaded) = streaming_stats(&world).mesh.unwrap();
assert_eq!((resident, pending, unloaded), (1, 0, 0));
}
#[test]
fn voxel_world_rebases_the_draw_view_onto_the_chunk_origin() {
use concinnity_core::components::VoxelWorld;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(VoxelWorld {
seed: 1,
view_radius: 1,
..Default::default()
});
let mut world = b.build();
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(
streaming_stats(&world).chunk.is_some(),
"chunk pool streaming"
);
let cam_pos = [40.0, 5.0, -40.0];
let view = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[-40.0, -5.0, 40.0, 1.0],
];
{
let mut ctx = world.context();
let cam = ctx.query_mut::<Camera3D>().next().unwrap();
cam.position = cam_pos;
cam.view_matrix = view;
}
step(&mut gs, &mut world);
let origin = [32.0, 0.0, -48.0];
let expected_cam = [8.0, 5.0, 8.0];
let expected_view = chunk_coord::camera_relative_view(view, cam_pos, origin);
let s = lock(&state);
match s.last_draw_frame() {
Some(Call::DrawFrame { cam_pos: cp, .. }) => {
assert_eq!(
cp, expected_cam,
"draw camera rebased onto the chunk origin"
);
assert_ne!(cp, cam_pos, "the rebase actually moved the camera");
}
other => panic!("expected a DrawFrame, got {other:?}"),
}
assert!(
s.saw(&Call::UpdateView(expected_view)),
"the camera-relative view reaches update_view, not the absolute one"
);
assert!(
!s.saw(&Call::UpdateView(view)),
"the absolute view is not what the backend received"
);
}
fn spawn_step(spawn: &mut crate::spawn::SpawnSystem, world: &mut World) {
use concinnity_core::ecs::System;
{
let mut ctx = world.context();
spawn.step(&mut ctx);
}
replay_pending_ops(world);
}
fn replay_pending_ops(world: &mut World) {
let Some(mut queues) = crate::ecs::ActiveRenderQueues::take(world.context().resources) else {
return;
};
if let Some(mut backend) = crate::ecs::ActiveRenderBackend::take(world.context().resources) {
queues.ops.replay(backend.as_mut());
crate::ecs::ActiveRenderBackend::put(world.context().resources, backend);
}
crate::ecs::ActiveRenderQueues::put(world.context().resources, queues);
}
fn entity_named(world: &mut World, name: AssetId) -> Entity {
world
.context()
.entity_of(name)
.expect("the id resolves to an entity")
}
#[test]
fn spawn_system_without_graphics_leaves_the_churn_pending() {
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
crate::ecs::ActiveRenderQueues::take(world.context().resources)
.expect("the recording surfaces were published");
{
let mut ctx = world.context();
ctx.events_mut::<DespawnRequest>().send(DespawnRequest {
target: PROP.into(),
});
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
assert_eq!(
world.context().query::<RenderHandle>().count(),
1,
"the despawn never ran without the slot allocator to retire into"
);
}
#[test]
fn reparent_request_repoints_the_child_under_the_named_parent() {
const OTHER: AssetId = AssetId(8);
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_textured_quad(AssetId(5), AssetId(6), AssetId(7), OTHER);
let mut world = b.build();
let _gs = init_graphics(&mut world, hooks);
let child = entity_named(&mut world, OTHER);
let parent = entity_named(&mut world, PROP);
{
let mut ctx = world.context();
ctx.events_mut::<ReparentRequest>().send(ReparentRequest {
child: OTHER.into(),
parent: Some(PROP.into()),
});
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
let ctx = world.context();
assert_eq!(
ctx.get::<Parent>(child).map(|p| p.0),
Some(parent),
"the child hangs off the named parent"
);
let global = ctx
.get::<GlobalTransform>(child)
.expect("child keeps a world matrix");
assert_eq!(
[global.0[3][0], global.0[3][1], global.0[3][2]],
[2.0, 4.0, 6.0],
"the child's world matrix recomposed under the parent chain"
);
}
#[test]
fn reparent_request_with_an_unresolved_parent_is_skipped() {
const OTHER: AssetId = AssetId(8);
const GHOST: AssetId = AssetId(900);
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_textured_quad(AssetId(5), AssetId(6), AssetId(7), OTHER);
let mut world = b.build();
let _gs = init_graphics(&mut world, hooks);
let child = entity_named(&mut world, OTHER);
let parent = entity_named(&mut world, PROP);
{
let mut ctx = world.context();
propagation::reparent(&mut ctx, child, Some(parent));
ctx.events_mut::<ReparentRequest>().send(ReparentRequest {
child: OTHER.into(),
parent: Some(GHOST.into()),
});
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
assert_eq!(
world.context().get::<Parent>(child).map(|p| p.0),
Some(parent),
"an unresolved parent name never detaches the child to a root"
);
}
#[test]
fn reparent_request_without_a_parent_detaches_the_child() {
const OTHER: AssetId = AssetId(8);
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_textured_quad(AssetId(5), AssetId(6), AssetId(7), OTHER);
let mut world = b.build();
let _gs = init_graphics(&mut world, hooks);
let child = entity_named(&mut world, OTHER);
let parent = entity_named(&mut world, PROP);
{
let mut ctx = world.context();
propagation::reparent(&mut ctx, child, Some(parent));
ctx.events_mut::<ReparentRequest>().send(ReparentRequest {
child: OTHER.into(),
parent: None,
});
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
let ctx = world.context();
assert!(
ctx.get::<Parent>(child).is_none(),
"an unnamed parent detaches the child"
);
let global = ctx.get::<GlobalTransform>(child).unwrap();
assert_eq!(
[global.0[3][0], global.0[3][1], global.0[3][2]],
[1.0, 2.0, 3.0],
"the detached child falls back to its own transform"
);
}
#[test]
fn expired_lifetime_despawns_the_entity_and_retires_its_slot() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
let entity = entity_named(&mut world, PROP);
world.context().insert(entity, Lifetime { remaining: 0.0 });
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
assert!(lock(&state).saw(&Call::RetireDrawObject(DrawIndex(0))));
assert_eq!(
world.context().query::<RenderHandle>().count(),
0,
"the expired entity is gone"
);
}
#[test]
fn due_spawner_clones_its_template_at_its_own_transform() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
let spawner = ctx.components.spawn();
ctx.insert(
spawner,
Spawner {
template: PROP,
interval: 1.0,
lifetime: 5.0,
elapsed: 1.0,
count: 0,
},
);
ctx.insert(
spawner,
Transform {
position: [9.0, 0.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
},
);
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
assert!(lock(&state).saw(&Call::CloneStaticDrawObject {
src: DrawIndex(0),
new_idx: DrawIndex(1)
}));
let ctx = world.context();
assert_eq!(
ctx.query::<Lifetime>()
.map(|l| l.remaining)
.collect::<Vec<_>>(),
vec![5.0],
"the cadence copy carries the spawner's lifetime"
);
let spawner_count = ctx.query::<Spawner>().map(|s| s.count).next();
assert_eq!(spawner_count, Some(1), "the spawner counted the copy");
}
#[test]
fn menu_active_freezes_lifetimes_and_spawners() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
let entity = entity_named(&mut world, PROP);
{
let mut ctx = world.context();
ctx.insert(entity, Lifetime { remaining: 0.0 });
let spawner = ctx.components.spawn();
ctx.insert(
spawner,
Spawner {
template: PROP,
interval: 1.0,
lifetime: 0.0,
elapsed: 1.0,
count: 0,
},
);
}
world.insert_resource(MenuActive(true));
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
{
let s = lock(&state);
assert!(
!s.saw(&Call::RetireDrawObject(DrawIndex(0))),
"the expired Lifetime does not fire behind the menu"
);
assert!(
!s.calls
.iter()
.any(|c| matches!(c, Call::CloneStaticDrawObject { .. })),
"the due Spawner does not fire behind the menu"
);
}
assert_eq!(
world.context().query::<Spawner>().map(|s| s.count).next(),
Some(0),
"the spawner's clock never advanced"
);
world.insert_resource(MenuActive(false));
spawn_step(&mut spawn, &mut world);
assert!(
lock(&state).saw(&Call::RetireDrawObject(DrawIndex(0))),
"the expiry fires once the menu closes"
);
}
#[test]
fn system_trait_delegates_to_init_and_step() {
use concinnity_core::ecs::System;
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = GraphicsSystem {
test_hooks: Some(hooks),
..GraphicsSystem::new(None)
};
{
let mut ctx = world.context();
crate::ecs::decompose::run(&mut ctx);
gs.init(&mut ctx);
}
assert!(
lock(&state).init.is_some(),
"System::init built the backend"
);
let result = {
let mut ctx = world.context();
gs.step(&mut ctx)
};
assert_eq!(result, StepResult::Continue);
assert_eq!(lock(&state).draw_frames(), 1, "System::step drew a frame");
let shown = format!("{gs:?}");
assert!(shown.contains("frame_count: 1"), "got {shown}");
assert!(shown.contains("failed: false"), "got {shown}");
}
fn post_config_scene(cfg: PostProcessConfig) -> WorldBuilder {
let mut b = scene_builder();
b.push(cfg);
b
}
fn settings_state(world: &World) -> &crate::settings::system::SettingsState {
world
.resource::<crate::settings::system::SettingsSlot>()
.expect("SettingsSlot parked at init")
.0
.as_ref()
.expect("SettingsState in its slot between steps")
}
fn profile_at(tier: GpuTier) -> GpuProfile {
GpuProfile {
vendor: GpuVendor::Amd,
tier,
memory_budget_bytes: 8 << 30,
unified_memory: false,
discrete: true,
}
}
#[test]
fn persisted_post_process_overrides_win_over_authored_config() {
use concinnity_core::components::{AaMode, IndirectLighting, PassResolution};
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Custom);
settings.graphics.exposure_ev = Some(2.0);
settings.graphics.bloom_intensity = Some(0.25);
settings.graphics.bloom_threshold = Some(1.5);
settings.graphics.bloom_knee = Some(0.2);
settings.graphics.vignette = Some(0.4);
settings.graphics.lut_strength = Some(0.3);
settings.graphics.ambient_intensity = Some(2.0);
settings.graphics.ssao = Some(true);
settings.graphics.ssr = Some(true);
settings.graphics.ray_traced_reflections = Some(true);
settings.graphics.ssgi = Some(true);
settings.graphics.auto_exposure = Some(true);
settings.graphics.aa_mode = Some(AaMode::Taa);
settings.graphics.ssgi_resolution = Some(PassResolution::Full);
settings.graphics.ssgi_rays = Some(2);
settings.graphics.reflection_blur_resolution = Some(PassResolution::Full);
settings.graphics.ssao_radius = Some(0.9);
settings.graphics.ssao_intensity = Some(2.0);
settings.graphics.ssr_intensity = Some(0.4);
settings.graphics.ssr_max_distance = Some(20.0);
settings.graphics.ssgi_intensity = Some(1.5);
settings.graphics.ssgi_max_distance = Some(4.0);
settings.graphics.auto_exposure_min_ev = Some(-4.0);
settings.graphics.auto_exposure_max_ev = Some(4.0);
settings.graphics.auto_exposure_speed = Some(3.0);
let (state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut world = post_config_scene(PostProcessConfig {
aa_mode: AaMode::Off,
ssao: false,
exposure_ev: -3.0,
..Default::default()
})
.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
{
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert!(
init.taa_enabled,
"the persisted TAA mode reaches the backend"
);
assert!(
init.ssao_enabled,
"the persisted SSAO toggle reaches the backend"
);
}
let live = settings_state(&world);
assert!(live.graphics.quality.post_config.ssao);
assert!(live.graphics.quality.post_config.ssr);
assert!(live.graphics.quality.post_config.ray_traced_reflections);
assert_eq!(
live.graphics.quality.post_config.indirect_lighting,
IndirectLighting::Ssgi
);
assert!(live.graphics.quality.post_config.auto_exposure);
assert_eq!(live.graphics.quality.post_config.aa_mode, AaMode::Taa);
assert_eq!(
live.graphics.quality.post_config.ssgi_resolution,
PassResolution::Full
);
assert_eq!(live.graphics.quality.post_config.ssgi_rays, 2);
assert_eq!(
live.graphics.quality.post_config.reflection_blur_resolution,
PassResolution::Full
);
assert_eq!(live.graphics.quality.post_config.ssao_radius, 0.9);
assert_eq!(live.graphics.quality.post_config.ssao_intensity, 2.0);
assert_eq!(live.graphics.quality.post_config.ssr_intensity, 0.4);
assert_eq!(live.graphics.quality.post_config.ssr_max_distance, 20.0);
assert_eq!(live.graphics.quality.post_config.ssgi_intensity, 1.5);
assert_eq!(live.graphics.quality.post_config.ssgi_max_distance, 4.0);
assert_eq!(live.graphics.quality.post_config.auto_exposure_min_ev, -4.0);
assert_eq!(live.graphics.quality.post_config.auto_exposure_max_ev, 4.0);
assert_eq!(live.graphics.quality.post_config.auto_exposure_speed, 3.0);
assert_eq!(
live.graphics.post_process.exposure, 4.0,
"2^2 EV, not the authored -3"
);
assert_eq!(live.graphics.post_process.bloom_intensity, 0.25);
assert_eq!(live.graphics.post_process.bloom_threshold, 1.5);
assert_eq!(live.graphics.post_process.bloom_knee, 0.2);
assert_eq!(live.graphics.post_process.vignette, 0.4);
assert_eq!(live.graphics.post_process.lut_strength, 0.3);
assert_eq!(live.graphics.ambient_intensity, 2.0);
assert_eq!(
live.graphics.post_process.fxaa, 1.0,
"TAA keeps the FXAA cleanup"
);
assert_eq!(live.authored.post_config.aa_mode, AaMode::Off);
assert!(!live.authored.post_config.ssao);
}
#[test]
fn low_preset_forces_authored_effects_off_but_keeps_the_baseline() {
use concinnity_core::components::{AaMode, IndirectLighting};
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Low);
let (state, hooks) = recording_hooks_with(settings, profile_at(GpuTier::Integrated));
let mut world = post_config_scene(PostProcessConfig {
ssao: true,
ssr: true,
ray_traced_reflections: true,
indirect_lighting: IndirectLighting::Ssgi,
auto_exposure: true,
aa_mode: AaMode::Taa,
..Default::default()
})
.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
{
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert!(
!init.ssao_enabled,
"the ceiling trims SSAO before the backend sizes it"
);
assert!(!init.taa_enabled, "TAA clamps down to a cheaper AA mode");
}
let live = settings_state(&world);
assert!(!live.graphics.quality.post_config.ssao);
assert!(!live.graphics.quality.post_config.ssr);
assert!(!live.graphics.quality.post_config.ray_traced_reflections);
assert_eq!(
live.graphics.quality.post_config.indirect_lighting,
IndirectLighting::Ibl
);
assert_ne!(live.graphics.quality.post_config.aa_mode, AaMode::Taa);
assert!(live.graphics.quality.post_config.auto_exposure);
assert!(live.authored.post_config.ssao);
assert!(live.authored.post_config.ssr);
assert_eq!(live.authored.post_config.aa_mode, AaMode::Taa);
}
#[test]
fn an_explicit_override_survives_the_preset_ceiling() {
use concinnity_core::components::AaMode;
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Low);
settings.graphics.ssao = Some(true);
settings.graphics.aa_mode = Some(AaMode::Taa);
let (_state, hooks) = recording_hooks_with(settings, profile_at(GpuTier::Integrated));
let mut world = post_config_scene(PostProcessConfig {
ssao: true,
ssr: true,
aa_mode: AaMode::Taa,
..Default::default()
})
.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let live = settings_state(&world);
assert!(
live.graphics.quality.post_config.ssao,
"the explicit SSAO choice wins"
);
assert_eq!(
live.graphics.quality.post_config.aa_mode,
AaMode::Taa,
"explicit AA mode wins"
);
assert!(
!live.graphics.quality.post_config.ssr,
"a row the user never touched still clamps under the ceiling"
);
}
#[test]
fn the_quality_preset_flag_reaches_the_ray_tracing_ceiling() {
let authored = PostProcessConfig {
ray_traced_reflections: true,
..Default::default()
};
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Auto);
let (clamped, hooks) = recording_hooks_with(settings.clone(), profile_at(GpuTier::MidDiscrete));
let mut world = post_config_scene(authored.clone()).build();
init_graphics(&mut world, hooks);
assert_eq!(settings_state(&world).quality_preset, QualityPreset::Auto);
assert!(
!lock(&clamped).init.as_ref().unwrap().rt_reflections_on,
"the Auto -> High ceiling clamps RT off, silently"
);
let (forced, hooks) = recording_hooks_with(settings, profile_at(GpuTier::MidDiscrete));
let mut world = post_config_scene(authored).build();
world.insert_resource(LaunchRequest {
quality_preset: Some(QualityPreset::Ultra),
..Default::default()
});
init_graphics(&mut world, hooks);
assert_eq!(
settings_state(&world).quality_preset,
QualityPreset::Ultra,
"the flag outranks the persisted Auto"
);
assert!(
lock(&forced).init.as_ref().unwrap().rt_reflections_on,
"the Ultra ceiling the flag forced permits RT on the same GPU"
);
}
#[test]
fn the_ray_tracing_flags_reach_the_backend() {
use concinnity_core::render::rt_geom::RtDynamicMode;
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
{
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert_eq!(init.rt_dynamic, RtDynamicMode::Auto);
assert!(init.rt_skinned_geometry);
}
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(LaunchRequest {
rt_dynamic: Some(RtDynamicMode::Rebuild),
rt_skinned_geometry: Some(false),
..Default::default()
});
init_graphics(&mut world, hooks);
{
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert_eq!(init.rt_dynamic, RtDynamicMode::Rebuild);
assert!(!init.rt_skinned_geometry);
}
}
#[test]
fn a_forced_dlss_preset_reaches_the_backend_and_the_menu_unpersisted() {
use concinnity_core::components::{SettingCommand, SettingOp};
use concinnity_core::render::dlss::DlssPreset;
const ROW_LABEL: AssetId = AssetId(700);
let mut saved_settings = crate::config::Settings::default();
saved_settings.graphics.dlss_preset = Some(DlssPreset::K);
let launch = |forced: Option<DlssPreset>| {
let (state, hooks) = recording_hooks_with(saved_settings.clone(), GpuProfile::UNKNOWN);
let mut b = post_config_scene(Default::default());
push_settings_row(&mut b, "dlss_preset", "next", ROW_LABEL);
let mut world = b.build();
world.insert_resource(LaunchRequest {
dlss_preset: forced,
..Default::default()
});
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let built = lock(&state).init.as_ref().unwrap().dlss_preset;
(world, built)
};
let menu = |world: &mut World| {
let live = settings_state(world);
let shown = (
live.graphics.dlss_preset,
live.persisted_graphics.dlss_preset,
);
(shown, label_text(world, ROW_LABEL))
};
let (mut world, built) = launch(None);
assert_eq!(built, DlssPreset::K);
assert_eq!(
menu(&mut world),
((DlssPreset::K, Some(DlssPreset::K)), "K".to_string())
);
let (mut world, built) = launch(Some(DlssPreset::M));
assert_eq!(built, DlssPreset::M);
assert_eq!(
menu(&mut world),
((DlssPreset::M, Some(DlssPreset::K)), "M".to_string())
);
let saved: Arc<Mutex<Vec<crate::config::Settings>>> = Arc::default();
{
let log = Arc::clone(&saved);
let slot = world
.resource_mut::<crate::settings::system::SettingsSlot>()
.expect("SettingsSlot parked at init");
let state = slot.0.as_mut().expect("SettingsState in its slot");
state.settings_cache = Some(saved_settings.clone());
state.settings_writer = Some(crate::settings::system::writer::SettingsWriter::with_sink(
move |cfg| {
log.lock().unwrap().push(cfg.clone());
Ok(())
},
));
}
world.events_mut::<SettingCommand>().send(SettingCommand {
setting: SettingKey::DlssPreset,
op: SettingOp::Next,
value_label: Some(ROW_LABEL),
persist: true,
});
{
use concinnity_core::ecs::System;
crate::settings::system::SettingsSystem::new().step(&mut world.context());
}
assert_eq!(
settings_state(&world).graphics.dlss_preset,
DlssPreset::Default
);
assert_eq!(label_text(&mut world, ROW_LABEL), "Default");
world
.resource_mut::<crate::settings::system::SettingsSlot>()
.unwrap()
.0
.as_mut()
.unwrap()
.settings_writer = None;
let mut expected = saved_settings;
expected.graphics.dlss_preset = Some(DlssPreset::Default);
assert_eq!(saved.lock().unwrap().as_slice(), [expected]);
}
#[test]
fn capture_stays_off_without_a_launch_request() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
assert!(!lock(&state).init.as_ref().unwrap().capture);
}
#[test]
fn a_capture_launch_request_reaches_the_backend() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(LaunchRequest {
capture: true,
..Default::default()
});
init_graphics(&mut world, hooks);
assert!(lock(&state).init.as_ref().unwrap().capture);
}
#[test]
fn a_dev_loop_launch_arms_frame_capture() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(LaunchRequest {
dev_loop: true,
..Default::default()
});
init_graphics(&mut world, hooks);
assert!(lock(&state).init.as_ref().unwrap().capture);
}
#[test]
fn an_embedded_surface_resource_reaches_the_backend() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
assert!(!lock(&state).init.as_ref().unwrap().embedded_surface);
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(concinnity_core::render::backend_init::EmbeddedSurface {
view: std::ptr::NonNull::dangling(),
pump_events: false,
});
init_graphics(&mut world, hooks);
assert!(lock(&state).init.as_ref().unwrap().embedded_surface);
}
#[test]
fn a_high_ceiling_never_enables_what_the_world_turned_off() {
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Ultra);
let (_state, hooks) = recording_hooks_with(settings, profile_at(GpuTier::HighDiscrete));
let mut world = post_config_scene(PostProcessConfig {
ssao: false,
ssr: false,
ray_traced_reflections: false,
..Default::default()
})
.build();
init_graphics(&mut world, hooks);
let live = settings_state(&world);
assert_eq!(live.quality_preset, QualityPreset::Ultra);
assert!(
!live.graphics.quality.post_config.ssao,
"a feature turned off stays off"
);
assert!(!live.graphics.quality.post_config.ssr);
assert!(!live.graphics.quality.post_config.ray_traced_reflections);
}
#[test]
fn the_top_tier_runs_the_default_stack_for_a_world_that_authors_nothing() {
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Ultra);
let (_state, hooks) = recording_hooks_with(settings, profile_at(GpuTier::HighDiscrete));
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
let live = settings_state(&world);
assert!(live.graphics.quality.post_config.ssao);
assert!(live.graphics.quality.post_config.ssr);
assert!(live.graphics.quality.post_config.ray_traced_reflections);
assert_eq!(live.graphics.quality.post_config.aa_mode, AaMode::Taa);
}
#[test]
fn a_low_tier_clamps_the_default_stack_off() {
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Low);
let (_state, hooks) = recording_hooks_with(settings, profile_at(GpuTier::Integrated));
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
let live = settings_state(&world);
assert!(!live.graphics.quality.post_config.ssao);
assert!(!live.graphics.quality.post_config.ssr);
assert!(!live.graphics.quality.post_config.ray_traced_reflections);
assert_eq!(
live.graphics.quality.post_config.indirect_lighting,
IndirectLighting::Ibl
);
assert_eq!(live.graphics.quality.post_config.aa_mode, AaMode::Fxaa);
}
#[test]
fn auto_preset_shadow_ceiling_tracks_the_detected_tier() {
let sizes: Vec<u32> = [
GpuTier::Integrated,
GpuTier::EntryDiscrete,
GpuTier::MidDiscrete,
GpuTier::HighDiscrete,
]
.into_iter()
.map(|tier| {
let (state, hooks) =
recording_hooks_with(crate::config::Settings::default(), profile_at(tier));
let mut world = scene_builder().build();
init_graphics(&mut world, hooks);
assert_eq!(settings_state(&world).quality_preset, QualityPreset::Auto);
lock(&state).init.as_ref().unwrap().shadows.map_size
})
.collect();
assert!(
sizes.windows(2).all(|w| w[0] <= w[1]),
"a stronger tier never shadows at a smaller map: {sizes:?}"
);
assert_eq!(
sizes.last().copied(),
Some(4096),
"the top tier leaves the world's authored 4096 unclamped"
);
}
#[test]
fn persisted_display_and_system_overrides_reach_the_backend() {
use concinnity_core::components::{UpscaleQuality, UpscalerBackend, WindowMode};
use concinnity_core::window::display_mode::DisplayMode;
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Custom);
settings.graphics.window_mode = Some(WindowMode::Fullscreen);
settings.graphics.resolution = Some([1920, 1080, 60]);
settings.graphics.render_scale = Some(UpscaleQuality::Performance);
settings.graphics.upscale_backend = Some(UpscalerBackend::Fsr3);
settings.graphics.temporal_upscaling = Some(true);
settings.graphics.hdr_display = Some(true);
settings.graphics.hdr_pq = Some(true);
settings.graphics.occlusion_two_pass = Some(true);
settings.graphics.texture_cap = Some(192);
settings.graphics.texture_budget = Some(8);
settings.graphics.perf_stats = Some(false);
settings.graphics.show_fps = Some(false);
settings.graphics.show_vram = Some(false);
let (state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut b = post_config_scene(Default::default());
b.push(StreamingConfig::default());
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
{
let s = lock(&state);
assert!(
s.saw(&Call::SetWindowMode(WindowMode::Fullscreen)),
"a persisted non-windowed mode is applied after construction"
);
assert!(
s.saw(&Call::SetDisplayMode(DisplayMode {
width: 1920,
height: 1080,
refresh_hz: 60,
})),
"the chosen fullscreen mode is handed to the backend"
);
}
let live = settings_state(&world);
assert_eq!(
live.graphics.quality.render_scale,
UpscaleQuality::Performance
);
assert_eq!(live.graphics.upscale_backend, UpscalerBackend::Fsr3);
assert!(live.graphics.temporal_upscaling);
assert!(live.graphics.hdr_display);
assert!(live.graphics.hdr_pq);
assert!(live.graphics.occlusion_two_pass);
assert_eq!(live.graphics.texture_cap, 192);
assert_eq!(live.graphics.texture_budget, 8);
assert!(!live.graphics.perf_stats);
assert!(!live.graphics.show_fps);
assert!(!live.graphics.show_vram);
assert_eq!(live.window_args.mode, WindowMode::Fullscreen);
assert_eq!(
live.resolution,
Some(DisplayMode {
width: 1920,
height: 1080,
refresh_hz: 60,
})
);
assert!(
!world
.resource::<crate::ecs::DisplayModes>()
.expect("mode list published for the dropdown")
.0
.is_empty()
);
assert!(world.resource::<FrameRateCap>().is_some());
}
fn push_settings_row(b: &mut WorldBuilder, key: &str, verb: &str, label: AssetId) {
b.push(HitRegion {
action: act(serde_json::json!({"setting": {"key": key, "verb": verb}})),
label: Some(Ref::new(label)),
..Default::default()
});
b.push_identified(
label,
TextLabel {
content: "<placeholder>".to_string(),
color: LIT,
..Default::default()
},
);
}
const LIT: [f32; 3] = [0.9, 0.9, 0.9];
fn label_text(world: &mut World, id: AssetId) -> String {
world
.context()
.get_by_id::<TextLabel>(id)
.expect("label present")
.content
.clone()
}
fn label_color(world: &mut World, id: AssetId) -> [f32; 3] {
world
.context()
.get_by_id::<TextLabel>(id)
.expect("label present")
.color
}
#[test]
fn settings_rows_show_their_live_values_at_init() {
use concinnity_core::components::ShadowUpdate;
let mut settings = crate::config::Settings::default();
settings.graphics.vsync = Some(true);
settings.graphics.fps_cap = Some(60);
settings.graphics.shadow_map_size = Some(4096);
settings.graphics.shadow_update = Some(ShadowUpdate::EveryFrame);
settings.graphics.shadow_distance = Some(160);
settings.graphics.shadow_cascades = Some(3);
settings.graphics.anisotropy = Some(16);
settings.graphics.frames_in_flight = Some(3);
settings.graphics.occlusion_two_pass = Some(true);
settings.graphics.texture_cap = Some(384);
settings.graphics.hdr_display = Some(true);
settings.graphics.ssao = Some(true);
settings.graphics.ssgi_rays = Some(4);
let (_state, hooks) = recording_hooks_with(settings, profile_at(GpuTier::MidDiscrete));
let mut b = post_config_scene(Default::default());
let rows: Vec<(&str, &str, AssetId)> = vec![
("vsync", "next", AssetId(100)),
("fps_cap", "next", AssetId(101)),
("window_mode", "next", AssetId(102)),
("render_scale", "next", AssetId(103)),
("master_volume", "next", AssetId(104)),
("temporal_upscaling", "next", AssetId(105)),
("hdr_display", "next", AssetId(106)),
("hdr_pq", "next", AssetId(107)),
("perf_stats", "next", AssetId(108)),
("show_fps", "next", AssetId(109)),
("show_vram", "next", AssetId(110)),
("shadow_map_size", "next", AssetId(111)),
("shadow_update", "next", AssetId(112)),
("shadow_distance", "next", AssetId(113)),
("shadow_cascades", "next", AssetId(114)),
("anisotropy", "next", AssetId(115)),
("frames_in_flight", "next", AssetId(116)),
("occlusion_two_pass", "next", AssetId(117)),
("texture_quality", "next", AssetId(118)),
("ssao", "next", AssetId(119)),
("ssgi_rays", "open", AssetId(120)),
("graphics_quality", "next", AssetId(121)),
];
for &(key, verb, label) in &rows {
push_settings_row(&mut b, key, verb, label);
}
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(
label_text(&mut world, AssetId(100)),
"On",
"persisted vsync"
);
assert_eq!(label_text(&mut world, AssetId(101)), "60");
assert_eq!(label_text(&mut world, AssetId(102)), "Windowed");
assert_eq!(label_text(&mut world, AssetId(106)), "On", "hdr_display");
assert_eq!(label_text(&mut world, AssetId(107)), "Off", "hdr_pq unset");
assert_eq!(label_text(&mut world, AssetId(111)), "4096");
assert_eq!(label_text(&mut world, AssetId(114)), "3");
assert_eq!(label_text(&mut world, AssetId(115)), "16x");
assert_eq!(label_text(&mut world, AssetId(116)), "3");
assert_eq!(label_text(&mut world, AssetId(117)), "On", "occlusion");
assert_eq!(label_text(&mut world, AssetId(119)), "On", "quality toggle");
assert_eq!(label_text(&mut world, AssetId(120)), "4", "cycle dropdown");
assert_eq!(
label_text(&mut world, AssetId(121)),
"Auto (High)",
"the master row carries the tier the Auto preset resolved to, \
which the static option table cannot express"
);
for &(key, _, label) in &rows {
if key == "not_a_setting" {
continue;
}
assert_ne!(
label_text(&mut world, label),
"<placeholder>",
"{key} still shows the build placeholder"
);
}
let live = settings_state(&world);
assert_eq!(
live.cycle_value_labels.get(&SettingKey::SsgiRays),
Some(&AssetId(120))
);
assert_eq!(
live.cycle_value_labels.get(&SettingKey::Vsync),
Some(&AssetId(100))
);
}
#[test]
fn slider_rows_sync_their_handle_and_label_to_the_live_value() {
let mut settings = crate::config::Settings::default();
settings.graphics.quality_preset = Some(QualityPreset::Custom);
settings.graphics.exposure_ev = Some(0.0);
settings.graphics.vignette = Some(0.5);
let (_state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut b = post_config_scene(Default::default());
for (key, handle, label) in [
("exposure", AssetId(200), AssetId(201)),
("vignette", AssetId(202), AssetId(203)),
] {
b.push(HitRegion {
action: act(serde_json::json!({"setting": {"key": key, "verb": "drag"}})),
x: 0.0,
width: 100.0,
drag_handle: Some(Ref::new(handle)),
label: Some(Ref::new(label)),
..Default::default()
});
b.push_identified(
handle,
Sprite {
width: 10.0,
..Default::default()
},
);
b.push_identified(
label,
TextLabel {
content: "<placeholder>".to_string(),
..Default::default()
},
);
}
b.push(HitRegion {
action: act(serde_json::json!({"setting": {"key": "exposure", "verb": "drag"}})),
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let handle_x = |world: &mut World, id: AssetId| {
world
.context()
.get_by_id::<Sprite>(id)
.expect("handle sprite present")
.x
};
assert_eq!(handle_x(&mut world, AssetId(200)), 45.0);
assert_eq!(label_text(&mut world, AssetId(201)), "+0.0 EV");
assert_eq!(handle_x(&mut world, AssetId(202)), 45.0);
assert_ne!(label_text(&mut world, AssetId(203)), "<placeholder>");
let live = settings_state(&world);
let keys: Vec<SettingKey> = live.sliders.iter().map(|s| s.key).collect();
assert!(keys.contains(&SettingKey::Exposure));
assert!(keys.contains(&SettingKey::Vignette));
let exposure = live
.sliders
.iter()
.find(|s| s.key == SettingKey::Exposure)
.unwrap();
assert_eq!(
(exposure.track_x, exposure.track_w, exposure.handle_w),
(0.0, 100.0, 10.0)
);
}
#[test]
fn every_owned_slider_key_recovers_a_live_value() {
let (_state, hooks) = recording_hooks();
let mut b = post_config_scene(Default::default());
let keys = [
"exposure",
"bloom_intensity",
"bloom_threshold",
"bloom_knee",
"vignette",
"lut_strength",
"ambient_intensity",
"ssao_radius",
"ssao_intensity",
"ssr_intensity",
"ssr_max_distance",
"ssgi_intensity",
"ssgi_max_distance",
"auto_exposure_min_ev",
"auto_exposure_max_ev",
"auto_exposure_speed",
];
for (i, key) in keys.iter().enumerate() {
let handle = AssetId(300 + i as u32 * 2);
let label = AssetId(301 + i as u32 * 2);
b.push(HitRegion {
action: act(serde_json::json!({"setting": {"key": key, "verb": "drag"}})),
x: 0.0,
width: 100.0,
drag_handle: Some(Ref::new(handle)),
label: Some(Ref::new(label)),
..Default::default()
});
b.push_identified(
handle,
Sprite {
width: 10.0,
..Default::default()
},
);
b.push_identified(
label,
TextLabel {
content: "<placeholder>".to_string(),
..Default::default()
},
);
}
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
for (i, key) in keys.iter().enumerate() {
let label = AssetId(301 + i as u32 * 2);
assert_ne!(
label_text(&mut world, label),
"<placeholder>",
"{key} never synced to a live value"
);
}
assert_eq!(settings_state(&world).sliders.len(), keys.len());
}
#[test]
fn rebind_rows_show_their_bound_keys_at_init() {
use concinnity_core::components::InputKey;
use concinnity_core::input::keymap::{Bindable, KeyMap};
let mut settings = crate::config::Settings::default();
settings.controls.keymap = Some(KeyMap {
forward: InputKey::Up,
..Default::default()
});
let (_state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut b = scene_builder();
for (i, action) in Bindable::ALL.iter().enumerate() {
let label = AssetId(400 + i as u32);
b.push(HitRegion {
action: act(
serde_json::json!({"setting": {"key": action.setting_key(), "verb": "rebind"}}),
),
label: Some(Ref::new(label)),
..Default::default()
});
b.push_identified(
label,
TextLabel {
content: "<placeholder>".to_string(),
..Default::default()
},
);
}
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(
label_text(&mut world, AssetId(400)),
InputKey::Up.display_name(),
"the persisted forward rebind shows on its row"
);
for i in 1..Bindable::ALL.len() {
assert_ne!(
label_text(&mut world, AssetId(400 + i as u32)),
"<placeholder>",
"row {i} never synced to its default binding"
);
}
let live = settings_state(&world);
assert_eq!(live.rebind_rows.len(), Bindable::ALL.len());
assert_eq!(
live.keymap.forward,
InputKey::Up,
"the persisted map is the live one"
);
}
#[test]
fn scroll_panel_rows_clip_their_elements_to_the_panel_band() {
use concinnity_core::components::{ScrollPanel, ScrollRow};
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(ScrollPanel {
x: 10.0,
y: 20.0,
width: 300.0,
height: 400.0,
rows: vec![
ScrollRow {
elements: vec![Ref::new(AssetId(500)), Ref::new(AssetId(501))],
..Default::default()
},
ScrollRow {
elements: vec![Ref::new(AssetId(502))],
..Default::default()
},
],
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let clips = &world
.resource::<crate::gfx::overlay::OverlayAssets>()
.expect("OverlayAssets parked at init")
.clip_rects;
let band = [10.0, 20.0, 300.0, 400.0];
for id in [500, 501, 502] {
assert_eq!(
clips.get(&AssetId(id)),
Some(&band),
"element {id} clips to its panel's band"
);
}
assert!(
clips.get(&AssetId(503)).is_none(),
"an unlisted element is unclipped"
);
}
#[test]
fn a_capability_gated_row_grays_out_its_whole_scroll_row() {
use concinnity_core::components::{ScrollPanel, ScrollRow};
let (state, hooks) = recording_hooks();
state.lock().unwrap().caps = backend::DeviceCapabilities {
selectable_upscaler: false,
..backend::DeviceCapabilities::ALL
};
let mut b = post_config_scene(Default::default());
push_settings_row(&mut b, "upscale_backend", "next", AssetId(600));
b.push_identified(
AssetId(601),
TextLabel {
content: "Upscaler".to_string(),
color: LIT,
..Default::default()
},
);
push_settings_row(&mut b, "vsync", "next", AssetId(602));
b.push(ScrollPanel {
rows: vec![
ScrollRow {
elements: vec![Ref::new(AssetId(600)), Ref::new(AssetId(601))],
..Default::default()
},
ScrollRow {
elements: vec![Ref::new(AssetId(602))],
..Default::default()
},
],
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(
label_color(&mut world, AssetId(600)),
DISABLED,
"the gated row's value label"
);
assert_eq!(
label_color(&mut world, AssetId(601)),
DISABLED,
"the gray expands to the row's name label, not just its value"
);
assert_eq!(
label_color(&mut world, AssetId(602)),
LIT,
"an available row stays lit"
);
let disabled: Vec<bool> = world
.context()
.query::<HitRegion>()
.filter(|r| {
matches!(
r.action,
Some(UiAction::Setting {
key: SettingKey::UpscaleBackend,
..
})
)
})
.map(|r| r.disabled)
.collect();
assert_eq!(disabled, vec![true]);
}
#[test]
fn the_dlss_preset_row_follows_whether_dlss_is_selectable() {
let preset_row_disabled = |vendor| {
let mut profile = GpuProfile::UNKNOWN;
profile.vendor = vendor;
let (_, hooks) = recording_hooks_with(crate::config::Settings::default(), profile);
let mut b = post_config_scene(Default::default());
push_settings_row(&mut b, "dlss_preset", "next", AssetId(600));
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let disabled: Vec<bool> = world
.context()
.query::<HitRegion>()
.filter(|r| {
matches!(
r.action,
Some(UiAction::Setting {
key: SettingKey::DlssPreset,
..
})
)
})
.map(|r| r.disabled)
.collect();
disabled
};
assert_eq!(preset_row_disabled(GpuVendor::Nvidia), vec![false]);
assert_eq!(preset_row_disabled(GpuVendor::Amd), vec![true]);
}
const DISABLED: [f32; 3] = crate::settings::system::rows::DISABLED_ROW_COLOR;
#[test]
fn master_toggles_gray_the_rows_they_govern() {
use concinnity_core::components::{ScrollPanel, ScrollRow};
let mut settings = crate::config::Settings::default();
settings.graphics.perf_stats = Some(false);
let (_state, hooks) = recording_hooks_with(settings, GpuProfile::UNKNOWN);
let mut b = scene_builder();
push_settings_row(&mut b, "show_fps", "next", AssetId(700));
push_settings_row(&mut b, "show_vram", "next", AssetId(701));
push_settings_row(&mut b, "resolution", "open", AssetId(702));
push_settings_row(&mut b, "vsync", "next", AssetId(703));
b.push(ScrollPanel {
rows: [700, 701, 702, 703]
.into_iter()
.map(|id| ScrollRow {
elements: vec![Ref::new(AssetId(id))],
..Default::default()
})
.collect(),
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(
label_color(&mut world, AssetId(700)),
DISABLED,
"show_fps grays under the master toggle"
);
assert_eq!(label_color(&mut world, AssetId(701)), DISABLED, "show_vram");
assert_eq!(
label_color(&mut world, AssetId(702)),
DISABLED,
"the Resolution row grays outside fullscreen"
);
assert_eq!(
label_color(&mut world, AssetId(703)),
LIT,
"an unrelated row"
);
let live = settings_state(&world);
assert!(live.perf_sub_row_labels.contains(&(AssetId(700), LIT)));
assert!(live.resolution_row_labels.contains(&(AssetId(702), LIT)));
}
fn font_payload(w: u32, h: u32) -> Vec<u8> {
let mut bytes = Vec::new();
for v in [w, h, 1, 32] {
bytes.extend_from_slice(&v.to_le_bytes());
}
bytes.extend(vec![0xFFu8; (w * h * 4) as usize]);
bytes.extend_from_slice(&1u32.to_le_bytes());
bytes.extend_from_slice(&(b'H' as u32).to_le_bytes());
for v in [0u16, 0, 16, 22] {
bytes.extend_from_slice(&v.to_le_bytes());
}
for v in [18.0f32, 1.0, 22.0] {
bytes.extend_from_slice(&v.to_le_bytes());
}
bytes
}
impl WorldBuilder {
fn push_resource(&mut self, kind: concinnity_core::ecs::ResourceKind, bytes: &[u8]) -> u32 {
let locator = self.payload(bytes);
let handle = self.kind_records(kind).len() as u32;
self.kind_records(kind)
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: kind,
handle,
payload: Some(locator),
data_bytes: Vec::new(),
});
handle
}
fn kind_records(
&mut self,
kind: concinnity_core::ecs::ResourceKind,
) -> &mut Vec<concinnity_core::ecs::ResourceRecord> {
use concinnity_core::ecs::ResourceKind;
match kind {
ResourceKind::Texture => &mut self.texture_records,
ResourceKind::Font => &mut self.font_records,
ResourceKind::ColorLut => &mut self.color_lut_records,
ResourceKind::EnvironmentMap => &mut self.env_map_records,
ResourceKind::SkinnedMesh => &mut self.skinned_records,
other => panic!("no record list wired for {other:?}"),
}
}
}
#[test]
fn text_naming_no_font_falls_back_to_the_built_in_face() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_identified(
AssetId(800),
TextLabel {
content: "Hello, world!".to_string(),
font: None,
visible: true,
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let labels: Vec<TextLabel> = world.context().query::<TextLabel>().cloned().collect();
let overlay = world
.resource::<crate::gfx::overlay::OverlayAssets>()
.expect("OverlayAssets parked at init");
let face = overlay
.fonts
.resolve(None)
.expect("a label naming no font has a face to draw with");
let calls = text::build_text_calls(&labels, &overlay.fonts, [640.0, 360.0]);
assert_eq!(calls.len(), 1, "one draw call for the one label");
let glyphs = "Hello, world!".chars().filter(|c| *c != ' ').count();
assert_eq!(
calls[0].vertices.len(),
glyphs * 4,
"four vertices per glyph quad"
);
assert_eq!(calls[0].atlas_slot, face.atlas_slot);
let s = lock(&state);
assert_eq!(
s.init.as_ref().unwrap().text_atlas_count,
1,
"the built-in atlas is uploaded for the font-less label"
);
assert_eq!(
face.atlas_slot, 0,
"it takes the slot after the world fonts"
);
}
#[test]
fn a_world_whose_text_names_its_fonts_registers_no_fallback() {
use concinnity_core::ecs::ResourceKind;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
let font = b.push_resource(ResourceKind::Font, &font_payload(32, 32));
b.push_identified(
AssetId(800),
TextLabel {
content: "Hello, world!".to_string(),
font: Some(FontHandle::new(font)),
visible: true,
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let overlay = world
.resource::<crate::gfx::overlay::OverlayAssets>()
.expect("OverlayAssets parked at init");
assert_eq!(overlay.fonts.len(), 1);
assert!(overlay.fonts.resolve(None).is_none());
let s = lock(&state);
assert_eq!(
s.init.as_ref().unwrap().text_atlas_count,
1,
"the world's own font atlas, and no second one"
);
}
#[test]
fn fonts_and_sprite_textures_share_the_text_atlas_pool() {
use concinnity_core::ecs::ResourceKind;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_resource(ResourceKind::Font, &font_payload(32, 32));
b.push_resource(ResourceKind::Font, &font_payload(64, 64));
let sprite_tex = b.push_resource(ResourceKind::Texture, &texture_payload(4, 4));
for id in [800u32, 801] {
b.push_identified(
AssetId(id),
Sprite {
texture: Some(TextureHandle::new(sprite_tex)),
visible: true,
..Default::default()
},
);
}
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
assert_eq!(
s.init.as_ref().unwrap().text_atlas_count,
3,
"two font atlases plus the one deduped sprite texture"
);
drop(s);
let overlay = world
.resource::<crate::gfx::overlay::OverlayAssets>()
.expect("OverlayAssets parked at init");
assert_eq!(overlay.fonts.len(), 2);
let face = |h| overlay.fonts.get(FontHandle::new(h)).expect("loaded");
assert_eq!(face(0).atlas_slot, 0, "font handle 0 owns atlas slot 0");
assert_eq!(face(1).atlas_slot, 1);
assert_eq!(face(1).atlas_w, 64);
assert_eq!(face(0).size_px, 32.0);
assert_eq!(
overlay
.sprite_texture_slots
.get(&TextureHandle::new(sprite_tex)),
Some(&2),
"the sprite texture is appended after the font atlases"
);
}
#[test]
fn a_sprite_with_an_unknown_texture_keeps_its_tint() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_identified(
AssetId(810),
Sprite {
texture: Some(TextureHandle::new(99)),
visible: true,
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed, "an unresolvable sprite texture is not fatal");
assert_eq!(
lock(&state).init.as_ref().unwrap().text_atlas_count,
0,
"nothing was appended to the text-atlas pool"
);
assert!(
world
.resource::<crate::gfx::overlay::OverlayAssets>()
.unwrap()
.sprite_texture_slots
.is_empty()
);
}
#[test]
fn a_malformed_font_payload_fails_init() {
use concinnity_core::ecs::ResourceKind;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_resource(ResourceKind::Font, b"not-a-font");
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
assert!(lock(&state).init.is_none(), "backend never constructed");
}
#[test]
fn a_malformed_texture_payload_fails_init() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
let bad = b.payload(b"not-a-texture");
b.texture_records[0].payload = Some(bad);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
assert!(lock(&state).init.is_none(), "backend never constructed");
}
#[test]
fn a_deferred_texture_slot_decodes_to_a_placeholder() {
use std::collections::HashSet;
let mut b = scene_builder();
b.push_resource(
concinnity_core::ecs::ResourceKind::Texture,
&texture_payload(4, 4),
);
let locators: Vec<PayloadLocator> = b
.texture_records
.iter()
.filter_map(|r| r.payload.clone())
.collect();
let mut world = b.build();
let deferred = HashSet::from([1]);
let disk = super::texture_payloads::decode_texture_payloads(
&mut world.context(),
&locators,
&deferred,
true,
)
.expect("decodes");
assert_eq!(disk.images.len(), 2);
assert_eq!((disk.images[0].width(), disk.images[0].height()), (2, 2));
assert_eq!((disk.images[1].width(), disk.images[1].height()), (1, 1));
assert!(disk.payloads.is_empty());
let ram = super::texture_payloads::decode_texture_payloads(
&mut world.context(),
&locators,
&deferred,
false,
)
.expect("decodes");
assert_eq!((ram.images[1].width(), ram.images[1].height()), (1, 1));
assert_eq!(
ram.payloads,
vec![texture_payload(2, 2), texture_payload(4, 4)]
);
}
#[test]
fn environment_map_and_color_lut_payloads_survive_to_the_backend() {
use concinnity_core::ecs::ResourceKind;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_resource(ResourceKind::EnvironmentMap, b"ibl-cube-bytes");
b.push_resource(ResourceKind::EnvironmentMap, b"second-ibl");
b.push_resource(ResourceKind::ColorLut, b"lut-bytes");
b.push_resource(ResourceKind::ColorLut, b"second-lut");
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed, "extra singletons are ignored, not fatal");
assert!(lock(&state).init.is_some(), "the backend was built");
}
#[test]
fn an_unreadable_environment_map_payload_fails_init() {
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.env_map_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::EnvironmentMap,
handle: 0,
payload: Some(PayloadLocator {
blob_index: 0,
offset: 1 << 30,
len: 16,
}),
data_bytes: Vec::new(),
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
}
#[test]
fn volumetric_fog_resolves_into_the_backend_init() {
use concinnity_core::components::VolumetricFog;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(VolumetricFog {
enabled: true,
density: 0.05,
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(
lock(&state).init.as_ref().unwrap().fog,
"the fog pass is on"
);
let pushed = world.resource::<super::parked::PushedFogSettings>();
assert!(pushed.is_some_and(|fog| fog.0.is_some()));
}
#[test]
fn disabled_volumetric_fog_skips_the_fog_pass() {
use concinnity_core::components::VolumetricFog;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(VolumetricFog {
enabled: false,
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(!lock(&state).init.as_ref().unwrap().fog);
let pushed = world.resource::<super::parked::PushedFogSettings>();
assert!(pushed.is_some_and(|fog| fog.0.is_none()));
}
#[test]
fn declared_reflection_probes_replace_the_auto_seed() {
use concinnity_core::components::ReflectionProbe;
use concinnity_core::render::reflection_probe::ProbePlacement;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
let probes = [(0.0, None), (10.0, Some(6.0))].map(|(x, capture_distance)| ReflectionProbe {
position: [x, 1.0, 0.0],
half_extents: [4.0; 3],
capture_distance,
});
for probe in &probes {
b.push(probe.clone());
}
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(
lock(&state).saw(&Call::SetReflectionProbes(
probes.iter().map(ProbePlacement::from).collect()
)),
"both declared placements reach the backend, capture distance and all"
);
assert_eq!(
world.context().query::<ReflectionProbe>().count(),
2,
"the probes stay resident after their placements reach the backend"
);
}
#[test]
fn instanced_prop_bakes_its_instances_into_one_cluster() {
use concinnity_core::components::{InstanceTransform, InstancedProp};
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_identified(
AssetId(820),
InstancedProp {
mesh: Some(MeshHandle::new(0)),
material: Some(MaterialHandle::new(0)),
instances: (0..3)
.map(|i| InstanceTransform {
position: [i as f32 * 2.0, 0.0, 0.0],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
})
.collect(),
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert_eq!(init.instanced_cluster_count, 1, "one mesh = one cluster");
assert_eq!(
init.draw_objects.len(),
1,
"the instances ride the cluster, not one draw object each"
);
drop(s);
assert_eq!(
world.context().query::<InstancedProp>().count(),
0,
"drained"
);
}
#[test]
fn one_shot_world_fx_are_resolved_and_drained_at_init() {
use concinnity_core::components::{
Decal, GlassPanel, ParticleEmitter, SdfVolume, WaterSurface,
};
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_identified(
AssetId(830),
Decal {
texture: Some(TextureHandle::new(0)),
size: [1.0; 3],
visible: true,
..Default::default()
},
);
b.push_identified(
AssetId(831),
ParticleEmitter {
texture: Some(TextureHandle::new(0)),
max_particles: 16,
visible: true,
..Default::default()
},
);
b.push(WaterSurface::default());
b.push(GlassPanel::default());
let sdf_frag = b.payload(b"sdf-fragment-bytes");
b.push_identified(
AssetId(832),
SdfVolume {
extent: [2.0; 3],
locator: Some(sdf_frag),
visible: true,
..Default::default()
},
);
b.push_identified(
AssetId(833),
SdfVolume {
extent: [2.0; 3],
locator: None,
visible: true,
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed, "a payload-less SdfVolume is not fatal");
assert!(lock(&state).init.is_some());
let ctx = world.context();
assert_eq!(ctx.query::<Decal>().count(), 0);
assert_eq!(ctx.query::<ParticleEmitter>().count(), 0);
assert_eq!(ctx.query::<WaterSurface>().count(), 0);
assert_eq!(ctx.query::<GlassPanel>().count(), 0);
assert_eq!(ctx.query::<SdfVolume>().count(), 0);
}
#[test]
fn sdf_volumes_without_a_readable_payload_are_skipped() {
use concinnity_core::components::SdfVolume;
use concinnity_host::thread::asset_id;
let mut b = WorldBuilder::new();
let named = asset_id::intern("glass_orb");
let frag = b.payload(b"sdf-fragment-bytes");
b.push_identified(
named,
SdfVolume {
locator: Some(frag),
..Default::default()
},
);
b.push_identified(
AssetId(841),
SdfVolume {
locator: None,
..Default::default()
},
);
b.push_identified(
AssetId(842),
SdfVolume {
locator: Some(PayloadLocator {
blob_index: 7,
offset: 0,
len: 4,
}),
..Default::default()
},
);
let mut world = b.build();
let volumes = super::world_fx::drain_sdf_volumes(&mut world.context());
assert_eq!(volumes.len(), 1);
assert_eq!(volumes[0].fragment_source, b"sdf-fragment-bytes");
assert_eq!(volumes[0].label, "glass_orb");
assert_eq!(world.context().query::<SdfVolume>().count(), 0, "drained");
}
#[test]
fn a_backend_that_fails_to_build_marks_graphics_failed() {
let (state, mut hooks) = recording_hooks();
hooks.backend_factory = Box::new(|_init| {
Err(error::RenderError::OutOfDeviceMemory(
"no device memory".to_string(),
))
});
let mut world = scene_builder().build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
assert!(lock(&state).init.is_none());
let mut gs = gs;
assert_eq!(step(&mut gs, &mut world), StepResult::Stop);
}
#[test]
fn an_unsupported_backend_marks_graphics_failed() {
let (_state, mut hooks) = recording_hooks();
hooks.backend_factory =
Box::new(|_init| Err(error::RenderError::Unsupported { op: "init_backend" }));
let mut world = scene_builder().build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
}
fn skinned_payload(n: u16, lods: &[(f32, Vec<u16>)]) -> Vec<u8> {
skinned_payload_with_morphs(n, lods, &mesh_payload::PayloadMorphs::default())
}
fn skinned_payload_with_morphs(
n: u16,
lods: &[(f32, Vec<u16>)],
morphs: &mesh_payload::PayloadMorphs,
) -> Vec<u8> {
use concinnity_core::gfx::mesh_payload::{
PayloadJoint, SkinnedVertex, serialize_skinned_with_lods,
};
let vertices: Vec<SkinnedVertex> = (0..n)
.map(|i| SkinnedVertex {
pos: [i as f32, 0.0, 0.0],
normal: [0.0, 1.0, 0.0],
tangent: [1.0, 0.0, 0.0],
color: [1.0; 3],
uv: [0.0, 0.0],
joints: [0, 0, 0, 0],
weights: [1.0, 0.0, 0.0, 0.0],
})
.collect();
let joint = |name: &str, parent: i32| PayloadJoint {
name: name.to_string(),
parent,
translation: [0.0; 3],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
};
serialize_skinned_with_lods(
&vertices,
&[0, 1, 2],
&[joint("root", -1), joint("child", 0)],
morphs,
lods,
)
}
fn push_skinned_mesh(b: &mut WorldBuilder, name: AssetId, sm: SkinnedMesh, n: u16) {
push_skinned_payload(b, name, sm, &skinned_payload(n, &[]));
}
fn push_skinned_payload(b: &mut WorldBuilder, name: AssetId, sm: SkinnedMesh, payload: &[u8]) {
let locator = b.payload(payload);
let handle = b.skinned_records.len() as u32;
let data = postcard::to_allocvec(&(name.0, sm)).unwrap();
b.skinned_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::SkinnedMesh,
handle,
payload: Some(locator),
data_bytes: data,
});
}
#[test]
fn morph_target_upload_failure_fails_init() {
use concinnity_core::components::SkinnedMesh;
const MORPHED: AssetId = AssetId(846);
let delta = mesh_payload::MorphDelta {
position: [0.0, 1.0, 0.0],
normal: [0.0; 3],
};
let morphs =
mesh_payload::PayloadMorphs::from_dense(vec!["raise".to_string()], 3, &[delta; 3]).unwrap();
let payload = skinned_payload_with_morphs(3, &[], &morphs);
for fail in [
None,
Some(error::RenderError::Other(
"morph buffer rejected".to_string(),
)),
] {
let failing = fail.is_some();
let (state, hooks) = recording_hooks();
lock(&state).fail_morph_upload = fail;
let mut b = scene_builder();
push_skinned_payload(&mut b, MORPHED, SkinnedMesh::default(), &payload);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert_eq!(gs.failed, failing);
let s = lock(&state);
assert!(s.saw(&Call::UploadSkinnedMorphs), "{:?}", s.calls);
}
}
#[test]
fn skinned_mesh_world_uploads_geometry_and_publishes_poses() {
use concinnity_core::components::{CharacterCapsule, SkinnedMesh};
const RIGGED: AssetId = AssetId(840);
const PLAIN: AssetId = AssetId(841);
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(
&mut b,
RIGGED,
SkinnedMesh {
material: Some(MaterialHandle::new(0)),
position: [5.0, 0.0, 0.0],
capsule: Some(CharacterCapsule {
half_height: 0.9,
radius: 0.3,
}),
..Default::default()
},
4,
);
push_skinned_mesh(&mut b, PLAIN, SkinnedMesh::default(), 3);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
{
let s = lock(&state);
assert!(
s.saw(&Call::UploadSkinned {
vertices: 7,
draws: 2,
}),
"both meshes' geometry rides one upload: {:?}",
s.calls
);
assert_eq!(s.init.as_ref().unwrap().n_skinned, 2);
}
let ctx = world.context();
let mut poses: Vec<(u32, SkinnedIndex)> = ctx
.query::<SkeletonPose>()
.map(|p| (p.mesh_id.0, p.skinned_index))
.collect();
poses.sort();
assert_eq!(poses, vec![(0, SkinnedIndex(0)), (1, SkinnedIndex(1))]);
let rigs: Vec<u32> = ctx.query::<CharacterRig>().map(|r| r.target.0).collect();
assert_eq!(rigs, vec![0], "only the capsule mesh gets a character rig");
let names = ctx
.resource::<crate::gfx::skinned_mesh_map::SkinnedMeshNameIndex>()
.expect("name index published before AnimationSystem inits");
assert_eq!(names.0.get(&RIGGED).map(|h| h.0), Some(0));
assert_eq!(names.0.get(&PLAIN).map(|h| h.0), Some(1));
assert!(ctx.entity_of(RIGGED).is_some());
assert!(ctx.entity_of(PLAIN).is_some());
}
#[test]
fn skinned_instance_reserves_get_their_own_vertex_regions() {
use concinnity_core::components::SkinnedMesh;
const HERO: AssetId = AssetId(842);
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(
&mut b,
HERO,
SkinnedMesh {
max_instances: 3,
..Default::default()
},
4,
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
assert!(
s.saw(&Call::UploadSkinned {
vertices: 16,
draws: 4,
}),
"each reserved copy owns a distinct vertex region: {:?}",
s.calls
);
assert_eq!(
world
.resource::<crate::ecs::ActiveRenderQueues>()
.and_then(|slot| slot.0.as_ref())
.expect("graphics init publishes the recording surfaces")
.slots
.skinned_free(),
3,
"the engine's instance pool is seeded with the reserved copies"
);
assert_eq!(s.init.as_ref().unwrap().n_skinned, 4);
}
#[test]
fn a_skinned_mesh_with_an_unknown_material_fails_init() {
use concinnity_core::components::SkinnedMesh;
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(
&mut b,
AssetId(843),
SkinnedMesh {
material: Some(MaterialHandle::new(99)),
..Default::default()
},
3,
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
}
#[test]
fn a_skinned_mesh_without_usable_geometry_fails_init() {
use concinnity_core::components::SkinnedMesh;
use concinnity_core::ecs::{ResourceKind, ResourceRecord};
let record = |b: &mut WorldBuilder, payload: Option<PayloadLocator>| {
let data = postcard::to_allocvec(&(844u32, SkinnedMesh::default())).unwrap();
b.skinned_records.push(ResourceRecord {
resource_kind: ResourceKind::SkinnedMesh,
handle: 0,
payload,
data_bytes: data,
});
};
let mut b = scene_builder();
let loc = b.payload(b"not-a-skinned-mesh");
record(&mut b, Some(loc));
let (_state, hooks) = recording_hooks();
let mut world = b.build();
assert!(init_graphics(&mut world, hooks).failed, "malformed payload");
let mut b = scene_builder();
record(&mut b, None);
let (_state, hooks) = recording_hooks();
let mut world = b.build();
assert!(init_graphics(&mut world, hooks).failed, "no payload");
let mut b = scene_builder();
b.skinned_records.push(ResourceRecord {
resource_kind: ResourceKind::SkinnedMesh,
handle: 0,
payload: None,
data_bytes: vec![0xFF; 3],
});
let (_state, hooks) = recording_hooks();
let mut world = b.build();
assert!(init_graphics(&mut world, hooks).failed, "undecodable data");
}
#[test]
fn skinned_poses_upload_when_flagged_and_freeze_behind_a_menu() {
use concinnity_core::components::SkinnedMesh;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(&mut b, AssetId(845), SkinnedMesh::default(), 3);
let mut world = b.build();
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let flag_pose = |world: &mut World| {
let mut ctx = world.context();
let pose = ctx.query_mut::<SkeletonPose>().next().unwrap();
pose.updated = true;
};
step(&mut gs, &mut world);
assert!(
lock(&state).saw(&Call::UpdateSkinnedPose(SkinnedIndex(0))),
"the seeded bind pose is pushed on the first frame"
);
lock(&state).calls.clear();
step(&mut gs, &mut world);
assert!(
!lock(&state).saw(&Call::UpdateSkinnedPose(SkinnedIndex(0))),
"an untouched pose is not re-uploaded"
);
flag_pose(&mut world);
lock(&state).calls.clear();
step(&mut gs, &mut world);
assert!(
lock(&state).saw(&Call::UpdateSkinnedPose(SkinnedIndex(0))),
"a flagged pose is uploaded"
);
flag_pose(&mut world);
world.insert_resource(ScreenStack {
pauses_world: true,
..Default::default()
});
lock(&state).calls.clear();
step(&mut gs, &mut world);
assert!(
!lock(&state).saw(&Call::UpdateSkinnedPose(SkinnedIndex(0))),
"pose uploads are skipped while a menu is open"
);
world.insert_resource(MenuOverride(Some(true)));
lock(&state).calls.clear();
step(&mut gs, &mut world);
assert!(
lock(&state).saw(&Call::UpdateSkinnedPose(SkinnedIndex(0))),
"the editor override uploads a flagged pose"
);
}
impl WorldBuilder {
fn push_material(&mut self, mat: Material) -> MaterialHandle {
let handle = self.material_records.len() as u32;
self.material_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::Material,
handle,
payload: None,
data_bytes: postcard::to_allocvec(&mat).unwrap(),
});
MaterialHandle::new(handle)
}
}
#[test]
fn every_material_texture_reference_resolves_to_its_shared_pool_slot() {
use concinnity_core::ecs::ResourceKind;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
let slots: Vec<u32> = (0..4)
.map(|_| b.push_resource(ResourceKind::Texture, &texture_payload(2, 2)))
.collect();
let mat = b.push_material(Material {
albedo: Some(TextureHandle::new(slots[0])),
normal_map: Some(TextureHandle::new(slots[1])),
emissive_map: Some(TextureHandle::new(slots[2])),
orm_map: Some(TextureHandle::new(slots[3])),
..Default::default()
});
b.push_identified(
AssetId(850),
Prop {
mesh: Some(MeshHandle::new(0)),
material: Some(mat),
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert_eq!(init.texture_count, 5, "one pool holds every texture");
let draw = init
.draw_objects
.iter()
.find(|d| d.texture_slot == slots[0] as usize)
.expect("the prop's draw resolved its albedo to the pool slot");
assert_eq!(
draw.normal_map_slot, slots[1] as usize,
"a normal map resolves to the same shared pool, not a separate one"
);
assert_eq!(draw.material.emissive_map_index, slots[2]);
assert_eq!(draw.material.orm_map_index, slots[3]);
}
#[test]
fn a_material_texture_handle_past_the_pool_fails_init() {
let out_of_range = Some(TextureHandle::new(99));
let cases: Vec<(&str, Material)> = vec![
(
"albedo",
Material {
albedo: out_of_range,
..Default::default()
},
),
(
"normal_map",
Material {
normal_map: out_of_range,
..Default::default()
},
),
(
"emissive_map",
Material {
emissive_map: out_of_range,
..Default::default()
},
),
(
"orm_map",
Material {
orm_map: out_of_range,
..Default::default()
},
),
];
for (role, mat) in cases {
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push_material(mat);
let mut world = b.build();
assert!(
init_graphics(&mut world, hooks).failed,
"an out-of-range {role} handle must fail the build"
);
}
}
#[test]
fn an_undecodable_material_record_fails_init() {
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
b.material_records
.push(concinnity_core::ecs::ResourceRecord {
resource_kind: concinnity_core::ecs::ResourceKind::Material,
handle: 1,
payload: None,
data_bytes: vec![0xFF; 2],
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(gs.failed);
assert!(!backend_parked(&world));
}
#[test]
fn an_instanced_vertex_shader_payload_reaches_the_backend() {
use concinnity_core::components::{InstanceTransform, InstancedProp};
let (state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window {
title: "mock world".to_string(),
width: 640,
height: 360,
..Default::default()
});
b.push(GraphicsConfig {
clear_color: [0.1, 0.2, 0.3, 1.0],
..Default::default()
});
b.push_shader(&["vertex_main_bindless", "fragment_main_bindless"]);
b.push(Camera3D::bake(Default::default()));
b.push_textured_quad(MESH, TEX, MAT, PROP);
b.push_identified(
AssetId(851),
InstancedProp {
mesh: Some(MeshHandle::new(0)),
material: Some(MaterialHandle::new(0)),
instances: vec![InstanceTransform {
position: [0.0; 3],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
}],
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(
lock(&state).init.as_ref().unwrap().instanced_cluster_count,
1
);
}
#[test]
fn a_shader_without_a_payload_fails_init() {
let (_state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push(Window::default());
b.push(GraphicsConfig::default());
b.push(Shader {
locator: None,
..Default::default()
});
let mut world = b.build();
assert!(
init_graphics(&mut world, hooks).failed,
"a Shader with no compiled payload must fail the build"
);
}
#[test]
fn skinned_lod_alternates_rebase_onto_their_slot_vertex_region() {
use concinnity_core::components::SkinnedMesh;
use concinnity_core::ecs::{ResourceKind, ResourceRecord};
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
let locator = b.payload(&skinned_payload(4, &[(10.0, vec![0, 1, 2])]));
let data = postcard::to_allocvec(&(
852u32,
SkinnedMesh {
max_instances: 1,
..Default::default()
},
))
.unwrap();
b.skinned_records.push(ResourceRecord {
resource_kind: ResourceKind::SkinnedMesh,
handle: 0,
payload: Some(locator),
data_bytes: data,
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
assert!(
s.saw(&Call::UploadSkinned {
vertices: 8,
draws: 2,
}),
"an LOD alternate shares its slot's vertex region: {:?}",
s.calls
);
assert_eq!(
world
.resource::<crate::ecs::ActiveRenderQueues>()
.and_then(|slot| slot.0.as_ref())
.expect("graphics init publishes the recording surfaces")
.slots
.skinned_free(),
1,
"the engine's instance pool is seeded with the reserved copy"
);
}
#[test]
fn a_world_without_a_window_or_config_builds_on_the_defaults() {
let (state, hooks) = recording_hooks();
let mut b = WorldBuilder::new();
b.push_shaders();
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
let init = s.init.as_ref().unwrap();
assert_eq!(init.frames_in_flight, 2, "the system's own default");
assert_eq!(init.window_width, Window::default().width);
}
#[test]
fn a_step_without_a_parked_backend_finishes_the_system() {
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
crate::ecs::ActiveRenderBackend::take(world.context().resources).expect("backend was parked");
assert_eq!(step(&mut gs, &mut world), StepResult::Done);
}
#[test]
fn reparent_request_with_an_unresolved_child_is_skipped() {
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
ctx.events_mut::<ReparentRequest>().send(ReparentRequest {
child: AssetId(901).into(),
parent: Some(PROP.into()),
});
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
let child = entity_named(&mut world, PROP);
assert!(
world
.context()
.get::<Children>(child)
.is_none_or(|c| c.0.is_empty()),
"nothing was parented under the named parent"
);
}
#[test]
fn a_spawn_naming_an_unknown_template_is_skipped() {
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let _gs = init_graphics(&mut world, hooks);
{
let mut ctx = world.context();
ctx.events_mut::<SpawnRequest>().send(SpawnRequest {
template: AssetId(902),
name: Some(AssetId(903)),
transform: Transform::default(),
lifetime_secs: None,
});
let spawner = ctx.components.spawn();
ctx.insert(
spawner,
Spawner {
template: AssetId(904),
interval: 1.0,
lifetime: 0.0,
elapsed: 1.0,
count: 0,
},
);
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
assert!(
!lock(&state)
.calls
.iter()
.any(|c| matches!(c, Call::CloneStaticDrawObject { .. })),
"an unresolvable template clones nothing"
);
assert_eq!(
world.context().query::<RenderHandle>().count(),
1,
"only the original"
);
}
#[test]
fn a_spawn_naming_a_skinned_template_takes_the_instance_pool_path() {
use concinnity_core::components::SkinnedMesh;
const HERO: AssetId = AssetId(853);
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(
&mut b,
HERO,
SkinnedMesh {
max_instances: 0,
..Default::default()
},
3,
);
let mut world = b.build();
let _gs = init_graphics(&mut world, hooks);
let poses_before = world.context().query::<SkeletonPose>().count();
{
let mut ctx = world.context();
ctx.events_mut::<SpawnRequest>().send(SpawnRequest {
template: HERO,
name: Some(AssetId(905)),
transform: Transform::default(),
lifetime_secs: Some(2.0),
});
let spawner = ctx.components.spawn();
ctx.insert(
spawner,
Spawner {
template: HERO,
interval: 1.0,
lifetime: 2.0,
elapsed: 1.0,
count: 0,
},
);
}
let mut spawn = crate::spawn::SpawnSystem::new();
spawn_step(&mut spawn, &mut world);
assert!(
!lock(&state)
.calls
.iter()
.any(|c| matches!(c, Call::CloneStaticDrawObject { .. })),
"a skinned template never clones a static draw slot"
);
assert_eq!(
world.context().query::<SkeletonPose>().count(),
poses_before,
"an exhausted instance pool spawns nothing rather than growing a buffer"
);
}
#[test]
fn hot_reload_seams_park_the_fog_beside_the_backend() {
use concinnity_core::components::VolumetricFog;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
b.push(VolumetricFog {
enabled: true,
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(
world
.resource::<super::hot_reload_sources::HotReloadSources>()
.is_none(),
"a plain build captures no source catalog"
);
let resources = world.context().resources;
let (backend, fog, slots) = resources.get_disjoint_mut::<
crate::ecs::ActiveRenderBackend,
super::parked::PushedFogSettings,
super::parked::TextureNameSlots,
>();
assert!(
fog.is_some_and(|fog| fog.0.is_some()),
"the reload pass dedupes against the fog init pushed to the backend"
);
assert!(
slots.is_none(),
"the texture-name map is captured only under cn debug"
);
backend
.and_then(|slot| slot.0.as_deref_mut())
.expect("init parks the backend")
.wait_idle();
assert!(
lock(&state).saw(&Call::WaitIdle),
"the parked backend is the one init built"
);
}
#[test]
fn a_residency_cap_below_the_streamed_set_reserves_a_smaller_seed() {
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
for i in 0..2 {
b.push_textured_quad(
AssetId(10 + i),
AssetId(20 + i),
AssetId(30 + i),
AssetId(860 + i),
);
}
b.push(StreamingConfig {
mesh_cap: 1,
..Default::default()
});
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
let s = lock(&state);
assert!(
s.saw(&Call::SeedMeshStreaming),
"the backend's sub-allocator is seeded with the smaller headroom block"
);
let init = s.init.as_ref().unwrap();
assert_eq!(init.draw_objects.len(), 3);
assert!(
init.vertex_count < 12,
"the seed buffer is born smaller than the whole streamed set: {}",
init.vertex_count
);
}
#[test]
fn story_stage_images_are_resident_before_any_sprite_references_them() {
use concinnity_core::components::{Story, StoryImage, StoryNode, StoryPage, StoryStage};
use concinnity_core::ecs::ResourceKind;
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
let slots: Vec<u32> = (0..4)
.map(|_| b.push_resource(ResourceKind::Texture, &texture_payload(2, 2)))
.collect();
let image = |slot: u32| {
Some(StoryImage {
texture: TextureHandle::new(slot),
..Default::default()
})
};
b.push_identified(
AssetId(870),
Story {
nodes: vec![StoryNode {
pages: vec![StoryPage {
stage: StoryStage {
bg: image(slots[0]),
left: image(slots[1]),
..Default::default()
},
..Default::default()
}],
choice_stage: StoryStage {
center: image(slots[2]),
right: image(slots[3]),
..Default::default()
},
..Default::default()
}],
..Default::default()
},
);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(
lock(&state).init.as_ref().unwrap().text_atlas_count,
4,
"every stage image is decoded into the pool ahead of the swap"
);
let overlay = world
.resource::<crate::gfx::overlay::OverlayAssets>()
.unwrap();
for slot in &slots {
assert!(
overlay
.sprite_texture_slots
.contains_key(&TextureHandle::new(*slot)),
"stage texture {slot} never became resident"
);
}
}
#[test]
fn pick_index_publishes_world_bounds_only_when_opted_in() {
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(PickIndex::default());
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(gs.pick_candidates.len(), 1, "one candidate per prop");
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let index = world
.resource::<PickIndex>()
.expect("step publishes the index");
assert_eq!(index.entries.len(), 1);
let e = &index.entries[0];
assert_eq!(e.asset_id, PROP);
assert_eq!(e.bb_min, [1.0, 2.0, 3.0]);
assert_eq!(e.bb_max, [2.0, 2.0, 4.0]);
let (_state, hooks) = recording_hooks();
let mut world = scene_builder().build();
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(gs.pick_candidates.is_empty());
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(world.resource::<PickIndex>().is_none());
}
#[test]
fn editor_hidden_collapses_draws_and_skips_the_pick_index() {
const COLLAPSED: [[f32; 4]; 4] = [[0.0; 4], [0.0; 4], [0.0; 4], [0.0, 0.0, 0.0, 1.0]];
let (state, hooks) = recording_hooks();
let mut world = scene_builder().build();
world.insert_resource(PickIndex::default());
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
world.insert_resource(HiddenAssets([PROP].into_iter().collect()));
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
{
let index = world.resource::<PickIndex>().unwrap();
assert!(index.entries.is_empty(), "a hidden prop is not pickable");
let s = lock(&state);
let model = s.models.get(&DrawIndex(0)).expect("slot 0 model pushed");
assert_eq!(*model, COLLAPSED, "the hidden prop's slot is degenerate");
}
world.insert_resource(HiddenAssets::default());
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let index = world.resource::<PickIndex>().unwrap();
assert_eq!(index.entries.len(), 1, "clearing the set restores the prop");
let s = lock(&state);
let model = s.models.get(&DrawIndex(0)).unwrap();
assert_ne!(*model, COLLAPSED, "the real transform is pushed again");
}
#[test]
fn skinned_mesh_joins_the_pick_index_when_opted_in() {
use concinnity_core::components::SkinnedMesh;
const BODY: AssetId = AssetId(842);
let (state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(
&mut b,
BODY,
SkinnedMesh {
material: Some(MaterialHandle::new(0)),
position: [5.0, 0.0, 0.0],
..Default::default()
},
4,
);
let mut world = b.build();
world.insert_resource(PickIndex::default());
let mut gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert_eq!(gs.pick_candidates.len(), 2, "the prop and the skinned mesh");
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
let index = world
.resource::<PickIndex>()
.expect("step publishes the index");
let body = index
.entries
.iter()
.find(|e| e.asset_id == BODY)
.expect("the skinned mesh indexes");
assert!(body.bb_min[0] >= 5.0, "bounds follow the authored position");
let entity = world
.context()
.join2::<SkeletonPose, Transform>()
.map(|(e, _, _)| e)
.next()
.expect("the template pose carries a Transform");
world
.context()
.get_mut::<Transform>(entity)
.unwrap()
.position = [7.0, 0.0, 0.0];
assert_eq!(step(&mut gs, &mut world), StepResult::Continue);
assert!(
lock(&state)
.calls
.iter()
.any(|c| matches!(c, Call::UpdateSkinnedModel(SkinnedIndex(0)))),
"the moved template reaches the backend"
);
let index = world.resource::<PickIndex>().unwrap();
let body = index.entries.iter().find(|e| e.asset_id == BODY).unwrap();
assert!(body.bb_min[0] >= 7.0, "the index follows the move");
let (_state, hooks) = recording_hooks();
let mut b = scene_builder();
push_skinned_mesh(&mut b, BODY, SkinnedMesh::default(), 4);
let mut world = b.build();
let gs = init_graphics(&mut world, hooks);
assert!(!gs.failed);
assert!(gs.pick_candidates.is_empty());
assert_eq!(
world.context().join2::<SkeletonPose, Transform>().count(),
0
);
}
fn act(action: serde_json::Value) -> Option<UiAction> {
Some(serde_json::from_value(action).unwrap())
}