Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 27, 2025

Per-pixel color conversion (e.g., Color.Color16bppRgb565) is the real bottleneck in drawing operations, not bounds checking or rotation calculations. This caches the converted pen color in native buffer format to eliminate redundant conversions when drawing many pixels with the same color.

Changes

MicroGraphics.cs

  • Add cached pen color fields (_penColor16bpp, _penColor8bpp, _penColor1bpp) with lazy initialization
  • Invalidate cache on PenColor setter
  • Add DrawPixelWithCachedColor() that bypasses color conversion for known buffer types
  • Update hot paths to use cached color when color == PenColor:
    • DrawSingleWidthLine() (Bresenham)
    • DrawCircleOutline() (8-way symmetry)
    • DrawBitmap() (text rendering)

Buffer classes

Add native line drawing methods with bounds checking:

  • BufferRgb565: DrawHorizontalLine/DrawVerticalLine(x, y, length, ushort color)
  • BufferRgb332: DrawHorizontalLine/DrawVerticalLine(x, y, length, byte color)
  • Buffer1bpp: DrawHorizontalLine/DrawVerticalLine(x, y, length, bool enabled)

Example

// Before: color converted on EVERY pixel
for (var x = x0; x <= x1; x++) {
    DrawPixel(x, y, color);  // color.Color16bppRgb565 called each time
}

// After: conversion cached, native SetPixel called
EnsurePenColorCached();  // Convert once
for (var x = x0; x <= x1; x++) {
    buf565.SetPixel(rx, ry, _penColor16bpp);  // No conversion
}

Expected Impact

  • Lines/circles: +20-35%
  • Text rendering: +25-40%
  • Minimal impact on gradient fills (colors change per line anyway)
Original prompt

Summary

Improve MicroGraphics drawing performance by optimizing at the buffer level. Previous attempts to optimize at the MicroGraphics layer showed no measurable improvement - the real bottleneck is in the per-pixel color conversion and buffer access patterns.

Problem Analysis

Benchmarks show that optimizations to bounds checking and rotation calculations in MicroGraphics had zero impact on performance. The actual bottlenecks are:

1. Per-Pixel Color Conversion

Every SetPixel(x, y, Color color) call triggers a color format conversion:

// BufferRgb565.cs line 89-92
public override void SetPixel(int x, int y, Color color)
{
    SetPixel(x, y, color.Color16bppRgb565);  // Conversion on EVERY pixel!
}

The Color16bppRgb565 property performs bit manipulation every time:

// Color.cs
public readonly ushort Color16bppRgb565 => (ushort)(((R & 0b11111000) << 8) | ((G & 0b11111100) << 3) | (B >> 3));

2. No Native-Format Drawing Methods in MicroGraphics

MicroGraphics always works with Color objects, forcing conversion even when drawing many pixels with the same color (lines, fills, text).

Proposed Solution

1. Add Native Color Caching to MicroGraphics

Cache the converted pen color in the native buffer format to avoid repeated conversions:

// In MicroGraphics.cs
private ushort _penColor16bpp;
private byte _penColor8bpp;
private bool _penColor1bpp;
private bool _penColorCacheValid = false;

public Color PenColor 
{ 
    get => _penColor;
    set 
    { 
        _penColor = value;
        _penColorCacheValid = false;  // Invalidate cache
    }
}

private void EnsurePenColorCached()
{
    if (!_penColorCacheValid)
    {
        _penColor16bpp = _penColor.Color16bppRgb565;
        _penColor8bpp = _penColor.Color8bppRgb332;
        _penColor1bpp = _penColor.Color1bpp;
        _penColorCacheValid = true;
    }
}

2. Add Native-Format SetPixel Methods to Buffer Classes

Add methods that accept pre-converted colors to skip conversion:

// In BufferRgb565.cs - add alongside existing SetPixel
// The existing SetPixel(int x, int y, ushort color) already exists and is fast!
// We just need MicroGraphics to use it.

