This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch tymux
in repository terminology.

View the commit online.

commit 5acf742992ffd4f616f61b26282fd144b682533a
Author: [email protected] <[email protected]>
AuthorDate: Sun Mar 22 10:31:23 2026 -0600

    feat: implement daemon-side backlog buffer management
    
    The tymux daemon now mirrors its Termsave scrollback into memfd-backed
    shared-memory buffers.  Each buffer holds up to 256 lines at a fixed
    column width.  New buffers are created when the head fills or the
    terminal width changes.  Oldest buffers are evicted when total lines
    exceed the configured scrollback limit.  The NOTIFY message now carries
    backlog state so clients can track buffer availability.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 src/bin/termd_tymux.c | 231 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 229 insertions(+), 2 deletions(-)

diff --git a/src/bin/termd_tymux.c b/src/bin/termd_tymux.c
index 1d46f6db..37cc1b00 100644
--- a/src/bin/termd_tymux.c
+++ b/src/bin/termd_tymux.c
@@ -17,6 +17,7 @@
 #include "termd_tymux.h"
 #include "termpty.h"
 #include "termsrv.h"
+#include "backlog.h"
 
 /* ── log domain ─────────────────────────────────────────────────── */
 
@@ -62,6 +63,18 @@ struct _TermDTymux
    int            shm_fd;
    TermShmHeader *shm;
    Eina_List     *clients;
+
+   /* backlog buffer chain */
+   struct {
+      int                   fd;          /* memfd */
+      TermBacklogBufHeader *hdr;         /* mmap'd header */
+      size_t                total_size;  /* mmap size */
+   }              *backlog_bufs;         /* dynamic array of buffers */
+   int             backlog_buf_count;    /* number of buffers in chain */
+   int             backlog_buf_alloc;    /* allocated array slots */
+   int             oldest_buf_id;        /* global id of oldest buffer */
+   int             next_buf_id;          /* next global id to assign */
+   size_t          last_flushed_backpos; /* backpos at last flush */
 };
 
 /* ── synthetic Config for headless use ───────────────────────────── */
@@ -79,6 +92,193 @@ _default_config_new(void)
    return cfg;
 }
 
+/* ── backlog buffer helpers ──────────────────────────────────────── */
+
+static int
+_backlog_buf_new(TermDTymux *sess, int cols)
+{
+   char                  memfd_name[128];
+   size_t                total_size;
+   int                   fd;
+   TermBacklogBufHeader *hdr;
+   int                   idx;
+
+   total_size = TERMSRV_BACKLOG_TOTAL_SIZE((unsigned)cols);
+
+   snprintf(memfd_name, sizeof(memfd_name),
+            "ty-backlog-%s-%d", sess->name, sess->next_buf_id);
+
+   fd = _memfd_create(memfd_name, MFD_CLOEXEC);
+   if (fd < 0)
+     {
+        ERR("memfd_create backlog failed: %s", strerror(errno));
+        return -1;
+     }
+
+   if (ftruncate(fd, (off_t)total_size) < 0)
+     {
+        ERR("ftruncate backlog failed: %s", strerror(errno));
+        close(fd);
+        return -1;
+     }
+
+   hdr = mmap(NULL, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+   if (hdr == MAP_FAILED)
+     {
+        ERR("mmap backlog failed: %s", strerror(errno));
+        close(fd);
+        return -1;
+     }
+
+   /* grow array if needed */
+   if (sess->backlog_buf_count >= sess->backlog_buf_alloc)
+     {
+        int new_alloc = sess->backlog_buf_alloc ? sess->backlog_buf_alloc * 2 : 4;
+        void *tmp = realloc(sess->backlog_bufs,
+                            (size_t)new_alloc * sizeof(*sess->backlog_bufs));
+        if (!tmp)
+          {
+             ERR("realloc backlog_bufs failed");
+             munmap(hdr, total_size);
+             close(fd);
+             return -1;
+          }
+        sess->backlog_bufs      = tmp;
+        sess->backlog_buf_alloc = new_alloc;
+     }
+
+   idx = sess->backlog_buf_count;
+   sess->backlog_bufs[idx].fd         = fd;
+   sess->backlog_bufs[idx].hdr        = hdr;
+   sess->backlog_bufs[idx].total_size = total_size;
+   sess->backlog_buf_count++;
+
+   hdr->magic     = TERMSRV_BACKLOG_MAGIC;
+   hdr->version   = TERMSRV_BACKLOG_VERSION;
+   hdr->cols      = (uint32_t)cols;
+   hdr->num_lines = 0;
+   hdr->max_lines = TERMSRV_BACKLOG_MAX_LINES;
+   atomic_store_explicit(&hdr->write_seq, 0, memory_order_relaxed);
+
+   sess->next_buf_id++;
+   return idx;
+}
+
+static void
+_backlog_buf_evict_oldest(TermDTymux *sess)
+{
+   if (sess->backlog_buf_count <= 0)
+     return;
+
+   munmap(sess->backlog_bufs[0].hdr, sess->backlog_bufs[0].total_size);
+   close(sess->backlog_bufs[0].fd);
+
+   int remaining = sess->backlog_buf_count - 1;
+   if (remaining > 0)
+     memmove(&sess->backlog_bufs[0], &sess->backlog_bufs[1],
+             (size_t)remaining * sizeof(*sess->backlog_bufs));
+
+   sess->backlog_buf_count--;
+   sess->oldest_buf_id++;
+}
+
+static void
+_flush_backlog_to_shm(TermDTymux *sess)
+{
+   Termpty *ty           = sess->pty;
+   size_t   cur_backpos  = ty->backpos;
+   size_t   old_backpos  = sess->last_flushed_backpos;
+   size_t   backsize     = ty->backsize;
+   size_t   new_lines;
+   size_t   i;
+
+   if (cur_backpos == old_backpos)
+     return;
+
+   /* calculate how many new lines have been saved since last flush,
+    * handling the circular wrap of backpos */
+   if (cur_backpos >= old_backpos)
+     new_lines = cur_backpos - old_backpos;
+   else
+     new_lines = backsize - old_backpos + cur_backpos;
+
+   /* cap to backsize to avoid processing stale entries after full wrap */
+   if (new_lines > backsize)
+     new_lines = backsize;
+
+   /* ensure at least one head buffer exists */
+   if (sess->backlog_buf_count == 0)
+     {
+        if (_backlog_buf_new(sess, ty->w) < 0)
+          return;
+     }
+
+   /* iterate new lines from oldest to newest:
+    * BACKLOG_ROW_GET(ty, i) with i=new_lines gives oldest new line,
+    * i=1 gives the most recent */
+   for (i = new_lines; i >= 1; i--)
+     {
+        TermBacklogBufHeader *hdr;
+        Termcell             *dst;
+        Termsave             *ts;
+        uint32_t              copy_cols;
+        uint32_t              line_idx;
+
+        /* get or create a head buffer with matching cols */
+        hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+        if (hdr->num_lines >= hdr->max_lines || (int)hdr->cols != ty->w)
+          {
+             if (_backlog_buf_new(sess, ty->w) < 0)
+               break;
+             hdr = sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+          }
+
+        ts = BACKLOG_ROW_GET(ty, (int)i);
+        if (!ts || !ts->cells)
+          continue;
+
+        line_idx  = hdr->num_lines;
+        dst       = TERMSRV_BACKLOG_LINE(hdr, line_idx);
+        copy_cols = (ts->w < hdr->cols) ? (uint32_t)ts->w : hdr->cols;
+
+        memcpy(dst, ts->cells, (size_t)copy_cols * sizeof(Termcell));
+        if (copy_cols < hdr->cols)
+          memset(dst + copy_cols, 0,
+                 (size_t)(hdr->cols - copy_cols) * sizeof(Termcell));
+
+        hdr->line_widths[line_idx] = copy_cols;
+        hdr->num_lines++;
+     }
+
+   /* evict oldest buffers while total lines exceed configured scrollback,
+    * but always keep at least one buffer */
+   {
+      int    total_lines = 0;
+      int    b;
+      for (b = 0; b < sess->backlog_buf_count; b++)
+        total_lines += (int)sess->backlog_bufs[b].hdr->num_lines;
+
+      while (total_lines > sess->config->scrollback &&
+             sess->backlog_buf_count > 1)
+        {
+           total_lines -= (int)sess->backlog_bufs[0].hdr->num_lines;
+           _backlog_buf_evict_oldest(sess);
+        }
+   }
+
+   /* bump head buffer write_seq with release semantics */
+   if (sess->backlog_buf_count > 0)
+     {
+        TermBacklogBufHeader *head =
+           sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+        uint64_t seq = atomic_load_explicit(&head->write_seq,
+                                            memory_order_relaxed);
+        atomic_store_explicit(&head->write_seq, seq + 1, memory_order_release);
+     }
+
+   sess->last_flushed_backpos = cur_backpos;
+}
+
 /* ── shm flush ───────────────────────────────────────────────────── */
 
 static void
@@ -140,9 +340,23 @@ _pty_change_cb(void *data)
    TermDClient      *c;
 
    _flush_screen_to_shm(sess);
+   _flush_backlog_to_shm(sess);
+
+   pay.write_seq        = atomic_load_explicit(&sess->shm->write_seq,
+                                               memory_order_acquire);
+   pay.backlog_buf_count = (uint32_t)sess->backlog_buf_count;
+   pay.oldest_buf_id     = (uint32_t)sess->oldest_buf_id;
+   if (sess->backlog_buf_count > 0)
+     {
+        TermBacklogBufHeader *head =
+           sess->backlog_bufs[sess->backlog_buf_count - 1].hdr;
+        pay.backlog_head_seq =
+           (uint32_t)atomic_load_explicit(&head->write_seq,
+                                          memory_order_acquire);
+     }
+   else
+     pay.backlog_head_seq = 0;
 
-   pay.write_seq = atomic_load_explicit(&sess->shm->write_seq,
-                                        memory_order_acquire);
    EINA_LIST_FOREACH(sess->clients, l, c)
      termsrv_msg_send(c->fd, TSRV_MSG_NOTIFY, &pay, sizeof(pay));
 }
@@ -336,6 +550,8 @@ termd_tymux_new(const char *name, const char *cmd)
    sess->pty->cb.exited.func    = _pty_exit_cb;
    sess->pty->cb.exited.data    = ""
 
+   sess->last_flushed_backpos = sess->pty->backpos;
+
    _flush_screen_to_shm(sess);
 
    return sess;
@@ -374,6 +590,17 @@ termd_tymux_free(TermDTymux *sess)
         free(c);
      }
 
+   {
+      int b;
+      for (b = 0; b < sess->backlog_buf_count; b++)
+        {
+           munmap(sess->backlog_bufs[b].hdr, sess->backlog_bufs[b].total_size);
+           close(sess->backlog_bufs[b].fd);
+        }
+      free(sess->backlog_bufs);
+      sess->backlog_bufs = NULL;
+   }
+
    if (sess->pty)
      {
         termpty_free(sess->pty);

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to