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}