celestialsim/
custom_surface.rs1pub const TEMPLATE: &str = include_str!("../shaders/custom_surface.glsl");
23
24const USER_CODE_MARKER: &str = "// __CELS_USER_CODE__";
26
27const USER_DEFINES_MARKER: &str = "// __CELS_USER_DEFINES__";
30
31pub const MAX_USER_PARAMS: usize = 16;
35
36#[derive(Debug, PartialEq, Eq)]
38pub enum AssembleError {
39 MissingFn(&'static str),
41 TemplateMarkerMissing,
43}
44
45impl std::fmt::Display for AssembleError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 match self {
48 AssembleError::MissingFn(name) => write!(
49 f,
50 "custom-surface GLSL must define `{name}` (see docs/custom_terrain_gpu.md)"
51 ),
52 AssembleError::TemplateMarkerMissing => {
53 write!(f, "custom-surface template is missing its user-code marker")
54 }
55 }
56 }
57}
58
59pub fn assemble_source(user_glsl: &str) -> Result<String, AssembleError> {
65 assemble_source_with_params(user_glsl, &[])
66}
67
68pub fn assemble_source_with_params(
74 user_glsl: &str,
75 param_names: &[String],
76) -> Result<String, AssembleError> {
77 for required in ["terrain_height", "terrain_color"] {
78 if !user_glsl.contains(required) {
79 return Err(AssembleError::MissingFn(match required {
80 "terrain_height" => "terrain_height",
81 _ => "terrain_color",
82 }));
83 }
84 }
85 if TEMPLATE.matches(USER_CODE_MARKER).count() != 1
89 || TEMPLATE.matches(USER_DEFINES_MARKER).count() != 1
90 {
91 return Err(AssembleError::TemplateMarkerMissing);
92 }
93 let mut defines = String::new();
94 for (i, name) in param_names.iter().take(MAX_USER_PARAMS).enumerate() {
95 defines.push_str(&format!("#define {} (P.cels_user[{}])\n", name.to_uppercase(), i));
96 }
97 let with_defines = TEMPLATE.replace(USER_DEFINES_MARKER, defines.trim_end());
99 Ok(with_defines.replace(USER_CODE_MARKER, user_glsl))
100}
101
102pub fn pack_params(
108 chunk_count: u32,
109 tile_res: u32,
110 water_height: f32,
111 height_scale: f32,
112 user: &[f32],
113) -> Vec<u8> {
114 let mut out = vec![0u8; 16 + 4 * MAX_USER_PARAMS];
115 out[0..4].copy_from_slice(&chunk_count.to_le_bytes());
116 out[4..8].copy_from_slice(&tile_res.to_le_bytes());
117 out[8..12].copy_from_slice(&water_height.to_le_bytes());
118 out[12..16].copy_from_slice(&height_scale.to_le_bytes());
119 for (i, v) in user.iter().take(MAX_USER_PARAMS).enumerate() {
120 let off = 16 + i * 4;
121 out[off..off + 4].copy_from_slice(&v.to_le_bytes());
122 }
123 out
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 const OK_USER: &str = r#"
131 float terrain_height(vec3 dir) { return 0.05 * sin(dir.x * 8.0); }
132 vec3 terrain_color(vec3 dir, float h) { return vec3(h, 0.5, 0.2); }
133 "#;
134
135 #[test]
136 fn assembles_user_code_into_template() {
137 let src = assemble_source(OK_USER).expect("valid user code assembles");
138 assert!(src.contains("0.05 * sin(dir.x * 8.0)"));
140 assert!(!src.contains(USER_CODE_MARKER));
141 assert!(src.contains("void main()"));
143 assert!(src.contains("surface_height["));
144 }
145
146 #[test]
147 fn missing_height_fn_is_rejected_before_gpu() {
148 let bad = "vec3 terrain_color(vec3 dir, float h) { return vec3(0.0); }";
149 assert_eq!(assemble_source(bad), Err(AssembleError::MissingFn("terrain_height")));
150 }
151
152 #[test]
153 fn missing_color_fn_is_rejected_before_gpu() {
154 let bad = "float terrain_height(vec3 dir) { return 0.0; }";
155 assert_eq!(assemble_source(bad), Err(AssembleError::MissingFn("terrain_color")));
156 }
157
158 #[test]
159 fn template_has_exactly_one_marker() {
160 assert_eq!(TEMPLATE.matches(USER_CODE_MARKER).count(), 1);
165 }
166
167 #[test]
168 fn multiline_user_code_assembles_without_corrupting_template() {
169 let multiline = "// a comment\nfloat terrain_height(vec3 dir) {\n return 0.1;\n}\nvec3 terrain_color(vec3 dir, float h) {\n return vec3(h);\n}\n";
172 let src = assemble_source(multiline).expect("multiline assembles");
173 assert!(src.contains("void main()"));
176 assert!(src.contains("cels_pack_rgba8"));
177 }
178
179 #[test]
180 fn pack_params_layout_fixed_head_plus_user_tail() {
181 let b = pack_params(3, 256, 0.45, 0.18, &[1.5, -2.0]);
182 assert_eq!(b.len(), 16 + 4 * MAX_USER_PARAMS);
183 assert_eq!(u32::from_le_bytes([b[0], b[1], b[2], b[3]]), 3);
184 assert_eq!(u32::from_le_bytes([b[4], b[5], b[6], b[7]]), 256);
185 assert_eq!(f32::from_le_bytes([b[8], b[9], b[10], b[11]]), 0.45);
186 assert_eq!(f32::from_le_bytes([b[12], b[13], b[14], b[15]]), 0.18);
187 assert_eq!(f32::from_le_bytes([b[16], b[17], b[18], b[19]]), 1.5);
189 assert_eq!(f32::from_le_bytes([b[20], b[21], b[22], b[23]]), -2.0);
190 assert_eq!(f32::from_le_bytes([b[24], b[25], b[26], b[27]]), 0.0);
191 }
192
193 #[test]
194 fn pack_params_ignores_user_overflow() {
195 let many: Vec<f32> = (0..MAX_USER_PARAMS + 4).map(|i| i as f32).collect();
196 let b = pack_params(0, 0, 0.0, 0.0, &many);
197 assert_eq!(b.len(), 16 + 4 * MAX_USER_PARAMS);
198 let off = 16 + (MAX_USER_PARAMS - 1) * 4;
200 assert_eq!(
201 f32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]]),
202 (MAX_USER_PARAMS - 1) as f32
203 );
204 }
205
206 #[test]
207 fn param_name_produces_define_at_expected_index() {
208 let src = assemble_source_with_params(OK_USER, &["ridge_sharpness".into(), "snow_line".into()])
209 .expect("assembles with params");
210 assert!(src.contains("#define RIDGE_SHARPNESS (P.cels_user[0])"));
211 assert!(src.contains("#define SNOW_LINE (P.cels_user[1])"));
212 assert!(!src.contains(USER_DEFINES_MARKER));
214 assert!(!src.contains(USER_CODE_MARKER));
215 assert!(src.contains("void main()"));
216 }
217
218 #[test]
219 fn template_has_exactly_one_defines_marker() {
220 assert_eq!(TEMPLATE.matches(USER_DEFINES_MARKER).count(), 1);
221 }
222}