use crate::gfx::graphics_system::hot_reload_sources::*;
#[derive(Default, Debug)]
pub(crate) struct FogReloadResult {
pub updated: bool,
}
pub(super) fn reload_volumetric_fog(
path: &str,
last_pushed: &mut Option<crate::gfx::volumetric_fog::FogSettings>,
backend: &mut dyn crate::gfx::backend::RenderBackend,
) -> FogReloadResult {
let mut result = FogReloadResult::default();
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
tracing::error!(
"VolumetricFog hot-reload: failed to read '{}': {} (no update)",
path,
e,
);
return result;
}
};
let entries = match concinnity_cook::build_only::expand_world_from_str(
&content,
crate::authoring::assets_root::assets_dir().as_deref(),
) {
Ok(v) => v,
Err(e) => {
tracing::error!(
"VolumetricFog hot-reload: failed to parse '{}': {} (no update)",
path,
e,
);
return result;
}
};
let mut resolved: Option<crate::gfx::volumetric_fog::FogSettings> = None;
for entry in &entries {
let asset_type = entry.get("type").and_then(|v| v.as_str()).unwrap_or("");
if asset_type != "VolumetricFog" {
continue;
}
let args = entry
.get("args")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parsed: crate::components::VolumetricFog = match serde_json::from_value(args) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
"VolumetricFog hot-reload: failed to parse args: {} (kept previous state)",
e,
);
return result;
}
};
let clamped = concinnity_cook::authoring::validate::volumetric_fog(parsed);
if clamped.enabled {
resolved = Some(crate::gfx::volumetric_fog::FogSettings::resolve(
clamped.color,
clamped.density,
clamped.height_falloff,
clamped.height_reference,
clamped.max_distance,
clamped.phase_g,
clamped.ambient,
));
}
break;
}
if resolved != *last_pushed {
backend.update_fog_settings(resolved);
*last_pushed = resolved;
result.updated = true;
}
result
}
#[derive(Default, Debug)]
pub(crate) struct ProceduralMeshReloadResult {
pub regenerated: usize,
pub unchanged: usize,
pub failed: usize,
}
pub(super) fn reload_procedural_meshes(
path: &str,
procedural_meshes: &mut ProceduralMeshSourceMap,
backend: &mut dyn crate::gfx::backend::RenderBackend,
) -> ProceduralMeshReloadResult {
let mut result = ProceduralMeshReloadResult::default();
if procedural_meshes.is_empty() {
return result;
}
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
tracing::error!(
"ProceduralMesh hot-reload: failed to read '{}': {} (no regen)",
path,
e
);
return result;
}
};
let entries = match concinnity_cook::build_only::expand_world_from_str(
&content,
crate::authoring::assets_root::assets_dir().as_deref(),
) {
Ok(v) => v,
Err(e) => {
tracing::error!(
"ProceduralMesh hot-reload: failed to parse '{}': {} (no regen)",
path,
e
);
return result;
}
};
let mut new_args_by_name: std::collections::HashMap<
String,
(crate::components::ProceduralMesh, serde_json::Value),
> = std::collections::HashMap::new();
for entry in &entries {
let asset_type = entry.get("type").and_then(|v| v.as_str()).unwrap_or("");
if asset_type != "ProceduralMesh" {
continue;
}
let Some(name) = entry.get("name").and_then(|v| v.as_str()) else {
continue;
};
let raw_args = entry
.get("args")
.cloned()
.unwrap_or(serde_json::Value::Null);
let parsed: crate::components::ProceduralMesh =
match serde_json::from_value(raw_args.clone()) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
"ProceduralMesh hot-reload: failed to parse '{}' args: {} \
(kept old geometry)",
name,
e
);
continue;
}
};
new_args_by_name.insert(name.to_string(), (parsed, raw_args));
}
let mut rebuild_changes: Vec<crate::gfx::backend::DrawGeometryUpdate> = Vec::new();
let mut rebuild_entry_indices: Vec<usize> = Vec::new();
let mut staged_args: Vec<(usize, crate::components::ProceduralMesh)> = Vec::new();
for (entry_idx, entry) in procedural_meshes.entries.iter().enumerate() {
let Some((new_args, raw_args)) = new_args_by_name.get(&entry.name) else {
result.unchanged += 1;
continue;
};
if new_args == &entry.args {
result.unchanged += 1;
continue;
}
let payload = match concinnity_cook::compile::mesh_compile::compile_mesh_payload(
raw_args,
crate::authoring::assets_root::assets_dir().as_deref(),
) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
"ProceduralMesh hot-reload: regen failed for '{}': {} (kept old geometry)",
entry.name,
e
);
result.failed += 1;
continue;
}
};
let (vertices, indices, lod_alternates) =
match crate::gfx::mesh_payload::deserialise_with_lods(&payload) {
Ok(t) => t,
Err(e) => {
tracing::warn!(
"ProceduralMesh hot-reload: deserialise failed for '{}': {} \
(kept old geometry)",
entry.name,
e
);
result.failed += 1;
continue;
}
};
let size_changed = entry.draw_indices.iter().any(|&draw_idx| {
let base_changed = match backend.draw_geometry_size(draw_idx) {
Some((v, i)) => v != vertices.len() || i != indices.len(),
None => false,
};
let lod_changed = match backend.draw_lod_index_counts(draw_idx) {
Some(counts) => {
counts.len() != lod_alternates.len()
|| counts
.iter()
.zip(lod_alternates.iter())
.any(|(c, (_, idx))| *c != idx.len())
}
None => false,
};
base_changed || lod_changed
});
if size_changed {
for &draw_idx in &entry.draw_indices {
rebuild_changes.push(crate::gfx::backend::DrawGeometryUpdate {
draw_idx,
vertices: vertices.clone(),
indices: indices.clone(),
lod_alternates: lod_alternates.clone(),
});
}
rebuild_entry_indices.push(entry_idx);
staged_args.push((entry_idx, new_args.clone()));
continue;
}
let mut slot_failures = 0usize;
for &draw_idx in &entry.draw_indices {
if let Err(e) =
backend.update_mesh_geometry(draw_idx, &vertices, &indices, &lod_alternates)
{
tracing::warn!(
"ProceduralMesh hot-reload: backend rejected update for '{}' draw {}: \
{}",
entry.name,
draw_idx,
e
);
slot_failures += 1;
}
}
if slot_failures == 0 {
result.regenerated += 1;
staged_args.push((entry_idx, new_args.clone()));
} else {
result.failed += 1;
}
}
if !rebuild_changes.is_empty() {
match backend.rebuild_static_geometry(rebuild_changes) {
Ok(()) => {
result.regenerated += rebuild_entry_indices.len();
}
Err(e) => {
tracing::error!(
"ProceduralMesh hot-reload: rebuild_static_geometry failed: {} \
({} entry/entries kept old geometry)",
e,
rebuild_entry_indices.len()
);
result.failed += rebuild_entry_indices.len();
let failed_idxs: std::collections::HashSet<usize> =
rebuild_entry_indices.iter().copied().collect();
staged_args.retain(|(idx, _)| !failed_idxs.contains(idx));
}
}
}
for (entry_idx, new_args) in staged_args {
if let Some(e) = procedural_meshes.entries.get_mut(entry_idx) {
e.args = new_args;
}
}
result
}
pub(super) fn reload_stories(
path: &str,
snapshots: &mut std::collections::HashMap<String, serde_json::Value>,
) -> Vec<crate::components::Story> {
crate::ecs::asset_id::ensure_name_resolver();
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
tracing::error!(
"story hot-reload: failed to read '{}': {} (no update)",
path,
e
);
return Vec::new();
}
};
let entries = match concinnity_cook::build_only::expand_world_from_str(
&content,
crate::authoring::assets_root::assets_dir().as_deref(),
) {
Ok(v) => v,
Err(e) => {
tracing::error!(
"story hot-reload: failed to expand '{}': {} (kept the running story)",
path,
e,
);
return Vec::new();
}
};
let mut out = Vec::new();
for entry in &entries {
if entry.get("type").and_then(|t| t.as_str()) != Some("Story") {
continue;
}
let Some(name) = entry.get("name").and_then(|n| n.as_str()) else {
continue;
};
let args = entry
.get("args")
.cloned()
.unwrap_or(serde_json::Value::Null);
if snapshots.get(name) == Some(&args) {
continue;
}
match serde_json::from_value::<crate::components::Story>(args.clone()) {
Ok(story) => {
snapshots.insert(name.to_string(), args);
out.push(story);
}
Err(e) => {
tracing::error!(
"story hot-reload: failed to parse compiled story '{}': {} \
(kept the running story)",
name,
e,
);
}
}
}
out
}
#[derive(Default, Debug)]
pub(crate) struct ShaderStageReloadResult {
pub recompiled: usize,
pub failed: usize,
pub pipelines_rebuilt: bool,
}
pub(super) fn reload_shader_stages(
shader_stages: &ShaderStageSourceMap,
backend: &mut dyn crate::gfx::backend::RenderBackend,
) -> ShaderStageReloadResult {
use crate::components::ShaderKind;
let mut result = ShaderStageReloadResult::default();
if shader_stages.is_empty() {
return result;
}
let mut compiled: std::collections::HashMap<ShaderKind, Vec<u8>> =
std::collections::HashMap::new();
for entry in &shader_stages.entries {
let compile_args = concinnity_cook::compile::shader::ShaderCompileArgs {
source_path: entry.resolved_path.clone(),
asset_name: format!(
"{}_{}",
std::path::Path::new(&entry.resolved_path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("shader_stage"),
match entry.kind {
ShaderKind::Vertex => "vert",
ShaderKind::Fragment => "frag",
ShaderKind::VertexInstanced => "vert_inst",
}
),
kind: entry.kind.compile_kind().to_string(),
required_entry: None,
};
match concinnity_cook::compile::shader::compile_shader(compile_args) {
Ok(bytes) => {
compiled.insert(entry.kind, bytes);
result.recompiled += 1;
}
Err(e) => {
tracing::error!(
"Shader hot-reload: failed to compile '{}' ({:?}): {} \
(live pipelines kept their previous source)",
entry.resolved_path,
entry.kind,
e
);
result.failed += 1;
}
}
}
if result.failed > 0 {
return result;
}
let vert = compiled.get(&ShaderKind::Vertex).map(|v| v.as_slice());
let frag = compiled.get(&ShaderKind::Fragment).map(|v| v.as_slice());
let vert_inst = compiled
.get(&ShaderKind::VertexInstanced)
.map(|v| v.as_slice());
match backend.update_world_shader_pipelines(vert, frag, None, vert_inst) {
Ok(()) => {
result.pipelines_rebuilt = true;
}
Err(e) => {
tracing::error!(
"Shader hot-reload: backend pipeline rebuild rejected: {} \
(live pipelines kept their previous source)",
e
);
}
}
result
}