Optimize incremental mixed-owner rendering

This commit is contained in:
Kinneyzhang 2026-08-24 02:08:53 +08:00
parent b65b5937cc
commit dd731cfbbf
7 changed files with 153 additions and 25 deletions

View File

@ -168,7 +168,7 @@ property`ebox-region-update` 接受其目标支持的可变 content/style/scr
| Padding | `:padding`、`:padding-inline`、`:padding-block`longhand `:padding-block-start`、`:padding-inline-end`、`:padding-block-end`、`:padding-inline-start`;别名 `:padding-top`、`:padding-right`、`:padding-bottom`、`:padding-left`,以及 `:padding-right-pixel`、`:padding-left-pixel`、`:padding-top-height`、`:padding-bottom-height` |
| Margin | `:margin`、`:margin-inline`、`:margin-block`longhand `:margin-block-start`、`:margin-inline-end`、`:margin-block-end`、`:margin-inline-start`;别名 `:margin-top`、`:margin-right`、`:margin-bottom`、`:margin-left`,以及 `:margin-right-pixel`、`:margin-left-pixel`、`:margin-top-height`、`:margin-bottom-height` |
| Border | `:border`、`:border-top`、`:border-right`、`:border-bottom`、`:border-left`、`:border-width`、`:border-style`、`:border-color`side longhand `:border-*-width`、`:border-*-style`、`:border-*-color`;兼容别名 `:border-top-p`、`:border-bottom-p`、`:border-left-pixel`、`:border-right-pixel` |
| Paint | `:color`、`:background-color`别名 `:bgcolor` |
| Paint | `:color`、`:background-color`别名 `:bgcolor` |
| Typography | `:font`、`:font-family`、`:font-height` 和别名 `:font-size`、`:font-weight`、`:font-slant` |
| Text/layout | `:text-align``left`、`center`、`right`)、`:vertical-align``top`、`center`/`middle`、`bottom`)、`:overflow``scroll`、`hidden`、`visible`)、`:wrap-mode``word`、`char`、`kp` 或 nil、`:visibility``visible` 或 `hidden` |
| Structural style | `:display` 会在 node/layout context 中计算;有效的 display tuple 通常由布局构造器选择。 |

View File

@ -8229,6 +8229,14 @@ result in the same owner-proof shape consumed by span publication."
(list :root (plist-get prepared :root)
:scope-node-ids scope-node-ids
:report-base report
;; A child Range replacement can legitimately change the separator
;; owned by an adjacent sibling when its item count changes. Keep
;; the fast scoped proof for stable topology, but let the surface
;; publisher use its exact root fallback when that proof cannot
;; establish the outside text invariant.
:on-mismatch
(and (> (or (plist-get prepared :range-replacement-count) 0) 0)
'root)
:projection-kind projection-kind
:preserve-identities-p t
:state-overrides

View File

@ -115,21 +115,30 @@ of pixel measurements it triggers do not each rebuild the display signature.")
(fillarray ebox--string-pixel-width-cache-ring nil)
(setq ebox--string-pixel-width-cache-ring-index 0))
(defun ebox--string-pixel-width-visual-key (line factor)
"Return a width cache key for LINE at FACTOR when visual properties are constant."
(let ((end (length line)))
(list factor
(substring-no-properties line)
(cl-loop for property in '(face display)
for value = (and (> end 0)
(get-text-property 0 property line))
when value append (list property value)))))
(defun ebox--string-pixel-width-cache-key (line factor)
"Return a safe display-scoped measurement key for LINE at FACTOR."
(let* ((end (length line))
(properties (and (> end 0) (text-properties-at 0 line)))
;; Ebox layout text uses `face' and `display' as its width-bearing
;; properties. Ownership/region metadata is deliberately ignored;
;; the other generic text properties are not emitted by Ebox's
;; layout pipeline and would make this hot key scan needlessly broad.
(constant-p
(= (or (next-property-change 0 line end) end) end)))
(cond
((and constant-p (null properties))
(list factor line))
((and constant-p
(= (length properties) 2)
(eq (car properties) 'face))
(list factor
line
(cadr properties))))))
(and (= (or (next-single-property-change 0 'face line end) end)
end)
(= (or (next-single-property-change 0 'display line end) end)
end))))
(and constant-p
(ebox--string-pixel-width-visual-key line factor))))
(defun ebox--cached-string-pixel-width (line factor key)
"Return LINE width at FACTOR through the render and display caches at KEY."
@ -161,7 +170,9 @@ of pixel measurements it triggers do not each rebuild the display signature.")
Text characters follow the current buffer text scale. Fixed display spaces
keep their absolute pixel width."
(ebox--ensure-display-cache-current)
(let* ((line (car (split-string (or string "") "\n")))
(let* ((source (or string ""))
(newline (and (stringp source) (string-match "\n" source)))
(line (if newline (substring source 0 newline) source))
(factor (ebox--text-scale-factor))
(pos 0)
(end (length line))
@ -171,17 +182,27 @@ keep their absolute pixel width."
(ebox--cached-string-pixel-width line factor cache-key)
(while (< pos end)
(let* ((display (get-text-property pos 'display line))
(next (or (next-single-property-change
pos 'display line end)
end))
(next
(cl-loop for property in
'(face display)
minimize (or (next-single-property-change
pos property line end)
end)))
(segment (substring line pos next))
;; The segment ends at every visual property change, so its
;; width key can skip the second constant-property scan.
(segment-key
(ebox--string-pixel-width-visual-key segment factor))
(space-width (ebox--display-space-width display)))
(setq total
(+ total
(if space-width
(* space-width (- next pos))
(ebox--scaled-string-pixel-width
(substring line pos next)
factor))))
(if segment-key
(ebox--cached-string-pixel-width
segment factor segment-key)
(ebox--scaled-string-pixel-width
segment factor)))))
(setq pos next)))
total)))

View File

@ -2077,6 +2077,12 @@ causes the caller to use the ordinary full projection."
(ebox-surface--formatting-context-reflow-output
buffer state)))))
(old-fragments (plist-get previous-state :surface-fragments))
(old-text
(and (buffer-live-p buffer)
(with-current-buffer buffer
(save-restriction
(widen)
(buffer-substring (point-min) (point-max))))))
(old-by-address (make-hash-table :test #'equal))
(old-by-local-address (make-hash-table :test #'equal)))
(when (and output old-fragments paint-owner-ids)
@ -2116,6 +2122,13 @@ causes the caller to use the ordinary full projection."
(old-fragment
(or (and address (gethash address old-by-address))
(and (= (length matches) 1) (car matches))))
(old-fragment-text
(and old-text old-fragment
(integerp (plist-get old-fragment :start))
(integerp (plist-get old-fragment :end))
(substring old-text
(plist-get old-fragment :start)
(plist-get old-fragment :end))))
(affected-p
(cl-some (lambda (entry)
(memq (cdr entry) paint-owner-ids))
@ -2141,10 +2154,45 @@ causes the caller to use the ordinary full projection."
(plist-get old-fragment
:paint-role-ids))
roles)))
(push (ebox-surface--repaint-fragment
copy previous-state state
old-face-cache new-face-cache)
pieces)))
;; Geometry reflow still produces a complete candidate text,
;; but an unaffected fragment whose text/properties are
;; byte-for-byte identical and whose semantic face
;; contributions are unchanged already has the correct
;; composed face. Reuse its candidate metadata without
;; rebuilding it; changed/paint-owned fragments retain the
;; existing baseline-safe repaint path.
(let* ((old-roles (plist-get copy :old-paint-role-ids))
(new-roles (plist-get copy :paint-role-ids))
(reusable-p
(and old-fragment
(not affected-p)
old-fragment-text
(equal-including-properties
old-fragment-text text)
(equal old-roles new-roles)
(let ((old-faces
(or (gethash old-roles old-face-cache)
(let ((value
(ebox-surface--face-contributions
previous-state old-roles)))
(puthash old-roles value old-face-cache)
value)))
(new-faces
(or (gethash new-roles new-face-cache)
(let ((value
(ebox-surface--face-contributions
state new-roles)))
(puthash new-roles value new-face-cache)
value))))
(equal old-faces new-faces)))))
(push (if reusable-p
(progn
(cl-remf copy :old-paint-role-ids)
copy)
(ebox-surface--repaint-fragment
copy previous-state state
old-face-cache new-face-cache))
pieces))))
(let* ((fragments (nreverse pieces))
(rendered
(apply #'concat

View File

@ -404,7 +404,6 @@ caller, such as the Playground preview, renders against a concrete window.")
;; 上下边框由于 Emacs 限制,用 overline/underline 实现,不支持设置宽度
:border-top-p nil :border-top-color nil
:border-bottom-p nil :border-bottom-color nil
:color nil
:bgcolor nil
:font nil :font-family nil :font-height nil
@ -4588,7 +4587,9 @@ Return the successful publication report stored by `ebox-buffer-update-report'."
buffer source scope-node-ids
(plist-get commit-input :report-base)
(plist-get commit-input :state-overrides)
callback nil nil
callback
(plist-get commit-input :on-mismatch)
nil
(plist-get commit-input :projection-kind)
t participant)
(ebox-surface-mount-buffer

View File

@ -1473,6 +1473,36 @@ remain retained identities."
(ebox-render (ebox--buffer-root-node buffer))))))))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(ert-deftest ebox-commit-mixed-owner-reflow-reuses-unchanged-fragments ()
"Skip face recomposition for unchanged fragments in a mixed projection."
(let* ((buffer
(ebox-render-to-buffer
(generate-new-buffer-name " *ebox-mixed-owner-reuse* ")
(ebox-commit-test--mixed-owner-root
"left-old" "right-old" "#111111" "#222222" "#333333")))
(surface (with-current-buffer buffer ebox-surface--buffer-surface))
(old-state (tp-surface-client-state surface))
(fragment-count (length (plist-get old-state :surface-fragments)))
(repaints 0)
(original (symbol-function 'ebox-surface--repaint-fragment)))
(unwind-protect
(progn
(cl-letf (((symbol-function 'ebox-surface--repaint-fragment)
(lambda (&rest args)
(cl-incf repaints)
(apply original args))))
(should (eq (plist-get
(ebox-commit
buffer
(ebox-commit-test--mixed-owner-candidate
buffer "left-new" "right-new"
"#AAAAAA" "#BBBBBB" "#CCCCCC"))
:projection-kind)
'mixed-owner-reflow)))
(should (> fragment-count 0))
(should (< repaints fragment-count)))
(when (buffer-live-p buffer) (kill-buffer buffer)))))
(provide 'ebox-commit-tests)
;;; ebox-commit-tests.el ends here

View File

@ -4543,6 +4543,26 @@
(should (= (ebox--string-max-pixel-width exact) 120))
(should (plist-get exact-parent :ebox-content-width-exact-p))))
(ert-deftest ebox-string-pixel-width-cache-ignores-ebox-ownership-metadata ()
"Reuse visual width measurements across Ebox-owned text properties."
(let ((line (propertize "cached row"
'ebox-content 7
'ebox-content-idx 0
'ebox-content-owner 7
'ebox-content-owners '(7)))
(calls 0)
(original (symbol-function 'ebox--scaled-string-pixel-width)))
(ebox--clear-string-pixel-width-cache)
(cl-letf (((symbol-function 'ebox--scaled-string-pixel-width)
(lambda (&rest args)
(cl-incf calls)
(apply original args))))
(let ((first (ebox--string-pixel-width line))
(second (ebox--string-pixel-width (copy-sequence line))))
(should (> first 0))
(should (= first second))))
(should (= calls 1))))
(ert-deftest ebox-render-caches-lazy-box-content-within-render-pass ()
"A lazy child layout should be rendered once per box/context in one pass."
(ebox-test--reset-runtime-state)