use crate::debug_hook::DebugHook;
use crate::ecs::World;
use crate::gfx::animation::AnimationSystem;
use crate::gfx::graphics_system::GraphicsSystem;
use std::io::BufReader;
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use concinnity_engine::shutdown::ShutdownToken;
use crate::debug::state::{AssetEntry, CameraSnapshot, DebugState};
use crate::debug::{hot_reload, runtime_spawn};
use crate::mcp::AppServer;
const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
const SNAPSHOT_INTERVAL: u64 = 30;
pub(crate) struct DebugServer {
shared: Arc<Mutex<DebugState>>,
frame: u64,
reload: hot_reload::HotReloadDriver,
camera_motion: Option<runtime_spawn::CameraMotion>,
}
impl DebugServer {
pub(crate) fn start(port: u16) -> std::io::Result<Self> {
let listener = TcpListener::bind(("127.0.0.1", port))?;
let shared = Arc::new(Mutex::new(DebugState::default()));
let shared_for_thread = Arc::clone(&shared);
std::thread::Builder::new()
.name("debug-server".to_string())
.spawn(move || serve(listener, shared_for_thread))?;
tracing::info!("debug server listening on http://127.0.0.1:{port}/mcp");
Ok(Self {
shared,
frame: 0,
reload: hot_reload::HotReloadDriver::new(),
camera_motion: None,
})
}
pub(crate) fn with_notifier(mut self, notifier: crate::editor::notify::Notifier) -> Self {
self.reload = self.reload.with_notifier(notifier);
self
}
}
impl DebugServer {
fn drive_runtime_commands(&mut self, world: &mut World) {
let mut deferred_ecs_cmds: Vec<runtime_spawn::RuntimeCommand> = Vec::new();
let (systems, mut backend) = concinnity_engine::ecs::systems_and_render_backend(world);
for system in systems {
if let Some(gs) = system.downcast_mut::<GraphicsSystem>() {
if let Some(backend) = backend.take() {
let apply = gs.hot_reload_apply_parts(backend);
for cmd in runtime_spawn::drain() {
if matches!(
cmd,
runtime_spawn::RuntimeCommand::CameraSet { .. }
| runtime_spawn::RuntimeCommand::CameraMove { .. }
| runtime_spawn::RuntimeCommand::CameraStop { .. }
| runtime_spawn::RuntimeCommand::QualitySet { .. }
| runtime_spawn::RuntimeCommand::Rebind { .. }
| runtime_spawn::RuntimeCommand::Despawn { .. }
| runtime_spawn::RuntimeCommand::Reparent { .. }
| runtime_spawn::RuntimeCommand::Spawn { .. }
| runtime_spawn::RuntimeCommand::Story { .. }
) {
deferred_ecs_cmds.push(cmd);
} else {
runtime_spawn::dispatch_runtime_spawn(
cmd,
apply.world_reload.as_ref(),
apply.backend,
);
}
}
}
} else if let Some(anim) = system.downcast_mut::<AnimationSystem>() {
anim.apply_runtime_commands();
}
}
for cmd in deferred_ecs_cmds {
match cmd {
runtime_spawn::RuntimeCommand::CameraSet { .. } => {
runtime_spawn::dispatch_camera_set(cmd, world);
}
runtime_spawn::RuntimeCommand::QualitySet { .. } => {
runtime_spawn::dispatch_quality_set(cmd, world);
}
runtime_spawn::RuntimeCommand::Rebind { .. } => {
runtime_spawn::dispatch_rebind(cmd, world);
}
runtime_spawn::RuntimeCommand::Despawn { .. } => {
runtime_spawn::dispatch_despawn(cmd, world);
}
runtime_spawn::RuntimeCommand::Reparent { .. } => {
runtime_spawn::dispatch_reparent(cmd, world);
}
runtime_spawn::RuntimeCommand::Spawn { .. } => {
runtime_spawn::dispatch_spawn(cmd, world);
}
runtime_spawn::RuntimeCommand::Story { .. } => {
runtime_spawn::dispatch_story(cmd, world);
}
runtime_spawn::RuntimeCommand::CameraMove { args, reply } => {
if world
.query::<crate::components::Camera3D>()
.next()
.is_some()
{
self.camera_motion = Some(runtime_spawn::CameraMotion::from_args(&args));
let _ = reply.send(Ok(()));
} else {
let _ = reply.send(Err("camera-move: no Camera3D in world".to_string()));
}
}
runtime_spawn::RuntimeCommand::CameraStop { reply } => {
self.camera_motion = None;
let _ = reply.send(Ok(()));
}
_ => {}
}
}
if let Some(motion) = self.camera_motion.take()
&& runtime_spawn::apply_camera_move_step(&motion, world)
{
self.camera_motion = motion.advanced();
}
}
}
impl DebugHook for DebugServer {
fn tick(&mut self, world: &mut World) {
self.frame += 1;
self.drive_runtime_commands(world);
self.reload.drive(world);
let mut state = match self.shared.lock() {
Ok(s) => s,
Err(poisoned) => poisoned.into_inner(),
};
state.frame = self.frame;
state.streaming = concinnity_engine::ecs::streaming_stats(world).unwrap_or_default();
state.scratch = world.scratch_stats();
state.streaming_pressure = concinnity_engine::ecs::streaming_pressure(world).map(|p| {
crate::debug::state::PressureSnapshot {
rss_bytes: p.rss_bytes,
budget_bytes: p.budget_bytes,
under_pressure: p.under_pressure,
}
});
if let (Some(threads), Some(memory)) = (
concinnity_engine::ecs::thread_budget(world),
concinnity_engine::ecs::memory_budget(world),
) {
state.budget = Some(crate::debug::state::BudgetSnapshot {
total_cores: threads.total_cores,
job_threads: threads.job_threads,
total_ram_mib: memory.total_ram_bytes.map(|b| b / (1024 * 1024)),
budget_mib: memory.budget_mib(),
overridden: memory.overridden,
rss_mib: concinnity_engine::app::sysmem::process_resident_bytes()
.map(|b| b / (1024 * 1024)),
});
}
if state.shader_reload.is_none()
&& let Some(flag) = world
.resource::<crate::ecs::ActiveRenderBackend>()
.and_then(|slot| slot.0.as_ref())
.and_then(|backend| backend.shader_reload_flag())
{
state.shader_reload = Some(flag);
}
if let Some(pending) = self.reload.pending() {
state.asset_reload = Some(pending);
}
let profile = world.profile();
state.profile_systems = profile
.system_timings()
.iter()
.map(|&(name, micros)| (name.to_string(), micros))
.collect();
state.profile_allocs = profile
.system_allocs()
.iter()
.map(|&(name, allocs)| (name.to_string(), allocs))
.collect();
state.profile_frame_allocs = profile.frame_allocs();
state.profile_render = profile.render;
state.camera = world
.query::<crate::components::Camera3D>()
.next()
.map(|c| CameraSnapshot {
position: c.position,
yaw: c.yaw,
pitch: c.pitch,
fov_y_degrees: c.fov_y_degrees,
near: c.near,
far: c.far,
});
if self.frame % SNAPSHOT_INTERVAL == 1 {
state.system_count = world.system_count();
state.component_count = world.component_count();
state.systems = world
.systems()
.iter()
.map(|s| s.name().to_string())
.collect();
state.assets = world
.component_census()
.into_iter()
.map(|(discriminant, count)| AssetEntry {
discriminant,
count,
})
.collect();
if state.names.is_empty() {
state.names = std::sync::Arc::new(crate::ecs::asset_id::name_table());
}
}
}
fn attach_shutdown(&mut self, shutdown: ShutdownToken) {
let mut state = match self.shared.lock() {
Ok(s) => s,
Err(poisoned) => poisoned.into_inner(),
};
state.shutdown_token = Some(shutdown);
}
}
fn serve(listener: TcpListener, shared: Arc<Mutex<DebugState>>) {
let server = Arc::new(AppServer::new(shared));
for stream in listener.incoming() {
let stream = match stream {
Ok(s) => s,
Err(e) => {
tracing::debug!("debug server accept error: {e}");
continue;
}
};
let server = Arc::clone(&server);
std::thread::spawn(move || {
if let Err(e) = handle_conn(stream, &server) {
tracing::debug!("debug client closed: {e}");
}
});
}
}
fn handle_conn(stream: TcpStream, server: &AppServer) -> std::io::Result<()> {
stream.set_read_timeout(Some(CONNECTION_TIMEOUT))?;
stream.set_write_timeout(Some(CONNECTION_TIMEOUT))?;
let mut input = BufReader::new(stream.try_clone()?);
let mut output = stream;
server.serve(&mut input, &mut output)
}