use concinnity_core::components::AppConfig;
use concinnity_core::ecs::World;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderMode {
Rendered,
Headless,
}
impl RenderMode {
pub fn renders(self) -> bool {
self == RenderMode::Rendered
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeadlessReason {
Requested,
NoBackend,
NoGpu,
NothingToDraw,
}
impl HeadlessReason {
fn as_str(self) -> &'static str {
match self {
HeadlessReason::Requested => "the world asks for it (AppConfig.headless)",
HeadlessReason::NoBackend => "this build compiles no render backend",
HeadlessReason::NoGpu => "this machine exposes no usable GPU",
HeadlessReason::NothingToDraw => "the world holds nothing to draw",
}
}
}
pub fn resolve(world: &World) -> (RenderMode, Option<HeadlessReason>) {
let headless = |r| (RenderMode::Headless, Some(r));
if world
.query::<AppConfig>()
.next()
.is_some_and(|c| c.headless)
{
return headless(HeadlessReason::Requested);
}
if !world.renders() {
return headless(HeadlessReason::NothingToDraw);
}
if !crate::device::AVAILABLE {
return headless(HeadlessReason::NoBackend);
}
if crate::device::probe_gpu_profile().is_none() {
return headless(HeadlessReason::NoGpu);
}
(RenderMode::Rendered, None)
}
pub fn from_content(world: &World) -> RenderMode {
let asked_for_headless = world
.query::<AppConfig>()
.next()
.is_some_and(|c| c.headless);
if !asked_for_headless && world.renders() {
RenderMode::Rendered
} else {
RenderMode::Headless
}
}
pub fn resolve_and_report(world: &World) -> RenderMode {
let (mode, reason) = resolve(world);
if let Some(reason) = reason {
tracing::info!("Running headless: {}", reason.as_str());
}
mode
}
#[cfg(test)]
mod tests {
use super::*;
use concinnity_core::components::{PhysicsConfig, Prop, TextLabel};
#[test]
fn a_world_with_something_to_draw_is_not_refused_for_being_empty() {
let mut world = World::new();
world.add_component(TextLabel {
content: "Hello, world!".into(),
..Default::default()
});
let (_, reason) = resolve(&world);
assert_ne!(reason, Some(HeadlessReason::NothingToDraw));
}
#[test]
fn a_world_with_nothing_to_draw_runs_headless() {
let mut world = World::new();
world.add_component(PhysicsConfig::default());
assert_eq!(
resolve(&world),
(RenderMode::Headless, Some(HeadlessReason::NothingToDraw))
);
}
#[test]
fn an_empty_world_runs_headless() {
assert_eq!(
resolve(&World::new()),
(RenderMode::Headless, Some(HeadlessReason::NothingToDraw))
);
}
#[test]
fn app_config_headless_overrides_a_world_full_of_geometry() {
let mut world = World::new();
world.add_component(Prop::default());
world.add_component(AppConfig {
headless: true,
..Default::default()
});
assert_eq!(
resolve(&world),
(RenderMode::Headless, Some(HeadlessReason::Requested))
);
}
#[test]
fn an_app_config_that_does_not_ask_for_headless_leaves_the_choice_to_content() {
let mut world = World::new();
world.add_component(Prop::default());
world.add_component(AppConfig::default());
let (_, reason) = resolve(&world);
assert_ne!(reason, Some(HeadlessReason::Requested));
}
#[test]
fn from_content_answers_for_the_world_not_the_machine() {
let mut world = World::new();
world.add_component(Prop::default());
assert_eq!(from_content(&world), RenderMode::Rendered);
let mut world = World::new();
world.add_component(PhysicsConfig::default());
assert_eq!(from_content(&world), RenderMode::Headless);
}
#[test]
fn from_content_honors_the_headless_opt_out() {
let mut world = World::new();
world.add_component(Prop::default());
world.add_component(AppConfig {
headless: true,
..Default::default()
});
assert_eq!(from_content(&world), RenderMode::Headless);
}
}