Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .ai/memory/architecture/activex.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ The ActiveX rendering coordinator selects a backend for a Core render scene.
GDI is the CPU fallback; OpenGL has a narrower supported-scene range and can
fall back to GDI. Backends consume Core-produced geometry and do not own
datasets or view state. The OCX owns the host-specific rendering resources.
The control composes every frame in an offscreen DIB-section `FrameBuffer`
and presents it with a single `BitBlt`; `WM_ERASEBKGND` is suppressed because
`OnDraw` paints an opaque frame.

Core objects are not thread-safe by default. The control serializes their use.
ActiveX-specific implementation units are also compiled into
Expand Down
105 changes: 105 additions & 0 deletions src/MapWinGIS.ActiveX/FrameBuffer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MPL-2.0

/**
* @file FrameBuffer.cpp
* @brief Implements the offscreen frame buffer.
*/
#include "FrameBuffer.h"

namespace mwg::activex
{
namespace
{
[[nodiscard]] HBITMAP CreateBufferBitmap(
const HDC deviceContext,
const int width,
const int height) noexcept
{
BITMAPINFO bitmapInfo{};
bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapInfo.bmiHeader.biWidth = width;
bitmapInfo.bmiHeader.biHeight = -height; // top-down rows
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression = BI_RGB;

void* bits = nullptr;
return CreateDIBSection(deviceContext, &bitmapInfo, DIB_RGB_COLORS, &bits, nullptr, 0);
}
}

FrameBuffer::~FrameBuffer()
{
Release();
}

bool FrameBuffer::Resize(const int width, const int height) noexcept
{
if (width <= 0 || height <= 0)
{
return false;
}

if (IsValid() && width == _width && height == _height)
{
return true;
}

// Allocate the replacement before releasing the current buffer so a
// failed resize keeps a presentable buffer.
HDC memoryDc = CreateCompatibleDC(nullptr);
if (memoryDc == nullptr)
{
::OutputDebugStringW(L"MapWinGIS: could not create the frame buffer device context.\n");
return false;
}

const HBITMAP bitmap = CreateBufferBitmap(memoryDc, width, height);
if (bitmap == nullptr)
{
::OutputDebugStringW(L"MapWinGIS: could not allocate the frame buffer bitmap.\n");
DeleteDC(memoryDc);
return false;
}

const HBITMAP previousBitmap = static_cast<HBITMAP>(SelectObject(memoryDc, bitmap));
if (previousBitmap == nullptr || previousBitmap == HGDI_ERROR)
{
::OutputDebugStringW(L"MapWinGIS: could not select the frame buffer bitmap.\n");
DeleteObject(bitmap);
DeleteDC(memoryDc);
return false;
}

Release();
_memoryDc = memoryDc;
_bitmap = bitmap;
_previousBitmap = previousBitmap;
_width = width;
_height = height;
return true;
}

void FrameBuffer::Release() noexcept
{
if (_memoryDc != nullptr)
{
if (_previousBitmap != nullptr && _previousBitmap != HGDI_ERROR)
{
SelectObject(_memoryDc, _previousBitmap);
_previousBitmap = nullptr;
}
DeleteDC(_memoryDc);
_memoryDc = nullptr;
}

if (_bitmap != nullptr)
{
DeleteObject(_bitmap);
_bitmap = nullptr;
}

_width = 0;
_height = 0;
}
}
54 changes: 54 additions & 0 deletions src/MapWinGIS.ActiveX/FrameBuffer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MPL-2.0

/**
* @file FrameBuffer.h
* @brief Owns a resizable offscreen device context for flicker-free painting.
*
* Backs a memory device context with a 32-bit DIB section so a complete
* frame can be composed offscreen and presented to the visible surface with
* a single blit. This is host-rendering infrastructure owned by the OCX
* layer; it holds no scene, dataset, or view state.
*/
#pragma once

#include <Windows.h>

