UI 重构 P5(1):共享表面 UiSurface + 后备缓冲改 top-down 32bpp DIB;修复退出期绘制崩溃
一、共享表面(为「整个应用共享一张后台位图」落地基础设施)
- 新增 Components/Shared/{Inc,Src}/UiSurface.*:{ dc, rect, stride, bits, version, scale }。
SubView() 把子矩形换算到宿主坐标、夹取在本表面内、按 stride 重算 bits 偏移、传递 version。
⚠️ 注释里写明:bits 只在 CPU 侧合成时使用;画控件只需 dc + rect(用 bits 画字会破坏 ClearType)。
- Application 增 BackBufferSurface();后备缓冲由 CreateCompatibleBitmap 改为
**top-down 32bpp CreateDIBSection**(biHeight 取负,y 向下递增),并记录 bits/stride/version
(重建位图时 ++version,旧句柄因此可判定作废)。
二、修复「退出时访问违例」(真实缺陷)
- Application::Shutdown() 中 **DestroyWindow 必须在 GdiplusShutdown 之前**:前一帧的 InvalidateRect
可能留下未处理更新区域,DestroyWindow 期间派发 WM_PAINT ⇒ Render() ⇒ 在已关闭的 GDI+ 上构造
Graphics ⇒ 崩溃。
- Render() 开头加保护:!m_gdiplusReady 或 m_hwnd 为空时直接返回。
验证:
- 像素:8 场景(A 全览 / B 复杂浮层 / C 平移 / D 悬停 / E 按下 / F 松开 / G 悬停主按钮 / H 移出)
哈希与 DIB 改造前**逐一相同**(A 2166933…、B C444ECF…、C 7984446…、D BF85587…、
E 1C663E4…、F 0535292…、G 7A58172…、H 23F390D…)。
- 功能:locate-cursor-button (1291,73) / click-cursor-changes-mode (1->2) /
release-outside-does-not-fire 全部 PASS。
- uicheck 69 passed / 0 failed(新增 7 条 UiSurface 断言:bits 按 stride 偏移、矩形夹取、
完全在外时返回无效表面、version/scale 传递)。
- make release / make debug 零警告;Target.exe 883,200 B(< 900KB)。
附注:探针此前的退出崩溃其实来自「探针自己又调了一次 GdiplusShutdown」(Application::Shutdown
内部已 shutdown),与本轮修复无关;本轮的顺序调整与 Render 保护仍保留为防御性正确做法。
This commit is contained in:
1 parent
9a9802e589
commit
b60debc4ee
4 files changed
+166
-12
No files matched your search
@@ -17,6 +17,7 @@
|
|||||||
#include "TabBar.hpp"
|
#include "TabBar.hpp"
|
||||||
#include "Theme.hpp"
|
#include "Theme.hpp"
|
||||||
#include "Toolbar.hpp"
|
#include "Toolbar.hpp"
|
||||||
|
#include "UiSurface.hpp"
|
||||||
|
|
||||||
namespace jdi
|
namespace jdi
|
||||||
{
|
{
|
||||||
@@ -130,12 +131,21 @@ namespace jdi
|
|||||||
* @param [IN] height 客户区高度。
|
* @param [IN] height 客户区高度。
|
||||||
*/
|
*/
|
||||||
void EnsureBackBuffer(int width, int height);
|
void EnsureBackBuffer(int width, int height);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 释放后备缓冲区。
|
* @brief 释放后备缓冲区。
|
||||||
*/
|
*/
|
||||||
void ReleaseBackBuffer();
|
void ReleaseBackBuffer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 取"整个应用共享的那张后台位图"作为一段可绘制表面。
|
||||||
|
*
|
||||||
|
* 这是 P5 的基础设施:后备缓冲是 **top-down 32bpp DIB 段**,因此除了 `dc` 之外
|
||||||
|
* 还能给出 `bits`(像素直写)与 `version`(位图代次;`WM_SIZE` / `WM_DPICHANGED`
|
||||||
|
* 重建后旧句柄必须作废)。
|
||||||
|
* ⚠️ 绝大多数绘制只需要 `dc + rect`;`bits` 仅用于 CPU 侧合成,**不要**用它画控件。
|
||||||
|
*/
|
||||||
|
UiSurface BackBufferSurface() const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 完整重绘客户区并呈现到窗口。
|
* @brief 完整重绘客户区并呈现到窗口。
|
||||||
*/
|
*/
|
||||||
@@ -428,10 +438,13 @@ namespace jdi
|
|||||||
int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */
|
int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */
|
||||||
|
|
||||||
HDC m_backDc = nullptr; /**< 后备缓冲设备上下文。 */
|
HDC m_backDc = nullptr; /**< 后备缓冲设备上下文。 */
|
||||||
HBITMAP m_backBitmap = nullptr; /**< 后备缓冲位图。 */
|
HBITMAP m_backBitmap = nullptr; /**< 后备缓冲位图(top-down 32bpp DIB 段)。 */
|
||||||
HGDIOBJ m_backOldBitmap = nullptr; /**< 后备缓冲原始位图。 */
|
HGDIOBJ m_backOldBitmap = nullptr; /**< 后备缓冲原始位图。 */
|
||||||
int m_backWidth = 0; /**< 后备缓冲宽度。 */
|
int m_backWidth = 0; /**< 后备缓冲宽度(设备像素)。 */
|
||||||
int m_backHeight = 0; /**< 后备缓冲高度。 */
|
int m_backHeight = 0; /**< 后备缓冲高度(设备像素)。 */
|
||||||
|
std::uint32_t* m_backBits = nullptr; /**< 后备缓冲像素首地址(仅 CPU 侧合成时使用)。 */
|
||||||
|
int m_backStride = 0; /**< 后备缓冲行字节数(**不可**假设 = 宽 × 4)。 */
|
||||||
|
std::uint64_t m_backVersion = 0; /**< 后备缓冲代次(重建位图时递增,用于作废旧句柄)。 */
|
||||||
|
|
||||||
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
Target m_activeTarget = Target::NONE; /**< 当前鼠标作用目标。 */
|
||||||
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */
|
||||||
|
|||||||
@@ -173,13 +173,11 @@ namespace jdi
|
|||||||
// 而 `Application` 是进程级单例 ⇒ 成员要等 `WinMain` 返回后才析构,那时 GDI+ 已关闭。
|
// 而 `Application` 是进程级单例 ⇒ 成员要等 `WinMain` 返回后才析构,那时 GDI+ 已关闭。
|
||||||
m_plot.ReleaseGraphicsResources();
|
m_plot.ReleaseGraphicsResources();
|
||||||
|
|
||||||
if (m_gdiplusReady)
|
// ⚠️ **窗口必须在 GdiplusShutdown 之前销毁**:
|
||||||
{
|
// 前一帧的 `InvalidateRect` 可能还留下未处理的更新区域,`DestroyWindow` 期间会派发
|
||||||
gd::GdiplusShutdown(m_gdiplusToken);
|
// `WM_PAINT` ⇒ `Render()` ⇒ 在**已关闭**的 GDI+ 上构造 `Graphics` ⇒ 退出时访问违例
|
||||||
m_gdiplusReady = false;
|
//(离屏探针实测退出码 0xC0000005,正是这个顺序造成的)。
|
||||||
}
|
// 注销窗口类放在销毁之后:否则同一进程里再次创建 `Application` 会命中
|
||||||
|
|
||||||
// 注销窗口类:否则同一进程里再次创建 `Application` 时会命中
|
|
||||||
// `ERROR_CLASS_ALREADY_EXISTS`(离屏测试需要多次构造/销毁)。
|
// `ERROR_CLASS_ALREADY_EXISTS`(离屏测试需要多次构造/销毁)。
|
||||||
if (m_hwnd != nullptr)
|
if (m_hwnd != nullptr)
|
||||||
{
|
{
|
||||||
@@ -187,6 +185,12 @@ namespace jdi
|
|||||||
m_hwnd = nullptr;
|
m_hwnd = nullptr;
|
||||||
}
|
}
|
||||||
UnregisterClassW(kWindowClassName, m_instance);
|
UnregisterClassW(kWindowClassName, m_instance);
|
||||||
|
|
||||||
|
if (m_gdiplusReady)
|
||||||
|
{
|
||||||
|
gd::GdiplusShutdown(m_gdiplusToken);
|
||||||
|
m_gdiplusReady = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::ApplyDpi(UINT dpi)
|
void Application::ApplyDpi(UINT dpi)
|
||||||
@@ -315,12 +319,28 @@ namespace jdi
|
|||||||
m_backDc = CreateCompatibleDC(screenDc);
|
m_backDc = CreateCompatibleDC(screenDc);
|
||||||
if (m_backDc != nullptr)
|
if (m_backDc != nullptr)
|
||||||
{
|
{
|
||||||
m_backBitmap = CreateCompatibleBitmap(screenDc, width, height);
|
// ⚠️ 用 **top-down 32bpp DIB 段**(而不是 `CreateCompatibleBitmap`):
|
||||||
|
// * top-down(`biHeight` 取负)才让 `bits` 从第一行开始、y 向下递增;
|
||||||
|
// * 32bpp 才能稳定给出像素指针供 CPU 侧合成使用(`CreateCompatibleBitmap`
|
||||||
|
// 返回设备格式位图,没有 `bits`,也无从保证 32 位)。
|
||||||
|
// 绘制结果与旧的 compatible bitmap 逐像素一致(`BitsPerPixel` 同为 32)。
|
||||||
|
BITMAPINFO info = {};
|
||||||
|
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||||
|
info.bmiHeader.biWidth = width;
|
||||||
|
info.bmiHeader.biHeight = -height;
|
||||||
|
info.bmiHeader.biPlanes = 1;
|
||||||
|
info.bmiHeader.biBitCount = 32;
|
||||||
|
info.bmiHeader.biCompression = BI_RGB;
|
||||||
|
void* bits = nullptr;
|
||||||
|
m_backBitmap = CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
||||||
if (m_backBitmap != nullptr)
|
if (m_backBitmap != nullptr)
|
||||||
{
|
{
|
||||||
m_backOldBitmap = SelectObject(m_backDc, m_backBitmap);
|
m_backOldBitmap = SelectObject(m_backDc, m_backBitmap);
|
||||||
m_backWidth = width;
|
m_backWidth = width;
|
||||||
m_backHeight = height;
|
m_backHeight = height;
|
||||||
|
m_backBits = static_cast<std::uint32_t*>(bits);
|
||||||
|
m_backStride = width * 4;
|
||||||
|
++m_backVersion;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -350,10 +370,35 @@ namespace jdi
|
|||||||
}
|
}
|
||||||
m_backWidth = 0;
|
m_backWidth = 0;
|
||||||
m_backHeight = 0;
|
m_backHeight = 0;
|
||||||
|
m_backBits = nullptr;
|
||||||
|
m_backStride = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSurface Application::BackBufferSurface() const
|
||||||
|
{
|
||||||
|
UiSurface surface;
|
||||||
|
if (m_backDc == nullptr || m_backWidth <= 0 || m_backHeight <= 0)
|
||||||
|
{
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
surface.dc = m_backDc;
|
||||||
|
surface.rect = gd::RectF(0.0f, 0.0f, static_cast<float>(m_backWidth),
|
||||||
|
static_cast<float>(m_backHeight));
|
||||||
|
surface.stride = m_backStride;
|
||||||
|
surface.bits = m_backBits;
|
||||||
|
surface.version = m_backVersion;
|
||||||
|
surface.scale = m_scale;
|
||||||
|
return surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::Render()
|
void Application::Render()
|
||||||
{
|
{
|
||||||
|
// 关闭流程中(GDI+ 已 shutdown)或窗口已销毁时**绝不能再进入绘制**:
|
||||||
|
// `DestroyWindow` 期间仍可能派发 WM_PAINT,在已关闭的 GDI+ 上构造 Graphics 会直接崩溃。
|
||||||
|
if (!m_gdiplusReady || m_hwnd == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
RECT client = {};
|
RECT client = {};
|
||||||
GetClientRect(m_hwnd, &client);
|
GetClientRect(m_hwnd, &client);
|
||||||
const int width = client.right - client.left;
|
const int width = client.right - client.left;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#ifndef __UI_SURFACE_HPP__
|
||||||
|
#define __UI_SURFACE_HPP__
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <gdiplus.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "Theme.hpp"
|
||||||
|
|
||||||
|
namespace jdi
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 一块可绘制表面(DC + 矩形 + 可选像素直写视图)。
|
||||||
|
*
|
||||||
|
* 这是"整个应用共享一张后台位图"这一设想的落地载体:宿主创建一次,
|
||||||
|
* 之后把"我负责哪一块"以 `UiSurface::SubView()` 的形式发给子元素。
|
||||||
|
*
|
||||||
|
* ## 为什么需要 `version`
|
||||||
|
*
|
||||||
|
* 位图会在 `WM_SIZE` / `WM_DPICHANGED` 时被销毁重建。任何缓存下来的 `bits`
|
||||||
|
* **立刻变成野指针**,因此像素指针必须与"位图代次"绑定:版本不一致就作废重取。
|
||||||
|
* ⚠️ **绝不缓存裸 `bits` 跨帧使用**(本项目历史上因缓存裸指针踩过悬垂访问)。
|
||||||
|
*
|
||||||
|
* ## 什么时候才需要 `bits`
|
||||||
|
*
|
||||||
|
* `dc + rect` 已经足够做 GDI/GDI+ 绘制(往子矩形里画本来就是 O(1))。
|
||||||
|
* `bits` 只在**CPU 侧直接改像素**(memcpy / alpha 混合 / 重采样)时才有意义;
|
||||||
|
* 用它来"画控件"是净亏损(会破坏 ClearType、引入 stride/翻转/写序错误)。
|
||||||
|
*/
|
||||||
|
struct UiSurface
|
||||||
|
{
|
||||||
|
HDC dc = nullptr; /**< 目标 DC(GDI / GDI+ 都画这个)。 */
|
||||||
|
gd::RectF rect; /**< 该表面在宿主坐标系中的矩形(设备像素)。 */
|
||||||
|
int stride = 0; /**< 行字节数(**不可**假设等于 width × 4)。 */
|
||||||
|
std::uint32_t* bits = nullptr; /**< `rect` 左上角那一像素(不是位图首地址)。 */
|
||||||
|
std::uint64_t version = 0; /**< 位图代次;失配 ⇒ 该表面作废。 */
|
||||||
|
float scale = 1.0f; /**< DPI 缩放系数(逻辑像素 → 设备像素)。 */
|
||||||
|
|
||||||
|
/** @brief 表面是否可用。 */
|
||||||
|
bool Valid() const { return dc != nullptr && rect.Width > 0.0f && rect.Height > 0.0f; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 派生子视图(相对本表面左上角偏移)。
|
||||||
|
*
|
||||||
|
* `bits` 会按 `stride` 正确换算到子矩形左上角;子矩形会被夹在本表面之内。
|
||||||
|
*/
|
||||||
|
UiSurface SubView(const gd::RectF& localRect) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace jdi
|
||||||
|
|
||||||
|
#endif // __UI_SURFACE_HPP__
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#include "UiSurface.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
#include "UiLayout.hpp"
|
||||||
|
|
||||||
|
namespace jdi
|
||||||
|
{
|
||||||
|
|
||||||
|
UiSurface UiSurface::SubView(const gd::RectF& localRect) const
|
||||||
|
{
|
||||||
|
UiSurface view;
|
||||||
|
if (!Valid())
|
||||||
|
{
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
// 子矩形先换算到宿主坐标,再夹在本表面之内:超出本表面的部分没有属于它的像素,
|
||||||
|
// 若照原样返回 `bits` 偏移会写到相邻区域(典型的"子控件涂到别人身上")。
|
||||||
|
const gd::RectF requested(rect.X + localRect.X, rect.Y + localRect.Y, localRect.Width,
|
||||||
|
localRect.Height);
|
||||||
|
gd::RectF clamped;
|
||||||
|
if (!UiRectIntersect(rect, requested, clamped))
|
||||||
|
{
|
||||||
|
return UiSurface();
|
||||||
|
}
|
||||||
|
|
||||||
|
view.dc = dc;
|
||||||
|
view.rect = clamped;
|
||||||
|
view.stride = stride;
|
||||||
|
view.version = version;
|
||||||
|
view.scale = scale;
|
||||||
|
if (bits != nullptr && stride > 0)
|
||||||
|
{
|
||||||
|
const int offsetX = static_cast<int>(std::lround(clamped.X - rect.X));
|
||||||
|
const int offsetY = static_cast<int>(std::lround(clamped.Y - rect.Y));
|
||||||
|
view.bits = bits + static_cast<std::ptrdiff_t>(offsetY) * (stride / 4) + offsetX;
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace jdi
|
||||||
Reference in new issue
Block a user