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
218 changes: 213 additions & 5 deletions src/MapWinGIS.ActiveX/MapWinGISControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
* 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.
*
* Pan caching: the last fully rendered layer frame is kept in a second
* offscreen buffer. While the user drags the map (WM_LBUTTONDOWN through
* WM_LBUTTONUP), OnDraw paints the background and blits the cached frame at
* the accumulated pixel offset instead of re-rasterizing every layer on
* each WM_MOUSEMOVE; the drag-end handler invalidates the cache so the new
* view is rasterized once. The cache is invalidated on layer add/remove/
* clear, zoom, resize, and backend-preference changes.
*/
#include "MapWinGISControl.h"

Expand Down Expand Up @@ -470,6 +478,7 @@ HRESULT CMapWinGISControl::LoadVectorDatasetImpl(
{
*layerId = convertedId;
}
InvalidateLayerCache();
FireViewChange();
return S_OK;
}
Expand All @@ -492,6 +501,7 @@ STDMETHODIMP CMapWinGISControl::ClearLayers()
if (_view.ClearLayers())
{
_featureCount = 0;
InvalidateLayerCache();
FireViewChange();
}
return S_OK;
Expand Down Expand Up @@ -570,6 +580,7 @@ STDMETHODIMP CMapWinGISControl::put_RenderingBackendPreference(BSTR preference)
return E_INVALIDARG;
}

InvalidateLayerCache();
FireViewChange();
return S_OK;
}
Expand All @@ -582,6 +593,7 @@ STDMETHODIMP CMapWinGISControl::FitToMap()
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}
return result;
Expand All @@ -595,6 +607,7 @@ STDMETHODIMP CMapWinGISControl::ZoomIn(const DOUBLE factor)
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}
return result;
Expand All @@ -608,6 +621,7 @@ STDMETHODIMP CMapWinGISControl::ZoomOut(const DOUBLE factor)
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}
return result;
Expand All @@ -623,6 +637,7 @@ STDMETHODIMP CMapWinGISControl::Pan(
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}
return result;
Expand All @@ -642,6 +657,7 @@ STDMETHODIMP CMapWinGISControl::ZoomAt(
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}
return result;
Expand All @@ -665,6 +681,7 @@ STDMETHODIMP CMapWinGISControl::ZoomToLayer(const LONGLONG layerId)
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}
return result;
Expand Down Expand Up @@ -750,10 +767,32 @@ void CMapWinGISControl::CancelMousePan() noexcept
if (_isPanning)
{
_isPanning = false;
// Drop the cache even when the control is closing or being destroyed:
// a reactivated control must never present a frame from a drag that
// ended without a final render.
InvalidateLayerCache();
ReleaseMouseCapture();
}
}

void CMapWinGISControl::EndMousePan() noexcept
{
if (!_isPanning)
{
return;
}

CancelMousePan();
// The drag ended: rasterize the layers once at the final view so the
// cached frame matches the new center exactly (a clamped pan can leave
// the blitted frame up to half a pixel off). Every drag termination
// path (button-up, capture loss, cancel mode, button released) goes
// through here, so a terminated drag never leaves the shifted cached
// frame on screen.
InvalidateLayerCache();
FireViewChange();
}

