bevy_elements 0.7.8

Living environments for Bevy: sun, moon and stars, climate-driven weather, GPU wind, volumetric clouds, rain, snow and fog, lightning, terrain with hydraulic and thermal erosion, flooding, snow cover, wetness and dust — simulated in compute shaders.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
//! Heightfield terrain: the `Terrain` component, its generator or heightmap import, the layer
//! buffers the hydrology and erosion kernels write, the packed render buffer, and the readback
//! behind [`Environment::terrain_height`](crate::Environment::terrain_height). See
//! `docs/research/terrain.md`.
//!
//! One terrain exists at a time (a second `Terrain` entity logs a warning and is ignored). It is
//! centred on its entity's `Transform`: the grid's minimum corner is `translation.xz − extent / 2`
//! and a stored height `h` is the height `translation.y + h` above the entity's origin, all in
//! the metre frame (world ÷ [`ElementsUnits::units_per_metre`](crate::ElementsUnits): the
//! entity's translation is read through it, the patches are written ×units). Rotation and scale
//! are not supported.

mod gpu;
#[cfg(test)]
mod gpu_tests;
pub(crate) mod lod;
mod material;
pub(crate) mod model;
mod sculpt;
mod textures;

use std::collections::HashMap;
use std::ops::Range;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};

use crate::gpu::grid::zeroed_buffer;
use crate::gpu::readback::{Readbacks, decode};
use crate::plugin::{
    ElementsFocus, ElementsQuality, ElementsSystems, ElementsUnits, ElementsView, ensure_gpu_core,
};
use crate::query::EnvironmentSnapshot;
use bevy::asset::{RenderAssetUsages, embedded_asset};
use bevy::camera::primitives::Aabb;
use bevy::camera::visibility::NoAutoAabb;
use bevy::math::UVec4;
use bevy::mesh::{Indices, MeshTag, PrimitiveTopology};
use bevy::pbr::MaterialPlugin;
use bevy::prelude::*;
use bevy::render::render_resource::{Extent3d, ShaderType, TextureDimension, TextureFormat};
use bevy::render::storage::ShaderBuffer;
use bevy::shader::load_shader_library;
use bevy::transform::TransformSystems;
pub(crate) use gpu::TerrainDispatch;
use lod::{BoundsTree, LodConfig, Patch, select_patches};
pub use material::{
    GroundExtension, GroundMaterial, GroundParams, TerrainExtension, TerrainExtensionKey,
    TerrainLook, TerrainMaterial, TerrainMaterialParams,
};
pub(crate) use material::{
    NO_TERRAIN, insert_placeholder, lod_centre_uniform, lod_uniform, shadow_inputs,
};
use model::{
    F_HALF, GAIN, LACUNARITY, LEAF_CELLS, OCTAVE_ROTATION, R_LOW, R_SPAN, octave_count,
    resolve_resolution, seed_mix,
};
pub use model::{TerrainGrid, TerrainSnapshot};
pub use sculpt::{
    DEFAULT_SCULPT_HARDNESS, RestoreTerrain, SculptKind, SculptTerrain, sculpt_weight,
};
pub use textures::{GroundLayerTextures, TerrainTextures};

/// Largest readback edge: a larger simulation grid is box-averaged down by 2 first.
const READBACK_MAX_CELLS: u32 = 1024;

/// How a terrain's surface is made.
#[derive(Clone, Debug, PartialEq)]
pub enum TerrainSource {
    /// Generated on the GPU from noise, once, when the terrain spawns.
    Procedural(ProceduralTerrain),
    /// Resampled from the red channel of an image. Heights are
    /// `lerp(height_range_m.start, height_range_m.end, value)` for a value in 0–1.
    ///
    /// The image must be read linearly: a `*Srgb` format (what the image loader yields for an
    /// 8-bit file by default) logs an error and leaves the terrain unspawned. Load the file with
    /// `ImageLoaderSettings { is_srgb: false, expand_grayscale: false, .. }`; 16-bit files are
    /// recommended (an 8-bit map has 256 height steps). Accepted formats: `R8Unorm`,
    /// `Rgba8Unorm`, `R16Unorm`, `Rgba16Unorm`, `R16Uint`, `R32Float`, `Rgba16Float`,
    /// `Rgba32Float`.
    Heightmap {
        /// The height map.
        image: Handle<Image>,
        /// Heights of the lowest and highest values, m.
        height_range_m: Range<f32>,
    },
    /// Level ground at this height, m.
    Flat(f32),
}

/// Parameters of the procedural generator (Musgrave fBm blended with a ridged multifractal).
#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ProceduralTerrain {
    /// Seed of the noise offsets.
    pub seed: u64,
    /// Height of the lowest ground, m. 0 in every preset.
    pub base_height_m: f32,
    /// Height range of one noise component, m. Blending the two components compresses the range
    /// of the sum (`terrain.md`: hills measure 96.7 m of 120).
    pub relief_m: f32,
    /// Wavelength of the base octave, m. The ratio `relief_m / feature_size_m` sets the slopes.
    pub feature_size_m: f32,
    /// Octaves, at most `⌊log₂(feature / 2 cell)⌋ + 1` (the finest wavelength stays above two
    /// cells).
    pub octaves: u32,
    /// Blend of the ridged multifractal into the fBm, 0–1.
    pub ridged: f32,
    /// Sediment laid over the generated surface, m (the surface stays the generated field; the
    /// bedrock is lowered by this depth).
    pub initial_sand_m: f32,
}

impl ProceduralTerrain {
    const fn preset(
        seed: u64,
        feature: f32,
        relief: f32,
        ridged: f32,
        octaves: u32,
        sand: f32,
    ) -> Self {
        Self {
            seed,
            base_height_m: 0.0,
            relief_m: relief,
            feature_size_m: feature,
            octaves,
            ridged,
            initial_sand_m: sand,
        }
    }

    /// Rolling hills: relief 120 m, features 600 m, a little ridging.
    pub const fn hills(seed: u64) -> Self {
        Self::preset(seed, 600.0, 120.0, 0.2, 8, 0.0)
    }

    /// Mountains: relief 900 m, features 1500 m, mostly ridged. Steep: 70 % of the ground starts
    /// above 40° (erosion relaxes it to the talus angle).
    pub const fn mountains(seed: u64) -> Self {
        Self::preset(seed, 1500.0, 900.0, 0.7, 8, 0.0)
    }

