ReactOS GDI DIB 边界计算 Bug 修复记录

ReactOS GDI DIB 边界计算 Bug 修复记录

概述

本文档记录了在 ReactOS GDI 子系统中发现并修复的多个与设备无关位图(DIB)边界计算相关的 Bug。这些问题导致 gdi32:dib 测试用例中的边界断言失败,具体表现为裁剪区域边界错误、RTL(从右到左)布局边界错误、负尺寸 StretchDIBits 边界错误以及 DIBSECTION 位字段错误。


问题 1:DIBSECTION 位字段在 BI_RGB 格式下不为 0

文件

d:\\reactos\\win32ss\\gdi\\ntgdi\\bitmaps.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitmaps.c)

问题描述

测试用例 dib.c:3179-3181 断言失败:

复制代码
got 0x00ff0000, 0x0000ff00, 0x000000ff

当 DIBSECTION 使用 BI_RGB 压缩格式时,dsBitfields 数组应全部为 0。但原始代码无条件地填充了位字段值,导致断言失败。

修复内容

EngCreateDIBSECTION 函数中,仅在 BI_BITFIELDS 压缩格式时填充 dsBitfields 字段:

c 复制代码
if (pds->dsBmih.biCompression == BI_BITFIELDS)
{
    pds->dsBitfields[0] = psurf->ppal->RedMask;
    pds->dsBitfields[1] = psurf->ppal->GreenMask;
    pds->dsBitfields[2] = psurf->ppal->BlueMask;
}
else
{
    pds->dsBitfields[0] = 0;
    pds->dsBitfields[1] = 0;
    pds->dsBitfields[2] = 0;
}

改动要点

变更 说明
条件判断 添加 BI_BITFIELDS 格式检查
非 BITFIELDS 格式 将位字段清零

问题 2:裁剪边界计算错误

文件

