1#![allow(
34 clippy::cast_possible_truncation,
35 clippy::cast_possible_wrap,
36 clippy::cast_sign_loss,
37 clippy::cast_precision_loss,
38 clippy::similar_names,
39 clippy::too_many_arguments,
40 clippy::too_many_lines,
41 clippy::cast_ptr_alignment, clippy::doc_markdown,
43 clippy::no_effect_underscore_binding, clippy::no_effect, clippy::ref_as_ptr,
46 clippy::float_cmp_const,
47 clippy::float_cmp,
48)]
49
50use roxlap_formats::kv6::{Kv6, Voxel};
51use roxlap_formats::sprite::{Sprite, SPRITE_FLAG_INVISIBLE, SPRITE_FLAG_KFA, SPRITE_FLAG_NO_Z};
52
53use crate::camera_math::CameraState;
54use crate::engine::{Engine, LightSrc, DEFAULT_KV6COL};
55use crate::equivec::iunivec;
56use crate::fixed::ftol;
57use crate::opticast::OpticastSettings;
58use crate::ptfaces16::PTFACES16;
59
60const MAX_LIGHTS: usize = 16;
64
65pub(crate) const KV6_MIPFACTOR_DEFAULT: i32 = 128;
72
73#[derive(Debug, Clone)]
82#[allow(dead_code)] pub(crate) struct Kv6DrawSetup<'a> {
84 pub kv: &'a Kv6,
87 pub ts: [f32; 3],
91 pub th: [f32; 3],
92 pub tf: [f32; 3],
93 pub mip: u32,
95}
96
97pub(crate) fn kv6_draw_prepare<'a>(
116 sprite: &'a Sprite,
117 cam: &CameraState,
118) -> Option<Kv6DrawSetup<'a>> {
119 let kv = &sprite.kv6;
120
121 let dx = sprite.p[0] - cam.pos[0];
128 let dy = sprite.p[1] - cam.pos[1];
129 let dz = sprite.p[2] - cam.pos[2];
130 let dist_estimate = ftol(dx * cam.forward[0] + dy * cam.forward[1] + dz * cam.forward[2]);
131 let _ = (dist_estimate, KV6_MIPFACTOR_DEFAULT);
132 let mip = 0u32;
133 let ts = sprite.s;
134 let th = sprite.h;
135 let tf = sprite.f;
136
137 #[allow(clippy::cast_precision_loss)]
142 let half_x = kv.xsiz as f32 * 0.5;
143 #[allow(clippy::cast_precision_loss)]
144 let half_y = kv.ysiz as f32 * 0.5;
145 #[allow(clippy::cast_precision_loss)]
146 let half_z = kv.zsiz as f32 * 0.5;
147 let off_x = half_x - kv.xpiv;
148 let off_y = half_y - kv.ypiv;
149 let off_z = half_z - kv.zpiv;
150 let npos = [
151 off_x * ts[0] + off_y * th[0] + off_z * tf[0] + dx,
152 off_x * ts[1] + off_y * th[1] + off_z * tf[1] + dy,
153 off_x * ts[2] + off_y * th[2] + off_z * tf[2] + dz,
154 ];
155 let nstr = [ts[0] * half_x, ts[1] * half_x, ts[2] * half_x];
156 let nhei = [th[0] * half_y, th[1] * half_y, th[2] * half_y];
157 let nfor = [tf[0] * half_z, tf[1] * half_z, tf[2] * half_z];
158
159 for n in &cam.nor {
161 let proj_str = (nstr[0] * n[0] + nstr[1] * n[1] + nstr[2] * n[2]).abs();
162 let proj_hei = (nhei[0] * n[0] + nhei[1] * n[1] + nhei[2] * n[2]).abs();
163 let proj_for = (nfor[0] * n[0] + nfor[1] * n[1] + nfor[2] * n[2]).abs();
164 let proj_pos = npos[0] * n[0] + npos[1] * n[1] + npos[2] * n[2];
165 if proj_str + proj_hei + proj_for + proj_pos < 0.0 {
166 return None;
167 }
168 }
169
170 Some(Kv6DrawSetup {
171 kv,
172 ts,
173 th,
174 tf,
175 mip,
176 })
177}
178
179#[allow(clippy::too_many_arguments)]
187pub(crate) fn mat2(
188 a_s: [f32; 3],
189 a_h: [f32; 3],
190 a_f: [f32; 3],
191 a_o: [f32; 3],
192 b_s: [f32; 3],
193 b_h: [f32; 3],
194 b_f: [f32; 3],
195 b_o: [f32; 3],
196) -> ([f32; 3], [f32; 3], [f32; 3], [f32; 3]) {
197 let c_s = [
198 a_s[0] * b_s[0] + a_h[0] * b_s[1] + a_f[0] * b_s[2],
199 a_s[1] * b_s[0] + a_h[1] * b_s[1] + a_f[1] * b_s[2],
200 a_s[2] * b_s[0] + a_h[2] * b_s[1] + a_f[2] * b_s[2],
201 ];
202 let c_h = [
203 a_s[0] * b_h[0] + a_h[0] * b_h[1] + a_f[0] * b_h[2],
204 a_s[1] * b_h[0] + a_h[1] * b_h[1] + a_f[1] * b_h[2],
205 a_s[2] * b_h[0] + a_h[2] * b_h[1] + a_f[2] * b_h[2],
206 ];
207 let c_f = [
208 a_s[0] * b_f[0] + a_h[0] * b_f[1] + a_f[0] * b_f[2],
209 a_s[1] * b_f[0] + a_h[1] * b_f[1] + a_f[1] * b_f[2],
210 a_s[2] * b_f[0] + a_h[2] * b_f[1] + a_f[2] * b_f[2],
211 ];
212 let c_o = [
213 a_s[0] * b_o[0] + a_h[0] * b_o[1] + a_f[0] * b_o[2] + a_o[0],
214 a_s[1] * b_o[0] + a_h[1] * b_o[1] + a_f[1] * b_o[2] + a_o[1],
215 a_s[2] * b_o[0] + a_h[2] * b_o[1] + a_f[2] * b_o[2] + a_o[2],
216 ];
217 (c_s, c_h, c_f, c_o)
218}
219
220#[inline]
223fn lbound(a: i32, b: i32, c: i32) -> i32 {
224 a.clamp(b, c)
225}
226
227#[derive(Debug, Clone)]
232#[allow(dead_code)] pub(crate) struct Kv6IterState<'a> {
234 pub kv: &'a Kv6,
235 pub inx: i32,
242 pub iny: i32,
243 pub inz: i32,
244 pub nxplanemin: i32,
249 pub nxplanemax: i32,
250}
251
252#[derive(Debug, Clone)]
261pub(crate) struct Kv6FullState<'a> {
262 pub iter: Kv6IterState<'a>,
263 pub cadd4: [[f32; 4]; 8],
269 pub ztab4_per_z: Vec<[f32; 4]>,
272 pub r1_initial: [f32; 4],
277 pub r2: [f32; 4],
280 pub scisdist: f32,
285 #[allow(dead_code)]
289 pub qsum0: [i16; 4],
290 #[allow(dead_code)]
292 pub qsum1: [i16; 4],
293 #[allow(dead_code)]
295 pub qbplbpp: [i16; 4],
296 pub kv6colmul: Box<[u64; 256]>,
302 pub kv6coladd: u64,
305}
306
307#[derive(Clone, Copy, Debug)]
324pub struct DrawTarget<'a> {
325 fb_ptr: *mut u32,
326 fb_len: usize,
327 zb_ptr: *mut f32,
328 zb_len: usize,
329 pub pitch_pixels: usize,
331 pub width: u32,
332 pub height: u32,
333 _marker: std::marker::PhantomData<&'a mut [u32]>,
334}
335
336unsafe impl Send for DrawTarget<'_> {}
342unsafe impl Sync for DrawTarget<'_> {}
343
344impl<'a> DrawTarget<'a> {
345 #[must_use]
350 pub fn new(
351 framebuffer: &'a mut [u32],
352 zbuffer: &'a mut [f32],
353 pitch_pixels: usize,
354 width: u32,
355 height: u32,
356 ) -> Self {
357 Self {
358 fb_ptr: framebuffer.as_mut_ptr(),
359 fb_len: framebuffer.len(),
360 zb_ptr: zbuffer.as_mut_ptr(),
361 zb_len: zbuffer.len(),
362 pitch_pixels,
363 width,
364 height,
365 _marker: std::marker::PhantomData,
366 }
367 }
368
369 #[inline]
378 pub unsafe fn fb_write(self, idx: usize, color: u32) {
379 debug_assert!(idx < self.fb_len, "fb idx {} >= len {}", idx, self.fb_len);
380 unsafe { self.fb_ptr.add(idx).write(color) };
383 }
384
385 #[inline]
393 #[must_use]
394 pub unsafe fn fb_read(self, idx: usize) -> u32 {
395 debug_assert!(idx < self.fb_len, "fb idx {} >= len {}", idx, self.fb_len);
396 unsafe { self.fb_ptr.add(idx).read() }
398 }
399
400 #[inline]
408 #[must_use]
409 pub unsafe fn z_test_write(self, idx: usize, color: u32, z: f32) -> bool {
410 debug_assert!(idx < self.fb_len, "fb idx {} >= len {}", idx, self.fb_len);
411 debug_assert!(idx < self.zb_len, "zb idx {} >= len {}", idx, self.zb_len);
412 unsafe {
414 let zp = self.zb_ptr.add(idx);
415 let cur_z = zp.read();
416 if z < cur_z {
417 zp.write(z);
418 self.fb_ptr.add(idx).write(color);
419 true
420 } else {
421 false
422 }
423 }
424 }
425}
426
427#[inline]
428fn vec4_add(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
429 [a[0] + b[0], a[1] + b[1], a[2] + b[2], a[3] + b[3]]
430}
431
432#[inline]
433fn vec4_sub(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
434 [a[0] - b[0], a[1] - b[1], a[2] - b[2], a[3] - b[3]]
435}
436
437#[inline]
438fn vec4_scale(a: [f32; 4], s: f32) -> [f32; 4] {
439 [a[0] * s, a[1] * s, a[2] * s, a[3] * s]
440}
441
442#[derive(Debug, Clone, Copy)]
455pub struct SpriteLighting<'a> {
456 pub kv6col: u32,
461 pub lightmode: u32,
464 pub lights: &'a [LightSrc],
467}
468
469impl<'a> SpriteLighting<'a> {
470 #[must_use]
474 pub fn from_engine(engine: &'a Engine) -> Self {
475 Self {
476 kv6col: engine.kv6col(),
477 lightmode: engine.lightmode(),
478 lights: engine.lights(),
479 }
480 }
481}
482
483impl SpriteLighting<'static> {
484 #[must_use]
489 pub fn default_oracle() -> Self {
490 Self {
491 kv6col: DEFAULT_KV6COL,
492 lightmode: 0,
493 lights: &[],
494 }
495 }
496}
497
498fn update_reflects(sprite: &Sprite, lighting: &SpriteLighting<'_>) -> (Box<[u64; 256]>, u64) {
516 let fogmul_lo: u32 = 0;
520 let kv6coladd: u64 = 0;
521
522 let kv6col = lighting.kv6col;
523
524 let g_pre = ((((fogmul_lo & 0x7fff) ^ 0x7fff) as i32) as f32) * (16.0 * 8.0 / 65536.0);
527
528 let mut kv6colmul = Box::new([0u64; 256]);
529
530 if lighting.lightmode < 2 {
531 let tp_x = sprite.s[0] + sprite.h[0] + sprite.f[0];
533 let tp_y = sprite.s[1] + sprite.h[1] + sprite.f[1];
534 let tp_z = sprite.s[2] + sprite.h[2] + sprite.f[2];
535
536 let f0 = 64.0_f32 / (tp_x * tp_x + tp_y * tp_y + tp_z * tp_z).sqrt();
537
538 let lo16 = kv6col & 0xffff;
541 let mid24 = kv6col & 0x00ff_ff00;
542 let is_grey = ((lo16 << 8) ^ mid24) == 0;
543
544 if is_grey {
545 let g = g_pre * (((kv6col & 0xff) as f32) / 256.0);
548 let f = f0 * g;
549
550 let l0 = (tp_x * f) as i16; let l1 = (tp_y * f) as i16;
552 let l2 = (tp_z * f) as i16;
553 let l3 = (g * 128.0) as i16;
554
555 let iu = iunivec();
556 for k in 0..256 {
557 let w = dot_iunivec_i16x4(iu[k], [l0, l1, l2, l3]);
558 let w64 = u64::from(w);
559 kv6colmul[k] = w64 | (w64 << 16) | (w64 << 32) | (w64 << 48);
560 }
561 } else {
562 let f = f0 * g_pre;
566
567 let l0 = (tp_x * f) as i16;
568 let l1 = (tp_y * f) as i16;
569 let l2 = (tp_z * f) as i16;
570 let l3 = (g_pre * 128.0) as i16;
571
572 let m = kv6col_channel_mods(kv6col);
573
574 let iu = iunivec();
575 for k in 0..256 {
576 let w = dot_iunivec_i16x4(iu[k], [l0, l1, l2, l3]);
577 kv6colmul[k] = pack_modulated_word(w, m);
578 }
579 }
580 } else {
581 let m = kv6col_channel_mods(kv6col);
587 build_kv6colmul_lightmode2(sprite, lighting.lights, &mut kv6colmul, fogmul_lo, m);
588 }
589
590 (kv6colmul, kv6coladd)
591}
592
593#[inline]
597fn dot_iunivec_i16x4(u: [i16; 4], l: [i16; 4]) -> u16 {
598 let u0 = i32::from(u[0]);
599 let u1 = i32::from(u[1]);
600 let u2 = i32::from(u[2]);
601 let u3 = i32::from(u[3]);
602 let lo = (u0.wrapping_mul(l[0].into())) as u32;
603 let lo = lo.wrapping_add((u1.wrapping_mul(l[1].into())) as u32);
604 let hi = (u2.wrapping_mul(l[2].into())) as u32;
605 let hi = hi.wrapping_add((u3.wrapping_mul(l[3].into())) as u32);
606 ((lo.wrapping_add(hi)) >> 16) as u16
607}
608
609#[inline]
613fn kv6col_channel_mods(kv6col: u32) -> [u16; 4] {
614 [
615 ((kv6col & 0xff) << 8) as u16,
616 (((kv6col >> 8) & 0xff) << 8) as u16,
617 (((kv6col >> 16) & 0xff) << 8) as u16,
618 (((kv6col >> 24) & 0xff) << 8) as u16,
619 ]
620}
621
622#[inline]
625fn pack_modulated_word(w_dot: u16, m: [u16; 4]) -> u64 {
626 let w = u32::from(w_dot);
627 let w0 = ((w * u32::from(m[0])) >> 16) as u16;
628 let w1 = ((w * u32::from(m[1])) >> 16) as u16;
629 let w2 = ((w * u32::from(m[2])) >> 16) as u16;
630 let w3 = ((w * u32::from(m[3])) >> 16) as u16;
631 u64::from(w0) | (u64::from(w1) << 16) | (u64::from(w2) << 32) | (u64::from(w3) << 48)
632}
633
634fn build_kv6colmul_lightmode2(
654 sprite: &Sprite,
655 lights: &[LightSrc],
656 kv6colmul: &mut [u64; 256],
657 fogmul_lo: u32,
658 m: [u16; 4],
659) {
660 let sprs = normalise(sprite.s);
665 let sprh = normalise(sprite.h);
666 let sprf = normalise(sprite.f);
667
668 let hh_initial = ((((fogmul_lo & 0x7fff) ^ 0x7fff) as i32) as f32) * (2.0 / 65536.0);
675
676 let mut lightlist: [[i16; 4]; MAX_LIGHTS + 1] = [[0; 4]; MAX_LIGHTS + 1];
678 let mut lightcnt: usize = 0;
679 for light in lights.iter().rev() {
680 if lightcnt >= MAX_LIGHTS {
681 break;
682 }
683 let fx = light.pos[0] - sprite.p[0];
684 let fy = light.pos[1] - sprite.p[1];
685 let fz = light.pos[2] - sprite.p[2];
686 let gg = fx * fx + fy * fy + fz * fz;
687 let ff = light.r2;
688 if gg >= ff || gg <= 0.0 {
694 continue;
695 }
696 let f = ff.sqrt();
697 let g = gg.sqrt();
698 let mut h = (f * ff - g * gg) / (f * ff * g * gg) * light.sc * 16.0;
700 if g * h > 4096.0 {
701 h = 4096.0 / g; }
703 h *= hh_initial;
704 let l0 = (fx * sprs[0] + fy * sprs[1] + fz * sprs[2]) * h;
705 let l1 = (fx * sprh[0] + fy * sprh[1] + fz * sprh[2]) * h;
706 let l2 = (fx * sprf[0] + fy * sprf[1] + fz * sprf[2]) * h;
707 lightlist[lightcnt] = [l0 as i16, l1 as i16, l2 as i16, 0];
708 lightcnt += 1;
709 }
710
711 let amb_fx = 0.0_f32;
716 let amb_fy = 0.5_f32;
717 let amb_fz = 1.0_f32;
718 let hh = hh_initial * (16.0 * 16.0 * 8.0 / 2.0);
719 let al0 = (sprs[0] * amb_fx + sprs[1] * amb_fy + sprs[2] * amb_fz) * hh;
720 let al1 = (sprh[0] * amb_fx + sprh[1] * amb_fy + sprh[2] * amb_fz) * hh;
721 let al2 = (sprf[0] * amb_fx + sprf[1] * amb_fy + sprf[2] * amb_fz) * hh;
722 let al3 = hh * (48.0 / 16.0);
723 lightlist[lightcnt] = [al0 as i16, al1 as i16, al2 as i16, al3 as i16];
724
725 let iu = iunivec();
726 for idx in 0..256 {
727 let u = iu[idx];
728 let u0 = i32::from(u[0]);
732 let u1 = i32::from(u[1]);
733 let u2 = i32::from(u[2]);
734 let u3 = i32::from(u[3]);
735 let amb = lightlist[lightcnt];
736 let base_lo = (u0.wrapping_mul(i32::from(amb[0]))) as u32;
737 let base_lo = base_lo.wrapping_add((u1.wrapping_mul(i32::from(amb[1]))) as u32);
738 let base_hi = (u2.wrapping_mul(i32::from(amb[2]))) as u32;
739 let base_hi = base_hi.wrapping_add((u3.wrapping_mul(i32::from(amb[3]))) as u32);
740 let mut base = base_lo.wrapping_add(base_hi);
741
742 for k in (0..lightcnt).rev() {
745 let l = lightlist[k];
746 let klo = (u0.wrapping_mul(i32::from(l[0]))) as u32;
747 let klo = klo.wrapping_add((u1.wrapping_mul(i32::from(l[1]))) as u32);
748 let khi = (u2.wrapping_mul(i32::from(l[2]))) as u32;
749 let khi = khi.wrapping_add((u3.wrapping_mul(i32::from(l[3]))) as u32);
750 let dot = klo.wrapping_add(khi);
751 let lo16 = (dot & 0xffff) as i16;
755 let hi16 = ((dot >> 16) & 0xffff) as i16;
756 let lo16c: u16 = if lo16 < 0 { lo16 as u16 } else { 0 };
757 let hi16c: u16 = if hi16 < 0 { hi16 as u16 } else { 0 };
758 let sub = (u32::from(hi16c) << 16) | u32::from(lo16c);
759 base = base.wrapping_sub(sub);
760 }
761
762 let w_dot = (base >> 16) as u16;
763 kv6colmul[idx] = pack_modulated_word(w_dot, m);
764 }
765}
766
767#[inline]
772fn normalise(v: [f32; 3]) -> [f32; 3] {
773 let len_sq = v[0] * v[0] + v[1] * v[1] + v[2] * v[2];
774 if len_sq <= 0.0 {
775 return v;
776 }
777 let inv = 1.0 / len_sq.sqrt();
778 [v[0] * inv, v[1] * inv, v[2] * inv]
779}
780
781pub(crate) fn kv6_compute_full_state<'a>(
784 setup: &Kv6DrawSetup<'a>,
785 sprite: &Sprite,
786 lighting: &SpriteLighting<'_>,
787 cam: &CameraState,
788 settings: &OpticastSettings,
789 fb_width: u32,
790 fb_height: u32,
791 fb_pitch_pixels: usize,
792) -> Kv6FullState<'a> {
793 let sprite_pos = sprite.p;
794 let kv = setup.kv;
795
796 let (nstr, mut nhei, mut nfor, mut npos) = mat2(
800 cam.xs, cam.ys, cam.zs, cam.add, setup.ts, setup.th, setup.tf, sprite_pos,
801 );
802
803 npos[0] -= kv.xpiv * nstr[0] + kv.ypiv * nhei[0] + kv.zpiv * nfor[0];
808 npos[1] -= kv.xpiv * nstr[1] + kv.ypiv * nhei[1] + kv.zpiv * nfor[1];
809 npos[2] -= kv.xpiv * nstr[2] + kv.ypiv * nhei[2] + kv.zpiv * nfor[2];
810
811 let tp = [
814 nhei[1] * nfor[2] - nfor[1] * nhei[2],
815 nfor[1] * nstr[2] - nstr[1] * nfor[2],
816 nstr[1] * nhei[2] - nhei[1] * nstr[2],
817 ];
818 let det = nstr[0] * tp[0] + nhei[0] * tp[1] + nfor[0] * tp[2];
819 let (raw_inx, raw_iny, raw_inz) = if det.to_bits() & 0x7fff_ffff != 0 {
822 let f_inv = -1.0 / det;
823 let tp2 = [
824 npos[1] * nfor[2] - nfor[1] * npos[2],
825 nhei[1] * npos[2] - npos[1] * nhei[2],
826 npos[1] * nstr[2] - nstr[1] * npos[2],
827 ];
828 (
829 ftol((npos[0] * tp[0] - nhei[0] * tp2[0] - nfor[0] * tp2[1]) * f_inv),
830 ftol((npos[0] * tp[1] + nstr[0] * tp2[0] - nfor[0] * tp2[2]) * f_inv),
831 ftol((npos[0] * tp[2] + nstr[0] * tp2[1] + nhei[0] * tp2[2]) * f_inv),
832 )
833 } else {
834 (-1, -1, -1)
835 };
836
837 let xsiz_i = kv.xsiz as i32;
838 let ysiz_i = kv.ysiz as i32;
839 let zsiz_i = kv.zsiz as i32;
840 let iter = Kv6IterState {
841 kv,
842 inx: lbound(raw_inx, -1, xsiz_i),
843 iny: lbound(raw_iny, -1, ysiz_i),
844 inz: lbound(raw_inz, -1, zsiz_i),
845 nxplanemin: 0,
847 nxplanemax: i32::MAX,
848 };
849
850 let swap_x = nhei[0];
858 nhei[0] = nfor[0];
859 nfor[0] = -swap_x;
860 let swap_y = nhei[1];
861 nhei[1] = nfor[1];
862 nfor[1] = -swap_y;
863 let swap_z = nhei[2];
864 nhei[2] = nfor[2];
865 nfor[2] = -swap_z;
866
867 let xres_i = settings.xres as i32;
872 let yres_i = settings.yres as i32;
873 let hx_i = ftol(settings.hx);
874 let hy_i = ftol(settings.hy);
875 let qsum0_x = (0x7fff - (xres_i - hx_i)) as i16;
876 let qsum0_y = (0x7fff - (yres_i - hy_i)) as i16;
877 let qsum0 = [qsum0_x, qsum0_y, qsum0_x, qsum0_y];
878
879 let mut scisdist = 0.0f32;
885 if (nstr[2].to_bits() as i32) < 0 {
886 scisdist -= nstr[2];
887 }
888 if (nhei[2].to_bits() as i32) < 0 {
889 scisdist -= nhei[2];
890 }
891 if (nfor[2].to_bits() as i32) < 0 {
892 scisdist -= nfor[2];
893 }
894
895 let gihz = settings.hz;
899 let cadd1 = [nstr[0] * gihz, nstr[1] * gihz, nstr[2], nstr[2]];
900 let cadd2 = [nhei[0] * gihz, nhei[1] * gihz, nhei[2], nhei[2]];
901 let cadd4_axis = [nfor[0] * gihz, nfor[1] * gihz, nfor[2], nfor[2]];
902 let cadd3 = vec4_add(cadd1, cadd2);
903 let cadd5 = vec4_add(cadd1, cadd4_axis);
904 let cadd6 = vec4_add(cadd2, cadd4_axis);
905 let cadd7 = vec4_add(cadd3, cadd4_axis);
906 let cadd4 = [
907 [0.0; 4], cadd1, cadd2, cadd3, cadd4_axis, cadd5, cadd6, cadd7,
908 ];
909
910 let zsiz = kv.zsiz as usize;
913 let mut ztab4_per_z = Vec::with_capacity(zsiz);
914 if zsiz > 0 {
915 ztab4_per_z.push([0.0f32; 4]);
916 for i in 1..zsiz {
917 let prev = ztab4_per_z[i - 1];
918 ztab4_per_z.push(vec4_add(prev, cadd4[2]));
919 }
920 }
921
922 let r1_pre = [npos[0] * gihz, npos[1] * gihz, npos[2], npos[2]];
927 let r1_initial = vec4_sub(r1_pre, cadd4[4]);
928
929 let r2 = vec4_scale(cadd4[4], -(ysiz_i as f32));
931
932 let pitch_bytes = (fb_pitch_pixels as i32).saturating_mul(4);
936 let qsum1_x = 0x7fff_i32 - fb_width as i32;
937 let qsum1_y = 0x7fff_i32 - fb_height as i32;
938 let qsum1 = [
939 qsum1_x as i16,
940 qsum1_y as i16,
941 qsum1_x as i16,
942 qsum1_y as i16,
943 ];
944 let qbplbpp = [4i16, pitch_bytes as i16, 4, pitch_bytes as i16];
945
946 let (kv6colmul, kv6coladd) = update_reflects(sprite, lighting);
947
948 Kv6FullState {
949 iter,
950 cadd4,
951 ztab4_per_z,
952 r1_initial,
953 r2,
954 scisdist,
955 qsum0,
956 qsum1,
957 qbplbpp,
958 kv6colmul,
959 kv6coladd,
960 }
961}
962
963#[cfg(target_arch = "x86_64")]
994#[allow(clippy::trivially_copy_pass_by_ref)] pub(crate) fn drawboundcubesse(
996 v: &Voxel,
997 mask: u32,
998 state: &Kv6FullState<'_>,
999 r0: [f32; 4],
1000 mm5_tail: &mut u32,
1001 target: &mut DrawTarget<'_>,
1002) -> u32 {
1003 use core::arch::x86_64::{
1004 __m128, __m128i, _mm_add_epi16, _mm_add_ps, _mm_adds_epi16, _mm_cvtsi128_si32,
1005 _mm_cvtsi32_si128, _mm_cvttps_epi32, _mm_loadl_epi64, _mm_loadu_ps, _mm_madd_epi16,
1006 _mm_max_epi16, _mm_min_epi16, _mm_movehl_ps, _mm_movelh_ps, _mm_mul_ps, _mm_mulhi_epu16,
1007 _mm_packs_epi32, _mm_packus_epi16, _mm_rcp_ps, _mm_setzero_si128, _mm_shufflelo_epi16,
1008 _mm_storeu_ps, _mm_storeu_si128, _mm_subs_epu16, _mm_unpackhi_epi64, _mm_unpacklo_epi32,
1009 _mm_unpacklo_epi8,
1010 };
1011
1012 let effmask = (mask & u32::from(v.vis)) as usize;
1013 if effmask == 0 || effmask >= PTFACES16.len() {
1014 return 0;
1015 }
1016 let face = PTFACES16[effmask];
1017 if face[0] == 0 {
1018 return 0;
1019 }
1020
1021 let z_idx = v.z as usize;
1023 if z_idx >= state.ztab4_per_z.len() {
1024 return 0;
1025 }
1026 let ztep = state.ztab4_per_z[z_idx];
1027 unsafe {
1031 let r0_v = _mm_loadu_ps(r0.as_ptr());
1032 let ztep_v = _mm_loadu_ps(ztep.as_ptr());
1033 let origin_v: __m128 = _mm_add_ps(r0_v, ztep_v);
1034 let mut origin_arr = [0.0f32; 4];
1035 _mm_storeu_ps(origin_arr.as_mut_ptr(), origin_v);
1036 if origin_arr[2] < state.scisdist {
1037 return 0;
1038 }
1039
1040 let project = |off_a: u8, off_b: u8| -> __m128 {
1046 let a = state.cadd4[(off_a >> 4) as usize];
1047 let b = state.cadd4[(off_b >> 4) as usize];
1048 let wva = _mm_add_ps(_mm_loadu_ps(a.as_ptr()), origin_v);
1049 let wvb = _mm_add_ps(_mm_loadu_ps(b.as_ptr()), origin_v);
1050 let wv0 = _mm_movehl_ps(wva, wvb); let wv1 = _mm_movelh_ps(wvb, wva); let wv0_inv = _mm_rcp_ps(wv0);
1053 _mm_mul_ps(wv0_inv, wv1)
1054 };
1055
1056 let pair01 = project(face[1], face[2]);
1057 let pair23 = project(face[3], face[4]);
1058
1059 let p01_i32 = _mm_cvttps_epi32(pair01);
1063 let p23_i32 = _mm_cvttps_epi32(pair23);
1064 let pack_lo = _mm_packs_epi32(p01_i32, p23_i32);
1065 let pack01 = pack_lo;
1066 let pack23 = _mm_unpackhi_epi64(pack_lo, _mm_setzero_si128());
1067 let mut mm_min = _mm_min_epi16(pack01, pack23);
1068 let mut mm_max = _mm_max_epi16(pack01, pack23);
1069
1070 if face[0] != 4 {
1071 let pair45 = project(face[5], face[6]);
1072 let p45_i32 = _mm_cvttps_epi32(pair45);
1073 let pack45 = _mm_packs_epi32(p45_i32, _mm_setzero_si128());
1074 mm_min = _mm_min_epi16(mm_min, pack45);
1075 mm_max = _mm_max_epi16(mm_max, pack45);
1076 }
1077
1078 let mm_min_hi = _mm_shufflelo_epi16(mm_min, 0x0e);
1081 let mm_max_hi = _mm_shufflelo_epi16(mm_max, 0x0e);
1082 let mm_min_red = _mm_min_epi16(mm_min, mm_min_hi);
1083 let mm_max_red = _mm_max_epi16(mm_max, mm_max_hi);
1084
1085 let bounds = _mm_unpacklo_epi32(mm_min_red, mm_max_red);
1093
1094 let qsum0_v = _mm_loadl_epi64(state.qsum0.as_ptr().cast::<__m128i>());
1097 let qsum1_v = _mm_loadl_epi64(state.qsum1.as_ptr().cast::<__m128i>());
1098 let bounds = _mm_adds_epi16(bounds, qsum0_v);
1099 let bounds = _mm_max_epi16(bounds, qsum1_v);
1100
1101 let bounds_hi = _mm_shufflelo_epi16(bounds, 0xee);
1104 let dxdy = _mm_subs_epu16(bounds_hi, bounds);
1105 let dxdy_low = _mm_cvtsi128_si32(dxdy) as u32;
1106 let dx = (dxdy_low & 0xffff) as i32;
1107 if dx == 0 {
1108 return 0;
1109 }
1110 let dy = ((dxdy_low >> 16) as i32) - 1;
1111 if dy < 0 {
1112 return 0;
1113 }
1114
1115 let mut bounds_arr = [0i16; 8];
1119 _mm_storeu_si128(bounds_arr.as_mut_ptr().cast::<__m128i>(), bounds);
1120 let pixel_min_x = i32::from(bounds_arr[0]) - i32::from(state.qsum1[0]);
1121 let pixel_min_y = i32::from(bounds_arr[1]) - i32::from(state.qsum1[1]);
1122
1123 let qbplbpp_v = _mm_loadl_epi64(state.qbplbpp.as_ptr().cast::<__m128i>());
1127 let _ = _mm_madd_epi16(bounds, qbplbpp_v);
1128
1129 let tail_in = *mm5_tail;
1131 let mm5 = _mm_cvtsi32_si128(tail_in as i32);
1132 let col_v = _mm_cvtsi32_si128(v.col as i32);
1133 let mm5 = _mm_unpacklo_epi8(mm5, col_v);
1134 let kvm = state.kv6colmul[v.dir as usize];
1135 let kvm_v = _mm_loadl_epi64(std::ptr::addr_of!(kvm).cast::<__m128i>());
1136 let mm5 = _mm_mulhi_epu16(mm5, kvm_v);
1137 let kva_v = _mm_loadl_epi64(std::ptr::addr_of!(state.kv6coladd).cast::<__m128i>());
1138 let mm5 = _mm_add_epi16(mm5, kva_v);
1139 let mm5 = _mm_packus_epi16(mm5, mm5);
1140 let color = _mm_cvtsi128_si32(mm5) as u32;
1141 *mm5_tail = color;
1142
1143 let z_val = origin_arr[2];
1148 let pitch = target.pitch_pixels;
1149 let x0 = pixel_min_x as usize;
1150 let x_end = x0 + dx as usize;
1151 let mut written: u32 = 0;
1152 for row in 0..=(dy as usize) {
1153 let y = pixel_min_y as usize + row;
1154 let row_start = y * pitch;
1155 for x in x0..x_end {
1156 let idx = row_start + x;
1157 if target.z_test_write(idx, color, z_val) {
1161 written += 1;
1162 }
1163 }
1164 }
1165 written
1166 }
1167}
1168
1169#[cfg(not(target_arch = "x86_64"))]
1176#[allow(clippy::trivially_copy_pass_by_ref)]
1177pub(crate) fn drawboundcubesse(
1178 v: &Voxel,
1179 mask: u32,
1180 state: &Kv6FullState<'_>,
1181 r0: [f32; 4],
1182 mm5_tail: &mut u32,
1183 target: &mut DrawTarget<'_>,
1184) -> u32 {
1185 let effmask = (mask & u32::from(v.vis)) as usize;
1186 if effmask == 0 || effmask >= PTFACES16.len() {
1187 return 0;
1188 }
1189 let face = PTFACES16[effmask];
1190 if face[0] == 0 {
1191 return 0;
1192 }
1193
1194 let z_idx = v.z as usize;
1196 if z_idx >= state.ztab4_per_z.len() {
1197 return 0;
1198 }
1199 let origin = vec4_add(r0, state.ztab4_per_z[z_idx]);
1200 if origin[2] < state.scisdist {
1201 return 0;
1202 }
1203
1204 let hx = (i32::from(state.qsum0[0]) - i32::from(state.qsum1[0])) as f32;
1208 let hy = (i32::from(state.qsum0[1]) - i32::from(state.qsum1[1])) as f32;
1209
1210 let project = |off: u8| -> (f32, f32) {
1212 let wv = vec4_add(state.cadd4[(off >> 4) as usize], origin);
1213 let inv_z = 1.0 / wv[2];
1214 (wv[0] * inv_z + hx, wv[1] * inv_z + hy)
1215 };
1216
1217 let (a0x, a0y) = project(face[1]);
1219 let (a1x, a1y) = project(face[2]);
1220 let (a2x, a2y) = project(face[3]);
1221 let (a3x, a3y) = project(face[4]);
1222 let mut min_x = a0x.min(a1x).min(a2x).min(a3x) as i32;
1223 let mut min_y = a0y.min(a1y).min(a2y).min(a3y) as i32;
1224 let mut max_x = a0x.max(a1x).max(a2x).max(a3x) as i32;
1225 let mut max_y = a0y.max(a1y).max(a2y).max(a3y) as i32;
1226
1227 if face[0] != 4 {
1228 let (a4x, a4y) = project(face[5]);
1229 let (a5x, a5y) = project(face[6]);
1230 min_x = min_x.min(a4x as i32).min(a5x as i32);
1231 min_y = min_y.min(a4y as i32).min(a5y as i32);
1232 max_x = max_x.max(a4x as i32).max(a5x as i32);
1233 max_y = max_y.max(a4y as i32).max(a5y as i32);
1234 }
1235
1236 let fb_w = target.width as i32;
1239 let fb_h = target.height as i32;
1240 min_x = min_x.max(0);
1241 min_y = min_y.max(0);
1242 max_x = max_x.min(fb_w - 1);
1243 max_y = max_y.min(fb_h - 1);
1244 if min_x > max_x || min_y > max_y {
1245 return 0;
1246 }
1247
1248 let t = mm5_tail.to_le_bytes();
1253 let c = v.col.to_le_bytes();
1254 let interleaved: [u16; 4] = [
1255 (u16::from(c[0]) << 8) | u16::from(t[0]),
1256 (u16::from(c[1]) << 8) | u16::from(t[1]),
1257 (u16::from(c[2]) << 8) | u16::from(t[2]),
1258 (u16::from(c[3]) << 8) | u16::from(t[3]),
1259 ];
1260 let kvm = state.kv6colmul[v.dir as usize];
1261 let kva = state.kv6coladd;
1262 let mut color_bytes = [0u8; 4];
1263 for i in 0..4 {
1264 let km = ((kvm >> (i * 16)) & 0xffff) as u16;
1265 let ka = ((kva >> (i * 16)) & 0xffff) as u16;
1266 let hi = ((u32::from(interleaved[i]) * u32::from(km)) >> 16) as u16;
1267 let val = hi.wrapping_add(ka) as i16;
1268 color_bytes[i] = val.clamp(0, 255) as u8;
1269 }
1270 let color = u32::from_le_bytes(color_bytes);
1271 *mm5_tail = color;
1272
1273 let z_val = origin[2];
1275 let pitch = target.pitch_pixels;
1276 let mut written: u32 = 0;
1277 for y in min_y..=max_y {
1278 let row_start = y as usize * pitch;
1279 for x in min_x..=max_x {
1280 let idx = row_start + x as usize;
1281 unsafe {
1283 if target.z_test_write(idx, color, z_val) {
1284 written += 1;
1285 }
1286 }
1287 }
1288 }
1289 written
1290}
1291
1292fn draw_boundcube_line<F: FnMut(&Voxel, u32, [f32; 4])>(
1307 voxels: &[Voxel],
1308 range_start: usize,
1309 range_end: usize,
1310 inz: i32,
1311 base_mask: u32,
1312 r0: [f32; 4],
1313 callback: &mut F,
1314) {
1315 if range_end <= range_start {
1316 return;
1317 }
1318 let mut v0 = range_start;
1319 let mut v1_excl = range_end;
1320
1321 while v0 < v1_excl && i32::from(voxels[v0].z) < inz {
1323 callback(&voxels[v0], base_mask | 0x20, r0);
1324 v0 += 1;
1325 }
1326 while v0 < v1_excl && i32::from(voxels[v1_excl - 1].z) > inz {
1328 callback(&voxels[v1_excl - 1], base_mask | 0x10, r0);
1329 v1_excl -= 1;
1330 }
1331 if v0 + 1 == v1_excl {
1333 callback(&voxels[v0], base_mask, r0);
1334 }
1335}
1336
1337#[allow(clippy::too_many_lines)]
1350pub(crate) fn kv6_iterate<F: FnMut(&Voxel, u32, [f32; 4])>(
1351 state: &Kv6FullState<'_>,
1352 mut callback: F,
1353) {
1354 let kv = state.iter.kv;
1355 let xsiz = kv.xsiz as i32;
1356 let ysiz = kv.ysiz as i32;
1357 let inx = state.iter.inx;
1358 let iny = state.iter.iny;
1359 let inz = state.iter.inz;
1360 let nxplanemin = state.iter.nxplanemin;
1361 let nxplanemax = state.iter.nxplanemax;
1362 let cadd1 = state.cadd4[1];
1363 let cadd_y = state.cadd4[4];
1364 let r2 = state.r2;
1365
1366 let mut xv: usize = 0;
1367 let mut r1 = state.r1_initial;
1368
1369 let mut x: i32 = 0;
1371 while x < inx {
1372 let xu = x as usize;
1373 let xlen = kv.xlen[xu] as usize;
1374 if x < nxplanemin || x >= nxplanemax {
1375 xv += xlen;
1376 r1 = vec4_add(r1, cadd1);
1377 x += 1;
1378 continue;
1379 }
1380 let yv_initial = xv + xlen;
1381 let mut r0 = r1; let mut xv_local = xv;
1385 let mut y: i32 = 0;
1386 while y < iny {
1387 let yu = y as usize;
1388 let len = kv.ylen[xu][yu] as usize;
1389 let v0 = xv_local;
1390 xv_local += len;
1391 draw_boundcube_line(&kv.voxels, v0, xv_local, inz, 0xa, r0, &mut callback);
1392 r0 = vec4_sub(r0, cadd_y); y += 1;
1394 }
1395
1396 let mut yv_local = yv_initial;
1399 r0 = vec4_add(r1, r2);
1400 r1 = vec4_add(r1, cadd1);
1401
1402 let mut y = ysiz - 1;
1404 while y > iny {
1405 r0 = vec4_add(r0, cadd_y); let yu = y as usize;
1407 let len = kv.ylen[xu][yu] as usize;
1408 let v1_excl = yv_local;
1409 yv_local -= len;
1410 draw_boundcube_line(&kv.voxels, yv_local, v1_excl, inz, 0x6, r0, &mut callback);
1411 y -= 1;
1412 }
1413
1414 if iny >= 0 && (iny as u32) < kv.ysiz {
1416 r0 = vec4_add(r0, cadd_y);
1417 let yu = iny as usize;
1418 let len = kv.ylen[xu][yu] as usize;
1419 let v1_excl = yv_local;
1420 yv_local -= len;
1421 draw_boundcube_line(&kv.voxels, yv_local, v1_excl, inz, 0x2, r0, &mut callback);
1422 }
1423
1424 xv += xlen;
1425 x += 1;
1426 }
1427
1428 let dx_remain = (xsiz - x) as f32;
1431 r1 = vec4_add(r1, vec4_scale(cadd1, dx_remain));
1432
1433 let mut xv2: usize = kv.voxels.len();
1436 let mut x = xsiz - 1;
1437 while x > inx {
1438 let xu = x as usize;
1439 let xlen = kv.xlen[xu] as usize;
1440 if x < nxplanemin || x >= nxplanemax {
1441 xv2 -= xlen;
1442 r1 = vec4_sub(r1, cadd1);
1443 x -= 1;
1444 continue;
1445 }
1446 let yv_initial = xv2 - xlen;
1447 r1 = vec4_sub(r1, cadd1);
1449 let mut r0 = vec4_add(r1, r2);
1450
1451 let mut xv_local = xv2;
1453 let mut y = ysiz - 1;
1454 while y > iny {
1455 r0 = vec4_add(r0, cadd_y);
1456 let yu = y as usize;
1457 let len = kv.ylen[xu][yu] as usize;
1458 let v1_excl = xv_local;
1459 xv_local -= len;
1460 draw_boundcube_line(&kv.voxels, xv_local, v1_excl, inz, 0x5, r0, &mut callback);
1461 y -= 1;
1462 }
1463
1464 let mut yv_local = yv_initial;
1466 r0 = r1;
1467
1468 let mut y: i32 = 0;
1470 while y < iny {
1471 let yu = y as usize;
1472 let len = kv.ylen[xu][yu] as usize;
1473 let v0 = yv_local;
1474 yv_local += len;
1475 draw_boundcube_line(&kv.voxels, v0, yv_local, inz, 0x9, r0, &mut callback);
1476 r0 = vec4_sub(r0, cadd_y);
1477 y += 1;
1478 }
1479
1480 if iny >= 0 && (iny as u32) < kv.ysiz {
1482 let yu = iny as usize;
1483 let len = kv.ylen[xu][yu] as usize;
1484 let v0 = yv_local;
1485 yv_local += len;
1486 draw_boundcube_line(&kv.voxels, v0, yv_local, inz, 0x1, r0, &mut callback);
1487 }
1488
1489 xv2 -= xlen;
1490 x -= 1;
1491 }
1492
1493 if inx >= 0 && (inx as u32) < kv.xsiz {
1495 let xu = inx as usize;
1496 if inx < nxplanemin || inx >= nxplanemax {
1497 return;
1498 }
1499 let xlen = kv.xlen[xu] as usize;
1500 let yv_initial = xv2 - xlen;
1501 r1 = vec4_sub(r1, cadd1);
1502 let mut r0 = vec4_add(r1, r2);
1503
1504 let mut xv_local = xv2;
1506 let mut y = ysiz - 1;
1507 while y > iny {
1508 r0 = vec4_add(r0, cadd_y);
1509 let yu = y as usize;
1510 let len = kv.ylen[xu][yu] as usize;
1511 let v1_excl = xv_local;
1512 xv_local -= len;
1513 draw_boundcube_line(&kv.voxels, xv_local, v1_excl, inz, 0x4, r0, &mut callback);
1514 y -= 1;
1515 }
1516
1517 let mut yv_local = yv_initial;
1519 r0 = r1;
1520
1521 let mut y: i32 = 0;
1523 while y < iny {
1524 let yu = y as usize;
1525 let len = kv.ylen[xu][yu] as usize;
1526 let v0 = yv_local;
1527 yv_local += len;
1528 draw_boundcube_line(&kv.voxels, v0, yv_local, inz, 0x8, r0, &mut callback);
1529 r0 = vec4_sub(r0, cadd_y);
1530 y += 1;
1531 }
1532
1533 if iny >= 0 && (iny as u32) < kv.ysiz {
1535 let yu = iny as usize;
1536 let len = kv.ylen[xu][yu] as usize;
1537 let v0 = yv_local;
1538 yv_local += len;
1539 draw_boundcube_line(&kv.voxels, v0, yv_local, inz, 0x0, r0, &mut callback);
1540 }
1541 }
1542}
1543
1544#[allow(clippy::module_name_repetitions)]
1584#[must_use]
1585pub fn draw_sprites_parallel(
1586 target: DrawTarget<'_>,
1587 cam: &CameraState,
1588 settings: &OpticastSettings,
1589 lighting: &SpriteLighting<'_>,
1590 sprites: &[Sprite],
1591) -> u32 {
1592 use rayon::prelude::*;
1593
1594 let render_one = |sprite: &Sprite| {
1595 let mut t = target;
1599 draw_sprite(&mut t, cam, settings, lighting, sprite)
1600 };
1601
1602 sprites.par_iter().map(render_one).sum()
1603}
1604
1605pub fn draw_sprite(
1606 target: &mut DrawTarget<'_>,
1607 cam: &CameraState,
1608 settings: &OpticastSettings,
1609 lighting: &SpriteLighting<'_>,
1610 sprite: &Sprite,
1611) -> u32 {
1612 if sprite.flags & SPRITE_FLAG_INVISIBLE != 0 {
1613 return 0;
1614 }
1615 if sprite.flags & SPRITE_FLAG_KFA != 0 {
1616 return 0;
1617 }
1618 if sprite.flags & SPRITE_FLAG_NO_Z != 0 {
1619 return 0;
1621 }
1622 let Some(setup) = kv6_draw_prepare(sprite, cam) else {
1623 return 0;
1624 };
1625 let state = kv6_compute_full_state(
1626 &setup,
1627 sprite,
1628 lighting,
1629 cam,
1630 settings,
1631 target.width,
1632 target.height,
1633 target.pitch_pixels,
1634 );
1635 let mut mm5_tail: u32 = 0;
1636 let mut total_written: u32 = 0;
1637 kv6_iterate(&state, |voxel, mask, r0| {
1638 total_written += drawboundcubesse(voxel, mask, &state, r0, &mut mm5_tail, target);
1639 });
1640 total_written
1641}
1642
1643#[cfg(test)]
1644mod tests {
1645 use super::*;
1646 use crate::camera_math;
1647 use crate::Camera;
1648 use roxlap_formats::kv6::Kv6;
1649
1650 fn empty_kv6() -> Kv6 {
1651 Kv6 {
1652 xsiz: 1,
1653 ysiz: 1,
1654 zsiz: 1,
1655 xpiv: 0.5,
1656 ypiv: 0.5,
1657 zpiv: 0.5,
1658 voxels: Vec::new(),
1659 xlen: vec![0],
1660 ylen: vec![vec![0]],
1661 palette: None,
1662 }
1663 }
1664
1665 fn cube_kv6() -> Kv6 {
1669 Kv6 {
1670 xsiz: 17,
1671 ysiz: 17,
1672 zsiz: 17,
1673 xpiv: 8.5,
1674 ypiv: 8.5,
1675 zpiv: 8.5,
1676 voxels: Vec::new(),
1677 xlen: vec![0; 17],
1678 ylen: vec![vec![0; 17]; 17],
1679 palette: None,
1680 }
1681 }
1682
1683 fn oracle_sprite_front_camera() -> camera_math::CameraState {
1686 let camera = Camera {
1687 pos: [1020.0, 1050.0, 175.0],
1688 right: [0.0, 1.0, 0.0],
1691 down: [0.0, 0.0, 1.0],
1692 forward: [1.0, 0.0, 0.0],
1693 };
1694 camera_math::derive(&camera, 640, 480, 320.0, 240.0, 320.0)
1695 }
1696
1697 fn oracle_settings() -> OpticastSettings {
1698 OpticastSettings::for_oracle_framebuffer(640, 480)
1699 }
1700
1701 fn compute_state_for_test<'a>(
1706 setup: &Kv6DrawSetup<'a>,
1707 sprite: &Sprite,
1708 cam: &camera_math::CameraState,
1709 ) -> Kv6FullState<'a> {
1710 let lighting = SpriteLighting::default_oracle();
1711 kv6_compute_full_state(
1712 setup,
1713 sprite,
1714 &lighting,
1715 cam,
1716 &oracle_settings(),
1717 640,
1718 480,
1719 640,
1720 )
1721 }
1722
1723 fn alloc_target() -> (Vec<u32>, Vec<f32>) {
1727 let pixels = 640usize * 480usize;
1728 (vec![0u32; pixels], vec![f32::INFINITY; pixels])
1729 }
1730
1731 fn make_target<'a>(fb: &'a mut [u32], zb: &'a mut [f32]) -> DrawTarget<'a> {
1732 DrawTarget::new(fb, zb, 640, 640, 480)
1733 }
1734
1735 fn bits4(a: [f32; 4]) -> [u32; 4] {
1739 a.map(f32::to_bits)
1740 }
1741
1742 const SPRITE_MELTSPHERE_KV6: &[u8] = include_bytes!("../tests/fixtures/sprite_meltsphere.kv6");
1746
1747 #[test]
1748 fn axis_aligned_sets_identity_basis() {
1749 let bits = |a: [f32; 3]| a.map(f32::to_bits);
1752 let s = Sprite::axis_aligned(empty_kv6(), [10.0, 20.0, 30.0]);
1753 assert_eq!(bits(s.p), bits([10.0, 20.0, 30.0]));
1754 assert_eq!(bits(s.s), bits([1.0, 0.0, 0.0]));
1755 assert_eq!(bits(s.h), bits([0.0, 1.0, 0.0]));
1756 assert_eq!(bits(s.f), bits([0.0, 0.0, 1.0]));
1757 assert_eq!(s.flags, 0);
1758 }
1759
1760 #[test]
1761 fn invisible_flag_skips_dispatch() {
1762 let cam = oracle_sprite_front_camera();
1763 let mut s = Sprite::axis_aligned(cube_kv6(), [1050.0, 1050.0, 175.0]);
1764 s.flags = SPRITE_FLAG_INVISIBLE;
1765 let (mut fb, mut zb) = alloc_target();
1766 let mut target = make_target(&mut fb, &mut zb);
1767 let lighting = SpriteLighting::default_oracle();
1768 assert_eq!(
1769 draw_sprite(&mut target, &cam, &oracle_settings(), &lighting, &s),
1770 0
1771 );
1772 }
1773
1774 #[test]
1775 fn kfa_flag_skips_dispatch() {
1776 let cam = oracle_sprite_front_camera();
1777 let mut s = Sprite::axis_aligned(cube_kv6(), [1050.0, 1050.0, 175.0]);
1778 s.flags = SPRITE_FLAG_KFA;
1779 let (mut fb, mut zb) = alloc_target();
1780 let mut target = make_target(&mut fb, &mut zb);
1781 let lighting = SpriteLighting::default_oracle();
1782 assert_eq!(
1783 draw_sprite(&mut target, &cam, &oracle_settings(), &lighting, &s),
1784 0
1785 );
1786 }
1787
1788 #[test]
1789 fn cull_keeps_oracle_sprite_in_front_of_camera() {
1790 let cam = oracle_sprite_front_camera();
1794 let s = Sprite::axis_aligned(cube_kv6(), [1050.0, 1050.0, 175.0]);
1795 assert!(
1796 kv6_draw_prepare(&s, &cam).is_some(),
1797 "front-of-camera sprite must NOT be culled"
1798 );
1799 }
1800
1801 #[test]
1802 fn cull_removes_sprite_far_behind_camera() {
1803 let cam = oracle_sprite_front_camera();
1806 let s = Sprite::axis_aligned(cube_kv6(), [1020.0 - 500.0, 1050.0, 175.0]);
1807 assert!(
1808 kv6_draw_prepare(&s, &cam).is_none(),
1809 "behind-camera sprite must be culled"
1810 );
1811 }
1812
1813 #[test]
1814 fn cull_removes_sprite_far_to_the_right() {
1815 let cam = oracle_sprite_front_camera();
1819 let s = Sprite::axis_aligned(cube_kv6(), [1050.0, 1050.0 + 200.0, 175.0]);
1822 assert!(
1823 kv6_draw_prepare(&s, &cam).is_none(),
1824 "far-right sprite must be culled"
1825 );
1826 }
1827
1828 #[test]
1829 fn cull_keeps_sprite_at_camera_position() {
1830 let cam = oracle_sprite_front_camera();
1834 let s = Sprite::axis_aligned(cube_kv6(), cam.pos);
1835 assert!(
1836 kv6_draw_prepare(&s, &cam).is_some(),
1837 "sprite at camera position must not be culled"
1838 );
1839 }
1840
1841 #[test]
1842 fn iterate_visits_each_voxel_exactly_once() {
1843 let xsiz: u32 = 3;
1848 let ysiz: u32 = 3;
1849 let zsiz: u32 = 3;
1850 let mut voxels = Vec::new();
1851 let mut xlen = vec![0u32; xsiz as usize];
1852 let mut ylen = vec![vec![0u16; ysiz as usize]; xsiz as usize];
1853 for x in 0..xsiz {
1854 for y in 0..ysiz {
1855 let z = ((x + y) % 3) as u16;
1856 voxels.push(Voxel {
1857 col: 0x0080_0000,
1858 z,
1859 vis: 63,
1860 dir: 0,
1861 });
1862 xlen[x as usize] += 1;
1863 ylen[x as usize][y as usize] = 1;
1864 }
1865 }
1866 let kv = Kv6 {
1867 xsiz,
1868 ysiz,
1869 zsiz,
1870 xpiv: 1.5,
1871 ypiv: 1.5,
1872 zpiv: 1.5,
1873 voxels,
1874 xlen,
1875 ylen,
1876 palette: None,
1877 };
1878 let setup = Kv6DrawSetup {
1879 kv: &kv,
1880 ts: [1.0, 0.0, 0.0],
1881 th: [0.0, 1.0, 0.0],
1882 tf: [0.0, 0.0, 1.0],
1883 mip: 0,
1884 };
1885 let cam = oracle_sprite_front_camera();
1886 let synth_sprite = Sprite::axis_aligned(empty_kv6(), [1050.0, 1050.0, 175.0]);
1887 let state = compute_state_for_test(&setup, &synth_sprite, &cam);
1888
1889 let voxels_ptr = kv.voxels.as_ptr();
1892 let mut visited = vec![0u32; kv.voxels.len()];
1893 let mut total: u32 = 0;
1894 kv6_iterate(&state, |v, _mask, _r0| {
1895 let idx = unsafe { std::ptr::from_ref::<Voxel>(v).offset_from(voxels_ptr) } as usize;
1898 visited[idx] += 1;
1899 total += 1;
1900 });
1901 assert_eq!(total as usize, kv.voxels.len(), "total callback fires");
1902 for (i, &n) in visited.iter().enumerate() {
1903 assert_eq!(n, 1, "voxel {i} visited {n} times (want 1)");
1904 }
1905 }
1906
1907 #[test]
1908 fn iterate_meltsphere_oracle_visits_each_voxel_once() {
1909 let kv = roxlap_formats::kv6::parse(SPRITE_MELTSPHERE_KV6).expect("parse fixture");
1914 assert_eq!(kv.voxels.len(), 401, "fixture voxel count");
1915
1916 let sprite = Sprite::axis_aligned(kv, [1050.0, 1050.0, 175.0]);
1917 let cam = oracle_sprite_front_camera();
1918 let setup = kv6_draw_prepare(&sprite, &cam).expect("oracle sprite must pass cull");
1919 let state = compute_state_for_test(&setup, &sprite, &cam);
1920
1921 let voxels_ptr = sprite.kv6.voxels.as_ptr();
1922 let mut visited = vec![0u32; sprite.kv6.voxels.len()];
1923 let mut total: u32 = 0;
1924 kv6_iterate(&state, |v, _mask, _r0| {
1925 let idx = unsafe { std::ptr::from_ref::<Voxel>(v).offset_from(voxels_ptr) } as usize;
1926 visited[idx] += 1;
1927 total += 1;
1928 });
1929 assert_eq!(total, 401);
1930 let max = visited.iter().copied().max().unwrap();
1931 let min = visited.iter().copied().min().unwrap();
1932 assert_eq!(max, 1, "no voxel may be visited twice");
1933 assert_eq!(min, 1, "no voxel may be skipped");
1934 }
1935
1936 #[test]
1937 fn full_state_basic_invariants() {
1938 let kv = roxlap_formats::kv6::parse(SPRITE_MELTSPHERE_KV6).expect("parse fixture");
1944 let sprite = Sprite::axis_aligned(kv, [1050.0, 1050.0, 175.0]);
1945 let cam = oracle_sprite_front_camera();
1946 let setup = kv6_draw_prepare(&sprite, &cam).expect("cull pass");
1947 let state = compute_state_for_test(&setup, &sprite, &cam);
1948
1949 assert_eq!(bits4(state.ztab4_per_z[0]), bits4([0.0; 4]));
1951
1952 for z in 1..state.ztab4_per_z.len() {
1954 let want = vec4_add(state.ztab4_per_z[z - 1], state.cadd4[2]);
1955 assert_eq!(bits4(state.ztab4_per_z[z]), bits4(want), "ztab4_per_z[{z}]");
1956 }
1957
1958 assert_eq!(
1961 bits4(state.cadd4[3]),
1962 bits4(vec4_add(state.cadd4[1], state.cadd4[2]))
1963 );
1964 assert_eq!(
1965 bits4(state.cadd4[5]),
1966 bits4(vec4_add(state.cadd4[1], state.cadd4[4]))
1967 );
1968 assert_eq!(
1969 bits4(state.cadd4[6]),
1970 bits4(vec4_add(state.cadd4[2], state.cadd4[4]))
1971 );
1972 assert_eq!(
1973 bits4(state.cadd4[7]),
1974 bits4(vec4_add(state.cadd4[3], state.cadd4[4]))
1975 );
1976 assert_eq!(bits4(state.cadd4[0]), bits4([0.0; 4]));
1977
1978 let want_r2 = vec4_scale(state.cadd4[4], -(state.iter.kv.ysiz as f32));
1980 assert_eq!(bits4(state.r2), bits4(want_r2));
1981 }
1982
1983 #[test]
1984 fn drawboundcubesse_culls_invisible_face_mask() {
1985 let v = Voxel {
1988 col: 0,
1989 z: 0,
1990 vis: 0,
1991 dir: 0,
1992 };
1993 let kv = roxlap_formats::kv6::parse(SPRITE_MELTSPHERE_KV6).expect("parse fixture");
1994 let sprite = Sprite::axis_aligned(kv, [1050.0, 1050.0, 175.0]);
1995 let cam = oracle_sprite_front_camera();
1996 let setup = kv6_draw_prepare(&sprite, &cam).expect("cull pass");
1997 let state = compute_state_for_test(&setup, &sprite, &cam);
1998 let (mut fb, mut zb) = alloc_target();
1999 let mut target = make_target(&mut fb, &mut zb);
2000 let mut tail = 0u32;
2001 assert_eq!(
2002 drawboundcubesse(
2003 &v,
2004 0xff,
2005 &state,
2006 [0.0, 0.0, 100.0, 100.0],
2007 &mut tail,
2008 &mut target,
2009 ),
2010 0
2011 );
2012 }
2013
2014 #[test]
2015 fn drawboundcubesse_culls_voxel_behind_near_plane() {
2016 let v = Voxel {
2022 col: 0xff,
2023 z: 0,
2024 vis: 0xff,
2025 dir: 0,
2026 };
2027 let kv = roxlap_formats::kv6::parse(SPRITE_MELTSPHERE_KV6).expect("parse fixture");
2028 let sprite = Sprite::axis_aligned(kv, [1050.0, 1050.0, 175.0]);
2029 let cam = oracle_sprite_front_camera();
2030 let setup = kv6_draw_prepare(&sprite, &cam).expect("cull pass");
2031 let state = compute_state_for_test(&setup, &sprite, &cam);
2032 let r0 = [0.0, 0.0, -1000.0, -1000.0];
2035 let (mut fb, mut zb) = alloc_target();
2036 let mut target = make_target(&mut fb, &mut zb);
2037 let mut tail = 0u32;
2038 assert_eq!(
2039 drawboundcubesse(&v, 0xff, &state, r0, &mut tail, &mut target),
2040 0
2041 );
2042 }
2043
2044 #[test]
2045 fn iterate_no_voxels_when_culled() {
2046 let cam = oracle_sprite_front_camera();
2049 let s = Sprite::axis_aligned(cube_kv6(), [1020.0 - 500.0, 1050.0, 175.0]);
2050 assert!(kv6_draw_prepare(&s, &cam).is_none());
2052 }
2053
2054 #[test]
2055 fn draw_sprite_writes_pixels_for_oracle_meltsphere() {
2056 let kv = roxlap_formats::kv6::parse(SPRITE_MELTSPHERE_KV6).expect("parse fixture");
2060 let sprite = Sprite::axis_aligned(kv, [1050.0, 1050.0, 175.0]);
2061 let cam = oracle_sprite_front_camera();
2062 let (mut fb, mut zb) = alloc_target();
2063 let mut target = make_target(&mut fb, &mut zb);
2064 let lighting = SpriteLighting::default_oracle();
2065 let written = draw_sprite(&mut target, &cam, &oracle_settings(), &lighting, &sprite);
2066 assert!(written > 0, "expected some pixels to be written");
2067 assert!(
2068 fb.iter().any(|&p| p != 0),
2069 "expected at least one non-zero framebuffer entry"
2070 );
2071 assert!(
2073 zb.iter().any(|&z| z.is_finite()),
2074 "expected at least one finite zbuffer entry"
2075 );
2076 }
2077
2078 #[test]
2079 fn draw_sprite_returns_zero_for_culled_sprite() {
2080 let cam = oracle_sprite_front_camera();
2081 let s = Sprite::axis_aligned(cube_kv6(), [1020.0 - 500.0, 1050.0, 175.0]);
2082 let (mut fb, mut zb) = alloc_target();
2083 let mut target = make_target(&mut fb, &mut zb);
2084 let lighting = SpriteLighting::default_oracle();
2085 assert_eq!(
2086 draw_sprite(&mut target, &cam, &oracle_settings(), &lighting, &s),
2087 0
2088 );
2089 assert!(fb.iter().all(|&p| p == 0));
2090 }
2091
2092 #[test]
2097 fn update_reflects_nolighta_lanes_match() {
2098 let s = Sprite::axis_aligned(empty_kv6(), [1050.0, 1050.0, 175.0]);
2099 let lighting = SpriteLighting::default_oracle();
2100 let (cm, ca) = update_reflects(&s, &lighting);
2101 assert_eq!(ca, 0, "kv6coladd must be zero (no fog)");
2102 for (k, e) in cm.iter().enumerate() {
2103 let l0 = (e & 0xffff) as u16;
2104 let l1 = ((e >> 16) & 0xffff) as u16;
2105 let l2 = ((e >> 32) & 0xffff) as u16;
2106 let l3 = ((e >> 48) & 0xffff) as u16;
2107 assert_eq!(l0, l1, "kv6colmul[{k}] lane0 != lane1");
2108 assert_eq!(l0, l2, "kv6colmul[{k}] lane0 != lane2");
2109 assert_eq!(l0, l3, "kv6colmul[{k}] lane0 != lane3");
2110 }
2111 }
2112
2113 #[test]
2118 fn update_reflects_nolightb_lanes_diverge_for_tinted_kv6col() {
2119 let s = Sprite::axis_aligned(empty_kv6(), [1050.0, 1050.0, 175.0]);
2120 let lighting = SpriteLighting {
2121 kv6col: 0x0040_8040, lightmode: 0,
2123 lights: &[],
2124 };
2125 let (cm, _) = update_reflects(&s, &lighting);
2126 let mut saw_divergence = false;
2129 for e in cm.iter() {
2130 let l0 = (e & 0xffff) as u16;
2131 let l1 = ((e >> 16) & 0xffff) as u16;
2132 let l2 = ((e >> 32) & 0xffff) as u16;
2133 if l0 != l1 || l0 != l2 {
2134 saw_divergence = true;
2135 break;
2136 }
2137 }
2138 assert!(
2139 saw_divergence,
2140 "non-grey kv6col must produce per-channel divergence in some kv6colmul slot"
2141 );
2142 }
2143
2144 #[test]
2151 fn update_reflects_lightmode2_produces_directional_shading() {
2152 let s = Sprite::axis_aligned(empty_kv6(), [100.0, 100.0, 100.0]);
2153 let lights = [LightSrc {
2154 pos: [110.0, 100.0, 100.0],
2155 r2: 100.0,
2156 sc: 16.0,
2157 }];
2158 let lighting = SpriteLighting {
2159 kv6col: DEFAULT_KV6COL,
2160 lightmode: 2,
2161 lights: &lights,
2162 };
2163 let (cm, _) = update_reflects(&s, &lighting);
2164 let mut min_w = u16::MAX;
2168 let mut max_w = 0u16;
2169 for e in cm.iter() {
2170 let l0 = (e & 0xffff) as u16;
2171 min_w = min_w.min(l0);
2172 max_w = max_w.max(l0);
2173 }
2174 assert!(
2175 max_w > min_w + 16,
2176 "lightmode-2 should produce directional shading: min={min_w} max={max_w}"
2177 );
2178 }
2179
2180 #[test]
2184 fn update_reflects_lightmode2_no_lights_falls_back_to_ambient() {
2185 let s = Sprite::axis_aligned(empty_kv6(), [100.0, 100.0, 100.0]);
2186 let lighting = SpriteLighting {
2187 kv6col: DEFAULT_KV6COL,
2188 lightmode: 2,
2189 lights: &[],
2190 };
2191 let (cm, _) = update_reflects(&s, &lighting);
2192 let any_nonzero = cm.iter().any(|&e| e != 0);
2193 assert!(
2194 any_nonzero,
2195 "lightmode-2 with no lights should still emit ambient shading"
2196 );
2197 }
2198}