51f41728
main: wire blink state machine into the render loop
a73x 2026-04-19 07:10
Commit message
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -338,13 +338,22 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 338 | var resize_pending = false; | 338 | var resize_pending = false; |
| 339 | var scale_pending = false; | 339 | var scale_pending = false; |
| 340 | var consecutive_vk_timeouts: u32 = 0; | 340 | var consecutive_vk_timeouts: u32 = 0; |
| 341 | var blink_state: BlinkState = .{}; | ||
| 342 | var previous_has_focus: bool = keyboard.has_focus; | ||
| 341 | 343 | ||
| 342 | while (!window.should_close and p.isChildAlive()) { | 344 | while (!window.should_close and p.isChildAlive()) { |
| 343 | const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs()); | 345 | const repeat_timeout_ms = remainingRepeatTimeoutMs(keyboard.nextRepeatDeadlineNs()); |
| 344 | // Blink deadline wired up in Task 4; null for now. | 346 | const blink_timeout_ms = remainingRepeatTimeoutMs(blink_state.next_deadline_ns); |
| 345 | const timeout = computePollTimeoutMs(repeat_timeout_ms, null, render_pending); | 347 | const timeout = computePollTimeoutMs(repeat_timeout_ms, blink_timeout_ms, render_pending); |
| 346 | try frame_loop.waitForWork(&pollfds_extra, timeout); | 348 | try frame_loop.waitForWork(&pollfds_extra, timeout); |
| 347 | 349 | ||
| 350 | // Phase-flip first so a flipped-this-tick deadline marks render_pending | ||
| 351 | // before the `if (!render_pending) continue;` bail below. | ||
| 352 | const tick = tickBlinkPhase(blink_state, std.time.nanoTimestamp()); | ||
| 353 | blink_state = tick.state; | ||
| 354 | var cursor_rebuild_from_blink = tick.flipped; | ||
| 355 | if (tick.flipped) render_pending = true; | ||
| 356 | |||
| 348 | // PTY output | 357 | // PTY output |
| 349 | if (pollfds_extra[0].revents & std.posix.POLL.IN != 0) { | 358 | if (pollfds_extra[0].revents & std.posix.POLL.IN != 0) { |
| 350 | while (true) { | 359 | while (true) { |
| @@ -495,6 +504,33 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 495 | try term.snapshot(); | 504 | try term.snapshot(); |
| 496 | frame_timing.snapshot_us = usFromTimer(§ion_timer); | 505 | frame_timing.snapshot_us = usFromTimer(§ion_timer); |
| 497 | 506 | ||
| 507 | // Recompute blink arm/disarm from current state. Edge-driven updates | ||
| 508 | // are deliberately avoided — `want_blink` is a pure function of the | ||
| 509 | // state we see right now, so no transition can be "missed". | ||
| 510 | const current_cursor = term.render_state.cursor; | ||
| 511 | const viewport_present = current_cursor.viewport != null; | ||
| 512 | const want_blink = current_cursor.visible | ||
| 513 | and viewport_present | ||
| 514 | and current_cursor.blinking | ||
| 515 | and keyboard.has_focus; | ||
| 516 | |||
| 517 | const cursor_identity_changed = | ||
| 518 | !std.meta.eql(current_cursor.viewport, previous_cursor.viewport) | ||
| 519 | or current_cursor.visual_style != previous_cursor.visual_style | ||
| 520 | or current_cursor.visible != previous_cursor.visible | ||
| 521 | or current_cursor.blinking != previous_cursor.blinking; | ||
| 522 | const focus_regained = keyboard.has_focus and !previous_has_focus; | ||
| 523 | |||
| 524 | const reconf = reconfigureBlink( | ||
| 525 | blink_state, | ||
| 526 | want_blink, | ||
| 527 | cursor_identity_changed, | ||
| 528 | focus_regained, | ||
| 529 | std.time.nanoTimestamp(), | ||
| 530 | ); | ||
| 531 | blink_state = reconf.state; | ||
| 532 | if (reconf.cursor_rebuild) cursor_rebuild_from_blink = true; | ||
| 533 | |||
| 498 | section_timer = std.time.Timer.start() catch unreachable; | 534 | section_timer = std.time.Timer.start() catch unreachable; |
| 499 | const default_bg = term.backgroundColor(); | 535 | const default_bg = term.backgroundColor(); |
| 500 | const bg_uv = atlas.cursorUV(); | 536 | const bg_uv = atlas.cursorUV(); |
| @@ -517,6 +553,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 517 | }, | 553 | }, |
| 518 | }, | 554 | }, |
| 519 | ); | 555 | ); |
| 556 | const cursor_rebuild_effective = refresh_plan.cursor_rebuild or cursor_rebuild_from_blink; | ||
| 520 | 557 | ||
| 521 | var rows_rebuilt: usize = 0; | 558 | var rows_rebuilt: usize = 0; |
| 522 | var row_idx: usize = 0; | 559 | var row_idx: usize = 0; |
| @@ -551,11 +588,19 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 551 | } | 588 | } |
| 552 | 589 | ||
| 553 | var cursor_rebuilt = false; | 590 | var cursor_rebuilt = false; |
| 554 | if (refresh_plan.cursor_rebuild) { | 591 | if (cursor_rebuild_effective) { |
| 555 | var cursor_instances_buf: [1]renderer.Instance = undefined; | 592 | var cursor_instances_buf: [1]renderer.Instance = undefined; |
| 556 | var cursor_instances: []const renderer.Instance = &.{}; | 593 | var cursor_instances: []const renderer.Instance = &.{}; |
| 557 | if (term.render_state.cursor.viewport) |cursor| { | 594 | const draw_cursor = shouldDrawCursor( |
| 558 | const shape: renderer.CursorShape = switch (term.render_state.cursor.visual_style) { | 595 | current_cursor.visible, |
| 596 | current_cursor.viewport != null, | ||
| 597 | keyboard.has_focus, | ||
| 598 | current_cursor.blinking, | ||
| 599 | blink_state.blink_on, | ||
| 600 | ); | ||
| 601 | if (draw_cursor) { | ||
| 602 | const cursor = current_cursor.viewport.?; // guaranteed by shouldDrawCursor | ||
| 603 | const shape: renderer.CursorShape = switch (current_cursor.visual_style) { | ||
| 559 | .block, .block_hollow => .block, | 604 | .block, .block_hollow => .block, |
| 560 | .underline => .underline, | 605 | .underline => .underline, |
| 561 | .bar => .bar, | 606 | .bar => .bar, |
| @@ -715,6 +760,7 @@ fn runTerminal(alloc: std.mem.Allocator) !void { | |||
| 715 | frame_timing.present_us = submit_timing.present_us; | 760 | frame_timing.present_us = submit_timing.present_us; |
| 716 | 761 | ||
| 717 | frame_ring.push(frame_timing); | 762 | frame_ring.push(frame_timing); |
| 763 | previous_has_focus = keyboard.has_focus; | ||
| 718 | 764 | ||
| 719 | clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); | 765 | clearConsumedDirtyFlags(&term.render_state.dirty, dirty_rows, refresh_plan); |
| 720 | if (!bench_unthrottled) frame_loop.commitRender() catch {}; | 766 | if (!bench_unthrottled) frame_loop.commitRender() catch {}; |