namespace mwg::activex
{
class FrameBuffer final
{
public:
FrameBuffer() noexcept = default;
~FrameBuffer();

FrameBuffer(const FrameBuffer&) = delete;
FrameBuffer& operator=(const FrameBuffer&) = delete;

[[nodiscard]] HDC Get() const noexcept { return _memoryDc; }
[[nodiscard]] int Width() const noexcept { return _width; }
[[nodiscard]] int Height() const noexcept { return _height; }

/// True when Get returns a device context with the bitmap selected.
[[nodiscard]] bool IsValid() const noexcept
{
return _memoryDc != nullptr && _bitmap != nullptr;
}

/// Recreates the buffer when the size changed and keeps the current
/// bitmap for an equal size. Returns false for non-positive sizes
/// and allocation failures; a failed resize leaves the previous
/// buffer intact so it can still be presented.
[[nodiscard]] bool Resize(int width, int height) noexcept;

/// Releases the device context and bitmap; the next Resize
/// recreates them.
void Release() noexcept;

private:
HDC _memoryDc = nullptr;
HBITMAP _bitmap = nullptr;
HBITMAP _previousBitmap = nullptr;
int _width = 0;
int _height = 0;
};
}
2 changes: 2 additions & 0 deletions src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.Impl.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@

<ItemGroup>
<ClInclude Include="ComConversion.h" />
<ClInclude Include="FrameBuffer.h" />
<ClInclude Include="GdalOperationDispatcher.h" />
<ClInclude Include="GdiVectorLayerRenderer.h" />
<ClInclude Include="OpenGlVectorLayerRenderer.h" />
Expand All @@ -57,6 +58,7 @@

<ItemGroup>
<ClCompile Include="ComConversion.cpp" />
<ClCompile Include="FrameBuffer.cpp" />
<ClCompile Include="GdalOperationDispatcher.cpp" />
<ClCompile Include="GdiVectorLayerRenderer.cpp" />
<ClCompile Include="OpenGlVectorLayerRenderer.cpp" />
Expand Down
1 change: 1 addition & 0 deletions src/MapWinGIS.ActiveX/MapWinGIS.ActiveX.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
<ItemGroup>
<ClInclude Include="ActiveXModule.h" />
<ClInclude Include="ComConversion.h" />
<ClInclude Include="FrameBuffer.h" />
<ClInclude Include="GdalDispatchWindow.h" />
<ClInclude Include="GdalEventSource.h" />
<ClInclude Include="GdalOperationDispatcher.h" />
Expand Down
128 changes: 95 additions & 33 deletions src/MapWinGIS.ActiveX/MapWinGISControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
*
* Redraws: every map or view change made through the control goes through
* its MapView; the control calls FireViewChange when the MapView reports
* that the frame changed. OnDraw draws the MapView's render scene through
* its viewport, so every layer uses the same transformation.
* that the frame changed. OnDraw composes the MapView's render scene through
* its viewport into an offscreen frame buffer and presents the finished
* frame with a single blit, so every layer uses the same transformation and
* the visible surface never shows a partially drawn frame.
*/
#include "MapWinGISControl.h"

Expand Down Expand Up @@ -1041,46 +1043,106 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo)
drawInfo.prcBounds->right,
drawInfo.prcBounds->bottom};

if (::FillRect(
drawInfo.hdcDraw,
&bounds,
::GetSysColorBrush(COLOR_WINDOW)) == 0)
{
return E_FAIL;
}

const LONGLONG width = static_cast<LONGLONG>(bounds.right) - bounds.left;
const LONGLONG height = static_cast<LONGLONG>(bounds.bottom) - bounds.top;
if (width <= 0 || height <= 0 ||
width > mwg::Viewport::kMaxDimension || height > mwg::Viewport::kMaxDimension)
{
return S_OK;
}
try
{
const mwg::rendering::MapRenderScene& scene = _view.Scene();
// Printing and scaled draws use a snapshot, never the live viewport.
mwg::Viewport viewport = _view.GetViewport();
viewport.SetSize(static_cast<int>(width), static_cast<int>(height));
if (!viewport.HasView())
const bool drawable = width > 0 && height > 0 &&
width <= mwg::Viewport::kMaxDimension && height <= mwg::Viewport::kMaxDimension;

// Compose the frame in an offscreen buffer and present it with a single
// blit, so the visible surface never shows the background erase or a
// partially drawn frame. Drawing straight to the target device context
// is the fallback for sizes the buffer cannot hold.
const bool buffered = drawable &&
_frameBuffer.Resize(
static_cast<int>(width),
static_cast<int>(height));
HDC frameDc = buffered ? _frameBuffer.Get() : drawInfo.hdcDraw;
RECT frameBounds = buffered
? RECT{0, 0, static_cast<LONG>(width), static_cast<LONG>(height)}
: bounds;

const auto renderFrame = [&]() -> HRESULT
{
if (::FillRect(
frameDc,
&frameBounds,
::GetSysColorBrush(COLOR_WINDOW)) == 0)
{
(void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding);
return E_FAIL;
}
if (scene.layers.empty() || !viewport.IsValid())

if (!drawable)
{
return S_OK;
}
return _renderer.Draw(drawInfo.hdcDraw, bounds, scene, viewport);
}
catch (const std::bad_alloc&)
{
return E_OUTOFMEMORY;
}
catch (const std::exception& exception)

try
{
const mwg::rendering::MapRenderScene& scene = _view.Scene();
// Printing and scaled draws use a snapshot, never the live viewport.
mwg::Viewport viewport = _view.GetViewport();
viewport.SetSize(static_cast<int>(width), static_cast<int>(height));
if (!viewport.HasView())
{
(void)viewport.FitExtent(_view.GetMap().GetExtent(), mwg::MapView::kDefaultPadding);
}
if (!scene.layers.empty() && viewport.IsValid())
{
return _renderer.Draw(frameDc, frameBounds, scene, viewport);
}
return S_OK;
}
catch (const std::bad_alloc&)
{
return E_OUTOFMEMORY;
}
catch (const std::exception& exception)
{
::OutputDebugStringA(exception.what());
return E_FAIL;
}
};