    /// Dunes: relief 40 m, features 300 m, smooth, under 4 m of sand.
    pub const fn dunes(seed: u64) -> Self {
        Self::preset(seed, 300.0, 40.0, 0.0, 7, 4.0)
    }

    /// Plains: relief 15 m, features 800 m.
    pub const fn plains(seed: u64) -> Self {
        Self::preset(seed, 800.0, 15.0, 0.0, 8, 0.0)
    }

    /// The surface this generator draws, evaluated on the CPU: `size.x × size.y` heights, m,
    /// row-major (`z * size.x + x`), the height of cell `(x, z)` taken at its centre
    /// `((x, z) + ½) · cell_m` from the terrain's minimum corner, relative to the terrain
    /// transform, with the octave count the GPU kernel uses for that cell size. Needs no app, no
    /// GPU and no [`TerrainPlugin`]: a tool can bake a preset into a height map, a game can
    /// preview it or test against it. The kernel evaluates the same noise in `f32` on the GPU,
    /// so the two agree to within the sampling tolerance of `terrain.md`, not bit for bit. It
    /// is deterministic per seed. A zero `size` or a `cell_m` that is not a positive finite
    /// number gives an empty vector.
    ///
    /// Sediment ([`initial_sand_m`](Self::initial_sand_m)) is not part of the surface.
    pub fn sample_surface(&self, size: UVec2, cell_m: f32) -> Vec<f32> {
        if size.x == 0 || size.y == 0 || !(cell_m.is_finite() && cell_m > 0.0) {
            return Vec::new();
        }
        model::surface::generate_surface(self, size, cell_m)
    }
}

/// Who draws the ground.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TerrainRender {
    /// The crate draws the ground with its own level-of-detail mesh and layer-blending material.
    #[default]
    Owned,
    /// The consumer's meshes are the ground: the crate simulates and shades through
    /// `GroundExtension` only.
    External,
}

/// The terrain. Spawn one entity with it. It is centred on the entity's `Transform` (the grid's
/// minimum corner is `translation.xz − extent / 2` and a stored height `h` is the height
/// `translation.y + h`, both read through [`ElementsUnits`](crate::ElementsUnits): `Terrain`'s
/// own lengths are metres); rotation and scale are ignored. One terrain exists at a time: a second
/// `Terrain` entity logs a warning and is ignored.
#[derive(Component, Clone, Debug, PartialEq)]
#[require(Transform, Visibility)]
pub struct Terrain {
    /// Where the surface comes from.
    pub source: TerrainSource,
    /// World size, m. Default 2048 × 2048.
    pub extent_m: Vec2,
    /// Cells per axis: clamped to
    /// [`ElementsQuality::terrain_max_resolution`] and rounded down to a power of two. Cells
    /// are square: when `extent_m` and `resolution` disagree about the cell size, the coarser
    /// axis's cell is kept and both counts are re-derived from it (with a warning), so neither
    /// exceeds the cap. Default 1024².
    pub resolution: UVec2,
    /// Who draws it.
    pub render: TerrainRender,
}

impl Terrain {
    fn with_source(source: TerrainSource) -> Self {
        Self {
            source,
            extent_m: Vec2::splat(2048.0),
            resolution: UVec2::splat(1024),
            render: TerrainRender::Owned,
        }
    }

    /// A procedurally generated terrain.
    pub fn procedural(procedural: ProceduralTerrain) -> Self {
        Self::with_source(TerrainSource::Procedural(procedural))
    }

    /// A terrain resampled from a height map; see [`TerrainSource::Heightmap`].
    pub fn from_heightmap(image: Handle<Image>, height_range_m: Range<f32>) -> Self {
        Self::with_source(TerrainSource::Heightmap {
            image,
            height_range_m,
        })
    }

    /// Level ground at `height_m`.
    pub fn flat(height_m: f32) -> Self {
        Self::with_source(TerrainSource::Flat(height_m))
    }

    /// Set the world size, m.
    pub fn with_extent(mut self, extent_m: Vec2) -> Self {
        self.extent_m = extent_m;
        self
    }

    /// Set the cells per axis (before clamping to the quality level).
    pub fn with_resolution(mut self, resolution: UVec2) -> Self {
        self.resolution = resolution;
        self
    }

    /// The consumer draws the ground ([`TerrainRender::External`]).
    pub fn external(mut self) -> Self {
        self.render = TerrainRender::External;
        self
    }
}

impl TerrainSnapshot {
    /// Lowest to highest stored surface height, m (relative to `grid.y0`). A level surface is
    /// widened to one metre so the range is never empty.
    pub fn height_range_m(&self) -> Range<f32> {
        let (lo, hi) = self
            .surface
            .iter()
            .fold((f32::INFINITY, f32::NEG_INFINITY), |(lo, hi), &h| {
                (lo.min(h), hi.max(h))
            });
        if hi <= lo {
            let lo = if lo.is_finite() { lo } else { 0.0 };
            return lo..lo + 1.0;
        }
        lo..hi
    }

    /// The surface as a height map [`TerrainSource::Heightmap`] reads back exactly: an
    /// `R32Float` image of one texel per cell holding `(h − lo) / (hi − lo)`, and the range
    /// `lo..hi` that decodes it ([`height_range_m`](Self::height_range_m)).
    pub fn to_heightmap(&self) -> (Image, Range<f32>) {
        let range = self.height_range_m();
        let span = range.end - range.start;
        let bytes: Vec<u8> = self
            .surface
            .iter()
            .flat_map(|&h| ((h - range.start) / span).to_le_bytes())
            .collect();
        let image = Image::new(
            Extent3d {
                width: self.grid.size.x,
                height: self.grid.size.y,
                depth_or_array_layers: 1,
            },
            TextureDimension::D2,
            bytes,
            TextureFormat::R32Float,
            RenderAssetUsages::default(),
        );
        (image, range)
    }

    /// A [`Terrain`] that redraws this surface: the height map of
    /// [`to_heightmap`](Self::to_heightmap) added to `images`, at the grid's extent and
    /// resolution. Spawn it at [`centre_m`](Self::centre_m) (times
    /// [`ElementsUnits::units_per_metre`](crate::ElementsUnits::units_per_metre)) so the ground
    /// lies where it was. The surface comes back exactly, all of it as bedrock: the sediment
    /// layer over the rock is not restored.
    pub fn terrain(&self, images: &mut Assets<Image>) -> Terrain {
        let (image, range) = self.to_heightmap();
        Terrain::from_heightmap(images.add(image), range)
            .with_extent(self.grid.extent())
            .with_resolution(self.grid.size)
    }

