1use serde::Serialize;
6use std::collections::BTreeMap;
7
8use crate::components::{Light, LightKind, Transform};
9use crate::light_unit::LightUnit;
10use crate::world::{Entity, World};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum LightTableKind {
16 Point,
17 Spot,
18}
19
20impl LightTableKind {
21 pub fn holds(self, light: &Light) -> bool {
23 matches!(
24 (self, light.kind),
25 (LightTableKind::Point, LightKind::Point) | (LightTableKind::Spot, LightKind::Spot { .. })
26 )
27 }
28}
29
30#[derive(Clone, Debug, PartialEq, Serialize)]
36pub struct LightTable {
37 pub kind: LightTableKind,
38 pub stride: u32,
40 pub capacity: u32,
42 pub fields: Vec<LightField>,
43}
44
45#[derive(Clone, Debug, PartialEq, Serialize)]
47pub struct LightField {
48 pub offset: u32,
49 pub value: LightValue,
50}
51
52#[derive(Clone, Debug, PartialEq, Serialize)]
57#[serde(rename_all = "snake_case")]
58#[non_exhaustive]
59pub enum LightValue {
60 ViewPosition,
62 Range,
64 InverseRangeSquared,
66 InverseFalloffDistance,
69 TranslucencyStrength,
71 TranslucencyExponentScale,
73 Colour,
76 SpecularColour,
80 ViewToCone,
88 ViewBoundingSphere,
95 VerticalConeSphere { margin: f32 },
100 ViewDepthMin,
104 ViewDepthMax,
106 Shadowed,
111 ShadowProjection { floats: u8 },
114 ShadowCentre,
117 ShadowFaces { faces: u8 },
120 Constant { value: f32, why: String },
122 Unknown { floats: u8, name: String, why: String },
125}
126
127impl LightValue {
128 pub fn floats(&self) -> u32 {
130 match self {
131 LightValue::ViewPosition | LightValue::Colour | LightValue::SpecularColour => 3,
132 LightValue::Range
133 | LightValue::InverseRangeSquared
134 | LightValue::InverseFalloffDistance
135 | LightValue::TranslucencyStrength
136 | LightValue::TranslucencyExponentScale
137 | LightValue::ViewDepthMin
138 | LightValue::ViewDepthMax
139 | LightValue::Shadowed
140 | LightValue::Constant { .. } => 1,
141 LightValue::ViewToCone => 16,
142 LightValue::ViewBoundingSphere
143 | LightValue::VerticalConeSphere { .. }
144 | LightValue::ShadowCentre => 4,
145 LightValue::ShadowProjection { floats } | LightValue::Unknown { floats, .. } => {
146 u32::from(*floats)
147 }
148 LightValue::ShadowFaces { faces } => 4 * u32::from(*faces),
149 }
150 }
151
152 fn is_shadow_placement(&self) -> bool {
154 matches!(
155 self,
156 LightValue::ShadowProjection { .. } | LightValue::ShadowCentre | LightValue::ShadowFaces { .. }
157 )
158 }
159}
160
161#[derive(Clone, Debug, PartialEq, Serialize)]
165pub struct LocalShadow {
166 pub projection: Vec<f32>,
169 pub centre: [f32; 4],
172 pub faces: Vec<[f32; 4]>,
175}
176
177#[derive(Clone, Debug, Default, PartialEq)]
180pub struct LocalShadows(pub BTreeMap<Entity, LocalShadow>);
181
182impl LocalShadows {
183 pub fn none() -> Self {
185 Self::default()
186 }
187}
188
189#[derive(Clone, Debug, PartialEq)]
191pub struct PackedLights {
192 pub bytes: Vec<u8>,
194 pub lights: Vec<Entity>,
196 pub left_out: usize,
198 pub unknown: Vec<String>,
201 pub specular_from_colour: usize,
204 pub shadowed: Vec<u32>,
207 pub unshadowed_casters: Vec<Entity>,
210}
211
212type Vec3 = [f64; 3];
213
214struct Cone {
217 right: Vec3,
218 up: Vec3,
219 beam: Vec3,
220 tan_horizontal: f64,
221 tan_vertical: f64,
222 vertical: f64,
223 widest: f64,
224 near: f64,
225}
226
227fn dot(a: Vec3, b: Vec3) -> f64 {
228 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
229}
230
231fn unit(v: Vec3) -> Result<Vec3, String> {
232 let n = dot(v, v).sqrt();
233 if n > 0.0 && n.is_finite() {
234 Ok(v.map(|x| x / n))
235 } else {
236 Err("a degenerate light axis".into())
237 }
238}
239
240impl LightTable {
241 pub fn holds(&self, light: &Light) -> bool {
243 self.kind.holds(light)
244 }
245
246 pub fn verify(&self, element_stride: Option<u32>) -> Result<(), String> {
251 if self.stride == 0 || !self.stride.is_multiple_of(4) {
252 return Err(format!(
253 "a light record of {} bytes, not whole floats",
254 self.stride
255 ));
256 }
257 if let Some(element) = element_stride
258 && element != self.stride
259 {
260 return Err(format!(
261 "light records of {} bytes in a buffer of {element}-byte elements",
262 self.stride
263 ));
264 }
265 if self.capacity == 0 {
266 return Err("a light table that holds no records".into());
267 }
268 let mut spans = Vec::with_capacity(self.fields.len());
269 for f in &self.fields {
270 let floats = f.value.floats();
271 let end = f.offset.checked_add(floats * 4);
272 if !f.offset.is_multiple_of(4) || floats == 0 || end.is_none_or(|end| end > self.stride) {
273 return Err(format!(
274 "light field {:?} at {} ({floats} floats) is not inside the {}-byte record",
275 f.value, f.offset, self.stride
276 ));
277 }
278 match &f.value {
279 LightValue::ViewToCone | LightValue::VerticalConeSphere { .. }
280 if self.kind != LightTableKind::Spot =>
281 {
282 return Err("a cone in a table of point lights".into());
283 }
284 LightValue::VerticalConeSphere { margin } if !(margin.is_finite() && *margin >= 0.0) => {
285 return Err(format!(
286 "a cone sphere's margin {margin} is not finite and non-negative"
287 ));
288 }
289 LightValue::Constant { value, why } if !value.is_finite() || why.trim().is_empty() => {
290 return Err(format!(
291 "light constant {value} at {} is not finite or says not why",
292 f.offset
293 ));
294 }
295 LightValue::Unknown { name, why, .. } if name.trim().is_empty() || why.trim().is_empty() => {
296 return Err(format!(
297 "unknown light field at {} has no name or says not why",
298 f.offset
299 ));
300 }
301 _ => {}
302 }
303 if f.value.is_shadow_placement() && !self.reads_shadowed() {
304 return Err(format!(
305 "light field {:?} at {} places a shadow in a table that does not say which lights are shadowed",
306 f.value, f.offset
307 ));
308 }
309 spans.push((f.offset, f.offset + floats * 4));
310 }
311 spans.sort_unstable();
312 if let Some(pair) = spans.windows(2).find(|p| p[1].0 < p[0].1) {
313 return Err(format!(
314 "light fields overlap: bytes {}..{} and {}..{}",
315 pair[0].0, pair[0].1, pair[1].0, pair[1].1
316 ));
317 }
318 Ok(())
319 }
320
321 pub fn reads_shadowed(&self) -> bool {
323 self.fields.iter().any(|f| f.value == LightValue::Shadowed)
324 }
325
326 pub fn pack(
338 &self,
339 w: &World,
340 view: &[[f32; 4]; 4],
341 shadows: &LocalShadows,
342 ) -> Result<PackedLights, String> {
343 self.verify(None)?;
344 let unit_value = w.resource::<LightUnit>().map(|u| f64::from(u.candela_per_unit));
345 let row = |r: usize| view[r].map(f64::from);
346 let to_view = |p: Vec3| -> Vec3 {
347 [0, 1, 2].map(|r| {
348 let v = row(r);
349 v[0] * p[0] + v[1] * p[1] + v[2] * p[2] + v[3]
350 })
351 };
352 let turn = |d: Vec3| -> Vec3 {
353 [0, 1, 2].map(|r| {
354 let v = row(r);
355 v[0] * d[0] + v[1] * d[1] + v[2] * d[2]
356 })
357 };
358 let mut lights = Vec::new();
360 for (e, light) in w.iter::<Light>() {
361 if !self.holds(light) {
362 continue;
363 }
364 let t = w
365 .get::<Transform>(e)
366 .ok_or_else(|| format!("{e:?}: a light without a core.transform"))?;
367 let at = to_view(t.cols[3].map(f64::from));
368 lights.push((dot(at, at), e, light, t, at));
369 }
370 lights.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
371 let left_out = lights.len().saturating_sub(self.capacity as usize);
372 lights.truncate(self.capacity as usize);
373 let mut out = PackedLights {
374 bytes: vec![0; lights.len() * self.stride as usize],
375 lights: Vec::with_capacity(lights.len()),
376 left_out,
377 unknown: Vec::new(),
378 specular_from_colour: 0,
379 shadowed: Vec::new(),
380 unshadowed_casters: Vec::new(),
381 };
382 let reads_shadowed = self.reads_shadowed();
383 let reads_specular = self.fields.iter().any(|f| f.value == LightValue::SpecularColour);
384 for f in &self.fields {
385 if let LightValue::Unknown { name, .. } = &f.value {
386 out.unknown.push(name.clone());
387 }
388 }
389 let colour = |color: [f32; 3], intensity: f32| -> Result<Vec<f64>, String> {
390 let unit = unit_value
391 .ok_or("the world's lights have no core.light_unit: a light record's colour has no value")?;
392 Ok(color
393 .iter()
394 .map(|c| f64::from(*c) * f64::from(intensity) / unit)
395 .collect())
396 };
397 for (n, (_, e, light, t, at)) in lights.into_iter().enumerate() {
398 if reads_specular && light.specular.is_none() {
399 out.specular_from_colour += 1;
400 }
401 let shadow = shadows.0.get(&e).filter(|_| reads_shadowed);
402 if shadow.is_some() && light.cast_shadows == Some(false) {
403 return Err(format!("{e:?}: a shadow for a light that casts none"));
404 }
405 if shadow.is_some() {
406 out.shadowed.push(n as u32);
407 } else if light.cast_shadows == Some(true) {
408 out.unshadowed_casters.push(e);
409 }
410 let range = light
411 .range
412 .map(f64::from)
413 .filter(|r| r.is_finite() && *r > 0.0)
414 .ok_or_else(|| format!("{e:?}: a light with no range, which a light record holds"))?;
415 let cone = match light.kind {
417 LightKind::Spot { outer_cone, .. } => {
418 let frustum = w.get::<crate::SpotFrustum>(e);
419 if let Some(f) = frustum {
420 <crate::SpotFrustum as crate::Component>::verify(f, e, w)?;
421 }
422 let outer = f64::from(outer_cone);
423 let (horizontal, vertical, near) = frustum.map_or((outer, outer, 0.0), |f| {
424 (
425 f64::from(f.horizontal_half_angle),
426 f64::from(f.vertical_half_angle),
427 f64::from(f.near),
428 )
429 });
430 let (tan_horizontal, tan_vertical) = (horizontal.tan(), vertical.tan());
431 for tan in [tan_horizontal, tan_vertical] {
432 if !(tan.is_finite() && tan > 0.0) {
433 return Err(format!(
434 "{e:?}: a spot's cone {horizontal} by {vertical} has no tangent above 0"
435 ));
436 }
437 }
438 if near >= range {
439 return Err(format!(
440 "{e:?}: a spot's near plane {near} not before its range {range}"
441 ));
442 }
443 let axis = |c: usize| unit(turn(t.cols[c].map(f64::from)));
444 let beam = axis(2)?.map(|x| -x);
445 Some(Cone {
446 right: axis(0)?,
447 up: axis(1)?,
448 beam,
449 tan_horizontal,
450 tan_vertical,
451 vertical,
452 widest: match frustum {
454 Some(_) => tan_horizontal.hypot(tan_vertical).atan(),
455 None => outer,
456 },
457 near,
458 })
459 }
460 _ => None,
461 };
462 let mut values: Vec<(u32, Vec<f64>)> = Vec::with_capacity(self.fields.len());
463 for f in &self.fields {
464 let v: Vec<f64> = match &f.value {
465 LightValue::ViewPosition => at.to_vec(),
466 LightValue::Range => vec![range],
467 LightValue::InverseRangeSquared => vec![1.0 / (range * range)],
468 LightValue::InverseFalloffDistance => match w.get::<crate::LightFalloff>(e) {
469 Some(falloff) => vec![f64::from(
470 falloff
471 .inverse_distance()
472 .map_err(|why| format!("{e:?}: {why}"))?,
473 )],
474 None => {
475 let name = w
476 .get::<crate::Name>(e)
477 .map(|n| n.0.as_str())
478 .or_else(|| w.get::<crate::Source>(e).map(|s| s.0.as_str()));
479 out.unknown.push(format!("light {e:?}{}: source falloff distance not stated; inverse falloff distance supplied as zero",
480 name.map(|n| format!(" ({n})")).unwrap_or_default()));
481 vec![0.]
482 }
483 },
484 LightValue::TranslucencyStrength | LightValue::TranslucencyExponentScale => {
485 let source = w
486 .get::<crate::LightTranslucency>(e)
487 .ok_or_else(|| format!("{e:?}: light translucency parameters are not stated"))?;
488 <crate::LightTranslucency as crate::Component>::verify(source, e, w)?;
489 vec![f64::from(if f.value == LightValue::TranslucencyStrength {
490 source.strength
491 } else {
492 source.exponent_scale
493 })]
494 }
495 LightValue::Colour => colour(light.color, light.intensity)?,
496 LightValue::SpecularColour => match &light.specular {
497 Some(s) => colour(s.color, s.intensity)?,
498 None => colour(light.color, light.intensity)?,
499 },
500 LightValue::ViewToCone => {
501 let c = cone.as_ref().ok_or("a cone for a light that is no spot")?;
502 let depth = range / (range - c.near);
504 let mut m = Vec::with_capacity(16);
505 for (axis, scale, shift) in [
506 (c.right, 1.0 / c.tan_horizontal, 0.0),
507 (c.up, 1.0 / c.tan_vertical, 0.0),
508 (c.beam, depth, c.near),
509 (c.beam, 1.0, 0.0),
510 ] {
511 m.extend(axis.map(|x| x * scale));
512 m.push((-dot(axis, at) - shift) * scale);
513 }
514 m
515 }
516 LightValue::ViewBoundingSphere => match &cone {
517 None => vec![at[0], at[1], at[2], range],
518 Some(c) => {
519 let outer = c.widest;
520 let radius = if outer <= std::f64::consts::FRAC_PI_4 {
521 range / (2.0 * outer.cos().powi(2))
522 } else {
523 range * outer.tan()
524 };
525 let along = if outer <= std::f64::consts::FRAC_PI_4 {
526 radius
527 } else {
528 range
529 };
530 vec![
531 at[0] + c.beam[0] * along,
532 at[1] + c.beam[1] * along,
533 at[2] + c.beam[2] * along,
534 radius,
535 ]
536 }
537 },
538 LightValue::VerticalConeSphere { margin } => {
539 let c = cone.as_ref().ok_or("a cone sphere for a light that is no spot")?;
540 let along = range / (2.0 * c.vertical.cos().powi(2));
541 vec![
542 at[0] + c.beam[0] * along,
543 at[1] + c.beam[1] * along,
544 at[2] + c.beam[2] * along,
545 along + f64::from(*margin),
546 ]
547 }
548 LightValue::ViewDepthMin | LightValue::ViewDepthMax => {
549 let (least, most) = match &cone {
550 None => (at[2] - range, at[2] + range),
551 Some(c) => {
552 let base = at[2] + c.beam[2] * range;
553 let reach = range
554 * (c.tan_horizontal * c.right[2].abs() + c.tan_vertical * c.up[2].abs());
555 (at[2].min(base - reach), at[2].max(base + reach))
556 }
557 };
558 vec![if f.value == LightValue::ViewDepthMin {
559 least
560 } else {
561 most
562 }]
563 }
564 LightValue::Shadowed => vec![if shadow.is_some() { 1.0 } else { 0.0 }],
565 LightValue::ShadowProjection { floats } => match shadow {
566 Some(s) => s
567 .projection
568 .get(..usize::from(*floats))
569 .ok_or_else(|| {
570 format!("{e:?}: a shadow with fewer than {floats} projection terms")
571 })?
572 .iter()
573 .map(|x| f64::from(*x))
574 .collect(),
575 None => vec![0.0; usize::from(*floats)],
576 },
577 LightValue::ShadowCentre => match shadow {
578 Some(s) => s.centre.map(f64::from).to_vec(),
579 None => vec![0.0; 4],
580 },
581 LightValue::ShadowFaces { faces } => match shadow {
582 Some(s) => s
583 .faces
584 .get(..usize::from(*faces))
585 .ok_or_else(|| format!("{e:?}: a shadow with fewer than {faces} faces"))?
586 .iter()
587 .flatten()
588 .map(|x| f64::from(*x))
589 .collect(),
590 None => vec![0.0; 4 * usize::from(*faces)],
591 },
592 LightValue::Constant { value, .. } => vec![f64::from(*value)],
593 LightValue::Unknown { floats, .. } => vec![0.0; usize::from(*floats)],
594 };
595 values.push((f.offset, v));
596 }
597 let record = &mut out.bytes[n * self.stride as usize..(n + 1) * self.stride as usize];
598 for (offset, v) in values {
599 for (k, x) in v.into_iter().enumerate() {
600 let x = x as f32;
601 if !x.is_finite() {
602 return Err(format!("{e:?}: a light record value at {offset} is not finite"));
603 }
604 let byte = offset as usize + 4 * k;
605 record[byte..byte + 4].copy_from_slice(&x.to_le_bytes());
606 }
607 }
608 out.lights.push(e);
609 }
610 Ok(out)
611 }
612}
613
614#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
617pub struct ListBits {
618 pub offset: u32,
619 pub shift: u8,
620 pub bits: u8,
621}
622
623impl ListBits {
624 pub fn max(&self) -> u32 {
626 if self.bits >= 32 {
627 u32::MAX
628 } else {
629 (1u32 << self.bits) - 1
630 }
631 }
632
633 fn mask(&self) -> u32 {
634 self.max() << self.shift
635 }
636
637 fn verify(&self, stride: u32, what: &str) -> Result<(), String> {
638 let end = u32::from(self.shift) + u32::from(self.bits);
639 if self.bits == 0 || end > 32 || !self.offset.is_multiple_of(4) || self.offset + 4 > stride {
640 return Err(format!(
641 "{what}: bits {}..{end} of the word at {} are not inside a {stride}-byte record",
642 self.shift, self.offset
643 ));
644 }
645 Ok(())
646 }
647
648 fn write(&self, record: &mut [u8], value: u32) {
649 let at = self.offset as usize;
650 let mut word = u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]]);
651 word = (word & !self.mask()) | ((value & self.max()) << self.shift);
652 record[at..at + 4].copy_from_slice(&word.to_le_bytes());
653 }
654}
655
656#[derive(Clone, Debug, PartialEq, Serialize)]
658pub struct ListConstant {
659 pub bits: ListBits,
660 pub value: u32,
661 pub why: String,
662}
663
664#[derive(Clone, Debug, PartialEq, Serialize)]
667pub struct ListUnknown {
668 pub bits: ListBits,
669 pub name: String,
670 pub why: String,
671}
672
673#[derive(Clone, Debug, PartialEq, Serialize)]
680#[serde(rename_all = "snake_case")]
681pub enum LightListPart {
682 Lookup(LightLookup),
683 Indices(LightIndices),
684}
685
686#[derive(Clone, Debug, PartialEq, Serialize)]
688pub struct LightLookup {
689 pub stride: u32,
690 pub capacity: u32,
691 pub first: ListBits,
693 pub counts: Vec<(LightTableKind, ListBits)>,
695 #[serde(skip_serializing_if = "Vec::is_empty")]
698 pub count_copies: Vec<(LightTableKind, ListBits)>,
699 pub constants: Vec<ListConstant>,
700 pub unknown: Vec<ListUnknown>,
701}
702
703#[derive(Clone, Debug, PartialEq, Serialize)]
705pub struct LightIndices {
706 pub stride: u32,
707 pub capacity: u32,
708 pub index: ListBits,
710 pub slices: ListBits,
713 #[serde(skip_serializing_if = "Option::is_none")]
717 pub tile_pass: Option<ListBits>,
718 pub constants: Vec<ListConstant>,
719 pub unknown: Vec<ListUnknown>,
720}
721
722#[derive(Clone, Debug, PartialEq)]
724pub struct PackedLightLists {
725 pub lookup: Vec<u8>,
726 pub indices: Vec<u8>,
727 pub unknown: Vec<String>,
729}
730
731fn verify_record(
733 stride: u32,
734 capacity: u32,
735 fields: &[(ListBits, &str)],
736 constants: &[ListConstant],
737 unknown: &[ListUnknown],
738 what: &str,
739) -> Result<(), String> {
740 if stride == 0 || !stride.is_multiple_of(4) {
741 return Err(format!(
742 "a light {what} record of {stride} bytes, not whole words"
743 ));
744 }
745 if capacity == 0 {
746 return Err(format!("a light {what} that holds no records"));
747 }
748 let mut all: Vec<(ListBits, String)> = fields.iter().map(|(b, n)| (*b, (*n).to_owned())).collect();
749 for c in constants {
750 if c.why.trim().is_empty() || c.value > c.bits.max() {
751 return Err(format!(
752 "light {what} constant {} does not fit its bits or says not why",
753 c.value
754 ));
755 }
756 all.push((c.bits, format!("constant {}", c.value)));
757 }
758 for u in unknown {
759 if u.name.trim().is_empty() || u.why.trim().is_empty() {
760 return Err(format!(
761 "an unknown light {what} field has no name or says not why"
762 ));
763 }
764 all.push((u.bits, u.name.clone()));
765 }
766 for (i, (b, name)) in all.iter().enumerate() {
767 b.verify(stride, &format!("light {what} {name}"))?;
768 if let Some((_, other)) = all[..i]
769 .iter()
770 .find(|(o, _)| o.offset == b.offset && o.mask() & b.mask() != 0)
771 {
772 return Err(format!("light {what} fields {other} and {name} overlap"));
773 }
774 }
775 Ok(())
776}
777
778impl LightListPart {
779 pub fn verify(&self, element_stride: Option<u32>) -> Result<(), String> {
783 let stride = match self {
784 LightListPart::Lookup(l) => {
785 let mut fields = vec![(l.first, "first entry")];
786 let mut kinds = Vec::new();
787 for (kind, bits) in &l.counts {
788 if kinds.contains(kind) {
789 return Err(format!("a light lookup that counts {kind:?} lights twice"));
790 }
791 kinds.push(*kind);
792 fields.push((*bits, "count"));
793 }
794 if l.counts.is_empty() {
795 return Err("a light lookup that counts no lights".into());
796 }
797 for (kind, bits) in &l.count_copies {
798 if !kinds.contains(kind) {
799 return Err(format!("a light lookup copies an unlisted {kind:?} count"));
800 }
801 fields.push((*bits, "count copy"));
802 }
803 verify_record(l.stride, l.capacity, &fields, &l.constants, &l.unknown, "lookup")?;
804 l.stride
805 }
806 LightListPart::Indices(x) => {
807 let mut fields = vec![(x.index, "index"), (x.slices, "slices")];
808 if let Some(bits) = x.tile_pass {
809 fields.push((bits, "tile pass"));
810 }
811 verify_record(
812 x.stride,
813 x.capacity,
814 &fields,
815 &x.constants,
816 &x.unknown,
817 "index list",
818 )?;
819 x.stride
820 }
821 };
822 match element_stride {
823 Some(element) if element != stride => Err(format!(
824 "light list records of {stride} bytes in a buffer of {element}-byte elements"
825 )),
826 _ => Ok(()),
827 }
828 }
829}
830
831pub fn pack_light_lists(
841 lookup: &LightLookup,
842 indices: &LightIndices,
843 counts: &[(LightTableKind, u32)],
844 shadowed: &[(LightTableKind, u32)],
845 tiles: u32,
846) -> Result<PackedLightLists, String> {
847 LightListPart::Lookup(lookup.clone()).verify(None)?;
848 LightListPart::Indices(indices.clone()).verify(None)?;
849 if tiles == 0 || tiles > lookup.capacity {
850 return Err(format!(
851 "{tiles} tiles for a light lookup of {} records",
852 lookup.capacity
853 ));
854 }
855 let count_of =
856 |kind: LightTableKind| -> u32 { counts.iter().filter(|(k, _)| *k == kind).map(|(_, n)| *n).sum() };
857 let mut marked: Vec<(LightTableKind, u32)> = shadowed.to_vec();
858 marked.sort_unstable();
859 marked.dedup();
860 for (kind, index) in &marked {
861 if *index >= count_of(*kind) {
862 return Err(format!(
863 "shadowed {kind:?} record {index} is not among the {} the lists count",
864 count_of(*kind)
865 ));
866 }
867 }
868 if !marked.is_empty() && indices.tile_pass.is_none() {
869 return Err("the light lists have no tile-pass bit to leave a shadowed light out with".into());
870 }
871 let shadowed_of =
872 |kind: LightTableKind| -> u32 { marked.iter().filter(|(k, _)| *k == kind).count() as u32 };
873 let mut entries = Vec::new();
874 for (kind, bits) in &lookup.counts {
875 let n = count_of(*kind);
876 if n > bits.max() || n.saturating_sub(1) > indices.index.max() {
877 return Err(format!("{n} {kind:?} lights do not fit the light lists' bits"));
878 }
879 entries.extend((0..n).map(|i| (*kind, i)));
880 }
881 for (kind, bits) in &lookup.count_copies {
882 let n = count_of(*kind);
883 if n > bits.max() {
884 return Err(format!(
885 "{n} {kind:?} lights do not fit the lookup's copied count bits"
886 ));
887 }
888 }
889 if entries.len() as u64 > u64::from(indices.capacity) {
890 return Err(format!(
891 "{} light list entries for an index list of {}",
892 entries.len(),
893 indices.capacity
894 ));
895 }
896 let mut out = PackedLightLists {
897 lookup: vec![0; tiles as usize * lookup.stride as usize],
898 indices: vec![0; entries.len() * indices.stride as usize],
899 unknown: lookup
900 .unknown
901 .iter()
902 .chain(&indices.unknown)
903 .map(|u| u.name.clone())
904 .collect(),
905 };
906 for record in out.lookup.chunks_exact_mut(lookup.stride as usize) {
907 lookup.first.write(record, 0);
908 for (kind, bits) in &lookup.counts {
909 bits.write(record, count_of(*kind));
910 }
911 for (kind, bits) in &lookup.count_copies {
912 bits.write(record, count_of(*kind) - shadowed_of(*kind));
913 }
914 for c in &lookup.constants {
915 c.bits.write(record, c.value);
916 }
917 }
918 for (record, (kind, index)) in out.indices.chunks_exact_mut(indices.stride as usize).zip(entries) {
919 indices.index.write(record, index);
920 indices.slices.write(record, indices.slices.max());
921 if let Some(bits) = indices.tile_pass {
922 bits.write(record, u32::from(!marked.contains(&(kind, index))));
923 }
924 for c in &indices.constants {
925 c.bits.write(record, c.value);
926 }
927 }
928 Ok(out)
929}
930
931#[cfg(test)]
932mod tests {
933 use super::*;
934 use crate::World;
935 use crate::components::LightSpecular;
936
937 fn floats(bytes: &[u8]) -> Vec<f32> {
938 bytes
939 .as_chunks::<4>()
940 .0
941 .iter()
942 .map(|b| f32::from_le_bytes(*b))
943 .collect()
944 }
945
946 fn close(got: f32, want: f64, what: &str) {
947 assert!(
948 (f64::from(got) - want).abs() <= 1e-5 * want.abs().max(1.0),
949 "{what}: {got}, worked by hand {want}"
950 );
951 }
952
953 const VIEW: [[f32; 4]; 4] = [
956 [1.0, 0.0, 0.0, 0.0],
957 [0.0, 1.0, 0.0, 0.0],
958 [0.0, 0.0, -1.0, 10.0],
959 [0.0, 0.0, 0.0, 1.0],
960 ];
961
962 fn table(kind: LightTableKind, stride: u32, fields: Vec<(u32, LightValue)>) -> LightTable {
963 LightTable {
964 kind,
965 stride,
966 capacity: 4,
967 fields: fields
968 .into_iter()
969 .map(|(offset, value)| LightField { offset, value })
970 .collect(),
971 }
972 }
973
974 #[test]
976 fn source_falloff_and_translucency_pack_without_changing_range_or_radius() {
977 let mut w = World::new();
978 let e = w.spawn();
979 w.insert(e, crate::Name("measured light".into()));
980 w.insert(
981 e,
982 Transform {
983 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0.; 3]],
984 },
985 );
986 w.insert(
987 e,
988 Light {
989 kind: LightKind::Point,
990 color: [1.; 3],
991 intensity: 1.,
992 range: Some(20.),
993 radius: Some(3.),
994 cast_shadows: None,
995 specular: None,
996 },
997 );
998 w.insert(
999 e,
1000 crate::LightTranslucency {
1001 strength: 0.25,
1002 exponent_scale: 0.01,
1003 },
1004 );
1005 let t = table(
1006 LightTableKind::Point,
1007 16,
1008 vec![
1009 (0, LightValue::InverseFalloffDistance),
1010 (4, LightValue::TranslucencyStrength),
1011 (8, LightValue::TranslucencyExponentScale),
1012 (12, LightValue::Range),
1013 ],
1014 );
1015 let missing = t.pack(&w, &VIEW, &LocalShadows::none()).unwrap();
1016 assert_eq!(floats(&missing.bytes), vec![0., 0.25, 0.01, 20.]);
1017 assert_eq!(missing.unknown.len(), 1);
1018 assert!(missing.unknown[0].contains("measured light"));
1019 for distance in [8., 1.5, 0.5] {
1020 w.insert(e, crate::LightFalloff { distance });
1021 let packed = t.pack(&w, &VIEW, &LocalShadows::none()).unwrap();
1022 assert!(packed.unknown.is_empty());
1023 assert_eq!(floats(&packed.bytes), vec![distance.recip(), 0.25, 0.01, 20.]);
1024 assert_eq!(w.get::<Light>(e).unwrap().radius, Some(3.));
1025 }
1026 for distance in [0., -1., f32::NAN, f32::INFINITY, f32::from_bits(1)] {
1027 w.insert(e, crate::LightFalloff { distance });
1028 assert!(
1029 t.pack(&w, &VIEW, &LocalShadows::none())
1030 .unwrap_err()
1031 .contains("falloff distance")
1032 );
1033 }
1034 }
1035
1036 #[test]
1042 fn a_shadowed_light_packs_its_flag_and_its_place() {
1043 let mut w = World::new();
1044 w.insert_resource(LightUnit {
1045 candela_per_unit: 1.0,
1046 });
1047 let place = |w: &mut World, z: f32, cast_shadows: Option<bool>| {
1048 let e = w.spawn();
1049 w.insert(
1050 e,
1051 Transform {
1052 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0., 0., z]],
1053 },
1054 );
1055 w.insert(
1056 e,
1057 Light {
1058 kind: LightKind::Point,
1059 color: [1.0; 3],
1060 intensity: 1.0,
1061 range: Some(4.0),
1062 cast_shadows,
1063 radius: None,
1064 specular: None,
1065 },
1066 );
1067 e
1068 };
1069 let first = place(&mut w, 3., Some(true));
1070 let second = place(&mut w, 2., Some(true));
1071 let quiet = place(&mut w, 1., None);
1072 let t = table(
1073 LightTableKind::Point,
1074 72,
1075 vec![
1076 (0, LightValue::ViewPosition),
1077 (12, LightValue::Shadowed),
1078 (16, LightValue::ShadowProjection { floats: 2 }),
1079 (24, LightValue::ShadowCentre),
1080 (40, LightValue::ShadowFaces { faces: 2 }),
1081 ],
1082 );
1083 assert_eq!(t.verify(None), Ok(()));
1084 let record = |packed: &PackedLights, e: Entity| -> Vec<f32> {
1085 let i = packed.lights.iter().position(|l| *l == e).unwrap();
1086 floats(&packed.bytes[72 * i..72 * (i + 1)])
1087 };
1088 let none = t.pack(&w, &VIEW, &LocalShadows::none()).unwrap();
1089 assert_eq!(none.lights, vec![first, second, quiet]);
1090 assert!(none.shadowed.is_empty());
1091 assert_eq!(none.unshadowed_casters, vec![first, second]);
1092 for e in [first, second, quiet] {
1093 assert!(record(&none, e)[3..].iter().all(|x| *x == 0.0));
1094 }
1095 let shadow = LocalShadow {
1096 projection: vec![1.001, -0.008, 7.0],
1097 centre: [-34.25, 8.0, -0.5, 1.0],
1098 faces: vec![[0.25, -0.25, 0.5, 0.5], [0.25, -0.25, 1.5, 0.5]],
1099 };
1100 let shadows = LocalShadows([(second, shadow.clone())].into());
1101 let one = t.pack(&w, &VIEW, &shadows).unwrap();
1102 assert_eq!(one.shadowed, vec![1]);
1103 assert_eq!(one.unshadowed_casters, vec![first]);
1104 assert_eq!(
1105 record(&one, second)[3..],
1106 [
1107 1.0, 1.001, -0.008, -34.25, 8.0, -0.5, 1.0, 0.25, -0.25, 0.5, 0.5, 0.25, -0.25, 1.5, 0.5
1108 ]
1109 );
1110 assert!(record(&one, first)[3..].iter().all(|x| *x == 0.0));
1111
1112 w.get_mut::<Light>(second).unwrap().cast_shadows = Some(false);
1113 assert!(t.pack(&w, &VIEW, &shadows).unwrap_err().contains("casts none"));
1114 w.get_mut::<Light>(second).unwrap().cast_shadows = Some(true);
1115 let mut short = shadow;
1116 short.faces.pop();
1117 let refused = t.pack(&w, &VIEW, &LocalShadows([(second, short)].into()));
1118 assert!(refused.unwrap_err().contains("fewer than 2 faces"));
1119 let unsaid = table(LightTableKind::Point, 16, vec![(0, LightValue::ShadowCentre)]);
1120 assert!(unsaid.verify(None).unwrap_err().contains("does not say"));
1121 }
1122
1123 #[test]
1127 fn a_point_light_packs_into_its_record() {
1128 let mut w = World::new();
1129 let place = |w: &mut World, at: [f32; 3]| {
1130 let e = w.spawn();
1131 w.insert(
1132 e,
1133 Transform {
1134 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], at],
1135 },
1136 );
1137 w.insert(
1138 e,
1139 Light {
1140 kind: LightKind::Point,
1141 color: [0.5, 0.25, 1.0],
1142 intensity: 8.0,
1143 range: Some(4.0),
1144 cast_shadows: None,
1145 radius: None,
1146 specular: None,
1147 },
1148 );
1149 e
1150 };
1151 let near = place(&mut w, [1., 2., 3.]);
1152 place(&mut w, [1., 2., -30.]);
1153 let mut t = table(
1154 LightTableKind::Point,
1155 48,
1156 vec![
1157 (0, LightValue::ViewPosition),
1158 (12, LightValue::InverseRangeSquared),
1159 (16, LightValue::Colour),
1160 (
1161 28,
1162 LightValue::Unknown {
1163 floats: 2,
1164 name: "Wrap".into(),
1165 why: "test".into(),
1166 },
1167 ),
1168 (36, LightValue::Range),
1169 (
1170 40,
1171 LightValue::Constant {
1172 value: 0.0,
1173 why: "test".into(),
1174 },
1175 ),
1176 ],
1177 );
1178 assert!(t.pack(&w, &VIEW, &LocalShadows::none()).is_err());
1180 w.insert_resource(LightUnit {
1181 candela_per_unit: 2.0,
1182 });
1183 t.capacity = 1;
1184 let packed = t.pack(&w, &VIEW, &LocalShadows::none()).unwrap();
1185 assert_eq!((packed.lights.clone(), packed.left_out), (vec![near], 1));
1186 assert_eq!(packed.unknown, vec!["Wrap".to_owned()]);
1187 let f = floats(&packed.bytes);
1188 let want = [1.0, 2.0, 7.0, 1.0 / 16.0, 2.0, 1.0, 4.0, 0.0, 0.0, 4.0, 0.0];
1189 for (i, v) in want.into_iter().enumerate() {
1190 close(f[i], v, &format!("float {i}"));
1191 }
1192 assert_eq!(f.len(), 12);
1193 assert_eq!(f[11], 0.0, "a byte no field covers is zero");
1194 }
1195
1196 #[test]
1200 fn a_light_packs_its_own_specular_and_one_without_packs_its_colour() {
1201 let mut w = World::new();
1202 w.insert_resource(LightUnit {
1203 candela_per_unit: 2.0,
1204 });
1205 let place = |w: &mut World, z: f32, specular: Option<LightSpecular>| {
1206 let e = w.spawn();
1207 w.insert(
1208 e,
1209 Transform {
1210 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0., 0., z]],
1211 },
1212 );
1213 w.insert(
1214 e,
1215 Light {
1216 kind: LightKind::Point,
1217 color: [0.5, 0.25, 1.0],
1218 intensity: 8.0,
1219 range: Some(4.0),
1220 cast_shadows: None,
1221 radius: None,
1222 specular,
1223 },
1224 );
1225 e
1226 };
1227 let own = place(
1228 &mut w,
1229 0.0,
1230 Some(LightSpecular {
1231 color: [1.0, 0.5, 0.0],
1232 intensity: 4.0,
1233 }),
1234 );
1235 let plain = place(&mut w, -5.0, None);
1236 let t = table(
1237 LightTableKind::Point,
1238 24,
1239 vec![(0, LightValue::Colour), (12, LightValue::SpecularColour)],
1240 );
1241 let packed = t.pack(&w, &VIEW, &LocalShadows::none()).unwrap();
1242 assert_eq!(packed.lights, vec![own, plain]);
1243 assert_eq!(packed.specular_from_colour, 1);
1244 let f = floats(&packed.bytes);
1245 let want = [2.0, 1.0, 4.0, 2.0, 1.0, 0.0, 2.0, 1.0, 4.0, 2.0, 1.0, 4.0];
1246 for (i, v) in want.into_iter().enumerate() {
1247 close(f[i], v, &format!("float {i}"));
1248 }
1249 let colour_only = table(LightTableKind::Point, 12, vec![(0, LightValue::Colour)]);
1251 assert_eq!(
1252 colour_only
1253 .pack(&w, &VIEW, &LocalShadows::none())
1254 .unwrap()
1255 .specular_from_colour,
1256 0
1257 );
1258 }
1259
1260 #[test]
1265 fn a_spot_packs_its_cone_sphere_and_depths() {
1266 let mut w = World::new();
1267 w.insert_resource(LightUnit {
1268 candela_per_unit: 1.0,
1269 });
1270 let e = w.spawn();
1271 w.insert(
1272 e,
1273 Transform {
1274 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0., 0., 0.]],
1275 },
1276 );
1277 w.insert(
1278 e,
1279 Light {
1280 kind: LightKind::Spot {
1281 inner_cone: 0.4,
1282 outer_cone: std::f32::consts::FRAC_PI_6,
1283 },
1284 color: [1.0, 1.0, 1.0],
1285 intensity: 1.0,
1286 range: Some(6.0),
1287 cast_shadows: None,
1288 radius: None,
1289 specular: None,
1290 },
1291 );
1292 let t = table(
1293 LightTableKind::Spot,
1294 112,
1295 vec![
1296 (0, LightValue::ViewToCone),
1297 (64, LightValue::ViewBoundingSphere),
1298 (80, LightValue::ViewDepthMin),
1299 (84, LightValue::ViewDepthMax),
1300 ],
1301 );
1302 let f = floats(&t.pack(&w, &VIEW, &LocalShadows::none()).unwrap().bytes);
1303 let cot = 1.0 / (std::f64::consts::FRAC_PI_6).tan();
1304 let want = [
1306 cot, 0.0, 0.0, 0.0, 0.0, cot, 0.0, 0.0, 0.0, 0.0, 1.0, -10.0, 0.0, 0.0, 1.0, -10.0, 0.0, 0.0, 14.0, 4.0, 10.0, 16.0, ];
1313 for (i, v) in want.into_iter().enumerate() {
1314 close(f[i], v, &format!("float {i}"));
1315 }
1316 let p = [3.0 * (std::f64::consts::FRAC_PI_6).tan(), 0.0, 13.0, 1.0];
1318 let row = |r: usize| (0..4).map(|k| f64::from(f[4 * r + k]) * p[k]).sum::<f64>();
1319 close((row(0) / row(3)) as f32, 1.0, "the cone's edge");
1320 }
1321
1322 #[test]
1329 fn a_spot_frustum_shapes_its_cone_near_plane_and_spheres() {
1330 let mut w = World::new();
1331 w.insert_resource(LightUnit {
1332 candela_per_unit: 1.0,
1333 });
1334 let e = w.spawn();
1335 w.insert(
1336 e,
1337 Transform {
1338 cols: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [0., 0., 0.]],
1339 },
1340 );
1341 let spot = Light {
1342 kind: LightKind::Spot {
1343 inner_cone: 0.3,
1344 outer_cone: 0.6,
1345 },
1346 color: [1.0, 1.0, 1.0],
1347 intensity: 1.0,
1348 range: Some(6.0),
1349 cast_shadows: None,
1350 radius: None,
1351 specular: None,
1352 };
1353 w.insert(e, spot.clone());
1354 let frustum = crate::SpotFrustum {
1355 horizontal_half_angle: std::f32::consts::FRAC_PI_4,
1356 vertical_half_angle: std::f32::consts::FRAC_PI_6,
1357 near: 2.0,
1358 };
1359 w.insert(e, frustum);
1360 let t = table(
1361 LightTableKind::Spot,
1362 128,
1363 vec![
1364 (0, LightValue::ViewToCone),
1365 (64, LightValue::ViewBoundingSphere),
1366 (80, LightValue::VerticalConeSphere { margin: 0.001 }),
1367 (96, LightValue::ViewDepthMin),
1368 (100, LightValue::ViewDepthMax),
1369 ],
1370 );
1371 let f = floats(&t.pack(&w, &VIEW, &LocalShadows::none()).unwrap().bytes);
1372 let corners = (4.0f64 / 3.0).sqrt();
1373 let want = [
1374 1.0,
1375 0.0,
1376 0.0,
1377 0.0, 0.0,
1379 3f64.sqrt(),
1380 0.0,
1381 0.0, 0.0,
1383 0.0,
1384 1.5,
1385 -18.0, 0.0,
1387 0.0,
1388 1.0,
1389 -10.0, 0.0,
1391 0.0,
1392 16.0,
1393 6.0 * corners, 0.0,
1395 0.0,
1396 14.0,
1397 4.001, 10.0,
1399 16.0, ];
1401 for (i, v) in want.into_iter().enumerate() {
1402 close(f[i], v, &format!("float {i}"));
1403 }
1404 let row = |r: usize, d: f64| {
1406 (0..4)
1407 .map(|k| f64::from(f[4 * r + k]) * [0.0, 0.0, 10.0 + d, 1.0][k])
1408 .sum::<f64>()
1409 };
1410 assert!(row(2, 1.0) < 0.0 && row(2, 3.0) > 0.0);
1411
1412 for bad in [
1415 crate::SpotFrustum { near: 6.0, ..frustum },
1416 crate::SpotFrustum {
1417 vertical_half_angle: std::f32::consts::FRAC_PI_2,
1418 ..frustum
1419 },
1420 ] {
1421 w.insert(e, bad);
1422 assert!(t.pack(&w, &VIEW, &LocalShadows::none()).is_err(), "{bad:?}");
1423 }
1424 w.insert(e, frustum);
1425 w.insert(
1426 e,
1427 Light {
1428 kind: LightKind::Point,
1429 ..spot
1430 },
1431 );
1432 assert!(<crate::SpotFrustum as crate::Component>::verify(&frustum, e, &w).is_err());
1433 let points = table(
1434 LightTableKind::Point,
1435 16,
1436 vec![(0, LightValue::VerticalConeSphere { margin: 0.0 })],
1437 );
1438 assert!(points.verify(None).is_err());
1439 }
1440
1441 #[test]
1444 fn a_light_table_agrees_with_itself() {
1445 let ok = table(LightTableKind::Point, 16, vec![(0, LightValue::ViewPosition)]);
1446 assert_eq!(ok.verify(Some(16)), Ok(()));
1447 assert!(ok.verify(Some(32)).is_err());
1448 assert!(table(LightTableKind::Point, 14, vec![]).verify(None).is_err());
1449 assert!(
1450 table(LightTableKind::Point, 16, vec![(8, LightValue::ViewPosition)])
1451 .verify(None)
1452 .is_err()
1453 );
1454 assert!(
1455 table(
1456 LightTableKind::Point,
1457 16,
1458 vec![(0, LightValue::ViewPosition), (8, LightValue::Range)]
1459 )
1460 .verify(None)
1461 .is_err()
1462 );
1463 assert!(
1464 table(LightTableKind::Point, 64, vec![(0, LightValue::ViewToCone)])
1465 .verify(None)
1466 .is_err()
1467 );
1468 assert!(
1469 table(
1470 LightTableKind::Point,
1471 16,
1472 vec![(
1473 0,
1474 LightValue::Constant {
1475 value: 0.0,
1476 why: " ".into()
1477 }
1478 )]
1479 )
1480 .verify(None)
1481 .is_err()
1482 );
1483 }
1484
1485 fn bits(offset: u32, shift: u8, bits: u8) -> ListBits {
1486 ListBits { offset, shift, bits }
1487 }
1488
1489 fn lists() -> (LightLookup, LightIndices) {
1493 (
1494 LightLookup {
1495 stride: 12,
1496 capacity: 4,
1497 first: bits(0, 0, 32),
1498 counts: vec![
1499 (LightTableKind::Point, bits(4, 0, 16)),
1500 (LightTableKind::Spot, bits(4, 16, 16)),
1501 ],
1502 count_copies: Vec::new(),
1503 constants: Vec::new(),
1504 unknown: vec![ListUnknown {
1505 bits: bits(8, 0, 32),
1506 name: "word 2".into(),
1507 why: "read only by a debug view".into(),
1508 }],
1509 },
1510 LightIndices {
1511 stride: 4,
1512 capacity: 8,
1513 index: bits(0, 0, 16),
1514 slices: bits(0, 16, 15),
1515 tile_pass: Some(bits(0, 31, 1)),
1516 constants: Vec::new(),
1517 unknown: Vec::new(),
1518 },
1519 )
1520 }
1521
1522 fn words(bytes: &[u8]) -> Vec<u32> {
1523 bytes
1524 .as_chunks::<4>()
1525 .0
1526 .iter()
1527 .map(|w| u32::from_le_bytes(*w))
1528 .collect()
1529 }
1530
1531 #[test]
1534 fn every_light_is_listed_in_every_tile_and_slice() {
1535 let (lookup, indices) = lists();
1536 let packed = pack_light_lists(
1537 &lookup,
1538 &indices,
1539 &[(LightTableKind::Spot, 1), (LightTableKind::Point, 2)],
1540 &[],
1541 3,
1542 )
1543 .unwrap();
1544 assert_eq!(
1545 words(&packed.lookup),
1546 [0, 0x0001_0002, 0, 0, 0x0001_0002, 0, 0, 0x0001_0002, 0]
1547 );
1548 assert_eq!(words(&packed.indices), [0xffff_0000, 0xffff_0001, 0xffff_0000]);
1549 assert_eq!(packed.unknown, ["word 2"]);
1550 }
1551
1552 #[test]
1555 fn copied_counts_keep_nonempty_tiles_and_do_not_duplicate_entries() {
1556 let (mut lookup, indices) = lists();
1557 lookup.unknown.clear();
1558 lookup.count_copies = vec![
1559 (LightTableKind::Point, bits(8, 0, 16)),
1560 (LightTableKind::Spot, bits(8, 16, 16)),
1561 ];
1562 let packed = pack_light_lists(
1563 &lookup,
1564 &indices,
1565 &[(LightTableKind::Point, 2), (LightTableKind::Spot, 1)],
1566 &[],
1567 2,
1568 )
1569 .unwrap();
1570 assert_eq!(
1571 words(&packed.lookup),
1572 [0, 0x0001_0002, 0x0001_0002, 0, 0x0001_0002, 0x0001_0002]
1573 );
1574 assert_eq!(words(&packed.indices), [0xffff_0000, 0xffff_0001, 0xffff_0000]);
1575 assert!(packed.unknown.is_empty());
1576 let empty = pack_light_lists(&lookup, &indices, &[], &[], 1).unwrap();
1577 assert_eq!(words(&empty.lookup), [0, 0, 0]);
1578 assert!(empty.indices.is_empty());
1579
1580 let mut invalid = lookup.clone();
1581 invalid.counts.pop();
1582 assert!(
1583 LightListPart::Lookup(invalid)
1584 .verify(None)
1585 .unwrap_err()
1586 .contains("unlisted")
1587 );
1588 let mut narrow = lookup.clone();
1589 narrow.count_copies[0].1 = bits(8, 0, 1);
1590 assert!(pack_light_lists(&narrow, &indices, &[(LightTableKind::Point, 2)], &[], 1).is_err());
1591 lookup.count_copies[0].1 = bits(4, 0, 16);
1592 assert!(
1593 LightListPart::Lookup(lookup)
1594 .verify(None)
1595 .unwrap_err()
1596 .contains("overlap")
1597 );
1598 }
1599
1600 #[test]
1604 fn a_shadowed_light_leaves_the_tile_pass_but_stays_listed() {
1605 let (mut lookup, mut indices) = lists();
1606 lookup.unknown.clear();
1607 lookup.count_copies = vec![
1608 (LightTableKind::Point, bits(8, 0, 16)),
1609 (LightTableKind::Spot, bits(8, 16, 16)),
1610 ];
1611 let counts = [(LightTableKind::Point, 2), (LightTableKind::Spot, 1)];
1612 let packed = pack_light_lists(&lookup, &indices, &counts, &[(LightTableKind::Point, 1)], 1).unwrap();
1613 assert_eq!(words(&packed.lookup), [0, 0x0001_0002, 0x0001_0001]);
1614 assert_eq!(words(&packed.indices), [0xffff_0000, 0x7fff_0001, 0xffff_0000]);
1615 assert!(
1616 pack_light_lists(&lookup, &indices, &counts, &[(LightTableKind::Spot, 1)], 1)
1617 .unwrap_err()
1618 .contains("not among")
1619 );
1620 indices.tile_pass = None;
1621 assert!(
1622 pack_light_lists(&lookup, &indices, &counts, &[(LightTableKind::Point, 0)], 1)
1623 .unwrap_err()
1624 .contains("no tile-pass bit")
1625 );
1626 }
1627
1628 #[test]
1631 fn light_lists_refuse_what_they_cannot_hold() {
1632 let (lookup, indices) = lists();
1633 assert!(pack_light_lists(&lookup, &indices, &[(LightTableKind::Point, 1)], &[], 5).is_err());
1634 assert!(pack_light_lists(&lookup, &indices, &[(LightTableKind::Point, 9)], &[], 1).is_err());
1635 let mut narrow = lookup.clone();
1636 narrow.counts[0].1 = bits(4, 0, 2);
1637 assert!(pack_light_lists(&narrow, &indices, &[(LightTableKind::Point, 4)], &[], 1).is_err());
1638 let mut overlap = indices.clone();
1639 overlap.slices = bits(0, 15, 15);
1640 assert!(LightListPart::Indices(overlap).verify(Some(4)).is_err());
1641 assert!(LightListPart::Lookup(lookup).verify(Some(16)).is_err());
1642 assert_eq!(LightListPart::Indices(indices).verify(Some(4)), Ok(()));
1643 }
1644}