if (buffered)
{
::OutputDebugStringA(exception.what());
return E_FAIL;
const HRESULT result = renderFrame();
if (FAILED(result))
{
return result;
}
if (::BitBlt(
drawInfo.hdcDraw,
bounds.left,
bounds.top,
static_cast<int>(width),
static_cast<int>(height),
frameDc,
0,
0,
SRCCOPY) != 0)
{
return result;
}

// A blit between different devices (printer or metafile targets)
// can fail; present the frame by drawing straight to the target.
frameDc = drawInfo.hdcDraw;
frameBounds = bounds;
}

return renderFrame();
}

LRESULT CMapWinGISControl::OnEraseBkgnd(
UINT,
WPARAM,
LPARAM,
BOOL& handled)
{
// OnDraw paints an opaque frame over the whole bounds, so erasing the
// background first would only flash the host's color through.
handled = TRUE;
return 1;
}

CMapWinGISControl::~CMapWinGISControl()
Expand Down
4 changes: 4 additions & 0 deletions src/MapWinGIS.ActiveX/MapWinGISControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <atlctl.h>

#include "mapwingis/map/MapView.h"
#include "FrameBuffer.h"
#include "GdalEventSource.h"
#include "VectorLayerRenderer.h"

Expand Down Expand Up @@ -98,6 +99,7 @@ class ATL_NO_VTABLE CMapWinGISControl :
MESSAGE_HANDLER(WM_CANCELMODE, OnCancelMode)
MESSAGE_HANDLER(WM_SIZE, OnSize)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd)
CHAIN_MSG_MAP(ATL::CComControl<CMapWinGISControl>)
END_MSG_MAP()

Expand Down Expand Up @@ -149,6 +151,7 @@ class ATL_NO_VTABLE CMapWinGISControl :
LRESULT OnSize(UINT, WPARAM, LPARAM, BOOL& handled);
LRESULT OnGdalDispatch(UINT, WPARAM, LPARAM, BOOL& handled);
LRESULT OnDestroy(UINT, WPARAM, LPARAM, BOOL& handled);
LRESULT OnEraseBkgnd(UINT, WPARAM, LPARAM, BOOL& handled);
static void DispatchGdalNotifications(void* control) noexcept;
HRESULT OnDraw(ATL_DRAWINFO& drawInfo);
~CMapWinGISControl();
Expand All @@ -166,6 +169,7 @@ class ATL_NO_VTABLE CMapWinGISControl :

mwg::MapView _view;
LONGLONG _featureCount = 0;
mwg::activex::FrameBuffer _frameBuffer;
mwg::activex::VectorLayerRenderer _renderer;
mwg::activex::GdalEventSource _gdalEvents;
POINT _lastPanPoint{};
Expand Down
3 changes: 2 additions & 1 deletion src/MapWinGIS.ActiveX/OpenGlVectorLayerRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,8 @@ namespace mwg::activex
if (!IsSupported(scene))
{
_lastFailureReason =
L"The scene uses geometry that the OpenGL backend does not support.";
L"The OpenGL preview backend supports only convex, hole-free "
L"polygons; this frame was rendered with GDI.";
return {S_FALSE, std::nullopt};
}

Expand Down
Loading