DevelopmentAll versions
On this page

KetraTerm Render Cache (:ketraterm-render-cache)

Reusable primitive copies of terminal render frames, plus leased publication between a render worker and its consumers. The module depends only on ketraterm-render-api.

Use TerminalRenderCache when one worker owns the copy. Use TerminalRenderPublisher when a writer and readers need separate storage. A TerminalSession already owns a publisher; session consumers should use readPublishedFrame rather than create another publication pipeline.

How to Use

For a custom renderer, the following owner publishes on a serialized render worker and copies the latest frame on a serialized drawing thread. The drawing callback runs after the publication lease has been released.

import io.github.ketraterm.render.api.TerminalRenderFrameReader
import io.github.ketraterm.render.cache.TerminalRenderCache
import io.github.ketraterm.render.cache.TerminalRenderPublisher

class RendererFrames(columns: Int, rows: Int) {
    private val publisher = TerminalRenderPublisher(columns, rows)
    private val drawingFrame = TerminalRenderCache(columns, rows)

    fun update(reader: TerminalRenderFrameReader) {
        publisher.updateAndPublish(reader)
    }

    fun draw(paint: (TerminalRenderCache) -> Unit): Boolean {
        val copied = publisher.readCurrent { source ->
            drawingFrame.updateFrom(source)
            true
        } ?: false
        if (copied) paint(drawingFrame)
        return copied
    }
}

The drawing cache belongs to this renderer. Do not mutate its exposed arrays or retain them across later draws. A renderer may instead paint directly inside readCurrent; all access must then finish before that callback returns. Before the first publication, readCurrent returns null without invoking its callback.

For direct reads, reuse a TerminalRenderCache and call updateFrom(reader). Its overloads request a scrollback offset and optional render-only overscan; the reader determines the resolved shape. Constructing with rowCapacityReserve can absorb row-count changes without replacing cell planes. Only columns * rows cells and rows row entries belong to the current frame. Use rowOffset(row) and the reported dimensions, not array capacity, for indexing. Interpret codeWords together with TerminalRenderCellFlags; clustered cells use clusterRefs and clusterCodepoints, and wide trailing cells carry no glyph.

images contains placements in back-to-front order. Its indexed getters expose fractional destination cells and pixel source crops; copyTo delivers the same metadata through a sink. Updates copy metadata and share immutable TerminalRenderImage references, without copying pixels. Keep backend raster preparation outside the source callback and reuse rasters by resource identity. The frame lifecycle defines the resource's retainable lifetime and placement clipping.

Ownership and validity

A cache is mutable and requires thread confinement or external serialization. Publisher leases make a published cache read-only for the duration of readCurrent. Do not retain a borrowed cache or its arrays, update it, or reenter the publisher from that callback. The publisher uses three caches and may wait for readers when no non-front buffer is available for writing. Retired publisher buffers release image references once their last reader leaves, while keeping primitive storage available for reuse.

hasFrame is false initially, after reset, and after an incomplete copy. Reset clears source data and metadata while retaining dimensions and storage capacity. Reader-based updates detect replacement readers and force a complete copy; direct accept(frame) callers must reset when changing sources. Replacing the content behind the same reader also requires an explicit reset.

Retained ranges

TerminalRenderRangeCopy copies the retained intersection of an inclusive absolute row range. Reuse it on one worker; its cache and bounds remain valid until the next read. The default limits are 64 rows and 4096 cells per read, with one physical row allowed even when wider than the cell budget. Cluster payloads accompany their row and are not bounded by the cell count.

Use firstAbsoluteRow and lastAbsoluteRow for the sliced origin; the cache's history metadata still describes the source. A false result means no requested row remains and cache.hasFrame is false. Assemble strings and perform analysis after read returns. The optional cancellation check runs during source copying and must be cheap and non-suspending.

Image placements intersecting a render range are translated into its local rows. The row/cell limits do not bound image pixels: the copy retains references to full immutable resources. TerminalTextRangeCopy does not copy images.

TerminalTextRangeCopy uses the same range and batch limits for text analysis. It requests copyTextLine and retains only code words, cell flags, cluster data, row identities/generations, wrapping and source generations. Every batch replaces its active prefix; unused capacity is not cleared. Read only rows * columns cells and rows row entries after a successful read. Failed or empty reads leave rows zero. Cluster references use the same packed offset/length layout as the render cache, and both copies retain at most 256 code points per cluster. The buffer belongs to one worker and remains valid only until its next read.

Allocation behavior

Primitive planes and cluster buffers are reused, and unchanged rows skip source cell copying. Construction, capacity growth, and some shape changes allocate; this is not a guarantee of allocation-free frame updates. The cursor convenience property and clusterText produce value objects or strings. Render hot paths should use the primitive cursor fields and packed cluster storage directly.

Sub-Documentation