1use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::Mutex;
23
24use godot::builtin::{Color, Vector3};
25
26use celestial_algo::quadtree::ChunkId;
27
28pub const MAX_HANDLE_DEPTH: u8 = 20;
31
32const PATH_BITS: u32 = 40;
34const DEPTH_BITS: u32 = 5;
35const FACE_BITS: u32 = 5;
36pub const TAG_BITS: u32 = 12;
39
40const DEPTH_SHIFT: u32 = PATH_BITS;
41const FACE_SHIFT: u32 = DEPTH_SHIFT + DEPTH_BITS;
42const TAG_SHIFT: u32 = FACE_SHIFT + FACE_BITS;
43
44const PATH_MASK: u64 = (1 << PATH_BITS) - 1;
45const DEPTH_MASK: u64 = (1 << DEPTH_BITS) - 1;
46const FACE_MASK: u64 = (1 << FACE_BITS) - 1;
47pub const TAG_MASK: u64 = (1 << TAG_BITS) - 1;
49
50pub fn handle_encode(tag: u16, id: ChunkId) -> Option<i64> {
57 if id.depth > MAX_HANDLE_DEPTH || id.face as u64 > FACE_MASK {
58 return None;
59 }
60 if id.path & !PATH_MASK != 0 {
61 return None;
62 }
63 let bits = (id.path & PATH_MASK)
64 | ((id.depth as u64 & DEPTH_MASK) << DEPTH_SHIFT)
65 | ((id.face as u64 & FACE_MASK) << FACE_SHIFT)
66 | ((tag as u64 & TAG_MASK) << TAG_SHIFT);
67 Some(bits as i64)
68}
69
70pub fn handle_decode(tag: u16, handle: i64) -> Option<ChunkId> {
73 if handle < 0 {
74 return None;
75 }
76 let bits = handle as u64;
77 if (bits >> TAG_SHIFT) & TAG_MASK != (tag as u64 & TAG_MASK) {
78 return None;
79 }
80 let depth = ((bits >> DEPTH_SHIFT) & DEPTH_MASK) as u8;
81 if depth > MAX_HANDLE_DEPTH {
82 return None;
83 }
84 Some(ChunkId {
85 face: ((bits >> FACE_SHIFT) & FACE_MASK) as u8,
86 depth,
87 path: bits & PATH_MASK,
88 })
89}
90
91pub fn chunk_dirs(corners: [Vector3; 3], tile_res: u32) -> Vec<Vector3> {
98 let n = tile_res as usize;
99 let res_f = tile_res as f32;
100 let (d0, d1, d2) =
101 (corners[0].normalized(), corners[1].normalized(), corners[2].normalized());
102 let mut dirs = Vec::with_capacity(n * n);
103 for ty in 0..n {
104 for tx in 0..n {
105 let mut u = (tx as f32 + 0.5) / res_f;
106 let mut v = (ty as f32 + 0.5) / res_f;
107 if u + v > 1.0 {
108 let s = u + v;
109 u /= s;
110 v /= s;
111 }
112 dirs.push((d0 * (1.0 - u - v) + d1 * u + d2 * v).normalized());
113 }
114 }
115 dirs
116}
117
118fn pack_component(c: f32) -> u8 {
120 ((c * 0.5 + 0.5).clamp(0.0, 1.0) * 255.0).round() as u8
121}
122
123pub fn pack_normals(
132 corners: [Vector3; 3],
133 dirs: &[Vector3],
134 height: &[f32],
135 tile_res: u32,
136 over: Option<&[Vector3]>,
137) -> Vec<u8> {
138 let n = tile_res as usize;
139 let res_f = tile_res as f32;
140 let ni = n as i32;
141 let k = corners[0].length().max(1.0); let ex = (corners[1] - corners[0]) / res_f;
143 let ey = (corners[2] - corners[0]) / res_f;
144
145 let mut normal = vec![0u8; n * n * 4];
146 for ty in 0..n {
147 for tx in 0..n {
148 let idx = ty * n + tx;
149 let dir = dirs[idx];
150 let nrm = if let Some(o) = over.and_then(|s| s.get(idx)) {
151 o.normalized()
152 } else {
153 let sample = |x: i32, y: i32| -> f32 {
154 let x = x.clamp(0, ni - 1) as usize;
155 let y = y.clamp(0, ni - 1) as usize;
156 height[y * n + x]
157 };
158 let (xi, yi) = (tx as i32, ty as i32);
159 let exl = ex - dir * dir.dot(ex);
160 let eyl = ey - dir * dir.dot(ey);
161 let dpx = exl * 2.0 + dir * (k * (sample(xi + 1, yi) - sample(xi - 1, yi)));
162 let dpy = eyl * 2.0 + dir * (k * (sample(xi, yi + 1) - sample(xi, yi - 1)));
163 let mut nrm = dpx.cross(dpy).normalized();
164 if nrm.dot(dir) < 0.0 {
165 nrm = -nrm;
166 }
167 nrm
168 };
169 normal[idx * 4] = pack_component(nrm.x);
170 normal[idx * 4 + 1] = pack_component(nrm.y);
171 normal[idx * 4 + 2] = pack_component(nrm.z);
172 normal[idx * 4 + 3] = 255;
173 }
174 }
175 normal
176}
177
178pub fn fd_normals(
180 corners: [Vector3; 3],
181 dirs: &[Vector3],
182 height: &[f32],
183 tile_res: u32,
184) -> Vec<u8> {
185 pack_normals(corners, dirs, height, tile_res, None)
186}
187
188#[derive(Clone, Copy, PartialEq, Eq, Debug)]
190pub enum SubmitError {
191 HeightLen { got: usize, want: usize },
193 ColorLen { got: usize, want: usize },
195 NormalLen { got: usize, want: usize },
197}
198
199impl std::fmt::Display for SubmitError {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 match self {
202 SubmitError::HeightLen { got, want } => {
203 write!(f, "heights has {got} entries, expected tile_res² = {want}")
204 }
205 SubmitError::ColorLen { got, want } => {
206 write!(f, "colors has {got} entries, expected tile_res² = {want}")
207 }
208 SubmitError::NormalLen { got, want } => {
209 write!(f, "normals has {got} entries, expected 0 or tile_res² = {want}")
210 }
211 }
212 }
213}
214
215#[derive(Clone, Debug)]
219pub struct RawSubmission {
220 pub handle: i64,
221 pub heights: Vec<f32>,
222 pub colors: Vec<Color>,
223 pub normals: Vec<Vector3>,
224}
225
226#[derive(Clone, Debug, PartialEq)]
229pub struct Submission {
230 pub handle: i64,
231 pub color: Vec<u8>,
232 pub height: Vec<f32>,
233 pub normal: Option<Vec<u8>>,
234}
235
236pub fn validate_submission(
245 raw: &RawSubmission,
246 tile_res: u32,
247) -> Result<Submission, SubmitError> {
248 let (heights, colors, normals) = (&raw.heights, &raw.colors, &raw.normals);
249 let want = (tile_res as usize) * (tile_res as usize);
250 if heights.len() != want {
251 return Err(SubmitError::HeightLen { got: heights.len(), want });
252 }
253 if colors.len() != want {
254 return Err(SubmitError::ColorLen { got: colors.len(), want });
255 }
256 if !normals.is_empty() && normals.len() != want {
257 return Err(SubmitError::NormalLen { got: normals.len(), want });
258 }
259
260 let height: Vec<f32> =
261 heights.iter().map(|h| if h.is_finite() { *h } else { 0.0 }).collect();
262
263 let mut color = vec![255u8; want * 4];
264 for (i, c) in colors.iter().enumerate() {
265 color[i * 4] = (c.r.clamp(0.0, 1.0) * 255.0) as u8;
266 color[i * 4 + 1] = (c.g.clamp(0.0, 1.0) * 255.0) as u8;
267 color[i * 4 + 2] = (c.b.clamp(0.0, 1.0) * 255.0) as u8;
268 color[i * 4 + 3] = 255;
269 }
270
271 let normal = if normals.is_empty() {
272 None
273 } else {
274 let mut packed = vec![0u8; want * 4];
275 for (i, v) in normals.iter().enumerate() {
276 let nrm = v.normalized();
277 packed[i * 4] = pack_component(nrm.x);
278 packed[i * 4 + 1] = pack_component(nrm.y);
279 packed[i * 4 + 2] = pack_component(nrm.z);
280 packed[i * 4 + 3] = 255;
281 }
282 Some(packed)
283 };
284
285 Ok(Submission { handle: raw.handle, color, height, normal })
286}
287
288#[derive(Default)]
295pub struct SubmitQueue {
296 items: Mutex<Vec<RawSubmission>>,
297 reported_error: AtomicBool,
300}
301
302impl SubmitQueue {
303 pub fn push(&self, s: RawSubmission) {
305 if let Ok(mut q) = self.items.lock() {
306 q.push(s);
307 }
308 }
309
310 pub fn drain(&self) -> Vec<RawSubmission> {
312 match self.items.lock() {
313 Ok(mut q) => std::mem::take(&mut *q),
314 Err(_) => Vec::new(),
315 }
316 }
317
318 pub fn clear(&self) {
320 if let Ok(mut q) = self.items.lock() {
321 q.clear();
322 }
323 }
324
325 pub fn should_report_error(&self) -> bool {
328 !self.reported_error.swap(true, Ordering::Relaxed)
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 fn id(face: u8, depth: u8, path: u64) -> ChunkId {
337 ChunkId { face, depth, path }
338 }
339
340 #[test]
341 fn handle_roundtrips_over_every_face_and_depth() {
342 for face in 0..20u8 {
343 for depth in 0..=MAX_HANDLE_DEPTH {
344 let path = 0xAAAA_AAAA_AAu64 & ((1u64 << (2 * depth as u32)) - 1).max(0);
346 let want = id(face, depth, path);
347 let h = handle_encode(0x5A5, want).expect("encodable");
348 assert!(h >= 0, "handle must be a non-negative GDScript int");
349 assert_eq!(handle_decode(0x5A5, h), Some(want));
350 }
351 }
352 }
353
354 #[test]
355 fn handle_rejects_a_foreign_planet_tag() {
356 let h = handle_encode(7, id(3, 9, 0b1101)).unwrap();
357 assert_eq!(handle_decode(7, h), Some(id(3, 9, 0b1101)));
358 assert_eq!(handle_decode(8, h), None, "another planet's tag must not decode");
359 }
360
361 #[test]
362 fn handle_rejects_too_deep_and_negative() {
363 assert_eq!(handle_encode(0, id(0, MAX_HANDLE_DEPTH + 1, 0)), None);
364 assert_eq!(handle_decode(0, -1), None);
365 }
366
367 #[test]
368 fn handle_tag_is_masked_not_truncated_into_the_sign_bit() {
369 let h = handle_encode(TAG_MASK as u16, id(31, MAX_HANDLE_DEPTH, PATH_MASK)).unwrap();
371 assert!(h > 0);
372 assert_eq!(handle_decode(TAG_MASK as u16, h).unwrap().face, 31);
373 }
374
375 fn raw(handle: i64, heights: &[f32], colors: &[Color], normals: &[Vector3]) -> RawSubmission {
376 RawSubmission {
377 handle,
378 heights: heights.to_vec(),
379 colors: colors.to_vec(),
380 normals: normals.to_vec(),
381 }
382 }
383
384 #[test]
385 fn validate_rejects_wrong_lengths() {
386 let c = vec![Color::from_rgba(1.0, 1.0, 1.0, 1.0); 4];
387 assert_eq!(
388 validate_submission(&raw(0, &[0.0; 3], &c, &[]), 2),
389 Err(SubmitError::HeightLen { got: 3, want: 4 })
390 );
391 assert_eq!(
392 validate_submission(&raw(0, &[0.0; 4], &c[..3], &[]), 2),
393 Err(SubmitError::ColorLen { got: 3, want: 4 })
394 );
395 assert_eq!(
396 validate_submission(&raw(0, &[0.0; 4], &c, &[Vector3::UP; 2]), 2),
397 Err(SubmitError::NormalLen { got: 2, want: 4 })
398 );
399 }
400
401 #[test]
402 fn validate_sanitizes_non_finite_heights() {
403 let c = vec![Color::from_rgba(0.0, 0.0, 0.0, 1.0); 4];
404 let h = [1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY];
405 let s = validate_submission(&raw(0, &h, &c, &[]), 2).unwrap();
406 assert_eq!(s.height, vec![1.0, 0.0, 0.0, 0.0]);
407 assert!(s.height.iter().all(|v| v.is_finite()));
408 }
409
410 #[test]
411 fn validate_empty_normals_defers_to_finite_difference() {
412 let c = vec![Color::from_rgba(1.0, 0.5, 0.25, 1.0); 4];
413 let s = validate_submission(&raw(9, &[0.0; 4], &c, &[]), 2).unwrap();
414 assert_eq!(s.handle, 9);
415 assert!(s.normal.is_none(), "no normals ⇒ FD at drain time");
416 assert_eq!(&s.color[0..4], &[255, 127, 63, 255]);
417 }
418
419 #[test]
420 fn validate_packs_supplied_normals() {
421 let c = vec![Color::from_rgba(0.0, 0.0, 0.0, 1.0); 4];
422 let s =
424 validate_submission(&raw(0, &[0.0; 4], &c, &[Vector3::new(0.0, 5.0, 0.0); 4]), 2)
425 .unwrap();
426 let n = s.normal.expect("packed");
427 assert_eq!(&n[0..4], &[128, 255, 128, 255]);
428 }
429
430 #[test]
431 fn chunk_dirs_are_unit_and_row_major() {
432 let corners = [Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0), Vector3::new(0.0, 0.0, 1.0)];
433 let dirs = chunk_dirs(corners, 8);
434 assert_eq!(dirs.len(), 64);
435 assert!(dirs.iter().all(|d| (d.length() - 1.0).abs() < 1.0e-5));
436 }
437
438 #[test]
439 fn fd_normals_of_a_flat_chunk_point_outward() {
440 let corners =
441 [Vector3::new(1.0, 0.0, 0.0), Vector3::new(0.0, 1.0, 0.0), Vector3::new(0.0, 0.0, 1.0)];
442 let dirs = chunk_dirs(corners, 4);
443 let n = fd_normals(corners, &dirs, &[0.0; 16], 4);
444 for i in 0..16 {
445 let v = Vector3::new(
446 n[i * 4] as f32 / 255.0 * 2.0 - 1.0,
447 n[i * 4 + 1] as f32 / 255.0 * 2.0 - 1.0,
448 n[i * 4 + 2] as f32 / 255.0 * 2.0 - 1.0,
449 );
450 assert!(v.dot(dirs[i]) > 0.0, "normal must face away from the planet centre");
451 }
452 }
453
454 #[test]
455 fn queue_drains_once_and_reports_one_error() {
456 let q = SubmitQueue::default();
457 q.push(raw(1, &[], &[], &[]));
458 q.push(raw(2, &[], &[], &[]));
459 let got = q.drain();
460 assert_eq!(got.len(), 2);
461 assert!(q.drain().is_empty(), "drain takes everything");
462
463 assert!(q.should_report_error(), "first bad payload reports");
464 assert!(!q.should_report_error(), "subsequent ones stay quiet");
465 }
466
467 #[test]
468 fn queue_clear_drops_stale_submissions() {
469 let q = SubmitQueue::default();
470 q.push(raw(1, &[], &[], &[]));
471 q.clear();
472 assert!(q.drain().is_empty());
473 }
474
475 #[test]
476 fn queue_push_is_safe_from_many_threads() {
477 let q = std::sync::Arc::new(SubmitQueue::default());
478 let hs: Vec<_> = (0..8)
479 .map(|t| {
480 let q = std::sync::Arc::clone(&q);
481 std::thread::spawn(move || {
482 for i in 0..32 {
483 q.push(raw(t * 32 + i, &[], &[], &[]));
484 }
485 })
486 })
487 .collect();
488 for h in hs {
489 h.join().unwrap();
490 }
491 assert_eq!(q.drain().len(), 8 * 32);
492 }
493}