khora_agents/render_agent/agent.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 RenderAgent, the central orchestrator for the rendering subsystem.
16
17use super::mesh_preparation::MeshPreparationSystem;
18use khora_core::{
19 asset::Material,
20 math::Mat4,
21 renderer::{
22 api::{GpuMesh, RenderContext, RenderObject},
23 traits::CommandEncoder,
24 GraphicsDevice, Mesh, ViewInfo,
25 },
26};
27use khora_data::{
28 assets::Assets,
29 ecs::{Camera, GlobalTransform, World},
30};
31use khora_lanes::render_lane::{
32 ExtractRenderablesLane, ForwardPlusLane, LitForwardLane, RenderLane, RenderWorld,
33 SimpleUnlitLane,
34};
35use std::sync::{Arc, RwLock};
36
37/// Threshold for switching to Forward+ rendering.
38/// When the scene has more than this many lights, Forward+ is preferred.
39const FORWARD_PLUS_LIGHT_THRESHOLD: usize = 20;
40
41/// Rendering strategy selection mode.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum RenderingStrategy {
44 /// Simple unlit rendering (vertex colors only).
45 #[default]
46 Unlit,
47 /// Standard forward rendering with lighting.
48 LitForward,
49 /// Forward+ (tiled forward) rendering with compute-based light culling.
50 ForwardPlus,
51 /// Automatic selection based on scene complexity (light count).
52 Auto,
53}
54
55/// The agent responsible for managing the state and logic of the rendering pipeline.
56///
57/// It orchestrates the various systems and lanes involved in preparing and
58/// translating scene data from the ECS into a format consumable by the low-level
59/// `RenderSystem`.
60pub struct RenderAgent {
61 // Intermediate data structure populated by the extraction phase.
62 render_world: RenderWorld,
63 // Cache for GPU-side mesh assets.
64 gpu_meshes: Arc<RwLock<Assets<GpuMesh>>>,
65 // System that handles uploading CPU meshes to the GPU.
66 mesh_preparation_system: MeshPreparationSystem,
67 // Lane that extracts data from the ECS into the RenderWorld.
68 extract_lane: ExtractRenderablesLane,
69 // Available render lanes (strategies), extensible collection.
70 lanes: Vec<Box<dyn RenderLane>>,
71 // Current rendering strategy.
72 strategy: RenderingStrategy,
73}
74
75impl RenderAgent {
76 /// Creates a new `RenderAgent` with default lanes and automatic strategy selection.
77 pub fn new() -> Self {
78 let gpu_meshes = Arc::new(RwLock::new(Assets::new()));
79
80 // Default set of available lanes
81 let lanes: Vec<Box<dyn RenderLane>> = vec![
82 Box::new(SimpleUnlitLane::new()),
83 Box::new(LitForwardLane::new()),
84 Box::new(ForwardPlusLane::new()),
85 ];
86
87 Self {
88 render_world: RenderWorld::new(),
89 gpu_meshes: gpu_meshes.clone(),
90 mesh_preparation_system: MeshPreparationSystem::new(gpu_meshes),
91 extract_lane: ExtractRenderablesLane::new(),
92 lanes,
93 strategy: RenderingStrategy::Auto,
94 }
95 }
96
97 /// Creates a new `RenderAgent` with the specified rendering strategy.
98 pub fn with_strategy(strategy: RenderingStrategy) -> Self {
99 let mut agent = Self::new();
100 agent.strategy = strategy;
101 agent
102 }
103
104 /// Adds a custom render lane to the available lanes.
105 pub fn add_lane(&mut self, lane: Box<dyn RenderLane>) {
106 self.lanes.push(lane);
107 }
108
109 /// Sets the rendering strategy.
110 pub fn set_strategy(&mut self, strategy: RenderingStrategy) {
111 self.strategy = strategy;
112 }
113
114 /// Returns the current rendering strategy.
115 pub fn strategy(&self) -> RenderingStrategy {
116 self.strategy
117 }
118
119 /// Returns a reference to the available lanes.
120 pub fn lanes(&self) -> &[Box<dyn RenderLane>] {
121 &self.lanes
122 }
123
124 /// Finds a lane by its strategy name.
125 fn find_lane_by_name(&self, name: &str) -> Option<&dyn RenderLane> {
126 self.lanes
127 .iter()
128 .find(|lane| lane.strategy_name() == name)
129 .map(|boxed| boxed.as_ref())
130 }
131
132 /// Selects the appropriate render lane based on the current strategy.
133 pub fn select_lane(&self) -> &dyn RenderLane {
134 match self.strategy {
135 RenderingStrategy::Unlit => self
136 .find_lane_by_name("SimpleUnlit")
137 .unwrap_or(self.lanes.first().map(|b| b.as_ref()).unwrap()),
138 RenderingStrategy::LitForward => self
139 .find_lane_by_name("LitForward")
140 .unwrap_or(self.lanes.first().map(|b| b.as_ref()).unwrap()),
141 RenderingStrategy::ForwardPlus => self
142 .find_lane_by_name("ForwardPlus")
143 .unwrap_or(self.lanes.first().map(|b| b.as_ref()).unwrap()),
144 RenderingStrategy::Auto => {
145 // Automatic selection based on light count
146 let total_lights = self.render_world.directional_light_count()
147 + self.render_world.point_light_count()
148 + self.render_world.spot_light_count();
149
150 if total_lights > FORWARD_PLUS_LIGHT_THRESHOLD {
151 self.find_lane_by_name("ForwardPlus")
152 .unwrap_or(self.lanes.first().map(|b| b.as_ref()).unwrap())
153 } else if total_lights > 0 {
154 self.find_lane_by_name("LitForward")
155 .unwrap_or(self.lanes.first().map(|b| b.as_ref()).unwrap())
156 } else {
157 self.find_lane_by_name("SimpleUnlit")
158 .unwrap_or(self.lanes.first().map(|b| b.as_ref()).unwrap())
159 }
160 }
161 }
162 }
163
164 /// Prepares all rendering data for the current frame.
165 ///
166 /// This method runs the entire Control Plane logic for rendering:
167 /// 1. Prepares GPU resources for any newly loaded meshes.
168 /// 2. Extracts all visible objects from the ECS into the internal `RenderWorld`.
169 pub fn prepare_frame(
170 &mut self,
171 world: &mut World,
172 cpu_meshes: &Assets<Mesh>,
173 graphics_device: &dyn GraphicsDevice,
174 ) {
175 // First, ensure all necessary GpuMeshes are created and cached.
176 self.mesh_preparation_system
177 .run(world, cpu_meshes, graphics_device);
178
179 // Then, extract the prepared renderable data into our local RenderWorld.
180 self.extract_lane.run(world, &mut self.render_world);
181 }
182
183 /// Renders a frame by preparing the scene data and encoding GPU commands.
184 ///
185 /// This is the main rendering method that orchestrates the entire rendering pipeline:
186 /// 1. Calls `prepare_frame()` to extract and prepare all renderable data
187 /// 2. Calls `produce_render_objects()` to build the RenderObject list with proper pipelines
188 /// 3. Delegates to the selected render lane to encode GPU commands
189 ///
190 /// # Arguments
191 ///
192 /// * `world`: The ECS world containing scene data
193 /// * `cpu_meshes`: The cache of CPU-side mesh assets
194 /// * `graphics_device`: The graphics device for GPU resource creation
195 /// * `materials`: The cache of material assets
196 /// * `encoder`: The command encoder to record GPU commands into
197 /// * `color_target`: The texture view to render into (typically the swapchain)
198 /// * `clear_color`: The color to clear the framebuffer with
199 pub fn render(
200 &mut self,
201 world: &mut World,
202 cpu_meshes: &Assets<Mesh>,
203 graphics_device: &dyn GraphicsDevice,
204 materials: &RwLock<Assets<Box<dyn Material>>>,
205 encoder: &mut dyn CommandEncoder,
206 render_ctx: &RenderContext,
207 ) {
208 // Step 1: Prepare the frame (extract and prepare data)
209 self.prepare_frame(world, cpu_meshes, graphics_device);
210
211 // Step 2: Build RenderObjects with proper pipelines
212 // (This is where the lane determines which pipeline to use for each material)
213 let _render_objects = self.produce_render_objects(materials);
214
215 // Step 3: Delegate to the selected render lane to encode GPU commands
216 self.select_lane().render(
217 &self.render_world,
218 encoder,
219 render_ctx,
220 &self.gpu_meshes,
221 materials,
222 );
223 }
224
225 /// Translates the prepared data from the `RenderWorld` into a list of `RenderObject`s.
226 ///
227 /// This method should be called after `prepare_frame`. It reads the intermediate
228 /// `RenderWorld` and produces the final, low-level data structure required by
229 /// the `RenderSystem`.
230 ///
231 /// This logic uses the render lane to determine the appropriate pipeline for each
232 /// material, then builds the RenderObjects list.
233 ///
234 /// # Arguments
235 ///
236 /// * `materials`: The cache of material assets for pipeline selection
237 pub fn produce_render_objects(
238 &self,
239 materials: &RwLock<Assets<Box<dyn Material>>>,
240 ) -> Vec<RenderObject> {
241 let mut render_objects = Vec::with_capacity(self.render_world.meshes.len());
242 let gpu_meshes_guard = self.gpu_meshes.read().unwrap();
243 let materials_guard = materials.read().unwrap();
244
245 for extracted_mesh in &self.render_world.meshes {
246 // Find the corresponding GpuMesh in the cache
247 if let Some(gpu_mesh_handle) = gpu_meshes_guard.get(&extracted_mesh.gpu_mesh_uuid) {
248 // Use the selected render lane to determine the appropriate pipeline
249 let lane = self.select_lane();
250 let pipeline =
251 lane.get_pipeline_for_material(extracted_mesh.material_uuid, &materials_guard);
252
253 render_objects.push(RenderObject {
254 pipeline,
255 vertex_buffer: gpu_mesh_handle.vertex_buffer,
256 index_buffer: gpu_mesh_handle.index_buffer,
257 index_count: gpu_mesh_handle.index_count,
258 });
259 }
260 }
261
262 render_objects
263 }
264
265 /// Extracts the active camera from the ECS world and generates a `ViewInfo`.
266 ///
267 /// This method queries the ECS for entities with both a `Camera` and `GlobalTransform`
268 /// component, finds the first active camera, and constructs a ViewInfo containing
269 /// the camera's view and projection matrices.
270 ///
271 /// # Arguments
272 ///
273 /// * `world`: The ECS world containing camera entities
274 ///
275 /// # Returns
276 ///
277 /// A `ViewInfo` containing the camera's matrices and position. If no active camera
278 /// is found, returns a default ViewInfo with identity matrices.
279 pub fn extract_camera_view(&self, world: &World) -> ViewInfo {
280 // Query for entities with Camera and GlobalTransform components
281 let query = world.query::<(&Camera, &GlobalTransform)>();
282
283 // Find the first active camera
284 for (camera, global_transform) in query {
285 if camera.is_active {
286 // Extract camera position from the global transform
287 let camera_position = global_transform.0.translation();
288
289 // Calculate the view matrix from the global transform
290 // The view matrix is the inverse of the camera's world transform
291 let view_matrix = if let Some(inv) = global_transform.to_matrix().inverse() {
292 inv
293 } else {
294 eprintln!("Warning: Failed to invert camera transform, using identity");
295 Mat4::IDENTITY
296 };
297
298 // Get the projection matrix from the camera
299 let projection_matrix = camera.projection_matrix();
300
301 return ViewInfo::new(view_matrix, projection_matrix, camera_position);
302 }
303 }
304
305 // No active camera found, return default
306 eprintln!("Warning: No active camera found in scene, using default ViewInfo");
307 ViewInfo::default()
308 }
309}
310
311impl Default for RenderAgent {
312 fn default() -> Self {
313 Self::new()
314 }
315}