Skip to main content

wde_terrain/render/
renderer_gpu.rs

1use std::collections::HashMap;
2
3use bevy::{ecs::system::SystemParamItem, prelude::*};
4use wde_renderer::prelude::*;
5
6use crate::{
7    manager::{ChunkPos, TerrainDirtyTile},
8    prelude::TerrainExtractor,
9    render::{extractor, renderer::TerrainDrawList}
10};
11
12// ─── Render bind group (texture_2d_array sampling) ──────────────────────────
13
14/// Heightmaps (read texel by texel, unfiltered), then each splat map array with its sampler.
15#[derive(Asset, Clone, TypePath, Default)]
16pub struct TerrainChunkArrayBg {
17    pub heightmap_array: Option<Handle<Texture>>,
18    pub splatmap_arrays: Vec<Handle<Texture>>
19}
20impl RenderBinding for TerrainChunkArrayBg {
21    type Params = ();
22
23    fn describe(
24        &mut self,
25        _params: &SystemParamItem<Self::Params>,
26        builder: &mut RenderBindingBuilder
27    ) {
28        builder.add_texture_array_view_from_id(self.heightmap_array.as_ref().map(|h| h.id()));
29        for splatmap in &self.splatmap_arrays {
30            builder
31                .add_texture_array_view_from_id(Some(splatmap.id()))
32                .add_texture_sampler_from_id(Some(splatmap.id()));
33        }
34    }
35
36    fn label(&self) -> &str {
37        "terrain-chunk-array-render"
38    }
39}
40
41// ─── Compute bind group (texture_storage_2d_array read/write) ────────────────
42
43#[derive(Asset, Clone, TypePath, Default)]
44pub struct TerrainComputeArrayBg {
45    pub heightmap_array: Option<Handle<Texture>>,
46    pub splatmap_array: Option<Handle<Texture>>
47}
48impl RenderBinding for TerrainComputeArrayBg {
49    type Params = ();
50
51    fn describe(
52        &mut self,
53        _params: &SystemParamItem<Self::Params>,
54        builder: &mut RenderBindingBuilder
55    ) {
56        builder
57            .add_storage_texture_array_from_id(self.heightmap_array.as_ref().map(|h| h.id()))
58            .add_storage_texture_array_from_id(self.splatmap_array.as_ref().map(|h| h.id()));
59    }
60
61    fn label(&self) -> &str {
62        "terrain-chunk-array-compute"
63    }
64}
65
66// ─── GPU-side renderer resource ──────────────────────────────────────────────
67
68#[derive(Resource, Default)]
69pub struct TerrainRendererGPU {
70    /// True once bind groups are created and the renderer is ready.
71    pub ready: bool,
72    /// Maps chunk position → array layer index.
73    pub pos_to_layer: HashMap<ChunkPos, u32>,
74    /// Chunks to draw this frame.
75    pub draw_list: TerrainDrawList,
76
77    pub heightmap_array: Option<Handle<Texture>>,
78    pub splatmap_arrays: Vec<Handle<Texture>>,
79
80    pub render_bind_group: Option<Handle<TerrainChunkArrayBg>>,
81    pub compute_bind_group: Option<Handle<TerrainComputeArrayBg>>,
82
83    pub dirty: Vec<TerrainDirtyTile>
84}
85
86impl TerrainRendererGPU {
87    /// Transfer the physics extraction queue and dirty tile uploads from the main world.
88    /// Also does the one-time GPU handle initialisation on the first call.
89    #[allow(clippy::too_many_arguments)]
90    pub fn extract_dirty(
91        main_terrain_extractor: &mut TerrainExtractor,
92        render_terrain_extractor: &mut TerrainExtractor,
93        heightmap_array: Option<Handle<Texture>>,
94        splatmap_arrays: Vec<Handle<Texture>>,
95        pos_to_layer: HashMap<ChunkPos, u32>,
96        dirty: Vec<TerrainDirtyTile>,
97        draw_list: TerrainDrawList,
98        gpu_terrain_renderer: &mut TerrainRendererGPU
99    ) {
100        // Move the tile-extraction queue from main world → render world.
101        extractor::extract_dirty(main_terrain_extractor, render_terrain_extractor);
102
103        // One-time initialisation of GPU handles.
104        if gpu_terrain_renderer.heightmap_array.is_none() {
105            gpu_terrain_renderer.heightmap_array = heightmap_array;
106            gpu_terrain_renderer.splatmap_arrays = splatmap_arrays;
107            gpu_terrain_renderer.pos_to_layer = pos_to_layer;
108        }
109
110        gpu_terrain_renderer.dirty.extend(dirty);
111        gpu_terrain_renderer.draw_list = draw_list;
112    }
113
114    /// Upload dirty tile data to the appropriate array layer on the GPU.
115    pub fn upload_dirty(
116        mut gpu: ResMut<TerrainRendererGPU>,
117        textures: Res<RenderAssets<GpuTexture>>,
118        render_instance: Res<RenderInstance>
119    ) {
120        if gpu.dirty.is_empty() {
121            return;
122        }
123        let render_instance = render_instance.0.read().unwrap();
124        let dirty = std::mem::take(&mut gpu.dirty);
125        for (pos, map_type, splat_index, tile_data) in dirty {
126            let layer = match gpu.pos_to_layer.get(&pos) {
127                Some(&l) => l,
128                None => continue
129            };
130            match map_type {
131                0 => {
132                    if let Some(handle) = &gpu.heightmap_array
133                        && let Some(tex) = textures.get(handle)
134                    {
135                        tex.texture.copy_from_buffer_layered(
136                            &render_instance,
137                            tex.texture.format,
138                            layer,
139                            bytemuck::cast_slice(&tile_data)
140                        );
141                    }
142                }
143                1 => {
144                    if let Some(handle) = gpu.splatmap_arrays.get(splat_index as usize)
145                        && let Some(tex) = textures.get(handle)
146                    {
147                        tex.texture.copy_from_buffer_layered(
148                            &render_instance,
149                            tex.texture.format,
150                            layer,
151                            bytemuck::cast_slice(&tile_data)
152                        );
153                    }
154                }
155                _ => unreachable!()
156            }
157        }
158    }
159
160    /// Create the single render and compute bind groups once textures are ready.
161    pub fn prepare_bind_groups(
162        asset_server: Res<AssetServer>,
163        mut gpu: ResMut<TerrainRendererGPU>
164    ) {
165        if gpu.ready {
166            return;
167        }
168        if gpu.heightmap_array.is_none() || gpu.splatmap_arrays.is_empty() {
169            return;
170        }
171
172        if gpu.render_bind_group.is_none() {
173            gpu.render_bind_group = Some(asset_server.add(TerrainChunkArrayBg {
174                heightmap_array: gpu.heightmap_array.clone(),
175                splatmap_arrays: gpu.splatmap_arrays.clone()
176            }));
177        }
178        // The terrain editor paints the first splat map (materials 0 to 3)
179        if gpu.compute_bind_group.is_none() {
180            gpu.compute_bind_group = Some(asset_server.add(TerrainComputeArrayBg {
181                heightmap_array: gpu.heightmap_array.clone(),
182                splatmap_array: gpu.splatmap_arrays.first().cloned()
183            }));
184        }
185
186        gpu.ready = gpu.render_bind_group.is_some() && gpu.compute_bind_group.is_some();
187    }
188}