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