refactor(gui): 命中检测遵循 z-order,隔离 HUD 编辑器,修复 stencil 泄漏 colorMask - #170
Merged
Conversation
Member
Author
更正:stencil 修复的归因写错了PR 描述里把
真正的链路是: 修复本身没问题—— 另:这个修复并没有消除崩溃后续做了对照实验:把 最终定位:崩溃由调整窗口大小或切换全屏触发,即 GL 上下文重建路径。这是 LWJGL2 在 macOS(OpenGL 已废弃,经 Metal 转译)+ Rosetta 上的固有问题,与本仓库代码无关。 所以本 PR 里的 stencil 修复应当理解为「修掉了一个真实的 colorMask 泄漏」,而不是「修好了崩溃」。 🤖 Generated with Claude Code |
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
force-pushed
the
refactor/gui-zorder
branch
from
August 1, 2026 04:28
3cbf03e to
2a62eb3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
consumePressInBounds是先到先得,而控件都在画完自己之后立即调用它 —— 于是先绘制的、也就是最底层的控件赢得有争议的点击,正好是 z-order 的反面。这不是实现失误,是立即模式 GUI 的固有约束:单趟遍历里画到某个控件时,无法知道后面还有没有东西会画在它上面。补丁修不了,因为信息在需要它的时刻还不存在。
改动一:hoveredId(层内排序)
采用立即模式 GUI 的标准解法 —— 用上一帧的最顶层命中结果裁决本帧的点击。
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 编辑器会响应一个尚未绘制的面板底下的按压。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