    /// Where the terrain entity sits in the metre frame: the grid's centre in XZ and `grid.y0`
    /// in Y.
    pub fn centre_m(&self) -> Vec3 {
        let c = self.grid.origin + 0.5 * self.grid.extent();
        Vec3::new(c.x, self.grid.y0, c.y)
    }
}

/// The terrain's GPU layers and where they sit. Exists while a [`Terrain`] does.
///
/// Every layer is a row-major per-cell storage buffer on [`grid`](Self::grid) (row index along
/// world +Z). The surface height is `bedrock + sediment`; the packed `cells` and `flow_cells`
/// are what materials and other kernels read. Hydrology and erosion write the simulation layers;
/// everything else treats them as read-only.
#[derive(Resource, Clone, Debug)]
pub struct TerrainState {
    /// The terrain entity.
    pub entity: Entity,
    /// Placement of the simulation grid.
    pub grid: TerrainGrid,
    /// Bedrock height, `array<f32>`, m above `grid.y0`.
    pub bedrock: Handle<ShaderBuffer>,
    /// Loose sediment depth, `array<f32>`, m.
    pub sediment: Handle<ShaderBuffer>,
    /// Water depth above the surface, `array<f32>`, m.
    pub water: Handle<ShaderBuffer>,
    /// Outflow fluxes `(left, right, top, bottom)`, `array<vec4<f32>>`, m³/s.
    pub flux: Handle<ShaderBuffer>,
    /// Depth-averaged water velocity, `array<vec2<f32>>`, m/s.
    pub velocity: Handle<ShaderBuffer>,
    /// Suspended sediment, `array<f32>`, height-equivalent m.
    pub suspended: Handle<ShaderBuffer>,
    /// Scratch of the flux kernel, `array<vec2<f32>>`.
    pub transport: Handle<ShaderBuffer>,
    /// Water source map, `array<vec2<f32>>`.
    pub source_map: Handle<ShaderBuffer>,
    /// Thermal erosion fluxes, a ping-pong pair of `array<vec4<f32>>`.
    pub thermal_flux: [Handle<ShaderBuffer>; 2],
    /// The packed render buffer, `array<vec4<f32>>`: surface height, sediment depth, water depth,
    /// `bitcast<f32>(pack2x16float(∂h/∂x, ∂h/∂z))`.
    pub cells: Handle<ShaderBuffer>,
    /// The packed water-render buffer, `array<vec4<f32>>`: velocity xy, turbidity, spare.
    pub flow_cells: Handle<ShaderBuffer>,
    /// Surface height and sediment thickness per readback cell, `array<vec2<f32>>`
    /// (`readback_size` cells; the grid box-averaged by 2 above 1024²).
    pub height_readback: Handle<ShaderBuffer>,
    /// Bounds of every 32 × 32-cell leaf, `array<vec4<f32>>`: surface min, surface max, water
    /// surface max, water depth max.
    pub leaf_bounds: Handle<ShaderBuffer>,
    /// Cells of `height_readback` per axis.
    pub readback_size: UVec2,
    /// The generator's `n`-th completed pack, 0 before the terrain exists on the GPU. Rises
    /// whenever the packed buffer was rewritten (hydrology, erosion and edits bump it).
    pub version: Arc<AtomicU32>,
}

impl TerrainState {
    /// Whether the generator and the first pack have run on the GPU.
    pub fn is_live(&self) -> bool {
        self.version.load(Ordering::Relaxed) > 0
    }
}

/// Uniform of the terrain kernels and, embedded, of every shader that reads `terrain_cells`.
/// Mirrors `TerrainParams` of `common.wesl`.
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType)]
pub(crate) struct TerrainParamsGpu {
    origin: Vec2,
    cell: f32,
    pad0: f32,
    size: UVec2,
    pad1: UVec2,
    /// x: y0. y: readback downsample. z: live flag.
    misc: Vec4,
}

impl TerrainParamsGpu {
    pub(crate) fn new(grid: &TerrainGrid, downsample: u32, live: bool) -> Self {
        Self {
            origin: grid.origin,
            cell: grid.cell_m,
            pad0: 0.0,
            size: grid.size,
            pad1: UVec2::ZERO,
            misc: Vec4::new(grid.y0, downsample as f32, f32::from(u8::from(live)), 0.0),
        }
    }
}

/// Uniform of `generate.wesl`.
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType)]
pub(crate) struct GenParams {
    size: UVec4,
    shape: Vec4,
    base: Vec4,
    consts: Vec4,
    octave: Vec4,
}

/// Uniform of `heightmap.wesl`.
#[derive(Clone, Copy, Debug, Default, PartialEq, ShaderType)]
pub(crate) struct HeightmapParams {
    size: UVec4,
    range: Vec4,
}

/// The source of a terrain's first heights, for the kernels.
#[derive(Clone, Debug)]
pub(crate) enum GenerateSource {
    Procedural(GenParams),
    Heightmap {
        image: Handle<Image>,
        uint: bool,
        params: HeightmapParams,
    },
}

/// The GPU resources of the terrain and this frame's parameters; extracted to the render world
/// every frame.
#[derive(Resource, Clone)]
pub(crate) struct TerrainGpu {
    pub state: TerrainState,
    pub params: TerrainParamsGpu,
    pub source: GenerateSource,
    /// Bumped by anything that changed bedrock, sediment or water this frame; the render world
    /// runs the pack, bounds and readback kernels while it is ahead of `packed`. Extracted by
    /// value, so a later frame's bump never reaches an earlier render frame. Starts at 1: the
    /// generation itself is a change.
    pub changes: u64,
    /// The `changes` value the render world last packed.
    pub packed: Arc<std::sync::atomic::AtomicU64>,
    /// Set by the render world once the generator has run.
    pub generated: Arc<std::sync::atomic::AtomicBool>,
    /// The brush stroke the render world runs once, at the head of the frame's kernels, while
    /// `stroke_epoch` is ahead of `stroked`.
    pub stroke: Option<sculpt::SculptParamsGpu>,
    /// Counts the strokes handed to the render world.
    pub stroke_epoch: u64,
    /// The `stroke_epoch` the render world last ran.
    pub stroked: Arc<std::sync::atomic::AtomicU64>,
    /// The region the render world writes into the bedrock and sediment layers once, before the
    /// frame's kernels, while `restore_epoch` is ahead of `restored`.
    pub restore: Option<sculpt::RestoreGpu>,
    /// Counts the restores handed to the render world.
    pub restore_epoch: u64,
    /// The `restore_epoch` the render world last wrote.
    pub restored: Arc<std::sync::atomic::AtomicU64>,
}

