Rendering Modes - D0n9X1n/SonicTerm GitHub Wiki
Rendering Modes
SonicTerm always creates a wgpu adapter and device, then resolves whether to use normal GPU policy or software-render degradation. On Windows, degradation also switches final drawing to a CPU BGRA frame presented through GDI. On macOS and Linux, degradation keeps wgpu presentation but applies the lower-cost pacing and surface policy.
Text shaping, rasterization, and atlas ownership are described in Rendering and Fonts. Configuration keys are listed in Configuration, and retained host memory is in Memory.
Adapter classification and selection
The first renderer requests a surface-compatible high-performance adapter with
force_fallback_adapter = false; wgpu may still return a CPU adapter. Every
later window in the process — New Window, warm-pool, and tear-out windows —
reuses its adapter/device/queue through GpuSharedContext, so the process holds
one device. Closing the main window while another window stays open hides it and
keeps its renderer, so that device stays live. Each window owns its surface and
rendering state; no presenter can bypass failed wgpu startup.
Software classification is a pure function over wgpu::AdapterInfo. It returns
true when device_type == Cpu, or when the lowercased adapter name contains one
of:
microsoft basic render driver
llvmpipe
swiftshader
software adapter
The classification selects wgpu allocation policy as well as rendering policy.
Software adapters request MemoryHints::MemoryUsage; hardware adapters request
MemoryHints::Performance.
[appearance].software_render_mode resolves the degradation flag:
| Value | Result |
|---|---|
auto |
follow adapter classification |
force |
enable degradation on any adapter |
off |
disable degradation on any adapter |
The setting reloads live. Resolution always starts from the monitor’s own frame
period, so switching from degradation to off restores the monitor cadence
instead of retaining the previous cap. On Windows, force also overrides any
transparent backdrop with opaque, because the GDI presenter cannot composite
Mica, Acrylic, or Tabbed transparency. auto does not change the configured
backdrop at platform startup.
flowchart TD
adapter["wgpu adapter"] --> cpu{"device type is Cpu?"}
cpu -- yes --> detected["software detected"]
cpu -- no --> name{"name matches known software rasterizer?"}
name -- yes --> detected
name -- no --> hardware["hardware detected"]
detected --> setting{"software_render_mode"}
hardware --> setting
setting -- auto --> follow["follow detection"]
setting -- force --> degrade["degradation on"]
setting -- off --> normal["degradation off"]
follow --> platform{"resolved flag"}
degrade --> platform
normal --> platform
platform -- "Windows + on" --> gdi["CPU BGRA + GDI present"]
platform -- "macOS/Linux + on" --> wgpuSlow["wgpu + degraded policy"]
platform -- off --> wgpuFast["normal wgpu policy"]
Normal GPU policy
The hardware path follows the monitor period. A missing or zero monitor refresh
rate keeps the 60 Hz default. Surface presentation prefers Mailbox when the
backend offers it and otherwise uses Fifo. Opaque backdrops use
CompositeAlphaMode::Opaque; transparent backdrops use
CompositeAlphaMode::PreMultiplied. The desired maximum frame latency is 2.
On macOS, wgpu's Metal backend offers only Fifo and Immediate, so macOS
always presents Fifo. There, a window whose streaming output is deferred is
admitted on a display-link tick rather than one period after its last render
(see Owner-local frame scheduling).
SonicTerm renders into a retained offscreen frame texture. A frame key covers
visible pane revisions, geometry, selection, tabs, overlays, hover, inline
media, font/style state, and other image-affecting inputs. Effective scrollbar
opacity is quantized in each keyed pane record: Never, panes without
scrollback, and opacity at or below the shared emit floor all map to zero.
Every frame plans one of three modes. Noop rebuilds and submits nothing: the
frame key is unchanged, the only change is pane revisions whose dirt is all
scrolled out of view, or the key changed with empty damage and no dirty live
row. Any other frame is Partial or Full. A hardware frame is Partial when
its damage is narrower than the surface and nothing forces a full repaint: it is
not a first frame, has no full-class change, no overlay before or after, and a
valid ink record for every clean visible row. A Partial frame assembles every
dirty slot, every row whose ink-padded strip meets the damage, and every row
whose valid ink record meets the damage, the drawn cursor cell or the last
presented recolor bounds (Rendering and Fonts). Every
other frame is Full. The degraded path is never Partial.
The private production FramePlan owns that key together with final mode,
damage, pane full/content clips, resolved viewport rows, and expected revisions.
It receives metadata without grids or GPU objects; cell shaping and atlas
mutation remain in GpuRenderer. The same planner drives deterministic tests
and both presenters. Pane padding may leave an empty image-content clip even
though the existing cell layout retains its one-cell floor.
Surface-acquisition paths that do not successfully present clear the cached
frame key. Outdated and Suboptimal reconfigure the surface; Lost recreates
and configures it, and a later frame acquires from it only while the device
still accepts work; a Validation result stops the device (see Stopped GPU
device below). A SurfaceTexture is dropped before reconfiguration. The next
frame therefore cannot treat a blank or replaced swapchain as already rendered.
A normal renderer's glyph atlas starts at 1024×1024 at scale 1 (up to 1.5) and
at 2048×2048 above it, and doubles up to 2048×2048 when a glyph does not fit;
a warm spare starts at 256×256. On the GPU path the glyph texture has the same
size as the CPU atlas, so a scale-1 window starts with 4 MiB in each instead of
16 MiB, saving 12 MiB of CPU memory and 12 MiB of GPU memory. Scale 2 starts at
the maximum and saves nothing. Both starts are what the sizing rule in
sonicterm-text's start_size_inputs selects over CI measurements of the S9
and S12 working sets: the largest scale-1 working set fits 1024 and the largest
scale-2 one needs 2048.
Windows LCD subpixel policy
LCD eligibility and blending are documented in Rendering and Fonts.
Software-render degradation
Degradation replaces the monitor period with an exact 25,000 µs period, about
40 fps. This is an override, not max(monitor_period, 25 ms): even a slower
30 Hz monitor resolves to 25 ms. While an IME composition is active, the period
is 83,333 µs, about 12 fps. Ending composition immediately restores 25,000 µs.
The hardware path ignores the IME cap.
| Path | Frame period |
|---|---|
| hardware | monitor period |
| degraded software | 25,000 µs (~40 fps) |
| degraded software with IME composition | 83,333 µs (~12 fps) |
On the degraded path, all redraws—including input redraws—are coalesced to the
resolved period because every frame is CPU-expensive. Scrollbar auto-hide snaps
immediately to visible after activity and snaps hidden at the 600 ms idle
boundary. Accelerated windows keep the 150 ms fade-in and 300 ms fade-out. On
both paths, a settled scrollbar requests no frames until its idle deadline
(last_active + 600 ms). That deadline belongs to its window, fires once and is
re-armed by new activity. On the accelerated path it starts the 300 ms
fade-out. Edge hover and a thumb drag hold the bar and arm no deadline. The
first fade step after a target change is capped at one 60 Hz frame, so a late
wake still fades over several frames. The wgpu surface uses Fifo,
opaque compositing, and desired maximum frame latency 1.
The hidden warm-renderer pool defaults to one. A configured value of 0
disables it. Hardware honors targets through 5; degradation caps every nonzero
target at 1.
Owner-local frame scheduling
Window input and output causes never share an application-wide dirty latch. Hardware pure input may bypass pacing only for its live source window; input with visible output stays paced. Monitor periods are refreshed for each window on creation/adoption, move, scale, resize, focus gain and un-occlusion; an unavailable or zero rate keeps the last period. The exact 25,000 µs degraded and 83,333 µs IME periods still come from global degradation policy, not a per-window copy.
Each window keeps two pacing clocks. last_render is the attempt clock: every
renderer call moves it. stream_clock paces streaming output on the hardware path.
It moves with every attempt except one: a hardware attempt for a new input
generation that settled without presenting (Skipped), with no surface timeout or
contention floor pending. A keypress frame that found no echo yet therefore does
not delay the echo by a period; the echo is still streaming work, paced from the
previous non-exempt attempt. The surface-timeout retry waits from last_render,
the degraded software path paces every attempt from last_render, and presented,
cached, retried, failed and stopped attempts move both clocks. The armed Frame
deadline uses the same clock choice as admission. The window counter
stream_clock_exempt counts the attempts that kept stream_clock.
Owner-addressed wake entries service only due windows; maintenance does not wake
unrelated windows or suppress a coincident repaint. A native frame request already
in flight suppresses only duplicate Frame deadlines: notification and scrollbar
idle expiration remain armed, clear once when due, and coalesce their repaint
with that existing request. A scrollbar expiry requests a frame only when it
changed the bar; activity or a hold after the deadline was collected makes it a
no-op.
On macOS 14 and later, window registration installs a per-window
NSView.displayLink, created paused, whose preferred rate is the window's monitor
period and follows every refresh that changes it. A hardware deferral that the
streaming rule wins stores the pacing mode Link when the window has a link;
a surface-timeout or contention deferral, and a streaming deferral on the
degraded software path or in a window with no link, store Timer and keep the
rules above. A Sync deferral stores no mode and keeps one already stored; it
pauses the link until admission re-evaluates the hold. The stored mode holds until the frame is admitted; link invalidation
clears a stored Link but never a stored Timer. The link runs only while a
Link admission is pending: the wait fold starts it after collecting deadlines
and pauses it when nothing link-paced is pending or the window cannot schedule
frames. Each start bumps the window's link generation, and a tick is accepted
only for the running generation and a pending Link admission; an accepted tick
authorizes one frame. Native and backend occlusion, device stop, park, hide and
software degradation turning on invalidate link pacing at the writer, which bumps
the generation and drops an unused tick. If no tick comes, the Frame deadline is
a fallback ceiling two periods after the pacing clock. Input fast paths, the
surface-timeout retry, the contention floor, the 25,000 µs and 83,333 µs periods,
earlier macOS, Windows and Linux are unchanged: they install no link and pace
from the timer.
A display-link-paced frame can still wait for a drawable. Admission and the
synchronous present call run in the same RedrawRequested handler, so a frame
is never admitted before the previous present call returned, but a returned
present does not release its drawable. wgpu-hal's Metal surface sets
maximumDrawableCount to the frame latency plus one (3 on the hardware path),
disables allowsNextDrawableTimeout and ignores the acquire timeout, so when
three frames are still outstanding the next acquisition waits in nextDrawable.
Display-link pacing does not remove that wait, and the timer path has the same
exposure. No counter measures it; Metal System Trace in Instruments shows it.
Output events are serviced per pane. A VT worker keeps at most one PaneOutput
outstanding for each pane. The event loop acknowledges it in the window that holds
the pane now, runs that window's command maintenance, and requests an Output
frame only when the window's active tab, or its zoomed pane, has unseen output.
When only a tab's command badge or its frame-key command status changed, it
requests a Chrome frame instead. Otherwise it requests nothing, so output in a
background tab costs no frame: it stays in its pane's grid and output generation,
and switching to that tab dirties every pane and draws the latest content.
RequestRedraw, which harnesses and tests send, still requests an Output frame
unconditionally.
StructuralInvalid consumes the captured attempt's causes and parks the window. Parked windows contribute no frame, retry, pacing, cursor, scrollbar, notification, or badge deadline. Only Topology, Input, Visibility, or DeviceRecovered causes unpark; worker Output still runs command maintenance but does not unpark. A missing closing layout is silent. Device-stop reporting precedes parking and collection; no parser/media lock is needed to report the stop once. Clearing device-stop suppression additionally requires the installed renderer to be usable, not marked for destruction, and on a different generation; a recovery cause alone is not proof. This scheduling adapter does not rebuild devices; the shared recovery coordinator below installs the replacement before the owner-local adapter admits a frame.
Synchronized output (DEC 2026) adds the Sync deferral rule, after the surface-timeout
and contention rules and before streaming. A pane holds its window while it is in the
active tab's visible set, its published update is open, its 150 ms deadline has not
passed, and every reset it has published has reached a successful frame. The window
holds for at most 150 ms from the first Sync deferral of a stretch, whatever pane
caused it; a Presented, Cached or Settled outcome ends the stretch and a failed
outcome keeps it. A held window wakes at its earliest held deadline or its cap, and
its display link neither runs nor accepts ticks. The first frame, a pending
Visibility or DeviceRecovered, a changed surface size and a reconfigured or
recreated surface (Outdated, Suboptimal, SurfaceLost) force the frame through
the hold; only a presented frame clears the last two. A cleared retained key, such as
on a focus change, does not. Both redraw adapters recheck the hold under the collected
parser guards before applying receipts and abandon a held frame without settling it,
so no clock, retry floor, receipt or cause changes.
Occlusion and surface availability
Native Occluded events are handled per window on macOS and X11, before either
main or child collection. Occluded windows retain pending input/output identities
and grid dirt, but contribute no frame, cursor, scrollbar, or notification
deadlines and collect no parser/media state. PTY output and command maintenance
continue. App-hidden main windows and unadopted warm windows remain separate.
The device-refusal boundary, including smoke-only evidence and its one-time error
report, runs first; suppression changes neither pacing clock nor the contention
retry floor. A transition back to visible clears the retained renderer frame key,
marks one Visibility cause, and requests at most one frame on a usable device.
Duplicate visible events add nothing; visibility cannot revive a stopped device.
A window covered for 30 s is trimmed (Memory); on the GPU presenter its frame texture drops to 1×1. The next GPU present first rebuilds it at the surface size, before the device gate admits the frame's upload work; a refused rebuild returns rendering-unavailable before any size check and keeps the dirt for a later frame. A resize, a degrade switch or a recovery commit installs a surface-sized texture itself, so the present after it rebuilds nothing.
Typed SurfaceRetry(Timeout) belongs to the app at the owner's next effective
frame period, retaining dirt without a native self-retry loop. Typed
SurfaceRetry(Occluded) suppresses frames. Atlas, Outdated, Suboptimal, and
SurfaceLost retain their presenter retry ownership. The public render -> Result
adapter restores the legacy native retry only for Timeout and Occluded; it does
not double-request the other reasons. DeferStop and Stop keep their prior behavior.
Only backend-only occlusion on macOS arms an exceptional one-second surface availability probe. Native occlusion/visibility events cancel it, and app-hidden, parked, or stopped owners do not contribute it. The method admits GPU work through the device gate and rejects non-Metal adapters before acquisition: pinned Metal can discard an acquired texture, while Vulkan's discard is a no-op. It acquires and drops without encoding, uploading, submitting, presenting, or acknowledging. Success clears retained identity and schedules one Visibility frame; Timeout and Occluded rearm after one second. Outdated/Suboptimal reconfigure, SurfaceLost recreates, and all rearm only after rechecking the gate. A suboptimal texture is dropped before configure. Refused gates stop probing. Surface-recreation errors are logged and rearm the one-second probe only while the device remains usable and the owner remains backend-occluded, visible, and unparked. They do not assemble a frame or consume dirt. Native acquire/configure may block; this is a slow exceptional check, not a nonblocking guarantee or normal heartbeat.
__occlude_next_surface_acquire is a test fault seam for the real typed retry
exit at the wgpu presenter's acquire step, without a Space switch or a covered
window. It applies on every platform's wgpu presenter; the Windows software
presenter has no surface acquire, so an armed fault waits for a wgpu frame. The
Windows release-order test drives the compatibility wrapper through it and checks
that a surface retry keeps the grid's dirt. Fake-clock owner tests and source contracts
cover the policy, but do not replace same-window full-app CPU measurements or
native proof of the first full presented frame.
Lock-contention retry
Each window keeps retry_not_before separate from its last-frame timestamp.
Only a visible parser or image store the collector could not take enters this
path. A collection probes each visible parser, then waits for a busy one with
one 2 ms deadline shared by every visible pane; a parser still busy at the
deadline, or one taken after it, is a miss. Scheduler preemption can overrun the
deadline. Image stores are only probed. The PTY worker releases each parser
section fairly, handing the lock to a selected waiter before it can relock; a
waiting collection takes part in that selection with other blocking parser
callers and is not guaranteed the next handoff. Without the wait, a flood's
back-to-back sections left almost no gap for a nonwaiting probe, and most redraws
fell to this retry floor. Hidden-tab and
zoom-hidden stores are not visited by either role's frame collector. Both roles
hold visible parsers before copying visible media; these are separate, not atomic,
snapshots. Invalid topology skips the entire assembly without arming this floor,
and a closing tab with no layout skips silently.
A failed due parser/image collection sets that deadline to the attempt time plus the effective frame period. The retry is a floor over normal pacing, including degraded IME pacing. Earlier input or redraw events cannot bypass or postpone it. A due failure rearms it; coherent collection clears it before atlas/surface retry policy runs. Closing the window discards it. No unconditional redraw heartbeat or blocking parser/image lock is introduced. A frame holds the visible parser guards only while it assembles; they are released before the surface is acquired and the frame presented, on the GPU and GDI paths alike. The retry floor stays one full frame period.
Windows CPU presentation
When degradation is active on Windows, software_frame::SoftwareFrame composes
the same producer-built quads, text glyphs, color glyphs, and inline-image instances
into a complete premultiplied BGRA buffer. The Windows-only software_windows
bridge borrows the validated frame and presents it to the HWND with GDI
SetDIBitsToDevice; retained GPU damage is not used as a second software
presentation policy.
CPU composition contains no native-window or GDI imports and forbids unsafe code.
It compiles for Windows production and every host's unit tests. The flat sibling
software_frame_tests.rs keeps the pixel assertions; its existing GPU-parity
cases also require a headless wgpu adapter. cargo test -p sonicterm-gpu runs
these tests on macOS, Windows, and Linux. Native GDI capability, selection
presentation, and smoke checks remain Windows-only. Compiling the CPU compositor
for tests does not add a software presenter on macOS or Linux.
The software frame is limited to 16,384 pixels on either axis and 160 MiB total. Construction or resize beyond either limit fails without replacing the existing valid allocation. A frame-key hit can re-present the existing CPU frame without recomposing it.
CPU atlases supply software drawing; GPU mirrors remain 1×1 placeholders. Returning to GPU rebuilds full textures, resets UV-bearing caches, and forces a full redraw. Pixel conversion and sampling are shared with GPU drawing and are specified in Rendering and Fonts.
The wgpu frame texture is unused while the GDI presenter is active, so it is
1×1 (4 B) then and the surface size otherwise. build_frame_texture sizes it at
construction, resize, recovery and the degrade switch; leaving GDI allocates
the full texture once and forces a full next frame. A stopped device keeps the
old texture, and recovery builds it from the current mode. The texture is GPU
memory outside retained_amounts; GpuRenderer::frame_texture_extent reports
its size.
Stopped GPU device
A Validation, OutOfMemory, or Internal wgpu error, or a device loss, stops
rendering in every window, because the windows share one device; the
containment rules are in
Architecture Internals. Both presenters obey the stop:
the wgpu path submits and presents nothing, and the Windows CPU presenter
neither composes nor presents a frame, nor reblits an unchanged one. The windows stay open, and whether their
last presented pixels stay visible is up to the OS and driver. Dirty rows stay
unacknowledged, while shells, input, sessions, and window lifecycle keep
working. A software-render policy change made while the device is stopped is
recorded without configuring the surface or rebuilding GPU atlas textures.
An Unusable device without a recorded loss remains stopped. A Lost committed
device starts shared-context recovery; the sonic::gpu records on
Logging name the operation and error that stopped it.
Shared-device recovery
The application owns one committed context and one recovery coordinator. A loss creates a new instance, adapter, device, and queue using startup's feature and allocation policy. One persistent worker requests the adapter/device without blocking the event loop; a timed-out worker is never replaced or joined.
There are at most five attempts per budget. Their delays are 0, 250 ms, 1 s, 4 s, and 16 s, with a 10 s request deadline. An attempt due while an older request still runs consumes its budget without starting another worker. A late result is discarded and its candidate destroyed on the event-loop thread. A new generation starts a fresh budget only if a later loss occurs at least 30 s after its first acknowledged presentation; merely creating a device is not stability evidence. Exhaustion leaves rendering stopped and keeps PTYs alive.
The event loop first prepares every live and warm renderer on the candidate,
then commits all of them in one callback. Surfaces, retained frame textures,
pipelines and atlas uploads are rebuilt, even when dimensions match. CPU
atlases and UV-bearing caches reset; fonts, cell metrics, terminal state and
frame counters remain. The current software-render policy is re-read, while
existing windows keep their native backdrop. A partial commit closes and
destroys the candidate before event dispatch resumes. A successful commit
retires the old device and admits each rebound owner through its validated
replacement snapshot. Hidden or natively occluded owners retain dirt without a
frame request; each renderable owner coalesces one recovery frame. Grid dirt is
acknowledged only after a real Presented outcome.
Generation-tagged callbacks cannot revive retired devices or start recovery for them. A closed requesting window does not invalidate its owned in-flight surface, and surviving windows are re-evaluated when the result arrives. Recovery-only timers do not request redraws. Pending results are checked at 100 ms intervals, reduced to 1 s after exhaustion, solely to dispose them if a completion hint was missed; an idle coordinator has no such timer.
The request deadline bounds the decision to abandon an attempt, not native driver destruction or surface configuration. Normal result disposal runs on the event loop. After application shutdown, a detached worker's late result has ownership-safe best-effort disposal, which may block on native main-thread destruction; shutdown never waits for that worker. Startup device failure still follows the ordinary startup path, and recovery adds no new renderer backend.
Retained pixels and damage
Damage and draw order are documented in Rendering and Fonts.
Grid dirty rows index the live buffer, so the frame plan maps each one to the
viewport slot that draws it. Live row r is absolute row scrollback_len + r.
A primary view scrolled back by k rows draws it at slot r + k, and draws it
nowhere when r + k is past the last row. Primary-screen damage covers only
those slots; the alternate screen keeps no scrollback and still damages its
whole pane. On every assembled frame, Full or Partial, the background-quad
cache drops absolute row scrollback_len + r for every dirty live row of each
pane on the surface, whether that row is on screen or not. The glyph cache drops
nothing for dirt: it keys rows by content, so unchanged content stays eligible
for reuse at another position. Reuse still needs a retained complete entry,
matching keyed inputs and atlas identity, and valid software-block dimensions.
A Partial frame assembles only the rows listed above. A row it does not emit
keeps its retained pixels and its ink record, and a dirty live row whose slot the
frame did not draw keeps its dirt bit. Any frame that does not present (a surface
retry, an atlas retry, or a stopped device) commits no record or receipt and
clears the frame key, so the next frame is a whole-surface Full.
A frame whose only change is pane revisions, with every changed pane's dirt
scrolled out of view, presents nothing on either path: the plan is Noop with
empty damage and acknowledges no dirt. The rows stay dirty until a frame that
shows them, which is a whole-surface Full because scrolling changes the
viewport. While an overlay is active (IME preedit, search, palette,
notification, link preview, drag chip, or focus flash) in the old or new key, any
change to the frame key repaints the whole surface instead, because a preedit
follows the live cursor, while the frame key records only the drawn cursor cell,
which is absent when the cursor is hidden, the window unfocused, the pane
read-only or the view scrolled back. A
revision change with no dirty live row and no other damage, such as one from
set_autowrap, also plans Noop on both paths.
Cursor, focus, tab-band, selection and scrollbar changes damage only their own areas (Rendering and Fonts). That narrow damage takes effect only where the wgpu retained path draws: macOS, and Windows or Linux through wgpu. The degraded path repaints the whole surface for any of these changes, and the Windows GDI presenter composes the whole frame without reading damage.
Diagnostics
Startup logs the adapter backend, name, device type, and
software_rendering=true|false. When degradation resolves on, the app logs:
software-render degrade engaged
with detected, mode, and frame_period fields. On Windows, breadcrumb
renderer identity distinguishes CPU/GDI software presentation from wgpu.
For frame phase timing, set [logging].level = "debug" and read the
render_timing target; it times the phases of each frame that completes. For
what it cannot see, read the frame_counters target at the same level
(Logging): redraws that were deferred or found
a lock busy, retries and other outcomes, present intervals, parser lock waits and
holds, flush-to-redraw delay, and dispatch stalls. Use render_timing when a
frame is slow, and frame_counters when frames are late, missing, or contended;
it writes at most one line a second per window. Memory snapshots and allocator-state interpretation are
owned by Logging and Memory.
Code locations
| Topic | Primary paths |
|---|---|
| Adapter classification and surface policy | crates/sonicterm-gpu/src/core.rs |
| Config-to-degradation decision | crates/sonicterm-app/src/app/{frame_pacing,event_loop,config_apply}.rs |
| Frame pacing | crates/sonicterm-app/src/app/{mod,frame_pacing,redraw,display_link}.rs |
| Retained frame and damage | crates/sonicterm-gpu/src/core.rs |
| Device error containment | crates/sonicterm-gpu/src/{device_errors,core,present}.rs |
| Shared-device recovery | crates/sonicterm-app/src/app/{gpu_recovery,gpu_recovery_worker}.rs, crates/sonicterm-gpu/src/{recovery,recovery_context,rebind}.rs |
| GPU draw | crates/sonicterm-gpu/src/wezterm_pipeline.rs |
| Retained-frame blit | crates/sonicterm-gpu/src/core.rs |
| CPU composition and Windows bridge | crates/sonicterm-gpu/src/{software_frame,software_windows}.rs |
| Windows backdrop override | crates/sonicterm-windows/src/{main,software_presenter}.rs |