diff --git a/data/openstreet_map.png b/data/openstreet_map.png new file mode 100644 index 0000000..dc5561a Binary files /dev/null and b/data/openstreet_map.png differ diff --git a/docs/opengl_texture_alignment_fix.md b/docs/opengl_texture_alignment_fix.md new file mode 100644 index 0000000..b4f9763 --- /dev/null +++ b/docs/opengl_texture_alignment_fix.md @@ -0,0 +1,184 @@ +# OpenGL Texture Alignment Issue and Fix + +## Overview + +This document explains a critical issue that was discovered in the QuickViz Canvas implementation where certain image dimensions would cause application crashes during texture upload to OpenGL, and describes the implemented fix. + +## Problem Description + +### Symptoms +- Application crashes with segmentation fault during `glTexImage2D` calls +- Crashes occurred specifically with images having dimensions 1203×809 pixels +- Other dimensions like 1756×1152 or 800×538 worked perfectly fine +- The issue was consistent across different image formats (PNG) and content + +### Initial Investigation +The crash occurred in `Canvas::SetupBackgroundImage()` specifically during the OpenGL texture upload phase: + +```cpp +glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, + 0, format, GL_UNSIGNED_BYTE, data); +``` + +The STB image loading was successful - the crash happened during GPU texture creation. + +## Root Cause Analysis + +### The Real Issue: OpenGL Row Alignment + +The root cause was **OpenGL row alignment requirements**. OpenGL expects texture data to be aligned to specific byte boundaries, controlled by the `GL_UNPACK_ALIGNMENT` parameter (default: 4 bytes). + +### Mathematical Analysis + +For RGB images (3 bytes per pixel): +- **1203 × 3 = 3609 bytes per row** +- **3609 % 4 = 1** (not aligned to 4-byte boundary) ❌ + +Working images: +- **1756 × 3 = 5268 bytes per row** +- **5268 % 4 = 0** (aligned) ✅ +- **800 × 3 = 2400 bytes per row** +- **2400 % 4 = 0** (aligned) ✅ + +### Why This Causes Crashes + +When `GL_UNPACK_ALIGNMENT` is set to 4 (default), OpenGL expects each row of texture data to start at a 4-byte aligned memory address. For unaligned data: + +1. OpenGL may read beyond the allocated memory boundary +2. This can cause segmentation faults or memory corruption +3. The behavior is undefined and driver-dependent + +## Solution Implementation + +### The Fix + +A generic solution was implemented that automatically detects and handles alignment issues: + +```cpp +// Check for OpenGL row alignment issues +// OpenGL expects texture data to be aligned to 4-byte boundaries by default +int bytes_per_pixel = (format == GL_RGBA) ? 4 : (format == GL_RGB) ? 3 : 1; +int bytes_per_row = width * bytes_per_pixel; +bool alignment_issue = (bytes_per_row % 4) != 0; + +if (alignment_issue) { + std::cout << "Canvas: Adjusting alignment for " << width << "x" << height + << " texture (row size: " << bytes_per_row << " bytes)" << std::endl; + // Set pixel alignment to 1 byte to handle unaligned row data + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); +} + +glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, + 0, format, GL_UNSIGNED_BYTE, data); + +// Restore default alignment if we changed it +if (alignment_issue) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // Restore default +} +``` + +### How the Fix Works + +1. **Detection**: Calculate bytes per row and check if divisible by 4 +2. **Adjustment**: Set `GL_UNPACK_ALIGNMENT` to 1 for problematic textures +3. **Upload**: Perform normal texture upload with adjusted alignment +4. **Restoration**: Restore default alignment setting + +### Why GL_UNPACK_ALIGNMENT = 1 Works + +Setting `GL_UNPACK_ALIGNMENT` to 1 tells OpenGL: +- "Don't assume any row alignment" +- "Read pixel data byte-by-byte without alignment requirements" +- This works for any row size but may have minor performance implications + +## Technical Details + +### OpenGL Alignment Parameters + +- `GL_UNPACK_ALIGNMENT`: Controls alignment for data read from client memory (default: 4) +- `GL_PACK_ALIGNMENT`: Controls alignment for data written to client memory (default: 4) +- Valid values: 1, 2, 4, 8 bytes + +### Performance Considerations + +- Aligned textures (4-byte rows) have optimal performance +- Unaligned textures with `GL_UNPACK_ALIGNMENT=1` work correctly but may be slightly slower +- The performance difference is negligible for most applications + +### Affected Image Formats + +This issue affects images where `width × channels % 4 ≠ 0`: + +| Format | Problematic Width Examples | +|--------|---------------------------| +| RGB (3 channels) | 1203, 1205, 1207, 1209, ... (any width where width × 3 % 4 ≠ 0) | +| Grayscale (1 channel) | Any non-multiple of 4 | +| RGBA (4 channels) | Never problematic (4 × anything is divisible by 4) | + +## Alternative Solutions Considered + +### 1. Image Resizing (Workaround) +- Resize problematic images to aligned dimensions +- **Pros**: Guaranteed compatibility +- **Cons**: Requires manual intervention, data loss + +### 2. Row Padding (Memory Solution) +- Add padding bytes to make rows 4-byte aligned +- **Pros**: Maintains optimal performance +- **Cons**: Complex implementation, memory overhead + +### 3. Generic Alignment Fix (Chosen Solution) +- Automatically adjust `GL_UNPACK_ALIGNMENT` when needed +- **Pros**: Automatic, generic, no data loss +- **Cons**: Minor performance impact for unaligned textures + +## Testing and Validation + +### Test Cases +1. **Original problematic images**: Now load successfully +2. **Working images**: Continue to work without alignment adjustment +3. **Various dimensions**: Tested multiple alignment scenarios + +### Verification +The fix was verified with: +- Original 1203×809 images (both PNG variants) +- Working 1756×1152 and 800×538 images +- Edge cases with different channel counts + +## Best Practices + +### For Image Assets +1. **Prefer aligned dimensions** when possible for optimal performance +2. **RGB images**: Use widths that make `width × 3` divisible by 4 +3. **RGBA images**: Any width works (always aligned) + +### For Developers +1. **Don't hard-code dimension checks** - use generic alignment detection +2. **Always restore OpenGL state** after temporary changes +3. **Log alignment adjustments** for debugging purposes + +## Code Location + +The fix is implemented in: +- **File**: `src/renderer/src/renderable/canvas.cpp` +- **Function**: `Canvas::SetupBackgroundImage()` +- **Lines**: Texture upload section (~415-440) + +## Future Enhancements + +Potential improvements: +1. **Global alignment policy**: Allow configuration of default alignment behavior +2. **Performance metrics**: Measure alignment impact in real applications +3. **Memory optimization**: Implement row padding for frequently used textures + +## References + +- [OpenGL specification on pixel storage](https://www.khronos.org/opengl/wiki/Pixel_Transfer#Pixel_layout) +- [GL_UNPACK_ALIGNMENT documentation](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glPixelStore.xhtml) +- [STB Image library](https://github.com/nothings/stb) + +--- + +**Author**: Claude Code Assistant +**Date**: July 2025 +**Version**: 1.0 \ No newline at end of file diff --git a/src/renderer/CMakeLists.txt b/src/renderer/CMakeLists.txt index 56bcd93..f8f3383 100644 --- a/src/renderer/CMakeLists.txt +++ b/src/renderer/CMakeLists.txt @@ -15,6 +15,10 @@ add_library(renderer src/renderable/triangle.cpp src/renderable/point_cloud.cpp src/renderable/canvas.cpp + src/renderable/details/batched_render_strategy.cpp + src/renderable/details/individual_render_strategy.cpp + src/renderable/details/shape_renderer.cpp + src/renderable/details/shape_generators.cpp src/renderable/coordinate_frame.cpp src/renderable/texture.cpp ) diff --git a/src/renderer/include/renderer/camera.hpp b/src/renderer/include/renderer/camera.hpp index 16c43a4..62b786b 100644 --- a/src/renderer/include/renderer/camera.hpp +++ b/src/renderer/include/renderer/camera.hpp @@ -22,6 +22,7 @@ class Camera { static constexpr float default_movement_speed = 2.5f; static constexpr float default_mouse_sensitivity = 0.05f; static constexpr float default_fov = 45.0f; + static constexpr float default_zoom_speed = 2.0f; static constexpr float pitch_min = -89.0f; static constexpr float pitch_max = 89.0f; static constexpr float fov_min = 1.0f; @@ -49,7 +50,7 @@ class Camera { // public methods glm::mat4 GetViewMatrix() const; glm::mat4 GetProjectionMatrix(float aspect_ratio, float z_near = 0.1f, - float z_far = 100.0f) const; + float z_far = 1000.0f) const; void Reset(); void SetWorldUpVector(glm::vec3 up); @@ -83,6 +84,7 @@ class Camera { float movement_speed_ = default_movement_speed; float mouse_sensitivity_ = default_mouse_sensitivity; float fov_ = default_fov; + float zoom_speed_ = default_zoom_speed; }; } // namespace quickviz diff --git a/src/renderer/include/renderer/camera_controller.hpp b/src/renderer/include/renderer/camera_controller.hpp index 6f940fe..ba77a0b 100644 --- a/src/renderer/include/renderer/camera_controller.hpp +++ b/src/renderer/include/renderer/camera_controller.hpp @@ -27,14 +27,21 @@ class CameraController { void ProcessKeyboard(CameraMovement direction, float delta_time); void ProcessMouseMovement(float x_offset, float y_offset); void ProcessMouseScroll(float y_offset); - + // Set the current mouse button state for camera control void SetActiveMouseButton(int button); int GetActiveMouseButton() const { return active_mouse_button_; } + float GetHeight() const { return camera_.GetPosition().y; } + void SetHeight(float height); + + glm::vec2 GetPosition() const; + void SetPosition(const glm::vec2& position); + private: static constexpr float initial_orbit_distance = 10.0f; - static constexpr float initial_top_down_height = 10.0f; + static constexpr float default_orbit_zoom_speed = 2.0f; + static constexpr float default_topdown_zoom_speed = 2.0f; void UpdateOrbitPosition(); @@ -42,13 +49,16 @@ class CameraController { Mode mode_ = Mode::kOrbit; glm::vec3 orbit_target_ = glm::vec3(0.0f, 0.0f, 0.0f); float orbit_distance_ = initial_orbit_distance; - float top_down_height_ = initial_top_down_height; - + // For tracking mouse button states, -1 means no button pressed int active_mouse_button_ = MouseButton::kNone; - + // For tracking rotation in TopDown mode float top_down_rotation_ = 0.0f; + + // Zoom speed multipliers + float orbit_zoom_speed_ = default_orbit_zoom_speed; + float topdown_zoom_speed_ = default_topdown_zoom_speed; }; } // namespace quickviz diff --git a/src/renderer/include/renderer/gl_scene_manager.hpp b/src/renderer/include/renderer/gl_scene_manager.hpp index 58ceda1..8902325 100644 --- a/src/renderer/include/renderer/gl_scene_manager.hpp +++ b/src/renderer/include/renderer/gl_scene_manager.hpp @@ -44,6 +44,7 @@ class GlSceneManager : public Panel { void SetShowRenderingInfo(bool show); void SetBackgroundColor(float r, float g, float b, float a); + void SetClippingPlanes(float z_near, float z_far); void AddOpenGLObject(const std::string& name, std::unique_ptr object); @@ -85,6 +86,7 @@ class GlSceneManager : public Panel { } void Draw() override; + void RenderInsideWindow(); protected: void UpdateView(const glm::mat4& projection, const glm::mat4& view); @@ -106,6 +108,8 @@ class GlSceneManager : public Panel { // Coordinate system transformation bool use_coord_transform_ = true; glm::mat4 coord_transform_ = glm::mat4(1.0f); + float z_near_ = 0.1f; + float z_far_ = 1000.0f; // Pre-draw callback PreDrawCallback pre_draw_callback_; diff --git a/src/renderer/include/renderer/renderable/canvas.hpp b/src/renderer/include/renderer/renderable/canvas.hpp index d74ae0d..c8ccbf0 100644 --- a/src/renderer/include/renderer/renderable/canvas.hpp +++ b/src/renderer/include/renderer/renderable/canvas.hpp @@ -18,42 +18,35 @@ #include #include #include +#include #include #include "renderer/interface/opengl_object.hpp" #include "renderer/shader_program.hpp" #include "renderer/renderable/types.hpp" +#include "renderer/renderable/details/canvas_batching.hpp" +#include "renderer/renderable/details/canvas_performance.hpp" + +// Forward declarations for render strategies +namespace quickviz { +class RenderStrategy; +class BatchedRenderStrategy; +class IndividualRenderStrategy; +class ShapeRenderer; +} // namespace quickviz namespace quickviz { // Forward declaration of Point struct struct Point; struct CanvasData; -// Batched rendering structures for improved performance -struct LineBatch { - std::vector vertices; - std::vector colors; - std::vector thicknesses; - std::vector line_types; - uint32_t vao = 0; - uint32_t position_vbo = 0; - uint32_t color_vbo = 0; - bool needs_update = true; -}; - -struct ShapeBatch { - std::vector vertices; - std::vector indices; - std::vector colors; - uint32_t vao = 0; - uint32_t vertex_vbo = 0; - uint32_t color_vbo = 0; - uint32_t ebo = 0; - bool needs_update = true; -}; +// Note: LineBatch and ShapeBatch moved to details/canvas_batching.hpp class Canvas : public OpenGlObject { + public: + using PerformanceConfig = quickviz::PerformanceConfig; + public: Canvas(); ~Canvas(); @@ -85,133 +78,33 @@ class Canvas : public OpenGlObject { void Clear(); // Performance and rendering methods - void SetBatchingEnabled(bool enabled) { batching_enabled_ = enabled; } + void SetBatchingEnabled(bool enabled); bool IsBatchingEnabled() const { return batching_enabled_; } - void FlushBatches(); // Force immediate rendering of all batches - - // Performance monitoring - struct RenderStats { - // Rendering statistics - uint32_t points_rendered = 0; - uint32_t lines_rendered = 0; - uint32_t shapes_rendered = 0; - uint32_t draw_calls = 0; - uint32_t state_changes = 0; - - // Timing statistics - float last_frame_time_ms = 0.0f; - float avg_frame_time_ms = 0.0f; - float min_frame_time_ms = 999999.0f; - float max_frame_time_ms = 0.0f; - uint32_t frame_count = 0; - - // Memory statistics - size_t vertex_memory_used = 0; - size_t index_memory_used = 0; - size_t texture_memory_used = 0; - size_t total_memory_used = 0; - - // Batching efficiency - uint32_t batched_objects = 0; - uint32_t individual_objects = 0; - float batch_efficiency = 0.0f; // Percentage of objects that were batched - - // OpenGL resource usage - uint32_t active_vaos = 0; - uint32_t active_vbos = 0; - uint32_t active_textures = 0; - - void Reset() { - points_rendered = 0; - lines_rendered = 0; - shapes_rendered = 0; - draw_calls = 0; - state_changes = 0; - last_frame_time_ms = 0.0f; - // Note: Don't reset cumulative stats like avg, min, max, frame_count - vertex_memory_used = 0; - index_memory_used = 0; - texture_memory_used = 0; - total_memory_used = 0; - batched_objects = 0; - individual_objects = 0; - batch_efficiency = 0.0f; - active_vaos = 0; - active_vbos = 0; - active_textures = 0; - } - - void UpdateFrameStats(float frame_time_ms) { - last_frame_time_ms = frame_time_ms; - frame_count++; - - // Update min/max - min_frame_time_ms = std::min(min_frame_time_ms, frame_time_ms); - max_frame_time_ms = std::max(max_frame_time_ms, frame_time_ms); - - // Update rolling average (with decay factor) - const float alpha = 0.1f; // Smoothing factor - avg_frame_time_ms = avg_frame_time_ms * (1.0f - alpha) + frame_time_ms * alpha; - - // Calculate batch efficiency - uint32_t total_objects = batched_objects + individual_objects; - batch_efficiency = total_objects > 0 ? - (static_cast(batched_objects) / total_objects) * 100.0f : 0.0f; - } - - void UpdateOperationStats(float operation_time_ms) { - // Track individual operation timing (for non-batched operations) - last_frame_time_ms += operation_time_ms; // Add to total frame time - } - - // Convenience methods - float GetFPS() const { return last_frame_time_ms > 0 ? 1000.0f / last_frame_time_ms : 0.0f; } - float GetAvgFPS() const { return avg_frame_time_ms > 0 ? 1000.0f / avg_frame_time_ms : 0.0f; } - size_t GetTotalMemoryMB() const { return total_memory_used / (1024 * 1024); } - }; - - const RenderStats& GetRenderStats() const { return render_stats_; } - void ResetRenderStats() { render_stats_.Reset(); } - - // Performance tuning and memory optimization - struct PerformanceConfig { - // Batching configuration - bool auto_batching_enabled = true; - size_t max_batch_size = 10000; // Maximum objects per batch - size_t batch_resize_threshold = 5000; // When to resize batch buffers - - // Memory management - bool object_pooling_enabled = true; - size_t initial_pool_size = 1000; // Initial pool size for reusable objects - size_t max_pool_size = 50000; // Maximum pool size - bool aggressive_memory_cleanup = false; // Clean up unused memory more aggressively - - // Performance monitoring - bool detailed_timing_enabled = false; // Enable detailed per-operation timing - bool memory_tracking_enabled = true; // Track memory usage - size_t stats_update_frequency = 60; // Update stats every N frames - - // Quality vs Performance trade-offs - int circle_segments = 32; // Default circle tessellation - int ellipse_segments = 32; // Default ellipse tessellation - bool adaptive_tessellation = true; // Adjust tessellation based on size - float tessellation_scale_factor = 50.0f; // Pixels per segment for adaptive mode - }; - - void SetPerformanceConfig(const PerformanceConfig& config) { perf_config_ = config; } - const PerformanceConfig& GetPerformanceConfig() const { return perf_config_; } - + void FlushBatches(); // Force immediate rendering of all batches + + // Performance monitoring (moved to details/canvas_performance.hpp) + const RenderStats& GetRenderStats() const; + void ResetRenderStats(); + + // Performance tuning and memory optimization (moved to + // details/canvas_performance.hpp) + void SetPerformanceConfig(const PerformanceConfig& config); + const PerformanceConfig& GetPerformanceConfig() const; + // Memory optimization methods - void OptimizeMemory(); // Trigger memory optimization pass - void PreallocateMemory(size_t estimated_objects); // Pre-allocate for known workloads - void ShrinkToFit(); // Release unused memory - size_t GetMemoryUsage() const; // Get current memory usage in bytes + void OptimizeMemory(); // Trigger memory optimization pass + void PreallocateMemory( + size_t estimated_objects); // Pre-allocate for known workloads + void ShrinkToFit(); // Release unused memory + size_t GetMemoryUsage() const; // Get current memory usage in bytes void AllocateGpuResources() override; void ReleaseGpuResources() noexcept override; void OnDraw(const glm::mat4& projection, const glm::mat4& view, const glm::mat4& coord_transform = glm::mat4(1.0f)) override; - bool IsGpuResourcesAllocated() const noexcept override { return primitive_vao_ != 0; } + bool IsGpuResourcesAllocated() const noexcept override { + return primitive_vao_ != 0; + } private: // Load and setup background image @@ -238,30 +131,32 @@ class Canvas : public OpenGlObject { float thickness; LineType line_type; bool filled; - + // Command-specific parameters + struct ellipse_params { + float x, y, rx, ry, angle, start_angle, end_angle; + }; + union { struct { // Point parameters float x, y; } point; - + struct { // Line parameters float x1, y1, x2, y2; } line; - + struct { // Rectangle parameters float x, y, width, height; } rect; - + struct { // Circle parameters float x, y, radius; } circle; - - struct { // Ellipse parameters - float x, y, rx, ry, angle, start_angle, end_angle; - } ellipse; + + ellipse_params ellipse; }; - + // Polygon vertices (can't be in union) std::vector polygon_vertices; }; @@ -287,116 +182,78 @@ class Canvas : public OpenGlObject { ShaderProgram primitive_shader_; // Batching-related members - bool batching_enabled_ = true; // Re-enabled, ellipse/polygon renderMode fixed - LineBatch line_batch_; + bool batching_enabled_ = + true; // Re-enabled, ellipse/polygon renderMode fixed + std::unordered_map line_batches_; ShapeBatch filled_shape_batch_; - ShapeBatch outline_shape_batch_; + std::unordered_map outline_shape_batches_; // Batch management methods void InitializeBatches(); void ClearBatches(); void UpdateBatches(); - void RenderBatches(const glm::mat4& projection, const glm::mat4& view, + void RenderBatches(const glm::mat4& projection, const glm::mat4& view, const glm::mat4& coord_transform); - + // Individual shape rendering for non-batched shapes - void RenderIndividualShapes(const CanvasData& data, const glm::mat4& projection, - const glm::mat4& view, const glm::mat4& coord_transform); - + void RenderIndividualShapes(const CanvasData& data, + const glm::mat4& projection, + const glm::mat4& view, + const glm::mat4& coord_transform); + // Shape generation helpers void GenerateCircleVertices(float cx, float cy, float radius, int segments, - std::vector& vertices, std::vector& indices, - bool filled, uint32_t base_index); + std::vector& vertices, + std::vector& indices, bool filled, + uint32_t base_index); void GenerateRectangleVertices(float x, float y, float width, float height, - std::vector& vertices, std::vector& indices, - bool filled, uint32_t base_index); + std::vector& vertices, + std::vector& indices, bool filled, + uint32_t base_index); + void GenerateEllipseVertices(const PendingUpdate::ellipse_params& ellipse, + std::vector& vertices, + std::vector& indices, bool filled, + uint32_t base_index); + void GeneratePolygonVertices(const std::vector& points, + std::vector& vertices, + std::vector& indices, bool filled, + uint32_t base_index); // Performance monitoring RenderStats render_stats_; // Performance tuning and memory optimization PerformanceConfig perf_config_; - - // Object pooling for memory optimization - template - struct ObjectPool { - std::vector> available_objects; - std::vector> used_objects; - size_t total_created = 0; - size_t peak_usage = 0; - - T* Acquire() { - if (available_objects.empty()) { - try { - available_objects.push_back(std::make_unique()); - total_created++; - } catch (...) { - return nullptr; // Return nullptr on allocation failure - } - } - - auto obj = std::move(available_objects.back()); - available_objects.pop_back(); - T* ptr = obj.get(); - used_objects.push_back(std::move(obj)); - - peak_usage = std::max(peak_usage, used_objects.size()); - return ptr; - } - - void Release(T* obj) { - if (!obj) return; // Handle null pointer gracefully - - auto it = std::find_if(used_objects.begin(), used_objects.end(), - [obj](const std::unique_ptr& ptr) { return ptr.get() == obj; }); - - if (it != used_objects.end()) { - available_objects.push_back(std::move(*it)); - used_objects.erase(it); - } - } - - void ShrinkToFit(size_t max_size) { - while (available_objects.size() > max_size && !available_objects.empty()) { - available_objects.pop_back(); - } - available_objects.shrink_to_fit(); - used_objects.shrink_to_fit(); - } - - size_t GetMemoryUsage() const { - return (available_objects.size() + used_objects.size()) * sizeof(T) + - available_objects.capacity() * sizeof(std::unique_ptr) + - used_objects.capacity() * sizeof(std::unique_ptr); - } - - // Add clear method for cleanup - void Clear() { - available_objects.clear(); - used_objects.clear(); - total_created = 0; - peak_usage = 0; - } - }; - + + // Render strategy system (refactored from monolithic OnDraw) + RenderStrategy* current_render_strategy_; + std::unique_ptr batched_strategy_; + std::unique_ptr individual_strategy_; + + // Unified shape renderer (Phase 2 refactoring) + std::unique_ptr shape_renderer_; + + // Helper method to select appropriate render strategy + RenderStrategy* SelectRenderStrategy(const CanvasData& data); + // Memory tracking struct MemoryTracker { std::atomic current_usage{0}; std::atomic peak_usage{0}; std::atomic total_allocations{0}; std::atomic total_deallocations{0}; - + void RecordAllocation(size_t size) { current_usage += size; total_allocations++; peak_usage = std::max(peak_usage.load(), current_usage.load()); } - + void RecordDeallocation(size_t size) { current_usage -= size; total_deallocations++; } - + void Reset() { current_usage = 0; peak_usage = 0; @@ -404,24 +261,23 @@ class Canvas : public OpenGlObject { total_deallocations = 0; } }; - + mutable MemoryTracker memory_tracker_; - + // Timing utilities for performance monitoring struct PerformanceTimer { std::chrono::high_resolution_clock::time_point start_time; - - void Start() { - start_time = std::chrono::high_resolution_clock::now(); - } - + + void Start() { start_time = std::chrono::high_resolution_clock::now(); } + float ElapsedMs() const { auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); + auto duration = std::chrono::duration_cast( + end_time - start_time); return duration.count() / 1000.0f; } }; - + mutable PerformanceTimer frame_timer_; mutable PerformanceTimer operation_timer_; }; diff --git a/src/renderer/include/renderer/renderable/details/canvas_batching.hpp b/src/renderer/include/renderer/renderable/details/canvas_batching.hpp new file mode 100644 index 0000000..9b2b447 --- /dev/null +++ b/src/renderer/include/renderer/renderable/details/canvas_batching.hpp @@ -0,0 +1,55 @@ +/** + * @file canvas_batching.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Batching structures for Canvas rendering optimization + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_CANVAS_BATCHING_HPP +#define OPENGL_RENDERER_CANVAS_BATCHING_HPP + +#include +#include +#include +#include "renderer/renderable/types.hpp" + +namespace quickviz { + +/** + * @brief Batched line rendering data structure + * + * Aggregates multiple lines into a single draw call for better performance. + */ +struct LineBatch { + std::vector vertices; + std::vector colors; + std::vector thicknesses; + std::vector line_types; + uint32_t vao = 0; + uint32_t position_vbo = 0; + uint32_t color_vbo = 0; + bool needs_update = true; +}; + +/** + * @brief Batched shape rendering data structure + * + * Aggregates multiple shapes (rectangles, circles) into batches + * for efficient rendering with reduced draw calls. + */ +struct ShapeBatch { + std::vector vertices; + std::vector indices; + std::vector colors; + uint32_t vao = 0; + uint32_t vertex_vbo = 0; + uint32_t color_vbo = 0; + uint32_t ebo = 0; + bool needs_update = true; +}; + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_CANVAS_BATCHING_HPP */ \ No newline at end of file diff --git a/src/renderer/include/renderer/renderable/details/canvas_performance.hpp b/src/renderer/include/renderer/renderable/details/canvas_performance.hpp new file mode 100644 index 0000000..7202249 --- /dev/null +++ b/src/renderer/include/renderer/renderable/details/canvas_performance.hpp @@ -0,0 +1,149 @@ +/** + * @file canvas_performance.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Performance monitoring and configuration for Canvas rendering + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_CANVAS_PERFORMANCE_HPP +#define OPENGL_RENDERER_CANVAS_PERFORMANCE_HPP + +#include +#include +#include +#include + +namespace quickviz { + +/** + * @brief Comprehensive rendering statistics for performance monitoring + * + * Tracks frame timing, memory usage, batching efficiency, and OpenGL + * resource usage to help optimize Canvas rendering performance. + */ +struct RenderStats { + // Rendering statistics + uint32_t points_rendered = 0; + uint32_t lines_rendered = 0; + uint32_t shapes_rendered = 0; + uint32_t draw_calls = 0; + uint32_t state_changes = 0; + + // Timing statistics + float last_frame_time_ms = 0.0f; + float avg_frame_time_ms = 0.0f; + float min_frame_time_ms = std::numeric_limits::max(); + float max_frame_time_ms = 0.0f; + uint32_t frame_count = 0; + + // Memory statistics + size_t vertex_memory_used = 0; + size_t index_memory_used = 0; + size_t texture_memory_used = 0; + size_t total_memory_used = 0; + + // Batching efficiency + uint32_t batched_objects = 0; + uint32_t individual_objects = 0; + float batch_efficiency = 0.0f; // Percentage of objects that were batched + + // OpenGL resource usage + uint32_t active_vaos = 0; + uint32_t active_vbos = 0; + uint32_t active_textures = 0; + + /** + * @brief Reset per-frame statistics (preserves cumulative data) + */ + void Reset() { + points_rendered = 0; + lines_rendered = 0; + shapes_rendered = 0; + draw_calls = 0; + state_changes = 0; + last_frame_time_ms = 0.0f; + // Note: Don't reset cumulative stats like avg, min, max, frame_count + vertex_memory_used = 0; + index_memory_used = 0; + texture_memory_used = 0; + total_memory_used = 0; + batched_objects = 0; + individual_objects = 0; + batch_efficiency = 0.0f; + active_vaos = 0; + active_vbos = 0; + active_textures = 0; + } + + /** + * @brief Update frame timing statistics with exponential smoothing + * @param frame_time_ms Time taken for the current frame in milliseconds + */ + void UpdateFrameStats(float frame_time_ms) { + last_frame_time_ms = frame_time_ms; + frame_count++; + + // Update min/max + min_frame_time_ms = std::min(min_frame_time_ms, frame_time_ms); + max_frame_time_ms = std::max(max_frame_time_ms, frame_time_ms); + + // Update rolling average (with decay factor) + const float alpha = 0.1f; // Smoothing factor + avg_frame_time_ms = avg_frame_time_ms * (1.0f - alpha) + frame_time_ms * alpha; + + // Calculate batch efficiency + uint32_t total_objects = batched_objects + individual_objects; + batch_efficiency = total_objects > 0 ? + (static_cast(batched_objects) / total_objects) * 100.0f : 0.0f; + } + + /** + * @brief Update statistics for individual operations + * @param operation_time_ms Time taken for a single operation in milliseconds + */ + void UpdateOperationStats(float operation_time_ms) { + // Track individual operation timing (for non-batched operations) + last_frame_time_ms += operation_time_ms; // Add to total frame time + } + + // Convenience methods + float GetFPS() const { return last_frame_time_ms > 0 ? 1000.0f / last_frame_time_ms : 0.0f; } + float GetAvgFPS() const { return avg_frame_time_ms > 0 ? 1000.0f / avg_frame_time_ms : 0.0f; } + size_t GetTotalMemoryMB() const { return total_memory_used / (1024 * 1024); } +}; + +/** + * @brief Configuration settings for Canvas performance tuning + * + * Controls batching behavior, memory management, tessellation quality, + * and performance monitoring options. + */ +struct PerformanceConfig { + // Batching configuration + bool auto_batching_enabled = true; + size_t max_batch_size = 10000; // Maximum objects per batch + size_t batch_resize_threshold = 5000; // When to resize batch buffers + + // Memory management + bool object_pooling_enabled = true; + size_t initial_pool_size = 1000; // Initial pool size for reusable objects + size_t max_pool_size = 50000; // Maximum pool size + bool aggressive_memory_cleanup = false; // Clean up unused memory more aggressively + + // Performance monitoring + bool detailed_timing_enabled = false; // Enable detailed per-operation timing + bool memory_tracking_enabled = true; // Track memory usage + size_t stats_update_frequency = 60; // Update stats every N frames + + // Quality vs Performance trade-offs + int circle_segments = 32; // Default circle tessellation + int ellipse_segments = 32; // Default ellipse tessellation + bool adaptive_tessellation = true; // Adjust tessellation based on size + float tessellation_scale_factor = 50.0f; // Pixels per segment for adaptive mode +}; + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_CANVAS_PERFORMANCE_HPP */ \ No newline at end of file diff --git a/src/renderer/src/camera.cpp b/src/renderer/src/camera.cpp index bc6fde1..7fc4c70 100644 --- a/src/renderer/src/camera.cpp +++ b/src/renderer/src/camera.cpp @@ -131,7 +131,7 @@ void Camera::ProcessMouseMovement(float x_offset, float y_offset, } void Camera::ProcessMouseScroll(float y_offset) { - fov_ -= y_offset; + fov_ -= y_offset * zoom_speed_; if (fov_ < fov_min) fov_ = fov_min; if (fov_ > fov_max) fov_ = fov_max; } diff --git a/src/renderer/src/camera_controller.cpp b/src/renderer/src/camera_controller.cpp index c393688..ae555a4 100644 --- a/src/renderer/src/camera_controller.cpp +++ b/src/renderer/src/camera_controller.cpp @@ -21,9 +21,7 @@ CameraController::CameraController(Camera& camera, glm::vec3 position, UpdateOrbitPosition(); } -void CameraController::Reset() { - camera_.Reset(); -} +void CameraController::Reset() { camera_.Reset(); } void CameraController::SetMode(CameraController::Mode mode) { if (mode == mode_) return; @@ -39,7 +37,7 @@ void CameraController::SetMode(CameraController::Mode mode) { glm::vec3 position = camera_.GetPosition(); if (position.y < 1.0f) position.y = 1.0f; // Set a minimum height camera_.SetPosition(position); - + // Reset rotation angle for top-down view top_down_rotation_ = 0.0f; } @@ -49,6 +47,25 @@ void CameraController::SetActiveMouseButton(int button) { active_mouse_button_ = button; } +void CameraController::SetHeight(float height) { + glm::vec3 position = camera_.GetPosition(); + position.y = height; + camera_.SetPosition(position); +} + +glm::vec2 CameraController::GetPosition() const { + auto pos = camera_.GetPosition(); + // Return only X and Z coordinates for 2D position + return glm::vec2(pos.x, pos.z); +} + +void CameraController::SetPosition(const glm::vec2& position) { + glm::vec3 pos = camera_.GetPosition(); + pos.x = position.x; + pos.z = position.y; // Use Y for Z in 2D view + camera_.SetPosition(pos); +} + void CameraController::ProcessKeyboard( CameraController::CameraMovement direction, float delta_time) { if (mode_ == Mode::kOrbit) return; @@ -83,43 +100,48 @@ void CameraController::ProcessMouseMovement(float x_offset, float y_offset) { case Mode::kTopDown: // Handle mouse movement for top-down view based on mouse button if (active_mouse_button_ == MouseButton::kLeft) { - // Instead of directly setting camera yaw, we'll track rotation angle ourselves - // and apply it only when there's significant mouse movement + // Instead of directly setting camera yaw, we'll track rotation angle + // ourselves and apply it only when there's significant mouse movement float rotation_sensitivity = 0.5f; - + // Only process rotation if there's actual mouse movement if (std::abs(x_offset) > 0.1f) { // Update our own rotation variable top_down_rotation_ += x_offset * rotation_sensitivity; - + // Keep rotation in range [0, 360) while (top_down_rotation_ >= 360.0f) top_down_rotation_ -= 360.0f; while (top_down_rotation_ < 0.0f) top_down_rotation_ += 360.0f; - + // Set the camera yaw directly instead of using ProcessMouseMovement camera_.SetYaw(top_down_rotation_); } - } - else if (active_mouse_button_ == MouseButton::kMiddle) { - // Translation/panning on the X-Z plane - implement true dragging behavior + } else if (active_mouse_button_ == MouseButton::kMiddle) { + // Translation/panning on the X-Z plane - implement true dragging + // behavior float sensitivity = 0.01f; - - // Calculate movement based on camera height for consistent speed at different zoom levels - float height_factor = camera_.GetPosition().y / 10.0f; // Normalize based on height - if (height_factor < 0.1f) height_factor = 0.1f; // Minimum factor - - // Apply rotation to the mouse movement vectors to correctly map to the rotated world + + // Calculate movement based on camera height for consistent speed at + // different zoom levels + float height_factor = + camera_.GetPosition().y / 10.0f; // Normalize based on height + if (height_factor < 0.1f) height_factor = 0.1f; // Minimum factor + + // Apply rotation to the mouse movement vectors to correctly map to the + // rotated world // 1. Create a rotation matrix for the current rotation angle float angle_rad = glm::radians(top_down_rotation_); - + // 2. Calculate rotated axes based on the current rotation - float rot_dx = -y_offset * std::cos(angle_rad) - x_offset * std::sin(angle_rad); - float rot_dz = -y_offset * std::sin(angle_rad) + x_offset * std::cos(angle_rad); - + float rot_dx = + -y_offset * std::cos(angle_rad) - x_offset * std::sin(angle_rad); + float rot_dz = + -y_offset * std::sin(angle_rad) + x_offset * std::cos(angle_rad); + // 3. Apply sensitivity and height scaling rot_dx *= sensitivity * height_factor; rot_dz *= sensitivity * height_factor; - + // 4. Update camera position glm::vec3 position = camera_.GetPosition(); position.x += rot_dx; @@ -132,13 +154,13 @@ void CameraController::ProcessMouseMovement(float x_offset, float y_offset) { void CameraController::ProcessMouseScroll(float y_offset) { if (mode_ == Mode::kOrbit) { - orbit_distance_ -= y_offset; + orbit_distance_ -= y_offset * orbit_zoom_speed_; if (orbit_distance_ < 1.0f) orbit_distance_ = 1.0f; UpdateOrbitPosition(); } else if (mode_ == Mode::kTopDown) { glm::vec3 position = camera_.GetPosition(); // In TopDown mode, adjust Y position (height) with scroll - position.y -= y_offset; + position.y -= y_offset * topdown_zoom_speed_; if (position.y < 1.0f) position.y = 1.0f; // Set a minimum height camera_.SetPosition(position); } else { diff --git a/src/renderer/src/gl_scene_manager.cpp b/src/renderer/src/gl_scene_manager.cpp index 2d5acf6..090d21b 100644 --- a/src/renderer/src/gl_scene_manager.cpp +++ b/src/renderer/src/gl_scene_manager.cpp @@ -52,6 +52,11 @@ void GlSceneManager::SetBackgroundColor(float r, float g, float b, float a) { background_color_ = glm::vec4(r, g, b, a); } +void GlSceneManager::SetClippingPlanes(float z_near, float z_far) { + z_near_ = z_near; + z_far_ = z_far; +} + void GlSceneManager::AddOpenGLObject(const std::string& name, std::unique_ptr object) { if (object == nullptr) { @@ -117,9 +122,7 @@ void GlSceneManager::DrawOpenGLObject() { } } -void GlSceneManager::Draw() { - Begin(); - +void GlSceneManager::RenderInsideWindow() { // update view according to user input ImGuiIO& io = ImGui::GetIO(); ImVec2 content_size = ImGui::GetContentRegionAvail(); @@ -159,7 +162,8 @@ void GlSceneManager::Draw() { ? static_cast(content_size.x) / static_cast(content_size.y) : frame_buffer_->GetAspectRatio(); - glm::mat4 projection = camera_->GetProjectionMatrix(aspect_ratio); + glm::mat4 projection = + camera_->GetProjectionMatrix(aspect_ratio, z_near_, z_far_); glm::mat4 view = camera_->GetViewMatrix(); UpdateView(projection, view); @@ -181,6 +185,12 @@ void GlSceneManager::Draw() { ImGui::PopStyleColor(); ImGui::PopFont(); } +} + +void GlSceneManager::Draw() { + Begin(); + + RenderInsideWindow(); End(); } diff --git a/src/renderer/src/renderable/canvas.cpp b/src/renderer/src/renderable/canvas.cpp index 27fd73d..3c3cb4b 100644 --- a/src/renderer/src/renderable/canvas.cpp +++ b/src/renderer/src/renderable/canvas.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" @@ -20,6 +21,12 @@ #include #include "renderable/details/canvas_data.hpp" +#include "renderer/renderable/details/canvas_batching.hpp" +#include "renderer/renderable/details/canvas_performance.hpp" +#include "renderable/details/render_strategy.hpp" +#include "renderable/details/batched_render_strategy.hpp" +#include "renderable/details/individual_render_strategy.hpp" +#include "renderable/details/shape_renderer.hpp" namespace quickviz { namespace { @@ -142,11 +149,11 @@ void main() { FragColor = texColor; // Add a subtle border for visual feedback on the texture boundaries - float border = 0.005; - if (TexCoord.x < border || TexCoord.x > 1.0 - border || - TexCoord.y < border || TexCoord.y > 1.0 - border) { - FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), texColor, 0.3); // Subtle red border - } + // float border = 0.005; + // if (TexCoord.x < border || TexCoord.x > 1.0 - border || + // TexCoord.y < border || TexCoord.y > 1.0 - border) { + // FragColor = mix(vec4(1.0, 0.0, 0.0, 1.0), texColor, 0.3); // Subtle red border + // } } )"; } // namespace @@ -163,11 +170,30 @@ Canvas::Canvas() { // Initialize the data object (just to be extra safe) data_->Clear(); + // Initialize render strategies + // Note: shape_renderer_ will be created after GPU resources are allocated + batched_strategy_ = std::make_unique( + line_batches_, filled_shape_batch_, outline_shape_batches_); + individual_strategy_ = std::make_unique(); + + // Set default strategy based on batching preference + current_render_strategy_ = batching_enabled_ ? + static_cast(batched_strategy_.get()) : + static_cast(individual_strategy_.get()); + // Re-enable batching now that ellipse/polygon renderMode is fixed batching_enabled_ = true; AllocateGpuResources(); InitializeBatches(); + + // Create shape renderer after GPU resources are allocated + shape_renderer_ = std::make_unique(&primitive_shader_); + + // Update render strategies with shape renderer + batched_strategy_ = std::make_unique( + line_batches_, filled_shape_batch_, outline_shape_batches_, shape_renderer_.get()); + individual_strategy_ = std::make_unique(shape_renderer_.get()); } Canvas::~Canvas() { @@ -274,13 +300,13 @@ void Canvas::AddBackgroundImage(const std::string& image_path, std::cout << " Top-right: (" << tr.x << ", " << tr.y << ")" << std::endl; std::cout << " Top-left: (" << tl.x << ", " << tl.y << ")" << std::endl; - // Vertices for the transformed quad (all on z=0 plane) + // Vertices for the transformed quad (placed behind primitives) float vertices[] = { // Positions (x, y, z) // Texture coords - bl.x, bl.y, 0.0f, 0.0f, 0.0f, // Bottom left - br.x, br.y, 0.0f, 1.0f, 0.0f, // Bottom right - tr.x, tr.y, 0.0f, 1.0f, 1.0f, // Top right - tl.x, tl.y, 0.0f, 0.0f, 1.0f // Top left + bl.x, bl.y, -0.01f, 0.0f, 0.0f, // Bottom left + br.x, br.y, -0.01f, 1.0f, 0.0f, // Bottom right + tr.x, tr.y, -0.01f, 1.0f, 1.0f, // Top right + tl.x, tl.y, -0.01f, 0.0f, 1.0f // Top left }; // Update the vertex buffer with the new positions @@ -408,6 +434,22 @@ void Canvas::SetupBackgroundImage(int width, int height, int channels, uint32_t texture_id = background_texture_.load(); glBindTexture(GL_TEXTURE_2D, texture_id); + // Check for OpenGL row alignment issues + // OpenGL expects texture data to be aligned to 4-byte boundaries by default + int bytes_per_pixel = (format == GL_RGBA) ? 4 : (format == GL_RGB) ? 3 : 1; + int bytes_per_row = width * bytes_per_pixel; + bool alignment_issue = (bytes_per_row % 4) != 0; + + if (alignment_issue) { + std::cout << "Canvas: Adjusting alignment for " << width << "x" << height + << " texture (row size: " << bytes_per_row << " bytes)" << std::endl; + // Set pixel alignment to 1 byte to handle unaligned row data + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + } + + // Clear any previous OpenGL errors + while (glGetError() != GL_NO_ERROR); + glTexImage2D(GL_TEXTURE_2D, 0, // Mipmap level internal_format, // Internal format @@ -417,14 +459,59 @@ void Canvas::SetupBackgroundImage(int width, int height, int channels, GL_UNSIGNED_BYTE, // Type data // Data ); + + // Restore default alignment if we changed it + if (alignment_issue) { + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // Restore default + } + + // Check for GL errors after texture upload + GLenum error = glGetError(); + if (error != GL_NO_ERROR) { + std::cerr << "OpenGL error after glTexImage2D: " << error; + switch (error) { + case GL_INVALID_ENUM: + std::cerr << " (GL_INVALID_ENUM - format/internal format invalid)"; + break; + case GL_INVALID_VALUE: + std::cerr << " (GL_INVALID_VALUE - width/height invalid)"; + break; + case GL_INVALID_OPERATION: + std::cerr << " (GL_INVALID_OPERATION - format/internal format incompatible)"; + break; + case GL_OUT_OF_MEMORY: + std::cerr << " (GL_OUT_OF_MEMORY - insufficient memory)"; + break; + default: + std::cerr << " (Unknown error)"; + break; + } + std::cerr << std::endl; + glBindTexture(GL_TEXTURE_2D, 0); + return; + } // Generate mipmaps for better quality when scaled glGenerateMipmap(GL_TEXTURE_2D); - // Check for GL errors - GLenum error = glGetError(); + // Check for GL errors after mipmap generation + error = glGetError(); if (error != GL_NO_ERROR) { - std::cerr << "OpenGL error after texture setup: " << error << std::endl; + std::cerr << "OpenGL error after glGenerateMipmap: " << error; + switch (error) { + case GL_INVALID_ENUM: + std::cerr << " (GL_INVALID_ENUM - target invalid)"; + break; + case GL_INVALID_OPERATION: + std::cerr << " (GL_INVALID_OPERATION - texture not complete)"; + break; + default: + std::cerr << " (Unknown error)"; + break; + } + std::cerr << std::endl; + glBindTexture(GL_TEXTURE_2D, 0); + return; } // Unbind the texture @@ -452,12 +539,13 @@ void Canvas::SetupBackgroundImage(int width, int height, int channels, // Default setup of a simple quad - actual coordinates will be updated in // AddBackgroundImage We're creating a 1x1 quad centered at the origin + // Using very small negative Z to ensure background renders behind primitives while minimizing alignment issues float vertices[] = { // Positions (x, y, z) // Texture coords - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // Bottom left - 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Bottom right - 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // Top right - -0.5f, 0.5f, 0.0f, 0.0f, 1.0f // Top left + -0.5f, -0.5f, -0.001f, 0.0f, 0.0f, // Bottom left + 0.5f, -0.5f, -0.001f, 1.0f, 0.0f, // Bottom right + 0.5f, 0.5f, -0.001f, 1.0f, 1.0f, // Top right + -0.5f, 0.5f, -0.001f, 0.0f, 1.0f // Top left }; // Setup the VBO and VAO @@ -607,114 +695,142 @@ void Canvas::ProcessPendingUpdates() { // Points still use the original system since they're already efficient data_->AddPoint(update.point.x, update.point.y, update.color, update.thickness); break; - - case PendingUpdate::Type::kLine: - // Add line to batch - line_batch_.vertices.emplace_back(update.line.x1, update.line.y1, 0.0f); - line_batch_.vertices.emplace_back(update.line.x2, update.line.y2, 0.0f); - line_batch_.colors.push_back(update.color); - line_batch_.colors.push_back(update.color); - line_batch_.thicknesses.push_back(update.thickness); - line_batch_.line_types.push_back(update.line_type); - line_batch_.needs_update = true; + case PendingUpdate::Type::kLine: { + // Add line to batch based on line type + auto& line_batch = line_batches_[update.line_type]; + line_batch.vertices.emplace_back(update.line.x1, update.line.y1, 0.0f); + line_batch.vertices.emplace_back(update.line.x2, update.line.y2, 0.0f); + line_batch.colors.push_back(update.color); + line_batch.colors.push_back(update.color); + line_batch.thicknesses.push_back(update.thickness); + line_batch.line_types.push_back(update.line_type); + line_batch.needs_update = true; break; - + } case PendingUpdate::Type::kRectangle: { - uint32_t base_index = update.filled ? - filled_shape_batch_.vertices.size() / 3 : - outline_shape_batch_.vertices.size() / 3; - if (update.filled) { - GenerateRectangleVertices(update.rect.x, update.rect.y, - update.rect.width, update.rect.height, - filled_shape_batch_.vertices, - filled_shape_batch_.indices, - true, base_index); + uint32_t base_index = filled_shape_batch_.vertices.size() / 3; + GenerateRectangleVertices(update.rect.x, update.rect.y, + update.rect.width, update.rect.height, + filled_shape_batch_.vertices, + filled_shape_batch_.indices, + true, base_index); // Add color for each vertex (4 vertices for rectangle) for (int i = 0; i < 4; i++) { filled_shape_batch_.colors.push_back(update.color); } filled_shape_batch_.needs_update = true; } else { + auto& outline_batch = outline_shape_batches_[update.line_type]; + uint32_t base_index = outline_batch.vertices.size() / 3; GenerateRectangleVertices(update.rect.x, update.rect.y, - update.rect.width, update.rect.height, - outline_shape_batch_.vertices, - outline_shape_batch_.indices, - false, base_index); + update.rect.width, update.rect.height, + outline_batch.vertices, + outline_batch.indices, + false, base_index); // Add color for each vertex (4 vertices for rectangle) for (int i = 0; i < 4; i++) { - outline_shape_batch_.colors.push_back(update.color); + outline_batch.colors.push_back(update.color); } - outline_shape_batch_.needs_update = true; + outline_batch.needs_update = true; } break; } - case PendingUpdate::Type::kCircle: { const int segments = 32; // Could be made adaptive based on radius - uint32_t base_index = update.filled ? - filled_shape_batch_.vertices.size() / 3 : - outline_shape_batch_.vertices.size() / 3; - if (update.filled) { - GenerateCircleVertices(update.circle.x, update.circle.y, - update.circle.radius, segments, - filled_shape_batch_.vertices, - filled_shape_batch_.indices, - true, base_index); + uint32_t base_index = filled_shape_batch_.vertices.size() / 3; + GenerateCircleVertices(update.circle.x, update.circle.y, + update.circle.radius, segments, + filled_shape_batch_.vertices, + filled_shape_batch_.indices, + true, base_index); // Add color for center + perimeter vertices - for (int i = 0; i <= segments + 1; i++) { + // Center vertex (1) + perimeter vertices (segments + 1) = segments + 2 total + for (int i = 0; i < segments + 2; i++) { filled_shape_batch_.colors.push_back(update.color); } filled_shape_batch_.needs_update = true; } else { + auto& outline_batch = outline_shape_batches_[update.line_type]; + uint32_t base_index = outline_batch.vertices.size() / 3; GenerateCircleVertices(update.circle.x, update.circle.y, - update.circle.radius, segments, - outline_shape_batch_.vertices, - outline_shape_batch_.indices, - false, base_index); + update.circle.radius, segments, + outline_batch.vertices, + outline_batch.indices, + false, base_index); // Add color for perimeter vertices - for (int i = 0; i <= segments; i++) { - outline_shape_batch_.colors.push_back(update.color); + // Perimeter vertices: segments + 1 total (0 to segments inclusive) + for (int i = 0; i < segments + 1; i++) { + outline_batch.colors.push_back(update.color); } - outline_shape_batch_.needs_update = true; + outline_batch.needs_update = true; } break; } - - case PendingUpdate::Type::kEllipse: - // For now, fall back to individual rendering for ellipses - // TODO: Implement ellipse batching - data_->AddEllipse(update.ellipse.x, update.ellipse.y, update.ellipse.rx, - update.ellipse.ry, update.ellipse.angle, - update.ellipse.start_angle, update.ellipse.end_angle, - update.color, update.filled, update.thickness, - update.line_type); + case PendingUpdate::Type::kEllipse: { + const int segments = 32; + if (update.filled) { + uint32_t base_index = filled_shape_batch_.vertices.size() / 3; + GenerateEllipseVertices(update.ellipse, filled_shape_batch_.vertices, + filled_shape_batch_.indices, true, base_index); + for (int i = 0; i < segments + 2; i++) { + filled_shape_batch_.colors.push_back(update.color); + } + filled_shape_batch_.needs_update = true; + } else { + auto& outline_batch = outline_shape_batches_[update.line_type]; + uint32_t base_index = outline_batch.vertices.size() / 3; + GenerateEllipseVertices(update.ellipse, outline_batch.vertices, + outline_batch.indices, false, base_index); + for (int i = 0; i < segments + 1; i++) { + outline_batch.colors.push_back(update.color); + } + outline_batch.needs_update = true; + } break; - - case PendingUpdate::Type::kPolygon: - // For now, fall back to individual rendering for polygons - // TODO: Implement polygon batching - data_->AddPolygon(update.polygon_vertices, update.color, update.filled, - update.thickness, update.line_type); + } + case PendingUpdate::Type::kPolygon: { + if (update.filled) { + uint32_t base_index = filled_shape_batch_.vertices.size() / 3; + GeneratePolygonVertices(update.polygon_vertices, filled_shape_batch_.vertices, + filled_shape_batch_.indices, true, base_index); + for (int i = 0; i < update.polygon_vertices.size(); i++) { + filled_shape_batch_.colors.push_back(update.color); + } + filled_shape_batch_.needs_update = true; + } else { + auto& outline_batch = outline_shape_batches_[update.line_type]; + uint32_t base_index = outline_batch.vertices.size() / 3; + GeneratePolygonVertices(update.polygon_vertices, outline_batch.vertices, + outline_batch.indices, false, base_index); + for (int i = 0; i < update.polygon_vertices.size(); i++) { + outline_batch.colors.push_back(update.color); + } + outline_batch.needs_update = true; + } break; - + } case PendingUpdate::Type::kClear: // Clear both traditional data and batches data_->Clear(); - line_batch_.vertices.clear(); - line_batch_.colors.clear(); - line_batch_.thicknesses.clear(); - line_batch_.line_types.clear(); + for (auto& [line_type, line_batch] : line_batches_) { + line_batch.vertices.clear(); + line_batch.colors.clear(); + line_batch.thicknesses.clear(); + line_batch.line_types.clear(); + line_batch.needs_update = true; + } filled_shape_batch_.vertices.clear(); filled_shape_batch_.indices.clear(); filled_shape_batch_.colors.clear(); - outline_shape_batch_.vertices.clear(); - outline_shape_batch_.indices.clear(); - outline_shape_batch_.colors.clear(); - line_batch_.needs_update = true; + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_batch.vertices.clear(); + outline_batch.indices.clear(); + outline_batch.colors.clear(); + outline_batch.needs_update = true; + } filled_shape_batch_.needs_update = true; - outline_shape_batch_.needs_update = true; break; } } else { @@ -894,7 +1010,14 @@ void Canvas::OnDraw(const glm::mat4& projection, const glm::mat4& view, ProcessPendingUpdates(); } - // Draw background if available + // Get a copy of the data to avoid locking during rendering + CanvasData data; + { + std::lock_guard lock(data_mutex_); + data = *data_; + } + + // Draw background if available (render first, so primitives appear on top) { std::lock_guard lock(background_mutex_); uint32_t texture_id = background_texture_.load(); @@ -914,6 +1037,9 @@ void Canvas::OnDraw(const glm::mat4& projection, const glm::mat4& view, // Setup blending for proper transparency glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Enable depth testing to ensure background renders behind primitives + glEnable(GL_DEPTH_TEST); // Draw quad glBindVertexArray(background_vao_); @@ -935,459 +1061,40 @@ void Canvas::OnDraw(const glm::mat4& projection, const glm::mat4& view, glUseProgram(0); } } - - // Get a copy of the data to avoid locking during rendering - CanvasData data; - { - std::lock_guard lock(data_mutex_); - data = *data_; - } - + // Skip if there's no data to render - if (data.points.empty() && data.lines.empty() && - data.rectangles.empty() && data.circles.empty() && - data.ellipses.empty() && data.polygons.empty()) { - return; - } - - // Choose rendering path based on batching setting - if (batching_enabled_) { - // Use efficient batched rendering for better performance - RenderBatches(projection, view, coord_transform); - - // Still render points individually (they're already efficient) - if (!data.points.empty()) { - // Setup for point rendering - primitive_shader_.Use(); - primitive_shader_.TrySetUniform("projection", projection); - primitive_shader_.TrySetUniform("view", view); - primitive_shader_.TrySetUniform("model", glm::mat4(1.0f)); - primitive_shader_.TrySetUniform("coordSystemTransform", coord_transform); - primitive_shader_.TrySetUniform("renderMode", 0); // Points mode - - glEnable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glBindVertexArray(primitive_vao_); - glEnable(GL_PROGRAM_POINT_SIZE); - - // Update buffer with point data - glBindBuffer(GL_ARRAY_BUFFER, primitive_vbo_); - glBufferData(GL_ARRAY_BUFFER, sizeof(Point) * data.points.size(), - data.points.data(), GL_DYNAMIC_DRAW); - - // Enable point attributes - glEnableVertexAttribArray(0); // Position - glEnableVertexAttribArray(1); // Color - glEnableVertexAttribArray(2); // Size - - // Draw points - glDrawArrays(GL_POINTS, 0, data.points.size()); - - // Cleanup point rendering - glDisable(GL_PROGRAM_POINT_SIZE); - glDisableVertexAttribArray(0); - glDisableVertexAttribArray(1); - glDisableVertexAttribArray(2); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - - // Update stats - render_stats_.points_rendered = data.points.size(); - render_stats_.draw_calls++; - } - - // Handle non-batched shapes (ellipses, polygons) with individual rendering - if (!data.ellipses.empty() || !data.polygons.empty()) { - RenderIndividualShapes(data, projection, view, coord_transform); - } - - return; // Early return to skip the old rendering path - } - - // Original individual rendering system (fallback when batching is disabled) - // Setup common rendering state for primitives - primitive_shader_.Use(); - primitive_shader_.TrySetUniform("projection", projection); - primitive_shader_.TrySetUniform("view", view); - primitive_shader_.TrySetUniform("model", glm::mat4(1.0f)); - primitive_shader_.TrySetUniform("coordSystemTransform", coord_transform); - primitive_shader_.TrySetUniform("lineType", 0); // Default to solid line - primitive_shader_.TrySetUniform("thickness", 1.0f); // Default thickness - primitive_shader_.TrySetUniform("uColor", glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); // Default to white - - // Enable depth test and blending - glEnable(GL_DEPTH_TEST); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // 1. Draw Points - if (!data.points.empty()) { - glBindVertexArray(primitive_vao_); - glEnable(GL_PROGRAM_POINT_SIZE); - - // Set drawing mode to points - primitive_shader_.TrySetUniform("renderMode", 0); - - // Update the buffer with current point data - glBindBuffer(GL_ARRAY_BUFFER, primitive_vbo_); - glBufferData(GL_ARRAY_BUFFER, sizeof(Point) * data.points.size(), data.points.data(), GL_DYNAMIC_DRAW); - - // For points, we use the per-vertex color from the buffer - // This color is passed to the shader via the 'aColor' attribute and becomes 'fragColor' - - // Make sure all attributes are correctly enabled for points - glEnableVertexAttribArray(0); // Position - glEnableVertexAttribArray(1); // Color - glEnableVertexAttribArray(2); // Size - - // Draw the points - glDrawArrays(GL_POINTS, 0, data.points.size()); - - glDisable(GL_PROGRAM_POINT_SIZE); - - // Clean up point rendering state - glDisableVertexAttribArray(0); - glDisableVertexAttribArray(1); - glDisableVertexAttribArray(2); - - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - } - - // Set drawing mode to lines/shapes for all subsequent primitives - primitive_shader_.TrySetUniform("renderMode", 1); - - // 2. Draw Lines - Use modern OpenGL approach - if (!data.lines.empty()) { - for (const auto& line : data.lines) { - // Set line width - glLineWidth(line.thickness); - - // Create temporary VBO/VAO for the line - GLuint tempVAO, tempVBO; - glGenVertexArrays(1, &tempVAO); - glGenBuffers(1, &tempVBO); - - glBindVertexArray(tempVAO); - glBindBuffer(GL_ARRAY_BUFFER, tempVBO); - - // Line vertices - float vertices[] = { - line.start.x, line.start.y, line.start.z, - line.end.x, line.end.y, line.end.z - }; - - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Make sure other attributes are disabled - glDisableVertexAttribArray(1); - glDisableVertexAttribArray(2); - - // Set line color in the shader - primitive_shader_.TrySetUniform("uColor", line.color); - - // Handle different line types with shader-based approach - primitive_shader_.TrySetUniform("lineType", static_cast(line.line_type)); - primitive_shader_.TrySetUniform("thickness", line.thickness); - - // Draw the line - glDrawArrays(GL_LINES, 0, 2); - - // Clean up - glDisableVertexAttribArray(0); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDeleteVertexArrays(1, &tempVAO); - glDeleteBuffers(1, &tempVBO); + bool has_line_data = false; + for (const auto& [line_type, line_batch] : line_batches_) { + if (!line_batch.vertices.empty()) { + has_line_data = true; + break; } - - // Reset line width - glLineWidth(1.0f); } - - // 3. Draw Rectangles - Use modern OpenGL approach - if (!data.rectangles.empty()) { - for (const auto& rect : data.rectangles) { - // Create temporary VBO/VAO for the rectangle - GLuint tempVAO, tempVBO; - glGenVertexArrays(1, &tempVAO); - glGenBuffers(1, &tempVBO); - - glBindVertexArray(tempVAO); - glBindBuffer(GL_ARRAY_BUFFER, tempVBO); - - // Rectangle vertices (2 triangles making a quad) - float x = rect.position.x; - float y = rect.position.y; - float z = rect.position.z; - float w = rect.width; - float h = rect.height; - - float vertices[] = { - x, y, z, // Bottom left - x + w, y, z, // Bottom right - x + w, y + h, z, // Top right - x, y + h, z // Top left - }; - - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Make sure other attributes are disabled - glDisableVertexAttribArray(1); - glDisableVertexAttribArray(2); - - // Set rectangle color - primitive_shader_.TrySetUniform("uColor", rect.color); - - if (rect.filled) { - // Draw filled rectangle - primitive_shader_.TrySetUniform("lineType", 0); // Solid fill - glDrawArrays(GL_TRIANGLE_FAN, 0, 4); - } else { - // Draw outline - glLineWidth(rect.thickness); - - // Handle different line types with shader-based approach - primitive_shader_.TrySetUniform("lineType", static_cast(rect.line_type)); - primitive_shader_.TrySetUniform("thickness", rect.thickness); - - // Draw rectangle outline - glDrawArrays(GL_LINE_LOOP, 0, 4); - - glLineWidth(1.0f); - } - - // Clean up - glDisableVertexAttribArray(0); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDeleteVertexArrays(1, &tempVAO); - glDeleteBuffers(1, &tempVBO); + + bool has_outline_data = false; + for (const auto& [line_type, outline_batch] : outline_shape_batches_) { + if (!outline_batch.vertices.empty()) { + has_outline_data = true; + break; } } - - // 4. Draw Circles - Use modern OpenGL approach - if (!data.circles.empty()) { - for (const auto& circle : data.circles) { - // Create temporary VBO/VAO for the circle - GLuint tempVAO, tempVBO; - glGenVertexArrays(1, &tempVAO); - glGenBuffers(1, &tempVBO); - - glBindVertexArray(tempVAO); - glBindBuffer(GL_ARRAY_BUFFER, tempVBO); - - // Generate circle vertices - const int segments = circle.num_segments; - std::vector vertices; - vertices.reserve((segments + 2) * 3); // +2 for center and closing point - - // Center point (for filled circles) - if (circle.filled) { - vertices.push_back(circle.center.x); - vertices.push_back(circle.center.y); - vertices.push_back(circle.center.z); - } - - // Circle perimeter points - for (int i = 0; i <= segments; i++) { - float angle = 2.0f * M_PI * i / segments; - float x = circle.center.x + circle.radius * std::cos(angle); - float y = circle.center.y + circle.radius * std::sin(angle); - float z = circle.center.z; - - vertices.push_back(x); - vertices.push_back(y); - vertices.push_back(z); - } - - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Make sure other attributes are disabled - glDisableVertexAttribArray(1); - glDisableVertexAttribArray(2); - - // Set circle color - primitive_shader_.TrySetUniform("uColor", circle.color); - - if (circle.filled) { - // Draw filled circle - primitive_shader_.TrySetUniform("lineType", 0); // Solid fill - glDrawArrays(GL_TRIANGLE_FAN, 0, vertices.size() / 3); - } else { - // Draw outline - glLineWidth(circle.thickness); - - // Handle different line types with shader-based approach - primitive_shader_.TrySetUniform("lineType", static_cast(circle.line_type)); - primitive_shader_.TrySetUniform("thickness", circle.thickness); - - // Draw circle outline - glDrawArrays(GL_LINE_STRIP, circle.filled ? 1 : 0, segments + 1); - - glLineWidth(1.0f); - } - - // Clean up - glDisableVertexAttribArray(0); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDeleteVertexArrays(1, &tempVAO); - glDeleteBuffers(1, &tempVBO); - } + + if (data.points.empty() && data.lines.empty() && data.rectangles.empty() && + data.circles.empty() && data.ellipses.empty() && + data.polygons.empty() && !has_line_data && + filled_shape_batch_.vertices.empty() && !has_outline_data) { + return; } - // 5. Draw Ellipses - Use modern OpenGL approach - if (!data.ellipses.empty()) { - for (const auto& ellipse : data.ellipses) { - // Create temporary VBO/VAO for the ellipse - GLuint tempVAO, tempVBO; - glGenVertexArrays(1, &tempVAO); - glGenBuffers(1, &tempVBO); - - glBindVertexArray(tempVAO); - glBindBuffer(GL_ARRAY_BUFFER, tempVBO); - - // Generate ellipse vertices - const int segments = ellipse.num_segments; - std::vector vertices; - vertices.reserve((segments + 2) * 3); // +2 for center and closing point - - // Center point (for filled ellipses) - if (ellipse.filled) { - vertices.insert(vertices.end(), {ellipse.center.x, ellipse.center.y, ellipse.center.z}); - } - - for (int i = 0; i <= segments; i++) { - float t = ellipse.start_angle + (ellipse.end_angle - ellipse.start_angle) * i / segments; - float x_local = ellipse.rx * std::cos(t); - float y_local = ellipse.ry * std::sin(t); - float x_rotated = x_local * std::cos(ellipse.angle) - y_local * std::sin(ellipse.angle); - float y_rotated = x_local * std::sin(ellipse.angle) + y_local * std::cos(ellipse.angle); - float x = ellipse.center.x + x_rotated; - float y = ellipse.center.y + y_rotated; - float z = ellipse.center.z; - vertices.insert(vertices.end(), {x, y, z}); - } - - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); - - // Set up vertex attributes correctly - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Explicitly set default values for disabled attributes to ensure proper shader behavior - glDisableVertexAttribArray(1); // aColor - will default to (0,0,0,0) - glDisableVertexAttribArray(2); // aSize - will default to 0 - - // Set explicit default values for vertex attributes that aren't used - glVertexAttrib4f(1, 0.0f, 0.0f, 0.0f, 0.0f); // Ensure fragColor.a = 0 to trigger uniform fallback - glVertexAttrib1f(2, 1.0f); // Set default size - - primitive_shader_.TrySetUniform("uColor", ellipse.color); - - if (ellipse.filled) { - primitive_shader_.TrySetUniform("renderMode", 2); // Filled shapes mode - primitive_shader_.TrySetUniform("lineType", 0); // Solid fill - glDrawArrays(GL_TRIANGLE_FAN, 0, vertices.size() / 3); - } else { - primitive_shader_.TrySetUniform("renderMode", 3); // Outline shapes mode - primitive_shader_.TrySetUniform("lineType", static_cast(ellipse.line_type)); - glLineWidth(ellipse.thickness); - glDrawArrays(GL_LINE_STRIP, ellipse.filled ? 1 : 0, segments + 1); - glLineWidth(1.0f); - } - - glDisableVertexAttribArray(0); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDeleteVertexArrays(1, &tempVAO); - glDeleteBuffers(1, &tempVBO); - - render_stats_.shapes_rendered++; - render_stats_.draw_calls++; - } + // Render individual shapes + if (!data.ellipses.empty() || !data.polygons.empty()) { + RenderIndividualShapes(data, projection, view, coord_transform); } - // 6. Draw Polygons - Use modern OpenGL approach - if (!data.polygons.empty()) { - for (const auto& polygon : data.polygons) { - // Create temporary VBO/VAO for the polygon - GLuint tempVAO, tempVBO; - glGenVertexArrays(1, &tempVAO); - glGenBuffers(1, &tempVBO); - - glBindVertexArray(tempVAO); - glBindBuffer(GL_ARRAY_BUFFER, tempVBO); - - // Convert vertices to flat array - std::vector vertices; - vertices.reserve(polygon.vertices.size() * 3); - - for (const auto& vertex : polygon.vertices) { - vertices.push_back(vertex.x); - vertices.push_back(vertex.y); - vertices.push_back(vertex.z); - } - - glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); - - // Position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - - // Make sure other attributes are disabled - glDisableVertexAttribArray(1); - glDisableVertexAttribArray(2); - - // Set polygon color using the uniform (not per-vertex colors) - primitive_shader_.TrySetUniform("uColor", polygon.color); - - if (polygon.filled) { - // Draw filled polygon - primitive_shader_.TrySetUniform("renderMode", 2); // Filled shapes mode - primitive_shader_.TrySetUniform("lineType", 0); // Solid fill - glDrawArrays(GL_TRIANGLE_FAN, 0, polygon.vertices.size()); - } else { - // Draw outline - primitive_shader_.TrySetUniform("renderMode", 3); // Outline shapes mode - primitive_shader_.TrySetUniform("lineType", static_cast(polygon.line_type)); - glLineWidth(polygon.thickness); - glDrawArrays(GL_LINE_LOOP, 0, polygon.vertices.size()); - glLineWidth(1.0f); - } - - // Clean up - glDisableVertexAttribArray(0); - glBindVertexArray(0); - glBindBuffer(GL_ARRAY_BUFFER, 0); - glDeleteVertexArrays(1, &tempVAO); - glDeleteBuffers(1, &tempVBO); - - render_stats_.shapes_rendered++; - render_stats_.draw_calls++; - } + // Render batched shapes + if (has_line_data || !filled_shape_batch_.vertices.empty() || has_outline_data) { + RenderBatches(projection, view, coord_transform); } - - // Reset OpenGL state - glDisable(GL_DEPTH_TEST); - glDisable(GL_BLEND); - glBindVertexArray(0); - glUseProgram(0); } void Canvas::RenderBatches(const glm::mat4& projection, const glm::mat4& view, @@ -1400,14 +1107,26 @@ void Canvas::RenderBatches(const glm::mat4& projection, const glm::mat4& view, // Track memory usage if (perf_config_.memory_tracking_enabled) { + size_t line_vertex_memory = 0; + for (const auto& [key, batch] : line_batches_) { + line_vertex_memory += batch.vertices.size() * sizeof(glm::vec3); + } + + size_t outline_vertex_memory = 0; + size_t outline_index_memory = 0; + for (const auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_vertex_memory += outline_batch.vertices.size() * sizeof(float); + outline_index_memory += outline_batch.indices.size() * sizeof(uint32_t); + } + render_stats_.vertex_memory_used = - line_batch_.vertices.size() * sizeof(glm::vec3) + + line_vertex_memory + filled_shape_batch_.vertices.size() * sizeof(float) + - outline_shape_batch_.vertices.size() * sizeof(float); + outline_vertex_memory; render_stats_.index_memory_used = filled_shape_batch_.indices.size() * sizeof(uint32_t) + - outline_shape_batch_.indices.size() * sizeof(uint32_t); + outline_index_memory; render_stats_.total_memory_used = render_stats_.vertex_memory_used + @@ -1431,19 +1150,34 @@ void Canvas::RenderBatches(const glm::mat4& projection, const glm::mat4& view, glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); render_stats_.state_changes += 3; // Track state changes - // Render lines batch - if (!line_batch_.vertices.empty()) { - primitive_shader_.TrySetUniform("renderMode", 1); // Lines mode - primitive_shader_.TrySetUniform("lineType", 0); // Solid lines for now - - glBindVertexArray(line_batch_.vao); - glDrawArrays(GL_LINES, 0, line_batch_.vertices.size()); - glBindVertexArray(0); - - render_stats_.lines_rendered = line_batch_.vertices.size() / 2; - render_stats_.batched_objects += line_batch_.vertices.size() / 2; - render_stats_.draw_calls++; - render_stats_.state_changes += 2; // VAO bind/unbind + // Render lines batch by line type + for (auto& [line_type, line_batch] : line_batches_) { + if (!line_batch.vertices.empty()) { + primitive_shader_.TrySetUniform("renderMode", 1); // Lines mode + primitive_shader_.TrySetUniform("lineType", static_cast(line_type)); + + // Group by thickness and render each group + std::map>> thickness_groups; + for (size_t i = 0; i < line_batch.thicknesses.size(); ++i) { + float thickness = line_batch.thicknesses[i]; + thickness_groups[thickness].emplace_back(i * 2, 2); // vertex start, count + } + + glBindVertexArray(line_batch.vao); + for (const auto& [thickness, ranges] : thickness_groups) { + glLineWidth(thickness); + for (const auto& [start, count] : ranges) { + glDrawArrays(GL_LINES, start, count); + render_stats_.draw_calls++; + } + render_stats_.lines_rendered += ranges.size(); + render_stats_.batched_objects += ranges.size(); + } + glLineWidth(1.0f); // Reset to default + glBindVertexArray(0); + + render_stats_.state_changes += 2; // VAO bind/unbind + } } // Render filled shapes batch @@ -1460,19 +1194,21 @@ void Canvas::RenderBatches(const glm::mat4& projection, const glm::mat4& view, render_stats_.state_changes += 2; // VAO bind/unbind } - // Render outline shapes batch - if (!outline_shape_batch_.vertices.empty() && !outline_shape_batch_.indices.empty()) { - primitive_shader_.TrySetUniform("renderMode", 3); // Outline shapes mode - primitive_shader_.TrySetUniform("lineType", 0); // Solid lines for now - - glBindVertexArray(outline_shape_batch_.vao); - glDrawElements(GL_LINES, outline_shape_batch_.indices.size(), GL_UNSIGNED_INT, 0); - glBindVertexArray(0); - - render_stats_.shapes_rendered += outline_shape_batch_.indices.size() / 8; // Approximate shapes - render_stats_.batched_objects += outline_shape_batch_.indices.size() / 8; - render_stats_.draw_calls++; - render_stats_.state_changes += 2; // VAO bind/unbind + // Render outline shapes batch by line type + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + if (!outline_batch.vertices.empty() && !outline_batch.indices.empty()) { + primitive_shader_.TrySetUniform("renderMode", 3); // Outline shapes mode + primitive_shader_.TrySetUniform("lineType", static_cast(line_type)); + + glBindVertexArray(outline_batch.vao); + glDrawElements(GL_LINES, outline_batch.indices.size(), GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + + render_stats_.shapes_rendered += outline_batch.indices.size() / 8; // Approximate shapes + render_stats_.batched_objects += outline_batch.indices.size() / 8; + render_stats_.draw_calls++; + render_stats_.state_changes += 2; // VAO bind/unbind + } } // Reset OpenGL state @@ -1606,18 +1342,22 @@ void Canvas::RenderIndividualShapes(const CanvasData& data, const glm::mat4& pro glDisableVertexAttribArray(1); glDisableVertexAttribArray(2); - // Set polygon color using the uniform (not per-vertex colors) - primitive_shader_.TrySetUniform("uColor", polygon.color); + // Set default values for disabled attributes (CRITICAL FIX!) + glVertexAttrib4f(1, polygon.color.r, polygon.color.g, polygon.color.b, polygon.color.a); // Set color attribute to match polygon + glVertexAttrib1f(2, 1.0f); // Set default size attribute if (polygon.filled) { - // Draw filled polygon + // Draw filled polygon - set renderMode FIRST, then color primitive_shader_.TrySetUniform("renderMode", 2); // Filled shapes mode primitive_shader_.TrySetUniform("lineType", 0); // Solid fill + primitive_shader_.TrySetUniform("uColor", polygon.color); glDrawArrays(GL_TRIANGLE_FAN, 0, polygon.vertices.size()); } else { - // Draw outline + // Draw outline - set renderMode FIRST, then color primitive_shader_.TrySetUniform("renderMode", 3); // Outline shapes mode primitive_shader_.TrySetUniform("lineType", static_cast(polygon.line_type)); + primitive_shader_.TrySetUniform("thickness", polygon.thickness); + primitive_shader_.TrySetUniform("uColor", polygon.color); glLineWidth(polygon.thickness); glDrawArrays(GL_LINE_LOOP, 0, polygon.vertices.size()); glLineWidth(1.0f); @@ -1633,6 +1373,157 @@ void Canvas::RenderIndividualShapes(const CanvasData& data, const glm::mat4& pro render_stats_.shapes_rendered++; render_stats_.draw_calls++; } + + // Render lines individually + for (const auto& line : data.lines) { + GLuint tempVAO, tempVBO; + glGenVertexArrays(1, &tempVAO); + glGenBuffers(1, &tempVBO); + + glBindVertexArray(tempVAO); + glBindBuffer(GL_ARRAY_BUFFER, tempVBO); + + std::vector vertices = {line.start.x, line.start.y, line.start.z, line.end.x, line.end.y, line.end.z}; + + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + + glVertexAttrib4f(1, 0.0f, 0.0f, 0.0f, 0.0f); + glVertexAttrib1f(2, 1.0f); + + primitive_shader_.TrySetUniform("uColor", line.color); + primitive_shader_.TrySetUniform("renderMode", 1); + primitive_shader_.TrySetUniform("lineType", static_cast(line.line_type)); + + glLineWidth(line.thickness); + glDrawArrays(GL_LINES, 0, 2); + glLineWidth(1.0f); + + glDisableVertexAttribArray(0); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteVertexArrays(1, &tempVAO); + glDeleteBuffers(1, &tempVBO); + + render_stats_.lines_rendered++; + render_stats_.draw_calls++; + } + + // Render circles individually + for (const auto& circle : data.circles) { + GLuint tempVAO, tempVBO; + glGenVertexArrays(1, &tempVAO); + glGenBuffers(1, &tempVBO); + + glBindVertexArray(tempVAO); + glBindBuffer(GL_ARRAY_BUFFER, tempVBO); + + const int segments = circle.num_segments; + std::vector vertices; + vertices.reserve((segments + 2) * 3); + + if (circle.filled) { + vertices.insert(vertices.end(), {circle.center.x, circle.center.y, circle.center.z}); + } + + for (int i = 0; i <= segments; i++) { + float angle = 2.0f * M_PI * i / segments; + float x = circle.center.x + circle.radius * std::cos(angle); + float y = circle.center.y + circle.radius * std::sin(angle); + float z = circle.center.z; + vertices.insert(vertices.end(), {x, y, z}); + } + + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + + glVertexAttrib4f(1, 0.0f, 0.0f, 0.0f, 0.0f); + glVertexAttrib1f(2, 1.0f); + + primitive_shader_.TrySetUniform("uColor", circle.color); + + if (circle.filled) { + primitive_shader_.TrySetUniform("renderMode", 2); + primitive_shader_.TrySetUniform("lineType", 0); + glDrawArrays(GL_TRIANGLE_FAN, 0, vertices.size() / 3); + } else { + primitive_shader_.TrySetUniform("renderMode", 3); + primitive_shader_.TrySetUniform("lineType", static_cast(circle.line_type)); + glLineWidth(circle.thickness); + glDrawArrays(GL_LINE_LOOP, circle.filled ? 1 : 0, segments + 1); + glLineWidth(1.0f); + } + + glDisableVertexAttribArray(0); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteVertexArrays(1, &tempVAO); + glDeleteBuffers(1, &tempVBO); + + render_stats_.shapes_rendered++; + render_stats_.draw_calls++; + } + + // Render rectangles individually + for (const auto& rect : data.rectangles) { + GLuint tempVAO, tempVBO; + glGenVertexArrays(1, &tempVAO); + glGenBuffers(1, &tempVBO); + + glBindVertexArray(tempVAO); + glBindBuffer(GL_ARRAY_BUFFER, tempVBO); + + std::vector vertices = { + rect.position.x, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y + rect.height, rect.position.z, + rect.position.x, rect.position.y + rect.height, rect.position.z + }; + + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + + glVertexAttrib4f(1, 0.0f, 0.0f, 0.0f, 0.0f); + glVertexAttrib1f(2, 1.0f); + + primitive_shader_.TrySetUniform("uColor", rect.color); + + if (rect.filled) { + primitive_shader_.TrySetUniform("renderMode", 2); + primitive_shader_.TrySetUniform("lineType", 0); + glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + } else { + primitive_shader_.TrySetUniform("renderMode", 3); + primitive_shader_.TrySetUniform("lineType", static_cast(rect.line_type)); + glLineWidth(rect.thickness); + glDrawArrays(GL_LINE_LOOP, 0, 4); + glLineWidth(1.0f); + } + + glDisableVertexAttribArray(0); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteVertexArrays(1, &tempVAO); + glDeleteBuffers(1, &tempVBO); + + render_stats_.shapes_rendered++; + render_stats_.draw_calls++; + } // Clean up OpenGL state glDisable(GL_DEPTH_TEST); @@ -1647,83 +1538,91 @@ void Canvas::RenderIndividualShapes(const CanvasData& data, const glm::mat4& pro } void Canvas::InitializeBatches() { - // Initialize line batch - glGenVertexArrays(1, &line_batch_.vao); - glGenBuffers(1, &line_batch_.position_vbo); - glGenBuffers(1, &line_batch_.color_vbo); - - glBindVertexArray(line_batch_.vao); - - // Position buffer - glBindBuffer(GL_ARRAY_BUFFER, line_batch_.position_vbo); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(0); - - // Color buffer - glBindBuffer(GL_ARRAY_BUFFER, line_batch_.color_vbo); - glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(1); - - glBindVertexArray(0); - + // Initialize line batches for each line type + for (auto line_type : {LineType::kSolid, LineType::kDashed, LineType::kDotted}) { + auto& line_batch = line_batches_[line_type]; + glGenVertexArrays(1, &line_batch.vao); + glGenBuffers(1, &line_batch.position_vbo); + glGenBuffers(1, &line_batch.color_vbo); + + glBindVertexArray(line_batch.vao); + + // Position buffer + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, line_batch.position_vbo); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); + + // Color buffer + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, line_batch.color_vbo); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0); + + glBindVertexArray(0); + } + // Initialize filled shape batch glGenVertexArrays(1, &filled_shape_batch_.vao); glGenBuffers(1, &filled_shape_batch_.vertex_vbo); glGenBuffers(1, &filled_shape_batch_.color_vbo); glGenBuffers(1, &filled_shape_batch_.ebo); - + glBindVertexArray(filled_shape_batch_.vao); - + // Vertex buffer + glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, filled_shape_batch_.vertex_vbo); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(0); - + // Color buffer + glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, filled_shape_batch_.color_vbo); glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(1); - + // Element buffer glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, filled_shape_batch_.ebo); - - glBindVertexArray(0); - - // Initialize outline shape batch - glGenVertexArrays(1, &outline_shape_batch_.vao); - glGenBuffers(1, &outline_shape_batch_.vertex_vbo); - glGenBuffers(1, &outline_shape_batch_.color_vbo); - glGenBuffers(1, &outline_shape_batch_.ebo); - - glBindVertexArray(outline_shape_batch_.vao); - - // Vertex buffer - glBindBuffer(GL_ARRAY_BUFFER, outline_shape_batch_.vertex_vbo); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(0); - - // Color buffer - glBindBuffer(GL_ARRAY_BUFFER, outline_shape_batch_.color_vbo); - glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(1); - - // Element buffer - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, outline_shape_batch_.ebo); - + glBindVertexArray(0); - + + // Initialize outline shape batches for each line type + for (auto line_type : {LineType::kSolid, LineType::kDashed, LineType::kDotted}) { + auto& outline_batch = outline_shape_batches_[line_type]; + glGenVertexArrays(1, &outline_batch.vao); + glGenBuffers(1, &outline_batch.vertex_vbo); + glGenBuffers(1, &outline_batch.color_vbo); + glGenBuffers(1, &outline_batch.ebo); + + glBindVertexArray(outline_batch.vao); + + // Vertex buffer + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, outline_batch.vertex_vbo); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); + + // Color buffer + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, outline_batch.color_vbo); + glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, 0); + + // Element buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, outline_batch.ebo); + + glBindVertexArray(0); + } + std::cout << "Batching system initialized successfully." << std::endl; } void Canvas::ClearBatches() { - // Clean up line batch - if (line_batch_.vao != 0) { - glDeleteVertexArrays(1, &line_batch_.vao); - glDeleteBuffers(1, &line_batch_.position_vbo); - glDeleteBuffers(1, &line_batch_.color_vbo); - line_batch_.vao = 0; - line_batch_.position_vbo = 0; - line_batch_.color_vbo = 0; + // Clean up line batches + for (auto& [line_type, line_batch] : line_batches_) { + if (line_batch.vao != 0) { + glDeleteVertexArrays(1, &line_batch.vao); + glDeleteBuffers(1, &line_batch.position_vbo); + glDeleteBuffers(1, &line_batch.color_vbo); + line_batch.vao = 0; + line_batch.position_vbo = 0; + line_batch.color_vbo = 0; + } } // Clean up filled shape batch @@ -1738,60 +1637,72 @@ void Canvas::ClearBatches() { filled_shape_batch_.ebo = 0; } - // Clean up outline shape batch - if (outline_shape_batch_.vao != 0) { - glDeleteVertexArrays(1, &outline_shape_batch_.vao); - glDeleteBuffers(1, &outline_shape_batch_.vertex_vbo); - glDeleteBuffers(1, &outline_shape_batch_.color_vbo); - glDeleteBuffers(1, &outline_shape_batch_.ebo); - outline_shape_batch_.vao = 0; - outline_shape_batch_.vertex_vbo = 0; - outline_shape_batch_.color_vbo = 0; - outline_shape_batch_.ebo = 0; + // Clean up outline shape batches + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + if (outline_batch.vao != 0) { + glDeleteVertexArrays(1, &outline_batch.vao); + glDeleteBuffers(1, &outline_batch.vertex_vbo); + glDeleteBuffers(1, &outline_batch.color_vbo); + glDeleteBuffers(1, &outline_batch.ebo); + outline_batch.vao = 0; + outline_batch.vertex_vbo = 0; + outline_batch.color_vbo = 0; + outline_batch.ebo = 0; + } } // Clear CPU data - line_batch_.vertices.clear(); - line_batch_.colors.clear(); - line_batch_.thicknesses.clear(); - line_batch_.line_types.clear(); + for (auto& [line_type, line_batch] : line_batches_) { + line_batch.vertices.clear(); + line_batch.colors.clear(); + line_batch.thicknesses.clear(); + line_batch.line_types.clear(); + } filled_shape_batch_.vertices.clear(); filled_shape_batch_.indices.clear(); filled_shape_batch_.colors.clear(); - outline_shape_batch_.vertices.clear(); - outline_shape_batch_.indices.clear(); - outline_shape_batch_.colors.clear(); + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_batch.vertices.clear(); + outline_batch.indices.clear(); + outline_batch.colors.clear(); + } } void Canvas::FlushBatches() { // Force update of all batches - line_batch_.needs_update = true; + for (auto& [line_type, line_batch] : line_batches_) { + line_batch.needs_update = true; + } filled_shape_batch_.needs_update = true; - outline_shape_batch_.needs_update = true; + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_batch.needs_update = true; + } UpdateBatches(); } void Canvas::UpdateBatches() { - // Update line batch - if (line_batch_.needs_update && !line_batch_.vertices.empty()) { - glBindVertexArray(line_batch_.vao); - - // Update position buffer - glBindBuffer(GL_ARRAY_BUFFER, line_batch_.position_vbo); - glBufferData(GL_ARRAY_BUFFER, - line_batch_.vertices.size() * sizeof(glm::vec3), - line_batch_.vertices.data(), GL_DYNAMIC_DRAW); - - // Update color buffer - glBindBuffer(GL_ARRAY_BUFFER, line_batch_.color_vbo); - glBufferData(GL_ARRAY_BUFFER, - line_batch_.colors.size() * sizeof(glm::vec4), - line_batch_.colors.data(), GL_DYNAMIC_DRAW); - - glBindVertexArray(0); - line_batch_.needs_update = false; + // Update line batches + for (auto& [line_type, line_batch] : line_batches_) { + if (line_batch.needs_update && !line_batch.vertices.empty()) { + glBindVertexArray(line_batch.vao); + + // Update position buffer + glBindBuffer(GL_ARRAY_BUFFER, line_batch.position_vbo); + glBufferData(GL_ARRAY_BUFFER, + line_batch.vertices.size() * sizeof(glm::vec3), + line_batch.vertices.data(), GL_DYNAMIC_DRAW); + + // Update color buffer + glBindBuffer(GL_ARRAY_BUFFER, line_batch.color_vbo); + glBufferData(GL_ARRAY_BUFFER, + line_batch.colors.size() * sizeof(glm::vec4), + line_batch.colors.data(), GL_DYNAMIC_DRAW); + + glBindVertexArray(0); + line_batch.needs_update = false; + } } // Update filled shape batch @@ -1804,18 +1715,19 @@ void Canvas::UpdateBatches() { filled_shape_batch_.vertices.size() * sizeof(float), filled_shape_batch_.vertices.data(), GL_DYNAMIC_DRAW); - // Update color buffer (expand to match vertices) - std::vector expanded_colors; - expanded_colors.reserve(filled_shape_batch_.vertices.size() / 3); - for (size_t i = 0; i < filled_shape_batch_.colors.size(); ++i) { - // Each color applies to one vertex (vertices.size() / 3 vertices total) - expanded_colors.push_back(filled_shape_batch_.colors[i]); + // Update color buffer - colors should already match vertices 1:1 + // Each color in filled_shape_batch_.colors corresponds to one vertex + size_t expected_vertex_count = filled_shape_batch_.vertices.size() / 3; + if (filled_shape_batch_.colors.size() != expected_vertex_count) { + std::cerr << "ERROR: Color count mismatch in filled_shape_batch! " + << "Expected " << expected_vertex_count << " colors, got " + << filled_shape_batch_.colors.size() << std::endl; } glBindBuffer(GL_ARRAY_BUFFER, filled_shape_batch_.color_vbo); glBufferData(GL_ARRAY_BUFFER, - expanded_colors.size() * sizeof(glm::vec4), - expanded_colors.data(), GL_DYNAMIC_DRAW); + filled_shape_batch_.colors.size() * sizeof(glm::vec4), + filled_shape_batch_.colors.data(), GL_DYNAMIC_DRAW); // Update index buffer glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, filled_shape_batch_.ebo); @@ -1827,36 +1739,40 @@ void Canvas::UpdateBatches() { filled_shape_batch_.needs_update = false; } - // Update outline shape batch - if (outline_shape_batch_.needs_update && !outline_shape_batch_.vertices.empty()) { - glBindVertexArray(outline_shape_batch_.vao); - - // Update vertex buffer - glBindBuffer(GL_ARRAY_BUFFER, outline_shape_batch_.vertex_vbo); - glBufferData(GL_ARRAY_BUFFER, - outline_shape_batch_.vertices.size() * sizeof(float), - outline_shape_batch_.vertices.data(), GL_DYNAMIC_DRAW); - - // Update color buffer - std::vector expanded_colors; - expanded_colors.reserve(outline_shape_batch_.vertices.size() / 3); - for (size_t i = 0; i < outline_shape_batch_.colors.size(); ++i) { - expanded_colors.push_back(outline_shape_batch_.colors[i]); + // Update outline shape batches + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + if (outline_batch.needs_update && !outline_batch.vertices.empty()) { + glBindVertexArray(outline_batch.vao); + + // Update vertex buffer + glBindBuffer(GL_ARRAY_BUFFER, outline_batch.vertex_vbo); + glBufferData(GL_ARRAY_BUFFER, + outline_batch.vertices.size() * sizeof(float), + outline_batch.vertices.data(), GL_DYNAMIC_DRAW); + + // Update color buffer - colors should already match vertices 1:1 + // Each color in outline_batch.colors corresponds to one vertex + size_t expected_vertex_count = outline_batch.vertices.size() / 3; + if (outline_batch.colors.size() != expected_vertex_count) { + std::cerr << "ERROR: Color count mismatch in outline_batch! " + << "Expected " << expected_vertex_count << " colors, got " + << outline_batch.colors.size() << std::endl; + } + + glBindBuffer(GL_ARRAY_BUFFER, outline_batch.color_vbo); + glBufferData(GL_ARRAY_BUFFER, + outline_batch.colors.size() * sizeof(glm::vec4), + outline_batch.colors.data(), GL_DYNAMIC_DRAW); + + // Update index buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, outline_batch.ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + outline_batch.indices.size() * sizeof(uint32_t), + outline_batch.indices.data(), GL_DYNAMIC_DRAW); + + glBindVertexArray(0); + outline_batch.needs_update = false; } - - glBindBuffer(GL_ARRAY_BUFFER, outline_shape_batch_.color_vbo); - glBufferData(GL_ARRAY_BUFFER, - expanded_colors.size() * sizeof(glm::vec4), - expanded_colors.data(), GL_DYNAMIC_DRAW); - - // Update index buffer - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, outline_shape_batch_.ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, - outline_shape_batch_.indices.size() * sizeof(uint32_t), - outline_shape_batch_.indices.data(), GL_DYNAMIC_DRAW); - - glBindVertexArray(0); - outline_shape_batch_.needs_update = false; } } @@ -1930,22 +1846,76 @@ void Canvas::GenerateRectangleVertices(float x, float y, float width, float heig } } +void Canvas::GenerateEllipseVertices(const PendingUpdate::ellipse_params& ellipse, + std::vector& vertices, std::vector& indices, + bool filled, uint32_t base_index) { + const int segments = 32; + if (filled) { + vertices.insert(vertices.end(), {ellipse.x, ellipse.y, 0.0f}); + for (int i = 0; i <= segments; i++) { + float angle = ellipse.start_angle + (ellipse.end_angle - ellipse.start_angle) * i / segments; + float x_local = ellipse.rx * std::cos(angle); + float y_local = ellipse.ry * std::sin(angle); + float x_rotated = x_local * std::cos(ellipse.angle) - y_local * std::sin(ellipse.angle); + float y_rotated = x_local * std::sin(ellipse.angle) + y_local * std::cos(ellipse.angle); + vertices.insert(vertices.end(), {ellipse.x + x_rotated, ellipse.y + y_rotated, 0.0f}); + if (i < segments) { + indices.insert(indices.end(), {base_index, base_index + 1 + i, base_index + 1 + ((i + 1) % segments)}); + } + } + } else { + for (int i = 0; i <= segments; i++) { + float angle = ellipse.start_angle + (ellipse.end_angle - ellipse.start_angle) * i / segments; + float x_local = ellipse.rx * std::cos(angle); + float y_local = ellipse.ry * std::sin(angle); + float x_rotated = x_local * std::cos(ellipse.angle) - y_local * std::sin(ellipse.angle); + float y_rotated = x_local * std::sin(ellipse.angle) + y_local * std::cos(ellipse.angle); + vertices.insert(vertices.end(), {ellipse.x + x_rotated, ellipse.y + y_rotated, 0.0f}); + if (i < segments) { + indices.insert(indices.end(), {base_index + i, base_index + (i + 1)}); + } + } + } +} + +void Canvas::GeneratePolygonVertices(const std::vector& points, + std::vector& vertices, std::vector& indices, + bool filled, uint32_t base_index) { + for (const auto& point : points) { + vertices.insert(vertices.end(), {point.x, point.y, 0.0f}); + } + if (filled) { + // Simple fan triangulation, works for convex polygons + for (size_t i = 1; i < points.size() - 1; ++i) { + indices.insert(indices.end(), {static_cast(base_index), static_cast(base_index + i), static_cast(base_index + i + 1)}); + } + } else { + for (size_t i = 0; i < points.size(); ++i) { + indices.insert(indices.end(), {static_cast(base_index + i), static_cast(base_index + (i + 1) % points.size())}); + } + } +} + void Canvas::OptimizeMemory() { std::lock_guard lock(data_mutex_); if (perf_config_.aggressive_memory_cleanup) { // Shrink batch vectors to fit current usage - line_batch_.vertices.shrink_to_fit(); - line_batch_.colors.shrink_to_fit(); - line_batch_.thicknesses.shrink_to_fit(); + for (auto& [line_type, line_batch] : line_batches_) { + line_batch.vertices.shrink_to_fit(); + line_batch.colors.shrink_to_fit(); + line_batch.thicknesses.shrink_to_fit(); + } filled_shape_batch_.vertices.shrink_to_fit(); filled_shape_batch_.indices.shrink_to_fit(); filled_shape_batch_.colors.shrink_to_fit(); - outline_shape_batch_.vertices.shrink_to_fit(); - outline_shape_batch_.indices.shrink_to_fit(); - outline_shape_batch_.colors.shrink_to_fit(); + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_batch.vertices.shrink_to_fit(); + outline_batch.indices.shrink_to_fit(); + outline_batch.colors.shrink_to_fit(); + } // Clear pending updates queue if it's grown too large if (pending_updates_.size() == 0) { @@ -1967,17 +1937,21 @@ void Canvas::PreallocateMemory(size_t estimated_objects) { // Pre-allocate batch vectors based on estimated usage size_t reserve_size = std::min(estimated_objects, perf_config_.max_batch_size); - line_batch_.vertices.reserve(reserve_size * 2); // 2 vertices per line - line_batch_.colors.reserve(reserve_size * 2); - line_batch_.thicknesses.reserve(reserve_size); + for (auto& [line_type, line_batch] : line_batches_) { + line_batch.vertices.reserve(reserve_size * 2); // 2 vertices per line + line_batch.colors.reserve(reserve_size * 2); + line_batch.thicknesses.reserve(reserve_size); + } filled_shape_batch_.vertices.reserve(reserve_size * 12); // ~4 vertices per shape * 3 components filled_shape_batch_.indices.reserve(reserve_size * 6); // ~2 triangles per shape filled_shape_batch_.colors.reserve(reserve_size * 4); // 4 vertices per shape - outline_shape_batch_.vertices.reserve(reserve_size * 12); - outline_shape_batch_.indices.reserve(reserve_size * 8); // ~4 edges per shape - outline_shape_batch_.colors.reserve(reserve_size * 4); + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_batch.vertices.reserve(reserve_size * 12); + outline_batch.indices.reserve(reserve_size * 8); // ~4 edges per shape + outline_batch.colors.reserve(reserve_size * 4); + } // Record the pre-allocation if (perf_config_.memory_tracking_enabled) { @@ -1990,17 +1964,21 @@ void Canvas::ShrinkToFit() { std::lock_guard lock(data_mutex_); // Shrink all batch vectors to minimum required size - line_batch_.vertices.shrink_to_fit(); - line_batch_.colors.shrink_to_fit(); - line_batch_.thicknesses.shrink_to_fit(); + for (auto& [line_type, line_batch] : line_batches_) { + line_batch.vertices.shrink_to_fit(); + line_batch.colors.shrink_to_fit(); + line_batch.thicknesses.shrink_to_fit(); + } filled_shape_batch_.vertices.shrink_to_fit(); filled_shape_batch_.indices.shrink_to_fit(); filled_shape_batch_.colors.shrink_to_fit(); - outline_shape_batch_.vertices.shrink_to_fit(); - outline_shape_batch_.indices.shrink_to_fit(); - outline_shape_batch_.colors.shrink_to_fit(); + for (auto& [line_type, outline_batch] : outline_shape_batches_) { + outline_batch.vertices.shrink_to_fit(); + outline_batch.indices.shrink_to_fit(); + outline_batch.colors.shrink_to_fit(); + } // Clear and shrink pending updates queue std::queue empty_queue; @@ -2011,17 +1989,21 @@ size_t Canvas::GetMemoryUsage() const { size_t total_usage = 0; // Calculate batch memory usage - total_usage += line_batch_.vertices.capacity() * sizeof(glm::vec3); - total_usage += line_batch_.colors.capacity() * sizeof(glm::vec4); - total_usage += line_batch_.thicknesses.capacity() * sizeof(float); + for (const auto& [line_type, line_batch] : line_batches_) { + total_usage += line_batch.vertices.capacity() * sizeof(glm::vec3); + total_usage += line_batch.colors.capacity() * sizeof(glm::vec4); + total_usage += line_batch.thicknesses.capacity() * sizeof(float); + } total_usage += filled_shape_batch_.vertices.capacity() * sizeof(float); total_usage += filled_shape_batch_.indices.capacity() * sizeof(uint32_t); total_usage += filled_shape_batch_.colors.capacity() * sizeof(glm::vec4); - total_usage += outline_shape_batch_.vertices.capacity() * sizeof(float); - total_usage += outline_shape_batch_.indices.capacity() * sizeof(uint32_t); - total_usage += outline_shape_batch_.colors.capacity() * sizeof(glm::vec4); + for (const auto& [line_type, outline_batch] : outline_shape_batches_) { + total_usage += outline_batch.vertices.capacity() * sizeof(float); + total_usage += outline_batch.indices.capacity() * sizeof(uint32_t); + total_usage += outline_batch.colors.capacity() * sizeof(glm::vec4); + } // Add estimated size of pending updates queue total_usage += pending_updates_.size() * sizeof(PendingUpdate); @@ -2035,4 +2017,42 @@ size_t Canvas::GetMemoryUsage() const { return total_usage; } +void Canvas::SetBatchingEnabled(bool enabled) { + batching_enabled_ = enabled; + // Update current render strategy based on batching preference + current_render_strategy_ = enabled ? + static_cast(batched_strategy_.get()) : + static_cast(individual_strategy_.get()); +} + +RenderStrategy* Canvas::SelectRenderStrategy(const CanvasData& data) { + // Select render strategy based on batching setting and data characteristics + if (batching_enabled_) { + return batched_strategy_.get(); + } else { + return individual_strategy_.get(); + } + + // Future enhancement: Could select strategy based on data characteristics + // For example, use individual strategy for scenes with many complex polygons + // and batched strategy for scenes with many simple shapes +} + +// Performance monitoring methods (moved from inline in header) +const RenderStats& Canvas::GetRenderStats() const { + return render_stats_; +} + +void Canvas::ResetRenderStats() { + render_stats_.Reset(); +} + +void Canvas::SetPerformanceConfig(const PerformanceConfig& config) { + perf_config_ = config; +} + +const PerformanceConfig& Canvas::GetPerformanceConfig() const { + return perf_config_; +} + } // namespace quickviz diff --git a/src/renderer/src/renderable/details/batched_render_strategy.cpp b/src/renderer/src/renderable/details/batched_render_strategy.cpp new file mode 100644 index 0000000..a9aae5e --- /dev/null +++ b/src/renderer/src/renderable/details/batched_render_strategy.cpp @@ -0,0 +1,163 @@ +/** + * @file batched_render_strategy.cpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Implementation of batched rendering strategy + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#include "batched_render_strategy.hpp" +#include "shape_generators.hpp" + +#include +#include "glad/glad.h" +#include "renderer/shader_program.hpp" +#include "renderer/renderable/canvas.hpp" +#include "canvas_data.hpp" + +namespace quickviz { + +BatchedRenderStrategy::BatchedRenderStrategy(std::unordered_map& line_batches, + ShapeBatch& filled_batch, + std::unordered_map& outline_batches, + ShapeRenderer* shape_renderer) + : line_batches_(line_batches), filled_shape_batch_(filled_batch), outline_shape_batches_(outline_batches), shape_renderer_(shape_renderer) { +} + +bool BatchedRenderStrategy::CanHandle(const CanvasData& data) const { + // Batched strategy can handle any data, but works best with many similar shapes + return true; +} + +void BatchedRenderStrategy::Render(const CanvasData& data, const RenderContext& context) { + // Skip if there's no data to render + if (data.points.empty() && data.lines.empty() && + data.rectangles.empty() && data.circles.empty() && + data.ellipses.empty() && data.polygons.empty()) { + return; + } + + // Use efficient batched rendering for better performance + RenderBatches(context); + + // Still render points individually (they're already efficient) + RenderPoints(data, context); + + // Handle non-batched shapes (ellipses, polygons) with individual rendering + if (!data.ellipses.empty() || !data.polygons.empty()) { + RenderIndividualShapes(data, context); + } +} + +void BatchedRenderStrategy::RenderBatches(const RenderContext& context) { + // This method will contain the batched rendering logic + // For now, this is a placeholder - the actual batching logic + // will be moved from the original Canvas::RenderBatches method + + context.primitive_shader->Use(); + context.primitive_shader->TrySetUniform("projection", context.projection); + context.primitive_shader->TrySetUniform("view", context.view); + context.primitive_shader->TrySetUniform("model", glm::mat4(1.0f)); + context.primitive_shader->TrySetUniform("coordSystemTransform", context.coord_transform); + + // Enable OpenGL states + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // TODO: Implement actual batch rendering logic + // This will be moved from Canvas::RenderBatches in the next step +} + +void BatchedRenderStrategy::RenderPoints(const CanvasData& data, const RenderContext& context) { + if (data.points.empty()) return; + + // Setup for point rendering + context.primitive_shader->Use(); + context.primitive_shader->TrySetUniform("projection", context.projection); + context.primitive_shader->TrySetUniform("view", context.view); + context.primitive_shader->TrySetUniform("model", glm::mat4(1.0f)); + context.primitive_shader->TrySetUniform("coordSystemTransform", context.coord_transform); + context.primitive_shader->TrySetUniform("renderMode", 0); // Points mode + + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glBindVertexArray(context.primitive_vao); + glEnable(GL_PROGRAM_POINT_SIZE); + + // Update buffer with point data + glBindBuffer(GL_ARRAY_BUFFER, context.primitive_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(Point) * data.points.size(), + data.points.data(), GL_DYNAMIC_DRAW); + + // Enable point attributes + glEnableVertexAttribArray(0); // Position + glEnableVertexAttribArray(1); // Color + glEnableVertexAttribArray(2); // Size + + // Draw points + glDrawArrays(GL_POINTS, 0, data.points.size()); + + // Cleanup point rendering + glDisable(GL_PROGRAM_POINT_SIZE); + glDisableVertexAttribArray(0); + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + // Update stats (cast void* back to proper type) + if (context.render_stats) { + // Note: This casting is safe because we control what gets passed in + // Will be improved in Phase 2 when we extract proper interfaces + // auto* stats = static_cast(context.render_stats); + // stats->points_rendered = data.points.size(); + // stats->draw_calls++; + } +} + +void BatchedRenderStrategy::RenderIndividualShapes(const CanvasData& data, const RenderContext& context) { + if (!shape_renderer_) { + // Fallback to original individual rendering if no unified renderer available + return; + } + + if (data.ellipses.empty() && data.polygons.empty()) { + return; + } + + // Render ellipses using unified renderer + for (const auto& ellipse : data.ellipses) { + auto vertices = ShapeGenerators::GenerateEllipseVertices(ellipse); + VertexLayout layout = VertexLayout::PositionOnly(); + + RenderParams params; + params.color = ellipse.color; + params.thickness = ellipse.thickness; + params.filled = ellipse.filled; + params.primitive_type = ellipse.filled ? GL_TRIANGLE_FAN : GL_LINE_LOOP; + params.vertex_count = vertices.size() / 3; + + shape_renderer_->RenderShape(vertices, layout, params); + } + + // Render polygons using unified renderer + for (const auto& polygon : data.polygons) { + auto vertices = ShapeGenerators::GeneratePolygonVertices(polygon); + VertexLayout layout = VertexLayout::PositionOnly(); + + RenderParams params; + params.color = polygon.color; + params.thickness = polygon.thickness; + params.filled = polygon.filled; + params.primitive_type = polygon.filled ? GL_TRIANGLE_FAN : GL_LINE_LOOP; + params.vertex_count = vertices.size() / 3; + + shape_renderer_->RenderShape(vertices, layout, params); + } +} + +} // namespace quickviz \ No newline at end of file diff --git a/src/renderer/src/renderable/details/batched_render_strategy.hpp b/src/renderer/src/renderable/details/batched_render_strategy.hpp new file mode 100644 index 0000000..44198c2 --- /dev/null +++ b/src/renderer/src/renderable/details/batched_render_strategy.hpp @@ -0,0 +1,64 @@ +/** + * @file batched_render_strategy.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Batched rendering strategy for Canvas + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_BATCHED_RENDER_STRATEGY_HPP +#define OPENGL_RENDERER_BATCHED_RENDER_STRATEGY_HPP + +#include +#include "render_strategy.hpp" +#include "shape_renderer.hpp" +#include "renderer/renderable/types.hpp" + +namespace quickviz { + +// Forward declarations +struct LineBatch; +struct ShapeBatch; + +/** + * @brief Render strategy that uses batching for improved performance + * + * This strategy batches similar primitives together to reduce draw calls + * and state changes, providing better performance for scenes with many objects. + */ +class BatchedRenderStrategy : public RenderStrategy { +public: + /** + * @brief Constructor + * @param line_batch Reference to line batch data + * @param filled_batch Reference to filled shape batch data + * @param outline_batch Reference to outline shape batch data + * @param shape_renderer Unified shape renderer for individual shapes + */ + BatchedRenderStrategy(std::unordered_map& line_batches, ShapeBatch& filled_batch, std::unordered_map& outline_batches, ShapeRenderer* shape_renderer = nullptr); + + ~BatchedRenderStrategy() override = default; + + // RenderStrategy interface + void Render(const CanvasData& data, const RenderContext& context) override; + bool CanHandle(const CanvasData& data) const override; + +private: + // Batch rendering methods + void RenderBatches(const RenderContext& context); + void RenderPoints(const CanvasData& data, const RenderContext& context); + void RenderIndividualShapes(const CanvasData& data, const RenderContext& context); + + // References to batch data (owned by Canvas) + std::unordered_map& line_batches_; + ShapeBatch& filled_shape_batch_; + std::unordered_map& outline_shape_batches_; + + // Unified shape renderer for individual shapes + ShapeRenderer* shape_renderer_; +}; + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_BATCHED_RENDER_STRATEGY_HPP */ \ No newline at end of file diff --git a/src/renderer/src/renderable/details/individual_render_strategy.cpp b/src/renderer/src/renderable/details/individual_render_strategy.cpp new file mode 100644 index 0000000..14a5765 --- /dev/null +++ b/src/renderer/src/renderable/details/individual_render_strategy.cpp @@ -0,0 +1,135 @@ +/** + * @file individual_render_strategy.cpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Implementation of individual shape rendering strategy + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#include "individual_render_strategy.hpp" +#include "shape_generators.hpp" + +#include +#include "glad/glad.h" +#include "renderer/shader_program.hpp" +#include "canvas_data.hpp" + +namespace quickviz { + +IndividualRenderStrategy::IndividualRenderStrategy(ShapeRenderer* shape_renderer) + : shape_renderer_(shape_renderer) { +} + +bool IndividualRenderStrategy::CanHandle(const CanvasData& data) const { + // Individual strategy can handle any data + return true; +} + +void IndividualRenderStrategy::Render(const CanvasData& data, const RenderContext& context) { + // Skip if there's no data to render + if (data.points.empty() && data.lines.empty() && + data.rectangles.empty() && data.circles.empty() && + data.ellipses.empty() && data.polygons.empty()) { + return; + } + + SetupCommonRenderState(context); + + // Render each type of shape individually + RenderPoints(data, context); + RenderLines(data, context); + RenderRectangles(data, context); + RenderCircles(data, context); + RenderEllipses(data, context); + RenderPolygons(data, context); + + CleanupRenderState(); +} + +void IndividualRenderStrategy::SetupCommonRenderState(const RenderContext& context) { + // Setup common rendering state for primitives + context.primitive_shader->Use(); + context.primitive_shader->TrySetUniform("projection", context.projection); + context.primitive_shader->TrySetUniform("view", context.view); + context.primitive_shader->TrySetUniform("model", glm::mat4(1.0f)); + context.primitive_shader->TrySetUniform("coordSystemTransform", context.coord_transform); + context.primitive_shader->TrySetUniform("lineType", 0); // Default to solid line + context.primitive_shader->TrySetUniform("thickness", 1.0f); // Default thickness + context.primitive_shader->TrySetUniform("uColor", glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); // Default to white + + // Enable depth test and blending + glEnable(GL_DEPTH_TEST); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + +void IndividualRenderStrategy::CleanupRenderState() { + // Clean up OpenGL state + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + glBindVertexArray(0); + glUseProgram(0); +} + +void IndividualRenderStrategy::RenderPoints(const CanvasData& data, const RenderContext& context) { + // TODO: Move point rendering logic from original Canvas implementation + // For now this is a placeholder +} + +void IndividualRenderStrategy::RenderLines(const CanvasData& data, const RenderContext& context) { + // TODO: Move line rendering logic from original Canvas implementation + // For now this is a placeholder +} + +void IndividualRenderStrategy::RenderRectangles(const CanvasData& data, const RenderContext& context) { + // TODO: Move rectangle rendering logic from original Canvas implementation + // For now this is a placeholder +} + +void IndividualRenderStrategy::RenderCircles(const CanvasData& data, const RenderContext& context) { + // TODO: Move circle rendering logic from original Canvas implementation + // For now this is a placeholder +} + +void IndividualRenderStrategy::RenderEllipses(const CanvasData& data, const RenderContext& context) { + if (!shape_renderer_ || data.ellipses.empty()) { + return; + } + + for (const auto& ellipse : data.ellipses) { + auto vertices = ShapeGenerators::GenerateEllipseVertices(ellipse); + VertexLayout layout = VertexLayout::PositionOnly(); + + RenderParams params; + params.color = ellipse.color; + params.thickness = ellipse.thickness; + params.filled = ellipse.filled; + params.primitive_type = ellipse.filled ? GL_TRIANGLE_FAN : GL_LINE_LOOP; + params.vertex_count = vertices.size() / 3; + + shape_renderer_->RenderShape(vertices, layout, params); + } +} + +void IndividualRenderStrategy::RenderPolygons(const CanvasData& data, const RenderContext& context) { + if (!shape_renderer_ || data.polygons.empty()) { + return; + } + + for (const auto& polygon : data.polygons) { + auto vertices = ShapeGenerators::GeneratePolygonVertices(polygon); + VertexLayout layout = VertexLayout::PositionOnly(); + + RenderParams params; + params.color = polygon.color; + params.thickness = polygon.thickness; + params.filled = polygon.filled; + params.primitive_type = polygon.filled ? GL_TRIANGLE_FAN : GL_LINE_LOOP; + params.vertex_count = vertices.size() / 3; + + shape_renderer_->RenderShape(vertices, layout, params); + } +} + +} // namespace quickviz \ No newline at end of file diff --git a/src/renderer/src/renderable/details/individual_render_strategy.hpp b/src/renderer/src/renderable/details/individual_render_strategy.hpp new file mode 100644 index 0000000..a857314 --- /dev/null +++ b/src/renderer/src/renderable/details/individual_render_strategy.hpp @@ -0,0 +1,53 @@ +/** + * @file individual_render_strategy.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Individual shape rendering strategy for Canvas + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_INDIVIDUAL_RENDER_STRATEGY_HPP +#define OPENGL_RENDERER_INDIVIDUAL_RENDER_STRATEGY_HPP + +#include "render_strategy.hpp" +#include "shape_renderer.hpp" + +namespace quickviz { + +/** + * @brief Render strategy that renders each shape individually + * + * This strategy renders shapes one by one without batching. It's used + * as a fallback when batching is disabled or for shapes that don't + * benefit from batching (e.g., complex polygons, ellipses). + */ +class IndividualRenderStrategy : public RenderStrategy { +public: + IndividualRenderStrategy(ShapeRenderer* shape_renderer = nullptr); + ~IndividualRenderStrategy() override = default; + + // RenderStrategy interface + void Render(const CanvasData& data, const RenderContext& context) override; + bool CanHandle(const CanvasData& data) const override; + +private: + // Individual shape rendering methods + void RenderPoints(const CanvasData& data, const RenderContext& context); + void RenderLines(const CanvasData& data, const RenderContext& context); + void RenderRectangles(const CanvasData& data, const RenderContext& context); + void RenderCircles(const CanvasData& data, const RenderContext& context); + void RenderEllipses(const CanvasData& data, const RenderContext& context); + void RenderPolygons(const CanvasData& data, const RenderContext& context); + + // Helper methods + void SetupCommonRenderState(const RenderContext& context); + void CleanupRenderState(); + + // Unified shape renderer for individual shapes + ShapeRenderer* shape_renderer_; +}; + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_INDIVIDUAL_RENDER_STRATEGY_HPP */ \ No newline at end of file diff --git a/src/renderer/src/renderable/details/render_strategy.hpp b/src/renderer/src/renderable/details/render_strategy.hpp new file mode 100644 index 0000000..f5dcf89 --- /dev/null +++ b/src/renderer/src/renderable/details/render_strategy.hpp @@ -0,0 +1,71 @@ +/** + * @file render_strategy.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Render strategy interface for Canvas implementation + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_RENDER_STRATEGY_HPP +#define OPENGL_RENDERER_RENDER_STRATEGY_HPP + +#include +#include "renderable/details/canvas_data.hpp" + +namespace quickviz { + +// Forward declarations +class ShaderProgram; + +/** + * @brief Context passed to render strategies containing shared rendering state + */ +struct RenderContext { + glm::mat4 projection; + glm::mat4 view; + glm::mat4 coord_transform; + + // OpenGL resources + ShaderProgram* primitive_shader; + uint32_t primitive_vao; + uint32_t primitive_vbo; + + // Performance tracking (using void* to avoid forward declaration issues) + void* render_stats; + const void* perf_config; + + // Constructor + RenderContext(const glm::mat4& proj, const glm::mat4& v, const glm::mat4& coord, + ShaderProgram* shader, uint32_t vao, uint32_t vbo, + void* stats, const void* config) + : projection(proj), view(v), coord_transform(coord), + primitive_shader(shader), primitive_vao(vao), primitive_vbo(vbo), + render_stats(stats), perf_config(config) {} +}; + +/** + * @brief Abstract base class for Canvas rendering strategies + */ +class RenderStrategy { +public: + virtual ~RenderStrategy() = default; + + /** + * @brief Render the canvas data using this strategy + * @param data Canvas data to render + * @param context Rendering context with matrices and OpenGL resources + */ + virtual void Render(const CanvasData& data, const RenderContext& context) = 0; + + /** + * @brief Check if this strategy can handle the given data efficiently + * @param data Canvas data to check + * @return true if this strategy is suitable for the data + */ + virtual bool CanHandle(const CanvasData& data) const = 0; +}; + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_RENDER_STRATEGY_HPP */ \ No newline at end of file diff --git a/src/renderer/src/renderable/details/shape_generators.cpp b/src/renderer/src/renderable/details/shape_generators.cpp new file mode 100644 index 0000000..bd1ae6c --- /dev/null +++ b/src/renderer/src/renderable/details/shape_generators.cpp @@ -0,0 +1,148 @@ +/** + * @file shape_generators.cpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Implementation of shape vertex generators + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#include "shape_generators.hpp" +#include + +namespace quickviz { +namespace ShapeGenerators { + +std::vector GenerateCircleVertices(const Circle& circle) { + return GeometryUtils::CreateCircle(circle.center, circle.radius, + circle.num_segments, circle.filled); +} + +std::vector GenerateEllipseVertices(const Ellipse& ellipse) { + return GeometryUtils::CreateEllipse(ellipse.center, ellipse.rx, ellipse.ry, + ellipse.angle, ellipse.start_angle, ellipse.end_angle, + ellipse.num_segments, ellipse.filled); +} + +std::vector GeneratePolygonVertices(const Polygon& polygon) { + std::vector vertices; + vertices.reserve(polygon.vertices.size() * 3); + + for (const auto& vertex : polygon.vertices) { + vertices.insert(vertices.end(), {vertex.x, vertex.y, vertex.z}); + } + + return vertices; +} + +std::vector GenerateRectangleVertices(const Rectangle& rect) { + std::vector vertices; + + if (rect.filled) { + // Two triangles for filled rectangle + vertices = { + // Triangle 1 + rect.position.x, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y + rect.height, rect.position.z, + + // Triangle 2 + rect.position.x, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y + rect.height, rect.position.z, + rect.position.x, rect.position.y + rect.height, rect.position.z + }; + } else { + // Line loop for outline rectangle + vertices = { + rect.position.x, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y, rect.position.z, + rect.position.x + rect.width, rect.position.y + rect.height, rect.position.z, + rect.position.x, rect.position.y + rect.height, rect.position.z + }; + } + + return vertices; +} + +std::vector GenerateLineVertices(const Line& line) { + return { + line.start.x, line.start.y, line.start.z, + line.end.x, line.end.y, line.end.z + }; +} + +std::vector GeneratePointVertices(const Point& point, float size) { + // Generate a small quad centered on the point for better visibility + float half_size = size * 0.5f; + + return { + // Triangle 1 + point.position.x - half_size, point.position.y - half_size, point.position.z, + point.position.x + half_size, point.position.y - half_size, point.position.z, + point.position.x + half_size, point.position.y + half_size, point.position.z, + + // Triangle 2 + point.position.x - half_size, point.position.y - half_size, point.position.z, + point.position.x + half_size, point.position.y + half_size, point.position.z, + point.position.x - half_size, point.position.y + half_size, point.position.z + }; +} + +namespace GeometryUtils { + +std::vector CreateCircle(const glm::vec3& center, float radius, + int segments, bool filled) { + std::vector vertices; + + if (filled) { + // Center vertex for triangle fan + vertices.insert(vertices.end(), {center.x, center.y, center.z}); + } + + // Generate circumference vertices + for (int i = 0; i <= segments; ++i) { + float angle = 2.0f * M_PI * i / segments; + float x = center.x + radius * std::cos(angle); + float y = center.y + radius * std::sin(angle); + vertices.insert(vertices.end(), {x, y, center.z}); + } + + return vertices; +} + +std::vector CreateEllipse(const glm::vec3& center, float rx, float ry, + float angle, float start_angle, float end_angle, + int segments, bool filled) { + std::vector vertices; + + if (filled) { + // Center vertex for triangle fan + vertices.insert(vertices.end(), {center.x, center.y, center.z}); + } + + // Generate ellipse vertices + for (int i = 0; i <= segments; ++i) { + float t = start_angle + (end_angle - start_angle) * i / segments; + + // Local ellipse coordinates + float x_local = rx * std::cos(t); + float y_local = ry * std::sin(t); + + // Apply rotation + float x_rotated = x_local * std::cos(angle) - y_local * std::sin(angle); + float y_rotated = x_local * std::sin(angle) + y_local * std::cos(angle); + + // Translate to center + float x = center.x + x_rotated; + float y = center.y + y_rotated; + + vertices.insert(vertices.end(), {x, y, center.z}); + } + + return vertices; +} + +} // namespace GeometryUtils + +} // namespace ShapeGenerators +} // namespace quickviz \ No newline at end of file diff --git a/src/renderer/src/renderable/details/shape_generators.hpp b/src/renderer/src/renderable/details/shape_generators.hpp new file mode 100644 index 0000000..2732eec --- /dev/null +++ b/src/renderer/src/renderable/details/shape_generators.hpp @@ -0,0 +1,106 @@ +/** + * @file shape_generators.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Shape vertex generators for unified renderer + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_SHAPE_GENERATORS_HPP +#define OPENGL_RENDERER_SHAPE_GENERATORS_HPP + +#include +#include +#include "canvas_data.hpp" + +namespace quickviz { + +/** + * @brief Collection of vertex generation functions for different shape types + * + * These functions generate vertex data that can be used with the unified + * ShapeRenderer, eliminating code duplication in shape-specific rendering. + */ +namespace ShapeGenerators { + +/** + * @brief Generate vertices for a circle + * @param circle Circle data + * @return Flat array of vertex positions + */ +std::vector GenerateCircleVertices(const Circle& circle); + +/** + * @brief Generate vertices for an ellipse + * @param ellipse Ellipse data + * @return Flat array of vertex positions + */ +std::vector GenerateEllipseVertices(const Ellipse& ellipse); + +/** + * @brief Generate vertices for a polygon + * @param polygon Polygon data + * @return Flat array of vertex positions + */ +std::vector GeneratePolygonVertices(const Polygon& polygon); + +/** + * @brief Generate vertices for a rectangle + * @param rect Rectangle data + * @return Flat array of vertex positions + */ +std::vector GenerateRectangleVertices(const Rectangle& rect); + +/** + * @brief Generate vertices for a line + * @param line Line data + * @return Flat array of vertex positions + */ +std::vector GenerateLineVertices(const Line& line); + +/** + * @brief Generate vertices for a point (as a small quad for visibility) + * @param point Point data + * @param size Point size for quad generation + * @return Flat array of vertex positions + */ +std::vector GeneratePointVertices(const Point& point, float size = 0.01f); + +/** + * @brief Helper functions for common geometry operations + */ +namespace GeometryUtils { + /** + * @brief Generate vertices for a circle with specified parameters + * @param center Circle center + * @param radius Circle radius + * @param segments Number of segments for tessellation + * @param filled Whether to include center vertex for filled circles + * @return Flat array of vertex positions + */ + std::vector CreateCircle(const glm::vec3& center, float radius, + int segments, bool filled); + + /** + * @brief Generate vertices for an ellipse with specified parameters + * @param center Ellipse center + * @param rx X-axis radius + * @param ry Y-axis radius + * @param angle Rotation angle in radians + * @param start_angle Start angle for partial ellipses + * @param end_angle End angle for partial ellipses + * @param segments Number of segments for tessellation + * @param filled Whether to include center vertex + * @return Flat array of vertex positions + */ + std::vector CreateEllipse(const glm::vec3& center, float rx, float ry, + float angle, float start_angle, float end_angle, + int segments, bool filled); +} + +} // namespace ShapeGenerators + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_SHAPE_GENERATORS_HPP */ \ No newline at end of file diff --git a/src/renderer/src/renderable/details/shape_renderer.cpp b/src/renderer/src/renderable/details/shape_renderer.cpp new file mode 100644 index 0000000..8a578aa --- /dev/null +++ b/src/renderer/src/renderable/details/shape_renderer.cpp @@ -0,0 +1,198 @@ +/** + * @file shape_renderer.cpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Implementation of unified shape renderer + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#include "shape_renderer.hpp" +#include "renderer/shader_program.hpp" +#include + +namespace quickviz { + +// TempGLResources implementation +TempGLResources::TempGLResources() { + glGenVertexArrays(1, &vao_); + glGenBuffers(1, &vbo_); +} + +TempGLResources::~TempGLResources() { + Cleanup(); +} + +TempGLResources::TempGLResources(TempGLResources&& other) noexcept + : vao_(other.vao_), vbo_(other.vbo_) { + other.vao_ = 0; + other.vbo_ = 0; +} + +TempGLResources& TempGLResources::operator=(TempGLResources&& other) noexcept { + if (this != &other) { + Cleanup(); + vao_ = other.vao_; + vbo_ = other.vbo_; + other.vao_ = 0; + other.vbo_ = 0; + } + return *this; +} + +void TempGLResources::Bind() { + glBindVertexArray(vao_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); +} + +void TempGLResources::UploadVertices(const std::vector& vertices) { + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), + vertices.data(), GL_STATIC_DRAW); +} + +void TempGLResources::SetupVertexAttributes() { + // Position attribute (location 0) + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + + // Make sure other attributes are disabled + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); +} + +void TempGLResources::SetupDefaultAttributes(const glm::vec4& color) { + // Set default values for disabled attributes + glVertexAttrib4f(1, color.r, color.g, color.b, color.a); // Color attribute + glVertexAttrib1f(2, 1.0f); // Size attribute +} + +void TempGLResources::Cleanup() { + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } +} + +// ShapeRenderer implementation +ShapeRenderer::ShapeRenderer(ShaderProgram* shader) : shader_(shader) { + if (!shader_) { + throw std::invalid_argument("ShapeRenderer requires valid shader program"); + } +} + +void ShapeRenderer::RenderShape(const std::vector& vertices, + const VertexLayout& layout, + const RenderParams& params) { + if (vertices.empty()) return; + + // Create temporary OpenGL resources + TempGLResources resources; + resources.Bind(); + resources.UploadVertices(vertices); + + // Setup vertex attributes based on layout + SetupVertexAttributes(layout); + resources.SetupDefaultAttributes(params.color); + + // Set shader uniforms + SetShaderUniforms(params); + + // Render the primitive + RenderPrimitive(params); + + // Cleanup (handled automatically by TempGLResources destructor) + glDisableVertexAttribArray(0); + glBindVertexArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); +} + +void ShapeRenderer::RenderShape(const std::function()>& vertex_generator, + const VertexLayout& layout, + const RenderParams& params) { + auto vertices = vertex_generator(); + RenderShape(vertices, layout, params); +} + +void ShapeRenderer::SetShaderUniforms(const RenderParams& params) { + shader_->TrySetUniform("uColor", params.color); + + if (params.filled) { + shader_->TrySetUniform("renderMode", 2); // Filled shapes mode + shader_->TrySetUniform("lineType", 0); // Solid fill + } else { + shader_->TrySetUniform("renderMode", 3); // Outline shapes mode + shader_->TrySetUniform("lineType", params.line_type); + shader_->TrySetUniform("thickness", params.thickness); + } +} + +void ShapeRenderer::SetupVertexAttributes(const VertexLayout& layout) { + if (layout.has_position) { + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + } + + // For now, we only support position-only layouts + // Color and size attributes are handled via uniforms and default attributes + glDisableVertexAttribArray(1); + glDisableVertexAttribArray(2); +} + +void ShapeRenderer::RenderPrimitive(const RenderParams& params) { + if (params.filled) { + glDrawArrays(params.primitive_type, 0, params.vertex_count); + } else { + glLineWidth(params.thickness); + glDrawArrays(params.primitive_type, 0, params.vertex_count); + glLineWidth(1.0f); // Reset line width + } +} + +// RenderParamFactory implementation +namespace RenderParamFactory { + +RenderParams FilledShape(const glm::vec4& color, float thickness) { + RenderParams params; + params.color = color; + params.thickness = thickness; + params.filled = true; + params.primitive_type = GL_TRIANGLE_FAN; + return params; +} + +RenderParams OutlineShape(const glm::vec4& color, float thickness, int line_type) { + RenderParams params; + params.color = color; + params.thickness = thickness; + params.filled = false; + params.line_type = line_type; + params.primitive_type = GL_LINE_LOOP; + return params; +} + +RenderParams Points(const glm::vec4& color, float size) { + RenderParams params; + params.color = color; + params.thickness = size; + params.filled = true; + params.primitive_type = GL_POINTS; + return params; +} + +RenderParams Lines(const glm::vec4& color, float thickness, int line_type) { + RenderParams params; + params.color = color; + params.thickness = thickness; + params.filled = false; + params.line_type = line_type; + params.primitive_type = GL_LINES; + return params; +} + +} // namespace RenderParamFactory + +} // namespace quickviz \ No newline at end of file diff --git a/src/renderer/src/renderable/details/shape_renderer.hpp b/src/renderer/src/renderable/details/shape_renderer.hpp new file mode 100644 index 0000000..6e72f3f --- /dev/null +++ b/src/renderer/src/renderable/details/shape_renderer.hpp @@ -0,0 +1,148 @@ +/** + * @file shape_renderer.hpp + * @author Claude Code Assistant + * @date 2025-01-04 + * @brief Unified shape renderer to eliminate code duplication + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#ifndef OPENGL_RENDERER_SHAPE_RENDERER_HPP +#define OPENGL_RENDERER_SHAPE_RENDERER_HPP + +#include +#include +#include +#include "glad/glad.h" + +namespace quickviz { + +// Forward declarations +class ShaderProgram; + +/** + * @brief OpenGL resource manager for temporary rendering resources + * + * Manages VAO/VBO creation, binding, and cleanup to eliminate duplication + * across different shape rendering methods. + */ +class TempGLResources { +public: + TempGLResources(); + ~TempGLResources(); + + // Non-copyable, movable + TempGLResources(const TempGLResources&) = delete; + TempGLResources& operator=(const TempGLResources&) = delete; + TempGLResources(TempGLResources&& other) noexcept; + TempGLResources& operator=(TempGLResources&& other) noexcept; + + void Bind(); + void UploadVertices(const std::vector& vertices); + void SetupVertexAttributes(); + void SetupDefaultAttributes(const glm::vec4& color); + + GLuint GetVAO() const { return vao_; } + GLuint GetVBO() const { return vbo_; } + +private: + GLuint vao_ = 0; + GLuint vbo_ = 0; + + void Cleanup(); +}; + +/** + * @brief Vertex layout specification for different shape types + */ +struct VertexLayout { + bool has_position = true; + bool has_color = false; + bool has_size = false; + + static VertexLayout PositionOnly() { + return {true, false, false}; + } + + static VertexLayout PositionColor() { + return {true, true, false}; + } + + static VertexLayout PositionColorSize() { + return {true, true, true}; + } +}; + +/** + * @brief Rendering parameters for shape drawing + */ +struct RenderParams { + glm::vec4 color; + float thickness = 1.0f; + bool filled = true; + int line_type = 0; // LineType as int to avoid forward declaration + GLenum primitive_type = GL_TRIANGLES; + int vertex_count = 0; + + // Shader uniforms + int render_mode = 0; +}; + +/** + * @brief Unified shape renderer that eliminates code duplication + * + * This class encapsulates the common OpenGL rendering patterns used + * across all shape types, reducing code duplication and improving + * maintainability. + */ +class ShapeRenderer { +public: + explicit ShapeRenderer(ShaderProgram* shader); + ~ShapeRenderer() = default; + + /** + * @brief Render a shape with given vertices and parameters + * @param vertices Flat array of vertex data + * @param layout Vertex attribute layout + * @param params Rendering parameters + */ + void RenderShape(const std::vector& vertices, + const VertexLayout& layout, + const RenderParams& params); + + /** + * @brief Render a shape using a vertex generator function + * @param vertex_generator Function that generates vertices + * @param layout Vertex attribute layout + * @param params Rendering parameters + */ + void RenderShape(const std::function()>& vertex_generator, + const VertexLayout& layout, + const RenderParams& params); + + /** + * @brief Set common shader uniforms for shape rendering + * @param params Rendering parameters containing uniform values + */ + void SetShaderUniforms(const RenderParams& params); + +private: + ShaderProgram* shader_; + + void SetupVertexAttributes(const VertexLayout& layout); + void RenderPrimitive(const RenderParams& params); +}; + +/** + * @brief Factory functions for creating common render parameters + */ +namespace RenderParamFactory { + RenderParams FilledShape(const glm::vec4& color, float thickness = 1.0f); + RenderParams OutlineShape(const glm::vec4& color, float thickness = 1.0f, int line_type = 0); + RenderParams Points(const glm::vec4& color, float size = 1.0f); + RenderParams Lines(const glm::vec4& color, float thickness = 1.0f, int line_type = 0); +} + +} // namespace quickviz + +#endif /* OPENGL_RENDERER_SHAPE_RENDERER_HPP */ \ No newline at end of file diff --git a/src/renderer/test/CMakeLists.txt b/src/renderer/test/CMakeLists.txt index 83e5432..0b5f618 100644 --- a/src/renderer/test/CMakeLists.txt +++ b/src/renderer/test/CMakeLists.txt @@ -28,4 +28,7 @@ add_executable(test_texture test_texture.cpp) target_link_libraries(test_texture PRIVATE renderer) add_executable(test_canvas_st test_canvas_st.cpp) -target_link_libraries(test_canvas_st PRIVATE renderer) \ No newline at end of file +target_link_libraries(test_canvas_st PRIVATE renderer) + +add_executable(test_nav_map_rendering test_nav_map_rendering.cpp) +target_link_libraries(test_nav_map_rendering PRIVATE renderer) \ No newline at end of file diff --git a/src/renderer/test/test_canvas_st.cpp b/src/renderer/test/test_canvas_st.cpp index 8efd793..7a1f499 100644 --- a/src/renderer/test/test_canvas_st.cpp +++ b/src/renderer/test/test_canvas_st.cpp @@ -28,59 +28,56 @@ namespace fs = std::filesystem; // Function to test all canvas drawing functions void TestAllCanvasFunctions(Canvas* canvas) { - // Add some points with different colors and sizes - canvas->AddPoint(0.0f, 0.0f, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), 5.0f); // Red - canvas->AddPoint(1.0f, 1.0f, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), 8.0f); // Green - canvas->AddPoint(-1.5f, -1.5f, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f), 10.0f); // Blue - - // Add lines with different styles - canvas->AddLine(2.0f, 2.0f, 3.0f, 3.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), 2.0f, LineType::kSolid); // Yellow solid - canvas->AddLine(-2.0f, 2.0f, -3.0f, 3.0f, glm::vec4(1.0f, 0.0f, 1.0f, 1.0f), 3.0f, LineType::kDashed); // Magenta dashed - canvas->AddLine(3.0f, -2.0f, 4.0f, -3.0f, glm::vec4(0.0f, 1.0f, 1.0f, 1.0f), 4.0f, LineType::kDotted); // Cyan dotted - - // Add rectangles - filled and outlined - canvas->AddRectangle(-4.0f, -4.0f, 1.0f, 1.0f, glm::vec4(1.0f, 0.5f, 0.0f, 0.7f), true, 2.0f); // Orange filled - canvas->AddRectangle(3.0f, -4.0f, 1.0f, 1.0f, glm::vec4(0.5f, 0.0f, 0.5f, 0.7f), false, 2.0f); // Purple outlined - - // Add circles - filled and outlined - canvas->AddCircle(-2.0f, -2.0f, 0.7f, glm::vec4(0.0f, 0.5f, 0.0f, 0.8f), true, 2.0f); // Dark green filled - canvas->AddCircle(2.0f, 0.0f, 0.5f, glm::vec4(0.7f, 0.7f, 0.7f, 0.8f), false, 2.0f); // Gray outlined - - // Add ellipses - filled and outlined - canvas->AddEllipse(0.0f, 3.0f, 1.0f, 0.5f, 0.0f, 0.0f, 6.28f, - glm::vec4(0.5f, 0.5f, 0.0f, 0.8f), true, 2.0f); // Olive filled - canvas->AddEllipse(-3.0f, 0.0f, 0.7f, 0.4f, 0.7f, 0.0f, 6.28f, - glm::vec4(0.5f, 0.0f, 0.0f, 0.8f), false, 2.0f); // Dark red outlined, rotated - - // Add a polygon - std::vector star_vertices = { - {0.0f, 5.0f}, - {1.0f, 2.0f}, - {4.0f, 2.0f}, - {2.0f, 0.0f}, - {3.0f, -3.0f}, - {0.0f, -1.0f}, - {-3.0f, -3.0f}, - {-2.0f, 0.0f}, - {-4.0f, 2.0f}, - {-1.0f, 2.0f}, + // Add reference points to verify coordinate system + canvas->AddPoint(0.0f, 0.0f, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), + 10.0f); // White center + canvas->AddPoint(0.0f, 2.0f, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), + 8.0f); // Green top + canvas->AddPoint(-4.0f, 1.0f, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f), + 8.0f); // Blue left + canvas->AddPoint(4.0f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 8.0f); // Yellow right + + canvas->AddLine(-1.0f, -1.0f, -1.0f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 2.0f, LineType::kSolid); // Yellow solid + canvas->AddLine(-1.25f, -1.0f, -1.25f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 2.0f, LineType::kDashed); + canvas->AddLine(-1.5f, -1.0f, -1.5f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 2.0f, LineType::kDotted); + + canvas->AddRectangle(-2.5, -2.5, 1.0, 1.0, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), + true, 2.0f); // Red filled rectangle + canvas->AddRectangle(-2.25, -1.25, 0.5, 0.5, + glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), false, + 2.0f); // Red filled rectangle + + canvas->AddCircle(3.0f, 0.0f, 0.5f, glm::vec4(0.0f, 0.5f, 0.0f, 0.8f), true, + 2.0f); // Dark green filled + canvas->AddCircle(2.0f, 0.0f, 0.25f, glm::vec4(0.0f, 0.5f, 0.0f, 0.8f), false, + 2.0f); + + canvas->AddEllipse(-3.0f, 0.0f, 1.0f, 0.5f, 0.0f, 0.0f, 6.28f, + glm::vec4(0.5f, 0.5f, 0.0f, 0.8f), true, + 2.0f); // Olive filled + canvas->AddEllipse(-3.0f, 1.5f, 0.5f, 0.25f, 0.0f, 0.0f, 6.28f, + glm::vec4(0.5f, 0.5f, 0.0f, 0.8f), false, 2.0f); + + // Add a simple test polygon + std::vector simple_triangle = { + {-0.5f, -1.0f}, // Bottom-left + {0.5f, -1.0f}, // Bottom-right + {0.0f, -0.5f} // Top }; - - // Scale down the star vertices - for (auto& vertex : star_vertices) { - vertex *= 0.3f; + canvas->AddPolygon(simple_triangle, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), true, + 3.0f); + for (auto& v : simple_triangle) { + v += glm::vec2(0.0f, -1.0f); } - - // Move the star to a different position - for (auto& vertex : star_vertices) { - vertex += glm::vec2(4.0f, 3.0f); - } - - canvas->AddPolygon(star_vertices, glm::vec4(0.8f, 0.8f, 0.0f, 0.9f), true, 2.0f); // Gold filled + canvas->AddPolygon(simple_triangle, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), false, + 3.0f); } int main(int argc, char* argv[]) { - Viewer viewer; // create a box to manage size & position of the OpenGL scene @@ -109,6 +106,7 @@ int main(int argc, char* argv[]) { gl_sm->AddOpenGLObject("coordinate_frame", std::move(coord_frame)); auto canvas = std::make_unique(); + canvas->SetBatchingEnabled(true); gl_sm->AddOpenGLObject("canvas", std::move(canvas)); // now let's do some drawing on the canvas @@ -117,7 +115,7 @@ int main(int argc, char* argv[]) { // Add background image first so it's behind all other drawings std::string image_path = "../data/fish.png"; - + // Check if file exists and get absolute path fs::path abs_path = fs::absolute(image_path); std::cout << "Checking image path: " << abs_path.string() << std::endl; @@ -125,32 +123,36 @@ int main(int argc, char* argv[]) { std::cout << "Image file exists!" << std::endl; } else { std::cout << "Image file does not exist!" << std::endl; - + // Try alternative paths std::string alt_path1 = "data/fish.png"; fs::path abs_alt_path1 = fs::absolute(alt_path1); - std::cout << "Trying alternative path: " << abs_alt_path1.string() << std::endl; + std::cout << "Trying alternative path: " << abs_alt_path1.string() + << std::endl; if (fs::exists(abs_alt_path1)) { std::cout << "Alternative image file exists!" << std::endl; image_path = alt_path1; } - + std::string alt_path2 = "fish.png"; fs::path abs_alt_path2 = fs::absolute(alt_path2); - std::cout << "Trying alternative path: " << abs_alt_path2.string() << std::endl; + std::cout << "Trying alternative path: " << abs_alt_path2.string() + << std::endl; if (fs::exists(abs_alt_path2)) { std::cout << "Alternative image file exists!" << std::endl; image_path = alt_path2; } } - - // Add background image using a small origin offset and 1:100 resolution for debugging - canvas->AddBackgroundImage(image_path, glm::vec3(1.0f, 1.0f, 0.785f), 0.005f); - + + // Add background image using a small origin offset and 1:100 resolution for + // debugging + canvas->AddBackgroundImage(image_path, glm::vec3(1.0f, 1.0f, 0.785f), + 0.005f); + // Test all canvas drawing functions TestAllCanvasFunctions(canvas); } - + // finally pass the OpenGL scene managers to the box and add it to the viewer box->AddChild(gl_sm); viewer.AddSceneObject(box); diff --git a/src/renderer/test/test_nav_map_rendering.cpp b/src/renderer/test/test_nav_map_rendering.cpp new file mode 100644 index 0000000..ad4960d --- /dev/null +++ b/src/renderer/test/test_nav_map_rendering.cpp @@ -0,0 +1,207 @@ +/** + * @file test_gl_scene_manager.cpp + * @author Ruixiang Du (ruixiang.du@gmail.com) + * @date 2025-03-06 + * @brief + * + * Copyright (c) 2025 Ruixiang Du (rdu) + */ + +#include +#include + +#include +#include +#include + +#include "imview/box.hpp" +#include "imview/viewer.hpp" + +#include "renderer/gl_scene_manager.hpp" +#include "renderer/renderable/grid.hpp" +#include "renderer/renderable/triangle.hpp" +#include "renderer/renderable/coordinate_frame.hpp" +#include "renderer/renderable/canvas.hpp" + +using namespace quickviz; +namespace fs = std::filesystem; + +// Function to test all canvas drawing functions +void TestAllCanvasFunctions(Canvas* canvas) { + // Add reference points to verify coordinate system + canvas->AddPoint(0.0f, 0.0f, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), + 10.0f); // White center + canvas->AddPoint(0.0f, 2.0f, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f), + 8.0f); // Green top + canvas->AddPoint(-4.0f, 1.0f, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f), + 8.0f); // Blue left + canvas->AddPoint(4.0f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 8.0f); // Yellow right + + canvas->AddLine(-1.0f, -1.0f, -1.0f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 2.0f, LineType::kSolid); // Yellow solid + canvas->AddLine(-1.25f, -1.0f, -1.25f, 1.0f, + glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), 2.0f, LineType::kDashed); + canvas->AddLine(-1.5f, -1.0f, -1.5f, 1.0f, glm::vec4(1.0f, 1.0f, 0.0f, 1.0f), + 2.0f, LineType::kDotted); + + canvas->AddRectangle(-2.5, -2.5, 1.0, 1.0, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), + true, 2.0f); // Red filled rectangle + canvas->AddRectangle(-2.25, -1.25, 0.5, 0.5, + glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), false, + 2.0f); // Red filled rectangle + + canvas->AddCircle(3.0f, 0.0f, 0.5f, glm::vec4(0.0f, 0.5f, 0.0f, 0.8f), true, + 2.0f); // Dark green filled + canvas->AddCircle(2.0f, 0.0f, 0.25f, glm::vec4(0.0f, 0.5f, 0.0f, 0.8f), false, + 2.0f); + + canvas->AddEllipse(-3.0f, 0.0f, 1.0f, 0.5f, 0.0f, 0.0f, 6.28f, + glm::vec4(0.5f, 0.5f, 0.0f, 0.8f), true, + 2.0f); // Olive filled + canvas->AddEllipse(-3.0f, 1.5f, 0.5f, 0.25f, 0.0f, 0.0f, 6.28f, + glm::vec4(0.5f, 0.5f, 0.0f, 0.8f), false, 2.0f); + + // Add a simple test polygon + std::vector simple_triangle = { + {-0.5f, -1.0f}, // Bottom-left + {0.5f, -1.0f}, // Bottom-right + {0.0f, -0.5f} // Top + }; + canvas->AddPolygon(simple_triangle, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), true, + 3.0f); + for (auto& v : simple_triangle) { + v += glm::vec2(0.0f, -1.0f); + } + canvas->AddPolygon(simple_triangle, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f), false, + 3.0f); +} + +void DrawRobotMarker(float x, float y, float theta, Canvas* canvas, + float size = 1.0f) { + // Professional navigation marker with high-contrast colors for visibility + const glm::vec4 outer_ring(1.0f, 0.4f, 0.0f, 1.0f); // Bright orange outer ring + const glm::vec4 inner_ring(0.6f, 0.6f, 0.6f, 1.0f); // Medium gray inner ring + const glm::vec4 circle_fill(0.1f, 0.1f, 0.1f, 1.0f); // Very dark gray fill + const glm::vec4 arrow_fill(1.0f, 1.0f, 1.0f, 1.0f); // Pure white arrow + const glm::vec4 arrow_outline(0.0f, 0.0f, 0.0f, 1.0f); // Black outline + + // Calculate direction vector + float cos_theta = std::cos(theta); + float sin_theta = std::sin(theta); + + // Professional proportions - make outer ring much wider + const float outer_radius = 1.0f * size; + const float inner_radius = 1.0f * size; + const float fill_radius = 0.85f * size; + const float arrow_length = 0.6f * size; + const float arrow_base_width = 0.85f * size; + + // Draw layered circular background for depth + canvas->AddCircle(x, y, fill_radius, circle_fill, true); + canvas->AddCircle(x, y, outer_radius, outer_ring, true); // Black outer ring + // canvas->AddCircle(x, y, inner_radius, inner_ring, true); // Gray + // inner ring + + // Calculate arrow geometry with professional notch + float tip_x = x + arrow_length * cos_theta; + float tip_y = y + arrow_length * sin_theta; + + // Base corners (perpendicular to direction) - wider base + float base_offset_x = arrow_base_width * 0.5f * sin_theta; + float base_offset_y = -arrow_base_width * 0.5f * cos_theta; + + // Position base closer to center for better proportions + float base_center_x = x - 0.7f * arrow_length * cos_theta; + float base_center_y = y - 0.7f * arrow_length * sin_theta; + + float base_left_x = base_center_x + base_offset_x; + float base_left_y = base_center_y + base_offset_y; + float base_right_x = base_center_x - base_offset_x; + float base_right_y = base_center_y - base_offset_y; + + // Create notch at the bottom (professional navigation marker style) + const float notch_depth = 0.2f * size; + float notch_center_x = x - (0.4f - notch_depth) * arrow_length * cos_theta; + float notch_center_y = y - (0.4f - notch_depth) * arrow_length * sin_theta; + + // Create the notched arrow shape (outline version) + std::vector notched_arrow_outline = { + {tip_x, tip_y}, // Arrow tip + {base_left_x, base_left_y}, // Left base corner + {notch_center_x, notch_center_y}, // Bottom notch center + {base_right_x, base_right_y} // Right base corner + }; + + // Draw white arrow fill first + canvas->AddPolygon(notched_arrow_outline, arrow_fill, true, 2.0f); +} + +int main(int argc, char* argv[]) { + Viewer viewer; + + // create a box to manage size & position of the OpenGL scene + auto box = std::make_shared("main_box"); + box->SetFlexDirection(Styling::FlexDirection::kRow); + box->SetJustifyContent(Styling::JustifyContent::kFlexStart); + box->SetAlignItems(Styling::AlignItems::kStretch); + + // create a OpenGL scene manager to manage the OpenGL objects + auto gl_sm = std::make_shared("OpenGL Scene (2D)", + GlSceneManager::Mode::k2D); + gl_sm->SetAutoLayout(true); + gl_sm->SetNoTitleBar(true); + gl_sm->SetFlexGrow(1.0f); + gl_sm->SetFlexShrink(1.0f); + + // now add the rendering objects to the OpenGL scene manager + // auto triangle = std::make_unique(1.0f, glm::vec3(0.0f, 0.5f, + // 0.5f)); gl_sm->AddOpenGLObject("triangle", std::move(triangle)); + + auto grid = std::make_unique(10.0f, 1.0f, glm::vec3(0.7f, 0.7f, 0.7f)); + gl_sm->AddOpenGLObject("grid", std::move(grid)); + + // Add a coordinate frame in 2D mode (should show X and Z axes) + auto coord_frame = std::make_unique(0.5f, true); + gl_sm->AddOpenGLObject("coordinate_frame", std::move(coord_frame)); + + auto canvas = std::make_unique(); + canvas->SetBatchingEnabled(false); + gl_sm->AddOpenGLObject("canvas", std::move(canvas)); + + // now let's do some drawing on the canvas + { + auto canvas = static_cast(gl_sm->GetOpenGLObject("canvas")); + + // Add background image first so it's behind all other drawings + std::string image_path = "../data/openstreet_map.png"; + + // Check if file exists and get absolute path + fs::path abs_path = fs::absolute(image_path); + std::cout << "Checking image path: " << abs_path.string() << std::endl; + if (fs::exists(abs_path)) { + std::cout << "Image file exists!" << std::endl; + canvas->AddBackgroundImage(image_path, glm::vec3(5.0f, -5.0f, 1.57f), + 0.05f); + } else { + std::cout << "Image file does not exist!" << std::endl; + } + + // Test all canvas drawing functions + // TestAllCanvasFunctions(canvas); + DrawRobotMarker(0.0, 0.0, 0.0, canvas, 0.5f); // Pointing right + DrawRobotMarker(1.0, 1.0, M_PI / 4.0f, canvas, 0.5f); // Pointing up + DrawRobotMarker(3.0, 3.0, M_PI / 2.0f, canvas, 0.5f); // Pointing up + DrawRobotMarker(-3.0, 3.0, M_PI, canvas, 0.5f); // Pointing left + DrawRobotMarker(-2.0, 2.5, -M_PI/4.0f, canvas, 0.5f); // Pointing left + DrawRobotMarker(3.0, -3.0, -M_PI / 2.0f, canvas, 0.5f); // Pointing down + } + + // finally pass the OpenGL scene managers to the box and add it to the viewer + box->AddChild(gl_sm); + viewer.AddSceneObject(box); + + viewer.Show(); + + return 0; +} \ No newline at end of file