/// Entities per patch pool: the measured worst case of the selection (187 on flat Ultra, 133 at
/// High) plus margin (`terrain.md` Deviation 9).
pub(crate) const POOL_CAPACITY: usize = 256;

/// One entity of the patch pool and the patch it currently draws.
struct PoolSlot {
    entity: Entity,
    patch: Option<Patch>,
}

/// Main-world bookkeeping of the active terrain.
#[derive(Resource, Default)]
pub(crate) struct TerrainRuntime {
    /// What the current resources were built from.
    active: Option<(Entity, Terrain, ElementsQuality)>,
    /// A terrain that failed validation: not retried until it changes.
    failed: Option<Terrain>,
    warned_extra: bool,
    warned_transform: bool,
    warned_capacity: bool,
    /// The quadtree geometry of the active terrain.
    pub(crate) lod: Option<LodConfig>,
    /// Min and max heights of every node, from the last bounds readback (before the first, the
    /// generator's analytic range).
    bounds: Option<BoundsTree>,
    bounds_version: u32,
    /// The owned render's patch pool, empty for an external terrain.
    pool: Vec<PoolSlot>,
    patch_mesh: Option<Handle<Mesh>>,
    /// The patches the last selection chose (the owned pool's contents, or what it would hold
    /// for an external terrain): the water pool draws the wet ones.
    pub(crate) selected: Vec<Patch>,
    /// Surface min and water-surface max of every node, for the water patches' boxes.
    pub(crate) water_box: Option<BoundsTree>,
    /// Water depth max of every node (both entries): `get(..).1` is the deepest cell's depth.
    pub(crate) water_depth: Option<BoundsTree>,
    /// What the patches are selected and morphed about under an orthographic followed camera,
    /// metre frame (`docs/research/orthographic-lod.md` Model 1); `None`: the eye.
    pub(crate) lod_centre: Option<Vec3>,
}

impl TerrainRuntime {
    /// Whether the crate draws the active terrain with its own patches (not a consumer's mesh).
    pub(crate) fn draws_ground(&self) -> bool {
        self.active
            .as_ref()
            .is_some_and(|(_, terrain, _)| terrain.render == TerrainRender::Owned)
    }

    /// The point the patches are selected and morphed about for a followed camera at `eye`
    /// (metre frame).
    pub(crate) fn morph_centre(&self, eye: Vec3) -> Vec3 {
        self.lod_centre.unwrap_or(eye)
    }
}

/// Heightmap formats the generator reads linearly, and whether the kernel is the integer one.
fn heightmap_format(format: TextureFormat) -> Option<bool> {
    match format {
        TextureFormat::R8Unorm
        | TextureFormat::Rgba8Unorm
        | TextureFormat::R16Unorm
        | TextureFormat::Rgba16Unorm
        | TextureFormat::R32Float
        | TextureFormat::Rgba16Float
        | TextureFormat::Rgba32Float => Some(false),
        TextureFormat::R16Uint => Some(true),
        _ => None,
    }
}

/// Heightfield terrain with GPU-generated layers.
///
/// Spawn a [`Terrain`]; the layers are created and generated on the next frame and
/// [`TerrainState`] appears. Needs an [`ElementsCamera`](crate::ElementsCamera) only for the
/// level-of-detail selection of the owned render.
pub struct TerrainPlugin;

impl Plugin for TerrainPlugin {
    fn build(&self, app: &mut App) {
        ensure_gpu_core(app);
        embedded_asset!(app, "generate.wesl");
        embedded_asset!(app, "heightmap.wesl");
        embedded_asset!(app, "pack.wesl");
        embedded_asset!(app, "flow.wesl");
        embedded_asset!(app, "sculpt.wesl");
        embedded_asset!(app, "render.wesl");
        embedded_asset!(app, "ground.wesl");
        // The ground's detail textures' hex-tiling, imported by `render.wesl`.
        load_shader_library!(app, "textures.wesl");
        app.add_plugins((
            MaterialPlugin::<TerrainMaterial>::default(),
            MaterialPlugin::<GroundMaterial>::default(),
        ))
        .add_message::<SculptTerrain>()
        .add_message::<RestoreTerrain>()
        .init_resource::<sculpt::SculptQueue>()
        .init_resource::<TerrainRuntime>()
        .init_resource::<textures::GroundTexturePack>()
        .init_resource::<TerrainLook>()
        .add_systems(
            Update,
            (
                ensure_terrain,
                sculpt::apply_sculpts.after(ensure_terrain),
                update_lod_centre,
            )
                .in_set(ElementsSystems::Simulation),
        )
        .add_systems(
            Update,
            (
                read_terrain.in_set(ElementsSystems::Readback),
                textures::pack_terrain_textures,
                material::update_materials,
            )
                .chain()
                .after(ElementsSystems::Simulation),
        )
        .add_systems(
            PostUpdate,
            update_patches.before(TransformSystems::Propagate),
        );
        gpu::plugin(app);
    }
}

