use concinnity_core::animation::skeleton;
use concinnity_core::components::SkeletonJoint;
use concinnity_core::components::Story;
use concinnity_core::gfx::mesh_payload;
use concinnity_core::gfx::render_types::SkinnedIndex;
use concinnity_core::render::backend::RenderBackend;
use concinnity_engine::live_edit::hot_reload_sources::*;
use concinnity_engine::live_edit::parked::PushedFogSettings;
use std::sync::{Arc, Mutex};
use super::decode::{poll_pending_assets, poll_pending_envmap, reload_assets};
use super::passes::{reload_procedural_meshes, reload_stories, reload_volumetric_fog};
use super::report::ReloadReport;
use super::sdf::SdfReload;
use super::shader::ShaderReload;
use super::signals::ReloadSignals;
use super::watcher::spawn_watcher;
type Inflight<T> = Mutex<Option<std::sync::mpsc::Receiver<T>>>;
#[derive(Debug)]
pub(crate) struct DecodedTexture {
pub slot: usize,
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
pub source: String,
}
#[derive(Debug)]
pub(crate) struct DecodedColorLut {
pub size: u32,
pub data: Vec<u8>,
pub source: String,
}
#[derive(Debug)]
pub(crate) struct DecodedMesh {
pub entry_idx: usize,
pub vertices: Vec<mesh_payload::Vertex>,
pub indices: Vec<u16>,
pub lod_alternates: Vec<(f32, Vec<u16>)>,
}
#[derive(Debug)]
pub(crate) struct DecodedSkinnedMesh {
pub entry_idx: usize,
pub vertices: Vec<mesh_payload::SkinnedVertex>,
pub indices: Vec<u16>,
pub skeleton: Vec<SkeletonJoint>,
}
#[derive(Debug, Default)]
pub(crate) struct DecodedAssetBatch {
pub textures: Vec<DecodedTexture>,
pub color_lut: Option<DecodedColorLut>,
pub meshes: Vec<DecodedMesh>,
pub skinned_meshes: Vec<DecodedSkinnedMesh>,
pub decode_failures: usize,
}
pub(crate) struct AssetHotReloadState {
pub map: TextureSourceMap,
pub color_lut: Option<ColorLutSource>,
pub environment_map: Option<EnvironmentMapSource>,
pub meshes: MeshSourceMap,
pub skinned_meshes: SkinnedMeshSourceMap,
pub procedural_meshes: ProceduralMeshSourceMap,
pub shaders: ShaderReload,
pub sdf_fields: SdfReload,
pub world_jsonl_path: Option<String>,
pub signals: Arc<ReloadSignals>,
pub env_map_inflight: Inflight<Result<Vec<u8>, String>>,
pub asset_batch_inflight: Inflight<DecodedAssetBatch>,
pub story_snapshots: std::collections::HashMap<String, serde_json::Value>,
pub pending_skeleton_updates: Vec<PendingSkeletonUpdate>,
watcher: Option<notify::RecommendedWatcher>,
}
#[derive(Debug)]
pub(crate) struct PendingSkeletonUpdate {
pub skinned_index: SkinnedIndex,
pub new_skeleton: skeleton::Skeleton,
}
impl std::fmt::Debug for AssetHotReloadState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let env_inflight = self
.env_map_inflight
.try_lock()
.map(|g| g.is_some())
.unwrap_or(false);
let batch_inflight = self
.asset_batch_inflight
.try_lock()
.map(|g| g.is_some())
.unwrap_or(false);
f.debug_struct("AssetHotReloadState")
.field("entries", &self.map.entries.len())
.field("color_lut", &self.color_lut.is_some())
.field("environment_map", &self.environment_map.is_some())
.field("meshes", &self.meshes.entries.len())
.field("skinned_meshes", &self.skinned_meshes.entries.len())
.field("procedural_meshes", &self.procedural_meshes.entries.len())
.field("shaders", &self.shaders.catalog.len())
.field("sdf_fields", &self.sdf_fields.catalog.len())
.field("world_jsonl_path", &self.world_jsonl_path)
.field("signals", &self.signals)
.field("env_map_inflight", &env_inflight)
.field("asset_batch_inflight", &batch_inflight)
.field("watcher", &self.watcher.is_some())
.finish()
}
}
impl AssetHotReloadState {
pub(crate) fn from_sources(
sources: HotReloadSources,
world_jsonl_path: Option<String>,
signals: Arc<ReloadSignals>,
) -> Self {
let watcher = if sources.is_empty() && world_jsonl_path.is_none() {
None
} else {
spawn_watcher(&sources, world_jsonl_path.as_deref(), Arc::clone(&signals))
};
let HotReloadSources {
map,
color_lut,
environment_map,
meshes,
skinned_meshes,
procedural_meshes,
shaders,
shader_overrides,
sdf_fields,
} = sources;
Self {
map,
color_lut,
environment_map,
meshes,
skinned_meshes,
procedural_meshes,
shaders: ShaderReload::new(shaders, shader_overrides),
sdf_fields: SdfReload::new(sdf_fields),
world_jsonl_path,
signals,
env_map_inflight: Mutex::new(None),
asset_batch_inflight: Mutex::new(None),
story_snapshots: std::collections::HashMap::new(),
pending_skeleton_updates: Vec::new(),
watcher,
}
}
pub(crate) fn drain_pending_skeleton_updates(&mut self) -> Vec<PendingSkeletonUpdate> {
std::mem::take(&mut self.pending_skeleton_updates)
}
}
pub(crate) struct FrameHotReloadEffects {
pub skeleton_updates: Vec<PendingSkeletonUpdate>,
pub story_updates: Vec<Story>,
pub reload_reports: Vec<ReloadReport>,
}
pub(crate) fn run_frame(
state: &mut AssetHotReloadState,
backend: &mut dyn RenderBackend,
fog: &mut PushedFogSettings,
notify: Option<&crate::editor::notify::Notifier>,
) -> FrameHotReloadEffects {
use crate::editor::notify::Action;
let mut effects = FrameHotReloadEffects {
skeleton_updates: Vec::new(),
story_updates: Vec::new(),
reload_reports: Vec::new(),
};
poll_pending_envmap(state, backend);
poll_pending_assets(state, backend);
effects.skeleton_updates = state.drain_pending_skeleton_updates();
if state.signals.take_assets() {
reload_assets(state);
}
let pending_shaders = state.signals.take_shaders();
let mut reload_reports = Vec::new();
if !pending_shaders.is_empty() {
reload_reports = state
.shaders
.request(&pending_shaders, backend.pipeline_builder());
}
reload_reports.extend(state.shaders.poll(backend));
let pending_volumes = state.signals.take_sdf_volumes();
if !pending_volumes.is_empty() {
reload_reports.extend(
state
.sdf_fields
.request(&pending_volumes, backend.pipeline_builder()),
);
}
reload_reports.extend(state.sdf_fields.poll(backend));
super::report::report(&reload_reports, notify);
effects.reload_reports = reload_reports;
if state.signals.take_stories() {
let path = state.world_jsonl_path.clone();
if let Some(path) = path {
effects.story_updates = reload_stories(&path, &mut state.story_snapshots);
if !effects.story_updates.is_empty() {
tracing::info!(
"story hot-reload: {} story graph(s) re-compiled",
effects.story_updates.len(),
);
if let Some(n) = notify {
n.success(&format!(
"{} story graph(s) reloaded",
effects.story_updates.len()
));
}
}
}
}
if state.signals.take_world() {
let path = state.world_jsonl_path.clone();
if let Some(path) = path {
let pm_result = reload_procedural_meshes(&path, &mut state.procedural_meshes, backend);
if pm_result.regenerated > 0 || pm_result.failed > 0 {
tracing::info!(
"ProceduralMesh hot-reload: regenerated={} unchanged={} failed={}",
pm_result.regenerated,
pm_result.unchanged,
pm_result.failed,
);
if let Some(n) = notify {
if pm_result.failed > 0 {
n.error_with(
&format!("Mesh reload: {} failed", pm_result.failed),
Action::OpenConsole,
);
} else {
n.success(&format!("Meshes regenerated ({})", pm_result.regenerated));
}
}
}
let fog_result = reload_volumetric_fog(&path, &mut fog.0, backend);
if fog_result.updated {
tracing::info!(
"VolumetricFog hot-reload: applied ({})",
match fog.0 {
Some(s) => format!(
"density={:.3} falloff={:.2} dist={:.0} g={:.2}",
s.density, s.height_falloff, s.max_distance, s.phase_g,
),
None => "disabled".to_string(),
}
);
}
}
}
effects
}