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