# Rendering Pipeline — Implementation Plan > **For Hermes:** Implement this plan task-by-task. **Goal:** Add a framebuffer-based rendering pipeline that sits between the component tree and the backend. Eliminates flicker via incremental diff output. Enables future features (mouse text selection, click-to-open-link). **Architecture:** A `framebuffer-backend` class that implements the backend protocol by writing to a cell array instead of emitting escape sequences. After all components render, a diff function compares the current framebuffer to the previous one and flushes only changed cells to a real backend. **Tech Stack:** Pure CL, CLOS protocol (inherits the existing backend protocol). --- ### Task 1: Create framebuffer.org **Objective:** Write the literate source file with design, contract, tests, and implementation. **Files:** - Create: `org/framebuffer.org` **Structure:** ``` #+TITLE: Rendering Pipeline (v0.13.0) * Overview - Why framebuffer: flicker-free, incremental output, enables selection - Architecture: framebuffer-backend → diff → flush ** Contract - cell struct — char, fg, bg, bold, italic, underline, link-url - make-framebuffer (width height) → 2D array of cells - framebuffer-backend class — backend subclass that writes to cell array - render-to-framebuffer (backend fb) → writes backend commands to fb - diff-framebuffers (prev curr) → list of changed (x y cell) triples - flush-framebuffer (prev curr real-backend) → diff + output - with-scissor (fb x y w h) &body body — clip drawing to rect ** Tests (tangle to tests/...) ** Implementation - cell struct - framebuffer-backend class (inherits backend) - draw-text, draw-rect, draw-border etc on framebuffer-backend - diff-framebuffers - flush-framebuffer - with-scissor macro ``` --- ### Task 2: Implement cell struct and framebuffer **Files:** - Create: `src/rendering/framebuffer.lisp` **Code:** ```lisp (defpackage :cl-tty.rendering (:use :cl :cl-tty.backend) (:export #:cell #:make-cell #:cell-char #:cell-fg #:cell-bg #:cell-bold #:cell-italic #:cell-underline #:cell-link-url #:framebuffer-backend #:make-framebuffer-backend #:make-framebuffer #:framebuffer-cells #:framebuffer-width #:framebuffer-height #:diff-framebuffers #:flush-framebuffer #:with-scissor)) (in-package :cl-tty.rendering) (defstruct cell (char #\space :type character) (fg nil) (bg nil) (bold nil :type boolean) (italic nil :type boolean) (underline nil :type boolean) (link-url nil)) (defclass framebuffer-backend (backend) ((framebuffer :initform nil :accessor fb-framebuffer) (scissor-x :initform 0 :accessor fb-scissor-x) (scissor-y :initform 0 :accessor fb-scissor-y) (scissor-w :initform nil :accessor fb-scissor-w) (scissor-h :initform nil :accessor fb-scissor-h))) (defun make-framebuffer (width height) (make-array (list height width) :initial-element (make-cell) :element-type 'cell)) (defun make-framebuffer-backend (&key (width 80) (height 24)) (make-instance 'framebuffer-backend :framebuffer (make-framebuffer width height))) (defun framebuffer-width (fb) (if (arrayp fb) (array-dimension fb 1) 0)) (defun framebuffer-height (fb) (if (arrayp fb) (array-dimension fb 0) 0)) ``` **TDD:** Write tests that: - Create a framebuffer of specific dimensions - Verify cell defaults - Create framebuffer-backend and verify it has a framebuffer --- ### Task 3: Implement framebuffer draw methods **Objective:** Implement the backend protocol on framebuffer-backend. **Files:** - Modify: `src/rendering/framebuffer.lisp` **Key method — draw-text:** ```lisp (defmethod draw-text ((fb framebuffer-backend) x y string fg bg &rest attrs) (let ((cells (fb-framebuffer fb)) (sx (fb-scissor-x fb)) (sy (fb-scissor-y fb)) (sw (fb-scissor-w fb)) (sh (fb-scissor-h fb))) (loop for i from 0 below (length string) for cx = (+ x i) for cy = y when (and (or (null sw) (and (>= cx sx) (< cx (+ sx sw)))) (or (null sh) (and (>= cy sy) (< cy (+ sy sh)))) (< cy (framebuffer-height cells)) (< cx (framebuffer-width cells))) do (setf (aref cells cy cx) (make-cell :char (char string i) :fg fg :bg bg :bold (getf attrs :bold) :italic (getf attrs :italic) :underline (getf attrs :underline) :link-url (getf attrs :link-url)))))) ``` Similar methods for draw-rect, draw-border, backend-clear. --- ### Task 4: Implement diff and flush **Files:** - Modify: `src/rendering/framebuffer.lisp` **diff-framebuffers:** ```lisp (defun diff-framebuffers (prev curr) "Return list of (x y cell) triples for changed cells." (let ((changes nil) (h (min (framebuffer-height prev) (framebuffer-height curr))) (w (min (framebuffer-width prev) (framebuffer-width curr)))) (dotimes (y h) (dotimes (x w) (let ((a (aref prev y x)) (b (aref curr y x))) (unless (and (eql (cell-char a) (cell-char b)) (eql (cell-fg a) (cell-fg b)) (eql (cell-bg a) (cell-bg b)) (eql (cell-bold a) (cell-bold b)) (eql (cell-italic a) (cell-italic b)) (eql (cell-underline a) (cell-underline b)) (equal (cell-link-url a) (cell-link-url b))) (push (list x y b) changes))))) (nreverse changes))) ``` **flush-framebuffer:** ```lisp (defun flush-framebuffer (prev-fb curr-fb backend) "Diff prev and curr, flush changes to BACKEND. Returns count of changed cells." (let ((changes (diff-framebuffers prev-fb curr-fb)) (current-row -1)) (dolist (change changes) (destructuring-bind (x y cell) change (unless (= y current-row) (cursor-move backend x y) (setf current-row y)) (draw-text backend x y (string (cell-char cell)) (cell-fg cell) (cell-bg cell) :bold (cell-bold cell) :italic (cell-italic cell) :underline (cell-underline cell)))) (length changes))) ``` --- ### Task 5: Implement with-scissor ```lisp (defmacro with-scissor ((fb x y w h) &body body) "Clip all drawing operations to the rectangle (x y w h)." (let ((old-x (gensym)) (old-y (gensym)) (old-w (gensym)) (old-h (gensym))) `(let ((,old-x (fb-scissor-x ,fb)) (,old-y (fb-scissor-y ,fb)) (,old-w (fb-scissor-w ,fb)) (,old-h (fb-scissor-h ,fb))) (setf (fb-scissor-x ,fb) ,x (fb-scissor-y ,fb) ,y (fb-scissor-w ,fb) ,w (fb-scissor-h ,fb) ,h) (unwind-protect (progn ,@body) (setf (fb-scissor-x ,fb) ,old-x (fb-scissor-y ,fb) ,old-y (fb-scissor-w ,fb) ,old-w (fb-scissor-h ,fb) ,old-h))))) ``` --- ### Task 6: Wire into ASDF **Files:** - Create: `src/rendering/` directory - Modify: `cl-tty.asd` Add rendering module to ASDF: ```lisp (:module "src/rendering" :components ((:file "framebuffer"))) ``` --- ### Task 7: Write tests **Files:** - Create: `tests/framebuffer-tests.lisp` Tests to write: 1. `make-framebuffer-creates-correct-size` — verify dimensions 2. `cell-defaults-are-space` — default cell has #\space char 3. `draw-text-on-fb-sets-cells` — verify text lands in right cells 4. `draw-text-clips-at-bounds` — text beyond width is ignored 5. `diff-identical-fbs-returns-empty` — no changes detected 6. `diff-changed-fb-returns-changes` — changed cells detected 7. `with-scissor-clips-drawing` — drawing outside scissor is ignored 8. `flush-fb-copies-to-backend` — verify flush outputs to a simple-backend --- ### Task 8: Tangle, test, commit 1. Tangle all org files 2. Run full test suite (verify ~368 tests pass) 3. Commit with message