Skip to content

Add fractional scroll offsets for embedders#61

Open
lawrencecchen wants to merge 1 commit into
mainfrom
cmux-smooth-scrolling
Open

Add fractional scroll offsets for embedders#61
lawrencecchen wants to merge 1 commit into
mainfrom
cmux-smooth-scrolling

Conversation

@lawrencecchen
Copy link
Copy Markdown

@lawrencecchen lawrencecchen commented May 20, 2026

Adds a libghostty embedded API for fractional scrollback offsets used by cmux smooth scrolling.

Changes:

  • Exposes ghostty_surface_scroll_to_offset.
  • Stores smooth scroll offset in renderer state and shader uniforms.
  • Applies the fractional y offset in Metal and OpenGL cell background, text, and image shaders.
  • Clears the fractional offset on normal row-based viewport changes.

Testing:

  • ./scripts/ensure-ghosttykit.sh from the cmux worktree.
  • nm -gU ghostty/macos/GhosttyKit.xcframework/macos-arm64_x86_64/ghostty-internal.a | rg "ghostty_surface_scroll_to_offset|ghostty_surface_mouse_scroll".

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

Summary by cubic

Enables smooth, sub-row scrolling for embedders by adding a fractional scroll offset API. Renderer and shaders now apply a pixel offset while keeping the terminal viewport row-based.

  • New Features
    • Adds ghostty_surface_scroll_to_offset(surface, double offset) to scroll to a fractional row offset from the top of scrollback.
    • Tracks smooth_scroll_offset and image_scroll_offset in renderer state; passed as uniforms to Metal/OpenGL.
    • Applies the fractional Y offset in cell background, text, and image shaders.
    • Clamps offset to valid scrollback range and converts the fraction to a pixel shift.
    • Automatically resets the fractional offset on normal row-based viewport changes (wheel, page/jump, selection-driven scroll).

Written for commit 8577c2b. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Added ability to scroll to a specific offset in terminal's scrollback history.
  • Improvements

    • Enhanced smooth scrolling behavior with improved offset tracking and state management across scroll operations (mouse wheel, keybindings, prompt navigation).
    • Improved rendering pipeline's smooth scroll handling and consistency.

Review Change Stack

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR adds fractional-offset scrolling to Ghostty's surface API. It introduces renderer state fields to track smooth scroll position, updates Metal shaders to apply offsets, syncs state to GPU uniforms each frame, implements Surface.scrollToOffset with fractional remainder handling, and exposes the feature through a public C API with automatic resets at viewport boundaries.

Changes

Smooth Scroll Fractional Offset

Layer / File(s) Summary
Renderer state tracking for smooth scroll
src/renderer/State.zig
Introduces smooth_scroll_offset (f32) and image_scroll_offset ([2]u16) fields to track fractional scroll position, plus resetSmoothScrollOffset method to clear both to zero.
Shader uniform contracts and Metal rendering
src/renderer/metal/shaders.zig, src/renderer/opengl/shaders.zig, src/renderer/shaders/shaders.metal
Adds smooth scroll uniform fields to Metal and OpenGL shader definitions, then implements offset application in Metal cell background, text, and image vertex calculations.
Renderer frame state sync to GPU
src/renderer/generic.zig
Initializes uniform fields during renderer setup, resets smooth scroll when auto-scroll-to-bottom triggers, and syncs current state values to GPU uniforms each frame.
Surface scrollToOffset and viewport scroll resets
src/Surface.zig, src/termio/Termio.zig
Implements Surface.scrollToOffset to scroll to fractional row offsets by splitting integral/fractional components and syncing offsets. Also resets smooth-scroll in selection scrolling, scroll-wheel, keybinding scroll-to-row, keybinding scroll-to-selection, viewport scroll, and prompt jump paths.
Public C API for smooth scroll
include/ghostty.h, src/apprt/embedded.zig
Declares ghostty_surface_scroll_to_offset in public header and implements C ABI wrapper that forwards to Surface.scrollToOffset with error logging.

