Add visual QA checklist for the GPU rendering backend

This commit is contained in:
Andros Fenollosa 2026-06-17 15:41:10 +02:00
parent 1fad8a6141
commit 8955c39a9b

517
VISUAL-QA.org Normal file
View file

@ -0,0 +1,517 @@
#+TITLE: Visual QA Checklist — GPU Backend
#+DESCRIPTION: Visual behaviors to verify manually after any change to gfxterm.c, mtlterm.m, glterm.c or gfxdrv.h
#+STARTUP: overview
#+TODO: TODO | PASS FAIL SKIP
* Instructions
Mark each item as PASS, FAIL or SKIP before pushing or releasing.
SKIP = does not apply to the current platform or the specific change being made.
Useful commands to reproduce test cases:
- macOS (Metal): =/tmp/emacs-mtl-buildfull/src/emacs -Q=
- macOS (NS baseline): =EMACS_GPU_DISABLE=1 /tmp/emacs-mtl-buildfull/src/emacs -Q=
- Linux (GPU): =/usr/local/bin/emacs -Q= on the test laptop (ssh debian)
- Linux (cairo baseline): =EMACS_GPU_DISABLE=1 /usr/local/bin/emacs -Q=
General principle: if anything looks different from the NS/cairo baseline
in equivalent cases, it is a failure unless explicitly documented as an
accepted difference.
* 1. Render cycle and present
** TODO 1.1 No black flash on startup
Open without -Q (real config). The window must not show a black or blank
frame before the content is painted.
** TODO 1.2 No flash when switching buffers
=C-x b= between buffers. No previous buffer content or blank frame should
appear for even one vblank.
(Known regression: was the XDBE back buffer on Linux, fixed with
inhibit-double-buffering; on Metal it was swapchain pressure.)
** TODO 1.3 No flash when resizing the window (WM resize)
Drag the window border with the mouse. No black band or displaced text
should appear.
(Regression: v0.4.1 — partial present was caching a stale surface size.)
** TODO 1.4 No flash when opening/closing the minibuffer
=M-x= then =C-g=. The minibuffer area must appear and disappear cleanly.
** TODO 1.5 Clear-only cycle does not present (avoids white flash)
=C-x t t= (tab-bar). The first time it is activated, the tab-bar must
appear without a fully white frame flashing first.
** TODO 1.6 Deferred flush: mouse-face does not flicker
Move the mouse over a link or a mode-line button. The highlight must
appear and disappear without flickering between frames.
** TODO 1.7 Pending scroll-bar clears are applied correctly
Open a long buffer, scroll, then resize. The scroll-bar gutters must not
appear dirty.
* 2. Text and glyphs
** TODO 2.1 Basic ASCII
Open a plain-text ASCII buffer. Characters must be sharp and aligned with
the NS/cairo baseline.
** TODO 2.2 Bold and italic fonts
=(propertize "hello" 'face '(:weight bold))= and =:slant italic=.
Both must be visually distinct from each other and from regular text.
** TODO 2.3 Extended Latin (ñ, ü, é, ç)
Type accented characters. They must not appear vertically shifted
relative to the rest of the line.
** TODO 2.4 CJK (kanji, hangul, Chinese characters)
Open a buffer with CJK text. Characters must occupy twice the width of
ASCII and be centered in their cell.
** TODO 2.5 Symbols and box-drawing (─ │ ┌ ┐ └ ┘)
Open neotree or a buffer with Unicode borders. Lines must connect without
gaps or overlaps.
** TODO 2.6 Font-lock (multiple face colors on the same line)
Open a .el or .py file. Syntax-highlight colors must match the baseline
at the same positions.
** TODO 2.7 Face inheritance (:inherit)
Verify that a face inheriting from another applies its attributes
correctly (color, weight, height).
** TODO 2.8 Relative :height (e.g. :height 1.5)
=(propertize "big" 'face '(:height 1.5))= in a buffer. The text must be
1.5x taller and adjacent lines must not overlap.
** TODO 2.9 Variable-pitch and fixed-pitch on the same line
Enable variable-pitch-mode in a mixed buffer (org-mode with code blocks).
Both fonts must align to the same baseline.
** TODO 2.10 text-scale-increase / text-scale-decrease
=C-x C-= and =C-x C--=. Text must scale cleanly without glyphs from
the previous size remaining visible in the atlas.
** TODO 2.11 Advance precision (no drift on long lines)
Open a line of ~200 characters with wrapping disabled. The cursor at the
end must be exactly at the expected column.
** TODO 2.12 Stroke weight (gamma ^0.82 on Metal)
Compare text in Metal vs NS. Accepted difference: Metal may appear
marginally thinner. Unacceptable: noticeably thicker or thinner than
previous releases.
* 3. Compositions and special scripts
** TODO 3.1 Combining characters (e + ́ = composed é)
Text with combining characters (U+0301 etc.). They must not appear
shifted relative to their base character.
** TODO 3.2 Arabic and Hebrew (shaped, RTL)
Open an Arabic or Hebrew buffer. Text must appear right-to-left with
correctly joined glyphs (proper shaping).
** TODO 3.3 Ligatures (if the font supports them)
With Fira Code or another font with ligatures, verify that -> renders
as a ligated arrow rather than two separate characters.
** TODO 3.4 Automatic composition (auto-composition-mode)
=(setq auto-composition-mode t)= and type text that triggers composition.
Composed glyphs must appear at the correct position.
** TODO 3.5 Mixed BiDi (Arabic + ASCII on the same line)
A buffer with interleaved L2R and R2L text. Verify that the visual
order is correct and cursors move as expected.
* 4. Emoji and color glyphs
** TODO 4.1 Basic emoji (U+1F600, U+1F4BB)
Insert emoji in a buffer. They must appear in color, not grayscale,
and at the correct size (one or two cells).
** TODO 4.2 U+2764 RED HEART (may hang vanilla under Xvfb)
Verify on real hardware only, not in headless Xvfb.
The heart must appear in red.
** TODO 4.3 Emoji in the mode-line
=(setq mode-line-format '("" (:eval "📁 %b")))=
The emoji must render in the mode-line without overlapping adjacent text.
** TODO 4.4 ZWJ sequences (family emoji, flag emoji)
Insert a ZWJ sequence. It must render as a single composed glyph, not
as a sequence of separate characters.
* 5. Face decorations
** TODO 5.1 Single underline
=(propertize "test" 'face '(:underline t))=
One line below the text, aligned to the font's baseline metrics.
** TODO 5.2 Double underline
=(propertize "test" 'face '(:underline (:style double)))=
Two parallel lines below the text.
** TODO 5.3 Wave underline
=(propertize "test" 'face '(:underline (:style wave :color "red")))=
A 3px-tall zigzag line. Verify that it is continuous across adjacent
strings (the phase must not reset per fragment).
** TODO 5.4 Dot underline
=(propertize "test" 'face '(:underline (:style dots)))=
Dotted segments. The phase must be continuous across fragments.
** TODO 5.5 Dash underline
=(propertize "test" 'face '(:underline (:style dashes)))=
Dash segments of 3x the line thickness. Phase must be continuous.
** TODO 5.6 Underline with specific color
=(propertize "test" 'face '(:underline (:color "blue")))=
The underline must be blue; the text must remain in its default color.
** TODO 5.7 Overline
=(propertize "test" 'face '(:overline "green"))=
A green line at the top of the text.
** TODO 5.8 Strike-through
=(propertize "test" 'face '(:strike-through "red"))=
A red line at mid-height of the text.
** TODO 5.9 Underline clamped to descent
Text with a font that has a small descent. The underline must not
escape the line cell. If pos+thickness > descent, it must collapse
to 1px at descent-1.
* 6. Boxes and reliefs
** TODO 6.1 Simple box outline
=(propertize "test" 'face '(:box 1))=
A 1px border around the text.
** TODO 6.2 Box with larger line width
=(propertize "test" 'face '(:box (:line-width 3 :color "blue")))=
A blue 3px border. Horizontal and vertical edges must have the correct
thickness.
** TODO 6.3 Raised relief (mode-line, buttons)
Activate the mode-line and verify that tab-bar buttons appear raised
(light border top/left, dark border bottom/right).
** TODO 6.4 Sunken relief
=(propertize "test" 'face '(:box (:line-width 2 :style sunken-box)))=
The border must appear sunken (dark top/left, light bottom/right).
** TODO 6.5 Relief corners must not appear light
On a raised relief of >= 2px, corners where the light and shadow edges
overlap must appear dark (corner darkening).
** TODO 6.6 Relief colors in dark mode (Metal/macOS)
Switch to a dark theme. The mode-line relief colors must remain
coherent (not inverted or broken).
** TODO 6.7 Relief on images (:relief in tool-bar)
Verify that tool-bar buttons have correct relief (raised at rest,
sunken when pressed if applicable).
* 7. Cursor
** TODO 7.1 FILLED_BOX (default cursor)
The filled box cursor must have inverted colors: cursor background
color, text in the cursor background color.
** TODO 7.2 HOLLOW_BOX
=(setq cursor-type 'hollow)=
Four 1px edges around the cell, transparent interior.
** TODO 7.3 BAR (vertical bar cursor)
=(setq cursor-type 'bar)=
A 1-2px vertical bar on the left edge of the cell.
For R2L text: on the right edge.
** TODO 7.4 HBAR (horizontal bar cursor)
=(setq cursor-type 'hbar)=
A horizontal bar at the bottom of the cell.
** TODO 7.5 Cursor on fringe when no glyph is present
Move the cursor to an empty line at the end of the buffer.
The cursor must appear in the fringe, not disappear.
** TODO 7.6 Cursor adapts to text-scale
Change text-scale and verify that the cursor adapts to the new cell
size.
** TODO 7.7 Invisible cursor (blink off state)
With blink-cursor-mode active, verify that during the "off" phase
the cursor disappears completely.
** TODO 7.8 [Metal] BLOCK cursor — no animation effect
=(setq gpu-cursor-animation 'block)=
The cursor must be static with no trail or animation when moving.
** TODO 7.9 [Metal] SPRING cursor
=(setq gpu-cursor-animation 'spring)=
The cursor must slide to the new position with critical damping
(~150ms settling time).
** TODO 7.10 [Metal] TORPEDO cursor (trail)
=(setq gpu-cursor-animation 'torpedo)=
Moving the cursor quickly must leave a fading trail behind it.
** TODO 7.11 [Metal] SONICBOOM, RIPPLE, PIXIEDUST effects on large jumps
Jump with =M-g g= to a distant line. Verify the burst/ripple/dust
effect appears around the destination.
** TODO 7.12 [Metal] Effect suppression during typing
=(setq gpu-cursor-effects-while-typing nil)=
Typing text rapidly must not trigger cursor animation effects on
every character insertion.
* 8. Fringe
** TODO 8.1 Line continuation arrows
Enable truncate-lines and create a very long line. Continuation
arrows must appear in the right fringe.
** TODO 8.2 Empty-line indicator
Move past the last line of a buffer with trailing empty lines.
The tilde indicator must appear in the left fringe.
** TODO 8.3 Truncation arrows (left/right)
In a buffer with hscroll, truncation arrows must appear in the
appropriate fringe on each side.
** TODO 8.4 Custom fringe bitmap
=(define-fringe-bitmap 'my-bm [8 4 2 1 2 4 8] nil nil 'center)=
The bitmap must render at the correct position and color.
** TODO 8.5 Fringe background color
=(set-face-background 'fringe "yellow")=
The fringe area must have a yellow background.
** TODO 8.6 Fringe bitmap does not bleed onto the mode-line
With a long buffer, scroll to the end. The 72px empty-line indicator
must not overlap the mode-line.
** TODO 8.7 Overlay bitmap does not erase underlying content
Bitmaps with the overlay flag must be drawn on top of existing content
without clearing the fringe background first.
* 9. Images
** TODO 9.1 Opaque PNG
=(insert-image (create-image "/path/to/opaque.png"))=
The image must appear with correct colors.
** TODO 9.2 PNG with alpha channel
A PNG image with transparency. The Emacs background must show through
the transparent areas.
** TODO 9.3 JPEG
A JPEG image must appear without compression artifacts added by the
backend.
** TODO 9.4 SVG
=(insert-image (create-image "/path/to/image.svg" 'svg))=
The vector image must render sharply.
** TODO 9.5 Static GIF (first frame)
A static GIF must display correctly (the backend always shows the first
frame in image-mode).
** TODO 9.6 Image with :scale
=(create-image ... :scale 2.0)=
The image must appear twice as large as the original.
** TODO 9.7 Image with :rotation
=(create-image ... :rotation 90)=
The image must appear rotated 90 degrees.
** TODO 9.8 Image slice (UV subrect)
=(insert-image img nil nil '(0 0 50 50))=
Only the specified crop of the image must be visible.
** TODO 9.9 Image with :relief
=(create-image ... :relief 2)=
The image must have a raised-relief border around it.
** TODO 9.10 Image with :margin
The image must have correct spacing relative to adjacent text.
** TODO 9.11 Image cache (same image inserted twice)
Insert the same image in two different places in the buffer. Both
occurrences must be identical.
** TODO 9.12 Inline images (iimage-mode)
=(iimage-mode)= in a directory with referenced images.
Inline images must render correctly.
* 10. Scrolling and copy_region
** TODO 10.1 Smooth scroll down (C-n, C-v)
Existing text must slide upward without artifacts. New content must
appear cleanly at the bottom.
** TODO 10.2 Smooth scroll up (C-p, M-v)
Same as above but in the opposite direction.
** TODO 10.3 scroll_run: pixel-perfect parity with NS/cairo
The result of scrolling N lines must be identical to a fresh redraw
of the same lines. (See the run-scroll.sh harness.)
** TODO 10.4 Overlapping copy_region (scroll with overlap)
A 1-line scroll where src and dst overlap. Content must move correctly
(bounced through the scratch texture).
** TODO 10.5 [Metal] Scroll with EASE_OUT_QUAD easing
=(setq gpu-scroll-easing 'ease-out-quad)=
The scroll must show a smooth deceleration at the end.
** TODO 10.6 [Metal] Scroll with SPRING easing
=(setq gpu-scroll-easing 'spring)=
The scroll must slightly overshoot and bounce back.
** TODO 10.7 [Metal] Instant scroll (no easing)
=(setq gpu-scroll-easing 'none)=
The scroll must be instant, matching the baseline.
** TODO 10.8 Horizontal scroll (hscroll)
A long line with truncate-lines. =C-e= to jump to the end.
Content must scroll horizontally without artifacts.
** TODO 10.9 Glyph shift on insert (gfx_shift_glyphs_for_insert)
With overwrite-mode disabled, insert a character in the middle of a
line. Text to the right must shift cleanly.
* 11. Emacs bars
** TODO 11.1 Basic mode-line
The mode-line must show the buffer name, major mode and line/column
info with correct foreground and background colors.
** TODO 11.2 Mode-line with custom face
=(set-face-background 'mode-line "navy")=
The mode-line must have a navy-blue background.
** TODO 11.3 Inactive mode-line (another window)
The mode-line of inactive windows must use mode-line-inactive face.
** TODO 11.4 Header-line
=(setq header-line-format "Header")=
A bar must appear above the text area.
** TODO 11.5 Tab-line (per-window)
=(global-tab-line-mode)=
Per-window tabs must render with relief and hover highlighting.
** TODO 11.6 Tab-bar (per-frame)
=(tab-bar-mode)=
Per-frame tabs must appear at the top without flickering when
switching tabs.
** TODO 11.7 Tool-bar
=(tool-bar-mode)=
Tool-bar icons must have raised relief and respond to hover without
flickering.
** TODO 11.8 Scroll-bar
=(scroll-bar-mode)=
The scroll-bar must appear on the right and update its position when
scrolling.
* 12. Windows and splits
** TODO 12.1 Vertical split (C-x 2)
Two stacked windows. The dividing line must be visible and the
content of each window must not overlap.
** TODO 12.2 Horizontal split (C-x 3)
Two side-by-side windows. The vertical divider must be visible.
** TODO 12.3 Window divider >= 3px (first/last pixel colors)
=(setq window-divider-default-bottom-width 5)=
A 5px divider must have distinct colors on the first and last pixel.
** TODO 12.4 Resize windows with C-x ^, C-x {, C-x }
Windows must resize cleanly without rendering artifacts.
** TODO 12.5 Internal frame border (internal-border-width)
=(set-frame-parameter nil 'internal-border-width 10)=
A 10px border must appear with the correct color.
** TODO 12.6 Multiple frames (make-frame)
Create a second frame. Both frames must render independently without
interfering with each other.
** TODO 12.7 Child frame (corfu, company popup)
Open a completion popup. The child frame must appear above the text
without artifacts on its edges.
* 13. Display properties and overlays
** TODO 13.1 Invisible text
=(put-text-property 1 5 'invisible t)=
Invisible text must not appear on screen but the cursor must skip
over it correctly.
** TODO 13.2 Display string (replacing text)
=(put-text-property 1 5 'display "REPLACED")=
The original text must be replaced by "REPLACED".
** TODO 13.3 Inline image via display property
=(put-text-property 1 2 'display (create-image ...))=
The image must appear in place of the character.
** TODO 13.4 Display space (:width, :align-to)
=(put-text-property 1 2 'display '(space :width 10))=
A space of 10 columns must appear.
** TODO 13.5 Overlay with face
=(overlay-put (make-overlay 1 10) 'face '(:background "yellow"))=
The overlay text must have a yellow background.
** TODO 13.6 Overlay with after-string / before-string
The extra string must appear at the correct position without
unexpectedly displacing surrounding text.
** TODO 13.7 Line numbers (display-line-numbers-mode)
=(display-line-numbers-mode)=
Line numbers must appear right-aligned in the left margin.
** TODO 13.8 hl-line-mode
=(hl-line-mode)=
The cursor line must have a distinct highlight background.
** TODO 13.9 Margins (left-margin-width, right-margin-width)
=(setq left-margin-width 4)=
A 4-column blank left margin must appear.
** TODO 13.10 Line wrapping (word-wrap, visual-line-mode)
Long lines that wrap. Wrapping must occur at word boundaries and
continuation lines must be correctly indented.
** TODO 13.11 Truncate lines
=(setq truncate-lines t)=
Long lines must be cut with a truncation arrow in the fringe.
* 14. Common major modes (real-world integration)
** TODO 14.1 org-mode with headings, code blocks and tables
Open a real .org file. Headings with colors, code blocks with
background, tables with lines. No artifacts.
** TODO 14.2 dired
=(dired "~")= List a directory. File names, permissions and dates
must align correctly.
** TODO 14.3 magit (git interface)
Open magit-status. Diff hunks with add/remove colors, foldable
sections. No flickering when expanding/collapsing.
** TODO 14.4 corfu / company (completion popup)
In a code buffer, trigger completion. The popup (child frame or
overlay) must appear without flickering and with correct highlight
on the selected candidate.
** TODO 14.5 vertico / ivy / helm (minibuffer with candidates)
Open =M-x= with vertico. The candidate list must render cleanly
with the current item highlighted.
** TODO 14.6 elfeed (RSS reader with links and mixed fonts)
Open an article in elfeed. Links must have underlines and paragraph
font must be correct.
** TODO 14.7 treemacs / neotree (file tree with icons)
Open the file tree. Unicode/emoji icons must render at the correct
column.
* 15. HiDPI / Retina
** TODO 15.1 [macOS] Retina @2x: atlas rasterized at physical resolution
On a Retina display, open the app and capture with gpu-capture-frame.
The capture size must be exactly double the logical size.
** TODO 15.2 [macOS] Text sharpness @2x
Zoom in 6x on the capture. Glyphs must show per-physical-pixel
antialiasing, not blurry upscaling from @1x.
** TODO 15.3 [macOS] NS vs Metal parity @2x
Compare Metal vs NS capture (EMACS_GPU_DISABLE=1) with the harness.
fuzz5%=~0.5%, fuzz20%=~0.05% are acceptable (AA on glyph edges).
** TODO 15.4 [Linux] GL_SCALE > 1 (supersampling)
=GL_SCALE=2 emacs -Q=
Text must appear sharper than at scale 1x.
* 16. Buffer-switch transition (crossfade)
** TODO 16.1 Crossfade when switching buffers
=(setq gpu-buffer-transitions t)=
On =C-x b=, the previous buffer must fade out smoothly as the
new content appears.
** TODO 16.2 Configurable duration
=(setq gpu-buffer-transition-duration 0.5)=
The crossfade must last approximately 0.5 seconds.
** TODO 16.3 No artifacts after the fade
When the fade ends, the new buffer must appear perfectly clean,
with no residue from the previous buffer.
** TODO 16.4 Pump cadence during fade (60Hz)
The fade must be smooth (~60fps). It must not look like 30fps or
show frame jumps.
* 17. Inline video (macOS Metal only)
** TODO 17.1 Insert video with gpu-video-insert
=(gpu-video-insert "/path/to/video.mp4")=
A placeholder must appear in the buffer.
** TODO 17.2 Playback: video appears at the placeholder position
When playback starts, the video must appear over the placeholder,
not at another position in the frame.
** TODO 17.3 Video follows scrolling
Scroll the buffer while the video is playing. The video must move
with the text, not remain fixed on screen.
** TODO 17.4 Video clipped to window interior
The video must not bleed over the mode-line or other windows.
** TODO 17.5 Stop with gpu-video-stop
The placeholder must remain and the video must stop playing.
** TODO 17.6 Stable 30fps cadence (no jumps, no timer starvation)
The median interval between frames must be ~33ms with p90 < 40ms.
* 18. GL backend / Linux (platform-specific)
** TODO 18.1 Partial present (EGL_EXT_buffer_age)
Scroll in a large buffer. Only the changed region must be submitted
(verify with GL_LOG_PRESENT=1 that damage is partial).
** TODO 18.2 No flash from stale XDBE buffers
=inhibit-double-buffering= must be set for GL frames. No startup
content should appear during buffer transitions.
** TODO 18.3 Scroll parity: cairo vs GPU
Run run-bench.sh and compare page-scroll and line-scroll FPS against
the cairo baseline. page-scroll must be at parity (1.0x),
full-redraw > 1.0x.
** TODO 18.4 Resize under Xvfb without black bands
Resize the window under Xvfb + openbox. The resized frame must not
show black bands or text from the previous size.
** TODO 18.5 buffer-age == 0: re-query surface size
After a resize, the first frame with age==0 must use the new size,
not the cached stale size.
** TODO 18.6 EGL surfaceless headless (GL_FORCE_SURFACELESS=1)
The comparison harness under Xvfb must still produce the correct
FBO output and acceptable diffs.
* 19. Edge cases and performance
** TODO 19.1 Buffer with 100k lines
Open a large file (logs). Scrolling must be fluid with no perceptible
lag.
** TODO 19.2 Many simultaneous overlays (hl-line + multiple packages)
With hl-line, doom-modeline, corfu and company all active, verify
there is no flickering in the mode-line or windows.
** TODO 19.3 Theme change (M-x load-theme)
Switch from a light to a dark theme. All colors must update without
artifacts or remnants of the previous theme.
** TODO 19.4 Atlas overflow (glyph atlas reset)
Use many different fonts and sizes until the atlas fills and resets.
Text must continue to display correctly after the reset.
** TODO 19.5 Garbaged frame (iconify and restore)
Iconify and then restore the window. Content must be fully and
correctly redrawn.
** TODO 19.6 MTL_LOG_SEQ=1 — no critical errors
=MTL_LOG_SEQ=1 emacs -Q= and perform normal operations. Logs must
not show unexpected "outside cycle" entries or presents without a
corresponding draw.
* 20. Configuration and knobs
** TODO 20.1 gpu-status reports the active backend
=(gpu-status)= must report "Metal" or "OpenGL" according to the
platform.
** TODO 20.2 EMACS_GPU_DISABLE=1 disables the backend
Start with that env var. Rendering must be identical to vanilla Emacs
(NS on macOS, cairo on Linux).
** TODO 20.3 gpu-enable-for-frame / gpu-disable-for-frame
Disable GPU rendering on a specific frame. That frame must use the
native renderer while others remain on GPU.
** TODO 20.4 gpu-capture-frame (macOS)
=(gpu-capture-frame "/tmp/capture.png")=
The capture must match what is visible on screen.
** TODO 20.5 Warm glyph cache on startup (no blink on first draw)
Open Emacs and switch tabs immediately. The first redraw must not
show a blank frame while ASCII is being rasterized into the atlas.
* Platform notes
- Items marked [Metal] apply only on macOS with the Metal driver.
- Items marked [Linux] or [GL] apply only with the OpenGL/EGL driver.
- Unmarked items must be verified on both platforms.
- The comparison baseline is always: same binary with EMACS_GPU_DISABLE=1.