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` | | 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` | | 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` | | 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` | | 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` | | 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 通常由布局构造器选择。 | | 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) (list :root (plist-get prepared :root)
:scope-node-ids scope-node-ids :scope-node-ids scope-node-ids
:report-base report :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 :projection-kind projection-kind
:preserve-identities-p t :preserve-identities-p t
:state-overrides :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) (fillarray ebox--string-pixel-width-cache-ring nil)
(setq ebox--string-pixel-width-cache-ring-index 0)) (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) (defun ebox--string-pixel-width-cache-key (line factor)
"Return a safe display-scoped measurement key for LINE at FACTOR." "Return a safe display-scoped measurement key for LINE at FACTOR."
(let* ((end (length line)) (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 (constant-p
(= (or (next-property-change 0 line end) end) end))) (and (= (or (next-single-property-change 0 'face line end) end)
(cond end)
((and constant-p (null properties)) (= (or (next-single-property-change 0 'display line end) end)
(list factor line)) end))))
((and constant-p (and constant-p
(= (length properties) 2) (ebox--string-pixel-width-visual-key line factor))))
(eq (car properties) 'face))
(list factor
line
(cadr properties))))))
(defun ebox--cached-string-pixel-width (line factor key) (defun ebox--cached-string-pixel-width (line factor key)
"Return LINE width at FACTOR through the render and display caches at 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 Text characters follow the current buffer text scale. Fixed display spaces
keep their absolute pixel width." keep their absolute pixel width."
(ebox--ensure-display-cache-current) (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)) (factor (ebox--text-scale-factor))
(pos 0) (pos 0)
(end (length line)) (end (length line))
@ -171,17 +182,27 @@ keep their absolute pixel width."
(ebox--cached-string-pixel-width line factor cache-key) (ebox--cached-string-pixel-width line factor cache-key)
(while (< pos end) (while (< pos end)
(let* ((display (get-text-property pos 'display line)) (let* ((display (get-text-property pos 'display line))
(next (or (next-single-property-change (next
pos 'display line end) (cl-loop for property in
end)) '(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))) (space-width (ebox--display-space-width display)))
(setq total (setq total
(+ total (+ total
(if space-width (if space-width
(* space-width (- next pos)) (* space-width (- next pos))
(ebox--scaled-string-pixel-width (if segment-key
(substring line pos next) (ebox--cached-string-pixel-width
factor)))) segment factor segment-key)
(ebox--scaled-string-pixel-width
segment factor)))))
(setq pos next))) (setq pos next)))
total))) total)))

View File

@ -2077,6 +2077,12 @@ causes the caller to use the ordinary full projection."
(ebox-surface--formatting-context-reflow-output (ebox-surface--formatting-context-reflow-output
buffer state))))) buffer state)))))
(old-fragments (plist-get previous-state :surface-fragments)) (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-address (make-hash-table :test #'equal))
(old-by-local-address (make-hash-table :test #'equal))) (old-by-local-address (make-hash-table :test #'equal)))
(when (and output old-fragments paint-owner-ids) (when (and output old-fragments paint-owner-ids)
@ -2116,6 +2122,13 @@ causes the caller to use the ordinary full projection."
(old-fragment (old-fragment
(or (and address (gethash address old-by-address)) (or (and address (gethash address old-by-address))
(and (= (length matches) 1) (car matches)))) (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 (affected-p
(cl-some (lambda (entry) (cl-some (lambda (entry)
(memq (cdr entry) paint-owner-ids)) (memq (cdr entry) paint-owner-ids))
@ -2141,10 +2154,45 @@ causes the caller to use the ordinary full projection."
(plist-get old-fragment (plist-get old-fragment
:paint-role-ids)) :paint-role-ids))
roles))) roles)))
(push (ebox-surface--repaint-fragment ;; Geometry reflow still produces a complete candidate text,
copy previous-state state ;; but an unaffected fragment whose text/properties are
old-face-cache new-face-cache) ;; byte-for-byte identical and whose semantic face
pieces))) ;; 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)) (let* ((fragments (nreverse pieces))
(rendered (rendered
(apply #'concat (apply #'concat

View File

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

View File

@ -1473,6 +1473,36 @@ remain retained identities."
(ebox-render (ebox--buffer-root-node buffer)))))))) (ebox-render (ebox--buffer-root-node buffer))))))))
(when (buffer-live-p buffer) (kill-buffer 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) (provide 'ebox-commit-tests)
;;; ebox-commit-tests.el ends here ;;; ebox-commit-tests.el ends here

View File

@ -4543,6 +4543,26 @@
(should (= (ebox--string-max-pixel-width exact) 120)) (should (= (ebox--string-max-pixel-width exact) 120))
(should (plist-get exact-parent :ebox-content-width-exact-p)))) (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 () (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." "A lazy child layout should be rendered once per box/context in one pass."
(ebox-test--reset-runtime-state) (ebox-test--reset-runtime-state)