Sequence Diagram

sequenceDiagram
  participant Client as Embedding Client
  participant CAPI as C API Layer
  participant Surface as Surface
  participant RendererState as RendererState
  participant Renderer as Renderer
  participant GPU as GPU/Shaders
  Client->>CAPI: ghostty_surface_scroll_to_offset(offset)
  CAPI->>Surface: scrollToOffset(offset)
  Surface->>RendererState: resetSmoothScrollOffset()
  RendererState->>RendererState: clear smooth_scroll_offset to 0
  Surface->>Surface: split offset into row + fraction
  Surface->>Surface: scroll by integral rows
  Surface->>RendererState: update smooth_scroll_offset, image_scroll_offset
  Surface->>Renderer: queue render
  Renderer->>RendererState: read smooth_scroll_offset
  Renderer->>Renderer: sync to uniforms
  Renderer->>GPU: upload uniforms
  GPU->>GPU: apply offset in cell_bg, cell_text, image shaders
  GPU->>Client: rendered frame
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A fractional hop through renderer state,
Smooth scrolls cascade through shader gates,
Uniform fields bloom in GPU light,
While Surface bounds the offset's flight,
Each API call resets the way—
Smooth scrolling comes to Ghostty's day! 🎨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add fractional scroll offsets for embedders' accurately and concisely summarizes the main change: exposing a new API for embedders to use fractional scroll offsets.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cmux-smooth-scrolling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/renderer/generic.zig`:
- Around line 1236-1237: The two assignments to self.uniforms
(self.uniforms.smooth_scroll_offset and self.uniforms.image_scroll_offset) are
under state.mutex and can race with drawFrame readers; move these uniform writes
so they occur while holding draw_mutex instead of state.mutex. Specifically,
after updating any state under state.mutex keep the new values and then acquire
draw_mutex before writing into self.uniforms (and release it immediately after),
ensuring drawFrame only reads uniforms while draw_mutex is held; reference the
symbols self.uniforms, state.mutex, draw_mutex, and drawFrame when locating
where to move the writes.

In `@src/Surface.zig`:
- Around line 3524-3535: The code currently truncates pixel_offset to an integer
which drops sub-pixel fractional scroll and causes visible stair-stepping;
change pixel_offset to remain a float (set pixel_offset = -fractional_offset *
cell_height_float as f64), assign that float into
renderer_state.smooth_scroll_offset (no `@trunc`), then compute extra_rows_float =
`@ceil`(`@abs`(pixel_offset) / cell_height_float) and clamp/convert to u16 the same
way (using maxInt(u16) and `@intFromFloat`) before storing into
renderer_state.image_scroll_offset; update references to pixel_offset,
smooth_scroll_offset, and extra_rows_float accordingly so fractional offsets are
preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52b460e9-edf0-45f9-b8e9-2fc58618078d

📥 Commits

Reviewing files that changed from the base of the PR and between cec20c5 and 8577c2b.

⛔ Files ignored due to path filters (4)
  • src/renderer/shaders/glsl/cell_bg.f.glsl is excluded by !**/*.glsl
  • src/renderer/shaders/glsl/cell_text.v.glsl is excluded by !**/*.glsl
  • src/renderer/shaders/glsl/common.glsl is excluded by !**/*.glsl
  • src/renderer/shaders/glsl/image.v.glsl is excluded by !**/*.glsl
📒 Files selected for processing (9)
  • include/ghostty.h
  • src/Surface.zig
  • src/apprt/embedded.zig
  • src/renderer/State.zig
  • src/renderer/generic.zig
  • src/renderer/metal/shaders.zig
  • src/renderer/opengl/shaders.zig
  • src/renderer/shaders/shaders.metal
  • src/termio/Termio.zig

Comment thread src/renderer/generic.zig
Comment on lines +1236 to +1237
self.uniforms.smooth_scroll_offset = state.smooth_scroll_offset;
self.uniforms.image_scroll_offset = state.image_scroll_offset;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Move uniform writes under draw_mutex.

self.uniforms is draw-time shared state; writing it here under only state.mutex can race with drawFrame reads and violates the file’s own lock contract.

🔧 Suggested fix
 const Critical = struct {
     links: terminal.RenderState.CellSet,
     mouse: renderer.State.Mouse,
     preedit: ?renderer.State.Preedit,
     scrollbar: terminal.Scrollbar,
+    smooth_scroll_offset: f32,
+    image_scroll_offset: [2]u16,
     overlay_features: []const Overlay.Feature,
 };
 ...
-const scrollbar = state.terminal.screens.active.pages.scrollbar();
-self.uniforms.smooth_scroll_offset = state.smooth_scroll_offset;
-self.uniforms.image_scroll_offset = state.image_scroll_offset;
+const scrollbar = state.terminal.screens.active.pages.scrollbar();
+const smooth_scroll_offset = state.smooth_scroll_offset;
+const image_scroll_offset = state.image_scroll_offset;
 ...
     break :critical .{
         .links = links,
         .mouse = state.mouse,
         .preedit = preedit,
         .scrollbar = scrollbar,
+        .smooth_scroll_offset = smooth_scroll_offset,
+        .image_scroll_offset = image_scroll_offset,
         .overlay_features = overlay_features,
     };
 };
 ...
 self.draw_mutex.lock();
 defer self.draw_mutex.unlock();
+self.uniforms.smooth_scroll_offset = critical.smooth_scroll_offset;
+self.uniforms.image_scroll_offset = critical.image_scroll_offset;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/generic.zig` around lines 1236 - 1237, The two assignments to
self.uniforms (self.uniforms.smooth_scroll_offset and
self.uniforms.image_scroll_offset) are under state.mutex and can race with
drawFrame readers; move these uniform writes so they occur while holding
draw_mutex instead of state.mutex. Specifically, after updating any state under
state.mutex keep the new values and then acquire draw_mutex before writing into
self.uniforms (and release it immediately after), ensuring drawFrame only reads
uniforms while draw_mutex is held; reference the symbols self.uniforms,
state.mutex, draw_mutex, and drawFrame when locating where to move the writes.