bool CMapWinGISControl::GetMousePoint(
const LPARAM lParam,
const bool screenCoordinates,
Expand Down Expand Up @@ -821,7 +860,7 @@ LRESULT CMapWinGISControl::OnMouseMove(

if ((wParam & MK_LBUTTON) == 0)
{
CancelMousePan();
EndMousePan();
handled = TRUE;
return 0;
}
Expand Down Expand Up @@ -849,6 +888,8 @@ LRESULT CMapWinGISControl::OnMouseMove(
return 0;
}

// The drag pan changes only the view center, so OnDraw can serve the
// frame from the layer cache; the cache is dropped on button-up.
bool changed = false;
const HRESULT result = RunViewOperation(
[this, pixelDeltaX, pixelDeltaY, resolution]()
Expand Down Expand Up @@ -879,7 +920,7 @@ LRESULT CMapWinGISControl::OnLButtonUp(
return 0;
}

CancelMousePan();
EndMousePan();
handled = TRUE;
return 0;
}
Expand Down Expand Up @@ -917,6 +958,7 @@ LRESULT CMapWinGISControl::OnMouseWheel(
changed);
if (SUCCEEDED(result) && changed)
{
InvalidateLayerCache();
FireViewChange();
}

Expand All @@ -930,7 +972,7 @@ LRESULT CMapWinGISControl::OnCaptureChanged(
LPARAM,
BOOL& handled)
{
_isPanning = false;
EndMousePan();
handled = TRUE;
return 0;
}
Expand All @@ -941,7 +983,7 @@ LRESULT CMapWinGISControl::OnCancelMode(
LPARAM,
BOOL& handled)
{
CancelMousePan();
EndMousePan();
handled = TRUE;
return 0;
}
Expand All @@ -955,6 +997,7 @@ LRESULT CMapWinGISControl::OnSize(
_gdalEvents.OnSize();
if (ResizeView(LOWORD(lParam), HIWORD(lParam)))
{
InvalidateLayerCache();
FireViewChange();
}
handled = TRUE;
Expand Down Expand Up @@ -1024,12 +1067,161 @@ STDMETHODIMP CMapWinGISControl::SetObjectRects(LPCRECT position, LPCRECT clip)
static_cast<LONG>(std::clamp<LONGLONG>(width, 0, mwg::Viewport::kMaxDimension)),
static_cast<LONG>(std::clamp<LONGLONG>(height, 0, mwg::Viewport::kMaxDimension))))
{
InvalidateLayerCache();
FireViewChange();
}
}
return result;
}

void CMapWinGISControl::InvalidateLayerCache() noexcept
{
_layerCacheValid = false;
}

bool CMapWinGISControl::TryBlitCachedLayers(
HDC frameDc,
const mwg::Viewport& viewport) noexcept
{
if (!_layerCacheValid ||
!_layerCache.IsValid() ||
_layerCache.Width() != viewport.Width() ||
_layerCache.Height() != viewport.Height())
{
return false;
}

const std::optional<double> resolution = viewport.Resolution();
const std::optional<double> cacheResolution = _layerCacheViewport.Resolution();
const std::optional<mwg::MapPoint> center = viewport.Center();
const std::optional<mwg::MapPoint> cacheCenter = _layerCacheViewport.Center();
if (!resolution.has_value() ||
!cacheResolution.has_value() ||
!center.has_value() ||
!cacheCenter.has_value() ||
*resolution != *cacheResolution)
{
// A resolution change (or a missing view) needs a re-render.
return false;
}

// The cached background must match the color the frame is cleared with;
// a system-color or high-contrast change would otherwise leave the old
// background showing through an otherwise valid cache.
if (::GetSysColor(COLOR_WINDOW) != _layerCacheBackground)
{
return false;
}

// Screen position of a fixed map point shifts by the center difference
// divided by the resolution, with the Y axis pointing down. The center
// accumulates rounding through repeated pan operations, so the offset can
// sit a tiny amount off an integer; anything within half a pixel rounds to
// the nearest pixel, which is imperceptible during the drag and corrected
// by the final render when the drag ends.
const double offsetX = (cacheCenter->x - center->x) / *resolution;
const double offsetY = (center->y - cacheCenter->y) / *resolution;
const LONG shiftX = static_cast<LONG>(std::lround(offsetX));
const LONG shiftY = static_cast<LONG>(std::lround(offsetY));
if (std::abs(offsetX - static_cast<double>(shiftX)) >= 0.5 ||
std::abs(offsetY - static_cast<double>(shiftY)) >= 0.5)
{
// A clamped pan (or extreme accumulated drift) leaves a sub-pixel-or-
// worse offset; re-render so the frame does not jitter between a
// shifted blit and a true render.
return false;
}

const LONG width = static_cast<LONG>(viewport.Width());
const LONG height = static_cast<LONG>(viewport.Height());

// Destination (x, y) shows cache pixel (x - shiftX, y - shiftY); blit the
// rectangle where that source position is inside the cached frame. The
// rest of the destination keeps the background already painted.
const LONG destLeft = std::max<LONG>(0, shiftX);
const LONG destTop = std::max<LONG>(0, shiftY);
const LONG destRight = std::min<LONG>(width, width + shiftX);
const LONG destBottom = std::min<LONG>(height, height + shiftY);
if (destRight <= destLeft || destBottom <= destTop)
{
// The cached content is entirely outside the frame.
return true;
}

return ::BitBlt(
frameDc,
destLeft,
destTop,
destRight - destLeft,
destBottom - destTop,
_layerCache.Get(),
destLeft - shiftX,
destTop - shiftY,
SRCCOPY) != 0;
}