3. Add Native Drawing Path in MicroGraphics

For operations that draw many pixels with PenColor, use the cached native color:

// In MicroGraphics.cs
private void DrawPixelWithCachedColor(int x, int y)
{
    if (IgnoreOutOfBoundsPixels && !IsCoordinateInBounds(x, y))
        return;
    
    int rx = GetXForRotation(x, y);
    int ry = GetYForRotation(x, y);
    
    // Use native format based on buffer type
    if (PixelBuffer is BufferRgb565 buf565)
    {
        EnsurePenColorCached();
        buf565.SetPixel(rx, ry, _penColor16bpp);
    }
    else if (PixelBuffer is BufferRgb332 buf332)
    {
        EnsurePenColorCached();
        buf332.SetPixel(rx, ry, _penColor8bpp);
    }
    else if (PixelBuffer is Buffer1bpp buf1)
    {
        EnsurePenColorCached();
        buf1.SetPixel(rx, ry, _penColor1bpp);
    }
    else
    {
        PixelBuffer.SetPixel(rx, ry, PenColor);
    }
}

4. Optimize DrawSingleWidthLine to Use Cached Color

private void DrawSingleWidthLine(int x0, int y0, int x1, int y1, Color color)
{
    // If drawing with PenColor, use the fast cached path
    bool useCachedPath = (color == PenColor);
    
    var steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0);
    if (steep) { Swap(ref x0, ref y0); Swap(ref x1, ref y1); }
    if (x0 > x1) { Swap(ref x0, ref x1); Swap(ref y0, ref y1); }
    
    var dx = x1 - x0;
    var dy = Math.Abs(y1 - y0);
    var error = dx >> 1;
    var ystep = y0 < y1 ? 1 : -1;
    var y = y0;

    if (useCachedPath)
    {
        for (var x = x0; x <= x1; x++)
        {
            DrawPixelWithCachedColor(steep ? y : x, steep ? x : y);
            error -= dy;
            if (error < 0) { y += ystep; error += dx; }
        }
    }
    else
    {
        for (var x = x0; x <= x1; x++)
        {
            DrawPixel(steep ? y : x, steep ? x : y, color);
            error -= dy;
            if (error < 0) { y += ystep; error += dx; }
        }
    }
}

5. Add Horizontal Line Drawing with Native Color

For filled shapes that use horizontal line fills:

// Add to BufferRgb565.cs
public unsafe void DrawHorizontalLine(int x, int y, int length, ushort color)
{
    if (length <= 0) return;
    
    fixed (byte* ptr = Buffer)
    {
        var pixelPtr = (ushort*)(ptr + ((y * Width + x) << 1));
        ushort swappedColor = (ushort)((color << 8) | (color >> 8));
        
        for (int i = 0; i < length; i++)
        {
            pixelPtr[i] = swappedColor;
        }
    }
}

Then in MicroGraphics, use it for horizontal lines when possible:

public void DrawHorizontalLine(int x, int y, int length, Color color)
{
    // ... bounds checking ...
    
    if (Rotation == RotationType.Default && PixelBuffer is BufferRgb565 buf5...

</details>

*This pull request was created as a result of the following prompt from Copilot chat.*
> ## Summary
> 
> Improve MicroGraphics drawing performance by optimizing at the buffer level. Previous attempts to optimize at the MicroGraphics layer showed no measurable improvement - the real bottleneck is in the per-pixel color conversion and buffer access patterns.
> 
> ## Problem Analysis
> 
> Benchmarks show that optimizations to bounds checking and rotation calculations in MicroGraphics had **zero impact** on performance. The actual bottlenecks are:
> 
> ### 1. Per-Pixel Color Conversion
> Every `SetPixel(x, y, Color color)` call triggers a color format conversion:
> 
> ```csharp
> // BufferRgb565.cs line 89-92
> public override void SetPixel(int x, int y, Color color)
> {
>     SetPixel(x, y, color.Color16bppRgb565);  // Conversion on EVERY pixel!
> }
> ```
> 
> The `Color16bppRgb565` property performs bit manipulation every time:
> ```csharp
> // Color.cs
> public readonly ushort Color16bppRgb565 => (ushort)(((R & 0b11111000) << 8) | ((G & 0b11111100) << 3) | (B >> 3));
> ```
> 
> ### 2. No Native-Format Drawing Methods in MicroGraphics
> MicroGraphics always works with `Color` objects, forcing conversion even when drawing many pixels with the same color (lines, fills, text).
> 
> ## Proposed Solution
> 
> ### 1. Add Native Color Caching to MicroGraphics
> 
> Cache the converted pen color in the native buffer format to avoid repeated conversions:
> 
> ```csharp
> // In MicroGraphics.cs
> private ushort _penColor16bpp;
> private byte _penColor8bpp;
> private bool _penColor1bpp;
> private bool _penColorCacheValid = false;
> 
> public Color PenColor 
> { 
>     get => _penColor;
>     set 
>     { 
>         _penColor = value;
>         _penColorCacheValid = false;  // Invalidate cache
>     }
> }
> 
> private void EnsurePenColorCached()
> {
>     if (!_penColorCacheValid)
>     {
>         _penColor16bpp = _penColor.Color16bppRgb565;
>         _penColor8bpp = _penColor.Color8bppRgb332;
>         _penColor1bpp = _penColor.Color1bpp;
>         _penColorCacheValid = true;
>     }
> }
> ```
> 
> ### 2. Add Native-Format SetPixel Methods to Buffer Classes
> 
> Add methods that accept pre-converted colors to skip conversion:
> 
> ```csharp
> // In BufferRgb565.cs - add alongside existing SetPixel
> // The existing SetPixel(int x, int y, ushort color) already exists and is fast!
> // We just need MicroGraphics to use it.
> ```
> 
> ### 3. Add Native Drawing Path in MicroGraphics
> 
> For operations that draw many pixels with PenColor, use the cached native color:
> 
> ```csharp
> // In MicroGraphics.cs
> private void DrawPixelWithCachedColor(int x, int y)
> {
>     if (IgnoreOutOfBoundsPixels && !IsCoordinateInBounds(x, y))
>         return;
>     
>     int rx = GetXForRotation(x, y);
>     int ry = GetYForRotation(x, y);
>     
>     // Use native format based on buffer type
>     if (PixelBuffer is BufferRgb565 buf565)
>     {
>         EnsurePenColorCached();
>         buf565.SetPixel(rx, ry, _penColor16bpp);
>     }
>     else if (PixelBuffer is BufferRgb332 buf332)
>     {
>         EnsurePenColorCached();
>         buf332.SetPixel(rx, ry, _penColor8bpp);
>     }
>     else if (PixelBuffer is Buffer1bpp buf1)
>     {
>         EnsurePenColorCached();
>         buf1.SetPixel(rx, ry, _penColor1bpp);
>     }
>     else
>     {
>         PixelBuffer.SetPixel(rx, ry, PenColor);
>     }
> }
> ```
> 
> ### 4. Optimize DrawSingleWidthLine to Use Cached Color
> 
> ```csharp
> private void DrawSingleWidthLine(int x0, int y0, int x1, int y1, Color color)
> {
>     // If drawing with PenColor, use the fast cached path
>     bool useCachedPath = (color == PenColor);
>     
>     var steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0);
>     if (steep) { Swap(ref x0, ref y0); Swap(ref x1, ref y1); }
>     if (x0 > x1) { Swap(ref x0, ref x1); Swap(ref y0, ref y1); }
>     
>     var dx = x1 - x0;
>     var dy = Math.Abs(y1 - y0);
>     var error = dx >> 1;
>     var ystep = y0 < y1 ? 1 : -1;
>     var y = y0;
> 
>     if (useCachedPath)
>     {
>         for (var x = x0; x <= x1; x++)
>         {
>             DrawPixelWithCachedColor(steep ? y : x, steep ? x : y);
>             error -= dy;
>             if (error < 0) { y += ystep; error += dx; }
>         }
>     }
>     else
>     {
>         for (var x = x0; x <= x1; x++)
>         {
>             DrawPixel(steep ? y : x, steep ? x : y, color);
>             error -= dy;
>             if (error < 0) { y += ystep; error += dx; }
>         }
>     }
> }
> ```
> 
> ### 5. Add Horizontal Line Drawing with Native Color
> 
> For filled shapes that use horizontal line fills:
> 
> ```csharp
> // Add to BufferRgb565.cs
> public unsafe void DrawHorizontalLine(int x, int y, int length, ushort color)
> {
>     if (length <= 0) return;
>     
>     fixed (byte* ptr = Buffer)
>     {
>         var pixelPtr = (ushort*)(ptr + ((y * Width + x) << 1));
>         ushort swappedColor = (ushort)((color << 8) | (color >> 8));
>         
>         for (int i = 0; i < length; i++)
>         {
>             pixelPtr[i] = swappedColor;
>         }
>     }
> }
> ```
> 
> Then in MicroGraphics, use it for horizontal lines when possible:
> 
> ```csharp
> public void DrawHorizontalLine(int x, int y, int length, Color color)
> {
>     // ... bounds checking ...
>     
>     if (Rotation == RotationType.Default && PixelBuffer is BufferRgb565 buf565)
>     {
>         buf565.DrawHorizontalLine(x, y, length, color.Color16bppRgb565);
>         return;
>     }
>     
>     // Fall back to pixel-by-pixel
>     Fill(x, y, length, 1, color);
> }
> ```
> 
> ## Files to Modify
> 
> 1. **`Source/Meadow.Foundation.Libraries_and_Frameworks/Graphics.MicroGraphics/Driver/MicroGraphics.cs`**
>    - Add cached pen color fields
>    - Add `EnsurePenColorCached()` method
>    - Add `DrawPixelWithCachedColor()` method  
>    - Update `DrawSingleWidthLine()` to use cached path
>    - Update `DrawCircleOutline()` to use cached path
>    - Update `DrawBitmap()` to use cached path
> 
> 2. **`Source/Meadow.Foundation.Core/Graphics/Buffers/BufferRgb565.cs`**
>    - Add `DrawHorizontalLine(int x, int y, int length, ushort color)` method
>    - Add `DrawVerticalLine(int x, int y, int length, ushort color)` method
> 
> 3. **`Source/Meadow.Foundation.Core/Graphics/Buffers/BufferRgb332.cs`**
>    - Add similar native drawing methods
> 
> 4. **`Source/Meadow.Foundation.Core/Graphics/Buffers/Buffer1bpp.cs`**
>    - Add similar native drawing methods
> 
> ## Expected Performance Improvement
> 
> By eliminating per-pixel color conversion for same-color operations:
> 
> | Benchmark | Expected Improvement |
> |-----------|---------------------|
> | Pixel | +15-25% (when using PenColor) |
> | Shapes | +20-35% (lines, circles use same color) |
> | Path | +10-20% |
> | Starfield | +15-25% |
> | GradientFill | Minimal (colors change per line) |
> | Text | +25-40% (all same color) |
> 
> ## Testing Considerations
> 
> - Ensure all color modes still work correctly (1bpp, 8bpp, 16bpp, etc.)
> - Verify PenColor cache invalidates properly when color changes
> - Test with rotation enabled
> - Benchmark before and after to verify improvements

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI changed the title [WIP] Improve MicroGraphics drawing performance at buffer level Optimize MicroGraphics drawing performance via native color caching Nov 27, 2025
Copilot AI requested a review from adrianstevens November 27, 2025 05:51
Copilot finished work on behalf of adrianstevens November 27, 2025 05:51
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