#[allow(clippy::too_many_arguments)]
fn ensure_terrain(
    mut commands: Commands,
    quality: Res<ElementsQuality>,
    terrains: Query<(Entity, &Terrain, &Transform, Option<&GlobalTransform>)>,
    units: Res<ElementsUnits>,
    asset_server: Res<AssetServer>,
    images: Res<Assets<Image>>,
    state: Option<ResMut<TerrainState>>,
    gpu: Option<ResMut<TerrainGpu>>,
    mut runtime: ResMut<TerrainRuntime>,
    mut snapshot: ResMut<EnvironmentSnapshot>,
    mut buffers: ResMut<Assets<ShaderBuffer>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<TerrainMaterial>>,
) {
    // The terrain in use: the one already built, else the lowest entity.
    let active_entity = runtime
        .active
        .as_ref()
        .map(|a| a.0)
        .filter(|e| terrains.contains(*e))
        .or_else(|| terrains.iter().map(|t| t.0).min());
    let Some((entity, terrain, local, global)) = active_entity.and_then(|e| terrains.get(e).ok())
    else {
        if state.is_some() {
            commands.remove_resource::<TerrainState>();
            commands.remove_resource::<TerrainGpu>();
            snapshot.terrain = None;
            snapshot.terrain_leaves = None;
        }
        clear_pool(&mut commands, &mut runtime);
        runtime.active = None;
        return;
    };
    if terrains.iter().count() > 1 && !runtime.warned_extra {
        warn!(
            "more than one Terrain entity exists; only {entity} is used and the others are ignored"
        );
        runtime.warned_extra = true;
    }
    let transform = global.map_or(*local, |g| g.compute_transform());
    // The grid lives in the metre frame; the entity's translation is in world units.
    let translation = units.point_to_metres(transform.translation);
    if !runtime.warned_transform
        && (transform.rotation.angle_between(Quat::IDENTITY) > 1e-4
            || (transform.scale - Vec3::ONE).abs().max_element() > 1e-4)
    {
        warn!(
            "a Terrain's rotation and scale are ignored: its grid is axis-aligned and its heights are in metres"
        );
        runtime.warned_transform = true;
    }

    let wanted = (entity, terrain.clone(), *quality);
    let max = quality.terrain_max_resolution();
    let (size, cell, adjusted) = resolve_resolution(terrain.extent_m, terrain.resolution, max);
    // The grid follows the transform every frame; the layers are rebuilt only when what they
    // were built from changes.
    let grid = TerrainGrid {
        origin: translation.xz() - size.as_vec2() * cell * 0.5,
        cell_m: cell,
        size,
        y0: translation.y,
    };
    if runtime.active.as_ref() == Some(&wanted) {
        if let (Some(mut state), Some(mut gpu)) = (state, gpu) {
            state.grid = grid;
            gpu.state.grid = grid;
            gpu.params = TerrainParamsGpu::new(&grid, readback_downsample(size), true);
        }
        return;
    }
    if runtime.failed.as_ref() == Some(terrain) {
        return;
    }

    let source = match &terrain.source {
        TerrainSource::Procedural(p) => {
            let octaves = octave_count(p.octaves, p.feature_size_m, cell);
            GenerateSource::Procedural(GenParams {
                size: UVec4::new(size.x, size.y, octaves, seed_mix(p.seed)),
                shape: Vec4::new(
                    cell,
                    p.feature_size_m.max(cell),
                    p.relief_m,
                    p.ridged.clamp(0.0, 1.0),
                ),
                base: Vec4::new(p.base_height_m, p.initial_sand_m.max(0.0), 0.0, 0.0),
                consts: Vec4::new(F_HALF, R_LOW, R_SPAN, GAIN),
                octave: Vec4::new(LACUNARITY, OCTAVE_ROTATION, 0.0, 0.0),
            })
        }
        TerrainSource::Flat(height) => GenerateSource::Procedural(GenParams {
            size: UVec4::new(size.x, size.y, 1, 0),
            shape: Vec4::new(cell, 1000.0, 0.0, 0.0),
            base: Vec4::new(*height, 0.0, 0.0, 0.0),
            consts: Vec4::new(F_HALF, R_LOW, R_SPAN, GAIN),
            octave: Vec4::new(LACUNARITY, OCTAVE_ROTATION, 0.0, 0.0),
        }),
        TerrainSource::Heightmap {
            image,
            height_range_m,
        } => {
            use bevy::asset::LoadState;
            match asset_server.load_state(image) {
                LoadState::Failed(error) => {
                    error!(
                        "terrain heightmap {:?} failed to load, the terrain stays unspawned: {error}",
                        image.path()
                    );
                    runtime.failed = Some(terrain.clone());
                    return;
                }
                LoadState::Loaded => {}
                // Still loading (or a handle that was added to the assets directly).
                _ if images.get(image).is_none() => return,
                _ => {}
            }
            let Some(loaded) = images.get(image) else {
                return;
            };
            let format = loaded.texture_descriptor.format;
            let Some(uint) = heightmap_format(format) else {
                error!(
                    "terrain heightmap {:?} has format {format:?}, which is not read linearly; the terrain stays unspawned. Load it with ImageLoaderSettings {{ is_srgb: false, expand_grayscale: false, .. }} (and prefer 16 bit)",
                    image.path()
                );
                runtime.failed = Some(terrain.clone());
                return;
            };
            if matches!(format, TextureFormat::R8Unorm | TextureFormat::Rgba8Unorm)
                && (height_range_m.end - height_range_m.start).abs() > 100.0
            {
                info!(
                    "terrain heightmap {:?} is 8 bit: its {:.0} m range has 256 height steps; a 16-bit map is recommended",
                    image.path(),
                    (height_range_m.end - height_range_m.start).abs()
                );
            }
            GenerateSource::Heightmap {
                image: image.clone(),
                uint,
                params: HeightmapParams {
                    size: UVec4::new(size.x, size.y, 0, 0),
                    range: Vec4::new(height_range_m.start, height_range_m.end, 0.0, 0.0),
                },
            }
        }
    };
    if adjusted {
        warn!(
            "terrain extent {:?} and resolution {:?} give non-square cells; the grid is re-derived as {} for {:.3} m cells",
            terrain.extent_m, terrain.resolution, size, cell
        );
    }

    let cells = u64::from(size.x) * u64::from(size.y);
    let ds = readback_downsample(size);
    let readback_size = size / ds;
    let leaves = u64::from(size.x / LEAF_CELLS) * u64::from(size.y / LEAF_CELLS);
    let mut layer = |label: &'static str, bytes_per_cell: u64| {
        zeroed_buffer(&mut buffers, label, cells * bytes_per_cell)
    };
    let new_state = TerrainState {
        entity,
        grid,
        bedrock: layer("elements_terrain_bedrock", 4),
        sediment: layer("elements_terrain_sediment", 4),
        water: layer("elements_terrain_water", 4),
        flux: layer("elements_terrain_flux", 16),
        velocity: layer("elements_terrain_velocity", 8),
        suspended: layer("elements_terrain_suspended", 4),
        transport: layer("elements_terrain_transport", 8),
        source_map: layer("elements_terrain_source_map", 8),
        thermal_flux: [
            layer("elements_terrain_thermal_flux_a", 16),
            layer("elements_terrain_thermal_flux_b", 16),
        ],
        cells: layer("elements_terrain_cells", 16),
        flow_cells: layer("elements_terrain_flow_cells", 16),
        height_readback: zeroed_buffer(
            &mut buffers,
            "elements_terrain_height_readback",
            u64::from(readback_size.x) * u64::from(readback_size.y) * 8,
        ),
        leaf_bounds: zeroed_buffer(&mut buffers, "elements_terrain_leaf_bounds", leaves * 16),
        readback_size,
        version: Arc::new(AtomicU32::new(0)),
    };
    commands.insert_resource(TerrainGpu {
        state: new_state.clone(),
        params: TerrainParamsGpu::new(&grid, ds, true),
        source,
        changes: 1,
        packed: Arc::new(std::sync::atomic::AtomicU64::new(0)),
        generated: Arc::new(std::sync::atomic::AtomicBool::new(false)),
        stroke: None,
        stroke_epoch: 0,
        stroked: Arc::new(std::sync::atomic::AtomicU64::new(0)),
        restore: None,
        restore_epoch: 0,
        restored: Arc::new(std::sync::atomic::AtomicU64::new(0)),
    });
    let cells_handle = new_state.cells.clone();
    commands.insert_resource(new_state);
    snapshot.terrain = None;
    snapshot.terrain_leaves = None;
    clear_pool(&mut commands, &mut runtime);
    let lod = LodConfig::new(size.as_vec2() * cell, cell);
    let (low, high) = analytic_range(&terrain.source);
    runtime.bounds = Some(BoundsTree::uniform(&lod, low, high));
    runtime.water_box = Some(BoundsTree::uniform(&lod, low, high));
    runtime.water_depth = Some(BoundsTree::uniform(&lod, 0.0, 0.0));
    runtime.selected.clear();
    runtime.bounds_version = 0;
    runtime.lod = Some(lod);
    if terrain.render == TerrainRender::Owned {
        let mesh = runtime
            .patch_mesh
            .get_or_insert_with(|| meshes.add(patch_mesh()))
            .clone();
        let material = materials.add(TerrainMaterial {
            base: StandardMaterial {
                base_color: Color::WHITE,
                perceptual_roughness: 0.9,
                metallic: 0.0,
                ..default()
            },
            extension: TerrainExtension {
                params: TerrainMaterialParams::default(),
                cells: cells_handle,
                cover: NO_TERRAIN,
                cloud_shadow: None,
                trail: None,
                ground: None,
                path: None,
            },
        });
        // Hidden until the first selection puts a patch in a slot.
        let analytic = Aabb::from_min_max(
            Vec3::new(0.0, units.world(low - 2.0), 0.0),
            Vec3::new(1.0, units.world(high + 2.0), 1.0),
        );
        let mut slots = Vec::with_capacity(POOL_CAPACITY);
        commands.entity(entity).with_children(|parent| {
            for _ in 0..POOL_CAPACITY {
                let entity = parent
                    .spawn((
                        Name::new("Terrain patch"),
                        Mesh3d(mesh.clone()),
                        MeshMaterial3d(material.clone()),
                        Transform::default(),
                        MeshTag::new(0),
                        analytic,
                        NoAutoAabb,
                        Visibility::Hidden,
                    ))
                    .id();
                slots.push(PoolSlot {
                    entity,
                    patch: None,
                });
            }
        });
        runtime.pool = slots;
    }
    runtime.active = Some(wanted);
    runtime.failed = None;
}