Comment thread src/Surface.zig
Comment on lines +3524 to +3535
const cell_height_float: f64 = @floatFromInt(self.size.cell.height);
if (fractional_offset > 0 and cell_height_float > 0) {
const pixel_offset = @trunc(-fractional_offset * cell_height_float);
self.renderer_state.smooth_scroll_offset = @floatCast(pixel_offset);

const extra_rows_float = @ceil(@abs(pixel_offset) / cell_height_float);
const max_u16_float: f64 = @floatFromInt(std.math.maxInt(u16));
const extra_rows: u16 = if (extra_rows_float >= max_u16_float)
std.math.maxInt(u16)
else
@intFromFloat(extra_rows_float);
self.renderer_state.image_scroll_offset = .{ extra_rows, extra_rows };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the pixel offset fractional.

Line 3526 truncates the requested smooth-scroll displacement to a whole pixel, so small offset updates are dropped and the new API will visibly stair-step instead of tracking the embedder’s fractional position.

Proposed fix
         const cell_height_float: f64 = `@floatFromInt`(self.size.cell.height);
         if (fractional_offset > 0 and cell_height_float > 0) {
-            const pixel_offset = `@trunc`(-fractional_offset * cell_height_float);
+            const pixel_offset = -fractional_offset * cell_height_float;
             self.renderer_state.smooth_scroll_offset = `@floatCast`(pixel_offset);

             const extra_rows_float = `@ceil`(`@abs`(pixel_offset) / cell_height_float);
             const max_u16_float: f64 = `@floatFromInt`(std.math.maxInt(u16));
             const extra_rows: u16 = if (extra_rows_float >= max_u16_float)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cell_height_float: f64 = @floatFromInt(self.size.cell.height);
if (fractional_offset > 0 and cell_height_float > 0) {
const pixel_offset = @trunc(-fractional_offset * cell_height_float);
self.renderer_state.smooth_scroll_offset = @floatCast(pixel_offset);
const extra_rows_float = @ceil(@abs(pixel_offset) / cell_height_float);
const max_u16_float: f64 = @floatFromInt(std.math.maxInt(u16));
const extra_rows: u16 = if (extra_rows_float >= max_u16_float)
std.math.maxInt(u16)
else
@intFromFloat(extra_rows_float);
self.renderer_state.image_scroll_offset = .{ extra_rows, extra_rows };
const cell_height_float: f64 = `@floatFromInt`(self.size.cell.height);
if (fractional_offset > 0 and cell_height_float > 0) {
const pixel_offset = -fractional_offset * cell_height_float;
self.renderer_state.smooth_scroll_offset = `@floatCast`(pixel_offset);
const extra_rows_float = `@ceil`(`@abs`(pixel_offset) / cell_height_float);
const max_u16_float: f64 = `@floatFromInt`(std.math.maxInt(u16));
const extra_rows: u16 = if (extra_rows_float >= max_u16_float)
std.math.maxInt(u16)
else
`@intFromFloat`(extra_rows_float);
self.renderer_state.image_scroll_offset = .{ extra_rows, extra_rows };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Surface.zig` around lines 3524 - 3535, The code currently truncates
pixel_offset to an integer which drops sub-pixel fractional scroll and causes
visible stair-stepping; change pixel_offset to remain a float (set pixel_offset
= -fractional_offset * cell_height_float as f64), assign that float into
renderer_state.smooth_scroll_offset (no `@trunc`), then compute extra_rows_float =
`@ceil`(`@abs`(pixel_offset) / cell_height_float) and clamp/convert to u16 the same
way (using maxInt(u16) and `@intFromFloat`) before storing into
renderer_state.image_scroll_offset; update references to pixel_offset,
smooth_scroll_offset, and extra_rows_float accordingly so fractional offsets are
preserved.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 20, 2026

Greptile Summary

This PR exposes a new ghostty_surface_scroll_to_offset libghostty API that lets embedders (cmux) drive the scrollback viewport to a fractional row position. The terminal model scrolls to the integer row and the remaining sub-row fraction is stored as a pixel offset applied in every Metal and OpenGL cell-background, cell-text, and image vertex shader.

  • Adds scrollToOffset in Surface.zig with clamping, row decomposition, and fractional pixel offset calculation; inserts resetSmoothScrollOffset calls at all existing row-based scroll sites to clear stale offsets.
  • Adds smooth_scroll_offset and image_scroll_offset to renderer.State, both GPU uniform structs, and all relevant shaders; the Zig struct layouts correctly match the std140 and Metal packing rules.
  • image_scroll_offset is populated and uploaded to the GPU every frame but is not referenced in any GLSL or Metal shader, and kittyUpdate is not extended to draw images that become partially visible due to the fractional shift.

Confidence Score: 3/5

The core smooth-scrolling path works correctly for text, but the Kitty image rendering at fractional scroll positions is incomplete — image_scroll_offset is computed and uploaded but no code ever acts on it.

The smooth_scroll_offset wiring through shaders and all reset-on-scroll paths looks solid. However, image_scroll_offset is declared, populated, and written into GPU uniforms every frame yet is never read by any GLSL or Metal shader, and kittyUpdate is not adjusted to include the extra rows that become partially visible due to the fractional shift. Any terminal with Kitty inline images near the viewport boundary will show a missing image strip during smooth scrolling.

src/renderer/generic.zig (image_scroll_offset copy with no consumer) and src/Surface.zig (scrollToOffset pixel-offset calculation) deserve the most attention before merge.

Important Files Changed

Filename Overview
src/Surface.zig Adds scrollToOffset API and clears smooth scroll offset on all standard scroll paths; @trunc on the pixel offset loses sub-pixel precision for smooth animation.
src/renderer/generic.zig Transfers smooth_scroll_offset and image_scroll_offset from renderer state to GPU uniforms; image_scroll_offset is uploaded but never consumed by any shader or by kittyUpdate, leaving partially-visible Kitty images unrendered at fractional scroll positions.
src/renderer/State.zig Adds smooth_scroll_offset and image_scroll_offset fields with a resetSmoothScrollOffset helper; straightforward and correct.
src/renderer/shaders/glsl/common.glsl Adds smooth_scroll_offset and image_scroll_offset_packed_2u16 to the std140 uniform block; layout offsets match the Zig struct. image_scroll_offset_packed_2u16 is declared but never referenced in any GLSL shader.
src/renderer/shaders/shaders.metal Applies smooth_scroll_offset in cell_bg_fragment, cell_text_vertex, and image_vertex; layout matches the Zig Metal uniforms struct.
src/apprt/embedded.zig Exports ghostty_surface_scroll_to_offset C API function matching the header declaration; errors are logged and swallowed, consistent with other C API wrappers in this file.
src/renderer/metal/shaders.zig Inserts smooth_scroll_offset and image_scroll_offset between grid_padding and padding_extend in the Metal Uniforms extern struct; alignment matches the Metal shader struct.
src/renderer/opengl/shaders.zig Inserts smooth_scroll_offset and image_scroll_offset at the same relative position as in the Metal uniforms struct; std140 offsets match the GLSL uniform block.
src/termio/Termio.zig Adds resetSmoothScrollOffset calls in scrollViewport and jumpToPrompt; straightforward and correct.

Sequence Diagram

sequenceDiagram
    participant Embedder as Embedder (cmux)
    participant CAPI as ghostty_surface_scroll_to_offset
    participant Surface as Surface.scrollToOffset
    participant State as renderer.State
    participant Renderer as generic.Renderer
    participant Shader as GPU Shader

    Embedder->>CAPI: scrollToOffset(surface, 2.73)
    CAPI->>Surface: scrollToOffset(2.73)
    Surface->>Surface: clamp offset to 2.73
    Surface->>Surface: "floor to row=2, frac=0.73"
    Surface->>State: "terminal.scroll(.{.row=2})"
    Surface->>State: resetSmoothScrollOffset()
    Surface->>State: "smooth_scroll_offset = trunc(-0.73 x cell_h)"
    Surface->>State: "image_scroll_offset = {1,1}"
    Surface->>Renderer: queueRender()
    Renderer->>State: read smooth_scroll_offset
    Renderer->>State: read image_scroll_offset (unused by shaders)
    Renderer->>Shader: "uniforms.smooth_scroll_offset = -14px"
    Shader->>Shader: "cell_pos.y += smooth_scroll_offset"
    Note over Shader: image_scroll_offset uploaded but never read
Loading

Reviews (1): Last reviewed commit: "Add fractional scroll offsets for embedd..." | Re-trigger Greptile

Comment thread src/renderer/generic.zig
// cross-thread mailbox message within the IO path.
const scrollbar = state.terminal.screens.active.pages.scrollbar();
self.uniforms.smooth_scroll_offset = state.smooth_scroll_offset;
self.uniforms.image_scroll_offset = state.image_scroll_offset;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 image_scroll_offset uniform is never read by any shader

image_scroll_offset is stored in the GPU uniform buffer here, but neither the GLSL image.v.glsl, the Metal image_vertex function, nor any fragment shader ever reads it. The kittyUpdate CPU path also doesn't receive the value to expand the image draw range. In practice, a Kitty image whose top row is just above the fractional-scroll boundary will be omitted from the draw list, leaving a blank strip where the partially-visible image row should appear.

Comment thread src/Surface.zig

const cell_height_float: f64 = @floatFromInt(self.size.cell.height);
if (fractional_offset > 0 and cell_height_float > 0) {
const pixel_offset = @trunc(-fractional_offset * cell_height_float);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 @trunc quantises the pixel offset to whole pixels, so smooth-scroll animations will visibly step in 1-pixel increments instead of moving continuously. Because smooth_scroll_offset is a f32 uniform the shader can already accept fractional pixels; drop the truncation to preserve sub-pixel precision.

Suggested change
const pixel_offset = @trunc(-fractional_offset * cell_height_float);
const pixel_offset = -fractional_offset * cell_height_float;

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/renderer/opengl/shaders.zig">

<violation number="1" location="src/renderer/opengl/shaders.zig:185">
P2: `image_scroll_offset` is wired into uniforms but never consumed by OpenGL shaders, so the extra image-row range for fractional scrolling is not actually applied.</violation>
</file>

<file name="src/renderer/generic.zig">

<violation number="1" location="src/renderer/generic.zig:1236">
P1: Write `self.uniforms` fields only while `draw_mutex` is held; updating them under `state.mutex` introduces a race with draw-time uniform reads.</violation>
</file>

<file name="src/Surface.zig">

<violation number="1" location="src/Surface.zig:3526">
P2: Do not truncate the computed pixel offset here; truncation removes sub-pixel smooth-scroll movement and causes visible stair-stepping.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/renderer/generic.zig
// can be expensive) and also makes it so we don't need another
// cross-thread mailbox message within the IO path.
const scrollbar = state.terminal.screens.active.pages.scrollbar();
self.uniforms.smooth_scroll_offset = state.smooth_scroll_offset;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Write self.uniforms fields only while draw_mutex is held; updating them under state.mutex introduces a race with draw-time uniform reads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/renderer/generic.zig, line 1236:

<comment>Write `self.uniforms` fields only while `draw_mutex` is held; updating them under `state.mutex` introduces a race with draw-time uniform reads.</comment>

<file context>
@@ -1230,6 +1233,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
                 // can be expensive) and also makes it so we don't need another
                 // cross-thread mailbox message within the IO path.
                 const scrollbar = state.terminal.screens.active.pages.scrollbar();
+                self.uniforms.smooth_scroll_offset = state.smooth_scroll_offset;
+                self.uniforms.image_scroll_offset = state.image_scroll_offset;
 
</file context>

smooth_scroll_offset: f32 align(4),

/// Extra image-row range to draw while smooth scrolling.
image_scroll_offset: [2]u16 align(4),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: image_scroll_offset is wired into uniforms but never consumed by OpenGL shaders, so the extra image-row range for fractional scrolling is not actually applied.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/renderer/opengl/shaders.zig, line 185:

<comment>`image_scroll_offset` is wired into uniforms but never consumed by OpenGL shaders, so the extra image-row range for fractional scrolling is not actually applied.</comment>

<file context>
@@ -178,6 +178,12 @@ pub const Uniforms = extern struct {
+    smooth_scroll_offset: f32 align(4),
+
+    /// Extra image-row range to draw while smooth scrolling.
+    image_scroll_offset: [2]u16 align(4),
+
     /// Bit mask defining which directions to
</file context>

Comment thread src/Surface.zig

const cell_height_float: f64 = @floatFromInt(self.size.cell.height);
if (fractional_offset > 0 and cell_height_float > 0) {
const pixel_offset = @trunc(-fractional_offset * cell_height_float);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not truncate the computed pixel offset here; truncation removes sub-pixel smooth-scroll movement and causes visible stair-stepping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/Surface.zig, line 3526:

<comment>Do not truncate the computed pixel offset here; truncation removes sub-pixel smooth-scroll movement and causes visible stair-stepping.</comment>

<file context>
@@ -3491,6 +3492,53 @@ const ScrollAmount = struct {
+
+        const cell_height_float: f64 = @floatFromInt(self.size.cell.height);
+        if (fractional_offset > 0 and cell_height_float > 0) {
+            const pixel_offset = @trunc(-fractional_offset * cell_height_float);
+            self.renderer_state.smooth_scroll_offset = @floatCast(pixel_offset);
+
</file context>
Suggested change
const pixel_offset = @trunc(-fractional_offset * cell_height_float);
const pixel_offset = -fractional_offset * cell_height_float;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant