1#![deny(
73 clippy::unwrap_used,
74 clippy::expect_used,
75 clippy::indexing_slicing,
76 clippy::panic,
77 clippy::unreachable
78)]
79
80mod route;
81mod shader;
82
83use super::common;
84use super::lines::GeneratorConfig;
85use super::{FamilyParam, FamilyRange, Scene, SeededRng};
86use crate::dsp::AnalysisFrame;
87use crate::render::feedback::PingPongField;
88use crate::render::gpu;
89use crate::render::palette::{self, Palette};
90use crate::render::scenes::{ParamGroup, ParamKind, ParamSpec, default_of};
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
94pub enum CellularFamily {
95 #[default]
98 LifeLike,
99 LargerThanLife,
103 Cyclic,
107}
108
109impl CellularFamily {
110 pub const ALL: [CellularFamily; 3] = [
113 CellularFamily::LifeLike,
114 CellularFamily::LargerThanLife,
115 CellularFamily::Cyclic,
116 ];
117
118 pub fn from_name(name: &str) -> Option<Self> {
120 Self::ALL.into_iter().find(|family| family.as_str() == name)
121 }
122
123 pub fn as_str(self) -> &'static str {
125 match self {
126 CellularFamily::LifeLike => "life_like",
127 CellularFamily::LargerThanLife => "larger_than_life",
128 CellularFamily::Cyclic => "cyclic",
129 }
130 }
131
132 fn index(self) -> u32 {
135 match self {
136 CellularFamily::LifeLike => 0,
137 CellularFamily::LargerThanLife => 1,
138 CellularFamily::Cyclic => 2,
139 }
140 }
141
142 fn density(self) -> f32 {
146 match self {
147 CellularFamily::LifeLike | CellularFamily::Cyclic => LIFE_DENSITY,
148 CellularFamily::LargerThanLife => LTL_DENSITY,
149 }
150 }
151
152 fn sums_rows(self) -> bool {
154 match self {
155 CellularFamily::LifeLike | CellularFamily::Cyclic => false,
156 CellularFamily::LargerThanLife => true,
157 }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct CellularConfig {
165 pub family: CellularFamily,
167 pub grid: u32,
170 pub wrap: bool,
173 pub salt: u32,
176}
177
178impl Default for CellularConfig {
179 fn default() -> Self {
180 Self {
181 family: CellularFamily::default(),
182 grid: DEFAULT_GRID,
183 wrap: true,
184 salt: 0,
185 }
186 }
187}
188
189pub const DEFAULT_GRID: u32 = 256;
192pub const MIN_GRID: u32 = 16;
195pub const MAX_GRID: u32 = 1024;
198
199pub const MAX_RULE: f32 = 511.0;
201
202pub const MAX_STEP_RATE: f32 = 240.0;
206
207pub use common::MAX_GENERATIONS_PER_FRAME;
208use common::WHOLE_SLACK;
209
210const RESEED_THRESHOLD: f32 = 0.5;
213const RESEED_RADIUS: f32 = 0.2;
215
216const LIFE_DENSITY: f32 = 0.35;
218const LTL_DENSITY: f32 = 0.5;
220
221pub const MAX_RADIUS: f32 = 10.0;
225
226pub const MAX_STATES: f32 = 24.0;
230pub const MAX_THRESHOLD: f32 = 8.0;
232
233const INTERVAL_SLACK: f32 = 1e-4;
237
238pub(crate) const AGE_CAP: f32 = 1023.0;
243pub const MAX_TRAIL: f32 = AGE_CAP;
246
247const FIELD_SEED_MIX: u32 = 0x4345_4C4C;
251const STAMP_SEED_MIX: u64 = 0x5253_4545_4443_4C31;
254
255const DEFAULT_BIRTH: f32 = default_of(PARAMS, "birth");
256const DEFAULT_SURVIVE: f32 = default_of(PARAMS, "survive");
257const DEFAULT_STEP_RATE: f32 = default_of(PARAMS, "step_rate");
258const DEFAULT_TRAIL: f32 = default_of(PARAMS, "trail");
259const DEFAULT_RADIUS: f32 = default_of(PARAMS, "radius");
260const DEFAULT_BIRTH_LO: f32 = default_of(PARAMS, "birth_lo");
261const DEFAULT_BIRTH_HI: f32 = default_of(PARAMS, "birth_hi");
262const DEFAULT_SURVIVE_LO: f32 = default_of(PARAMS, "survive_lo");
263const DEFAULT_SURVIVE_HI: f32 = default_of(PARAMS, "survive_hi");
264const DEFAULT_STATES: f32 = default_of(PARAMS, "states");
265const DEFAULT_THRESHOLD: f32 = default_of(PARAMS, "threshold");
266const DEFAULT_AGE_TINT: f32 = default_of(PARAMS, "age_tint");
267const DEFAULT_ROUTE: f32 = default_of(PARAMS, "route");
268const DEFAULT_ROUTE_COORD: f32 = default_of(PARAMS, "route_coord");
269const DEFAULT_ROUTE_GRADE: f32 = default_of(PARAMS, "route_grade");
270const DEFAULT_ROUTE_REVEAL: f32 = default_of(PARAMS, "route_reveal");
271const DEFAULT_HUE: f32 = 0.0;
272const DEFAULT_ZOOM: f32 = 1.0;
273
274pub(crate) fn field_seed(salt: u32) -> u32 {
276 salt ^ FIELD_SEED_MIX
277}
278
279pub(crate) fn live_threshold(density: f32) -> u32 {
282 (density.clamp(0.0, 1.0) * 16_777_216.0) as u32
283}
284
285pub(crate) fn applied_rule(value: f32, default: f32) -> u32 {
289 let v = if value.is_finite() { value } else { default };
290 v.clamp(0.0, MAX_RULE).round() as u32
291}
292
293pub(crate) fn applied_step_rate(value: f32) -> f32 {
296 if value.is_finite() {
297 value.clamp(0.0, MAX_STEP_RATE)
298 } else {
299 DEFAULT_STEP_RATE
300 }
301}
302
303pub(crate) fn applied_trail(value: f32) -> f32 {
307 if value.is_finite() {
308 value.clamp(0.0, MAX_TRAIL)
309 } else {
310 DEFAULT_TRAIL
311 }
312}
313
314pub(crate) fn applied_radius(value: f32, cap: f32) -> u32 {
320 let cap = cap.clamp(1.0, MAX_RADIUS);
321 let v = if value.is_finite() {
322 value
323 } else {
324 DEFAULT_RADIUS
325 };
326 v.clamp(1.0, cap).round() as u32
327}
328
329pub(crate) fn radius_clamp(
334 family: CellularFamily,
335 value: f32,
336 cap: f32,
337) -> Option<super::CapOverflow> {
338 if family != CellularFamily::LargerThanLife {
339 return None;
340 }
341 let asked = applied_radius(value, MAX_RADIUS);
342 let applied = applied_radius(value, cap);
343 (asked > applied).then_some(super::CapOverflow {
344 dropped: (asked - applied) as usize,
345 context: super::OverflowContext::Radius(asked),
346 cap: applied as usize,
347 })
348}
349
350pub(crate) fn grid_clamp(
353 config: CellularConfig,
354 cap: u32,
355) -> (CellularConfig, Option<super::CapOverflow>) {
356 if config.grid <= cap {
357 return (config, None);
358 }
359 let overflow = super::CapOverflow {
360 dropped: (config.grid - cap) as usize,
361 context: super::OverflowContext::Grid(config.grid),
362 cap: cap as usize,
363 };
364 (
365 CellularConfig {
366 grid: cap,
367 ..config
368 },
369 Some(overflow),
370 )
371}
372
373pub(crate) fn applied_states(value: f32) -> u32 {
378 let v = if value.is_finite() {
379 value
380 } else {
381 DEFAULT_STATES
382 };
383 v.clamp(2.0, MAX_STATES).round() as u32
384}
385
386pub(crate) fn applied_threshold(value: f32) -> u32 {
390 let v = if value.is_finite() {
391 value
392 } else {
393 DEFAULT_THRESHOLD
394 };
395 v.clamp(1.0, MAX_THRESHOLD).round() as u32
396}
397
398pub(crate) fn neighbourhood(radius: u32) -> u32 {
401 let side = 2 * radius + 1;
402 side * side - 1
403}
404
405pub(crate) fn interval_counts(lo: f32, hi: f32, radius: u32, fallback: [f32; 2]) -> [u32; 2] {
411 let n = neighbourhood(radius) as f32;
412 let lo = if lo.is_finite() { lo } else { fallback[0] };
413 let hi = if hi.is_finite() { hi } else { fallback[1] };
414 let first = (lo.clamp(0.0, 1.0) * n - INTERVAL_SLACK).ceil().max(0.0);
415 let last = (hi.clamp(0.0, 1.0) * n + INTERVAL_SLACK).floor();
416 if last < first {
417 return [u32::MAX, 0];
420 }
421 [first as u32, last as u32]
422}
423
424#[derive(Debug, Clone, Copy, Default, PartialEq)]
428pub(crate) struct GenerationClock(common::GenerationClock);
429
430impl GenerationClock {
431 pub(crate) fn advance(&mut self, rate: f32, dt: f32) -> u32 {
434 self.0.advance(applied_step_rate(rate), dt)
435 }
436
437 fn owed(&self) -> f64 {
439 self.0.owed
440 }
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub(crate) struct Stamp {
447 pub(crate) centre: [u32; 2],
448 pub(crate) radius_sq: u32,
449 pub(crate) seed: u32,
450}
451
452fn stamp_rng(salt: u32) -> SeededRng {
454 SeededRng::new(u64::from(salt) ^ STAMP_SEED_MIX)
455}
456
457fn next_stamp(rng: &mut SeededRng, grid: u32) -> Stamp {
459 let side = grid.max(1);
460 let cell = |u: f32| ((u * side as f32) as u32).min(side - 1);
461 let x = cell(rng.next_f32());
462 let y = cell(rng.next_f32());
463 let seed = (rng.next_f32() * 16_777_216.0) as u32;
464 let radius = RESEED_RADIUS * side as f32;
465 Stamp {
466 centre: [x, y],
467 radius_sq: (radius * radius) as u32,
468 seed,
469 }
470}
471
472pub const PARAMS: &[ParamSpec] = &[
477 ParamSpec {
478 name: "birth",
479 default: 8.0,
480 range: Some([0.0, MAX_RULE]),
481 doc: "Which live-neighbour counts bring a dead cell to life, as a bitmask over the \
482 counts 0-8: bit k set means k neighbours give birth. 8 (bit 3) is Conway's.",
483 kind: ParamKind::Structural,
484 group: ParamGroup::Shape,
485 main: true,
486 },
487 ParamSpec {
488 name: "survive",
489 default: 12.0,
490 range: Some([0.0, MAX_RULE]),
491 doc: "Which live-neighbour counts keep a live cell alive, as a bitmask over the counts \
492 0-8. 12 (bits 2 and 3) is Conway's.",
493 kind: ParamKind::Structural,
494 group: ParamGroup::Shape,
495 main: true,
496 },
497 ParamSpec {
498 name: "radius",
499 default: 5.0,
500 range: Some([1.0, MAX_RADIUS]),
501 doc: "How far the neighbourhood reaches, in cells: a square of side 2 x radius + 1 \
502 about each cell. Capped by the quality tier.",
503 kind: ParamKind::Structural,
504 group: ParamGroup::Shape,
505 main: false,
506 },
507 ParamSpec {
508 name: "birth_lo",
509 default: 0.28,
510 range: Some([0.0, 1.0]),
511 doc: "The least filled fraction of its neighbourhood at which a dead cell is born.",
512 kind: ParamKind::Modal,
513 group: ParamGroup::Shape,
514 main: false,
515 },
516 ParamSpec {
517 name: "birth_hi",
518 default: 0.385,
519 range: Some([0.0, 1.0]),
520 doc: "The most filled fraction of its neighbourhood at which a dead cell is born.",
521 kind: ParamKind::Modal,
522 group: ParamGroup::Shape,
523 main: false,
524 },
525 ParamSpec {
526 name: "survive_lo",
527 default: 0.26,
528 range: Some([0.0, 1.0]),
529 doc: "The least filled fraction of its neighbourhood at which a live cell survives.",
530 kind: ParamKind::Modal,
531 group: ParamGroup::Shape,
532 main: false,
533 },
534 ParamSpec {
535 name: "survive_hi",
536 default: 0.47,
537 range: Some([0.0, 1.0]),
538 doc: "The most filled fraction of its neighbourhood at which a live cell survives.",
539 kind: ParamKind::Modal,
540 group: ParamGroup::Shape,
541 main: false,
542 },
543 ParamSpec {
544 name: "states",
545 default: 3.0,
546 range: Some([2.0, MAX_STATES]),
547 doc: "How many colours the cycle holds; each cell advances to the next one round it.",
548 kind: ParamKind::Structural,
549 group: ParamGroup::Shape,
550 main: false,
551 },
552 ParamSpec {
553 name: "threshold",
554 default: 3.0,
555 range: Some([1.0, MAX_THRESHOLD]),
556 doc: "How many of its eight neighbours must already hold the next colour before a cell \
557 advances to it.",
558 kind: ParamKind::Structural,
559 group: ParamGroup::Shape,
560 main: false,
561 },
562 ParamSpec {
563 name: "step_rate",
564 default: 10.0,
565 range: Some([0.0, 60.0]),
566 doc: "How many generations the automaton runs per second, whatever the frame rate; 0 \
567 freezes it.",
568 kind: ParamKind::Modal,
569 group: ParamGroup::Motion,
570 main: true,
571 },
572 ParamSpec {
573 name: "reseed",
574 default: 0.0,
575 range: Some([0.0, 1.0]),
576 doc: "A rise past 0.5 refills one disc of the grid with fresh seeded cells, once per \
577 rise; bind a beat or a latch to it.",
578 kind: ParamKind::Modal,
579 group: ParamGroup::Motion,
580 main: false,
581 },
582 ParamSpec {
583 name: "trail",
584 default: 12.0,
585 range: Some([0.0, 64.0]),
586 doc: "How many generations a dead cell keeps glowing, fading as it goes; 0 draws only \
587 the live cells.",
588 kind: ParamKind::Modal,
589 group: ParamGroup::Motion,
590 main: false,
591 },
592 ParamSpec {
593 name: "age_tint",
594 default: 0.35,
595 range: Some([0.0, 1.0]),
596 doc: "How far along the palette a dead cell's glow travels as it fades; 0 keeps the \
597 wake the live cells' colour.",
598 kind: ParamKind::Modal,
599 group: ParamGroup::Colour,
600 main: false,
601 },
602 ParamSpec {
603 name: "route",
604 default: 0.0,
605 range: Some([0.0, 1.0]),
606 doc: "How strongly the route through the maze is drawn over it: the longest path the \
607 open cells hold, found while the maze stands still. 0 draws none and runs no \
608 search; inert on cyclic, which has no open cells.",
609 kind: ParamKind::Modal,
610 group: ParamGroup::Colour,
611 main: false,
612 },
613 ParamSpec {
614 name: "route_coord",
615 default: 0.5,
616 range: Some([0.0, 1.0]),
617 doc: "Where on the palette the route is painted, at its first end.",
618 kind: ParamKind::Modal,
619 group: ParamGroup::Colour,
620 main: false,
621 },
622 ParamSpec {
623 name: "route_grade",
624 default: 0.0,
625 range: Some([-1.0, 1.0]),
626 doc: "How far along the palette the route's colour travels from one end to the other; \
627 0 paints it one solid colour.",
628 kind: ParamKind::Modal,
629 group: ParamGroup::Colour,
630 main: false,
631 },
632 ParamSpec {
633 name: "route_reveal",
634 default: 1.0,
635 range: Some([0.0, route::MAX_REVEAL]),
636 doc: "Seconds a found route takes to draw in along its length, end to end; it fades \
637 out over a quarter of this when the maze moves. 0 draws it whole at once.",
638 kind: ParamKind::Modal,
639 group: ParamGroup::Motion,
640 main: false,
641 },
642 common::brightness(common::DEFAULT_BRIGHTNESS),
643 common::hue(DEFAULT_HUE),
644 common::zoom(DEFAULT_ZOOM),
645 common::PAN_X,
646 common::PAN_Y,
647 common::SATURATION,
648 common::PALETTE_MIX,
649 common::PALETTE_STEPS,
650 common::PALETTE_CONTOUR,
651 common::PALETTE_CONTOUR_STYLE,
652 common::PALETTE_CONTOUR_INK,
653];
654
655macro_rules! per_family {
658 ($name:literal: $life_like:expr, $larger_than_life:expr, $cyclic:expr $(,)?) => {
659 FamilyParam {
660 name: $name,
661 ranges: &[
662 FamilyRange {
663 family: "life_like",
664 range: $life_like,
665 },
666 FamilyRange {
667 family: "larger_than_life",
668 range: $larger_than_life,
669 },
670 FamilyRange {
671 family: "cyclic",
672 range: $cyclic,
673 },
674 ],
675 }
676 };
677}
678
679pub const FAMILY_PARAMS: &[FamilyParam] = &[
684 per_family!("birth": Some([0.0, MAX_RULE]), None, None),
685 per_family!("survive": Some([0.0, MAX_RULE]), None, None),
686 per_family!("radius": None, Some([1.0, MAX_RADIUS]), None),
687 per_family!("birth_lo": None, Some([0.0, 1.0]), None),
688 per_family!("birth_hi": None, Some([0.0, 1.0]), None),
689 per_family!("survive_lo": None, Some([0.0, 1.0]), None),
690 per_family!("survive_hi": None, Some([0.0, 1.0]), None),
691 per_family!("states": None, None, Some([2.0, MAX_STATES])),
692 per_family!("threshold": None, None, Some([1.0, MAX_THRESHOLD])),
693 per_family!("trail": Some([0.0, 64.0]), Some([0.0, 64.0]), None),
696 per_family!("age_tint": Some([0.0, 1.0]), Some([0.0, 1.0]), None),
697 per_family!("route": Some([0.0, 1.0]), Some([0.0, 1.0]), None),
699 per_family!("route_coord": Some([0.0, 1.0]), Some([0.0, 1.0]), None),
700 per_family!("route_grade": Some([-1.0, 1.0]), Some([-1.0, 1.0]), None),
701 per_family!(
702 "route_reveal": Some([0.0, route::MAX_REVEAL]),
703 Some([0.0, route::MAX_REVEAL]),
704 None,
705 ),
706];
707
708#[repr(C)]
719#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
720struct StepParams {
721 a: [u32; 4],
724 b: [u32; 4],
726 c: [u32; 4],
729 d: [u32; 4],
731 e: [u32; 4],
734}
735
736#[repr(C)]
737#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
738struct PresentParams {
739 a: [f32; 4],
741 b: [f32; 4],
743 c: [f32; 4],
745 d: [f32; 4],
747 e: [f32; 4],
749}
750
751struct ReadPair {
754 a: wgpu::BindGroup,
755 b: wgpu::BindGroup,
756}
757
758impl ReadPair {
759 fn for_field(&self, field: &PingPongField) -> &wgpu::BindGroup {
760 if field.reading_a() { &self.a } else { &self.b }
761 }
762}
763
764struct Resources {
767 grid: u32,
769 field: PingPongField,
770 _rows: wgpu::Texture,
774 rows_view: wgpu::TextureView,
775 seed_pipeline: wgpu::RenderPipeline,
778 stamp_pipeline: wgpu::RenderPipeline,
779 step_pipeline: wgpu::RenderPipeline,
780 rows_pipeline: wgpu::RenderPipeline,
781 present_pipeline: wgpu::RenderPipeline,
782 step_uniform: wgpu::Buffer,
783 present_uniform: wgpu::Buffer,
784 step_bg: ReadPair,
785 rows_bg: ReadPair,
786 present_bg: ReadPair,
787 present_layout: wgpu::BindGroupLayout,
790 luts: palette::LutPair,
793 route: Option<route::RouteResources>,
796}
797
798impl Resources {
799 fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat, grid: u32) -> Self {
800 let present_shader = gpu::fullscreen_shader(
801 device,
802 "cellular-present-shader",
803 gpu::FULLSCREEN_VS_UV_FLIPPED,
804 &format!("{}{}", shader::PRESENT_SHADER, shader::PRESENT_MAIN),
805 );
806
807 let field = PingPongField::new(device, grid, grid);
808
809 let step_uniform = gpu::uniform_buffer(
810 device,
811 "cellular-step-params",
812 std::mem::size_of::<StepParams>(),
813 );
814 let present_uniform = gpu::uniform_buffer(
815 device,
816 "cellular-present-params",
817 std::mem::size_of::<PresentParams>(),
818 );
819
820 let rows = device.create_texture(&wgpu::TextureDescriptor {
821 label: Some("cellular-rows"),
822 size: wgpu::Extent3d {
823 width: grid,
824 height: grid,
825 depth_or_array_layers: 1,
826 },
827 mip_level_count: 1,
828 sample_count: 1,
829 dimension: wgpu::TextureDimension::D2,
830 format: PingPongField::FORMAT,
833 usage: wgpu::TextureUsages::TEXTURE_BINDING
836 | wgpu::TextureUsages::RENDER_ATTACHMENT
837 | wgpu::TextureUsages::COPY_SRC,
838 view_formats: &[],
839 });
840 let rows_view = rows.create_view(&wgpu::TextureViewDescriptor::default());
841
842 let step_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
845 label: Some("cellular-step-layout"),
846 entries: &[
847 gpu::texture(0, false),
848 gpu::texture(1, false),
849 gpu::uniform(2, wgpu::ShaderStages::FRAGMENT),
850 ],
851 });
852 let step_bg = ReadPair {
853 a: step_bind_group(
854 device,
855 &step_layout,
856 field.view_a(),
857 &rows_view,
858 &step_uniform,
859 ),
860 b: step_bind_group(
861 device,
862 &step_layout,
863 field.view_b(),
864 &rows_view,
865 &step_uniform,
866 ),
867 };
868 let rows_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
872 label: Some("cellular-rows-layout"),
873 entries: &[
874 gpu::texture(0, false),
875 gpu::uniform(1, wgpu::ShaderStages::FRAGMENT),
876 ],
877 });
878 let rows_bg = ReadPair {
879 a: rows_bind_group(device, &rows_layout, field.view_a(), &step_uniform),
880 b: rows_bind_group(device, &rows_layout, field.view_b(), &step_uniform),
881 };
882 let rows_shader = gpu::fullscreen_shader(
883 device,
884 "cellular-rows",
885 gpu::FULLSCREEN_VS_UV_FLIPPED,
886 &format!(
887 "{}{}{}",
888 radius_const(),
889 shader::STEP_COMMON,
890 shader::ROWS_SHADER
891 ),
892 );
893 let rows_pipeline = gpu::fullscreen_pipeline(
894 device,
895 &rows_shader,
896 &[&rows_layout],
897 PingPongField::FORMAT,
898 wgpu::BlendState::REPLACE,
899 "cellular-rows",
900 );
901 let step_pipeline_for = |pass: StepPass| {
902 let label = pass.label();
903 let shader = gpu::fullscreen_shader(
904 device,
905 label,
906 gpu::FULLSCREEN_VS_UV_FLIPPED,
907 &pass.source(),
908 );
909 gpu::fullscreen_pipeline(
910 device,
911 &shader,
912 &[&step_layout],
913 PingPongField::FORMAT,
914 wgpu::BlendState::REPLACE,
915 label,
916 )
917 };
918 let seed_pipeline = step_pipeline_for(StepPass::Seed);
919 let stamp_pipeline = step_pipeline_for(StepPass::Stamp);
920 let step_pipeline = step_pipeline_for(StepPass::Step);
921
922 let luts = palette::LutPair::new(device, "cellular");
923 let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
927 label: Some("cellular-present-layout"),
928 entries: &[
929 gpu::texture(0, false),
930 gpu::texture(1, true),
931 gpu::texture(2, true),
932 gpu::sampler(3),
933 gpu::uniform(4, wgpu::ShaderStages::FRAGMENT),
934 ],
935 });
936 let present_bg = ReadPair {
937 a: present_bind_group(
938 device,
939 &present_layout,
940 field.view_a(),
941 &luts,
942 &present_uniform,
943 ),
944 b: present_bind_group(
945 device,
946 &present_layout,
947 field.view_b(),
948 &luts,
949 &present_uniform,
950 ),
951 };
952 let present_pipeline = gpu::fullscreen_pipeline(
953 device,
954 &present_shader,
955 &[&present_layout],
956 surface_format,
957 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
960 "cellular-present",
961 );
962
963 Self {
964 grid,
965 field,
966 _rows: rows,
967 rows_view,
968 seed_pipeline,
969 stamp_pipeline,
970 step_pipeline,
971 rows_pipeline,
972 present_pipeline,
973 step_uniform,
974 present_uniform,
975 step_bg,
976 rows_bg,
977 present_bg,
978 present_layout,
979 luts,
980 route: None,
981 }
982 }
983
984 fn encode_step(&mut self, encoder: &mut wgpu::CommandEncoder, pass: StepPass) {
987 let pipeline = match pass {
988 StepPass::Seed => &self.seed_pipeline,
989 StepPass::Stamp => &self.stamp_pipeline,
990 StepPass::Step => &self.step_pipeline,
991 };
992 {
993 let mut rpass = gpu::color_pass(
996 encoder,
997 pass.label(),
998 self.field.write_view(),
999 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1000 );
1001 rpass.set_pipeline(pipeline);
1002 rpass.set_bind_group(0, self.step_bg.for_field(&self.field), &[]);
1003 rpass.draw(0..3, 0..1);
1004 }
1005 self.field.swap();
1006 }
1007
1008 fn encode_rows(&self, encoder: &mut wgpu::CommandEncoder) {
1012 let mut rpass = gpu::color_pass(
1013 encoder,
1014 "cellular-rows",
1015 &self.rows_view,
1016 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1017 );
1018 rpass.set_pipeline(&self.rows_pipeline);
1019 rpass.set_bind_group(0, self.rows_bg.for_field(&self.field), &[]);
1020 rpass.draw(0..3, 0..1);
1021 }
1022}
1023
1024fn radius_const() -> String {
1026 format!("const MAX_RADIUS: i32 = {};\n", MAX_RADIUS as i32)
1027}
1028
1029#[derive(Clone, Copy)]
1031enum StepPass {
1032 Seed,
1034 Stamp,
1036 Step,
1038}
1039
1040impl StepPass {
1041 fn mode(self) -> u32 {
1043 match self {
1044 StepPass::Step => 0,
1045 StepPass::Seed => 1,
1046 StepPass::Stamp => 2,
1047 }
1048 }
1049
1050 fn label(self) -> &'static str {
1051 match self {
1052 StepPass::Seed => "cellular-seed",
1053 StepPass::Stamp => "cellular-stamp",
1054 StepPass::Step => "cellular-step",
1055 }
1056 }
1057
1058 fn source(self) -> String {
1061 format!(
1062 "const MODE: u32 = {}u;\nconst AGE_CAP: f32 = {:?};\n{}{}{}{}{}",
1063 self.mode(),
1064 AGE_CAP,
1065 radius_const(),
1066 gpu::HASH_WGSL,
1067 common::CELL_HASH_WGSL,
1068 shader::STEP_COMMON,
1069 shader::STEP_SHADER
1070 )
1071 }
1072}
1073
1074fn step_bind_group(
1075 device: &wgpu::Device,
1076 layout: &wgpu::BindGroupLayout,
1077 input: &wgpu::TextureView,
1078 rows: &wgpu::TextureView,
1079 uniform: &wgpu::Buffer,
1080) -> wgpu::BindGroup {
1081 device.create_bind_group(&wgpu::BindGroupDescriptor {
1082 label: Some("cellular-step-bg"),
1083 layout,
1084 entries: &[
1085 wgpu::BindGroupEntry {
1086 binding: 0,
1087 resource: wgpu::BindingResource::TextureView(input),
1088 },
1089 wgpu::BindGroupEntry {
1090 binding: 1,
1091 resource: wgpu::BindingResource::TextureView(rows),
1092 },
1093 wgpu::BindGroupEntry {
1094 binding: 2,
1095 resource: uniform.as_entire_binding(),
1096 },
1097 ],
1098 })
1099}
1100
1101fn rows_bind_group(
1102 device: &wgpu::Device,
1103 layout: &wgpu::BindGroupLayout,
1104 input: &wgpu::TextureView,
1105 uniform: &wgpu::Buffer,
1106) -> wgpu::BindGroup {
1107 device.create_bind_group(&wgpu::BindGroupDescriptor {
1108 label: Some("cellular-rows-bg"),
1109 layout,
1110 entries: &[
1111 wgpu::BindGroupEntry {
1112 binding: 0,
1113 resource: wgpu::BindingResource::TextureView(input),
1114 },
1115 wgpu::BindGroupEntry {
1116 binding: 1,
1117 resource: uniform.as_entire_binding(),
1118 },
1119 ],
1120 })
1121}
1122
1123fn present_bind_group(
1124 device: &wgpu::Device,
1125 layout: &wgpu::BindGroupLayout,
1126 input: &wgpu::TextureView,
1127 luts: &palette::LutPair,
1128 uniform: &wgpu::Buffer,
1129) -> wgpu::BindGroup {
1130 let [lut_a, lut_b, lut_sampler] = luts.bind_entries(1, 2, 3);
1131 device.create_bind_group(&wgpu::BindGroupDescriptor {
1132 label: Some("cellular-present-bg"),
1133 layout,
1134 entries: &[
1135 wgpu::BindGroupEntry {
1136 binding: 0,
1137 resource: wgpu::BindingResource::TextureView(input),
1138 },
1139 lut_a,
1140 lut_b,
1141 lut_sampler,
1142 wgpu::BindGroupEntry {
1143 binding: 4,
1144 resource: uniform.as_entire_binding(),
1145 },
1146 ],
1147 })
1148}
1149
1150pub struct CellularScene {
1153 device: wgpu::Device,
1155 surface_format: wgpu::TextureFormat,
1156 res: Option<Resources>,
1157 config: CellularConfig,
1159 needs_seed: bool,
1162 clock: GenerationClock,
1163 dt: f32,
1166 pending_generations: u32,
1168 stamp_rng: SeededRng,
1171 pending_stamp: Option<Stamp>,
1173 prev_reseed: f32,
1175 radius_cap: f32,
1178 grid_cap: u32,
1181 clamp: Option<super::CapOverflow>,
1186 birth: f32,
1187 survive: f32,
1188 radius: f32,
1189 birth_lo: f32,
1190 birth_hi: f32,
1191 survive_lo: f32,
1192 survive_hi: f32,
1193 states: f32,
1194 threshold: f32,
1195 step_rate: f32,
1196 reseed: f32,
1197 trail: f32,
1198 age_tint: f32,
1199 route: f32,
1200 route_coord: f32,
1201 route_grade: f32,
1202 route_reveal: f32,
1203 route_rate: f32,
1206 route_clock: route::RouteClock,
1207 generation_ticks: route::Ticks,
1210 pass_ticks: route::Ticks,
1211 frame_start: f64,
1215 scene_time: f64,
1216 route_reset: bool,
1219 colour: common::PaletteParams,
1220 pan: common::PanParams,
1221 zoom: f32,
1222 occlude: f32,
1227 palette: Palette,
1230}
1231
1232impl CellularScene {
1233 pub fn new(
1241 device: &wgpu::Device,
1242 surface_format: wgpu::TextureFormat,
1243 radius_cap: u32,
1244 grid_cap: u32,
1245 route_rate: u32,
1246 ) -> Self {
1247 let config = CellularConfig::default();
1248 Self {
1249 device: device.clone(),
1250 surface_format,
1251 res: None,
1252 config,
1253 needs_seed: true,
1254 clock: GenerationClock::default(),
1255 dt: 0.0,
1256 pending_generations: 0,
1257 stamp_rng: stamp_rng(config.salt),
1258 pending_stamp: None,
1259 prev_reseed: 0.0,
1260 radius_cap: (radius_cap as f32).clamp(1.0, MAX_RADIUS),
1261 grid_cap: grid_cap.clamp(MIN_GRID, MAX_GRID),
1262 clamp: None,
1263 birth: DEFAULT_BIRTH,
1264 survive: DEFAULT_SURVIVE,
1265 radius: DEFAULT_RADIUS,
1266 birth_lo: DEFAULT_BIRTH_LO,
1267 birth_hi: DEFAULT_BIRTH_HI,
1268 survive_lo: DEFAULT_SURVIVE_LO,
1269 survive_hi: DEFAULT_SURVIVE_HI,
1270 states: DEFAULT_STATES,
1271 threshold: DEFAULT_THRESHOLD,
1272 step_rate: DEFAULT_STEP_RATE,
1273 reseed: 0.0,
1274 trail: DEFAULT_TRAIL,
1275 age_tint: DEFAULT_AGE_TINT,
1276 route: DEFAULT_ROUTE,
1277 route_coord: DEFAULT_ROUTE_COORD,
1278 route_grade: DEFAULT_ROUTE_GRADE,
1279 route_reveal: DEFAULT_ROUTE_REVEAL,
1280 route_rate: route_rate as f32,
1281 route_clock: route::RouteClock::default(),
1282 generation_ticks: route::Ticks::default(),
1283 pass_ticks: route::Ticks::default(),
1284 frame_start: 0.0,
1285 scene_time: 0.0,
1286 route_reset: true,
1287 colour: common::PaletteParams::new(DEFAULT_HUE, common::DEFAULT_BRIGHTNESS),
1288 pan: common::PanParams::default(),
1289 zoom: DEFAULT_ZOOM,
1290 occlude: crate::render::post::DEFAULT_OCCLUDE,
1291 palette: Palette::default_spectrum(),
1292 }
1293 }
1294
1295 fn route_on(&self) -> bool {
1298 self.route.is_finite() && self.route > 0.0 && self.config.family != CellularFamily::Cyclic
1299 }
1300
1301 fn step_params(&self, stamp: Option<Stamp>) -> StepParams {
1304 let stamp = stamp.unwrap_or(Stamp {
1305 centre: [0, 0],
1306 radius_sq: 0,
1307 seed: 0,
1308 });
1309 let radius = applied_radius(self.radius, self.radius_cap);
1310 let birth = interval_counts(
1311 self.birth_lo,
1312 self.birth_hi,
1313 radius,
1314 [DEFAULT_BIRTH_LO, DEFAULT_BIRTH_HI],
1315 );
1316 let survive = interval_counts(
1317 self.survive_lo,
1318 self.survive_hi,
1319 radius,
1320 [DEFAULT_SURVIVE_LO, DEFAULT_SURVIVE_HI],
1321 );
1322 StepParams {
1323 a: [
1324 self.config.family.index(),
1325 u32::from(self.config.wrap),
1326 self.config.grid,
1327 live_threshold(self.config.family.density()),
1328 ],
1329 b: [
1330 applied_rule(self.birth, DEFAULT_BIRTH),
1331 applied_rule(self.survive, DEFAULT_SURVIVE),
1332 radius,
1333 applied_states(self.states),
1334 ],
1335 c: [field_seed(self.config.salt), stamp.seed, stamp.radius_sq, 0],
1336 d: [
1337 stamp.centre[0],
1338 stamp.centre[1],
1339 applied_threshold(self.threshold),
1340 0,
1341 ],
1342 e: [birth[0], birth[1], survive[0], survive[1]],
1343 }
1344 }
1345}
1346
1347impl Scene for CellularScene {
1348 fn name(&self) -> &'static str {
1349 "cellular"
1350 }
1351
1352 fn advance(&mut self, dt: f32) {
1353 self.dt = dt;
1354 }
1355
1356 fn set_occlude(&mut self, occlude: f32) {
1357 self.occlude = occlude;
1358 }
1359
1360 fn set_palette(&mut self, palette: &Palette) {
1361 self.palette = palette.clone();
1362 if let Some(res) = self.res.as_mut() {
1363 res.luts.set(palette);
1364 }
1365 }
1366
1367 fn configure(&mut self, cfg: &GeneratorConfig) -> Option<super::CapOverflow> {
1368 let GeneratorConfig::Cellular(config) = cfg else {
1369 return None;
1370 };
1371 let (config, overflow) = grid_clamp(*config, self.grid_cap);
1376 self.config = config;
1377 self.needs_seed = true;
1378 self.clock = GenerationClock::default();
1379 self.pending_generations = 0;
1380 self.stamp_rng = stamp_rng(config.salt);
1381 self.pending_stamp = None;
1382 self.prev_reseed = 0.0;
1383 self.clamp = None;
1385 self.route_clock.reset();
1387 self.generation_ticks = route::Ticks::default();
1388 self.pass_ticks = route::Ticks::default();
1389 self.frame_start = 0.0;
1390 self.scene_time = 0.0;
1391 self.route_reset = true;
1392 overflow
1393 }
1394
1395 fn mirror_overflow(&self) -> Option<&super::CapOverflow> {
1396 self.clamp.as_ref()
1397 }
1398
1399 fn reset_params(&mut self) {
1400 self.birth = DEFAULT_BIRTH;
1401 self.survive = DEFAULT_SURVIVE;
1402 self.radius = DEFAULT_RADIUS;
1403 self.birth_lo = DEFAULT_BIRTH_LO;
1404 self.birth_hi = DEFAULT_BIRTH_HI;
1405 self.survive_lo = DEFAULT_SURVIVE_LO;
1406 self.survive_hi = DEFAULT_SURVIVE_HI;
1407 self.states = DEFAULT_STATES;
1408 self.threshold = DEFAULT_THRESHOLD;
1409 self.step_rate = DEFAULT_STEP_RATE;
1410 self.reseed = 0.0;
1411 self.trail = DEFAULT_TRAIL;
1412 self.age_tint = DEFAULT_AGE_TINT;
1413 self.route = DEFAULT_ROUTE;
1414 self.route_coord = DEFAULT_ROUTE_COORD;
1415 self.route_grade = DEFAULT_ROUTE_GRADE;
1416 self.route_reveal = DEFAULT_ROUTE_REVEAL;
1417 self.colour.reset();
1418 self.pan.reset();
1419 self.zoom = DEFAULT_ZOOM;
1420 }
1421
1422 fn set_param(&mut self, name: &str, value: f32) {
1423 if self.colour.set(name, value) || self.pan.set(name, value) {
1426 return;
1427 }
1428 match name {
1429 "birth" => self.birth = value,
1430 "survive" => self.survive = value,
1431 "radius" => self.radius = value,
1432 "birth_lo" => self.birth_lo = value,
1433 "birth_hi" => self.birth_hi = value,
1434 "survive_lo" => self.survive_lo = value,
1435 "survive_hi" => self.survive_hi = value,
1436 "states" => self.states = value,
1437 "threshold" => self.threshold = value,
1438 "step_rate" => self.step_rate = value,
1439 "reseed" => self.reseed = value,
1440 "trail" => self.trail = value,
1441 "age_tint" => self.age_tint = value,
1442 "route" => self.route = value,
1443 "route_coord" => self.route_coord = value,
1444 "route_grade" => self.route_grade = value,
1445 "route_reveal" => self.route_reveal = value,
1446 "zoom" => self.zoom = value,
1447 _ => {}
1448 }
1449 }
1450
1451 fn update(&mut self, _frame: &AnalysisFrame) {
1452 let owed_before = self.clock.owed();
1453 self.pending_generations = self.clock.advance(self.step_rate, self.dt);
1454 self.generation_ticks = route::Ticks {
1455 owed_before,
1456 rate: f64::from(applied_step_rate(self.step_rate)),
1457 count: self.pending_generations,
1458 };
1459 self.pass_ticks = if self.route_on() {
1460 self.route_clock.advance(self.route_rate, self.dt)
1461 } else {
1462 self.route_clock.reset();
1463 route::Ticks::default()
1464 };
1465 self.frame_start = self.scene_time;
1466 self.scene_time += f64::from(self.dt);
1467 let reseed = if self.reseed.is_finite() {
1472 self.reseed
1473 } else {
1474 0.0
1475 };
1476 if reseed >= RESEED_THRESHOLD && self.prev_reseed < RESEED_THRESHOLD {
1477 self.pending_stamp = Some(next_stamp(&mut self.stamp_rng, self.config.grid));
1478 }
1479 self.prev_reseed = reseed;
1480 }
1481
1482 fn render(
1483 &mut self,
1484 queue: &wgpu::Queue,
1485 encoder: &mut wgpu::CommandEncoder,
1486 view: &wgpu::TextureView,
1487 _aspect: f32,
1488 ) {
1489 let grid = self.config.grid;
1490 if self.res.as_ref().is_none_or(|res| res.grid != grid) {
1491 let mut built = Resources::build(&self.device, self.surface_format, grid);
1492 built.luts.set(&self.palette);
1493 self.res = Some(built);
1494 self.needs_seed = true;
1495 }
1496
1497 let seed = std::mem::replace(&mut self.needs_seed, false);
1498 let stamp = self.pending_stamp.take();
1499 let generations = std::mem::take(&mut self.pending_generations);
1500 let step = self.step_params(stamp);
1501 self.clamp = radius_clamp(self.config.family, self.radius, self.radius_cap);
1502 let present = PresentParams {
1503 a: [
1504 self.colour.hue,
1505 self.colour.brightness,
1506 self.colour.saturation,
1507 self.colour.mix,
1508 ],
1509 b: [
1510 palette::band_steps(self.colour.steps),
1511 palette::band_contour(self.colour.contour),
1512 self.occlude,
1513 grid as f32,
1514 ],
1515 c: [
1516 if self.zoom.is_finite() && self.zoom > 1e-3 {
1517 self.zoom
1518 } else {
1519 DEFAULT_ZOOM
1520 },
1521 if self.pan.x.is_finite() {
1522 self.pan.x
1523 } else {
1524 0.0
1525 },
1526 if self.pan.y.is_finite() {
1527 self.pan.y
1528 } else {
1529 0.0
1530 },
1531 if self.config.wrap { 1.0 } else { 0.0 },
1532 ],
1533 d: [
1534 applied_trail(self.trail),
1535 if self.age_tint.is_finite() {
1536 self.age_tint
1537 } else {
1538 DEFAULT_AGE_TINT
1539 },
1540 if self.config.family == CellularFamily::Cyclic {
1541 1.0
1542 } else {
1543 0.0
1544 },
1545 applied_states(self.states) as f32,
1546 ],
1547 e: [
1548 palette::band_contour_style(self.colour.contour_style),
1549 self.colour.contour_ink,
1550 0.0,
1551 0.0,
1552 ],
1553 };
1554
1555 let route_on = self.route_on();
1556 let reveal = route::applied_reveal(self.route_reveal, DEFAULT_ROUTE_REVEAL);
1557 let route_present = route::RoutePresent {
1558 a: [
1559 self.route.clamp(0.0, 1.0),
1560 finite_or(self.route_coord, DEFAULT_ROUTE_COORD),
1561 finite_or(self.route_grade, DEFAULT_ROUTE_GRADE),
1562 reveal,
1563 ],
1564 b: [route::scene_ms(self.scene_time), 0, 0, 0],
1565 };
1566 let route_frame = route::FrameParams {
1567 wrap: self.config.wrap,
1568 fade_ms: route::fade_ms(reveal),
1569 };
1570 let generation_ticks = route::Ticks {
1573 count: generations,
1574 ..std::mem::take(&mut self.generation_ticks)
1575 };
1576 let pass_ticks = std::mem::take(&mut self.pass_ticks);
1577
1578 let Some(res) = self.res.as_mut() else {
1579 return;
1580 };
1581 if route_on && res.route.as_ref().is_none_or(|route| route.grid != grid) {
1582 res.route = Some(route::RouteResources::build(
1583 &self.device,
1584 self.surface_format,
1585 grid,
1586 &res.field,
1587 &res.present_layout,
1588 ));
1589 self.route_reset = true;
1590 }
1591 res.luts.flush(queue);
1592 queue.write_buffer(&res.present_uniform, 0, bytemuck::bytes_of(&present));
1593
1594 if seed || stamp.is_some() || generations > 0 {
1598 queue.write_buffer(&res.step_uniform, 0, bytemuck::bytes_of(&step));
1599 }
1600 if seed {
1601 res.encode_step(encoder, StepPass::Seed);
1602 }
1603 if stamp.is_some() {
1604 res.encode_step(encoder, StepPass::Stamp);
1605 }
1606 let sums_rows = self.config.family.sums_rows();
1607 let mut route_res = if route_on { res.route.take() } else { None };
1611 if let Some(route) = route_res.as_mut() {
1612 if std::mem::take(&mut self.route_reset) {
1613 route.reset(queue);
1614 }
1615 route.begin_frame();
1616 let timeline = route::Timeline::new(
1617 generation_ticks,
1618 pass_ticks,
1619 self.frame_start,
1620 f64::from(self.dt),
1621 );
1622 for (event, ms) in timeline {
1623 match event {
1624 route::RouteEvent::Generation => {
1625 if sums_rows {
1626 res.encode_rows(encoder);
1627 }
1628 res.encode_step(encoder, StepPass::Step);
1629 route.encode_generation(encoder, &res.field, route_frame, ms);
1630 }
1631 route::RouteEvent::Pass => {
1632 route.encode_pass(encoder, &res.field, route_frame, ms)
1633 }
1634 }
1635 }
1636 route.finish_frame(queue);
1637 queue.write_buffer(
1638 &route.present_uniform,
1639 0,
1640 bytemuck::bytes_of(&route_present),
1641 );
1642 } else {
1643 for _ in 0..generations {
1644 if sums_rows {
1645 res.encode_rows(encoder);
1646 }
1647 res.encode_step(encoder, StepPass::Step);
1648 }
1649 }
1650 if route_res.is_some() {
1651 res.route = route_res;
1652 }
1653
1654 let mut pass = gpu::color_pass(encoder, "cellular-present-pass", view, wgpu::LoadOp::Load);
1657 match res.route.as_ref().filter(|_| route_on) {
1658 Some(route) => {
1659 pass.set_pipeline(&route.present_pipeline);
1660 pass.set_bind_group(0, res.present_bg.for_field(&res.field), &[]);
1661 pass.set_bind_group(1, &route.present_bg, &[]);
1662 }
1663 None => {
1664 pass.set_pipeline(&res.present_pipeline);
1665 pass.set_bind_group(0, res.present_bg.for_field(&res.field), &[]);
1666 }
1667 }
1668 pass.draw(0..3, 0..1);
1669 }
1670}
1671
1672fn finite_or(value: f32, fallback: f32) -> f32 {
1674 if value.is_finite() { value } else { fallback }
1675}
1676
1677#[cfg(test)]
1678mod mirror;
1679#[cfg(test)]
1680mod tests;