Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
matrix_render_loop.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131(*****************************************************************************) (* *) (* SPDX-License-Identifier: MIT *) (* Copyright (c) 2025 Nomadic Labs <contact@nomadic-labs.com> *) (* *) (*****************************************************************************) [@@@warning "-69"] type t = { config : Matrix_config.t; buffer : Matrix_buffer.t; writer : Matrix_ansi_writer.t; write : string -> unit; shutdown_flag : bool Atomic.t; mutable render_domain : unit Domain.t option; mutable last_frame_time : float; mutable frame_count : int; mutable loop_count : int; mutable fps_start_time : float; current_fps : float Atomic.t; (* Actual renders per second *) loop_fps : float Atomic.t; (* Loop iterations per second (cap) *) } let do_render t = (* Reset style to ensure consistency. Close any active OSC 8 hyperlink before SGR reset. *) t.write "\027]8;;\027\\\027[0m" ; Matrix_ansi_writer.reset t.writer ; (* Compute diff atomically - holds lock during read to prevent torn reads. This also swaps the buffers and clears the dirty flag while holding the lock to prevent race conditions with new writes. *) let changes = Matrix_diff.compute_atomic t.buffer in (* Generate ANSI output *) let ansi = Matrix_ansi_writer.render t.writer changes in (* Write to output sink *) if String.length ansi > 0 then t.write ansi ; (* Update frame timing *) t.last_frame_time <- Unix.gettimeofday () ; t.frame_count <- t.frame_count + 1 (* Render domain main loop *) let render_loop_fn t = let frame_time_s = t.config.frame_time_ms /. 1000.0 in while not (Atomic.get t.shutdown_flag) do let frame_start = Unix.gettimeofday () in (* Count loop iteration *) t.loop_count <- t.loop_count + 1 ; (* Only render if buffer is dirty *) if Matrix_buffer.is_dirty t.buffer then do_render t ; (* Update loop FPS calculation every second *) let now = Unix.gettimeofday () in let elapsed_since_start = now -. t.fps_start_time in if elapsed_since_start >= 1.0 then begin Atomic.set t.loop_fps (float_of_int t.loop_count /. elapsed_since_start) ; Atomic.set t.current_fps (float_of_int t.frame_count /. elapsed_since_start) ; t.loop_count <- 0 ; t.frame_count <- 0 ; t.fps_start_time <- now end ; (* Sleep to maintain frame rate *) let elapsed = Unix.gettimeofday () -. frame_start in let sleep_time = frame_time_s -. elapsed in if sleep_time > 0.0 then (Thread.delay [@allow_forbidden "render loop runs in dedicated thread"]) sleep_time done let create ~config ~buffer ~writer ~write = let now = Unix.gettimeofday () in { config; buffer; writer; write; shutdown_flag = Atomic.make false; render_domain = None; last_frame_time = now; frame_count = 0; loop_count = 0; fps_start_time = now; current_fps = Atomic.make 0.0; loop_fps = Atomic.make 0.0; } (* Start the render domain *) let start t = if Option.is_none t.render_domain then begin let domain = Domain.spawn (fun () -> render_loop_fn t) in t.render_domain <- Some domain end (* Legacy API for compatibility - now just marks dirty *) let request_frame _t = () let frame_pending t = Matrix_buffer.is_dirty t.buffer (* Synchronous render - for use before domain starts or after shutdown *) let render_if_needed t = if Atomic.get t.shutdown_flag then false else if not (Matrix_buffer.is_dirty t.buffer) then false else begin do_render t ; true end let force_render t = if not (Atomic.get t.shutdown_flag) then do_render t let shutdown t = Atomic.set t.shutdown_flag true ; match t.render_domain with | Some domain -> Domain.join domain ; t.render_domain <- None | None -> () let current_fps t = Atomic.get t.current_fps let loop_fps t = Atomic.get t.loop_fps let is_running t = Option.is_some t.render_domain