khora_agents/render_agent/
mesh_preparation.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines the system responsible for preparing GPU resources for newly loaded meshes.
16
17use khora_core::{
18    asset::AssetHandle,
19    ecs::entity::EntityId,
20    renderer::{
21        api::{BufferDescriptor, BufferUsage, GpuMesh, IndexFormat},
22        GraphicsDevice, Mesh,
23    },
24};
25use khora_data::{
26    assets::Assets,
27    ecs::{HandleComponent, Without, World},
28};
29use std::collections::HashMap;
30use std::sync::{Arc, RwLock};
31
32/// Manages the GPU upload and caching of mesh assets.
33///
34/// This system observes the `World` for entities that have a `HandleComponent<Mesh>`
35/// (a CPU asset) but do not yet have a `HandleComponent<GpuMesh>` (a GPU asset).
36/// For each such entity, it ensures the corresponding mesh data is uploaded to the
37/// GPU, caches the resulting `GpuMesh` asset, and then adds the
38/// `HandleComponent<GpuMesh>` to the entity.
39pub struct MeshPreparationSystem {
40    /// A thread-safe, shared handle to the `GpuMesh` asset cache.
41    gpu_meshes: Arc<RwLock<Assets<GpuMesh>>>,
42}
43
44impl MeshPreparationSystem {
45    /// Creates a new `MeshPreparationSystem`.
46    ///
47    /// # Arguments
48    ///
49    /// * `gpu_meshes_cache`: A shared pointer to the `Assets` storage where
50    ///   `GpuMesh` assets will be cached.
51    pub fn new(gpu_meshes_cache: Arc<RwLock<Assets<GpuMesh>>>) -> Self {
52        Self {
53            gpu_meshes: gpu_meshes_cache,
54        }
55    }
56
57    /// Runs the preparation logic for one frame.
58    ///
59    /// This should be called once per frame, typically during the "Control Plane"
60    /// phase, before the render extraction process begins. The process is split
61    /// into two phases to comply with Rust's borrow checker: a read-only query
62    /// phase and a subsequent mutation phase.
63    ///
64    /// # Arguments
65    ///
66    /// * `world`: A mutable reference to the main ECS `World`.
67    /// * `cpu_meshes`: An immutable reference to the storage for loaded CPU `Mesh` assets.
68    /// * `graphics_device`: A trait object for the active graphics device, used for buffer creation.
69    pub fn run(
70        &self,
71        world: &mut World,
72        cpu_meshes: &Assets<Mesh>,
73        graphics_device: &dyn GraphicsDevice,
74    ) {
75        // A temporary map to store the components that need to be added to entities.
76        // We collect these first and add them later to avoid borrowing `world` mutably
77        // while iterating over its query results.
78        let mut pending_additions: HashMap<EntityId, HandleComponent<GpuMesh>> = HashMap::new();
79
80        // --- Phase 1: Query and Prepare (Read-only on World) ---
81
82        // Find all entities that have a CPU mesh handle but lack a GPU mesh handle.
83        let query = world.query::<(
84            EntityId,
85            &HandleComponent<Mesh>,
86            Without<HandleComponent<GpuMesh>>,
87        )>();
88
89        for (entity_id, mesh_handle_comp, _) in query {
90            let mesh_uuid = mesh_handle_comp.uuid;
91
92            // Check if the GpuMesh has already been created and cached by this system
93            // in a previous iteration or for another entity.
94            if !self.gpu_meshes.read().unwrap().contains(&mesh_uuid) {
95                // Cache Miss: This is the first time we've seen this mesh asset.
96                // We need to upload its data to the GPU.
97                if let Some(cpu_mesh) = cpu_meshes.get(&mesh_uuid) {
98                    let gpu_mesh = self.upload_mesh(cpu_mesh, graphics_device);
99                    // Lock the cache for writing and insert the new GpuMesh.
100                    self.gpu_meshes
101                        .write()
102                        .unwrap()
103                        .insert(mesh_uuid, AssetHandle::new(gpu_mesh));
104                }
105            }
106
107            // We schedule the addition of the corresponding handle component to the entity.
108            if let Some(gpu_mesh_handle) = self.gpu_meshes.read().unwrap().get(&mesh_uuid) {
109                pending_additions.insert(
110                    entity_id,
111                    HandleComponent {
112                        handle: gpu_mesh_handle.clone(),
113                        uuid: mesh_uuid,
114                    },
115                );
116            }
117        }
118
119        // --- Phase 2: Mutate World ---
120
121        // Now, iterate over the collected additions and apply them to the world.
122        // This is safe because we are no longer borrowing the world for the query.
123        for (entity_id, component) in pending_additions {
124            let _ = world.add_component(entity_id, component);
125        }
126    }
127
128    /// A private helper function that takes a CPU `Mesh` and uploads its data
129    /// to the GPU, returning a `GpuMesh` containing the new buffer handles.
130    fn upload_mesh(&self, mesh: &Mesh, device: &dyn GraphicsDevice) -> GpuMesh {
131        // 1. Create and upload the vertex buffer.
132        let vertex_data = mesh.create_vertex_buffer();
133
134        let vb_desc = BufferDescriptor {
135            label: Some("Mesh Vertex Buffer".into()),
136            size: vertex_data.len() as u64,
137            usage: BufferUsage::VERTEX | BufferUsage::COPY_DST,
138            mapped_at_creation: false,
139        };
140
141        let vertex_buffer = device
142            .create_buffer_with_data(&vb_desc, &vertex_data)
143            .expect("Failed to create vertex buffer");
144
145        // 2. Create and upload the index buffer, if the mesh has indices.
146        let (index_buffer, index_count) = if let Some(indices) = &mesh.indices {
147            let index_data = bytemuck::cast_slice(indices);
148
149            let ib_desc = BufferDescriptor {
150                label: Some("Mesh Index Buffer".into()),
151                size: index_data.len() as u64,
152                usage: BufferUsage::INDEX | BufferUsage::COPY_DST,
153                mapped_at_creation: false,
154            };
155
156            let buffer = device
157                .create_buffer_with_data(&ib_desc, index_data)
158                .expect("Failed to create index buffer");
159
160            (buffer, indices.len() as u32)
161        } else {
162            // If there are no indices, create an empty buffer as a placeholder.
163            let dummy_desc = BufferDescriptor {
164                label: Some("Empty Index Buffer".into()),
165                size: 0,
166                usage: BufferUsage::INDEX,
167                mapped_at_creation: false,
168            };
169
170            let buffer = device
171                .create_buffer(&dummy_desc)
172                .expect("Failed to create empty index buffer");
173
174            (buffer, 0)
175        };
176
177        // 3. Assemble and return the `GpuMesh` asset.
178        GpuMesh {
179            vertex_buffer,
180            index_buffer,
181            index_count,
182            index_format: IndexFormat::Uint32, // Assuming U32 indices as per our mesh loader.
183            primitive_topology: mesh.primitive_type,
184        }
185    }
186}