d:\\reactos\\win32ss\\gdi\\ntgdi\\dcutil.c(file:///d:/reactos/win32ss/gdi/ntgdi/dcutil.c)

问题描述

测试用例断言失败:

复制代码
expected (10,99)-(300,200) got (10,99)-(290,190)

右边界和下边界各少 10 像素。

根因分析

原始代码错误地使用区域宽度/高度来裁剪边界:

c 复制代码
rc.right  = min( rc.right,  rcRgn.right - rcRgn.left );
rc.bottom = min( rc.bottom, rcRgn.bottom - rcRgn.top );

rcRgn.right - rcRgn.left 计算的是区域宽度,但正确做法应该直接使用区域边界值。

修复内容

修改边界裁剪逻辑,直接使用区域边界值:

c 复制代码
rc.right  = min( rc.right,  rcRgn.right );
rc.bottom = min( rc.bottom, rcRgn.bottom );

问题 3:RTL 布局边界错误(右边界差 1 像素)

文件

d:\\reactos\\win32ss\\gdi\\ntgdi\\dcutil.c(file:///d:/reactos/win32ss/gdi/ntgdi/dcutil.c)

问题描述

测试用例断言失败:

复制代码
expected bounds (300,10)-(9,260) got (300,10)-(10,260)

在 RTL(从右到左)布局模式下,右边界差 1 像素。

根因分析

  1. ReactOS 的 RTL 实现缺陷DC_vSetLayout 没有实际镜像 x 轴坐标(viewport_extent_x 未设为 -1),坐标映射是恒等映射。
  2. 排他性矩形语义 :Win32 矩形使用排他性边界(exclusive bounds),即 right 表示最后一个像素 + 1。交换 left/right 后,原始 left(已成为 right)需要减 1 才能保持排他性语义。

修复内容

NtGdiGetBoundsRect 函数的 RTL 交换代码后添加边界调整:

c 复制代码
if (pdc->pdcattr->dwLayout & LAYOUT_RTL)
{
    LONG temp = rc.left;
    rc.left = rc.right;
    rc.right = temp;
    rc.right--;
}

改动要点

变更 说明
坐标交换 交换 left 和 right 以保持 RTL 方向
边界调整 rc.right-- 处理排他性矩形语义

问题 4:LPtoDP 后反转矩形导致边界计算错误

文件

d:\\reactos\\win32ss\\gdi\\ntgdi\\bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c)

d:\\reactos\\win32ss\\gdi\\ntgdi\\fillshap.c(file:///d:/reactos/win32ss/gdi/ntgdi/fillshap.c)

问题描述

在 RTL 模式下,IntLPtoDP 转换后矩形坐标会反转(left > right),但 RECTL_bUnionRect 不会规范化矩形,导致边界计算错误。

修复内容

在以下函数的 IntLPtoDP 转换后添加矩形规范化:

修复 1:IntPatBlt(bitblt.c)
c 复制代码
IntLPtoDP(pdc, (LPPOINT)&DestRect, 2);

DestRect.left   += pdc->ptlDCOrig.x;
DestRect.top    += pdc->ptlDCOrig.y;
DestRect.right  += pdc->ptlDCOrig.x;
DestRect.bottom += pdc->ptlDCOrig.y;

RECTL_vMakeWellOrdered(&DestRect);

if (pdc->fs & (DC_ACCUM_APP|DC_ACCUM_WMGR))
{
   IntUpdateBoundsRect(pdc, &DestRect);
}
修复 2:Rectangle(fillshap.c)
c 复制代码
IntLPtoDP(dc, (LPPOINT)&DestRect, 2);

DestRect.left   += dc->ptlDCOrig.x;
DestRect.right  += dc->ptlDCOrig.x;
DestRect.top    += dc->ptlDCOrig.y;
DestRect.bottom += dc->ptlDCOrig.y;

RECTL_vMakeWellOrdered(&DestRect);

if (dc->fs & (DC_ACCUM_APP|DC_ACCUM_WMGR))
{
   IntUpdateBoundsRect(dc, &DestRect);
}
修复 3:GradientFill(fillshap.c)
c 复制代码
IntLPtoDP(pdc, (LPPOINT)&rclExtent, 2);

rclExtent.left   += pdc->ptlDCOrig.x;
rclExtent.right  += pdc->ptlDCOrig.x;
rclExtent.top    += pdc->ptlDCOrig.y;
rclExtent.bottom += pdc->ptlDCOrig.y;

RECTL_vMakeWellOrdered(&rclExtent);

if (RECTL_bIsEmptyRect(&rclExtent))

改动要点

位置 函数 说明
bitblt.c IntPatBlt LPtoDP 后规范化矩形
fillshap.c Rectangle LPtoDP 后规范化矩形
fillshap.c GradientFill LPtoDP 后规范化矩形

问题 5:负尺寸 StretchDIBits 边界错误

文件

d:\\reactos\\win32ss\\gdi\\ntgdi\\dibobj.c(file:///d:/reactos/win32ss/gdi/ntgdi/dibobj.c)

d:\\reactos\\win32ss\\gdi\\ntgdi\\bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c)

问题描述

测试用例断言失败:

复制代码
expected (10,8)-(60,104) got (10,10)-(43,104)

使用负尺寸参数调用 StretchDIBits 时边界计算错误。

根因分析

  1. dibobj.c 中的负尺寸处理 :直接使用 rcDest.left + Width 计算右边界,当 Width 为负数时产生反转矩形。
  2. bitblt.c 中的 MaskBlt 路径 :当 cxSrc == cxDst && cySrc == cyDst 时,SRCCOPY 走 NtGdiMaskBlt 而非 NtGdiStretchBltNtGdiMaskBlt 创建反转矩形后,RECTL_bIsEmptyRect 返回 TRUE,导致 IntUpdateBoundsRect 跳过边界更新。

修复内容

修复 1:dibobj.c - 负尺寸矩形规范化
c 复制代码
if (WidthDest < 0)
{
    DestRect.left = XOriginDest + WidthDest;
    DestRect.right = XOriginDest;
}
else
{
    DestRect.left = XOriginDest;
    DestRect.right = XOriginDest + WidthDest;
}

if (HeightDest < 0)
{
    DestRect.top = YOriginDest + HeightDest;
    DestRect.bottom = YOriginDest;
}
else
{
    DestRect.top = YOriginDest;
    DestRect.bottom = YOriginDest + HeightDest;
}
修复 2:bitblt.c - NtGdiMaskBlt 边界规范化
c 复制代码
DestRect.left   = nXDest;
DestRect.top    = nYDest;
DestRect.right  = nXDest + nWidth;
DestRect.bottom = nYDest + nHeight;
IntLPtoDP(DCDest, (LPPOINT)&DestRect, 2);

DestRect.left   += DCDest->ptlDCOrig.x;
DestRect.top    += DCDest->ptlDCOrig.y;
DestRect.right  += DCDest->ptlDCOrig.x;
DestRect.bottom += DCDest->ptlDCOrig.y;

if (DestRect.left > DestRect.right)
{
    LONG temp = DestRect.left;
    DestRect.left = DestRect.right;
    DestRect.right = temp;
}
if (DestRect.top > DestRect.bottom)
{
    LONG temp = DestRect.top;
    DestRect.top = DestRect.bottom;
    DestRect.bottom = temp;
}

if (DCDest->fs & (DC_ACCUM_APP|DC_ACCUM_WMGR))
{
   IntUpdateBoundsRect(DCDest, &DestRect);
}

问题 6:rosautotest journal 目录创建失败

文件

d:\\reactos\\modules\\rostests\\rosautotest\\CJournaledTestList.cpp(file:///d:/reactos/modules/rostests/rosautotest/CJournaledTestList.cpp)

问题描述

rosautotest 因 journal 目录不存在而崩溃:

复制代码
CreateFileW failed (Error 3: PATH_NOT_FOUND)

修复内容

在目录创建失败时禁用 journal 功能:

cpp 复制代码
if (!CreateDirectory(journalPath.c_str(), NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
{
    DPRINT("Failed to create journal directory %s, disabling journal\n", journalPath.c_str());
    return;
}

编译验证

powershell 复制代码
# 设置环境变量
[Environment]::SetEnvironmentVariable("M4", "C:\RosBE\bin\m4.exe", "Process")
[Environment]::SetEnvironmentVariable("BISON_PKGDATADIR", "C:\RosBE\share\bison", "Process")
$env:PATH = "C:\RosBE\bin;" + $env:PATH

# 编译 win32k(问题 2-5)
cd d:\reactos\output-MinGW-i386
C:\RosBE\bin\ninja.exe win32k

# 编译 rosautotest(问题 6)
C:\RosBE\bin\ninja.exe rosautotest

# 生成 bootcd
C:\RosBE\bin\ninja.exe bootcd

测试结果

修复前

错误类型 数量
dsBitfields 断言失败 3
裁剪边界错误 1
RTL 边界错误 1
stretchdibits 边界错误 1
总计边界错误 6

修复后

错误类型 数量
expected bounds 错误 0

所有 GDI DIB 边界断言全部通过。


涉及的文件列表

文件 修复内容
bitmaps.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitmaps.c) BI_RGB 格式位字段清零
dcutil.c(file:///d:/reactos/win32ss/gdi/ntgdi/dcutil.c) 裁剪边界计算修复、RTL 边界调整
bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c) IntPatBlt 矩形规范化、MaskBlt 边界规范化
fillshap.c(file:///d:/reactos/win32ss/gdi/ntgdi/fillshap.c) Rectangle、GradientFill 矩形规范化
dibobj.c(file:///d:/reactos/win32ss/gdi/ntgdi/dibobj.c) 负尺寸矩形规范化
CJournaledTestList.cpp(file:///d:/reactos/modules/rostests/rosautotest/CJournaledTestList.cpp) journal 目录创建失败处理

技术要点总结

  1. 矩形语义 :Win32 矩形使用排他性边界(exclusive bounds),right = last_pixel_x + 1bottom = last_pixel_y + 1
  2. RTL 布局 :ReactOS 的 RTL 实现仅设置 dwLayout 标志,未实际镜像坐标(viewport_extent_x 未设为 -1),需通过交换坐标模拟。
  3. 负尺寸处理:负宽度/高度表示从右向左或从下向上绘制,需规范化矩形后再进行边界计算。
  4. 矩形规范化 :使用 RECTL_vMakeWellOrdered 确保 left <= righttop <= bottom,避免 RECTL_bIsEmptyRect 误判。