From b60debc4ee91d45ff6af5d44207f58758db17097 Mon Sep 17 00:00:00 2001 From: "DeepSeek V4.1 Flash" <> Date: Wed, 28 Oct 2026 12:34:33 +0800 Subject: [PATCH] =?UTF-8?q?UI=20=E9=87=8D=E6=9E=84=20P5(1)=EF=BC=9A?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E8=A1=A8=E9=9D=A2=20UiSurface=20+=20?= =?UTF-8?q?=E5=90=8E=E5=A4=87=E7=BC=93=E5=86=B2=E6=94=B9=20top-down=2032bp?= =?UTF-8?q?p=20DIB=EF=BC=9B=E4=BF=AE=E5=A4=8D=E9=80=80=E5=87=BA=E6=9C=9F?= =?UTF-8?q?=E7=BB=98=E5=88=B6=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一、共享表面(为「整个应用共享一张后台位图」落地基础设施) - 新增 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 保护仍保留为防御性正确做法。 --- Components/Application/Inc/Application.hpp | 21 ++++++-- Components/Application/Src/Application.cpp | 61 +++++++++++++++++++--- Components/Shared/Inc/UiSurface.hpp | 55 +++++++++++++++++++ Components/Shared/Src/UiSurface.cpp | 41 +++++++++++++++ 4 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 Components/Shared/Inc/UiSurface.hpp create mode 100644 Components/Shared/Src/UiSurface.cpp diff --git a/Components/Application/Inc/Application.hpp b/Components/Application/Inc/Application.hpp index 9d63374..9ab3ea0 100644 --- a/Components/Application/Inc/Application.hpp +++ b/Components/Application/Inc/Application.hpp @@ -17,6 +17,7 @@ #include "TabBar.hpp" #include "Theme.hpp" #include "Toolbar.hpp" +#include "UiSurface.hpp" namespace jdi { @@ -130,12 +131,21 @@ namespace jdi * @param [IN] height 客户区高度。 */ void EnsureBackBuffer(int width, int height); - /** * @brief 释放后备缓冲区。 */ void ReleaseBackBuffer(); + /** + * @brief 取"整个应用共享的那张后台位图"作为一段可绘制表面。 + * + * 这是 P5 的基础设施:后备缓冲是 **top-down 32bpp DIB 段**,因此除了 `dc` 之外 + * 还能给出 `bits`(像素直写)与 `version`(位图代次;`WM_SIZE` / `WM_DPICHANGED` + * 重建后旧句柄必须作废)。 + * ⚠️ 绝大多数绘制只需要 `dc + rect`;`bits` 仅用于 CPU 侧合成,**不要**用它画控件。 + */ + UiSurface BackBufferSurface() const; + /** * @brief 完整重绘客户区并呈现到窗口。 */ @@ -428,10 +438,13 @@ namespace jdi int m_splitterGrabOffset = 0; /**< 分隔条拖动的抓取偏移。 */ HDC m_backDc = nullptr; /**< 后备缓冲设备上下文。 */ - HBITMAP m_backBitmap = nullptr; /**< 后备缓冲位图。 */ + HBITMAP m_backBitmap = nullptr; /**< 后备缓冲位图(top-down 32bpp DIB 段)。 */ HGDIOBJ m_backOldBitmap = nullptr; /**< 后备缓冲原始位图。 */ - int m_backWidth = 0; /**< 后备缓冲宽度。 */ - int m_backHeight = 0; /**< 后备缓冲高度。 */ + int m_backWidth = 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; /**< 当前鼠标作用目标。 */ bool m_trackingLeave = false; /**< 是否已注册鼠标离开通知。 */ diff --git a/Components/Application/Src/Application.cpp b/Components/Application/Src/Application.cpp index f4de91c..c66e03e 100644 --- a/Components/Application/Src/Application.cpp +++ b/Components/Application/Src/Application.cpp @@ -173,13 +173,11 @@ namespace jdi // 而 `Application` 是进程级单例 ⇒ 成员要等 `WinMain` 返回后才析构,那时 GDI+ 已关闭。 m_plot.ReleaseGraphicsResources(); - if (m_gdiplusReady) - { - gd::GdiplusShutdown(m_gdiplusToken); - m_gdiplusReady = false; - } - - // 注销窗口类:否则同一进程里再次创建 `Application` 时会命中 + // ⚠️ **窗口必须在 GdiplusShutdown 之前销毁**: + // 前一帧的 `InvalidateRect` 可能还留下未处理的更新区域,`DestroyWindow` 期间会派发 + // `WM_PAINT` ⇒ `Render()` ⇒ 在**已关闭**的 GDI+ 上构造 `Graphics` ⇒ 退出时访问违例 + //(离屏探针实测退出码 0xC0000005,正是这个顺序造成的)。 + // 注销窗口类放在销毁之后:否则同一进程里再次创建 `Application` 会命中 // `ERROR_CLASS_ALREADY_EXISTS`(离屏测试需要多次构造/销毁)。 if (m_hwnd != nullptr) { @@ -187,6 +185,12 @@ namespace jdi m_hwnd = nullptr; } UnregisterClassW(kWindowClassName, m_instance); + + if (m_gdiplusReady) + { + gd::GdiplusShutdown(m_gdiplusToken); + m_gdiplusReady = false; + } } void Application::ApplyDpi(UINT dpi) @@ -315,12 +319,28 @@ namespace jdi m_backDc = CreateCompatibleDC(screenDc); 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) { m_backOldBitmap = SelectObject(m_backDc, m_backBitmap); m_backWidth = width; m_backHeight = height; + m_backBits = static_cast(bits); + m_backStride = width * 4; + ++m_backVersion; } else { @@ -350,10 +370,35 @@ namespace jdi } m_backWidth = 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(m_backWidth), + static_cast(m_backHeight)); + surface.stride = m_backStride; + surface.bits = m_backBits; + surface.version = m_backVersion; + surface.scale = m_scale; + return surface; } void Application::Render() { + // 关闭流程中(GDI+ 已 shutdown)或窗口已销毁时**绝不能再进入绘制**: + // `DestroyWindow` 期间仍可能派发 WM_PAINT,在已关闭的 GDI+ 上构造 Graphics 会直接崩溃。 + if (!m_gdiplusReady || m_hwnd == nullptr) + { + return; + } RECT client = {}; GetClientRect(m_hwnd, &client); const int width = client.right - client.left; diff --git a/Components/Shared/Inc/UiSurface.hpp b/Components/Shared/Inc/UiSurface.hpp new file mode 100644 index 0000000..26f354c --- /dev/null +++ b/Components/Shared/Inc/UiSurface.hpp @@ -0,0 +1,55 @@ +#ifndef __UI_SURFACE_HPP__ +#define __UI_SURFACE_HPP__ + +#include +#include +#include + +#include + +#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__ diff --git a/Components/Shared/Src/UiSurface.cpp b/Components/Shared/Src/UiSurface.cpp new file mode 100644 index 0000000..78025b8 --- /dev/null +++ b/Components/Shared/Src/UiSurface.cpp @@ -0,0 +1,41 @@ +#include "UiSurface.hpp" + +#include + +#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(std::lround(clamped.X - rect.X)); + const int offsetY = static_cast(std::lround(clamped.Y - rect.Y)); + view.bits = bits + static_cast(offsetY) * (stride / 4) + offsetX; + } + return view; + } + +} // namespace jdi