HRESULT CMapWinGISControl::DrawLayersAndUpdateCache(
HDC frameDc,
const RECT& frameBounds,
const mwg::rendering::MapRenderScene& scene,
const mwg::Viewport& viewport,
const bool useCache)
{
if (!useCache)
{
return _renderer.Draw(frameDc, frameBounds, scene, viewport);
}

const int width = viewport.Width();
const int height = viewport.Height();
if (!_layerCache.Resize(width, height))
{
// Without a cache the layers are drawn straight into the frame; the
// next draw retries the allocation.
return _renderer.Draw(frameDc, frameBounds, scene, viewport);
}

HDC cacheDc = _layerCache.Get();
const RECT cacheBounds{0, 0, width, height};
if (::FillRect(
cacheDc,
&cacheBounds,
::GetSysColorBrush(COLOR_WINDOW)) == 0)
{
return _renderer.Draw(frameDc, frameBounds, scene, viewport);
}

const HRESULT result = _renderer.Draw(cacheDc, cacheBounds, scene, viewport);
if (FAILED(result))
{
_layerCacheValid = false;
return result;
}

_layerCacheViewport = viewport;
_layerCacheBackground = ::GetSysColor(COLOR_WINDOW);
_layerCacheValid = true;

if (::BitBlt(
frameDc,
frameBounds.left,
frameBounds.top,
width,
height,
cacheDc,
0,
0,
SRCCOPY) != 0)
{
return result;
}

// A blit between different devices (printer or metafile targets) can
// fail; present the layers by drawing straight to the target.
_layerCacheValid = false;
return _renderer.Draw(frameDc, frameBounds, scene, viewport);
}

HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo)
{
if (drawInfo.hdcDraw == nullptr || drawInfo.prcBounds == nullptr)
Expand Down Expand Up @@ -1061,6 +1253,12 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo)
? RECT{0, 0, static_cast<LONG>(width), static_cast<LONG>(height)}
: bounds;

// The cache holds a viewport-sized bitmap that is blitted at the frame
// origin; it is only valid while the frame is the offscreen buffer. A
// failed presentation blit switches to the target DC and bounds, and
// must not use the cache.
bool useLayerCache = buffered;

const auto renderFrame = [&]() -> HRESULT
{
if (::FillRect(
Expand Down Expand Up @@ -1088,7 +1286,16 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo)
}
if (!scene.layers.empty() && viewport.IsValid())
{
return _renderer.Draw(frameDc, frameBounds, scene, viewport);
if (useLayerCache && TryBlitCachedLayers(frameDc, viewport))
{
return S_OK;
}
return DrawLayersAndUpdateCache(
frameDc,
frameBounds,
scene,
viewport,
useLayerCache);
}
return S_OK;
}
Expand Down Expand Up @@ -1128,6 +1335,7 @@ HRESULT CMapWinGISControl::OnDraw(ATL_DRAWINFO& drawInfo)
// can fail; present the frame by drawing straight to the target.
frameDc = drawInfo.hdcDraw;
frameBounds = bounds;
useLayerCache = false;
}

return renderFrame();
Expand Down
Loading
Loading