/// Despawn the patch pool (the terrain's children), if any.
fn clear_pool(commands: &mut Commands, runtime: &mut TerrainRuntime) {
    for slot in runtime.pool.drain(..) {
        // `try_`: the slots are the terrain's children, so a host that despawned the terrain
        // entity already took them.
        commands.entity(slot.entity).try_despawn();
    }
}

#[cfg(test)]
mod pool_tests {
    use super::*;
    use bevy::ecs::world::CommandQueue;

    /// Bevy's command error handlers log through the `log` crate, so the test installs a logger
    /// that counts the warnings the ECS reports. Only this test installs one in the process.
    static ECS_WARNINGS: AtomicU32 = AtomicU32::new(0);
    struct Counter;
    impl log::Log for Counter {
        fn enabled(&self, _: &log::Metadata<'_>) -> bool {
            true
        }
        fn log(&self, record: &log::Record<'_>) {
            if record.level() == log::Level::Warn && record.target().starts_with("bevy_ecs") {
                ECS_WARNINGS.fetch_add(1, Ordering::Relaxed);
            }
        }
        fn flush(&self) {}
    }

    fn world_with_pool(slots: usize) -> (World, Entity, Vec<Entity>) {
        let mut world = World::new();
        let terrain = world.spawn_empty().id();
        let children: Vec<Entity> = (0..slots)
            .map(|_| world.spawn(ChildOf(terrain)).id())
            .collect();
        let runtime = TerrainRuntime {
            pool: children
                .iter()
                .map(|&entity| PoolSlot {
                    entity,
                    patch: None,
                })
                .collect(),
            ..Default::default()
        };
        world.insert_resource(runtime);
        (world, terrain, children)
    }

    /// Clears the pool once and returns how many ECS warnings that logged.
    fn clear_counting_warnings(world: &mut World) -> u32 {
        let before = ECS_WARNINGS.load(Ordering::Relaxed);
        let mut queue = CommandQueue::default();
        let mut runtime = world
            .remove_resource::<TerrainRuntime>()
            .expect("the pool's runtime");
        clear_pool(&mut Commands::new(&mut queue, world), &mut runtime);
        world.insert_resource(runtime);
        queue.apply(world);
        ECS_WARNINGS.load(Ordering::Relaxed) - before
    }

