Skip to content

feat: SDF text rendering foundation (Phases 1-3) - #27

Merged
kcjonson merged 16 commits into
mainfrom
feature/sdf-text-rendering
Nov 20, 2025
Merged

kcjonson merged 16 commits into
mainfrom
feature/sdf-text-rendering

Conversation

@kcjonson

Copy link
Copy Markdown
Owner

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 ✅

  • Build Configuration: Fixed vcpkg msdfgen build by switching to Homebrew LLVM (Apple Clang 17 had libc++ compatibility issues)
  • Atlas Generator: Created tools/generate_sdf_atlas executable using msdfgen 1.12.1 API
  • Generated Assets:
    • fonts/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 ✅

  • Atlas Loading: LoadSDFAtlas() method loads PNG texture and JSON metadata
  • Batch Support: GenerateGlyphQuads() creates glyph quads for batched rendering
  • Data Structures: Added SDFGlyph and SDFAtlasMetadata types
  • Dual Mode: Supports both legacy FreeType and new SDF atlas rendering

Phase 3: SDF Shaders ✅

  • Vertex Shader (msdf_text.vert): Per-vertex color and texture coordinate handling
  • Fragment Shader (msdf_text.frag): Median-of-three MSDF algorithm with screen-space derivatives for quality scaling

Technical Details

Build System

  • Updated vcpkg.json with msdfgen extensions feature
  • Added tools/ subdirectory to CMake
  • Build requires: CC=/usr/local/opt/llvm/bin/clang CXX=/usr/local/opt/llvm/bin/clang++

Dependencies

  • msdfgen 1.12.1 (from vcpkg)
  • nlohmann-json (for metadata parsing)
  • stb_image (for PNG loading)

Documentation

  • /docs/technical/ui-framework/sdf-text-rendering.md - Complete technical design
  • /docs/technical/ui-framework/batched-text-rendering.md - Updated with implementation plan

Remaining Work (Phase 4)

Still in progress on this branch:

  • Create TextBatchRenderer class
  • Integrate with LayerManager z-ordering
  • Update Button component to use batched text rendering
  • Testing and validation

Test Plan

Build & Run Atlas Generator:

# Build with Homebrew LLVM
CC=/usr/local/opt/llvm/bin/clang CXX=/usr/local/opt/llvm/bin/clang++ cmake --build build -j8

# Generate atlas (creates fonts/Roboto-SDF.png and fonts/Roboto-SDF.json)
./build/tools/generate_sdf_atlas/generate_sdf_atlas

Verify Compilation:

CC=/usr/local/opt/llvm/bin/clang CXX=/usr/local/opt/llvm/bin/clang++ cmake --build build --target ui -j8

All code compiles successfully. Integration testing will be completed in Phase 4.

…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.
Copilot AI review requested due to automatic review settings November 19, 2025 18:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +158 to +184
// 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;
}
}
}

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copilot uses AI. Check for mistakes.
.fontSize = style.fontSize,
.hAlign = Foundation::HorizontalAlign::Left,
.vAlign = Foundation::VerticalAlign::Top},
.zIndex = zIndex + 0.1F, // Slightly above button background

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
.zIndex = zIndex + 0.1F, // Slightly above button background
.zIndex = zIndex + 1, // Above button background

Copilot uses AI. Check for mistakes.
Comment on lines +293 to +308
// 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();

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread fonts/Roboto-SDF.json
Comment on lines +1 to +492
{
"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
}
}
}

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copilot uses AI. Check for mistakes.
Comment thread libs/ui/font/font_renderer.cpp Outdated
#include <fstream>
#include <nlohmann/json.hpp>

#define STB_IMAGE_IMPLEMENTATION

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#define STB_IMAGE_IMPLEMENTATION

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +182
if (currentY + config.glyphSize > config.atlasHeight) {
std::cerr << "WARNING: Atlas too small for all glyphs\n";
break;
}

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread tools/generate_sdf_atlas/main.cpp Outdated
Comment on lines +60 to +61
} else if (arg == "--size" && i + 1 < argc) {
config.atlasWidth = config.atlasHeight = std::stoi(argv[++i]);

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread libs/ui/font/font_renderer.cpp Outdated
Comment on lines +306 to +314
// 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>();

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

int atlasWidth = 512;
int atlasHeight = 512;
double pixelRange = 4.0; // Distance field range in pixels

Copilot AI Nov 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copilot uses AI. Check for mistakes.
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.
Copilot AI review requested due to automatic review settings November 20, 2025 19:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copilot AI review requested due to automatic review settings November 20, 2025 19:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

// 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);

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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);

Copilot uses AI. Check for mistakes.
Comment thread libs/ui/font/text_batch_renderer.cpp Outdated
Comment on lines +88 to +312
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, &currentProgram);
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();

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Specify exact versions available in the baseline to avoid version mismatch errors in CI.
Copilot AI review requested due to automatic review settings November 20, 2025 19:34
…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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +358 to +362
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

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread libs/ui/font/font_renderer.cpp Outdated
Comment on lines +11 to +12
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copilot uses AI. Check for mistakes.
Comment thread vcpkg.json
{ "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" },

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copilot uses AI. Check for mistakes.
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'
Copilot AI review requested due to automatic review settings November 20, 2025 19:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copilot AI review requested due to automatic review settings November 20, 2025 20:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread libs/ui/font/text_batch_renderer.cpp Outdated
}

// Create 4 vertices for this glyph quad
// Both U and V flipped to correct mirroring and upside-down text

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copilot uses AI. Check for mistakes.
Comment thread fonts/Roboto-SDF.json
Comment on lines +17 to +57
"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},

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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},

Copilot uses AI. Check for mistakes.
Comment on lines +68 to 73
if (!fontLoaded) {
LOG_ERROR(UI, "FATAL ERROR: Failed to load any font");
FT_Done_FreeType(m_library);
m_library = nullptr;
std::exit(1);
}

Copilot AI Nov 20, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copilot uses AI. Check for mistakes.
- 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
@kcjonson
kcjonson merged commit 56d78b3 into main Nov 20, 2025
2 of 3 checks passed
kcjonson added a commit that referenced this pull request Dec 28, 2025
- 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants