Repository navigation
feat: SDF text rendering foundation (Phases 1-3) - #27
Conversation
…endering design This commit adds the Button UI component and creates a comprehensive design document for industry-standard batched rendering with proper z-ordering and transparency support. ## Button Component (libs/ui/components/button/) - Full state machine: Normal, Hover, Pressed, Disabled, Focused - Mouse and keyboard input (click, Tab navigation, Enter/Space activation) - Styled appearances (Primary, Secondary, Custom) - Text label with proper centering - Click callbacks with event handling - Demo scene with multiple button types and interactions ## Input Abstraction Layer (libs/engine/input/) - Created input_types.h with platform-independent Key and MouseButton enums - Updated InputManager to use enums instead of raw GLFW constants - Isolates GLFW dependencies from UI layer (architectural fix) - Internal conversion functions map enums to GLFW values - Updated all scenes (button_scene, input_test_scene) to use new enums ## Batched Rendering Design (docs/technical/ui-framework/) - Industry-standard command queue architecture (Unity, Unreal, Chrome pattern) - Material-based batching (shader + texture + blend mode) - Two-pass rendering: opaque (front-to-back) + transparent (back-to-front) - Explicit z-index control for proper layering - State change minimization (primary optimization target) - Performance analysis: 21-41 draw calls for complex UI (well under 100 target) - Addresses text rendering z-order issues with proper architectural solution ## Known Issues - Button text labels render behind button backgrounds (immediate vs batched rendering) - Will be fixed by implementing batched text rendering per design doc - Button component is architecturally complete, rendering bug requires renderer update Files changed: - libs/ui/components/button/ (NEW: button.h, button.cpp, button_style.h) - libs/engine/input/input_types.h (NEW: Key and MouseButton enums) - libs/engine/input/input_manager.h (API: use enums instead of int) - libs/engine/input/input_manager.cpp (impl: ToGLFW conversion functions) - apps/ui-sandbox/scenes/button_scene.cpp (NEW: Button demo scene) - apps/ui-sandbox/scenes/input_test_scene.cpp (update: use new input enums) - docs/technical/ui-framework/batched-text-rendering.md (NEW: complete design) - docs/status.md (update: Button complete, batched rendering tasks added)
Implement multi-channel signed distance field (MSDF) text rendering to fix z-ordering issues where button text renders behind backgrounds. **Phase 1: SDF Atlas Generation Tool** - Add msdfgen to vcpkg with extensions feature - Create tools/generate_sdf_atlas executable - Implement atlas generation using msdfgen 1.12.1 API - Generate Roboto-SDF atlas (512x512, 94 glyphs) - Output JSON metadata with glyph metrics and UV coords **Phase 2: FontRenderer SDF Support** - Add LoadSDFAtlas() method for JSON + PNG loading - Implement GenerateGlyphQuads() for batched text rendering - Add SDFGlyph and SDFAtlasMetadata structures - Support both legacy FreeType and new SDF atlas modes **Phase 3: SDF Shaders** - Create msdf_text.vert with per-vertex color support - Create msdf_text.frag with median-of-three algorithm - Implement screen-space derivative scaling for quality **Build Configuration** - Switch to Homebrew LLVM to fix vcpkg harfbuzz build issues - Update vcpkg baseline for compatibility - Add tools subdirectory to CMake **Documentation** - Add sdf-text-rendering.md technical design doc - Update batched-text-rendering.md with implementation plan Phase 4 (batched rendering integration) still in progress.
There was a problem hiding this comment.
Pull Request Overview
This PR implements the foundation for SDF (Signed Distance Field) text rendering to address critical z-ordering issues where button text renders behind backgrounds. The implementation spans three phases: atlas generation tooling, FontRenderer SDF support, and MSDF shaders, plus additional Button component implementation and input abstraction.
Key Changes:
- Added msdfgen-based atlas generation tool for creating SDF font textures
- Extended FontRenderer with SDF atlas loading and glyph quad generation
- Implemented MSDF vertex and fragment shaders for high-quality text rendering
- Created Button UI component with state management and styling system
- Introduced platform-independent input abstraction layer (Key, MouseButton enums)
Reviewed Changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
| vcpkg.json | Added msdfgen dependency with extensions feature; updated baseline |
| tools/generate_sdf_atlas/main.cpp | Atlas generator using msdfgen to create SDF textures and metadata |
| tools/generate_sdf_atlas/CMakeLists.txt | Build configuration for atlas generation tool |
| libs/ui/font/font_renderer.h | Added SDF data structures (SDFGlyph, SDFAtlasMetadata) and batch methods |
| libs/ui/font/font_renderer.cpp | Implemented LoadSDFAtlas and GenerateGlyphQuads for SDF rendering |
| libs/renderer/shaders/msdf_text.vert | Vertex shader for SDF text with per-vertex color/UVs |
| libs/renderer/shaders/msdf_text.frag | Fragment shader with median-of-three MSDF algorithm |
| libs/ui/components/button/* | Complete Button component with 5-state styling system |
| libs/engine/input/input_types.h | Platform-independent Key and MouseButton enumerations |
| libs/engine/input/input_manager.* | Updated to use type-safe input enums instead of raw GLFW constants |
| fonts/Roboto-SDF.json | Generated SDF atlas metadata with 94 glyphs |
| docs/technical/ui-framework/sdf-text-rendering.md | Comprehensive SDF implementation guide |
| docs/technical/ui-framework/batched-text-rendering.md | Command queue architecture documentation |
| apps/ui-sandbox/scenes/button_scene.cpp | Interactive demo showcasing Button states and keyboard navigation |
| apps/ui-sandbox/scenes/input_test_scene.cpp | Updated to use type-safe input enums |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Pack glyphs into atlas using simple grid layout | ||
| int glyphsPerRow = config.atlasWidth / config.glyphSize; | ||
| int currentX = 0; | ||
| int currentY = 0; | ||
|
|
||
| for (auto& glyph : glyphs) { | ||
| if (glyph.shape.contours.empty()) { | ||
| // Whitespace - no atlas space needed | ||
| continue; | ||
| } | ||
|
|
||
| glyph.atlasX = currentX; | ||
| glyph.atlasY = currentY; | ||
| glyph.atlasWidth = config.glyphSize; | ||
| glyph.atlasHeight = config.glyphSize; | ||
|
|
||
| currentX += config.glyphSize; | ||
| if (currentX + config.glyphSize > config.atlasWidth) { | ||
| currentX = 0; | ||
| currentY += config.glyphSize; | ||
|
|
||
| if (currentY + config.glyphSize > config.atlasHeight) { | ||
| std::cerr << "WARNING: Atlas too small for all glyphs\n"; | ||
| break; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
[nitpick] The atlas generation uses a simple grid layout (lines 159-184) which wastes space when glyphs have different sizes. For a 512x512 atlas with 32px glyph slots, this supports at most 256 glyphs (16x16 grid), but the charset has 94 characters. While this works for the initial set, it's inefficient. Consider using a proper bin-packing algorithm (e.g., shelf packing or the packer suggested in the documentation comments) for better space utilization, especially if more characters are added later.
| .fontSize = style.fontSize, | ||
| .hAlign = Foundation::HorizontalAlign::Left, | ||
| .vAlign = Foundation::VerticalAlign::Top}, | ||
| .zIndex = zIndex + 0.1F, // Slightly above button background |
There was a problem hiding this comment.
[nitpick] The button's z-index calculation zIndex + 0.1F (line 41 in button.cpp) to place text above the background is fragile. If the parent sets a z-index of 10.0, the text will be at 10.1, but if another element at 10.05 exists, it will render between them. Additionally, floating-point precision issues could cause problems. Consider using a more robust layering system or integer-based z-indices with guaranteed spacing.
| .zIndex = zIndex + 0.1F, // Slightly above button background | |
| .zIndex = zIndex + 1, // Above button background |
| // CRITICAL: We need to flush batched primitives BEFORE text renders | ||
| // Otherwise batched rectangles draw over immediately-rendered text | ||
|
|
||
| // Flush any previous batched primitives first | ||
| Renderer::Primitives::EndFrame(); | ||
|
|
||
| // Render layer hierarchy text labels (immediate rendering) | ||
| m_layerManager.RenderAll(); | ||
|
|
||
| // Render all buttons (batches rectangles, then immediately renders text) | ||
| for (const auto& button : m_buttons) { | ||
| button.Render(); | ||
| } | ||
|
|
||
| // Flush batched button rectangles so they appear | ||
| Renderer::Primitives::EndFrame(); |
There was a problem hiding this comment.
The button scene manually calls Renderer::Primitives::EndFrame() before and after rendering (lines 297 and 308). The comment at line 294 correctly identifies this is a workaround for the z-ordering bug that this PR is meant to fix. However, since Phase 4 (batched rendering integration) is incomplete, this workaround is necessary but creates the exact problem described in the batched-text-rendering.md documentation. This temporary solution should be clearly marked as TODO and removed when Phase 4 is complete.
| { | ||
| "atlas": { | ||
| "type": "msdf", | ||
| "distanceRange": 4, | ||
| "size": 32, | ||
| "width": 512, | ||
| "height": 512 | ||
| }, | ||
| "metrics": { | ||
| "emSize": 1, | ||
| "ascender": 0.927734, | ||
| "descender": -0.244141, | ||
| "lineHeight": 1.17188 | ||
| }, | ||
| "glyphs": { | ||
| "A": { | ||
| "atlas": {"x": 0, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0141602, "bottom": 0, "right": 0.639648, "top": 0.710938}, | ||
| "advance": 0.652344 | ||
| }, | ||
| "B": { | ||
| "atlas": {"x": 0.0625, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.566406, "top": 0.710938}, | ||
| "advance": 0.623047 | ||
| }, | ||
| "C": { | ||
| "atlas": {"x": 0.125, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0585938, "bottom": -0.00976562, "right": 0.605469, "top": 0.720703}, | ||
| "advance": 0.650879 | ||
| }, | ||
| "D": { | ||
| "atlas": {"x": 0.1875, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.597168, "top": 0.710938}, | ||
| "advance": 0.65625 | ||
| }, | ||
| "E": { | ||
| "atlas": {"x": 0.25, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.53418, "top": 0.710938}, | ||
| "advance": 0.568359 | ||
| }, | ||
| "F": { | ||
| "atlas": {"x": 0.3125, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.522949, "top": 0.710938}, | ||
| "advance": 0.552734 | ||
| }, | ||
| "G": { | ||
| "atlas": {"x": 0.375, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0595703, "bottom": -0.00976562, "right": 0.60791, "top": 0.720703}, | ||
| "advance": 0.681152 | ||
| }, | ||
| "H": { | ||
| "atlas": {"x": 0.4375, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.628906, "top": 0.710938}, | ||
| "advance": 0.713379 | ||
| }, | ||
| "I": { | ||
| "atlas": {"x": 0.5, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0893555, "bottom": 0, "right": 0.183594, "top": 0.710938}, | ||
| "advance": 0.271973 | ||
| }, | ||
| "J": { | ||
| "atlas": {"x": 0.5625, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0258789, "bottom": -0.00976562, "right": 0.474609, "top": 0.710938}, | ||
| "advance": 0.551758 | ||
| }, | ||
| "K": { | ||
| "atlas": {"x": 0.625, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.627441, "top": 0.710938}, | ||
| "advance": 0.627441 | ||
| }, | ||
| "L": { | ||
| "atlas": {"x": 0.6875, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.513672, "top": 0.710938}, | ||
| "advance": 0.538574 | ||
| }, | ||
| "M": { | ||
| "atlas": {"x": 0.75, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.790039, "top": 0.710938}, | ||
| "advance": 0.873047 | ||
| }, | ||
| "N": { | ||
| "atlas": {"x": 0.8125, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.629395, "top": 0.710938}, | ||
| "advance": 0.713379 | ||
| }, | ||
| "O": { | ||
| "atlas": {"x": 0.875, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0581055, "bottom": -0.00976562, "right": 0.629883, "top": 0.720703}, | ||
| "advance": 0.687988 | ||
| }, | ||
| "P": { | ||
| "atlas": {"x": 0.9375, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.594238, "top": 0.710938}, | ||
| "advance": 0.630859 | ||
| }, | ||
| "Q": { | ||
| "atlas": {"x": 0, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0537109, "bottom": -0.120117, "right": 0.62793, "top": 0.720703}, | ||
| "advance": 0.687988 | ||
| }, | ||
| "R": { | ||
| "atlas": {"x": 0.0625, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.598633, "top": 0.710938}, | ||
| "advance": 0.616211 | ||
| }, | ||
| "S": { | ||
| "atlas": {"x": 0.125, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0395508, "bottom": -0.00976562, "right": 0.556152, "top": 0.720703}, | ||
| "advance": 0.59375 | ||
| }, | ||
| "T": { | ||
| "atlas": {"x": 0.1875, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0244141, "bottom": 0, "right": 0.57373, "top": 0.710938}, | ||
| "advance": 0.59668 | ||
| }, | ||
| "U": { | ||
| "atlas": {"x": 0.25, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0683594, "bottom": -0.00976562, "right": 0.583008, "top": 0.710938}, | ||
| "advance": 0.648438 | ||
| }, | ||
| "V": { | ||
| "atlas": {"x": 0.3125, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0141602, "bottom": 0, "right": 0.623535, "top": 0.710938}, | ||
| "advance": 0.636719 | ||
| }, | ||
| "W": { | ||
| "atlas": {"x": 0.375, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0297852, "bottom": 0, "right": 0.865723, "top": 0.710938}, | ||
| "advance": 0.887207 | ||
| }, | ||
| "X": { | ||
| "atlas": {"x": 0.4375, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0283203, "bottom": 0, "right": 0.600586, "top": 0.710938}, | ||
| "advance": 0.626953 | ||
| }, | ||
| "Y": { | ||
| "atlas": {"x": 0.5, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.00732422, "bottom": 0, "right": 0.591797, "top": 0.710938}, | ||
| "advance": 0.600586 | ||
| }, | ||
| "Z": { | ||
| "atlas": {"x": 0.5625, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0424805, "bottom": 0, "right": 0.55957, "top": 0.710938}, | ||
| "advance": 0.599121 | ||
| }, | ||
| "a": { | ||
| "atlas": {"x": 0.625, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0532227, "bottom": -0.00976562, "right": 0.489258, "top": 0.538086}, | ||
| "advance": 0.543945 | ||
| }, | ||
| "b": { | ||
| "atlas": {"x": 0.6875, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0683594, "bottom": -0.00976562, "right": 0.516113, "top": 0.75}, | ||
| "advance": 0.561523 | ||
| }, | ||
| "c": { | ||
| "atlas": {"x": 0.75, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0454102, "bottom": -0.00976562, "right": 0.490723, "top": 0.538086}, | ||
| "advance": 0.523438 | ||
| }, | ||
| "d": { | ||
| "atlas": {"x": 0.8125, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0463867, "bottom": -0.00976562, "right": 0.492676, "top": 0.75}, | ||
| "advance": 0.563965 | ||
| }, | ||
| "e": { | ||
| "atlas": {"x": 0.875, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0454102, "bottom": -0.00976562, "right": 0.493652, "top": 0.538086}, | ||
| "advance": 0.530273 | ||
| }, | ||
| "f": { | ||
| "atlas": {"x": 0.9375, "y": 0.0625, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0297852, "bottom": 0, "right": 0.349121, "top": 0.760254}, | ||
| "advance": 0.347656 | ||
| }, | ||
| "g": { | ||
| "atlas": {"x": 0, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0473633, "bottom": -0.208496, "right": 0.493164, "top": 0.538086}, | ||
| "advance": 0.561523 | ||
| }, | ||
| "h": { | ||
| "atlas": {"x": 0.0625, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0688477, "bottom": 0, "right": 0.484375, "top": 0.75}, | ||
| "advance": 0.550781 | ||
| }, | ||
| "i": { | ||
| "atlas": {"x": 0.125, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0693359, "bottom": 0, "right": 0.17627, "top": 0.720703}, | ||
| "advance": 0.243164 | ||
| }, | ||
| "j": { | ||
| "atlas": {"x": 0.1875, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": -0.0322266, "bottom": -0.213379, "right": 0.168945, "top": 0.720703}, | ||
| "advance": 0.239258 | ||
| }, | ||
| "k": { | ||
| "atlas": {"x": 0.25, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0688477, "bottom": 0, "right": 0.506348, "top": 0.75}, | ||
| "advance": 0.506836 | ||
| }, | ||
| "l": { | ||
| "atlas": {"x": 0.3125, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0761719, "bottom": 0, "right": 0.166992, "top": 0.75}, | ||
| "advance": 0.243164 | ||
| }, | ||
| "m": { | ||
| "atlas": {"x": 0.375, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0678711, "bottom": 0, "right": 0.809082, "top": 0.538086}, | ||
| "advance": 0.876953 | ||
| }, | ||
| "n": { | ||
| "atlas": {"x": 0.4375, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0688477, "bottom": 0, "right": 0.484375, "top": 0.538086}, | ||
| "advance": 0.552246 | ||
| }, | ||
| "o": { | ||
| "atlas": {"x": 0.5, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0449219, "bottom": -0.00976562, "right": 0.525879, "top": 0.538086}, | ||
| "advance": 0.570312 | ||
| }, | ||
| "p": { | ||
| "atlas": {"x": 0.5625, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0683594, "bottom": -0.203125, "right": 0.515137, "top": 0.538086}, | ||
| "advance": 0.561523 | ||
| }, | ||
| "q": { | ||
| "atlas": {"x": 0.625, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0463867, "bottom": -0.203125, "right": 0.492188, "top": 0.538086}, | ||
| "advance": 0.568359 | ||
| }, | ||
| "r": { | ||
| "atlas": {"x": 0.6875, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0688477, "bottom": 0, "right": 0.324219, "top": 0.538086}, | ||
| "advance": 0.338867 | ||
| }, | ||
| "s": { | ||
| "atlas": {"x": 0.75, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0463867, "bottom": -0.00976562, "right": 0.466797, "top": 0.538086}, | ||
| "advance": 0.516113 | ||
| }, | ||
| "t": { | ||
| "atlas": {"x": 0.8125, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.00439453, "bottom": -0.00976562, "right": 0.29248, "top": 0.656738}, | ||
| "advance": 0.327148 | ||
| }, | ||
| "u": { | ||
| "atlas": {"x": 0.875, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0668945, "bottom": -0.00976562, "right": 0.48291, "top": 0.52832}, | ||
| "advance": 0.55127 | ||
| }, | ||
| "v": { | ||
| "atlas": {"x": 0.9375, "y": 0.125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0161133, "bottom": 0, "right": 0.466309, "top": 0.52832}, | ||
| "advance": 0.484375 | ||
| }, | ||
| "w": { | ||
| "atlas": {"x": 0, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0209961, "bottom": 0, "right": 0.728027, "top": 0.52832}, | ||
| "advance": 0.751465 | ||
| }, | ||
| "x": { | ||
| "atlas": {"x": 0.0625, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0205078, "bottom": 0, "right": 0.474121, "top": 0.52832}, | ||
| "advance": 0.496094 | ||
| }, | ||
| "y": { | ||
| "atlas": {"x": 0.125, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0107422, "bottom": -0.213379, "right": 0.460938, "top": 0.52832}, | ||
| "advance": 0.473145 | ||
| }, | ||
| "z": { | ||
| "atlas": {"x": 0.1875, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.043457, "bottom": 0, "right": 0.462402, "top": 0.52832}, | ||
| "advance": 0.496094 | ||
| }, | ||
| "0": { | ||
| "atlas": {"x": 0.25, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0561523, "bottom": -0.00976562, "right": 0.505371, "top": 0.720703}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "1": { | ||
| "atlas": {"x": 0.3125, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0834961, "bottom": 0, "right": 0.355957, "top": 0.714844}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "2": { | ||
| "atlas": {"x": 0.375, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0458984, "bottom": 0, "right": 0.524902, "top": 0.720703}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "3": { | ||
| "atlas": {"x": 0.4375, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0463867, "bottom": -0.00976562, "right": 0.49707, "top": 0.720703}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "4": { | ||
| "atlas": {"x": 0.5, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0258789, "bottom": 0, "right": 0.539551, "top": 0.710938}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "5": { | ||
| "atlas": {"x": 0.5625, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0751953, "bottom": -0.00976562, "right": 0.522461, "top": 0.710938}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "6": { | ||
| "atlas": {"x": 0.625, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0649414, "bottom": -0.00976562, "right": 0.51416, "top": 0.711914}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "7": { | ||
| "atlas": {"x": 0.6875, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0380859, "bottom": 0, "right": 0.518555, "top": 0.710938}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "8": { | ||
| "atlas": {"x": 0.75, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0551758, "bottom": -0.00976562, "right": 0.507324, "top": 0.720703}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "9": { | ||
| "atlas": {"x": 0.8125, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0488281, "bottom": -0.000976562, "right": 0.496094, "top": 0.720703}, | ||
| "advance": 0.562012 | ||
| }, | ||
| " ": { | ||
| "atlas": null, | ||
| "plane": null, | ||
| "advance": 0.248047 | ||
| }, | ||
| "!": { | ||
| "atlas": {"x": 0.875, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0786133, "bottom": -0.00585938, "right": 0.185547, "top": 0.710938}, | ||
| "advance": 0.257812 | ||
| }, | ||
| "@": { | ||
| "atlas": {"x": 0.9375, "y": 0.1875, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.055994, "bottom": -0.221191, "right": 0.848321, "top": 0.69873}, | ||
| "advance": 0.897949 | ||
| }, | ||
| "#": { | ||
| "atlas": {"x": 0, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0581055, "bottom": 0, "right": 0.603027, "top": 0.710938}, | ||
| "advance": 0.615723 | ||
| }, | ||
| "$": { | ||
| "atlas": {"x": 0.0625, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0537109, "bottom": -0.101562, "right": 0.508789, "top": 0.826172}, | ||
| "advance": 0.562012 | ||
| }, | ||
| "%": { | ||
| "atlas": {"x": 0.125, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0512695, "bottom": -0.0102539, "right": 0.688965, "top": 0.721191}, | ||
| "advance": 0.732422 | ||
| }, | ||
| "^": { | ||
| "atlas": {"x": 0.1875, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.03125, "bottom": 0.355957, "right": 0.385254, "top": 0.710938}, | ||
| "advance": 0.417969 | ||
| }, | ||
| "&": { | ||
| "atlas": {"x": 0.25, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0498047, "bottom": -0.00976562, "right": 0.618652, "top": 0.720703}, | ||
| "advance": 0.62207 | ||
| }, | ||
| "*": { | ||
| "atlas": {"x": 0.3125, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0136719, "bottom": 0.297852, "right": 0.416992, "top": 0.711426}, | ||
| "advance": 0.430664 | ||
| }, | ||
| "(": { | ||
| "atlas": {"x": 0.375, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0654297, "bottom": -0.229492, "right": 0.323242, "top": 0.802246}, | ||
| "advance": 0.342285 | ||
| }, | ||
| ")": { | ||
| "atlas": {"x": 0.4375, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.019043, "bottom": -0.229492, "right": 0.276855, "top": 0.802246}, | ||
| "advance": 0.348145 | ||
| }, | ||
| "_": { | ||
| "atlas": {"x": 0.5, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.00195312, "bottom": -0.0742188, "right": 0.449707, "top": 0}, | ||
| "advance": 0.451172 | ||
| }, | ||
| "+": { | ||
| "atlas": {"x": 0.5625, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0380859, "bottom": 0.0712891, "right": 0.525391, "top": 0.588867}, | ||
| "advance": 0.567383 | ||
| }, | ||
| "-": { | ||
| "atlas": {"x": 0.625, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0185547, "bottom": 0.265137, "right": 0.256836, "top": 0.339355}, | ||
| "advance": 0.276367 | ||
| }, | ||
| "=": { | ||
| "atlas": {"x": 0.6875, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0742188, "bottom": 0.194824, "right": 0.481445, "top": 0.476074}, | ||
| "advance": 0.548828 | ||
| }, | ||
| "[": { | ||
| "atlas": {"x": 0.75, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0717773, "bottom": -0.152344, "right": 0.255371, "top": 0.8125}, | ||
| "advance": 0.265137 | ||
| }, | ||
| "]": { | ||
| "atlas": {"x": 0.8125, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.00488281, "bottom": -0.152344, "right": 0.189453, "top": 0.8125}, | ||
| "advance": 0.265137 | ||
| }, | ||
| "{": { | ||
| "atlas": {"x": 0.875, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.03125, "bottom": -0.178711, "right": 0.327637, "top": 0.779785}, | ||
| "advance": 0.338379 | ||
| }, | ||
| "}": { | ||
| "atlas": {"x": 0.9375, "y": 0.25, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.00976562, "bottom": -0.178711, "right": 0.306152, "top": 0.779785}, | ||
| "advance": 0.338379 | ||
| }, | ||
| "|": { | ||
| "atlas": {"x": 0, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0859375, "bottom": -0.131836, "right": 0.158691, "top": 0.710938}, | ||
| "advance": 0.244141 | ||
| }, | ||
| ";": { | ||
| "atlas": {"x": 0.0625, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0200195, "bottom": -0.14209, "right": 0.166016, "top": 0.533691}, | ||
| "advance": 0.211426 | ||
| }, | ||
| "'": { | ||
| "atlas": {"x": 0.125, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0507812, "bottom": 0.516602, "right": 0.124023, "top": 0.75}, | ||
| "advance": 0.174805 | ||
| }, | ||
| ":": { | ||
| "atlas": {"x": 0.1875, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0649414, "bottom": -0.00585938, "right": 0.177734, "top": 0.533691}, | ||
| "advance": 0.242188 | ||
| }, | ||
| "\"": { | ||
| "atlas": {"x": 0.25, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0668945, "bottom": 0.509277, "right": 0.267578, "top": 0.75}, | ||
| "advance": 0.320312 | ||
| }, | ||
| ",": { | ||
| "atlas": {"x": 0.3125, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0141602, "bottom": -0.14209, "right": 0.150879, "top": 0.107422}, | ||
| "advance": 0.196777 | ||
| }, | ||
| ".": { | ||
| "atlas": {"x": 0.375, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0703125, "bottom": -0.00585938, "right": 0.182617, "top": 0.102539}, | ||
| "advance": 0.263672 | ||
| }, | ||
| "/": { | ||
| "atlas": {"x": 0.4375, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.00927734, "bottom": -0.0610352, "right": 0.383301, "top": 0.710938}, | ||
| "advance": 0.412598 | ||
| }, | ||
| "<": { | ||
| "atlas": {"x": 0.5, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0351562, "bottom": 0.0957031, "right": 0.43457, "top": 0.536133}, | ||
| "advance": 0.508301 | ||
| }, | ||
| ">": { | ||
| "atlas": {"x": 0.5625, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.065918, "bottom": 0.0961914, "right": 0.48291, "top": 0.537109}, | ||
| "advance": 0.522949 | ||
| }, | ||
| "?": { | ||
| "atlas": {"x": 0.625, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0366211, "bottom": -0.00585938, "right": 0.433105, "top": 0.720703}, | ||
| "advance": 0.472656 | ||
| }, | ||
| "`": { | ||
| "atlas": {"x": 0.6875, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.027832, "bottom": 0.606445, "right": 0.231445, "top": 0.75}, | ||
| "advance": 0.309082 | ||
| }, | ||
| "~": { | ||
| "atlas": {"x": 0.75, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0639648, "bottom": 0.196777, "right": 0.616699, "top": 0.39209}, | ||
| "advance": 0.680176 | ||
| }, | ||
| "\\": { | ||
| "atlas": {"x": 0.8125, "y": 0.3125, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0200195, "bottom": -0.0610352, "right": 0.402832, "top": 0.710938}, | ||
| "advance": 0.410645 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
[nitpick] The generated JSON metadata file uses different numeric precision inconsistently. Some values like "distanceRange": 4 use integers, while glyph metrics use high-precision floats (e.g., "ascender": 0.927734). The atlas coordinates use normalized floats with varying precision. While functionally correct, consider standardizing the precision for consistency and file size optimization (e.g., 6 decimal places for all normalized values).
| #include <fstream> | ||
| #include <nlohmann/json.hpp> | ||
|
|
||
| #define STB_IMAGE_IMPLEMENTATION |
There was a problem hiding this comment.
The STB_IMAGE_IMPLEMENTATION macro should only be defined once across the entire project to avoid linker errors due to duplicate symbol definitions. If stb_image is used elsewhere in the codebase, this will cause "multiple definition" errors during linking. Consider defining this in a dedicated .cpp file (e.g., stb_image.cpp) or ensuring it's only defined in one translation unit.
| #define STB_IMAGE_IMPLEMENTATION |
| if (currentY + config.glyphSize > config.atlasHeight) { | ||
| std::cerr << "WARNING: Atlas too small for all glyphs\n"; | ||
| break; | ||
| } |
There was a problem hiding this comment.
The atlas generator's warning "Atlas too small for all glyphs" (line 180) is logged but generation continues. This could result in missing glyphs with no clear indication in the output files. Consider either failing the generation with a clear error, or tracking which glyphs were skipped and reporting them in the JSON metadata or a separate log file.
| } else if (arg == "--size" && i + 1 < argc) { | ||
| config.atlasWidth = config.atlasHeight = std::stoi(argv[++i]); |
There was a problem hiding this comment.
Input validation missing: The --size command-line argument (line 61) directly converts to int with std::stoi without validation. Invalid inputs (negative numbers, non-numeric strings, values too large) will either throw an exception or produce nonsensical atlas sizes. Add validation to ensure the size is positive and reasonable (e.g., between 64 and 4096).
| // Parse atlas metadata | ||
| m_atlasMetadata.distanceRange = json["atlas"]["distanceRange"].get<float>(); | ||
| m_atlasMetadata.glyphSize = json["atlas"]["size"].get<int>(); | ||
| m_atlasMetadata.atlasWidth = json["atlas"]["width"].get<int>(); | ||
| m_atlasMetadata.atlasHeight = json["atlas"]["height"].get<int>(); | ||
| m_atlasMetadata.emSize = json["metrics"]["emSize"].get<float>(); | ||
| m_atlasMetadata.ascender = json["metrics"]["ascender"].get<float>(); | ||
| m_atlasMetadata.descender = json["metrics"]["descender"].get<float>(); | ||
| m_atlasMetadata.lineHeight = json["metrics"]["lineHeight"].get<float>(); |
There was a problem hiding this comment.
Missing error handling: The LoadSDFAtlas function uses array-style indexing on JSON objects (e.g., json["atlas"]["distanceRange"]) without checking if these keys exist. If the JSON file is malformed or missing expected fields, this will throw an exception that isn't caught, potentially crashing the application. Consider using .contains() or .value() with defaults, or wrap in a try-catch block with more specific error messages.
|
|
||
| int atlasWidth = 512; | ||
| int atlasHeight = 512; | ||
| double pixelRange = 4.0; // Distance field range in pixels |
There was a problem hiding this comment.
[nitpick] Magic number without explanation: The pixelRange is hardcoded to 4.0 with only a brief comment "Distance field range in pixels". This value critically affects the quality and appearance of the SDF text rendering. Consider adding more detailed documentation explaining why 4.0 was chosen, what happens with different values, and whether this should be configurable.
Implement command queue and two-pass rendering for proper text z-ordering.
Text now renders in front of shapes as expected.
**TextBatchRenderer Integration:**
- Added TextBatchRenderer accessors to Primitives API
- Text shapes now batch draw calls instead of immediate rendering
- LayerManager flushes text batch after rendering shapes
**RenderContext for Z-Index:**
- Created thread-local RenderContext for passing z-index
- LayerManager sets z-index before rendering each node
- Text shapes read z-index for proper layering
**Build System:**
- Added explicit LLVM libc++ library paths for macOS
- Added CoreFoundation framework linkage for httplib
- Fixed ABI mismatch between LLVM and vcpkg packages
Files changed:
- libs/ui/core/render_context.{h,cpp} (new)
- libs/ui/font/text_batch_renderer.{h,cpp} (new)
- libs/renderer/primitives/primitives.{h,cpp}
- libs/ui/shapes/shapes.cpp
- libs/ui/layer/layer_manager.cpp
- CMakeLists.txt
- libs/foundation/CMakeLists.txt
Fixed critical bug where glyphs were horizontally compressed, making narrow letters like 'I' and 'i' barely visible (2-3 pixels wide). Root Cause: - Atlas generator allocated 32×32 pixel cells but glyphs varied in size - Renderer sampled full cell UV coordinates, stretching narrow glyphs - Example: 'I' is ~3 pixels wide but was stretched across 32 pixels Solution: Following msdf-atlas-gen conventions, added atlasBounds to distinguish: - atlas: Full allocated cell (32×32 pixels) - atlasBounds: Actual glyph content (e.g., 3×22 pixels for 'I') - plane: Glyph positioning in EM units Changes: - tools/generate_sdf_atlas/main.cpp: Calculate and export atlasBounds - libs/ui/font/font_renderer.h: Add atlasBounds fields to SDFGlyph - libs/ui/font/font_renderer.cpp: Load atlasBounds from JSON, use for UVs - apps/ui-sandbox/scenes/sdf_minimal_scene.cpp: Test scene for validation - apps/ui-sandbox/CMakeLists.txt: Register sdf_minimal scene - docs/development-log.md: Detailed technical documentation - docs/status.md: Mark SDF rendering phases 1-4 complete References: - Chlumsky/msdf-atlas-gen#2 - Chlumsky/msdf-atlas-gen#17 - Chlumsky/msdf-atlas-gen#47
Fixes horizontal mirroring and adds necessary shader features for SDF text rendering: - Add SetUniform overloads for int and float to Shader class (needed for texture samplers) - Fix UV coordinate mapping in TextBatchRenderer to resolve horizontal mirroring - Add projection matrix storage in TextBatchRenderer - Integrate TextBatchRenderer into button_scene and text_shapes_scene - Add comprehensive debug log documenting the investigation process The UV fix swaps X-coordinates (uvMin.x ↔ uvMax.x) in quad vertex generation to correct the horizontal mirroring issue where text appeared backwards.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… quality - Use smoothstep instead of clamp in MSDF shader for proper anti-aliasing - Add comprehensive error handling for JSON parsing in LoadSDFAtlas - Use RAII pattern for stbi_load to prevent memory leaks - Add validation for required JSON fields and glyph data Addresses Copilot PR review comments about critical issues.
The newer baseline caused msdfgen API breakage. Reverting to the working baseline.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #include <vector> | ||
| #include <string> | ||
| #include <cmath> | ||
| #include <algorithm> |
There was a problem hiding this comment.
The #define STB_IMAGE_IMPLEMENTATION directive appears in font_renderer.cpp as well (line 11). This can cause linker errors if both files are included in the same compilation unit. Consider moving STB image implementation to a dedicated source file to avoid potential ODR violations.
| } | ||
|
|
||
| // Use RAII to ensure imageData is freed even if OpenGL operations fail | ||
| std::unique_ptr<unsigned char, decltype(&stbi_image_free)> imageData(imageDataRaw, stbi_image_free); |
There was a problem hiding this comment.
[nitpick] The imageData unique_ptr uses a custom deleter (stbi_image_free) which is correct. However, consider using std::unique_ptr<unsigned char[], void(*)(void*)> for array semantics, or create a type alias for clarity:
using StbImagePtr = std::unique_ptr<unsigned char, decltype(&stbi_image_free)>;
StbImagePtr imageData(imageDataRaw, stbi_image_free);| LOG_INFO(UI, "SetProjectionMatrix called"); | ||
| LOG_INFO(UI, " Matrix[0]: %.2f, %.2f, %.2f, %.2f", | ||
| projection[0][0], projection[0][1], projection[0][2], projection[0][3]); | ||
| LOG_INFO(UI, " Matrix[1]: %.2f, %.2f, %.2f, %.2f", | ||
| projection[1][0], projection[1][1], projection[1][2], projection[1][3]); | ||
| LOG_INFO(UI, " Matrix[2]: %.2f, %.2f, %.2f, %.2f", | ||
| projection[2][0], projection[2][1], projection[2][2], projection[2][3]); | ||
| LOG_INFO(UI, " Matrix[3]: %.2f, %.2f, %.2f, %.2f", | ||
| projection[3][0], projection[3][1], projection[3][2], projection[3][3]); | ||
|
|
||
| // Store projection matrix for use in Flush() | ||
| m_projection = projection; | ||
| } | ||
|
|
||
| void TextBatchRenderer::AddText( | ||
| const std::string& text, | ||
| const glm::vec2& position, | ||
| float scale, | ||
| const glm::vec4& color, | ||
| float zIndex | ||
| ) { | ||
| if (!m_fontRenderer) { | ||
| LOG_WARNING(UI, "AddText called but FontRenderer not set"); | ||
| return; | ||
| } | ||
|
|
||
| LOG_INFO(UI, "AddText called: text='%s', pos=(%.1f,%.1f), scale=%.2f", | ||
| text.c_str(), position.x, position.y, scale); | ||
|
|
||
| // Create command with generated glyphs | ||
| TextCommand cmd; | ||
| cmd.zIndex = zIndex; | ||
|
|
||
| // Generate glyph quads using FontRenderer | ||
| m_fontRenderer->GenerateGlyphQuads(text, position, scale, color, cmd.glyphs); | ||
|
|
||
| LOG_INFO(UI, " Generated %zu glyphs for text", cmd.glyphs.size()); | ||
|
|
||
| // Only add if we generated glyphs | ||
| if (!cmd.glyphs.empty()) { | ||
| m_commands.push_back(std::move(cmd)); | ||
| } | ||
| } | ||
|
|
||
| void TextBatchRenderer::Flush() { | ||
| if (m_commands.empty()) { | ||
| LOG_INFO(UI, "Flush called but no commands to render"); | ||
| return; // Nothing to render | ||
| } | ||
|
|
||
| LOG_INFO(UI, "Flush: Rendering %zu text commands", m_commands.size()); | ||
|
|
||
| // Sort commands by z-index (back to front) | ||
| std::stable_sort(m_commands.begin(), m_commands.end(), [](const TextCommand& a, const TextCommand& b) { | ||
| return a.zIndex < b.zIndex; | ||
| }); | ||
|
|
||
| // Build vertex and index buffers from sorted commands | ||
| m_vertices.clear(); | ||
| m_indices.clear(); | ||
|
|
||
| unsigned int vertexOffset = 0; | ||
|
|
||
| for (const auto& cmd : m_commands) { | ||
| for (const auto& glyph : cmd.glyphs) { | ||
| // Check if we have room for this quad (4 vertices, 6 indices) | ||
| if (m_vertices.size() + 4 > kMaxVertices || m_indices.size() + 6 > kMaxIndices) { | ||
| LOG_WARNING(UI, "TextBatchRenderer vertex/index buffer full, flushing partial batch"); | ||
| break; | ||
| } | ||
|
|
||
| // Create 4 vertices for this glyph quad | ||
| // Both U and V flipped to correct mirroring and upside-down text | ||
| // Top-left | ||
| m_vertices.push_back( | ||
| Vertex{ | ||
| .position = glyph.position, | ||
| .texCoord = glm::vec2(glyph.uvMin.x, glyph.uvMax.y), | ||
| .color = glyph.color | ||
| } | ||
| ); | ||
|
|
||
| // Top-right | ||
| m_vertices.push_back( | ||
| Vertex{ | ||
| .position = glm::vec2(glyph.position.x + glyph.size.x, glyph.position.y), | ||
| .texCoord = glm::vec2(glyph.uvMax.x, glyph.uvMax.y), | ||
| .color = glyph.color | ||
| } | ||
| ); | ||
|
|
||
| // Bottom-right | ||
| m_vertices.push_back( | ||
| Vertex{ | ||
| .position = glm::vec2(glyph.position.x + glyph.size.x, glyph.position.y + glyph.size.y), | ||
| .texCoord = glm::vec2(glyph.uvMax.x, glyph.uvMin.y), | ||
| .color = glyph.color | ||
| } | ||
| ); | ||
|
|
||
| // Bottom-left | ||
| m_vertices.push_back( | ||
| Vertex{ | ||
| .position = glm::vec2(glyph.position.x, glyph.position.y + glyph.size.y), | ||
| .texCoord = glm::vec2(glyph.uvMin.x, glyph.uvMin.y), | ||
| .color = glyph.color | ||
| } | ||
| ); | ||
|
|
||
| // Create indices for two triangles (0,1,2) and (0,2,3) | ||
| m_indices.push_back(vertexOffset + 0); | ||
| m_indices.push_back(vertexOffset + 1); | ||
| m_indices.push_back(vertexOffset + 2); | ||
|
|
||
| m_indices.push_back(vertexOffset + 0); | ||
| m_indices.push_back(vertexOffset + 2); | ||
| m_indices.push_back(vertexOffset + 3); | ||
|
|
||
| vertexOffset += 4; | ||
| } | ||
| } | ||
|
|
||
| // Upload and render | ||
| if (!m_vertices.empty() && !m_indices.empty()) { | ||
| // Check GL state before rendering | ||
| GLint viewport[4]; | ||
| glGetIntegerv(GL_VIEWPORT, viewport); | ||
| LOG_INFO(UI, " Viewport: x=%d, y=%d, w=%d, h=%d", viewport[0], viewport[1], viewport[2], viewport[3]); | ||
|
|
||
| GLboolean scissorEnabled; | ||
| glGetBooleanv(GL_SCISSOR_TEST, &scissorEnabled); | ||
| LOG_INFO(UI, " Scissor test: %s", scissorEnabled ? "ENABLED" : "disabled"); | ||
|
|
||
| // Log first vertex to verify data | ||
| if (!m_vertices.empty()) { | ||
| const auto& v = m_vertices[0]; | ||
| LOG_INFO(UI, " First vertex: pos=(%.2f, %.2f), uv=(%.4f, %.4f), color=(%.2f,%.2f,%.2f,%.2f)", | ||
| v.position.x, v.position.y, v.texCoord.x, v.texCoord.y, | ||
| v.color.r, v.color.g, v.color.b, v.color.a); | ||
| } | ||
|
|
||
| // Disable face culling (our quads are clockwise in screen space) | ||
| glDisable(GL_CULL_FACE); | ||
|
|
||
| // Disable depth testing for 2D text | ||
| glDisable(GL_DEPTH_TEST); | ||
|
|
||
| // Enable blending for text transparency | ||
| glEnable(GL_BLEND); | ||
| glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | ||
|
|
||
| LOG_INFO(UI, " About to use shader and bind VAO"); | ||
| m_shader.Use(); | ||
|
|
||
| // Verify shader is bound | ||
| GLint currentProgram = 0; | ||
| glGetIntegerv(GL_CURRENT_PROGRAM, ¤tProgram); | ||
| LOG_INFO(UI, " Current shader program: %d", currentProgram); | ||
|
|
||
| // Check for errors after shader use | ||
| GLenum err1 = glGetError(); | ||
| if (err1 != GL_NO_ERROR) { | ||
| LOG_ERROR(UI, "OpenGL error after shader.Use(): 0x%X", err1); | ||
| } | ||
|
|
||
| // Upload projection matrix | ||
| m_shader.SetUniform("projection", m_projection); | ||
|
|
||
| // Bind atlas texture | ||
| glActiveTexture(GL_TEXTURE0); | ||
| glBindTexture(GL_TEXTURE_2D, m_fontRenderer->GetAtlasTexture()); | ||
| m_shader.SetUniform("msdfAtlas", 0); | ||
|
|
||
| // Set pixel range uniform (must match the value used when generating the SDF atlas) | ||
| // Atlas generation used pixelRange=4.0 (from AtlasConfig) | ||
| m_shader.SetUniform("pixelRange", 4.0f); | ||
|
|
||
| // Check for errors after setting uniforms | ||
| GLenum err2 = glGetError(); | ||
| if (err2 != GL_NO_ERROR) { | ||
| LOG_ERROR(UI, "OpenGL error after setting uniforms: 0x%X", err2); | ||
| } | ||
|
|
||
| // Upload vertex data | ||
| glBindVertexArray(m_vao); | ||
|
|
||
| // Check for errors after VAO bind | ||
| GLenum err3 = glGetError(); | ||
| if (err3 != GL_NO_ERROR) { | ||
| LOG_ERROR(UI, "OpenGL error after VAO bind: 0x%X", err3); | ||
| } | ||
|
|
||
| glBindBuffer(GL_ARRAY_BUFFER, m_vbo); | ||
| glBufferSubData(GL_ARRAY_BUFFER, 0, m_vertices.size() * sizeof(Vertex), m_vertices.data()); | ||
|
|
||
| // Upload index data | ||
| glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo); | ||
| glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, m_indices.size() * sizeof(unsigned int), m_indices.data()); | ||
|
|
||
| // Check for errors after buffer uploads | ||
| GLenum err4 = glGetError(); | ||
| if (err4 != GL_NO_ERROR) { | ||
| LOG_ERROR(UI, "OpenGL error after buffer uploads: 0x%X", err4); | ||
| } | ||
|
|
||
| LOG_INFO(UI, " Drawing: %zu vertices, %zu indices, texture=%u", | ||
| m_vertices.size(), m_indices.size(), m_fontRenderer->GetAtlasTexture()); | ||
|
|
||
| // Draw | ||
| glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(m_indices.size()), GL_UNSIGNED_INT, 0); | ||
|
|
||
| // Check for OpenGL errors | ||
| GLenum err = glGetError(); | ||
| if (err != GL_NO_ERROR) { | ||
| LOG_ERROR(UI, "OpenGL error after draw: 0x%X", err); | ||
| } | ||
|
|
||
| // Cleanup | ||
| glBindVertexArray(0); | ||
| glBindTexture(GL_TEXTURE_2D, 0); | ||
| glDisable(GL_BLEND); | ||
| } | ||
|
|
||
| // Clear for next frame | ||
| Clear(); |
There was a problem hiding this comment.
Excessive logging in hot path. Lines 88-96, 114-115, 124, 134, 138, and throughout Flush() use LOG_INFO for every call. This will spam logs during normal operation. Consider using LOG_DEBUG or removing these logs after debugging is complete.
Specify exact versions available in the baseline to avoid version mismatch errors in CI.
…s size - Set m_firstGlyphTexture to first texture created in LoadFont() - Add validation for --size argument (must be 64-4096 pixels) - Add error handling for invalid size values Addresses PR feedback about uninitialized member and missing input validation.
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| jsonFile << "\"height\": " << normH << "},\n"; | ||
|
|
||
| // Atlas bounds (normalized 0-1) - actual glyph content within the cell | ||
| // Reference: https://github.com/Chlumsky/msdf-atlas-gen/issues/2 | ||
| // These are the pixel coordinates where the actual glyph is rendered |
There was a problem hiding this comment.
The atlasBounds fields are added but there's a missing NULL check in the JSON export. If a glyph doesn't have atlasBounds data (whitespace characters), the JSON export will still try to write these fields. Consider adding the same NULL check pattern used for atlas and plane fields on lines 371-373.
| #define STB_IMAGE_IMPLEMENTATION | ||
| #include <stb_image.h> |
There was a problem hiding this comment.
Using #define STB_IMAGE_IMPLEMENTATION in a header-included .cpp file can cause linker errors if this file is included in multiple translation units. Consider moving this to a dedicated .cpp file or wrapping it with an include guard check.
The standard practice is:
// In a single .cpp file (e.g., stb_image.cpp)
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
// In all other files, just include without the define
#include <stb_image.h>| { "name": "glm", "version": "1.0.1", "port-version": 3 }, | ||
| { "name": "gtest", "version": "1.17.0", "port-version": 1 }, | ||
| { "name": "libpng", "version": "1.6.47" }, | ||
| { "name": "msdfgen", "version": "1.12" }, |
There was a problem hiding this comment.
The msdfgen version override specifies "1.12" but the PR description mentions using "1.12.1". Version mismatches between documentation and configuration can lead to confusion. Consider updating the override to match the actual version used, or update the documentation to reflect "1.12".
Use std::get_if instead of std::get to safely handle variant access. Adds error logging if layer doesn't contain expected UI::Text type. Addresses PR feedback about memory safety.
Fixes compile error: no template named 'vector' in namespace 'std'
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Create dedicated stb_image.cpp to avoid potential ODR violations - Move STB_IMAGE_IMPLEMENTATION to separate compilation unit - Reduce excessive logging in TextBatchRenderer hot paths - Change LOG_INFO to LOG_DEBUG in SetProjectionMatrix, AddText, and Flush - Prevents log spam during normal rendering operations
Changed %s to %u for uint32_t layer ID
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| // Create 4 vertices for this glyph quad | ||
| // Both U and V flipped to correct mirroring and upside-down text |
There was a problem hiding this comment.
The comment says "Both U and V flipped" but the code only flips V (Y-axis). The U coordinates use uvMin.x and uvMax.x correctly without flipping. The comment should say "V flipped to correct upside-down text" or "Y texture coordinates inverted for OpenGL coordinate system".
| "atlas": {"x": 0, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0141602, "bottom": 0, "right": 0.639648, "top": 0.710938}, | ||
| "advance": 0.652344 | ||
| }, | ||
| "B": { | ||
| "atlas": {"x": 0.0625, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.566406, "top": 0.710938}, | ||
| "advance": 0.623047 | ||
| }, | ||
| "C": { | ||
| "atlas": {"x": 0.125, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0585938, "bottom": -0.00976562, "right": 0.605469, "top": 0.720703}, | ||
| "advance": 0.650879 | ||
| }, | ||
| "D": { | ||
| "atlas": {"x": 0.1875, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.597168, "top": 0.710938}, | ||
| "advance": 0.65625 | ||
| }, | ||
| "E": { | ||
| "atlas": {"x": 0.25, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.53418, "top": 0.710938}, | ||
| "advance": 0.568359 | ||
| }, | ||
| "F": { | ||
| "atlas": {"x": 0.3125, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.522949, "top": 0.710938}, | ||
| "advance": 0.552734 | ||
| }, | ||
| "G": { | ||
| "atlas": {"x": 0.375, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0595703, "bottom": -0.00976562, "right": 0.60791, "top": 0.720703}, | ||
| "advance": 0.681152 | ||
| }, | ||
| "H": { | ||
| "atlas": {"x": 0.4375, "y": 0, "width": 0.0625, "height": 0.0625}, | ||
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.628906, "top": 0.710938}, | ||
| "advance": 0.713379 | ||
| }, | ||
| "I": { | ||
| "atlas": {"x": 0.5, "y": 0, "width": 0.0625, "height": 0.0625}, |
There was a problem hiding this comment.
Missing atlasBounds field in the generated JSON. According to the development log and the loader code in font_renderer.cpp (lines 387-396), the generator should export atlasBounds to prevent horizontal squishing of narrow glyphs. The current output only includes atlas and plane, but the fix described in development-log.md (2025-11-20 entry) requires atlasBounds to be included.
| "atlas": {"x": 0, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0141602, "bottom": 0, "right": 0.639648, "top": 0.710938}, | |
| "advance": 0.652344 | |
| }, | |
| "B": { | |
| "atlas": {"x": 0.0625, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.566406, "top": 0.710938}, | |
| "advance": 0.623047 | |
| }, | |
| "C": { | |
| "atlas": {"x": 0.125, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0585938, "bottom": -0.00976562, "right": 0.605469, "top": 0.720703}, | |
| "advance": 0.650879 | |
| }, | |
| "D": { | |
| "atlas": {"x": 0.1875, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.597168, "top": 0.710938}, | |
| "advance": 0.65625 | |
| }, | |
| "E": { | |
| "atlas": {"x": 0.25, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.53418, "top": 0.710938}, | |
| "advance": 0.568359 | |
| }, | |
| "F": { | |
| "atlas": {"x": 0.3125, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.522949, "top": 0.710938}, | |
| "advance": 0.552734 | |
| }, | |
| "G": { | |
| "atlas": {"x": 0.375, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0595703, "bottom": -0.00976562, "right": 0.60791, "top": 0.720703}, | |
| "advance": 0.681152 | |
| }, | |
| "H": { | |
| "atlas": {"x": 0.4375, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.628906, "top": 0.710938}, | |
| "advance": 0.713379 | |
| }, | |
| "I": { | |
| "atlas": {"x": 0.5, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlas": {"x": 0, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0141602, "bottom": 0, "right": 0.639648, "top": 0.710938}, | |
| "advance": 0.652344 | |
| }, | |
| "B": { | |
| "atlas": {"x": 0.0625, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.0625, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.566406, "top": 0.710938}, | |
| "advance": 0.623047 | |
| }, | |
| "C": { | |
| "atlas": {"x": 0.125, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.125, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0585938, "bottom": -0.00976562, "right": 0.605469, "top": 0.720703}, | |
| "advance": 0.650879 | |
| }, | |
| "D": { | |
| "atlas": {"x": 0.1875, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.1875, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.597168, "top": 0.710938}, | |
| "advance": 0.65625 | |
| }, | |
| "E": { | |
| "atlas": {"x": 0.25, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.25, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.53418, "top": 0.710938}, | |
| "advance": 0.568359 | |
| }, | |
| "F": { | |
| "atlas": {"x": 0.3125, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.3125, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.522949, "top": 0.710938}, | |
| "advance": 0.552734 | |
| }, | |
| "G": { | |
| "atlas": {"x": 0.375, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.375, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0595703, "bottom": -0.00976562, "right": 0.60791, "top": 0.720703}, | |
| "advance": 0.681152 | |
| }, | |
| "H": { | |
| "atlas": {"x": 0.4375, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.4375, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "plane": {"left": 0.0825195, "bottom": 0, "right": 0.628906, "top": 0.710938}, | |
| "advance": 0.713379 | |
| }, | |
| "I": { | |
| "atlas": {"x": 0.5, "y": 0, "width": 0.0625, "height": 0.0625}, | |
| "atlasBounds": {"x": 0.5, "y": 0, "width": 0.0625, "height": 0.0625}, |
| if (!fontLoaded) { | ||
| LOG_ERROR(UI, "FATAL ERROR: Failed to load any font"); | ||
| FT_Done_FreeType(m_library); | ||
| m_library = nullptr; | ||
| std::exit(1); | ||
| } |
There was a problem hiding this comment.
[nitpick] The fallback condition only checks emSize == 1.0 but doesn't validate other critical metrics. If ascender or descender are zero or invalid, text positioning will fail. Consider validating all critical metrics or documenting that only emSize needs fallback handling.
- Fix misleading comment in text_batch_renderer.cpp Comment now correctly states only V (Y) coords are flipped, not both U and V - Regenerate Roboto-SDF.json to include atlasBounds fields Prevents horizontal squishing of narrow glyphs as documented in development log
- Use kItemGap constant instead of magic 4.0F for mood bar offset (#24) - Add kMoodLabelFontSize class constant (was local 11.0F) (#28) - Add comment clarifying kHeaderMoodBarHeight differs from kCompactHeight (#23) - Apply reserve/append pattern for mood text in renderTwoColumnLayout (#27) - Apply reserve/append pattern for text in renderTextSlot (#21) - Apply reserve/append pattern for mood text in updateValues (#25)
Summary
Implement multi-channel signed distance field (MSDF) text rendering foundation to fix z-ordering issues where button text renders behind backgrounds.
This PR completes Phases 1-3 of the SDF text rendering implementation. Phase 4 (batched rendering integration with LayerManager) is in progress and will be completed in this branch.
Phase 1: SDF Atlas Generation Tool ✅
tools/generate_sdf_atlasexecutable using msdfgen 1.12.1 APIfonts/Roboto-SDF.json- Metadata with glyph metrics and UV coordinates (94 glyphs)fonts/Roboto-SDF.png- 512x512 atlas texture (gitignored, regenerate with generator tool)Phase 2: FontRenderer SDF Support ✅
LoadSDFAtlas()method loads PNG texture and JSON metadataGenerateGlyphQuads()creates glyph quads for batched renderingSDFGlyphandSDFAtlasMetadatatypesPhase 3: SDF Shaders ✅
msdf_text.vert): Per-vertex color and texture coordinate handlingmsdf_text.frag): Median-of-three MSDF algorithm with screen-space derivatives for quality scalingTechnical Details
Build System
vcpkg.jsonwith msdfgen extensions featuretools/subdirectory to CMakeCC=/usr/local/opt/llvm/bin/clang CXX=/usr/local/opt/llvm/bin/clang++Dependencies
Documentation
/docs/technical/ui-framework/sdf-text-rendering.md- Complete technical design/docs/technical/ui-framework/batched-text-rendering.md- Updated with implementation planRemaining Work (Phase 4)
Still in progress on this branch:
Test Plan
Build & Run Atlas Generator:
Verify Compilation:
All code compiles successfully. Integration testing will be completed in Phase 4.