1#![deny(
73 clippy::unwrap_used,
74 clippy::expect_used,
75 clippy::indexing_slicing,
76 clippy::panic,
77 clippy::unreachable
78)]
79
80mod shader;
81
82use super::common;
83use super::lines::GeneratorConfig;
84use super::{FamilyParam, FamilyRange, Scene, SeededRng};
85use crate::dsp::AnalysisFrame;
86use crate::render::feedback::PingPongField;
87use crate::render::gpu;
88use crate::render::palette::{self, Palette};
89use crate::render::scenes::{ParamGroup, ParamKind, ParamSpec, default_of};
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub enum CellularFamily {
94 #[default]
97 LifeLike,
98 LargerThanLife,
102 Cyclic,
106}
107
108impl CellularFamily {
109 pub const ALL: [CellularFamily; 3] = [
112 CellularFamily::LifeLike,
113 CellularFamily::LargerThanLife,
114 CellularFamily::Cyclic,
115 ];
116
117 pub fn from_name(name: &str) -> Option<Self> {
119 Self::ALL.into_iter().find(|family| family.as_str() == name)
120 }
121
122 pub fn as_str(self) -> &'static str {
124 match self {
125 CellularFamily::LifeLike => "life_like",
126 CellularFamily::LargerThanLife => "larger_than_life",
127 CellularFamily::Cyclic => "cyclic",
128 }
129 }
130
131 fn index(self) -> u32 {
134 match self {
135 CellularFamily::LifeLike => 0,
136 CellularFamily::LargerThanLife => 1,
137 CellularFamily::Cyclic => 2,
138 }
139 }
140
141 fn density(self) -> f32 {
145 match self {
146 CellularFamily::LifeLike | CellularFamily::Cyclic => LIFE_DENSITY,
147 CellularFamily::LargerThanLife => LTL_DENSITY,
148 }
149 }
150
151 fn sums_rows(self) -> bool {
153 match self {
154 CellularFamily::LifeLike | CellularFamily::Cyclic => false,
155 CellularFamily::LargerThanLife => true,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct CellularConfig {
164 pub family: CellularFamily,
166 pub grid: u32,
169 pub wrap: bool,
172 pub salt: u32,
175}
176
177impl Default for CellularConfig {
178 fn default() -> Self {
179 Self {
180 family: CellularFamily::default(),
181 grid: DEFAULT_GRID,
182 wrap: true,
183 salt: 0,
184 }
185 }
186}
187
188pub const DEFAULT_GRID: u32 = 256;
191pub const MIN_GRID: u32 = 16;
194pub const MAX_GRID: u32 = 1024;
197
198pub const MAX_RULE: f32 = 511.0;
200
201pub const MAX_STEP_RATE: f32 = 240.0;
205
206pub const MAX_GENERATIONS_PER_FRAME: u32 = 8;
211
212const WHOLE_SLACK: f64 = 1e-6;
217
218const RESEED_THRESHOLD: f32 = 0.5;
221const RESEED_RADIUS: f32 = 0.2;
223
224const LIFE_DENSITY: f32 = 0.35;
226const LTL_DENSITY: f32 = 0.5;
228
229pub const MAX_RADIUS: f32 = 10.0;
233
234pub const MAX_STATES: f32 = 24.0;
238pub const MAX_THRESHOLD: f32 = 8.0;
240
241const INTERVAL_SLACK: f32 = 1e-4;
245
246pub(crate) const AGE_CAP: f32 = 1023.0;
251pub const MAX_TRAIL: f32 = AGE_CAP;
254
255const FIELD_SEED_MIX: u32 = 0x4345_4C4C;
259const STAMP_SEED_MIX: u64 = 0x5253_4545_4443_4C31;
262
263const DEFAULT_BIRTH: f32 = default_of(PARAMS, "birth");
264const DEFAULT_SURVIVE: f32 = default_of(PARAMS, "survive");
265const DEFAULT_STEP_RATE: f32 = default_of(PARAMS, "step_rate");
266const DEFAULT_TRAIL: f32 = default_of(PARAMS, "trail");
267const DEFAULT_RADIUS: f32 = default_of(PARAMS, "radius");
268const DEFAULT_BIRTH_LO: f32 = default_of(PARAMS, "birth_lo");
269const DEFAULT_BIRTH_HI: f32 = default_of(PARAMS, "birth_hi");
270const DEFAULT_SURVIVE_LO: f32 = default_of(PARAMS, "survive_lo");
271const DEFAULT_SURVIVE_HI: f32 = default_of(PARAMS, "survive_hi");
272const DEFAULT_STATES: f32 = default_of(PARAMS, "states");
273const DEFAULT_THRESHOLD: f32 = default_of(PARAMS, "threshold");
274const DEFAULT_AGE_TINT: f32 = default_of(PARAMS, "age_tint");
275const DEFAULT_HUE: f32 = 0.0;
276const DEFAULT_ZOOM: f32 = 1.0;
277
278pub(crate) fn field_seed(salt: u32) -> u32 {
280 salt ^ FIELD_SEED_MIX
281}
282
283pub(crate) fn live_threshold(density: f32) -> u32 {
286 (density.clamp(0.0, 1.0) * 16_777_216.0) as u32
287}
288
289pub(crate) fn applied_rule(value: f32, default: f32) -> u32 {
293 let v = if value.is_finite() { value } else { default };
294 v.clamp(0.0, MAX_RULE).round() as u32
295}
296
297pub(crate) fn applied_step_rate(value: f32) -> f32 {
300 if value.is_finite() {
301 value.clamp(0.0, MAX_STEP_RATE)
302 } else {
303 DEFAULT_STEP_RATE
304 }
305}
306
307pub(crate) fn applied_trail(value: f32) -> f32 {
311 if value.is_finite() {
312 value.clamp(0.0, MAX_TRAIL)
313 } else {
314 DEFAULT_TRAIL
315 }
316}
317
318pub(crate) fn applied_radius(value: f32, cap: f32) -> u32 {
324 let cap = cap.clamp(1.0, MAX_RADIUS);
325 let v = if value.is_finite() {
326 value
327 } else {
328 DEFAULT_RADIUS
329 };
330 v.clamp(1.0, cap).round() as u32
331}
332
333pub(crate) fn radius_clamp(
338 family: CellularFamily,
339 value: f32,
340 cap: f32,
341) -> Option<super::CapOverflow> {
342 if family != CellularFamily::LargerThanLife {
343 return None;
344 }
345 let asked = applied_radius(value, MAX_RADIUS);
346 let applied = applied_radius(value, cap);
347 (asked > applied).then_some(super::CapOverflow {
348 dropped: (asked - applied) as usize,
349 context: super::OverflowContext::Radius(asked),
350 cap: applied as usize,
351 })
352}
353
354pub(crate) fn grid_clamp(
357 config: CellularConfig,
358 cap: u32,
359) -> (CellularConfig, Option<super::CapOverflow>) {
360 if config.grid <= cap {
361 return (config, None);
362 }
363 let overflow = super::CapOverflow {
364 dropped: (config.grid - cap) as usize,
365 context: super::OverflowContext::Grid(config.grid),
366 cap: cap as usize,
367 };
368 (
369 CellularConfig {
370 grid: cap,
371 ..config
372 },
373 Some(overflow),
374 )
375}
376
377pub(crate) fn applied_states(value: f32) -> u32 {
382 let v = if value.is_finite() {
383 value
384 } else {
385 DEFAULT_STATES
386 };
387 v.clamp(2.0, MAX_STATES).round() as u32
388}
389
390pub(crate) fn applied_threshold(value: f32) -> u32 {
394 let v = if value.is_finite() {
395 value
396 } else {
397 DEFAULT_THRESHOLD
398 };
399 v.clamp(1.0, MAX_THRESHOLD).round() as u32
400}
401
402pub(crate) fn neighbourhood(radius: u32) -> u32 {
405 let side = 2 * radius + 1;
406 side * side - 1
407}
408
409pub(crate) fn interval_counts(lo: f32, hi: f32, radius: u32, fallback: [f32; 2]) -> [u32; 2] {
415 let n = neighbourhood(radius) as f32;
416 let lo = if lo.is_finite() { lo } else { fallback[0] };
417 let hi = if hi.is_finite() { hi } else { fallback[1] };
418 let first = (lo.clamp(0.0, 1.0) * n - INTERVAL_SLACK).ceil().max(0.0);
419 let last = (hi.clamp(0.0, 1.0) * n + INTERVAL_SLACK).floor();
420 if last < first {
421 return [u32::MAX, 0];
424 }
425 [first as u32, last as u32]
426}
427
428#[derive(Debug, Clone, Copy, Default, PartialEq)]
435pub(crate) struct GenerationClock {
436 owed: f64,
438}
439
440impl GenerationClock {
441 pub(crate) fn advance(&mut self, rate: f32, dt: f32) -> u32 {
449 self.owed += f64::from(applied_step_rate(rate)) * f64::from(dt);
450 let whole = (self.owed + WHOLE_SLACK).floor();
451 let cap = f64::from(MAX_GENERATIONS_PER_FRAME);
452 if whole > cap {
453 self.owed = 0.0;
454 return MAX_GENERATIONS_PER_FRAME;
455 }
456 self.owed = (self.owed - whole).max(0.0);
457 whole as u32
458 }
459}
460
461#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub(crate) struct Stamp {
465 pub(crate) centre: [u32; 2],
466 pub(crate) radius_sq: u32,
467 pub(crate) seed: u32,
468}
469
470fn stamp_rng(salt: u32) -> SeededRng {
472 SeededRng::new(u64::from(salt) ^ STAMP_SEED_MIX)
473}
474
475fn next_stamp(rng: &mut SeededRng, grid: u32) -> Stamp {
477 let side = grid.max(1);
478 let cell = |u: f32| ((u * side as f32) as u32).min(side - 1);
479 let x = cell(rng.next_f32());
480 let y = cell(rng.next_f32());
481 let seed = (rng.next_f32() * 16_777_216.0) as u32;
482 let radius = RESEED_RADIUS * side as f32;
483 Stamp {
484 centre: [x, y],
485 radius_sq: (radius * radius) as u32,
486 seed,
487 }
488}
489
490pub const PARAMS: &[ParamSpec] = &[
495 ParamSpec {
496 name: "birth",
497 default: 8.0,
498 range: Some([0.0, MAX_RULE]),
499 doc: "Which live-neighbour counts bring a dead cell to life, as a bitmask over the \
500 counts 0-8: bit k set means k neighbours give birth. 8 (bit 3) is Conway's.",
501 kind: ParamKind::Structural,
502 group: ParamGroup::Shape,
503 main: true,
504 },
505 ParamSpec {
506 name: "survive",
507 default: 12.0,
508 range: Some([0.0, MAX_RULE]),
509 doc: "Which live-neighbour counts keep a live cell alive, as a bitmask over the counts \
510 0-8. 12 (bits 2 and 3) is Conway's.",
511 kind: ParamKind::Structural,
512 group: ParamGroup::Shape,
513 main: true,
514 },
515 ParamSpec {
516 name: "radius",
517 default: 5.0,
518 range: Some([1.0, MAX_RADIUS]),
519 doc: "How far the neighbourhood reaches, in cells: a square of side 2 x radius + 1 \
520 about each cell. Capped by the quality tier.",
521 kind: ParamKind::Structural,
522 group: ParamGroup::Shape,
523 main: false,
524 },
525 ParamSpec {
526 name: "birth_lo",
527 default: 0.28,
528 range: Some([0.0, 1.0]),
529 doc: "The least filled fraction of its neighbourhood at which a dead cell is born.",
530 kind: ParamKind::Modal,
531 group: ParamGroup::Shape,
532 main: false,
533 },
534 ParamSpec {
535 name: "birth_hi",
536 default: 0.385,
537 range: Some([0.0, 1.0]),
538 doc: "The most filled fraction of its neighbourhood at which a dead cell is born.",
539 kind: ParamKind::Modal,
540 group: ParamGroup::Shape,
541 main: false,
542 },
543 ParamSpec {
544 name: "survive_lo",
545 default: 0.26,
546 range: Some([0.0, 1.0]),
547 doc: "The least filled fraction of its neighbourhood at which a live cell survives.",
548 kind: ParamKind::Modal,
549 group: ParamGroup::Shape,
550 main: false,
551 },
552 ParamSpec {
553 name: "survive_hi",
554 default: 0.47,
555 range: Some([0.0, 1.0]),
556 doc: "The most filled fraction of its neighbourhood at which a live cell survives.",
557 kind: ParamKind::Modal,
558 group: ParamGroup::Shape,
559 main: false,
560 },
561 ParamSpec {
562 name: "states",
563 default: 3.0,
564 range: Some([2.0, MAX_STATES]),
565 doc: "How many colours the cycle holds; each cell advances to the next one round it.",
566 kind: ParamKind::Structural,
567 group: ParamGroup::Shape,
568 main: false,
569 },
570 ParamSpec {
571 name: "threshold",
572 default: 3.0,
573 range: Some([1.0, MAX_THRESHOLD]),
574 doc: "How many of its eight neighbours must already hold the next colour before a cell \
575 advances to it.",
576 kind: ParamKind::Structural,
577 group: ParamGroup::Shape,
578 main: false,
579 },
580 ParamSpec {
581 name: "step_rate",
582 default: 10.0,
583 range: Some([0.0, 60.0]),
584 doc: "How many generations the automaton runs per second, whatever the frame rate; 0 \
585 freezes it.",
586 kind: ParamKind::Modal,
587 group: ParamGroup::Motion,
588 main: true,
589 },
590 ParamSpec {
591 name: "reseed",
592 default: 0.0,
593 range: Some([0.0, 1.0]),
594 doc: "A rise past 0.5 refills one disc of the grid with fresh seeded cells, once per \
595 rise; bind a beat or a latch to it.",
596 kind: ParamKind::Modal,
597 group: ParamGroup::Motion,
598 main: false,
599 },
600 ParamSpec {
601 name: "trail",
602 default: 12.0,
603 range: Some([0.0, 64.0]),
604 doc: "How many generations a dead cell keeps glowing, fading as it goes; 0 draws only \
605 the live cells.",
606 kind: ParamKind::Modal,
607 group: ParamGroup::Motion,
608 main: false,
609 },
610 ParamSpec {
611 name: "age_tint",
612 default: 0.35,
613 range: Some([0.0, 1.0]),
614 doc: "How far along the palette a dead cell's glow travels as it fades; 0 keeps the \
615 wake the live cells' colour.",
616 kind: ParamKind::Modal,
617 group: ParamGroup::Colour,
618 main: false,
619 },
620 common::brightness(common::DEFAULT_BRIGHTNESS),
621 common::hue(DEFAULT_HUE),
622 common::zoom(DEFAULT_ZOOM),
623 common::PAN_X,
624 common::PAN_Y,
625 common::SATURATION,
626 common::PALETTE_MIX,
627 common::PALETTE_STEPS,
628 common::PALETTE_CONTOUR,
629 common::PALETTE_CONTOUR_STYLE,
630 common::PALETTE_CONTOUR_INK,
631];
632
633macro_rules! per_family {
636 ($name:literal: $life_like:expr, $larger_than_life:expr, $cyclic:expr $(,)?) => {
637 FamilyParam {
638 name: $name,
639 ranges: &[
640 FamilyRange {
641 family: "life_like",
642 range: $life_like,
643 },
644 FamilyRange {
645 family: "larger_than_life",
646 range: $larger_than_life,
647 },
648 FamilyRange {
649 family: "cyclic",
650 range: $cyclic,
651 },
652 ],
653 }
654 };
655}
656
657pub const FAMILY_PARAMS: &[FamilyParam] = &[
662 per_family!("birth": Some([0.0, MAX_RULE]), None, None),
663 per_family!("survive": Some([0.0, MAX_RULE]), None, None),
664 per_family!("radius": None, Some([1.0, MAX_RADIUS]), None),
665 per_family!("birth_lo": None, Some([0.0, 1.0]), None),
666 per_family!("birth_hi": None, Some([0.0, 1.0]), None),
667 per_family!("survive_lo": None, Some([0.0, 1.0]), None),
668 per_family!("survive_hi": None, Some([0.0, 1.0]), None),
669 per_family!("states": None, None, Some([2.0, MAX_STATES])),
670 per_family!("threshold": None, None, Some([1.0, MAX_THRESHOLD])),
671 per_family!("trail": Some([0.0, 64.0]), Some([0.0, 64.0]), None),
674 per_family!("age_tint": Some([0.0, 1.0]), Some([0.0, 1.0]), None),
675];
676
677#[repr(C)]
687#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
688struct StepParams {
689 a: [u32; 4],
692 b: [u32; 4],
694 c: [u32; 4],
697 d: [u32; 4],
699 e: [u32; 4],
702}
703
704#[repr(C)]
705#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
706struct PresentParams {
707 a: [f32; 4],
709 b: [f32; 4],
711 c: [f32; 4],
713 d: [f32; 4],
715 e: [f32; 4],
717}
718
719struct ReadPair {
722 a: wgpu::BindGroup,
723 b: wgpu::BindGroup,
724}
725
726impl ReadPair {
727 fn for_field(&self, field: &PingPongField) -> &wgpu::BindGroup {
728 if field.reading_a() { &self.a } else { &self.b }
729 }
730}
731
732struct Resources {
735 grid: u32,
737 field: PingPongField,
738 _rows: wgpu::Texture,
742 rows_view: wgpu::TextureView,
743 seed_pipeline: wgpu::RenderPipeline,
746 stamp_pipeline: wgpu::RenderPipeline,
747 step_pipeline: wgpu::RenderPipeline,
748 rows_pipeline: wgpu::RenderPipeline,
749 present_pipeline: wgpu::RenderPipeline,
750 step_uniform: wgpu::Buffer,
751 present_uniform: wgpu::Buffer,
752 step_bg: ReadPair,
753 rows_bg: ReadPair,
754 present_bg: ReadPair,
755 luts: palette::LutPair,
758}
759
760impl Resources {
761 fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat, grid: u32) -> Self {
762 let present_shader = gpu::fullscreen_shader(
763 device,
764 "cellular-present-shader",
765 gpu::FULLSCREEN_VS_UV_FLIPPED,
766 shader::PRESENT_SHADER,
767 );
768
769 let field = PingPongField::new(device, grid, grid);
770
771 let step_uniform = gpu::uniform_buffer(
772 device,
773 "cellular-step-params",
774 std::mem::size_of::<StepParams>(),
775 );
776 let present_uniform = gpu::uniform_buffer(
777 device,
778 "cellular-present-params",
779 std::mem::size_of::<PresentParams>(),
780 );
781
782 let rows = device.create_texture(&wgpu::TextureDescriptor {
783 label: Some("cellular-rows"),
784 size: wgpu::Extent3d {
785 width: grid,
786 height: grid,
787 depth_or_array_layers: 1,
788 },
789 mip_level_count: 1,
790 sample_count: 1,
791 dimension: wgpu::TextureDimension::D2,
792 format: PingPongField::FORMAT,
795 usage: wgpu::TextureUsages::TEXTURE_BINDING
798 | wgpu::TextureUsages::RENDER_ATTACHMENT
799 | wgpu::TextureUsages::COPY_SRC,
800 view_formats: &[],
801 });
802 let rows_view = rows.create_view(&wgpu::TextureViewDescriptor::default());
803
804 let step_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
807 label: Some("cellular-step-layout"),
808 entries: &[
809 gpu::texture(0, false),
810 gpu::texture(1, false),
811 gpu::uniform(2, wgpu::ShaderStages::FRAGMENT),
812 ],
813 });
814 let step_bg = ReadPair {
815 a: step_bind_group(
816 device,
817 &step_layout,
818 field.view_a(),
819 &rows_view,
820 &step_uniform,
821 ),
822 b: step_bind_group(
823 device,
824 &step_layout,
825 field.view_b(),
826 &rows_view,
827 &step_uniform,
828 ),
829 };
830 let rows_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
834 label: Some("cellular-rows-layout"),
835 entries: &[
836 gpu::texture(0, false),
837 gpu::uniform(1, wgpu::ShaderStages::FRAGMENT),
838 ],
839 });
840 let rows_bg = ReadPair {
841 a: rows_bind_group(device, &rows_layout, field.view_a(), &step_uniform),
842 b: rows_bind_group(device, &rows_layout, field.view_b(), &step_uniform),
843 };
844 let rows_shader = gpu::fullscreen_shader(
845 device,
846 "cellular-rows",
847 gpu::FULLSCREEN_VS_UV_FLIPPED,
848 &format!(
849 "{}{}{}",
850 radius_const(),
851 shader::STEP_COMMON,
852 shader::ROWS_SHADER
853 ),
854 );
855 let rows_pipeline = gpu::fullscreen_pipeline(
856 device,
857 &rows_shader,
858 &[&rows_layout],
859 PingPongField::FORMAT,
860 wgpu::BlendState::REPLACE,
861 "cellular-rows",
862 );
863 let step_pipeline_for = |pass: StepPass| {
864 let label = pass.label();
865 let shader = gpu::fullscreen_shader(
866 device,
867 label,
868 gpu::FULLSCREEN_VS_UV_FLIPPED,
869 &pass.source(),
870 );
871 gpu::fullscreen_pipeline(
872 device,
873 &shader,
874 &[&step_layout],
875 PingPongField::FORMAT,
876 wgpu::BlendState::REPLACE,
877 label,
878 )
879 };
880 let seed_pipeline = step_pipeline_for(StepPass::Seed);
881 let stamp_pipeline = step_pipeline_for(StepPass::Stamp);
882 let step_pipeline = step_pipeline_for(StepPass::Step);
883
884 let luts = palette::LutPair::new(device, "cellular");
885 let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
889 label: Some("cellular-present-layout"),
890 entries: &[
891 gpu::texture(0, false),
892 gpu::texture(1, true),
893 gpu::texture(2, true),
894 gpu::sampler(3),
895 gpu::uniform(4, wgpu::ShaderStages::FRAGMENT),
896 ],
897 });
898 let present_bg = ReadPair {
899 a: present_bind_group(
900 device,
901 &present_layout,
902 field.view_a(),
903 &luts,
904 &present_uniform,
905 ),
906 b: present_bind_group(
907 device,
908 &present_layout,
909 field.view_b(),
910 &luts,
911 &present_uniform,
912 ),
913 };
914 let present_pipeline = gpu::fullscreen_pipeline(
915 device,
916 &present_shader,
917 &[&present_layout],
918 surface_format,
919 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
922 "cellular-present",
923 );
924
925 Self {
926 grid,
927 field,
928 _rows: rows,
929 rows_view,
930 seed_pipeline,
931 stamp_pipeline,
932 step_pipeline,
933 rows_pipeline,
934 present_pipeline,
935 step_uniform,
936 present_uniform,
937 step_bg,
938 rows_bg,
939 present_bg,
940 luts,
941 }
942 }
943
944 fn encode_step(&mut self, encoder: &mut wgpu::CommandEncoder, pass: StepPass) {
947 let pipeline = match pass {
948 StepPass::Seed => &self.seed_pipeline,
949 StepPass::Stamp => &self.stamp_pipeline,
950 StepPass::Step => &self.step_pipeline,
951 };
952 {
953 let mut rpass = gpu::color_pass(
956 encoder,
957 pass.label(),
958 self.field.write_view(),
959 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
960 );
961 rpass.set_pipeline(pipeline);
962 rpass.set_bind_group(0, self.step_bg.for_field(&self.field), &[]);
963 rpass.draw(0..3, 0..1);
964 }
965 self.field.swap();
966 }
967
968 fn encode_rows(&self, encoder: &mut wgpu::CommandEncoder) {
972 let mut rpass = gpu::color_pass(
973 encoder,
974 "cellular-rows",
975 &self.rows_view,
976 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
977 );
978 rpass.set_pipeline(&self.rows_pipeline);
979 rpass.set_bind_group(0, self.rows_bg.for_field(&self.field), &[]);
980 rpass.draw(0..3, 0..1);
981 }
982}
983
984fn radius_const() -> String {
986 format!("const MAX_RADIUS: i32 = {};\n", MAX_RADIUS as i32)
987}
988
989#[derive(Clone, Copy)]
991enum StepPass {
992 Seed,
994 Stamp,
996 Step,
998}
999
1000impl StepPass {
1001 fn mode(self) -> u32 {
1003 match self {
1004 StepPass::Step => 0,
1005 StepPass::Seed => 1,
1006 StepPass::Stamp => 2,
1007 }
1008 }
1009
1010 fn label(self) -> &'static str {
1011 match self {
1012 StepPass::Seed => "cellular-seed",
1013 StepPass::Stamp => "cellular-stamp",
1014 StepPass::Step => "cellular-step",
1015 }
1016 }
1017
1018 fn source(self) -> String {
1021 format!(
1022 "const MODE: u32 = {}u;\nconst AGE_CAP: f32 = {:?};\n{}{}{}{}",
1023 self.mode(),
1024 AGE_CAP,
1025 radius_const(),
1026 gpu::HASH_WGSL,
1027 shader::STEP_COMMON,
1028 shader::STEP_SHADER
1029 )
1030 }
1031}
1032
1033fn step_bind_group(
1034 device: &wgpu::Device,
1035 layout: &wgpu::BindGroupLayout,
1036 input: &wgpu::TextureView,
1037 rows: &wgpu::TextureView,
1038 uniform: &wgpu::Buffer,
1039) -> wgpu::BindGroup {
1040 device.create_bind_group(&wgpu::BindGroupDescriptor {
1041 label: Some("cellular-step-bg"),
1042 layout,
1043 entries: &[
1044 wgpu::BindGroupEntry {
1045 binding: 0,
1046 resource: wgpu::BindingResource::TextureView(input),
1047 },
1048 wgpu::BindGroupEntry {
1049 binding: 1,
1050 resource: wgpu::BindingResource::TextureView(rows),
1051 },
1052 wgpu::BindGroupEntry {
1053 binding: 2,
1054 resource: uniform.as_entire_binding(),
1055 },
1056 ],
1057 })
1058}
1059
1060fn rows_bind_group(
1061 device: &wgpu::Device,
1062 layout: &wgpu::BindGroupLayout,
1063 input: &wgpu::TextureView,
1064 uniform: &wgpu::Buffer,
1065) -> wgpu::BindGroup {
1066 device.create_bind_group(&wgpu::BindGroupDescriptor {
1067 label: Some("cellular-rows-bg"),
1068 layout,
1069 entries: &[
1070 wgpu::BindGroupEntry {
1071 binding: 0,
1072 resource: wgpu::BindingResource::TextureView(input),
1073 },
1074 wgpu::BindGroupEntry {
1075 binding: 1,
1076 resource: uniform.as_entire_binding(),
1077 },
1078 ],
1079 })
1080}
1081
1082fn present_bind_group(
1083 device: &wgpu::Device,
1084 layout: &wgpu::BindGroupLayout,
1085 input: &wgpu::TextureView,
1086 luts: &palette::LutPair,
1087 uniform: &wgpu::Buffer,
1088) -> wgpu::BindGroup {
1089 let [lut_a, lut_b, lut_sampler] = luts.bind_entries(1, 2, 3);
1090 device.create_bind_group(&wgpu::BindGroupDescriptor {
1091 label: Some("cellular-present-bg"),
1092 layout,
1093 entries: &[
1094 wgpu::BindGroupEntry {
1095 binding: 0,
1096 resource: wgpu::BindingResource::TextureView(input),
1097 },
1098 lut_a,
1099 lut_b,
1100 lut_sampler,
1101 wgpu::BindGroupEntry {
1102 binding: 4,
1103 resource: uniform.as_entire_binding(),
1104 },
1105 ],
1106 })
1107}
1108
1109pub struct CellularScene {
1112 device: wgpu::Device,
1114 surface_format: wgpu::TextureFormat,
1115 res: Option<Resources>,
1116 config: CellularConfig,
1118 needs_seed: bool,
1121 clock: GenerationClock,
1122 dt: f32,
1125 pending_generations: u32,
1127 stamp_rng: SeededRng,
1130 pending_stamp: Option<Stamp>,
1132 prev_reseed: f32,
1134 radius_cap: f32,
1137 grid_cap: u32,
1140 clamp: Option<super::CapOverflow>,
1145 birth: f32,
1146 survive: f32,
1147 radius: f32,
1148 birth_lo: f32,
1149 birth_hi: f32,
1150 survive_lo: f32,
1151 survive_hi: f32,
1152 states: f32,
1153 threshold: f32,
1154 step_rate: f32,
1155 reseed: f32,
1156 trail: f32,
1157 age_tint: f32,
1158 colour: common::PaletteParams,
1159 pan: common::PanParams,
1160 zoom: f32,
1161 occlude: f32,
1166 palette: Palette,
1169}
1170
1171impl CellularScene {
1172 pub fn new(
1178 device: &wgpu::Device,
1179 surface_format: wgpu::TextureFormat,
1180 radius_cap: u32,
1181 grid_cap: u32,
1182 ) -> Self {
1183 let config = CellularConfig::default();
1184 Self {
1185 device: device.clone(),
1186 surface_format,
1187 res: None,
1188 config,
1189 needs_seed: true,
1190 clock: GenerationClock::default(),
1191 dt: 0.0,
1192 pending_generations: 0,
1193 stamp_rng: stamp_rng(config.salt),
1194 pending_stamp: None,
1195 prev_reseed: 0.0,
1196 radius_cap: (radius_cap as f32).clamp(1.0, MAX_RADIUS),
1197 grid_cap: grid_cap.clamp(MIN_GRID, MAX_GRID),
1198 clamp: None,
1199 birth: DEFAULT_BIRTH,
1200 survive: DEFAULT_SURVIVE,
1201 radius: DEFAULT_RADIUS,
1202 birth_lo: DEFAULT_BIRTH_LO,
1203 birth_hi: DEFAULT_BIRTH_HI,
1204 survive_lo: DEFAULT_SURVIVE_LO,
1205 survive_hi: DEFAULT_SURVIVE_HI,
1206 states: DEFAULT_STATES,
1207 threshold: DEFAULT_THRESHOLD,
1208 step_rate: DEFAULT_STEP_RATE,
1209 reseed: 0.0,
1210 trail: DEFAULT_TRAIL,
1211 age_tint: DEFAULT_AGE_TINT,
1212 colour: common::PaletteParams::new(DEFAULT_HUE, common::DEFAULT_BRIGHTNESS),
1213 pan: common::PanParams::default(),
1214 zoom: DEFAULT_ZOOM,
1215 occlude: crate::render::post::DEFAULT_OCCLUDE,
1216 palette: Palette::default_spectrum(),
1217 }
1218 }
1219
1220 fn step_params(&self, stamp: Option<Stamp>) -> StepParams {
1223 let stamp = stamp.unwrap_or(Stamp {
1224 centre: [0, 0],
1225 radius_sq: 0,
1226 seed: 0,
1227 });
1228 let radius = applied_radius(self.radius, self.radius_cap);
1229 let birth = interval_counts(
1230 self.birth_lo,
1231 self.birth_hi,
1232 radius,
1233 [DEFAULT_BIRTH_LO, DEFAULT_BIRTH_HI],
1234 );
1235 let survive = interval_counts(
1236 self.survive_lo,
1237 self.survive_hi,
1238 radius,
1239 [DEFAULT_SURVIVE_LO, DEFAULT_SURVIVE_HI],
1240 );
1241 StepParams {
1242 a: [
1243 self.config.family.index(),
1244 u32::from(self.config.wrap),
1245 self.config.grid,
1246 live_threshold(self.config.family.density()),
1247 ],
1248 b: [
1249 applied_rule(self.birth, DEFAULT_BIRTH),
1250 applied_rule(self.survive, DEFAULT_SURVIVE),
1251 radius,
1252 applied_states(self.states),
1253 ],
1254 c: [field_seed(self.config.salt), stamp.seed, stamp.radius_sq, 0],
1255 d: [
1256 stamp.centre[0],
1257 stamp.centre[1],
1258 applied_threshold(self.threshold),
1259 0,
1260 ],
1261 e: [birth[0], birth[1], survive[0], survive[1]],
1262 }
1263 }
1264}
1265
1266impl Scene for CellularScene {
1267 fn name(&self) -> &'static str {
1268 "cellular"
1269 }
1270
1271 fn advance(&mut self, dt: f32) {
1272 self.dt = dt;
1273 }
1274
1275 fn set_occlude(&mut self, occlude: f32) {
1276 self.occlude = occlude;
1277 }
1278
1279 fn set_palette(&mut self, palette: &Palette) {
1280 self.palette = palette.clone();
1281 if let Some(res) = self.res.as_mut() {
1282 res.luts.set(palette);
1283 }
1284 }
1285
1286 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<super::CapOverflow> {
1287 let GeneratorConfig::Cellular(config) = cfg else {
1288 return None;
1289 };
1290 let (config, overflow) = grid_clamp(*config, self.grid_cap);
1295 self.config = config;
1296 self.needs_seed = true;
1297 self.clock = GenerationClock::default();
1298 self.pending_generations = 0;
1299 self.stamp_rng = stamp_rng(config.salt);
1300 self.pending_stamp = None;
1301 self.prev_reseed = 0.0;
1302 self.clamp = None;
1304 overflow
1305 }
1306
1307 fn mirror_overflow(&self) -> Option<&super::CapOverflow> {
1308 self.clamp.as_ref()
1309 }
1310
1311 fn reset_params(&mut self) {
1312 self.birth = DEFAULT_BIRTH;
1313 self.survive = DEFAULT_SURVIVE;
1314 self.radius = DEFAULT_RADIUS;
1315 self.birth_lo = DEFAULT_BIRTH_LO;
1316 self.birth_hi = DEFAULT_BIRTH_HI;
1317 self.survive_lo = DEFAULT_SURVIVE_LO;
1318 self.survive_hi = DEFAULT_SURVIVE_HI;
1319 self.states = DEFAULT_STATES;
1320 self.threshold = DEFAULT_THRESHOLD;
1321 self.step_rate = DEFAULT_STEP_RATE;
1322 self.reseed = 0.0;
1323 self.trail = DEFAULT_TRAIL;
1324 self.age_tint = DEFAULT_AGE_TINT;
1325 self.colour.reset();
1326 self.pan.reset();
1327 self.zoom = DEFAULT_ZOOM;
1328 }
1329
1330 fn set_param(&mut self, name: &str, value: f32) {
1331 if self.colour.set(name, value) || self.pan.set(name, value) {
1334 return;
1335 }
1336 match name {
1337 "birth" => self.birth = value,
1338 "survive" => self.survive = value,
1339 "radius" => self.radius = value,
1340 "birth_lo" => self.birth_lo = value,
1341 "birth_hi" => self.birth_hi = value,
1342 "survive_lo" => self.survive_lo = value,
1343 "survive_hi" => self.survive_hi = value,
1344 "states" => self.states = value,
1345 "threshold" => self.threshold = value,
1346 "step_rate" => self.step_rate = value,
1347 "reseed" => self.reseed = value,
1348 "trail" => self.trail = value,
1349 "age_tint" => self.age_tint = value,
1350 "zoom" => self.zoom = value,
1351 _ => {}
1352 }
1353 }
1354
1355 fn update(&mut self, _frame: &AnalysisFrame) {
1356 self.pending_generations = self.clock.advance(self.step_rate, self.dt);
1357 let reseed = if self.reseed.is_finite() {
1362 self.reseed
1363 } else {
1364 0.0
1365 };
1366 if reseed >= RESEED_THRESHOLD && self.prev_reseed < RESEED_THRESHOLD {
1367 self.pending_stamp = Some(next_stamp(&mut self.stamp_rng, self.config.grid));
1368 }
1369 self.prev_reseed = reseed;
1370 }
1371
1372 fn render(
1373 &mut self,
1374 queue: &wgpu::Queue,
1375 encoder: &mut wgpu::CommandEncoder,
1376 view: &wgpu::TextureView,
1377 _aspect: f32,
1378 ) {
1379 let grid = self.config.grid;
1380 if self.res.as_ref().is_none_or(|res| res.grid != grid) {
1381 let mut built = Resources::build(&self.device, self.surface_format, grid);
1382 built.luts.set(&self.palette);
1383 self.res = Some(built);
1384 self.needs_seed = true;
1385 }
1386
1387 let seed = std::mem::replace(&mut self.needs_seed, false);
1388 let stamp = self.pending_stamp.take();
1389 let generations = std::mem::take(&mut self.pending_generations);
1390 let step = self.step_params(stamp);
1391 self.clamp = radius_clamp(self.config.family, self.radius, self.radius_cap);
1392 let present = PresentParams {
1393 a: [
1394 self.colour.hue,
1395 self.colour.brightness,
1396 self.colour.saturation,
1397 self.colour.mix,
1398 ],
1399 b: [
1400 palette::band_steps(self.colour.steps),
1401 palette::band_contour(self.colour.contour),
1402 self.occlude,
1403 grid as f32,
1404 ],
1405 c: [
1406 if self.zoom.is_finite() && self.zoom > 1e-3 {
1407 self.zoom
1408 } else {
1409 DEFAULT_ZOOM
1410 },
1411 if self.pan.x.is_finite() {
1412 self.pan.x
1413 } else {
1414 0.0
1415 },
1416 if self.pan.y.is_finite() {
1417 self.pan.y
1418 } else {
1419 0.0
1420 },
1421 if self.config.wrap { 1.0 } else { 0.0 },
1422 ],
1423 d: [
1424 applied_trail(self.trail),
1425 if self.age_tint.is_finite() {
1426 self.age_tint
1427 } else {
1428 DEFAULT_AGE_TINT
1429 },
1430 if self.config.family == CellularFamily::Cyclic {
1431 1.0
1432 } else {
1433 0.0
1434 },
1435 applied_states(self.states) as f32,
1436 ],
1437 e: [
1438 palette::band_contour_style(self.colour.contour_style),
1439 self.colour.contour_ink,
1440 0.0,
1441 0.0,
1442 ],
1443 };
1444
1445 let Some(res) = self.res.as_mut() else {
1446 return;
1447 };
1448 res.luts.flush(queue);
1449 queue.write_buffer(&res.present_uniform, 0, bytemuck::bytes_of(&present));
1450
1451 if seed || stamp.is_some() || generations > 0 {
1455 queue.write_buffer(&res.step_uniform, 0, bytemuck::bytes_of(&step));
1456 }
1457 if seed {
1458 res.encode_step(encoder, StepPass::Seed);
1459 }
1460 if stamp.is_some() {
1461 res.encode_step(encoder, StepPass::Stamp);
1462 }
1463 let sums_rows = self.config.family.sums_rows();
1464 for _ in 0..generations {
1465 if sums_rows {
1466 res.encode_rows(encoder);
1467 }
1468 res.encode_step(encoder, StepPass::Step);
1469 }
1470
1471 let mut pass = gpu::color_pass(encoder, "cellular-present-pass", view, wgpu::LoadOp::Load);
1474 pass.set_pipeline(&res.present_pipeline);
1475 pass.set_bind_group(0, res.present_bg.for_field(&res.field), &[]);
1476 pass.draw(0..3, 0..1);
1477 }
1478}
1479
1480#[cfg(test)]
1481mod mirror;
1482#[cfg(test)]
1483mod tests;