    /// A host that despawns the terrain takes the pool with it, and clearing the pool afterwards
    /// must not warn once per slot (#47); a pool rebuilt under a live terrain still despawns the
    /// old slots, silently too.
    #[test]
    fn clearing_the_pool_warns_of_nothing_and_leaves_no_slot() {
        log::set_logger(&Counter).expect("no other logger in the lib tests");
        log::set_max_level(log::LevelFilter::Warn);

        let (mut world, terrain, children) = world_with_pool(8);
        world.despawn(terrain);
        assert!(children.iter().all(|&c| world.get_entity(c).is_err()));
        assert_eq!(clear_counting_warnings(&mut world), 0);
        assert!(world.resource::<TerrainRuntime>().pool.is_empty());

        let (mut world, terrain, children) = world_with_pool(8);
        assert_eq!(clear_counting_warnings(&mut world), 0);
        assert!(world.resource::<TerrainRuntime>().pool.is_empty());
        assert!(children.iter().all(|&c| world.get_entity(c).is_err()));
        assert!(world.get_entity(terrain).is_ok());
    }
}

/// The range of surface heights the source can produce before the first bounds readback, m.
fn analytic_range(source: &TerrainSource) -> (f32, f32) {
    match source {
        TerrainSource::Procedural(p) => {
            let (a, b) = (p.base_height_m, p.base_height_m + p.relief_m);
            (a.min(b), a.max(b))
        }
        TerrainSource::Heightmap { height_range_m, .. } => (
            height_range_m.start.min(height_range_m.end),
            height_range_m.start.max(height_range_m.end),
        ),
        TerrainSource::Flat(h) => (*h, *h),
    }
}

/// The patch every pool entity shares: a unit square of 32 × 32 quads (33 × 33 vertices,
/// `u16` indices) in the XZ plane. The vertex shader places the vertices; only the grid index
/// (position × 32) matters.
pub(crate) fn patch_mesh() -> Mesh {
    const N: u16 = 32;
    let mut positions = Vec::with_capacity(usize::from((N + 1) * (N + 1)));
    for z in 0..=N {
        for x in 0..=N {
            positions.push([
                f32::from(x) / f32::from(N),
                0.0,
                f32::from(z) / f32::from(N),
            ]);
        }
    }
    let mut indices = Vec::with_capacity(usize::from(N * N * 6));
    for z in 0..N {
        for x in 0..N {
            let i = z * (N + 1) + x;
            indices.extend_from_slice(&[i, i + N + 1, i + 1, i + 1, i + N + 1, i + N + 2]);
        }
    }
    Mesh::new(
        PrimitiveTopology::TriangleList,
        RenderAssetUsages::RENDER_WORLD,
    )
    .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
    .with_inserted_indices(Indices::U16(indices))
}

/// Slots for the `wanted` patches of a pool whose slots currently hold `current`: a patch keeps
/// the slot it already has, the others take the free ones (the farthest are dropped when there
/// are none). Returns the assignment and which slots are in use.
pub(crate) fn assign_slots(
    wanted: Vec<Patch>,
    current: &[Option<Patch>],
) -> (Vec<(Patch, usize)>, Vec<bool>) {
    let slot_of: HashMap<Patch, usize> = current
        .iter()
        .enumerate()
        .filter_map(|(i, patch)| patch.map(|p| (p, i)))
        .collect();
    let mut used = vec![false; current.len()];
    let mut assigned: Vec<(Patch, usize)> = Vec::with_capacity(wanted.len());
    let mut unmatched = Vec::new();
    for patch in wanted {
        match slot_of.get(&patch) {
            Some(&i) => {
                used[i] = true;
                assigned.push((patch, i));
            }
            None => unmatched.push(patch),
        }
    }
    let mut free = (0..used.len())
        .filter(|i| !used[*i])
        .collect::<Vec<_>>()
        .into_iter();
    for patch in unmatched {
        if let Some(i) = free.next() {
            used[i] = true;
            assigned.push((patch, i));
        }
    }
    (assigned, used)
}

/// The transform of a patch relative to the terrain entity: its corner on the vertex lattice
/// (the node's tile half a cell in from the grid's corner, so the vertices sit on the height
/// samples), scaled to its edge.
pub(crate) fn patch_transform(lod: &LodConfig, extent: Vec2, patch: Patch) -> (Vec3, Vec3) {
    let size = lod.node_m(patch.level);
    let offset = lod.sample_offset();
    (
        Vec3::new(
            patch.ix as f32 * size + offset - extent.x * 0.5,
            0.0,
            patch.iz as f32 * size + offset - extent.y * 0.5,
        ),
        Vec3::new(size, 1.0, size),
    )
}

/// Find what the patches are selected and morphed about this frame
/// (`docs/research/orthographic-lod.md` Model 1). Under a perspective followed camera, the eye
/// (`None`): Bevy's vertex stage morphs about the view's own translation. Under an orthographic
/// one, whose eye stands far back from what it shows, its [`ElementsFocus`], or without one
/// where its forward ray first meets the ground; a view that meets no ground keeps the eye.
pub(crate) fn update_lod_centre(
    mut runtime: ResMut<TerrainRuntime>,
    view: Res<ElementsView>,
    snapshot: Res<EnvironmentSnapshot>,
    cameras: Query<(&Projection, &GlobalTransform, Has<ElementsFocus>)>,
) {
    let centre = view
        .entity
        .and_then(|entity| cameras.get(entity).ok())
        .and_then(|(projection, transform, focused)| {
            if !matches!(projection, Projection::Orthographic(_)) {
                return None;
            }
            // A focus that resolved to the eye (an entity without a transform) is no focus.
            if focused && view.position != view.eye {
                return Some(view.position);
            }
            let snap = snapshot.terrain.as_ref()?;
            let (lo, hi) = runtime.bounds.as_ref()?.extremes();
            snap.ray_hit(
                view.eye,
                transform.forward().as_vec3(),
                snap.grid.y0 + lo,
                snap.grid.y0 + hi,
            )
        });
    if runtime.lod_centre != centre {
        runtime.lod_centre = centre;
    }
}

