From 26ce3b1621afc33accfe868186fd1caa31693d2a Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 20:57:17 -0500 Subject: [PATCH 1/8] direct2d: wip: take two doing things considering a common IDWriteFactory. --- src/direct2d.cpp | 807 +++++++++++++++++++++++-------------------- src/win_direct2d.cpp | 171 ++++----- 2 files changed, 516 insertions(+), 462 deletions(-) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index febc4b4..1042c1d 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -14,7 +14,8 @@ */ #include -#include +#include +#include #include #include @@ -26,25 +27,40 @@ #pragma comment(lib, "d2d1.lib") #pragma comment(lib, "dwrite.lib") +#include "pugl/pugl.h" + #include #include #include -#include "pugl/src/stub.h" +// Forward declare the pugl surface structure +typedef struct { + ID2D1Factory* d2dFactory; + ID2D1HwndRenderTarget* renderTarget; + IDWriteFactory* writeFactory; +} PuglWinDirect2DSurface; extern "C" { const PuglBackend* puglDirect2DBackend(); } -#define LUI_D2D_DEFAULT_FONT L"Segoe UI" +// Helper function to release COM interfaces safely +namespace { +template +void SafeRelease (Interface** ppInterfaceToRelease) { + if (*ppInterfaceToRelease != nullptr) { + (*ppInterfaceToRelease)->Release(); + (*ppInterfaceToRelease) = nullptr; + } +} +} namespace lui { namespace d2d { class Context : public DrawingContext { public: - explicit Context (ID2D1RenderTarget* target = nullptr) - : rt (target) { + explicit Context() { stack.reserve (64); } @@ -52,527 +68,584 @@ class Context : public DrawingContext { release_resources(); } - bool begin_frame (ID2D1RenderTarget* _rt, lui::Bounds bounds) { - rt = _rt; - state = {}; + bool begin_frame (ID2D1RenderTarget* renderTarget, IDWriteFactory* dwFactory, lui::Bounds bounds) { + rt = renderTarget; + writeFactory = dwFactory; + state = {}; + state.font = Font (14.0f); stack.clear(); - release_resources(); + release_path_resources(); - if (rt) { - // Get DWrite factory from render target - ID2D1Factory* factory = nullptr; - rt->GetFactory (&factory); - if (factory) { - factory->Release(); - } - } + if (! rt) + return false; + + // Clear to white background + rt->Clear (D2D1::ColorF (D2D1::ColorF::White)); + + // Initialize path + create_new_path(); - this->clip (bounds); + // Set initial clip without tracking (will be cleaned up in end_frame) + rt->PushAxisAlignedClip ( + D2D1::RectF ( + static_cast (bounds.x), + static_cast (bounds.y), + static_cast (bounds.x + bounds.width), + static_cast (bounds.y + bounds.height)), + D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + state.clip = bounds.as(); + state.clip_depth = 1; return true; } void end_frame() { - release_resources(); - rt = nullptr; + // Pop any remaining clips + while (state.clip_depth > 0) { + rt->PopAxisAlignedClip(); + state.clip_depth--; + } + release_path_resources(); + rt = nullptr; + writeFactory = nullptr; } double device_scale() const noexcept override { - assert (rt != nullptr); + if (! rt) + return 1.0; + FLOAT dpiX, dpiY; rt->GetDpi (&dpiX, &dpiY); - return static_cast (dpiX / 96.0); // 96 DPI is default + return static_cast (dpiX / 96.0); } void save() override { stack.push_back (state); - if (current_layer) { - rt->PushLayer (D2D1::LayerParameters(), current_layer); - } } void restore() override { - if (current_layer) { - rt->PopLayer(); - } if (stack.empty()) return; + + // Pop any extra clips that were pushed since save + while (state.clip_depth > stack.back().clip_depth) { + rt->PopAxisAlignedClip(); + state.clip_depth--; + } + std::swap (state, stack.back()); stack.pop_back(); } void set_line_width (double width) override { - state.line_width = static_cast (width); + state.line_width = static_cast (width); } void clear_path() override { - if (geometry_sink) { - geometry_sink->Close(); - geometry_sink->Release(); - geometry_sink = nullptr; - } - if (path_geometry) { - path_geometry->Release(); - path_geometry = nullptr; - } - - // Create new path geometry - ID2D1Factory* factory = nullptr; - rt->GetFactory (&factory); - if (factory) { - factory->CreatePathGeometry (&path_geometry); - if (path_geometry) { - path_geometry->Open (&geometry_sink); - needs_begin_figure = true; - } - factory->Release(); - } + release_path_resources(); + create_new_path(); + current_pos = D2D1::Point2F (0, 0); } void move_to (double x1, double y1) override { - ensure_path(); - if (geometry_sink) { - if (! needs_begin_figure) { - geometry_sink->EndFigure (D2D1_FIGURE_END_OPEN); - } - geometry_sink->BeginFigure ( - D2D1::Point2F (static_cast (x1), static_cast (y1)), - D2D1_FIGURE_BEGIN_FILLED); - needs_begin_figure = false; - current_point = D2D1::Point2F (static_cast (x1), static_cast (y1)); + if (! geometrySink) + return; + + if (figure_active) { + geometrySink->EndFigure (D2D1_FIGURE_END_OPEN); + figure_active = false; } + + current_pos = D2D1::Point2F (static_cast (x1), static_cast (y1)); + geometrySink->BeginFigure (current_pos, D2D1_FIGURE_BEGIN_FILLED); + figure_active = true; } void line_to (double x1, double y1) override { - ensure_path(); - ensure_figure(); - if (geometry_sink) { - current_point = D2D1::Point2F (static_cast (x1), static_cast (y1)); - geometry_sink->AddLine (current_point); - } + if (! geometrySink || ! figure_active) + return; + + D2D1_POINT_2F pt = D2D1::Point2F (static_cast (x1), static_cast (y1)); + geometrySink->AddLine (pt); + current_pos = pt; } void quad_to (double x1, double y1, double x2, double y2) override { - ensure_path(); - ensure_figure(); - if (geometry_sink) { - D2D1_QUADRATIC_BEZIER_SEGMENT segment = { - D2D1::Point2F (static_cast (x1), static_cast (y1)), - D2D1::Point2F (static_cast (x2), static_cast (y2)) - }; - geometry_sink->AddQuadraticBezier (segment); - current_point = segment.point2; - } + // Convert quadratic bezier to cubic + double cx1 = current_pos.x + 2.0 / 3.0 * (x1 - current_pos.x); + double cy1 = current_pos.y + 2.0 / 3.0 * (y1 - current_pos.y); + double cx2 = x2 + 2.0 / 3.0 * (x1 - x2); + double cy2 = y2 + 2.0 / 3.0 * (y1 - y2); + + cubic_to (cx1, cy1, cx2, cy2, x2, y2); } void cubic_to (double x1, double y1, double x2, double y2, double x3, double y3) override { - ensure_path(); - ensure_figure(); - if (geometry_sink) { - D2D1_BEZIER_SEGMENT segment = { - D2D1::Point2F (static_cast (x1), static_cast (y1)), - D2D1::Point2F (static_cast (x2), static_cast (y2)), - D2D1::Point2F (static_cast (x3), static_cast (y3)) - }; - geometry_sink->AddBezier (segment); - current_point = segment.point3; - } + if (! geometrySink || ! figure_active) + return; + + D2D1_BEZIER_SEGMENT bezier; + bezier.point1 = D2D1::Point2F (static_cast (x1), static_cast (y1)); + bezier.point2 = D2D1::Point2F (static_cast (x2), static_cast (y2)); + bezier.point3 = D2D1::Point2F (static_cast (x3), static_cast (y3)); + geometrySink->AddBezier (bezier); + current_pos = bezier.point3; } void close_path() override { - if (geometry_sink && ! needs_begin_figure) { - geometry_sink->EndFigure (D2D1_FIGURE_END_CLOSED); - needs_begin_figure = true; - } + if (! geometrySink || ! figure_active) + return; + + geometrySink->EndFigure (D2D1_FIGURE_END_CLOSED); + figure_active = false; } void fill() override { - apply_pending_state(); - close_geometry_sink(); - if (path_geometry && current_brush) { - rt->FillGeometry (path_geometry, current_brush); + if (! rt || ! pathGeometry) + return; + + finalize_path(); + + auto c = state.color; + ID2D1SolidColorBrush* brush = nullptr; + HRESULT hr = rt->CreateSolidColorBrush ( + D2D1::ColorF (c.red() / 255.0f, c.green() / 255.0f, c.blue() / 255.0f, c.alpha() / 255.0f), + &brush); + + if (SUCCEEDED (hr)) { + rt->FillGeometry (pathGeometry, brush); + SafeRelease (&brush); } } void stroke() override { - apply_pending_state(); - close_geometry_sink(); - if (path_geometry && current_brush) { - rt->DrawGeometry (path_geometry, current_brush, state.line_width); + if (! rt || ! pathGeometry) + return; + + finalize_path(); + + auto c = state.color; + ID2D1SolidColorBrush* brush = nullptr; + HRESULT hr = rt->CreateSolidColorBrush ( + D2D1::ColorF (c.red() / 255.0f, c.green() / 255.0f, c.blue() / 255.0f, c.alpha() / 255.0f), + &brush); + + if (SUCCEEDED (hr)) { + ID2D1StrokeStyle* strokeStyle = nullptr; + // Create stroke style with round caps + ID2D1Factory* factory = nullptr; + rt->GetFactory (&factory); + if (factory) { + D2D1_STROKE_STYLE_PROPERTIES strokeProps = D2D1::StrokeStyleProperties ( + D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE_ROUND, + D2D1_CAP_STYLE_ROUND, + D2D1_LINE_JOIN_ROUND); + + factory->CreateStrokeStyle (strokeProps, nullptr, 0, &strokeStyle); + factory->Release(); + } + + rt->DrawGeometry (pathGeometry, brush, state.line_width, strokeStyle); + SafeRelease (&brush); + SafeRelease (&strokeStyle); } } void translate (double x, double y) override { + if (! rt) + return; + D2D1_MATRIX_3X2_F transform; rt->GetTransform (&transform); - transform = transform * D2D1::Matrix3x2F::Translation (static_cast (x), static_cast (y)); - rt->SetTransform (transform); + + D2D1_MATRIX_3X2_F translation = D2D1::Matrix3x2F::Translation ( + static_cast (x), + static_cast (y)); + + rt->SetTransform (transform * translation); + state.clip.x -= x; state.clip.y -= y; } void transform (const Transform& mat) override { - D2D1_MATRIX_3X2_F d2dMatrix = D2D1::Matrix3x2F ( - static_cast (mat.m00), static_cast (mat.m10), static_cast (mat.m01), static_cast (mat.m11), static_cast (mat.m02), static_cast (mat.m12)); + if (! rt) + return; - D2D1_MATRIX_3X2_F current; - rt->GetTransform (¤t); - rt->SetTransform (current * d2dMatrix); - } + D2D1_MATRIX_3X2_F transform; + rt->GetTransform (&transform); - void reset_clip() noexcept { - state.clip = {}; - rt->PopAxisAlignedClip(); + D2D1_MATRIX_3X2_F newTransform = D2D1::Matrix3x2F ( + static_cast (mat.m00), + static_cast (mat.m10), + static_cast (mat.m01), + static_cast (mat.m11), + static_cast (mat.m02), + static_cast (mat.m12)); + + rt->SetTransform (transform * newTransform); } void clip (const Rectangle& r) override { state.clip = r.as(); + if (! rt) + return; + + // Push axis-aligned clip rt->PushAxisAlignedClip ( D2D1::RectF ( - static_cast (r.x), - static_cast (r.y), - static_cast (r.x + r.width), - static_cast (r.y + r.height)), + static_cast (r.x), + static_cast (r.y), + static_cast (r.x + r.width), + static_cast (r.y + r.height)), D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + state.clip_depth++; } - void exclude_clip (const Rectangle& r) override { - // TODO: Implement clip exclusion for Direct2D - lui::ignore (r); + void exclude_clip (const Rectangle&) override { + // Direct2D doesn't have direct exclude clip support + // This would require creating geometry and using PushLayer + // For now, we skip implementation as it's marked TODO in GDI too } Rectangle last_clip() const override { return state.clip.as(); } - Font font() const noexcept override { return state.font; } + Font font() const noexcept override { + return state.font; + } - void set_font (const Font& f) override { - state.font = f; - font_dirty = true; + void set_font (const Font& font) override { + state.font = font; } void set_fill (const Fill& fill) override { - auto c = state.color = fill.color(); - brush_dirty = false; - - // Release old brush - if (current_brush) { - current_brush->Release(); - current_brush = nullptr; + if (fill.is_color()) { + state.color = fill.color(); } - - // Create new solid color brush - rt->CreateSolidColorBrush ( - D2D1::ColorF (c.fred(), c.fgreen(), c.fblue(), c.alpha()), - ¤t_brush); } void fill_rect (const Rectangle& r) override { - apply_pending_state(); - if (current_brush) { + if (! rt) + return; + + auto c = state.color; + ID2D1SolidColorBrush* brush = nullptr; + HRESULT hr = rt->CreateSolidColorBrush ( + D2D1::ColorF (c.red() / 255.0f, c.green() / 255.0f, c.blue() / 255.0f, c.alpha() / 255.0f), + &brush); + + if (SUCCEEDED (hr)) { rt->FillRectangle ( D2D1::RectF ( - static_cast (r.x), - static_cast (r.y), - static_cast (r.x + r.width), - static_cast (r.y + r.height)), - current_brush); + static_cast (r.x), + static_cast (r.y), + static_cast (r.x + r.width), + static_cast (r.y + r.height)), + brush); + SafeRelease (&brush); } } FontMetrics font_metrics() const noexcept override { - FontMetrics fm; - - IDWriteTextFormat* format = create_text_format(); - if (format) { - // Get font collection - IDWriteFontCollection* fontCollection = nullptr; - format->GetFontCollection (&fontCollection); - - if (fontCollection) { - // Get font family - WCHAR familyName[100]; - format->GetFontFamilyName (familyName, 100); - - UINT32 index; - BOOL exists; - if (SUCCEEDED (fontCollection->FindFamilyName (familyName, &index, &exists)) && exists) { - IDWriteFontFamily* fontFamily = nullptr; - if (SUCCEEDED (fontCollection->GetFontFamily (index, &fontFamily))) { - IDWriteFont* font = nullptr; - if (SUCCEEDED (fontFamily->GetFirstMatchingFont ( - DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - &font))) { - DWRITE_FONT_METRICS metrics; - font->GetMetrics (&metrics); - - float fontSize = format->GetFontSize(); - float scale = fontSize / metrics.designUnitsPerEm; - - fm.ascent = metrics.ascent * scale; - fm.descent = metrics.descent * scale; - fm.height = fm.ascent + fm.descent; - fm.x_stride_max = fontSize; // Approximate max width as font size - fm.y_stride_max = (metrics.ascent + metrics.descent) * scale; - - font->Release(); - } - fontFamily->Release(); - } - } - fontCollection->Release(); - } - format->Release(); - } + if (! writeFactory) + return {}; - return fm; - } + IDWriteTextFormat* textFormat = nullptr; + HRESULT hr = writeFactory->CreateTextFormat ( + L"Roboto", + nullptr, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + state.font.height(), + L"en-us", + &textFormat); - TextMetrics text_metrics (std::string_view text) const noexcept override { - TextMetrics tm; + if (FAILED (hr)) + return {}; - IDWriteTextFormat* format = create_text_format(); - if (! format) - return tm; + IDWriteFontCollection* fontCollection = nullptr; + textFormat->GetFontCollection (&fontCollection); - // Convert UTF-8 to wide string - int wlen = MultiByteToWideChar (CP_UTF8, 0, text.data(), (int) text.size(), nullptr, 0); - if (wlen <= 0) { - format->Release(); - return tm; + UINT32 familyIndex = 0; + BOOL exists = FALSE; + if (fontCollection) { + fontCollection->FindFamilyName (L"Roboto", &familyIndex, &exists); } - std::vector wtext (wlen + 1); - MultiByteToWideChar (CP_UTF8, 0, text.data(), (int) text.size(), wtext.data(), wlen); - wtext[wlen] = 0; - - // Get DWrite factory - IDWriteFactory* writeFactory = get_write_factory(); - if (! writeFactory) { - format->Release(); - return tm; + IDWriteFontFamily* fontFamily = nullptr; + if (exists && fontCollection) { + fontCollection->GetFontFamily (familyIndex, &fontFamily); } - // Create text layout - IDWriteTextLayout* textLayout = nullptr; - HRESULT hr = writeFactory->CreateTextLayout ( - wtext.data(), - wlen, - format, - 10000.0f, // Max width - 10000.0f, // Max height - &textLayout); + IDWriteFont* font = nullptr; + if (fontFamily) { + fontFamily->GetFirstMatchingFont ( + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + &font); + } - if (SUCCEEDED (hr)) { - DWRITE_TEXT_METRICS metrics; - textLayout->GetMetrics (&metrics); + DWRITE_FONT_METRICS fontMetrics; + if (font) { + font->GetMetrics (&fontMetrics); + } - tm.width = metrics.width; - tm.height = metrics.height; - tm.x_offset = metrics.left; - tm.y_offset = metrics.top; - tm.x_stride = metrics.widthIncludingTrailingWhitespace; - tm.y_stride = 0.0; + SafeRelease (&font); + SafeRelease (&fontFamily); + SafeRelease (&fontCollection); + SafeRelease (&textFormat); - textLayout->Release(); + if (! font) { + // Return approximate values if we couldn't get font + const float height = state.font.height(); + return { + height * 0.8, + height * 0.2, + height, + height * 0.5, + height + }; } - format->Release(); - return tm; - } + const float fontSize = state.font.height(); + const float designSize = static_cast (fontMetrics.designUnitsPerEm); + const float ascent = fontSize * fontMetrics.ascent / designSize; + const float descent = fontSize * fontMetrics.descent / designSize; + const float height = ascent + descent; - bool show_text (const std::string_view text) override { - apply_pending_state(); + return { + static_cast (ascent), + static_cast (descent), + static_cast (height), + static_cast (fontSize / 2.0), + static_cast (height) + }; + } - // Convert UTF-8 to wide string - int wlen = MultiByteToWideChar (CP_UTF8, 0, text.data(), (int) text.size(), nullptr, 0); - if (wlen <= 0) - return false; + TextMetrics text_metrics (std::string_view text) const noexcept override { + if (! writeFactory || ! rt) + return {}; - std::vector wtext (wlen + 1); - MultiByteToWideChar (CP_UTF8, 0, text.data(), (int) text.size(), wtext.data(), wlen); - wtext[wlen] = 0; + std::wstring wtext (text.begin(), text.end()); - IDWriteTextFormat* format = create_text_format(); - if (! format) - return false; + IDWriteTextFormat* textFormat = nullptr; + HRESULT hr = writeFactory->CreateTextFormat ( + L"Roboto", + nullptr, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + state.font.height(), + L"en-us", + &textFormat); - // Get DWrite factory - IDWriteFactory* writeFactory = get_write_factory(); - if (! writeFactory) { - format->Release(); - return false; - } + if (FAILED (hr)) + return {}; - // Create text layout IDWriteTextLayout* textLayout = nullptr; - HRESULT hr = writeFactory->CreateTextLayout ( - wtext.data(), - wlen, - format, + hr = writeFactory->CreateTextLayout ( + wtext.c_str(), + static_cast (wtext.length()), + textFormat, 10000.0f, 10000.0f, &textLayout); - if (SUCCEEDED (hr) && current_brush) { - // Draw at current point - rt->DrawTextLayout ( - current_point, - textLayout, - current_brush, - D2D1_DRAW_TEXT_OPTIONS_NONE); - - textLayout->Release(); - format->Release(); - return true; + SafeRelease (&textFormat); + + if (FAILED (hr)) + return {}; + + DWRITE_TEXT_METRICS metrics; + textLayout->GetMetrics (&metrics); + SafeRelease (&textLayout); + + return { + static_cast (metrics.width), + static_cast (metrics.height), + 0.0, + 0.0, + static_cast (metrics.width), + static_cast (metrics.height) + }; + } + + bool show_text (std::string_view text) override { + if (! rt || ! writeFactory) + return false; + + std::wstring wtext (text.begin(), text.end()); + + IDWriteTextFormat* textFormat = nullptr; + HRESULT hr = writeFactory->CreateTextFormat ( + L"Roboto", + nullptr, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + state.font.height(), + L"en-us", + &textFormat); + + if (FAILED (hr)) + return false; + + // Get font metrics to calculate baseline adjustment + auto metrics = font_metrics(); + + auto c = state.color; + ID2D1SolidColorBrush* brush = nullptr; + hr = rt->CreateSolidColorBrush ( + D2D1::ColorF (c.red() / 255.0f, c.green() / 255.0f, c.blue() / 255.0f, c.alpha() / 255.0f), + &brush); + + if (SUCCEEDED (hr)) { + // DirectWrite uses top-left positioning, current_pos represents baseline + // Adjust Y coordinate by subtracting ascent + D2D1_RECT_F layoutRect = D2D1::RectF ( + current_pos.x, + current_pos.y - static_cast (metrics.ascent), + 10000.0f, + 10000.0f); + + rt->DrawText ( + wtext.c_str(), + static_cast (wtext.length()), + textFormat, + layoutRect, + brush); + + SafeRelease (&brush); } - if (textLayout) - textLayout->Release(); - format->Release(); - return false; + SafeRelease (&textFormat); + return SUCCEEDED (hr); } void draw_image (Image i, Transform matrix) override { - // Create D2D bitmap from image data - D2D1_BITMAP_PROPERTIES props = { - { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, - 96.0f, - 96.0f - }; + if (! rt) + return; + + // Create a D2D bitmap from the image data + D2D1_BITMAP_PROPERTIES bitmapProps; + bitmapProps.pixelFormat.format = DXGI_FORMAT_B8G8R8A8_UNORM; + bitmapProps.pixelFormat.alphaMode = D2D1_ALPHA_MODE_PREMULTIPLIED; + bitmapProps.dpiX = 96.0f; + bitmapProps.dpiY = 96.0f; ID2D1Bitmap* bitmap = nullptr; HRESULT hr = rt->CreateBitmap ( D2D1::SizeU (i.width(), i.height()), i.data(), i.stride(), - props, + bitmapProps, &bitmap); - if (SUCCEEDED (hr) && bitmap) { + if (SUCCEEDED (hr)) { + // Save current transform D2D1_MATRIX_3X2_F oldTransform; rt->GetTransform (&oldTransform); - transform (matrix); + // Apply the transformation matrix + D2D1_MATRIX_3X2_F newTransform = D2D1::Matrix3x2F ( + static_cast (matrix.m00), + static_cast (matrix.m10), + static_cast (matrix.m01), + static_cast (matrix.m11), + static_cast (matrix.m02), + static_cast (matrix.m12)); + + rt->SetTransform (oldTransform * newTransform); + + // Draw the bitmap + D2D1_RECT_F destRect = D2D1::RectF ( + 0.0f, + 0.0f, + static_cast (i.width()), + static_cast (i.height())); rt->DrawBitmap ( bitmap, - D2D1::RectF (0, 0, (FLOAT) i.width(), (FLOAT) i.height()), + destRect, 1.0f, - D2D1_BITMAP_INTERPOLATION_MODE_LINEAR); + D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, + nullptr); + // Restore transform rt->SetTransform (oldTransform); - bitmap->Release(); + + SafeRelease (&bitmap); } } private: - void ensure_path() { - if (! path_geometry) { - clear_path(); - } - } + void create_new_path() { + if (! rt) + return; - void ensure_figure() { - if (needs_begin_figure && geometry_sink) { - geometry_sink->BeginFigure (current_point, D2D1_FIGURE_BEGIN_FILLED); - needs_begin_figure = false; - } - } + ID2D1Factory* factory = nullptr; + rt->GetFactory (&factory); - void close_geometry_sink() { - if (geometry_sink) { - if (! needs_begin_figure) { - geometry_sink->EndFigure (D2D1_FIGURE_END_OPEN); - needs_begin_figure = true; + if (factory) { + factory->CreatePathGeometry (&pathGeometry); + if (pathGeometry) { + pathGeometry->Open (&geometrySink); } - geometry_sink->Close(); - geometry_sink->Release(); - geometry_sink = nullptr; + factory->Release(); } - } - void apply_pending_state() { - if (brush_dirty) { - set_fill (Fill { state.color }); - } - if (font_dirty) { - font_dirty = false; - } + figure_active = false; } - IDWriteTextFormat* create_text_format() const { - IDWriteFactory* writeFactory = get_write_factory(); - if (! writeFactory) - return nullptr; - - IDWriteTextFormat* format = nullptr; - writeFactory->CreateTextFormat ( - LUI_D2D_DEFAULT_FONT, - nullptr, - DWRITE_FONT_WEIGHT_NORMAL, - DWRITE_FONT_STYLE_NORMAL, - DWRITE_FONT_STRETCH_NORMAL, - static_cast (state.font.height()), - L"en-us", - &format); + void finalize_path() { + if (geometrySink && figure_active) { + geometrySink->EndFigure (D2D1_FIGURE_END_OPEN); + figure_active = false; + } - return format; + if (geometrySink) { + geometrySink->Close(); + SafeRelease (&geometrySink); + } } - IDWriteFactory* get_write_factory() const { - // This should be stored with the surface/view - // For now, create a shared instance - static IDWriteFactory* factory = nullptr; - if (! factory) { - DWriteCreateFactory ( - DWRITE_FACTORY_TYPE_SHARED, - __uuidof (IDWriteFactory), - reinterpret_cast (&factory)); + void release_path_resources() { + if (geometrySink && figure_active) { + geometrySink->EndFigure (D2D1_FIGURE_END_OPEN); + figure_active = false; } - return factory; + + SafeRelease (&geometrySink); + SafeRelease (&pathGeometry); } void release_resources() { - if (geometry_sink) { - geometry_sink->Release(); - geometry_sink = nullptr; - } - if (path_geometry) { - path_geometry->Release(); - path_geometry = nullptr; - } - if (current_brush) { - current_brush->Release(); - current_brush = nullptr; - } - if (current_layer) { - current_layer->Release(); - current_layer = nullptr; - } + release_path_resources(); + rt = nullptr; + writeFactory = nullptr; } struct State { Font font; Color color; Rectangle clip; - FLOAT line_width { 1.0f }; + float line_width { 1.0f }; + int clip_depth { 0 }; }; ID2D1RenderTarget* rt { nullptr }; - ID2D1PathGeometry* path_geometry { nullptr }; - ID2D1GeometrySink* geometry_sink { nullptr }; - ID2D1SolidColorBrush* current_brush { nullptr }; - ID2D1Layer* current_layer { nullptr }; - D2D1_POINT_2F current_point { 0, 0 }; - bool needs_begin_figure { true }; + IDWriteFactory* writeFactory { nullptr }; + ID2D1PathGeometry* pathGeometry { nullptr }; + ID2D1GeometrySink* geometrySink { nullptr }; + D2D1_POINT_2F current_pos { 0, 0 }; + bool figure_active { false }; State state; std::vector stack; - bool brush_dirty { false }; - bool font_dirty { false }; }; class View : public lui::View { @@ -588,17 +661,20 @@ class View : public lui::View { ~View() {} void expose (Bounds frame) override { - auto rt = (ID2D1RenderTarget*) puglGetContext (_view); - assert (rt != nullptr); - - if (true || ! _scale_set || _last_scale != scale_factor()) { - _scale_set = true; - const auto scale_x = scale_factor(); - const auto scale_y = scale_x; - _last_scale = scale_x; + auto surface = (PuglWinDirect2DSurface*) puglGetContext (_view); + if (! surface || ! surface->renderTarget) + return; + + auto rt = surface->renderTarget; + auto writeFactory = surface->writeFactory; + + const auto scale = scale_factor(); + if (scale != 1.0 && scale != _last_scale) { + rt->SetDpi (static_cast (scale * 96.0), static_cast (scale * 96.0)); + _last_scale = scale; } - if (_context->begin_frame (rt, frame)) { + if (_context->begin_frame (rt, writeFactory, frame)) { render (*_context); _context->end_frame(); } @@ -619,7 +695,6 @@ class View : public lui::View { using Parent = lui::View; PuglView* _view { nullptr }; std::unique_ptr _context; - bool _scale_set { false }; double _last_scale { 1.0 }; }; diff --git a/src/win_direct2d.cpp b/src/win_direct2d.cpp index a2ddb0c..636e8e6 100644 --- a/src/win_direct2d.cpp +++ b/src/win_direct2d.cpp @@ -2,7 +2,6 @@ // Copyright 2026 Kushview, LLC // SPDX-License-Identifier: ISC -#include "pugl/src/stub.h" #include "pugl/src/types.h" extern "C" { @@ -10,6 +9,7 @@ extern "C" { } #include +#include #include #include @@ -21,185 +21,164 @@ typedef struct { } PuglWinDirect2DSurface; static PuglStatus - puglWinDirect2DCreateDrawContext (PuglView* view) { - PuglInternals* const impl = view->impl; +puglWinDirect2DCreateDeviceResources (PuglView* view) { + PuglInternals* const impl = view->impl; PuglWinDirect2DSurface* const surface = (PuglWinDirect2DSurface*) impl->surface; - if (! surface->d2dFactory) { - return PUGL_CREATE_CONTEXT_FAILED; + if (surface->renderTarget) { + return PUGL_SUCCESS; } - // Create render target properties - D2D1_RENDER_TARGET_PROPERTIES props = { - D2D1_RENDER_TARGET_TYPE_DEFAULT, - { DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }, - 0.0f, - 0.0f, - D2D1_RENDER_TARGET_USAGE_NONE, - D2D1_FEATURE_LEVEL_DEFAULT - }; + // Get window client area size + RECT rc; + GetClientRect (impl->hwnd, &rc); - // Create HWND render target properties - D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = { - impl->hwnd, - { (UINT32) view->lastConfigure.width, (UINT32) view->lastConfigure.height }, - D2D1_PRESENT_OPTIONS_NONE - }; + D2D1_SIZE_U size = D2D1::SizeU ( + rc.right - rc.left, + rc.bottom - rc.top); + // Create a Direct2D render target HRESULT hr = surface->d2dFactory->CreateHwndRenderTarget ( - &props, - &hwndProps, + D2D1::RenderTargetProperties(), + D2D1::HwndRenderTargetProperties (impl->hwnd, size), &surface->renderTarget); - return (SUCCEEDED (hr)) ? PUGL_SUCCESS : PUGL_CREATE_CONTEXT_FAILED; + return SUCCEEDED (hr) ? PUGL_SUCCESS : PUGL_CREATE_CONTEXT_FAILED; } -static PuglStatus - puglWinDirect2DDestroyDrawContext (PuglView* view) { - PuglInternals* const impl = view->impl; +static void +puglWinDirect2DDiscardDeviceResources (PuglView* view) { + PuglInternals* const impl = view->impl; PuglWinDirect2DSurface* const surface = (PuglWinDirect2DSurface*) impl->surface; if (surface->renderTarget) { surface->renderTarget->Release(); - surface->renderTarget = NULL; + surface->renderTarget = nullptr; } - - return PUGL_SUCCESS; } static PuglStatus - puglWinDirect2DConfigure (PuglView* view) { +puglWinDirect2DConfigure (PuglView* view) { const PuglStatus st = puglWinConfigure (view); - if (! st) { - PuglWinDirect2DSurface* surface = - (PuglWinDirect2DSurface*) calloc (1, sizeof (PuglWinDirect2DSurface)); - - view->impl->surface = surface; + if (st != PUGL_SUCCESS) { + return st; + } - // Create D2D factory - HRESULT hr = D2D1CreateFactory ( - D2D1_FACTORY_TYPE_SINGLE_THREADED, - &surface->d2dFactory); + // Allocate surface structure + view->impl->surface = (PuglWinDirect2DSurface*) calloc (1, sizeof (PuglWinDirect2DSurface)); + if (! view->impl->surface) { + return PUGL_FAILURE; + } - if (FAILED (hr)) { - free (surface); - view->impl->surface = NULL; - return PUGL_CREATE_CONTEXT_FAILED; - } + PuglWinDirect2DSurface* surface = (PuglWinDirect2DSurface*) view->impl->surface; - // Create DWrite factory - hr = DWriteCreateFactory ( - DWRITE_FACTORY_TYPE_SHARED, - __uuidof (IDWriteFactory), - reinterpret_cast (&surface->writeFactory)); + // Create Direct2D factory (device-independent) + HRESULT hr = D2D1CreateFactory ( + D2D1_FACTORY_TYPE_SINGLE_THREADED, + &surface->d2dFactory); - if (FAILED (hr)) { - surface->d2dFactory->Release(); - free (surface); - view->impl->surface = NULL; - return PUGL_CREATE_CONTEXT_FAILED; - } + if (FAILED (hr)) { + free (view->impl->surface); + view->impl->surface = nullptr; + return PUGL_CREATE_CONTEXT_FAILED; } - return st; -} + // Create DirectWrite factory (device-independent) + hr = DWriteCreateFactory ( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + reinterpret_cast (&surface->writeFactory)); -static void - puglWinDirect2DClose (PuglView* view) { - // Direct2D doesn't need explicit close per frame - (void) view; + if (FAILED (hr)) { + surface->d2dFactory->Release(); + free (view->impl->surface); + view->impl->surface = nullptr; + return PUGL_CREATE_CONTEXT_FAILED; + } + + return PUGL_SUCCESS; } static PuglStatus - puglWinDirect2DOpen (PuglView* view) { - // Direct2D doesn't need explicit open per frame +puglWinDirect2DCreate (PuglView* view) { (void) view; return PUGL_SUCCESS; } static void - puglWinDirect2DDestroy (PuglView* view) { - PuglInternals* const impl = view->impl; +puglWinDirect2DDestroy (PuglView* view) { + PuglInternals* const impl = view->impl; PuglWinDirect2DSurface* const surface = (PuglWinDirect2DSurface*) impl->surface; if (surface) { - puglWinDirect2DClose (view); - puglWinDirect2DDestroyDrawContext (view); + puglWinDirect2DDiscardDeviceResources (view); if (surface->writeFactory) { surface->writeFactory->Release(); + surface->writeFactory = nullptr; } if (surface->d2dFactory) { surface->d2dFactory->Release(); + surface->d2dFactory = nullptr; } free (surface); - impl->surface = NULL; + impl->surface = nullptr; } } static PuglStatus - puglWinDirect2DEnter (PuglView* view, const PuglExposeEvent* expose) { +puglWinDirect2DEnter (PuglView* view, const PuglExposeEvent* expose) { PuglStatus st = PUGL_SUCCESS; if (expose) { - if (! st) { - st = puglWinDirect2DCreateDrawContext (view); - } - if (! st) { - st = puglWinDirect2DOpen (view); - } - if (! st) { - st = puglWinEnter (view, expose); + // Create device resources if needed + st = puglWinDirect2DCreateDeviceResources (view); + if (st != PUGL_SUCCESS) { + return st; } + PuglWinDirect2DSurface* surface = (PuglWinDirect2DSurface*) view->impl->surface; + // Begin drawing - if (! st) { - PuglWinDirect2DSurface* const surface = - (PuglWinDirect2DSurface*) view->impl->surface; - if (surface->renderTarget) { - surface->renderTarget->BeginDraw(); - } - } + surface->renderTarget->BeginDraw(); + surface->renderTarget->SetTransform (D2D1::Matrix3x2F::Identity()); } return st; } static PuglStatus - puglWinDirect2DLeave (PuglView* view, const PuglExposeEvent* expose) { - PuglInternals* const impl = view->impl; - PuglWinDirect2DSurface* const surface = (PuglWinDirect2DSurface*) impl->surface; +puglWinDirect2DLeave (PuglView* view, const PuglExposeEvent* expose) { + PuglWinDirect2DSurface* surface = (PuglWinDirect2DSurface*) view->impl->surface; if (expose && surface->renderTarget) { // End drawing HRESULT hr = surface->renderTarget->EndDraw(); + // Check if we need to recreate device resources if (hr == D2DERR_RECREATE_TARGET) { - // Need to recreate render target - puglWinDirect2DDestroyDrawContext (view); + puglWinDirect2DDiscardDeviceResources (view); } - - puglWinDirect2DClose (view); } - return puglWinLeave (view, expose); + return PUGL_SUCCESS; } static void* - puglWinDirect2DGetContext (PuglView* view) { - return ((PuglWinDirect2DSurface*) view->impl->surface)->renderTarget; +puglWinDirect2DGetContext (PuglView* view) { + return view->impl->surface; } extern "C" { const PuglBackend* - puglDirect2DBackend (void) { +puglDirect2DBackend (void) { static const PuglBackend backend = { puglWinDirect2DConfigure, - puglStubCreate, + puglWinDirect2DCreate, puglWinDirect2DDestroy, puglWinDirect2DEnter, puglWinDirect2DLeave, From ccbe5aa2a48791db0f479169369a178c40e8a2b6 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 21:19:08 -0500 Subject: [PATCH 2/8] enhance: direct2d save and restore transform matrix in drawing context state --- src/direct2d.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index 1042c1d..775e70a 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -119,6 +119,10 @@ class Context : public DrawingContext { } void save() override { + // Save the current transform matrix in state + if (rt) { + rt->GetTransform (&state.transform); + } stack.push_back (state); } @@ -132,6 +136,11 @@ class Context : public DrawingContext { state.clip_depth--; } + // Restore the transform matrix from saved state + if (rt) { + rt->SetTransform (stack.back().transform); + } + std::swap (state, stack.back()); stack.pop_back(); } @@ -635,6 +644,7 @@ class Context : public DrawingContext { Rectangle clip; float line_width { 1.0f }; int clip_depth { 0 }; + D2D1_MATRIX_3X2_F transform; }; ID2D1RenderTarget* rt { nullptr }; From ffe67a4c1f188b340cccc868db50f2feee2a905e Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 21:27:04 -0500 Subject: [PATCH 3/8] Implement Direct2D backend with full drawing support - Implement pugl backend integration (win_direct2d.cpp) * Proper Direct2D factory and DirectWrite factory initialization * Device-dependent resource management with render target recreation * Handle D2DERR_RECREATE_TARGET for device loss scenarios * Return surface structure from puglGetContext for clean resource access * Resize render target on window size changes * Use 96 DPI for 1:1 pixel coordinate mapping - Implement complete DrawingContext (direct2d.cpp) * Full path geometry support using ID2D1PathGeometry and ID2D1GeometrySink * All drawing primitives: move_to, line_to, cubic_to, quad_to, close_path * Fill and stroke operations with proper brush and stroke style creation * Transform support with save/restore in state stack * Clipping with proper push/pop management and depth tracking * DirectWrite text rendering with Roboto font and baseline alignment * Font metrics and text metrics calculation * Image rendering using ID2D1Bitmap with transformation support * State management including transform matrix in state stack Key design decisions: - Direct2D uses top-left origin naturally (no flipping needed) - Render target uses 96 DPI for identity pixel mapping - Transform matrix stored in State for proper save/restore - Clip depth tracking for proper clip stack management --- src/direct2d.cpp | 7 ------- src/win_direct2d.cpp | 25 +++++++++++++++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index 775e70a..dda068a 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -678,12 +678,6 @@ class View : public lui::View { auto rt = surface->renderTarget; auto writeFactory = surface->writeFactory; - const auto scale = scale_factor(); - if (scale != 1.0 && scale != _last_scale) { - rt->SetDpi (static_cast (scale * 96.0), static_cast (scale * 96.0)); - _last_scale = scale; - } - if (_context->begin_frame (rt, writeFactory, frame)) { render (*_context); _context->end_frame(); @@ -705,7 +699,6 @@ class View : public lui::View { using Parent = lui::View; PuglView* _view { nullptr }; std::unique_ptr _context; - double _last_scale { 1.0 }; }; } // namespace d2d diff --git a/src/win_direct2d.cpp b/src/win_direct2d.cpp index 636e8e6..38e92ed 100644 --- a/src/win_direct2d.cpp +++ b/src/win_direct2d.cpp @@ -26,20 +26,29 @@ puglWinDirect2DCreateDeviceResources (PuglView* view) { PuglWinDirect2DSurface* const surface = (PuglWinDirect2DSurface*) impl->surface; if (surface->renderTarget) { + // Just resize if it already exists + D2D1_SIZE_U size = D2D1::SizeU ( + (UINT32) view->lastConfigure.width, + (UINT32) view->lastConfigure.height); + surface->renderTarget->Resize (size); return PUGL_SUCCESS; } - // Get window client area size - RECT rc; - GetClientRect (impl->hwnd, &rc); - D2D1_SIZE_U size = D2D1::SizeU ( - rc.right - rc.left, - rc.bottom - rc.top); + (UINT32) view->lastConfigure.width, + (UINT32) view->lastConfigure.height); + + // Create render target with 96 DPI (identity DPI) so coordinates match 1:1 with pixels + // This matches how pugl reports window size and mouse coordinates + D2D1_RENDER_TARGET_PROPERTIES rtProps = D2D1::RenderTargetProperties(); + rtProps.dpiX = 96.0f; + rtProps.dpiY = 96.0f; + rtProps.pixelFormat = D2D1::PixelFormat ( + DXGI_FORMAT_B8G8R8A8_UNORM, + D2D1_ALPHA_MODE_PREMULTIPLIED); - // Create a Direct2D render target HRESULT hr = surface->d2dFactory->CreateHwndRenderTarget ( - D2D1::RenderTargetProperties(), + rtProps, D2D1::HwndRenderTargetProperties (impl->hwnd, size), &surface->renderTarget); From 624efe06dedacdfe7d2f3769cb01bbc41f42b293 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 21:38:14 -0500 Subject: [PATCH 4/8] fix: update text rendering to use Segoe UI font and improve baseline adjustment --- src/direct2d.cpp | 56 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index dda068a..b4993cc 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -363,7 +363,7 @@ class Context : public DrawingContext { IDWriteTextFormat* textFormat = nullptr; HRESULT hr = writeFactory->CreateTextFormat ( - L"Roboto", + L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, @@ -381,7 +381,7 @@ class Context : public DrawingContext { UINT32 familyIndex = 0; BOOL exists = FALSE; if (fontCollection) { - fontCollection->FindFamilyName (L"Roboto", &familyIndex, &exists); + fontCollection->FindFamilyName (L"Segoe UI", &familyIndex, &exists); } IDWriteFontFamily* fontFamily = nullptr; @@ -399,8 +399,10 @@ class Context : public DrawingContext { } DWRITE_FONT_METRICS fontMetrics; + bool gotMetrics = false; if (font) { font->GetMetrics (&fontMetrics); + gotMetrics = true; } SafeRelease (&font); @@ -408,7 +410,7 @@ class Context : public DrawingContext { SafeRelease (&fontCollection); SafeRelease (&textFormat); - if (! font) { + if (! gotMetrics) { // Return approximate values if we couldn't get font const float height = state.font.height(); return { @@ -443,7 +445,7 @@ class Context : public DrawingContext { IDWriteTextFormat* textFormat = nullptr; HRESULT hr = writeFactory->CreateTextFormat ( - L"Roboto", + L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, @@ -491,7 +493,7 @@ class Context : public DrawingContext { IDWriteTextFormat* textFormat = nullptr; HRESULT hr = writeFactory->CreateTextFormat ( - L"Roboto", + L"Segoe UI", nullptr, DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, @@ -500,6 +502,21 @@ class Context : public DrawingContext { L"en-us", &textFormat); + if (FAILED (hr)) + return false; + + // Create a text layout to measure and draw + IDWriteTextLayout* textLayout = nullptr; + hr = writeFactory->CreateTextLayout ( + wtext.c_str(), + static_cast (wtext.length()), + textFormat, + 10000.0f, + 10000.0f, + &textLayout); + + SafeRelease (&textFormat); + if (FAILED (hr)) return false; @@ -513,25 +530,26 @@ class Context : public DrawingContext { &brush); if (SUCCEEDED (hr)) { - // DirectWrite uses top-left positioning, current_pos represents baseline - // Adjust Y coordinate by subtracting ascent - D2D1_RECT_F layoutRect = D2D1::RectF ( + // Get font metrics to calculate baseline adjustment (same as GDI approach) + auto fm = font_metrics(); + + // DirectWrite DrawTextLayout uses top-left positioning + // current_pos is the baseline position from Graphics layer + // So subtract ascent to convert baseline to top-left + D2D1_POINT_2F origin = D2D1::Point2F ( current_pos.x, - current_pos.y - static_cast (metrics.ascent), - 10000.0f, - 10000.0f); - - rt->DrawText ( - wtext.c_str(), - static_cast (wtext.length()), - textFormat, - layoutRect, - brush); + current_pos.y - static_cast (fm.ascent)); + + rt->DrawTextLayout ( + origin, + textLayout, + brush, + D2D1_DRAW_TEXT_OPTIONS_NONE); SafeRelease (&brush); } - SafeRelease (&textFormat); + SafeRelease (&textLayout); return SUCCEEDED (hr); } From 54ae49ef31356862e3bea6710f2b691e1a24d494 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 21:41:10 -0500 Subject: [PATCH 5/8] Fix Direct2D text positioning - Fix move_to to always update current_pos even without geometry sink This is critical for text rendering since Graphics::draw_text calls move_to to set the baseline position before calling show_text - Fix font_metrics to properly check if metrics were retrieved before releasing COM objects (was always returning approximation) - Temporarily use Segoe UI instead of Roboto for testing (Roboto needs to be loaded from memory like GDI does) --- src/direct2d.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index b4993cc..85bb5b3 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -156,6 +156,9 @@ class Context : public DrawingContext { } void move_to (double x1, double y1) override { + // Always update current position (needed for text rendering) + current_pos = D2D1::Point2F (static_cast (x1), static_cast (y1)); + if (! geometrySink) return; @@ -164,7 +167,6 @@ class Context : public DrawingContext { figure_active = false; } - current_pos = D2D1::Point2F (static_cast (x1), static_cast (y1)); geometrySink->BeginFigure (current_pos, D2D1_FIGURE_BEGIN_FILLED); figure_active = true; } From 6e4706dbf80fdef3acefcf676cdd04a4434841d8 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 21:59:57 -0500 Subject: [PATCH 6/8] Fix Direct2D transform matrix multiplication order Direct2D matrix multiplication A * B applies B first, then A. We were doing currentTransform * newTransform, which applied the new transform before the current transform (backwards). Fixed by reversing to newTransform * currentTransform, so the current transform is applied first, then the new transform is applied on top of it. This fixes dial indicator lines now rotating correctly instead of gliding up and down. --- src/direct2d.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index 85bb5b3..e0769ef 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -172,8 +172,11 @@ class Context : public DrawingContext { } void line_to (double x1, double y1) override { - if (! geometrySink || ! figure_active) + if (! geometrySink || ! figure_active) { + // DEBUG: Check why this fails + // OutputDebugStringA (geometrySink ? "line_to: no active figure\n" : "line_to: no geometry sink\n"); return; + } D2D1_POINT_2F pt = D2D1::Point2F (static_cast (x1), static_cast (y1)); geometrySink->AddLine (pt); @@ -283,8 +286,8 @@ class Context : public DrawingContext { if (! rt) return; - D2D1_MATRIX_3X2_F transform; - rt->GetTransform (&transform); + D2D1_MATRIX_3X2_F currentTransform; + rt->GetTransform (¤tTransform); D2D1_MATRIX_3X2_F newTransform = D2D1::Matrix3x2F ( static_cast (mat.m00), @@ -294,7 +297,9 @@ class Context : public DrawingContext { static_cast (mat.m02), static_cast (mat.m12)); - rt->SetTransform (transform * newTransform); + // Matrix multiplication order: newTransform * currentTransform + // This applies current transform first, then new transform + rt->SetTransform (newTransform * currentTransform); } void clip (const Rectangle& r) override { From 6acb563d21454dd9966c2e1f49935171bb445080 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 22:03:31 -0500 Subject: [PATCH 7/8] fix: correct matrix multiplication order in Direct2D transform --- src/direct2d.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/direct2d.cpp b/src/direct2d.cpp index e0769ef..bafccf1 100644 --- a/src/direct2d.cpp +++ b/src/direct2d.cpp @@ -593,7 +593,9 @@ class Context : public DrawingContext { static_cast (matrix.m02), static_cast (matrix.m12)); - rt->SetTransform (oldTransform * newTransform); + // Matrix multiplication order: newTransform * oldTransform + // This applies old transform first, then image transform + rt->SetTransform (newTransform * oldTransform); // Draw the bitmap D2D1_RECT_F destRect = D2D1::RectF ( From 49e5419577ca0ab05e6d306e6a0915a44c5e70f6 Mon Sep 17 00:00:00 2001 From: Michael Fisher Date: Sun, 25 Jan 2026 22:03:51 -0500 Subject: [PATCH 8/8] Fix Direct2D image transform and document backend lessons - Fix draw_image to use correct matrix multiplication order (newTransform * oldTransform instead of oldTransform * newTransform) This fixes image alignment issues where images snapped left - Add comprehensive Graphics Backend Implementation Notes to copilot-instructions.md documenting all critical Direct2D and GDI backend lessons learned: * Matrix multiplication order (A * B applies B first, then A) * Text positioning requirements (move_to must update current_pos) * COM lifetime management for font metrics * Transform save/restore with D2D1_MATRIX_3X2_F * Clipping depth tracking * Path geometry lifecycle * DPI handling (96 DPI for 1:1 pixel mapping) * Resource management (device-independent vs device-dependent) * GDI+ comparison and patterns --- .github/copilot-instructions.md | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 571c2fc..fc3b50a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,3 +58,87 @@ When reviewing code: */ bool obscructed (int x, int y, int width, int height); ``` + +## Graphics Backend Implementation Notes + +### Direct2D Backend (src/direct2d.cpp, src/win_direct2d.cpp) + +**Critical Issues & Solutions:** + +1. **Matrix Multiplication Order** + - Direct2D matrix multiplication `A * B` applies B's transform FIRST, then A's transform + - When composing transforms: use `newTransform * currentTransform` to apply current first, then new + - This affects both `transform()` and `draw_image()` methods + - Wrong order causes rotation/translation to apply backwards (e.g., dial indicators gliding instead of rotating) + +2. **Text Positioning** + - `move_to()` MUST always update `current_pos` even when `geometrySink` is null + - Graphics layer calls `move_to()` to set baseline position before `show_text()` + - If `current_pos` isn't updated, text renders at (0,0) instead of intended position + - DirectWrite uses top-left positioning, so subtract ascent from baseline: `y - ascent` + +3. **Font Metrics COM Lifetime** + - Must check if metrics were retrieved BEFORE releasing COM objects + - Use a boolean flag (e.g., `gotMetrics`) set before `SafeRelease()` calls + - Checking `if (!font)` after `SafeRelease(&font)` always returns true (null pointer) + +4. **Transform Save/Restore** + - Must store `D2D1_MATRIX_3X2_F` in State struct for proper save/restore + - Call `rt->GetTransform()` in `save()` and `rt->SetTransform()` in `restore()` + - Without this, transforms compound incorrectly across save/restore boundaries + +5. **Clipping Management** + - Track clip depth with counter (`clip_depth`) in State struct + - `PushAxisAlignedClip()` increments depth, `PopAxisAlignedClip()` decrements + - In `restore()`, pop any extra clips pushed since the corresponding `save()` + - Initial frame clip counts as depth 1, cleaned up in `end_frame()` + +6. **Path Geometry Lifecycle** + - `ID2D1PathGeometry` is device-independent, created from factory + - `ID2D1GeometrySink` is obtained from `PathGeometry->Open()` + - Must call `BeginFigure()` before adding points, `EndFigure()` before drawing + - Track `figure_active` boolean to ensure proper figure state + - Call `geometrySink->Close()` before rendering geometry + - Geometry coordinates are NOT transformed; transform is applied during `DrawGeometry()` + +7. **DPI Handling** + - Use 96 DPI (identity mapping) in render target creation for 1:1 pixel coordinates + - This matches pugl's window size and mouse coordinate reporting + - Set both `dpiX` and `dpiY` to 96.0f in `D2D1_RENDER_TARGET_PROPERTIES` + +8. **Resource Management** + - Device-independent: `ID2D1Factory`, `IDWriteFactory` (created once, persist) + - Device-dependent: `ID2D1HwndRenderTarget` (recreated on window resize) + - Handle `D2DERR_RECREATE_TARGET` from `EndDraw()` by discarding device resources + - Use `SafeRelease()` template helper for consistent COM cleanup + +### GDI Backend (src/gdi.cpp, src/win_gdi.c) + +**Implementation Notes:** + +1. **GDI+ Transform Handling** + - `Graphics::MultiplyTransform()` multiplies current * new (same semantics as Direct2D) + - `GraphicsPath` accumulates points in local coordinates, transformed when drawn + - `Graphics::Save()` and `Graphics::Restore()` manage full graphics state including transform + +2. **Font Loading from Memory** + - Use `Gdiplus::PrivateFontCollection` for loading fonts from memory + - Call `AddMemoryFont()` with embedded font data (e.g., Roboto-Regular.h) + - Fonts persist for the lifetime of the PrivateFontCollection object + - Example pattern to follow when implementing custom font loading in Direct2D + +3. **Resource Cleanup** + - GDI+ uses smart pointers (`std::unique_ptr`) for automatic cleanup + - `Graphics`, `GraphicsPath`, `PrivateFontCollection` auto-release + - Manual cleanup still needed for some GDI handles (HDC, HBITMAP) + +4. **Coordinate System** + - GDI+ uses top-left origin like Direct2D (no flipping needed) + - Natural mapping to LUI's coordinate system + - Text baseline handling similar to Direct2D approach + +**Backend Comparison:** +- Direct2D: Lower-level, more control, explicit resource management, hardware-accelerated +- GDI+: Higher-level, more automatic, easier font handling, CPU-rendered +- Both use row-major 3x2 affine transformation matrices +- Both require careful attention to matrix multiplication order