use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use crate::gfx::graphics_system::HotReloadApplyParts;
use crate::gfx::graphics_system::hot_reload_sources::*;
use super::decode::{poll_pending_assets, poll_pending_envmap, reload_assets};
use super::passes::{
reload_procedural_meshes, reload_shader_stages, reload_stories, reload_volumetric_fog,
};
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<crate::gfx::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<crate::gfx::mesh_payload::SkinnedVertex>,
pub indices: Vec<u16>,
pub skeleton: Vec<crate::components::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 shader_stages: ShaderStageSourceMap,
pub world_jsonl_path: Option<String>,
pub pending: Arc<AtomicBool>,
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: usize,
pub new_skeleton: crate::gfx::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("shader_stages", &self.shader_stages.entries.len())
.field("world_jsonl_path", &self.world_jsonl_path)
.field("pending", &self.pending.load(Ordering::Relaxed))
.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) -> Self {
let pending = Arc::new(AtomicBool::new(false));
let nothing_to_watch = sources.map.is_empty()
&& sources.color_lut.is_none()
&& sources.environment_map.is_none()
&& sources.meshes.is_empty()
&& sources.skinned_meshes.is_empty()
&& sources.procedural_meshes.is_empty()
&& sources.shader_stages.is_empty()
&& sources.world_jsonl_path.is_none();
let watcher = if nothing_to_watch {
None
} else {
spawn_watcher(&sources, Arc::clone(&pending))
};
let HotReloadSources {
map,
color_lut,
environment_map,
meshes,
skinned_meshes,
procedural_meshes,
shader_stages,
world_jsonl_path,
} = sources;
Self {
map,
color_lut,
environment_map,
meshes,
skinned_meshes,
procedural_meshes,
shader_stages,
world_jsonl_path,
pending,
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) fn reload_requested(&self) -> bool {
self.pending.load(Ordering::SeqCst)
}
pub(crate) fn clear_flag(&self) {
self.pending.store(false, Ordering::SeqCst);
}
}
pub(crate) struct FrameHotReloadEffects {
pub skeleton_updates: Vec<PendingSkeletonUpdate>,
pub story_updates: Vec<crate::components::Story>,
}
pub(crate) fn run_frame(
state: &mut AssetHotReloadState,
apply: &mut HotReloadApplyParts,
notify: Option<&crate::editor::notify::Notifier>,
) -> FrameHotReloadEffects {
use crate::editor::notify::Action;
let mut effects = FrameHotReloadEffects {
skeleton_updates: Vec::new(),
story_updates: Vec::new(),
};
poll_pending_envmap(state, apply.backend);
poll_pending_assets(state, apply.backend);
effects.skeleton_updates = state.drain_pending_skeleton_updates();
if state.reload_requested() {
state.clear_flag();
reload_assets(state);
}
if super::pending::take_pending_shader_stages() {
let ss_result = reload_shader_stages(&state.shader_stages, apply.backend);
if ss_result.recompiled > 0 || ss_result.failed > 0 {
tracing::info!(
"Shader hot-reload: recompiled={} failed={} pipelines_rebuilt={}",
ss_result.recompiled,
ss_result.failed,
ss_result.pipelines_rebuilt,
);
if let Some(n) = notify {
if ss_result.failed > 0 {
n.error_with(
&format!("Shader reload: {} failed", ss_result.failed),
Action::OpenConsole,
);
} else {
n.success(&format!("Shaders reloaded ({})", ss_result.recompiled));
}
}
}
}
if super::pending::take_pending_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 super::pending::take_pending_world() {
let path = state.world_jsonl_path.clone();
if let Some(path) = path {
let pm_result =
reload_procedural_meshes(&path, &mut state.procedural_meshes, apply.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, apply.last_fog_settings, apply.backend);
if fog_result.updated {
tracing::info!(
"VolumetricFog hot-reload: applied ({})",
match apply.last_fog_settings {
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
}