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}