/// Select the patches for the followed camera and write them into the pool, touching only the
/// slots whose patch, transform or bounds changed. Idles without a camera and keeps the last
/// pool. Runs before transform propagation so the new transforms are drawn this frame.
pub(crate) fn update_patches(
    mut runtime: ResMut<TerrainRuntime>,
    state: Option<Res<TerrainState>>,
    view: Res<ElementsView>,
    units: Res<ElementsUnits>,
    snapshot: Res<EnvironmentSnapshot>,
    mut patches: Query<(&mut Transform, &mut Visibility, &mut MeshTag, &mut Aabb)>,
) {
    let (Some(state), Some(lod)) = (state, runtime.lod) else {
        return;
    };
    if view.entity.is_none() {
        return;
    }
    if let Some(leaves) = snapshot
        .terrain_leaves
        .as_ref()
        .filter(|l| l.version != runtime.bounds_version)
    {
        let tree =
            BoundsTree::from_leaves(&lod, leaves.dims, leaves.bounds.iter().map(|b| (b.x, b.y)));
        // The ranges follow the steepest node of every level.
        runtime.lod = Some(lod.derive(Some(&tree)));
        runtime.bounds = Some(tree);
        runtime.water_box = Some(BoundsTree::from_leaves(
            &lod,
            leaves.dims,
            leaves.bounds.iter().map(|b| (b.x, b.z)),
        ));
        runtime.water_depth = Some(BoundsTree::from_leaves(
            &lod,
            leaves.dims,
            leaves.bounds.iter().map(|b| (b.w, b.w)),
        ));
        runtime.bounds_version = leaves.version;
    }
    let Some(lod) = runtime.lod else {
        return;
    };
    let Some(tree) = runtime.bounds.take() else {
        return;
    };
    let grid = state.grid;
    // The same centre the vertex stage morphs about: the eye, Bevy's `lod_view_world_position`,
    // or the uniform `lod_centre` under an orthographic camera.
    let centre = runtime.morph_centre(view.eye);
    let camera = Vec3::new(
        centre.x - grid.origin.x,
        centre.y - grid.y0,
        centre.z - grid.origin.y,
    );
    let mut wanted = select_patches(&lod, &tree, camera);
    if wanted.len() > POOL_CAPACITY {
        if !runtime.warned_capacity {
            warn!(
                "terrain selection wants {} patches, more than the pool's {POOL_CAPACITY}; the farthest are dropped",
                wanted.len()
            );
            runtime.warned_capacity = true;
        }
        wanted.truncate(POOL_CAPACITY);
    }
    runtime.selected.clone_from(&wanted);
    if runtime.pool.is_empty() {
        runtime.bounds = Some(tree);
        return;
    }
    let current: Vec<Option<Patch>> = runtime.pool.iter().map(|s| s.patch).collect();
    let (assigned, used) = assign_slots(wanted, &current);
    let extent = grid.extent();
    for (i, slot) in runtime.pool.iter_mut().enumerate() {
        if used[i] {
            continue;
        }
        if slot.patch.take().is_some()
            && let Ok((_, mut visibility, _, _)) = patches.get_mut(slot.entity)
            && *visibility != Visibility::Hidden
        {
            *visibility = Visibility::Hidden;
        }
    }
    for (patch, i) in assigned {
        let entity = runtime.pool[i].entity;
        runtime.pool[i].patch = Some(patch);
        let Ok((mut transform, mut visibility, mut tag, mut aabb)) = patches.get_mut(entity) else {
            continue;
        };
        // The patch is placed in the world: its metre-frame corner and edge × units.
        let (translation, scale) = patch_transform(&lod, extent, patch);
        let (translation, scale) = (
            units.point_to_world(translation),
            Vec3::new(units.world(scale.x), scale.y, units.world(scale.z)),
        );
        if transform.translation != translation || transform.scale != scale {
            transform.translation = translation;
            transform.scale = scale;
        }
        if tag.value != patch.level {
            *tag = MeshTag::new(patch.level);
        }
        let (low, high) = tree.get(patch.level, patch.ix, patch.iz);
        let bounds = Aabb::from_min_max(
            Vec3::new(0.0, units.world(low - model::AABB_PAD_M), 0.0),
            Vec3::new(1.0, units.world(high + model::AABB_PAD_M), 1.0),
        );
        if *aabb != bounds {
            *aabb = bounds;
        }
        if *visibility != Visibility::Inherited {
            *visibility = Visibility::Inherited;
        }
    }
    runtime.bounds = Some(tree);
}

/// The factor the readback box-averages the grid down by.
fn readback_downsample(size: UVec2) -> u32 {
    if size.max_element() > READBACK_MAX_CELLS {
        2
    } else {
        1
    }
}

/// Bounds of every leaf patch at the last readback.
#[derive(Clone, Debug)]
pub(crate) struct TerrainLeaves {
    /// The surface version the bounds were read at.
    pub version: u32,
    /// Leaves per axis.
    pub dims: UVec2,
    /// Surface min, surface max, water-surface max, water depth max, per leaf, row-major.
    pub bounds: Vec<Vec4>,
}

/// Copy the height field and the leaf bounds back whenever the GPU's version moved.
fn read_terrain(
    state: Option<Res<TerrainState>>,
    snapshot: Res<EnvironmentSnapshot>,
    mut readbacks: Readbacks,
) {
    let Some(state) = state else {
        return;
    };
    let version = state.version.load(Ordering::Relaxed);
    if version == 0 {
        return;
    }
    if snapshot
        .terrain
        .as_ref()
        .is_none_or(|t| t.version != version)
    {
        let ds = state.grid.size.x / state.readback_size.x;
        let grid = TerrainGrid {
            cell_m: state.grid.cell_m * ds as f32,
            size: state.readback_size,
            ..state.grid
        };
        let expected = (grid.size.x * grid.size.y) as usize;
        readbacks.request(
            "terrain_height",
            state.height_readback.clone(),
            move |bytes, snapshot| {
                let Some(cells) = decode::<[f32; 2]>(bytes) else {
                    return;
                };
                if cells.len() < expected {
                    return;
                }
                let cells = &cells[..expected];
                snapshot.terrain = Some(TerrainSnapshot {
                    grid,
                    surface: cells.iter().map(|c| c[0]).collect(),
                    sediment: cells.iter().map(|c| c[1]).collect(),
                    version,
                });
            },
        );
    }
    if snapshot
        .terrain_leaves
        .as_ref()
        .is_none_or(|t| t.version != version)
    {
        let dims = state.grid.size / LEAF_CELLS;
        let expected = (dims.x * dims.y) as usize;
        readbacks.request(
            "terrain_bounds",
            state.leaf_bounds.clone(),
            move |bytes, snapshot| {
                let Some(bounds) = decode::<[f32; 4]>(bytes) else {
                    return;
                };
                if bounds.len() < expected {
                    return;
                }
                snapshot.terrain_leaves = Some(TerrainLeaves {
                    version,
                    dims,
                    bounds: bounds[..expected]
                        .iter()
                        .copied()
                        .map(Vec4::from_array)
                        .collect(),
                });
            },
        );
    }
}