Skip to content

refactor(gui): 命中检测遵循 z-order,隔离 HUD 编辑器,修复 stencil 泄漏 colorMask - #170

Merged
gaoyu06 merged 2 commits into
mainfrom
refactor/gui-zorder
Aug 1, 2026
Merged

refactor(gui): 命中检测遵循 z-order,隔离 HUD 编辑器,修复 stencil 泄漏 colorMask#170
gaoyu06 merged 2 commits into
mainfrom
refactor/gui-zorder

Conversation

@gaoyu06

@gaoyu06 gaoyu06 commented Aug 1, 2026

Copy link
Copy Markdown
Member

基于 #169,合并顺序 167 → 168 → 169 → 本 PR。

背景

consumePressInBounds 是先到先得,而控件都在画完自己之后立即调用它 —— 于是先绘制的、也就是最底层的控件赢得有争议的点击,正好是 z-order 的反面。

这不是实现失误,是立即模式 GUI 的固有约束:单趟遍历里画到某个控件时,无法知道后面还有没有东西会画在它上面。补丁修不了,因为信息在需要它的时刻还不存在。

改动一:hoveredId(层内排序)

采用立即模式 GUI 的标准解法 —— 用上一帧的最顶层命中结果裁决本帧的点击。

// 绘制期无条件覆盖:控件按绘制顺序访问,最后一个宣称光标在自己身上的就是最上层的
public void markHovered(Object id, float x, float y, float w, float h) {
    if (cursorInside) hoveredId = id;      // 后写即赢,不需要比较
}

finishFrame() 把结果交给下一帧,consumePressAsHovered(id, ...) 在消费前查它。GUI 布局帧间几乎不变,这一帧延迟不可感知。

老的 consumePressInBounds 保留,未迁移控件行为完全不变,可以逐个迁移。首批迁移 ModuleRenderer 的模块开关(模块头部会被上方模块展开的设置覆盖)。

顺带删掉两个补丁:

  • getLatestPress() 此前不检查 isConsumed()peekAnyPress() 因此会返回别人已处理的点击。MainPanel 里那句 hasPointerCapture() 就是为抵消它而存在的 —— 拖滑块时 beginDrag 消费了 press,分类栏却还看得见、于是在底下切了分类。现在从根上解决,该守卫连同其存在理由一起删除。
  • 新增 consumePressOutside 原语。OobeDropdown 此前只把 open 置 false 而不消费那次点击,所以关闭下拉的同一次点击会继续落到它覆盖的控件上 —— 单独修 getLatestPress 并不能解决,缺的就是这个原语。

改动二:GuiOcclusion(层间遮挡)

层内排序和层间遮挡是两个不同机制。后者是模态语义:屏幕内容区域应该遮挡它下面的一切。

EventRender2D 由 1.8.9 在绘制游戏内 overlay 时触发,即 currentScreen.drawScreen() 之前,所以 HUD 编辑器会响应一个尚未绘制的面板底下的按压。

复现:打开 ClickGUI,把面板挪到某个 HUD 元素上方,在面板的非滑块区域(模块行、分类栏、空白处)按住拖动 —— 背后的 HUD 元素跟着鼠标走。松开关掉 GUI,HUD 位置已被改掉。

ScaledGuiScreen 每帧上报绘制矩形(设备像素,两套系统唯一认同的空间),Component 查询光标是否被遮挡。

关键取舍:不是「面板一开就禁用 HUD 编辑」 —— 打开 ClickGUI 恰恰是用户想调 HUD 的时候。而是「光标落在面板矩形内时不响应」,所以面板旁边的 HUD 元素仍可正常拖。MainPanel 因此只上报面板矩形,其余屏幕默认全屏。

改动三:stencil 泄漏 colorMask(与 GUI 重构无关,但一直在制造崩溃)

StencilUtil 把 colorMask 的恢复放在 readStencilBuffer() 里 —— 那是个「切换到读模式」的方法,不是收尾方法。任何在写与读之间抛异常或提前返回的路径,都会让 colorMask 永久停在 (false,false,false,false)

症状离病因很远:之后任何地方的 glClear(GL_COLOR_BUFFER_BIT)(例如 KawaseBlur 的 framebufferClear)都在要求驱动清一个它被禁止写入的缓冲,在 Apple 的 Metal-backed GL 上直接断言中止 missing clear mask bits

本机实测的多次崩溃(主菜单天空盒、打开音乐播放器)栈顶各不相同却都是同一条断言,正因为它们都是受害者而非元凶。

  • uninitStencilBuffer() 一并恢复 colorMask,使收尾完整(幂等)
  • Blur.area()GlobalListener 的 blur 块补 try/finally

未覆盖

MusicScreen 仍绕过整套框架 —— 它 consumePressInBounds(0, 0, guiWidth, guiHeight) 一次吃掉整屏,再用 33 处私有 in(...) 各自重测,所以 consume-once 和 z-order 门控对它都无作用。另有独立 PR 处理。

🤖 Generated with Claude Code

@gaoyu06

gaoyu06 commented Aug 1, 2026

Copy link
Copy Markdown
Member Author

更正:stencil 修复的归因写错了

PR 描述里把 missing clear mask bits 的来源写成了 BetterScreen.blur → KawaseBlur.framebufferClear这是错的,后续排查中发现:

  • 测试配置里 BetterScreen.enabled = False,而它的 blur 在 if (BetterScreen.using) 块内,根本不执行
  • ClientSettings.blurMotionBlur 也都是关的
  • framebufferClear() 的全部四个调用者(MotionBlur / KawaseBloom / KawaseBlur ×2)在该配置下一个都没跑

真正的链路是:

