use bevy_app::prelude::*;
use bevy_asset::{AssetServer, Assets};
use bevy_ecs::prelude::*;
use bevy_shader::Shader;
const SCENE_BINDINGS_PATH: &str = "embedded://bevy_solari/scene/raytracing_scene_bindings.wgsl";
const OVERRIDE_WGSL: &str = include_str!("raytracing_scene_bindings_override.wgsl");
const T_MAX_LINE: &str = "const RAY_T_MAX = 1000000000.0f;";
const SCALE_LINE: &str = "const RAY_T_MIN_SCALE = 0.000001f;";
#[derive(Resource, Clone, Copy, Debug, PartialEq)]
pub struct SolariRayBias {
pub scale: f32,
pub t_max: f32,
}
impl Default for SolariRayBias {
fn default() -> Self {
Self {
scale: 1.0e-6,
t_max: 1.0e9,
}
}
}
pub struct SolariRayBiasPlugin;
impl Plugin for SolariRayBiasPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<SolariRayBias>();
app.add_systems(Update, apply_ray_bias_override);
}
}
#[derive(Default)]
struct BiasState {
applied: bool,
frames: u32,
warned: bool,
}
fn apply_ray_bias_override(
mut state: Local<BiasState>,
asset_server: Res<AssetServer>,
bias: Res<SolariRayBias>,
mut shaders: ResMut<Assets<Shader>>,
) {
if state.applied && !bias.is_changed() {
return;
}
let Some(handle) = asset_server.get_handle::<Shader>(SCENE_BINDINGS_PATH) else {
stall(&mut state, "scene_bindings not registered");
return;
};
let id = handle.id();
if shaders.get(id).is_none() {
stall(&mut state, "stock shader not loaded yet");
return;
}
let source = build_override_source(&bias);
if shaders
.insert(
id,
Shader::from_wgsl(source, SCENE_BINDINGS_PATH.to_string()),
)
.is_err()
{
stall(&mut state, "Assets::insert rejected the shader id");
return;
}
state.applied = true;
state.frames = 0;
state.warned = false;
tracing::info!(
target: "big_space_solari",
"SolariRayBiasPlugin applied scale-aware ray bias (scale={}, t_max={}) to `{SCENE_BINDINGS_PATH}`.",
bias.scale,
bias.t_max,
);
}
fn build_override_source(bias: &SolariRayBias) -> String {
let scale = sanitize(bias.scale, SolariRayBias::default().scale);
let t_max = sanitize(bias.t_max, SolariRayBias::default().t_max);
OVERRIDE_WGSL
.replacen(T_MAX_LINE, &format!("const RAY_T_MAX = {t_max}f;"), 1)
.replacen(SCALE_LINE, &format!("const RAY_T_MIN_SCALE = {scale}f;"), 1)
}
fn sanitize(v: f32, fallback: f32) -> f32 {
if v.is_finite() && v > 0.0 {
v
} else {
fallback
}
}
fn stall(state: &mut BiasState, reason: &str) {
state.frames += 1;
if !state.warned && state.frames > 600 {
state.warned = true;
tracing::warn!(
target: "big_space_solari",
"SolariRayBiasPlugin could not override `{SCENE_BINDINGS_PATH}` after \
{} frames ({reason}); scale-aware ray bias is inactive.",
state.frames,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn override_keeps_module_path_and_rewrites_both_constants() {
let src = build_override_source(&SolariRayBias {
scale: 2.0e-6,
t_max: 5.0e8,
});
assert!(src.starts_with("#define_import_path bevy_solari::scene_bindings"));
assert!(src.contains("const RAY_T_MAX = 500000000f;"));
assert!(src.contains("const RAY_T_MIN_SCALE = 0.000002f;"));
assert!(!src.contains(T_MAX_LINE));
assert!(!src.contains(SCALE_LINE));
assert!(src.contains("fn trace_ray("));
assert!(src.contains("length(ray_origin) * RAY_T_MIN_SCALE"));
}
#[test]
fn sanitize_rejects_nonpositive_and_nonfinite() {
assert_eq!(sanitize(1.0e-6, 9.0), 1.0e-6);
assert_eq!(sanitize(0.0, 9.0), 9.0);
assert_eq!(sanitize(-1.0, 9.0), 9.0);
assert_eq!(sanitize(f32::NAN, 9.0), 9.0);
assert_eq!(sanitize(f32::INFINITY, 9.0), 9.0);
}
#[test]
fn override_template_lines_match_the_replaced_constants_exactly_once() {
assert_eq!(OVERRIDE_WGSL.matches(T_MAX_LINE).count(), 1);
assert_eq!(OVERRIDE_WGSL.matches(SCALE_LINE).count(), 1);
}
}