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}