KeystrokesComponent(唯一开启的 stencil 使用者)
  → StencilUtil 设 colorMask = false
  → 未恢复(uninitStencilBuffer 原本不管 colorMask)
  → MC 自己每帧的主 framebuffer 清屏撞上 colorMask=false
  → Metal 断言

修复本身没问题——uninitStencilBuffer() 补上 colorMask 恢复是对的,实测后该断言确实归零。错的只是我对触发者的判断。

另:这个修复并没有消除崩溃

后续做了对照实验:把 Keystrokes 关掉(此时没有任何组件使用 stencil)、blur 类特性也全关,客户端照样崩溃,只是崩溃点变成了 addResource / endEncoding / objc_msgSend 等其他 Metal 驱动函数。

最终定位:崩溃由调整窗口大小或切换全屏触发,即 GL 上下文重建路径。这是 LWJGL2 在 macOS(OpenGL 已废弃,经 Metal 转译)+ Rosetta 上的固有问题,与本仓库代码无关。

所以本 PR 里的 stencil 修复应当理解为「修掉了一个真实的 colorMask 泄漏」,而不是「修好了崩溃」。

🤖 Generated with Claude Code

@gaoyu06
gaoyu06 changed the base branch from feat/hud-resize-handle to main August 1, 2026 04:25
gaoyu06 and others added 2 commits July 31, 2026 21:28
consumePressInBounds 是先到先得,而控件都在画完自己之后立即调用它 ——
于是先绘制的、也就是最底层的控件赢得有争议的点击,正好是 z-order 的反面。
这不是实现失误,是立即模式 GUI 的固有约束:单趟遍历里画到某个控件时,
无法知道后面还有没有东西会画在它上面。

采用立即模式 GUI 的标准解法:用上一帧的最顶层命中结果裁决本帧的点击。

- GuiInputState.markHovered 在绘制期无条件覆盖 hoveredId。控件按绘制顺序
  访问,所以最后一个宣称光标在自己身上的,就是画在最上层的那个 —— 不需要
  比较,后写即赢
- finishFrame 把结果存为 lastHoveredId 交给下一帧
- ScaledGuiScreen.consumePressAsHovered(id, ...) 在消费前先查这个结果

GUI 布局帧间几乎不变,这一帧延迟不可感知。老的 consumePressInBounds 保留,
未迁移的控件行为完全不变,因此可以逐个迁移。

同时修两处:

- getLatestPress() 此前不检查 isConsumed(),peekAnyPress() 因此会返回别的
  控件已经处理过的点击。MainPanel 里那句 hasPointerCapture() 就是为抵消它
  而存在的补丁 —— 拖动滑块时 beginDrag 消费了 press,分类栏却还看得见它、
  于是在底下切换了分类。现在 getLatestPress 跳过已消费事件,该补丁连同其
  存在理由一起删除
- 新增 consumePressOutside,供浮层实现「点外部关闭」。此前 OobeDropdown 只是
  把 open 置 false 而不消费那次点击,所以关闭下拉的同一次点击会继续落到下拉
  框覆盖的控件上 —— 单独修 getLatestPress 并不能解决它,缺的是这个原语

首批迁移 ModuleRenderer 的模块开关(模块头部会被上方模块展开的设置覆盖)。

Co-Authored-By: Claude <noreply@anthropic.com>
HUD 编辑器与 ClickGUI 之间没有仲裁。EventRender2D 由 1.8.9 在绘制游戏内
overlay 时触发,也就是在 currentScreen.drawScreen() 之前,所以编辑器会去
响应一个尚未绘制的面板底下的按压,且走的是完全独立的输入通路。原有的
hasPointerCapture() 守卫只覆盖「ClickGUI 已经持有拖拽捕获」这一种情况,
即面板上滑块/滚动条/取色器那一小块区域。

复现:打开 ClickGUI,把面板挪到某个 HUD 元素上方,在面板的非滑块区域
(模块行、分类栏、空白处)按住拖动,背后的 HUD 元素会跟着鼠标走。

新增 GuiOcclusion:ScaledGuiScreen 每帧上报自己绘制的矩形(设备像素),
Component 查询光标是否被遮挡。不是「面板一开就禁用 HUD 编辑」——打开
ClickGUI 恰恰是用户想调 HUD 的时候——而是「光标落在面板矩形内时不响应」,
所以面板旁边的 HUD 元素仍可正常拖动。MainPanel 因此只上报面板矩形,
其余屏幕默认上报全屏。

另修一处与本次 GUI 重构无关、但一直在制造崩溃的问题:

StencilUtil 把 colorMask 的恢复放在 readStencilBuffer() 里,而那是个
「切换到读模式」的方法,不是收尾方法。任何在写与读之间抛异常或提前返回的
路径,都会让 colorMask 永久停在 (false,false,false,false)。症状离病因很远:
之后任何地方的 glClear(GL_COLOR_BUFFER_BIT) —— 例如 KawaseBlur 的
framebufferClear —— 都在要求驱动清一个它被禁止写入的缓冲,在 Apple 的
Metal-backed GL 上直接断言中止 "missing clear mask bits"。

本机实测的多次崩溃(主菜单天空盒、打开音乐播放器)栈顶位置各不相同却都是
同一条断言,正是因为它们都是受害者而非元凶。

- uninitStencilBuffer() 一并恢复 colorMask,使收尾完整(幂等)
- Blur.area() 与 GlobalListener 的 blur 块补 try/finally

Co-Authored-By: Claude <noreply@anthropic.com>
@gaoyu06
gaoyu06 force-pushed the refactor/gui-zorder branch from 3cbf03e to 2a62eb3 Compare August 1, 2026 04:28
@gaoyu06
gaoyu06 merged commit a5d7834 into main Aug 1, 2026
@gaoyu06
gaoyu06 deleted the refactor/gui-zorder branch August 1, 2026 04:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant