fix(desktop): 建窗与置脏标记加关键区,消除并发导致的共槽/同 z_order

create() 由各 App 线程经 syscall 并发调用(开机多 App 同时建窗最易命中),但 _win_count/_next_z 的读-改-写无互斥:抢占后读到同一 _win_count ⇒ 两个 App 共用同一 AppWindow 槽,标题/坐标/z_order/fb 互相覆盖;读到同一 _next_z ⇒ z_order 相同,渲染排序退化成槽位下标顺序 —— 均表现为更下方窗口的 title 突然显示到最上方。

改为:关键区①只原子分配槽位,结构体填充留在区外(避免长时间关中断);关键区②把 active/z_order/焦点一次性发布(渲染线程只在 active==1 后读该槽)。_bring_to_front 的 _next_z 自增同加关键区;_mark_rows_dirty/_mark_all_dirty/_clear_dirty_flags 同加关键区,避免与 App 线程并发时丢置脏标记(丢标记会让该行永不重绘,旧合成滞留在屏幕上)。
This commit is contained in:
TermiNexus committed 2026-10-16 15:20:49 +08:00
1 parent 1b2f6853fc
commit 82e951ee84
1 file changed
+33 -5
+33 -5
View File
@@ -396,6 +396,11 @@ def _get_title_height(style: int) -> int:
def _mark_rows_dirty(start_y: int, count: int):
global _dirty_count, _dirty_y_min, _dirty_y_max
# 关键区:本函数会被 App 线程(draw_text/flush/set_title → syscall)与桌面线程并发调用,
# 对 _dirty_rows/_dirty_count/_dirty_y_min/_dirty_y_max 的读-改-写不加锁会丢置脏标记
# ⇒ 该行永远不重绘,旧合成(例如下方窗口的 title 压在其它窗口上)会留在屏幕上,
# 直到别的操作再次置脏才"弹回去"——正是"更下方的 title 突然跑到最上方、不易复现"。
asm.cli()
for i in range(start_y, start_y + count):
if i >= 0 and i < _screen_h:
if not _dirty_rows[i]:
@@ -405,23 +410,29 @@ def _mark_rows_dirty(start_y: int, count: int):
_dirty_y_min = i
if i > _dirty_y_max or _dirty_count == 1:
_dirty_y_max = i
asm.sti()
def _mark_all_dirty():
global _dirty_count, _dirty_y_min, _dirty_y_max
asm.cli()
string.memset(_dirty_rows, 1, _screen_h)
_dirty_count = _screen_h
_dirty_y_min = 0
_dirty_y_max = _screen_h - 1
asm.sti()
def _has_dirty_rows() -> int:
return _dirty_count
def _clear_dirty_flags():
global _dirty_count, _dirty_y_min, _dirty_y_max
# 关键区:清除与(App 线程的)置脏并发时,会把刚置上的行清掉 ⇒ 同样丢一帧更新
asm.cli()
string.memset(_dirty_rows, 0, _screen_h)
_dirty_count = 0
_dirty_y_min = _screen_h
_dirty_y_max = -1
asm.sti()
def _wait_vblank():
global _vblank_ok
@@ -573,8 +584,11 @@ def _bring_to_front(idx: int):
if idx < 0 or idx >= _win_count: return
aw: AppWindow* = &_wins[idx]
if not aw.active or aw.minimized: return
# 与 create() 共用 _next_z,同样必须原子自增,否则可能出现两个窗口 z_order 相同
asm.cli()
_next_z += 1
aw.z_order = _next_z
asm.sti()
def _hit_test_window(mx: int, my: int) -> int:
best: int = -1
@@ -2558,18 +2572,24 @@ def _draw_app_window(back: uint32*, win: AppWindow*):
def create(x: int, y: int, w: int, h: int, title: const str, style: int) -> int:
global _win_count, _next_z, _focused_win
if _win_count >= MAX_APP_WINDOWS: return -1
# 关键区①:槽位分配必须原子。create() 由各 App 线程经 syscall 并发调用(开机时多个
# App 同时建窗,最易命中)。若被定时器抢占后读到同一个 _win_count,两个 App 会共用
# 同一个 AppWindow 槽 ⇒ 标题/坐标/z_order/fb 被互相覆盖 ⇒ "更下方的 title 突然跑到
# 最上方"(现场)。这里只做"占位",结构体填充放在关键区外,避免长时间关中断。
asm.cli()
if _win_count >= MAX_APP_WINDOWS:
asm.sti()
return -1
idx: int = _win_count
_win_count += 1
asm.sti()
aw: AppWindow* = &_wins[idx]
aw.active = 1
aw.active = 0
aw.x = x
aw.y = y
aw.w = w
aw.h = h
aw.content_h = h - _get_title_height(style) - 2
_next_z += 1
aw.z_order = _next_z
aw.style = style
aw.close_hover = 0
aw.min_hover = 0
@@ -2608,8 +2628,16 @@ def create(x: int, y: int, w: int, h: int, title: const str, style: int) -> int:
fb_size: int = w * aw.content_h
for fi in range(fb_size):
aw.fb[fi] = bg_col
_mark_rows_dirty(y, h)
# 关键区②:发布窗口 —— active/z_order/焦点一次性生效。渲染线程只在 active==1 之后
# 才会读这个槽,所以上面的字段填充必须先完成;且 _next_z 与 _bring_to_front 共用,
# 必须原子自增,否则两个窗口会拿到相同 z_order ⇒ 排序退化成槽位下标顺序。
asm.cli()
_next_z += 1
aw.z_order = _next_z
aw.active = 1
_focused_win = idx
asm.sti()
_mark_rows_dirty(y, h)
return idx
def draw_text(win_id: int, x: int, y: int, text: const str, color: uint32) -> int: