# Shared rendering contract: design groundwork Status: proposed direction, not an implemented renderer abstraction. The current change fixes the native macOS backend and adds regression coverage. It does not move layout or drawing into a new shared module. ## Why the backends diverged The core shares editor state, cells, tag layers, body layers, and PDF placements. It does not yet specify every visual operation needed to present them. SDL and AppKit therefore independently reconstruct tag backgrounds, compact text bands, context separators, overlay order, and opacity. A correct cell snapshot can coexist with an incorrect rendered frame. The macOS/Linux comparison exposed concrete gaps in this contract: - Reduced tag glyph bands were also used as background bounds on macOS; SDL fills the entire row. Column and pane rules were missing or painted before an overlapping layer. - Tree-sitter context rows had matching compact geometry, but the separator used the scroll-track color instead of the border color. - Repeated source-over background fills compounded window opacity. At 71%, two overlapping fills yield about 92%, and three about 98%. The window's own colored background added another layer behind the view. - CoreText fallback runs did not inherit the selected foreground color. - PDF/image composition ignored window opacity on macOS, while SDL applies it to raster content. Theme colors also need an explicit sRGB interpretation. These are duplicated policy decisions, rather than a reason to replace CoreText or require identical fonts on every host. ## Proposed ownership boundary A shared scene builder should translate the existing surface and its layers into an ordered list of positioned drawing operations. Platform code should consume that list without deciding which rows are compact, which edge has a separator, or which objects participate in window opacity. The builder would own: - Tag and context geometry, background extents, separator positions and colors, clipping, and the order of backgrounds, text, images, cursors, and overlays. - Coordinate conversion rules: logical layout units, supplied font metrics, device scale, and the rounding policy for one-device-pixel rules. - Color and compositing semantics, including clear backgrounds, replacement background fills, image coverage, selection, and opaque native text/cursors. - Stable image references and a defined frame lifetime, including snapshots retained for transitions and postprocessing. Backends would continue to own native font selection, shaping/rasterization, image upload/cache management, and presentation. Native event handling, window management, and the text-only terminal frontend are outside this proposal. A small 2D operation vocabulary could include solid rectangles, positioned text runs, images with source/destination rectangles, and explicit clips. Operations must carry semantic blend behavior and a documented color space. Wrapping `fillRect` and `drawText` while leaving each backend to build the scene would preserve most of the duplication that caused these bugs. Do not freeze a new ABI before extracting one small path and checking its needs. Font metrics must enter the shared layout explicitly; changing fonts can still change line capacity and glyph appearance. Pixel equality across different fonts, rasterizers, and display scales is not the acceptance criterion. ## Compositing contract to preserve Let p be WindowOpacity in [0, 1]. An ordinary background region has alpha p, regardless of how many logical layers cover it. Clear background regions remain clear. Native text and cursors retain their own opacity. For an image sample with coverage a and straight RGB c, the existing SDL background-layer behavior produces premultiplied output: ``` out.rgb = p * a * c + (1 - a) * dst.rgb out.a = p * a + (1 - a) * dst.a ``` This describes ordinary image attachments and the existing SDL PDF path. macOS PDFs now separate unpainted paper from content: paper uses p and content uses its own coverage, without multiplying that coverage by p. A future shared scene must carry this paper/content distinction explicitly. Transparent image pixels preserve the destination. Ordinary source-over with source alpha p*a is not equivalent. Any future operation API needs to express this distinction directly and specify sampling and premultiplication. ## Tests available before extraction `test/macos-snapshots/tag-ink.snap` checks actual rendered tag foreground pixels, covering failures that a cell-grid golden cannot detect. `test/macos-snapshots/rendering-parity.snap` uses native backing-scale metrics and checks rendered tag backgrounds and rules, top/bottom tags, opacity 100/71/0, invalid opacity rejection, and nested multiline Tree-sitter context separators. It also checks PDF placement and sampled page alpha at those opacity values, height fit, tint modes, and scrolling. `context.zig` is the nested context fixture; `docs/9p.pdf` is the existing PDF fixture. Optional PNG output is enabled with `PARDES_TEST_CAPTURE_DIR`. The Zig test named `mac tag layer ABI preserves logical capacity and physical grip` covers tag-bottom and border-color metadata as well as existing capacity and grip behavior. The existing Linux `test/window_opacity.py` checks background changes, native glyph preservation, zero opacity, invalid input, and restoration. Run on macOS: ```sh zig build unit-test -Dplatform=macos '-Dtest-filter=mac tag layer ABI' zig build macos-e2e -Dplatform=macos zig build -Dplatform=macos ``` These are baseline regressions, not a comprehensive cross-backend image oracle. PDF alpha samples do not establish exact raster color or interpolation equality; offscreen AppKit captures do not test the desktop compositor. Live comparison still matters for window transparency and display color management. ## Suggested later extraction and acceptance gates 1. Add shared geometry/scene assertions for tags and context rows using supplied metrics at several scales. Include clipping, partial rows, bottom tags, and final separator order; expected values should come from the contract rather than a copy of either backend's implementation. 2. Extract that scene construction once and adapt both native renderers. Keep current backend pixel checks running without regenerating goldens merely to accept differences. Compare matching fixtures and settings on both systems. 3. Add synthetic RGBA image coverage cases before extracting image composition: opaque, transparent, and partial-alpha pixels over a known destination at p=0, intermediate p, and p=1. Include fit, crop, and clip boundaries. 4. Extend the contract to the remaining overlays and retained transition frames. Measure frame time and allocations before broadening the abstraction. The desired result is one definition of visual policy with small native drawing adapters. A new GPU engine, shared font rasterizer, or wholesale backend rewrite is not required to reach that boundary.